gadget/gadget-code/frontend/src/components/ErrorBoundary.tsx
Rob Colbert c14c3a235a fix: ACE editor integration crash — React 19 compat, Vite ?url pattern, Error Boundary
Root causes:
- react-ace v12 doesn't support React 19 (project uses ^19.2.5)
- Dynamic import(`ace-builds/src-noconflict/mode-${language}`) broken with Vite
  (template-literal dynamic imports can't be statically analyzed)
- No React Error Boundaries — ACE render crash whitescreens entire app
- ace-builds/react-ace duplicated in backend package.json

Fixes:
1. Upgrade react-ace ^12.0.0 → ^14.0.1 (React 19 support)
2. Upgrade ace-builds ^1.36.0 → ^1.43.6
3. Remove ACE deps from backend package.json (not used by Express)
4. Replace broken dynamic imports with Vite ?url + ace.config.setModuleUrl()
   pattern (canonical Vite solution per ace#4597)
5. Add ErrorBoundary component wrapping EditorPanel
6. Add vite.d.ts type declarations for ?url/?raw/?worker imports
7. Fix worker-typescript import (doesn't exist — TS uses worker-javascript)
8. Register 24 language modes, 4 workers, 1 theme, 2 extensions

Verified: TypeScript clean, production build passes, heartbeat worker intact.
2026-05-12 21:36:22 -04:00

57 lines
2.0 KiB
TypeScript

import { Component, type ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
/**
* React Error Boundary — catches render errors from child components
* and displays a fallback UI instead of crashing the entire app.
*
* Use this to wrap components that may throw during render (e.g., ACE editor).
*/
export default class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('[ErrorBoundary] caught render error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
if (this.props.fallback) return this.props.fallback;
return (
<div className="flex-1 flex items-center justify-center bg-bg-secondary">
<div className="text-center max-w-md">
<svg className="w-16 h-16 mx-auto mb-4 text-red-500 opacity-75" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
<h2 className="text-lg font-medium text-text-primary mb-2">Something went wrong</h2>
<p className="text-sm text-text-muted mb-4">{this.state.error?.message || 'An unexpected error occurred.'}</p>
<button
onClick={() => this.setState({ hasError: false, error: null })}
className="px-4 py-2 text-sm font-medium bg-brand text-white rounded hover:bg-brand/80 transition-colors"
>
Try Again
</button>
</div>
</div>
);
}
return this.props.children;
}
}