Эх сурвалжийг харах

more resilient (#85)

* more resilient

* more resilient
Chris Tate 5 сар өмнө
parent
commit
f435643817

+ 25 - 8
apps/web/lib/render/registry.tsx

@@ -823,7 +823,12 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
       const [localValue, setLocalValue] = useState<string>("");
       const value = props.statePath ? (boundValue ?? "") : localValue;
       const setValue = props.statePath ? setBoundValue! : setLocalValue;
-      const options = props.options ?? [];
+      const rawOptions = props.options ?? [];
+      // Coerce options to strings – AI may produce objects/numbers instead of
+      // plain strings which would cause duplicate `[object Object]` keys.
+      const options = rawOptions.map((opt) =>
+        typeof opt === "string" ? opt : String(opt ?? ""),
+      );
 
       return (
         <div className="space-y-2">
@@ -839,8 +844,11 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
               <SelectValue placeholder={props.placeholder ?? "Select..."} />
             </SelectTrigger>
             <SelectContent>
-              {options.map((opt) => (
-                <SelectItem key={opt} value={opt}>
+              {options.map((opt, idx) => (
+                <SelectItem
+                  key={`${idx}-${opt}`}
+                  value={opt || `option-${idx}`}
+                >
                   {opt}
                 </SelectItem>
               ))}
@@ -876,7 +884,10 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
     },
 
     Radio: ({ props, emit }) => {
-      const options = props.options ?? [];
+      const rawOptions = props.options ?? [];
+      const options = rawOptions.map((opt) =>
+        typeof opt === "string" ? opt : String(opt ?? ""),
+      );
       const [boundValue, setBoundValue] = props.statePath
         ? useStateBinding<string>(props.statePath) // eslint-disable-line react-hooks/rules-of-hooks
         : [undefined, undefined];
@@ -894,11 +905,17 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
               emit?.("change");
             }}
           >
-            {options.map((opt) => (
-              <div key={opt} className="flex items-center space-x-2">
-                <RadioGroupItem value={opt} id={`${props.name}-${opt}`} />
+            {options.map((opt, idx) => (
+              <div
+                key={`${idx}-${opt}`}
+                className="flex items-center space-x-2"
+              >
+                <RadioGroupItem
+                  value={opt || `option-${idx}`}
+                  id={`${props.name}-${idx}-${opt}`}
+                />
                 <Label
-                  htmlFor={`${props.name}-${opt}`}
+                  htmlFor={`${props.name}-${idx}-${opt}`}
                   className="cursor-pointer"
                 >
                   {opt}

+ 51 - 3
packages/react-native/src/renderer.tsx

@@ -1,5 +1,6 @@
 import React, {
   type ComponentType,
+  type ErrorInfo,
   type ReactNode,
   useCallback,
   useMemo,
@@ -81,6 +82,51 @@ export interface RendererProps {
   fallback?: ComponentRenderer;
 }
 
+// ---------------------------------------------------------------------------
+// ElementErrorBoundary – catches rendering errors in individual elements so
+// a single bad component never crashes the whole page.
+// ---------------------------------------------------------------------------
+
+interface ElementErrorBoundaryProps {
+  elementType: string;
+  children: ReactNode;
+}
+
+interface ElementErrorBoundaryState {
+  hasError: boolean;
+}
+
+class ElementErrorBoundary extends React.Component<
+  ElementErrorBoundaryProps,
+  ElementErrorBoundaryState
+> {
+  constructor(props: ElementErrorBoundaryProps) {
+    super(props);
+    this.state = { hasError: false };
+  }
+
+  static getDerivedStateFromError(): ElementErrorBoundaryState {
+    return { hasError: true };
+  }
+
+  componentDidCatch(error: Error, info: ErrorInfo) {
+    console.error(
+      `[json-render] Rendering error in <${this.props.elementType}>:`,
+      error,
+      info.componentStack,
+    );
+  }
+
+  render() {
+    if (this.state.hasError) {
+      // Render nothing – the element silently disappears rather than
+      // crashing the entire application.
+      return null;
+    }
+    return this.props.children;
+  }
+}
+
 /**
  * Element renderer component
  */
@@ -214,9 +260,11 @@ function ElementRenderer({
   );
 
   return (
-    <Component element={resolvedElement} emit={emit} loading={loading}>
-      {children}
-    </Component>
+    <ElementErrorBoundary elementType={resolvedElement.type}>
+      <Component element={resolvedElement} emit={emit} loading={loading}>
+        {children}
+      </Component>
+    </ElementErrorBoundary>
   );
 }
 

+ 51 - 3
packages/react/src/renderer.tsx

@@ -2,6 +2,7 @@
 
 import React, {
   type ComponentType,
+  type ErrorInfo,
   type ReactNode,
   useCallback,
   useMemo,
@@ -77,6 +78,51 @@ export interface RendererProps {
   fallback?: ComponentRenderer;
 }
 
+// ---------------------------------------------------------------------------
+// ElementErrorBoundary – catches rendering errors in individual elements so
+// a single bad component never crashes the whole page.
+// ---------------------------------------------------------------------------
+
+interface ElementErrorBoundaryProps {
+  elementType: string;
+  children: ReactNode;
+}
+
+interface ElementErrorBoundaryState {
+  hasError: boolean;
+}
+
+class ElementErrorBoundary extends React.Component<
+  ElementErrorBoundaryProps,
+  ElementErrorBoundaryState
+> {
+  constructor(props: ElementErrorBoundaryProps) {
+    super(props);
+    this.state = { hasError: false };
+  }
+
+  static getDerivedStateFromError(): ElementErrorBoundaryState {
+    return { hasError: true };
+  }
+
+  componentDidCatch(error: Error, info: ErrorInfo) {
+    console.error(
+      `[json-render] Rendering error in <${this.props.elementType}>:`,
+      error,
+      info.componentStack,
+    );
+  }
+
+  render() {
+    if (this.state.hasError) {
+      // Render nothing – the element silently disappears rather than
+      // crashing the entire application.
+      return null;
+    }
+    return this.props.children;
+  }
+}
+
 /**
  * Element renderer component
  */
@@ -210,9 +256,11 @@ function ElementRenderer({
   );
 
   return (
-    <Component element={resolvedElement} emit={emit} loading={loading}>
-      {children}
-    </Component>
+    <ElementErrorBoundary elementType={resolvedElement.type}>
+      <Component element={resolvedElement} emit={emit} loading={loading}>
+        {children}
+      </Component>
+    </ElementErrorBoundary>
   );
 }
 

+ 48 - 1
packages/remotion/src/components/Renderer.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import React, { type ErrorInfo, type ReactNode } from "react";
 import { AbsoluteFill, Sequence } from "remotion";
 import type { TimelineSpec, ComponentRegistry, Clip } from "./types";
 
@@ -33,6 +34,50 @@ export const standardComponents: ComponentRegistry = {
   VideoClip,
 };
 
+// ---------------------------------------------------------------------------
+// ClipErrorBoundary – catches rendering errors in individual clips so
+// a single bad clip never crashes the entire composition.
+// ---------------------------------------------------------------------------
+
+interface ClipErrorBoundaryProps {
+  clipId: string;
+  component: string;
+  children: ReactNode;
+}
+
+interface ClipErrorBoundaryState {
+  hasError: boolean;
+}
+
+class ClipErrorBoundary extends React.Component<
+  ClipErrorBoundaryProps,
+  ClipErrorBoundaryState
+> {
+  constructor(props: ClipErrorBoundaryProps) {
+    super(props);
+    this.state = { hasError: false };
+  }
+
+  static getDerivedStateFromError(): ClipErrorBoundaryState {
+    return { hasError: true };
+  }
+
+  componentDidCatch(error: Error, info: React.ErrorInfo) {
+    console.error(
+      `[json-render/remotion] Rendering error in clip "${this.props.clipId}" (<${this.props.component}>):`,
+      error,
+      info.componentStack,
+    );
+  }
+
+  render() {
+    if (this.state.hasError) {
+      return null;
+    }
+    return this.props.children;
+  }
+}
+
 interface RendererProps {
   /** The timeline spec to render */
   spec: TimelineSpec;
@@ -102,7 +147,9 @@ export function Renderer({
         from={clip.from}
         durationInFrames={clip.durationInFrames}
       >
-        <Component clip={clip} />
+        <ClipErrorBoundary clipId={clip.id} component={clip.component}>
+          <Component clip={clip} />
+        </ClipErrorBoundary>
       </Sequence>
     );
   };