Просмотр исходного кода

fix(runtime-vapor): restore component context after setup errors

daiwei 1 месяц назад
Родитель
Сommit
e9866b96fe

+ 53 - 1
packages/runtime-vapor/__tests__/component.spec.ts

@@ -2,6 +2,7 @@ import {
   type EffectScope,
   ReactiveEffect,
   type Ref,
+  type SuspenseBoundary,
   inject,
   nextTick,
   onBeforeMount,
@@ -25,8 +26,14 @@ import {
   txt,
 } from '../src'
 import { compile, compileToVaporRender, makeRender } from './_utils'
-import type { VaporComponentInstance } from '../src/component'
+import { type VaporComponentInstance, currentInstance } from '../src/component'
+import { currentSlotOwner, setCurrentSlotOwner } from '../src/componentSlots'
 import { setElementText, setText } from '../src/dom/prop'
+import {
+  enableSuspense,
+  parentSuspense,
+  setParentSuspense,
+} from '../src/suspense'
 
 const define = makeRender()
 
@@ -778,6 +785,51 @@ describe('component', () => {
 
     expect(mountedSpy).toHaveBeenCalledTimes(0)
   })
+
+  it('should restore component context when child setup throws', () => {
+    enableSuspense()
+
+    const err = new Error('setup boom')
+    const owner = { type: {} } as VaporComponentInstance
+    const previousSuspense = { pendingId: 1 } as SuspenseBoundary
+    const activeSuspense = { pendingId: 2 } as SuspenseBoundary
+    let caught: unknown
+    let ownerAfterThrow: VaporComponentInstance | null = null
+    let suspenseAfterThrow: SuspenseBoundary | null = null
+
+    const { component: Child } = define({
+      setup() {
+        throw err
+      },
+    })
+
+    define({
+      setup() {
+        const instance = currentInstance as VaporComponentInstance
+        instance.suspense = activeSuspense
+
+        const prevOwner = setCurrentSlotOwner(owner)
+        const prevSuspense = setParentSuspense(previousSuspense)
+        try {
+          createComponent(Child)
+        } catch (e) {
+          caught = e
+        }
+        ownerAfterThrow = currentSlotOwner
+        suspenseAfterThrow = parentSuspense
+        setCurrentSlotOwner(prevOwner)
+        setParentSuspense(prevSuspense)
+        return []
+      },
+    }).render()
+
+    expect(
+      `Unhandled error during execution of setup function`,
+    ).toHaveBeenWarned()
+    expect(caught).toBe(err)
+    expect(ownerAfterThrow).toBe(owner)
+    expect(suspenseAfterThrow).toBe(previousSuspense)
+  })
 })
 
 function getEffectsCount(scope: EffectScope): number {

+ 54 - 45
packages/runtime-vapor/src/component.ts

@@ -293,8 +293,9 @@ export function createComponent(
     resetInsertionState()
   }
 
+  let prevSuspense: SuspenseBoundary | null = null
+  let hasParentSuspense = false
   try {
-    let prevSuspense: SuspenseBoundary | null = null
     if (
       __FEATURE_SUSPENSE__ &&
       isSuspenseEnabled &&
@@ -302,6 +303,7 @@ export function createComponent(
       currentInstance.suspense
     ) {
       prevSuspense = setParentSuspense(currentInstance.suspense)
+      hasParentSuspense = true
     }
 
     if (
@@ -402,52 +404,56 @@ export function createComponent(
 
     // reset currentSlotOwner to null to avoid affecting the child components
     const prevSlotOwner = setCurrentSlotOwner(null)
+    let hasWarningContext = false
+    let hasInitMeasure = false
+    try {
+      // HMR
+      if (__DEV__) {
+        registerHMR(instance)
+        instance.isSingleRoot = isSingleRoot
+        instance.hmrRerender = hmrRerender.bind(null, instance)
+        instance.hmrReload = hmrReload.bind(null, instance)
+
+        pushWarningContext(instance)
+        hasWarningContext = true
+        startMeasure(instance, `init`)
+        hasInitMeasure = true
+
+        // cache normalized options for dev only emit check
+        instance.propsOptions = normalizePropsOptions(component)
+        instance.emitsOptions = normalizeEmitsOptions(component)
+      }
 
-    // HMR
-    if (__DEV__) {
-      registerHMR(instance)
-      instance.isSingleRoot = isSingleRoot
-      instance.hmrRerender = hmrRerender.bind(null, instance)
-      instance.hmrReload = hmrReload.bind(null, instance)
-
-      pushWarningContext(instance)
-      startMeasure(instance, `init`)
-
-      // cache normalized options for dev only emit check
-      instance.propsOptions = normalizePropsOptions(component)
-      instance.emitsOptions = normalizeEmitsOptions(component)
-    }
-
-    // hydrating async component
-    if (
-      isHydrating &&
-      isAsyncWrapper(instance) &&
-      component.__asyncHydrate &&
-      !component.__asyncResolved
-    ) {
-      component.__asyncHydrate(currentHydrationNode as Element, instance, () =>
-        setupComponent(instance, component),
-      )
-    } else {
-      setupComponent(instance, component)
-    }
-
-    if (__DEV__) {
-      popWarningContext()
-      endMeasure(instance, 'init')
-    }
-
-    if (
-      __FEATURE_SUSPENSE__ &&
-      isSuspenseEnabled &&
-      currentInstance &&
-      currentInstance.suspense
-    ) {
-      setParentSuspense(prevSuspense)
+      // hydrating async component
+      if (
+        isHydrating &&
+        isAsyncWrapper(instance) &&
+        component.__asyncHydrate &&
+        !component.__asyncResolved
+      ) {
+        component.__asyncHydrate(
+          currentHydrationNode as Element,
+          instance,
+          () => setupComponent(instance, component),
+        )
+      } else {
+        setupComponent(instance, component)
+      }
+    } finally {
+      if (__DEV__) {
+        if (hasWarningContext) {
+          popWarningContext()
+        }
+        if (hasInitMeasure) {
+          endMeasure(instance, 'init')
+        }
+      }
+      setCurrentSlotOwner(prevSlotOwner)
+      if (__FEATURE_SUSPENSE__ && isSuspenseEnabled && hasParentSuspense) {
+        setParentSuspense(prevSuspense)
+        hasParentSuspense = false
+      }
     }
-
-    // restore currentSlotOwner to previous value after setupFn is called
-    setCurrentSlotOwner(prevSlotOwner)
     onScopeDispose(() => unmountComponent(instance), true)
 
     if (!managedMount && (_insertionParent || isHydrating)) {
@@ -480,6 +486,9 @@ export function createComponent(
 
     return instance
   } finally {
+    if (hasParentSuspense) {
+      setParentSuspense(prevSuspense)
+    }
     if (isHydrating && !deferHydrationBoundary) {
       // Boundary cleanup still needs the component-local cursor. Only after
       // that do we restore the outer cursor's resume point.