From 1df5e3b300cded40ec9a252191a41208c4b6487f Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 2 Dec 2025 09:28:44 +0800 Subject: [PATCH 1/7] feat: vdom Suspense render vapor components --- packages/runtime-core/src/apiCreateApp.ts | 4 +- packages/runtime-core/src/component.ts | 13 + .../runtime-core/src/components/Suspense.ts | 74 +++--- packages/runtime-core/src/hydration.ts | 1 + packages/runtime-core/src/renderer.ts | 1 + .../__tests__/components/Suspense.spec.ts | 244 ++++++++++++++++++ packages/runtime-vapor/src/component.ts | 203 ++++++++++----- .../runtime-vapor/src/components/Suspense.ts | 20 ++ packages/runtime-vapor/src/vdomInterop.ts | 26 +- 9 files changed, 483 insertions(+), 103 deletions(-) create mode 100644 packages/runtime-vapor/__tests__/components/Suspense.spec.ts create mode 100644 packages/runtime-vapor/src/components/Suspense.ts diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index 2aca5f3ee1e..9bd13d600bc 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -27,7 +27,7 @@ import { warn } from './warning' import type { VNode } from './vnode' import { devtoolsInitApp, devtoolsUnmountApp } from './devtools' import { NO, extend, hasOwn, isFunction, isObject } from '@vue/shared' -import { type TransitionHooks, version } from '.' +import { type SuspenseBoundary, type TransitionHooks, version } from '.' import { installAppCompatProperties } from './compat/global' import type { NormalizedPropsOptions } from './componentProps' import type { ObjectEmitsOptions } from './componentEmits' @@ -187,6 +187,7 @@ export interface VaporInteropInterface { container: any, anchor: any, parentComponent: ComponentInternalInstance | null, + parentSuspense: SuspenseBoundary | null, ): GenericComponentInstance // VaporComponentInstance update(n1: VNode, n2: VNode, shouldUpdate: boolean): void unmount(vnode: VNode, doRemove?: boolean): void @@ -198,6 +199,7 @@ export interface VaporInteropInterface { container: any, anchor: any, parentComponent: ComponentInternalInstance | null, + parentSuspense: SuspenseBoundary | null, ): Node hydrateSlot(vnode: VNode, node: any): Node activate( diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index b49cd685425..88faf6306a9 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -461,6 +461,19 @@ export interface GenericComponentInstance { * @internal */ suspense: SuspenseBoundary | null + /** + * suspense pending batch id + * @internal + */ + suspenseId: number + /** + * @internal + */ + asyncDep: Promise | null + /** + * @internal + */ + asyncResolved: boolean /** * `updateTeleportCssVars` * For updating css vars on contained teleports diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts index 980ddc73c37..130064c85d9 100644 --- a/packages/runtime-core/src/components/Suspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -706,7 +706,7 @@ function createSuspenseBoundary( if (isInPendingSuspense) { suspense.deps++ } - const hydratedEl = instance.vnode.el + const hydratedEl = instance.vapor ? null : instance.vnode.el instance .asyncDep!.catch(err => { handleError(err, instance, ErrorCodes.SETUP_FUNCTION) @@ -723,39 +723,45 @@ function createSuspenseBoundary( } // retry from this component instance.asyncResolved = true - const { vnode } = instance - if (__DEV__) { - pushWarningContext(vnode) - } - handleSetupResult(instance, asyncSetupResult, false) - if (hydratedEl) { - // vnode may have been replaced if an update happened before the - // async dep is resolved. - vnode.el = hydratedEl - } - const placeholder = !hydratedEl && instance.subTree.el - setupRenderEffect( - instance, - vnode, - // component may have been moved before resolve. - // if this is not a hydration, instance.subTree will be the comment - // placeholder. - parentNode(hydratedEl || instance.subTree.el!)!, - // anchor will not be used if this is hydration, so only need to - // consider the comment placeholder case. - hydratedEl ? null : next(instance.subTree), - suspense, - namespace, - optimized, - ) - if (placeholder) { - // clean up placeholder reference - vnode.placeholder = null - remove(placeholder) - } - updateHOCHostEl(instance, vnode.el) - if (__DEV__) { - popWarningContext() + // vapor component + if (instance.vapor) { + // @ts-expect-error + setupRenderEffect(asyncSetupResult) + } else { + const { vnode } = instance + if (__DEV__) { + pushWarningContext(vnode) + } + handleSetupResult(instance, asyncSetupResult, false) + if (hydratedEl) { + // vnode may have been replaced if an update happened before the + // async dep is resolved. + vnode.el = hydratedEl + } + const placeholder = !hydratedEl && instance.subTree.el + setupRenderEffect( + instance, + vnode, + // component may have been moved before resolve. + // if this is not a hydration, instance.subTree will be the comment + // placeholder. + parentNode(hydratedEl || instance.subTree.el!)!, + // anchor will not be used if this is hydration, so only need to + // consider the comment placeholder case. + hydratedEl ? null : next(instance.subTree), + suspense, + namespace, + optimized, + ) + if (placeholder) { + // clean up placeholder reference + vnode.placeholder = null + remove(placeholder) + } + updateHOCHostEl(instance, vnode.el) + if (__DEV__) { + popWarningContext() + } } // only decrease deps count if suspense is not already resolved if (isInPendingSuspense && --suspense.deps === 0) { diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts index 32e260c7594..5188c1f1dbc 100644 --- a/packages/runtime-core/src/hydration.ts +++ b/packages/runtime-core/src/hydration.ts @@ -318,6 +318,7 @@ export function createHydrationFunctions( container, null, parentComponent, + parentSuspense, ) } else { mountComponent( diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 98a8c350375..8ab94485e3d 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -1186,6 +1186,7 @@ function baseCreateRenderer( container, anchor, parentComponent, + parentSuspense, ) } } else { diff --git a/packages/runtime-vapor/__tests__/components/Suspense.spec.ts b/packages/runtime-vapor/__tests__/components/Suspense.spec.ts new file mode 100644 index 00000000000..b13377f773f --- /dev/null +++ b/packages/runtime-vapor/__tests__/components/Suspense.spec.ts @@ -0,0 +1,244 @@ +import { nextTick, reactive } from 'vue' +import { compile, runtimeDom, runtimeVapor } from '../_utils' + +describe.todo('VaporSuspense', () => {}) + +describe('vdom interop', () => { + async function testSuspense( + code: string, + components: Record = {}, + data: any = {}, + { vapor = false } = {}, + ) { + const clientComponents: any = {} + for (const key in components) { + const comp = components[key] + let code = comp.code + const isVaporComp = !!comp.vapor + clientComponents[key] = compile(code, data, clientComponents, { + vapor: isVaporComp, + }) + } + + const clientComp = compile(code, data, clientComponents, { + vapor, + }) + + const app = (vapor ? runtimeVapor.createVaporApp : runtimeDom.createApp)( + clientComp, + ) + app.use(runtimeVapor.vaporInteropPlugin) + + const container = document.createElement('div') + document.body.appendChild(container) + app.mount(container) + return { container } + } + + function withAsyncScript(code: string) { + return { + code: ` + + ${code} + `, + vapor: true, + } + } + + test('vdom suspense: render vapor components', async () => { + const data = { deps: [] } + const { container } = await testSuspense( + ` + `, + { + VaporChild: withAsyncScript(``), + }, + data, + ) + + expect(container.innerHTML).toBe(`fallback`) + expect(data.deps.length).toBe(1) + await Promise.all(data.deps) + await nextTick() + expect(container.innerHTML).toBe(`
hi
`) + }) + + test('vdom suspense: nested async vapor components', async () => { + const data = { deps: [] } + const { container } = await testSuspense( + ` + `, + { + AsyncOuter: withAsyncScript( + ``, + ), + AsyncInner: withAsyncScript(``), + }, + data, + ) + + expect(container.innerHTML).toBe(`fallback`) + + await data.deps[0] + await nextTick() + expect(container.innerHTML).toBe(`fallback`) + + await Promise.all(data.deps) + await nextTick() + expect(container.innerHTML).toBe(`
inner
`) + }) + + test('vdom suspense: content update before suspense resolve', async () => { + const data = reactive({ msg: 'foo', deps: [] }) + const { container } = await testSuspense( + ` + `, + { + VaporChild: withAsyncScript( + ``, + ), + }, + data, + ) + + expect(container.innerHTML).toBe(`fallback foo`) + + data.msg = 'bar' + await nextTick() + expect(container.innerHTML).toBe(`fallback bar`) + + await Promise.all(data.deps) + await nextTick() + expect(container.innerHTML).toBe(`
bar
`) + }) + + test('vdom suspense: unmount before suspense resolve', async () => { + const data = reactive({ show: true, deps: [] }) + const { container } = await testSuspense( + ` + `, + { + VaporChild: withAsyncScript(``), + }, + data, + ) + + expect(container.innerHTML).toBe(`fallback`) + + data.show = false + await nextTick() + expect(container.innerHTML).toBe(``) + + await Promise.all(data.deps) + await nextTick() + expect(container.innerHTML).toBe(``) + }) + + test('vdom suspense: unmount suspense after resolve', async () => { + const data = reactive({ show: true, deps: [] }) + const { container } = await testSuspense( + ` + `, + { + VaporChild: withAsyncScript(``), + }, + data, + ) + + expect(container.innerHTML).toBe(`fallback`) + + await Promise.all(data.deps) + await nextTick() + expect(container.innerHTML).toBe(`
child
`) + + data.show = false + await nextTick() + expect(container.innerHTML).toBe(``) + }) + + test('vdom suspense: unmount suspense before resolve', async () => { + const data = reactive({ show: true, deps: [] }) + const { container } = await testSuspense( + ` + `, + { + VaporChild: withAsyncScript(``), + }, + data, + ) + + expect(container.innerHTML).toBe(`fallback`) + + data.show = false + await nextTick() + expect(container.innerHTML).toBe(``) + + await Promise.all(data.deps) + await nextTick() + expect(container.innerHTML).toBe(``) + }) +}) diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index bc72fcf7cde..d00e70bff32 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -20,6 +20,7 @@ import { getFunctionalFallthrough, isAsyncWrapper, isKeepAlive, + markAsyncBoundary, nextUid, popWarningContext, pushWarningContext, @@ -53,6 +54,7 @@ import { invokeArrayFns, isArray, isFunction, + isPromise, isString, } from '@vue/shared' import { @@ -104,6 +106,7 @@ import { } from './insertionState' import { DynamicFragment, isFragment } from './fragment' import type { VaporElement } from './apiDefineVaporCustomElement' +import { parentSuspense, setParentSuspense } from './components/Suspense' export { currentInstance } from '@vue/runtime-dom' @@ -199,6 +202,11 @@ export function createComponent( const parentInstance = getParentInstance() + let prevSuspense: SuspenseBoundary | null = null + if (__FEATURE_SUSPENSE__ && parentInstance && parentInstance.suspense) { + prevSuspense = setParentSuspense(parentInstance.suspense) + } + if ( (isSingleRoot || // transition has attrs fallthrough @@ -337,6 +345,10 @@ export function createComponent( endMeasure(instance, 'init') } + if (__FEATURE_SUSPENSE__ && parentInstance && parentInstance.suspense) { + setParentSuspense(prevSuspense) + } + // restore currentSlotConsumer to previous value after setupFn is called setCurrentSlotConsumer(prevSlotConsumer) onScopeDispose(() => unmountComponent(instance), true) @@ -371,67 +383,35 @@ export function setupComponent( ]) || EMPTY_OBJ : EMPTY_OBJ - if (__DEV__ && !isBlock(setupResult)) { - if (isFunction(component)) { - warn(`Functional vapor component must return a block directly.`) - instance.block = [] - } else if (!component.render) { + const isAsyncSetup = isPromise(setupResult) + + if ((isAsyncSetup || instance.sp) && !isAsyncWrapper(instance)) { + // async setup / serverPrefetch, mark as async boundary for useId() + markAsyncBoundary(instance) + } + + if (isAsyncSetup) { + if (__FEATURE_SUSPENSE__) { + // async setup returned Promise. + // bail here and wait for re-entry. + instance.asyncDep = setupResult + if (__DEV__ && !instance.suspense) { + const name = getComponentName(component) ?? 'Anonymous' + warn( + `Component <${name}>: setup function returned a promise, but no ` + + ` boundary was found in the parent component tree. ` + + `A component with async setup() must be nested in a ` + + `in order to be rendered.`, + ) + } + } else if (__DEV__) { warn( - `Vapor component setup() returned non-block value, and has no render function.`, + `setup() returned a Promise, but the version of Vue you are using ` + + `does not support it yet.`, ) - instance.block = [] - } else { - instance.devtoolsRawSetupState = setupResult - // TODO make the proxy warn non-existent property access during dev - instance.setupState = proxyRefs(setupResult) - devRender(instance) } } else { - // component has a render function but no setup function - // (typically components with only a template and no state) - if (!setupFn && component.render) { - instance.block = callWithErrorHandling( - component.render, - instance, - ErrorCodes.RENDER_FUNCTION, - ) - } else { - // in prod result can only be block - instance.block = setupResult as Block - } - } - - // single root, inherit attrs - if ( - instance.hasFallthrough && - component.inheritAttrs !== false && - Object.keys(instance.attrs).length - ) { - const root = getRootElement( - instance.block, - // attach attrs to root dynamic fragments for applying during each update - frag => (frag.attrs = instance.attrs), - false, - ) - if (root) { - renderEffect(() => { - const attrs = - isFunction(component) && !isVaporTransition(component) - ? getFunctionalFallthrough(instance.attrs) - : instance.attrs - if (attrs) applyFallthroughProps(root, attrs) - }) - } else if ( - __DEV__ && - ((!instance.accessedAttrs && - isArray(instance.block) && - instance.block.length) || - // preventing attrs fallthrough on Teleport - // consistent with VDOM Teleport behavior - instance.block instanceof TeleportFragment) - ) { - warnExtraneousAttributes(instance.attrs) - } + handleSetupResult(setupResult, component, instance, setupFn) } setActiveSub(prevSub) @@ -530,6 +510,9 @@ export class VaporComponentInstance implements GenericComponentInstance { ids: [string, number, number] // for suspense suspense: SuspenseBoundary | null + suspenseId: number + asyncDep: Promise | null + asyncResolved: boolean // for HMR and vapor custom element // all render effects associated with this instance @@ -608,12 +591,13 @@ export class VaporComponentInstance implements GenericComponentInstance { this.emit = emit.bind(null, this) this.expose = expose.bind(null, this) this.refs = EMPTY_OBJ - this.emitted = - this.exposed = - this.exposeProxy = - this.propsDefaults = - this.suspense = - null + this.emitted = this.exposed = this.exposeProxy = this.propsDefaults = null + + // suspense related + this.suspense = parentSuspense + this.suspenseId = parentSuspense ? parentSuspense.pendingId : 0 + this.asyncDep = null + this.asyncResolved = false this.isMounted = this.isUnmounted = @@ -784,6 +768,29 @@ export function mountComponent( parent: ParentNode, anchor?: Node | null | 0, ): void { + if ( + __FEATURE_SUSPENSE__ && + instance.suspense && + instance.asyncDep && + !instance.asyncResolved + ) { + const component = instance.type + instance.suspense.registerDep( + instance as any, + (setupResult: any) => { + handleSetupResult( + setupResult, + component, + instance, + isFunction(component) ? component : component.setup, + ) + mountComponent(instance, parent, anchor) + }, + false, + ) + return + } + if (instance.shapeFlag! & ShapeFlags.COMPONENT_KEPT_ALIVE) { findParentKeepAlive(instance)!.activate(instance, parent, anchor) return @@ -918,3 +925,73 @@ export function getRootElement( function isVaporTransition(component: VaporComponent): boolean { return getComponentName(component) === 'VaporTransition' } + +function handleSetupResult( + setupResult: any, + component: VaporComponent, + instance: VaporComponentInstance, + setupFn: VaporSetupFn | undefined, +) { + if (__DEV__ && !isBlock(setupResult)) { + if (isFunction(component)) { + warn(`Functional vapor component must return a block directly.`) + instance.block = [] + } else if (!component.render) { + warn( + `Vapor component setup() returned non-block value, and has no render function.`, + ) + instance.block = [] + } else { + instance.devtoolsRawSetupState = setupResult + // TODO make the proxy warn non-existent property access during dev + instance.setupState = proxyRefs(setupResult) + devRender(instance) + } + } else { + // component has a render function but no setup function + // (typically components with only a template and no state) + if (!setupFn && component.render) { + instance.block = callWithErrorHandling( + component.render, + instance, + ErrorCodes.RENDER_FUNCTION, + ) + } else { + // in prod result can only be block + instance.block = setupResult as Block + } + } + + // single root, inherit attrs + if ( + instance.hasFallthrough && + component.inheritAttrs !== false && + Object.keys(instance.attrs).length + ) { + const root = getRootElement( + instance.block, + // attach attrs to root dynamic fragments for applying during each update + frag => (frag.attrs = instance.attrs), + false, + ) + if (root) { + renderEffect(() => { + const attrs = + isFunction(component) && !isVaporTransition(component) + ? getFunctionalFallthrough(instance.attrs) + : instance.attrs + if (attrs) applyFallthroughProps(root, attrs) + }) + } else if ( + __DEV__ && + ((!instance.accessedAttrs && + isArray(instance.block) && + instance.block.length) || + // preventing attrs fallthrough on Teleport + // consistent with VDOM Teleport behavior + instance.block instanceof TeleportFragment) + ) { + warnExtraneousAttributes(instance.attrs) + } + } +} diff --git a/packages/runtime-vapor/src/components/Suspense.ts b/packages/runtime-vapor/src/components/Suspense.ts new file mode 100644 index 00000000000..6496a85570f --- /dev/null +++ b/packages/runtime-vapor/src/components/Suspense.ts @@ -0,0 +1,20 @@ +import type { SuspenseBoundary } from '@vue/runtime-dom' + +export let parentSuspense: SuspenseBoundary | null = null + +export function setParentSuspense( + suspense: SuspenseBoundary | null, +): SuspenseBoundary | null { + try { + return parentSuspense + } finally { + parentSuspense = suspense + } +} + +// TODO: implement this +export const VaporSuspenseImpl = { + name: 'VaporSuspense', + __isSuspense: true, + process(): void {}, +} diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index 7f051e064f1..d08946afcaa 100644 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -12,6 +12,7 @@ import { type RendererNode, type ShallowRef, type Slots, + type SuspenseBoundary, type TransitionHooks, type VNode, type VNodeNormalizedRef, @@ -80,6 +81,7 @@ import { deactivate, findParentKeepAlive, } from './components/KeepAlive' +import { setParentSuspense } from './components/Suspense' export const interopKey: unique symbol = Symbol(`interop`) @@ -88,7 +90,7 @@ const vaporInteropImpl: Omit< VaporInteropInterface, 'vdomMount' | 'vdomUnmount' | 'vdomSlot' > = { - mount(vnode, container, anchor, parentComponent) { + mount(vnode, container, anchor, parentComponent, parentSuspense) { let selfAnchor = (vnode.el = vnode.anchor = createTextNode()) if (isHydrating) { // avoid vdom hydration children mismatch by the selfAnchor, delay its insertion @@ -110,6 +112,11 @@ const vaporInteropImpl: Omit< const propsRef = shallowRef(props) const slotsRef = shallowRef(vnode.children) + let prevSuspense: SuspenseBoundary | null = null + if (__FEATURE_SUSPENSE__) { + prevSuspense = setParentSuspense(parentSuspense) + } + const dynamicPropSource: (() => any)[] & { [interopKey]?: boolean } = [ () => propsRef.value, ] @@ -140,6 +147,11 @@ const vaporInteropImpl: Omit< vnode.transition as VaporTransitionHooks, ) } + + if (__FEATURE_SUSPENSE__) { + setParentSuspense(prevSuspense) + } + mountComponent(instance, container, selfAnchor) simpleSetCurrentInstance(prev) return instance @@ -157,8 +169,12 @@ const vaporInteropImpl: Omit< unmount(vnode, doRemove) { const container = doRemove ? vnode.anchor!.parentNode : undefined - if (vnode.component) { - unmountComponent(vnode.component as any, container) + const instance = vnode.component as any as VaporComponentInstance + if (instance) { + // the async component may not be resolved yet, block is null + if (instance.block) { + unmountComponent(instance, container) + } } else if (vnode.vb) { remove(vnode.vb, container) } @@ -197,9 +213,9 @@ const vaporInteropImpl: Omit< insert(vnode.anchor as any, container, anchor) }, - hydrate(vnode, node, container, anchor, parentComponent) { + hydrate(vnode, node, container, anchor, parentComponent, parentSuspense) { vaporHydrateNode(node, () => - this.mount(vnode, container, anchor, parentComponent), + this.mount(vnode, container, anchor, parentComponent, parentSuspense), ) return _next(node) }, From 2d798629f88e605ed6ad270e06210733338c3564 Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 2 Dec 2025 09:56:09 +0800 Subject: [PATCH 2/7] refactor: move async component resolution rendering logic from Suspense to renderer-specific `registerDep` callbacks. --- .../runtime-core/src/components/Suspense.ts | 61 +++---------------- packages/runtime-core/src/renderer.ts | 43 ++++++++++++- packages/runtime-vapor/src/component.ts | 30 +++++---- 3 files changed, 65 insertions(+), 69 deletions(-) diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts index 130064c85d9..2cc5ba60a0c 100644 --- a/packages/runtime-core/src/components/Suspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -11,7 +11,7 @@ import { openBlock, } from '../vnode' import { ShapeFlags, isArray, isFunction, toNumber } from '@vue/shared' -import { type ComponentInternalInstance, handleSetupResult } from '../component' +import type { ComponentInternalInstance } from '../component' import type { Slots } from '../componentSlots' import { type ElementNamespace, @@ -19,17 +19,11 @@ import { type RendererElement, type RendererInternals, type RendererNode, - type SetupRenderEffectFn, queuePostRenderEffect, } from '../renderer' import { queuePostFlushCb } from '../scheduler' import { filterSingleRoot, updateHOCHostEl } from '../componentRenderUtils' -import { - assertNumber, - popWarningContext, - pushWarningContext, - warn, -} from '../warning' +import { assertNumber, warn } from '../warning' import { ErrorCodes, handleError } from '../errorHandling' import { NULL_DYNAMIC_COMPONENT } from '../helpers/resolveAssets' @@ -437,8 +431,7 @@ export interface SuspenseBoundary { next(): RendererNode | null registerDep( instance: ComponentInternalInstance, - setupRenderEffect: SetupRenderEffectFn, - optimized: boolean, + onResolve: (setupResult: unknown) => void, ): void unmount(parentSuspense: SuspenseBoundary | null, doRemove?: boolean): void } @@ -474,7 +467,7 @@ function createSuspenseBoundary( m: move, um: unmount, n: next, - o: { parentNode, remove }, + o: { parentNode }, } = rendererInternals // if set `suspensible: true`, set the current suspense as a dep of parent suspense @@ -701,12 +694,12 @@ function createSuspenseBoundary( return suspense.activeBranch && next(suspense.activeBranch) }, - registerDep(instance, setupRenderEffect, optimized) { + registerDep(instance, onResolve) { const isInPendingSuspense = !!suspense.pendingBranch if (isInPendingSuspense) { suspense.deps++ } - const hydratedEl = instance.vapor ? null : instance.vnode.el + instance .asyncDep!.catch(err => { handleError(err, instance, ErrorCodes.SETUP_FUNCTION) @@ -723,46 +716,8 @@ function createSuspenseBoundary( } // retry from this component instance.asyncResolved = true - // vapor component - if (instance.vapor) { - // @ts-expect-error - setupRenderEffect(asyncSetupResult) - } else { - const { vnode } = instance - if (__DEV__) { - pushWarningContext(vnode) - } - handleSetupResult(instance, asyncSetupResult, false) - if (hydratedEl) { - // vnode may have been replaced if an update happened before the - // async dep is resolved. - vnode.el = hydratedEl - } - const placeholder = !hydratedEl && instance.subTree.el - setupRenderEffect( - instance, - vnode, - // component may have been moved before resolve. - // if this is not a hydration, instance.subTree will be the comment - // placeholder. - parentNode(hydratedEl || instance.subTree.el!)!, - // anchor will not be used if this is hydration, so only need to - // consider the comment placeholder case. - hydratedEl ? null : next(instance.subTree), - suspense, - namespace, - optimized, - ) - if (placeholder) { - // clean up placeholder reference - vnode.placeholder = null - remove(placeholder) - } - updateHOCHostEl(instance, vnode.el) - if (__DEV__) { - popWarningContext() - } - } + onResolve(asyncSetupResult) + // only decrease deps count if suspense is not already resolved if (isInPendingSuspense && --suspense.deps === 0) { suspense.resolve() diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 8ab94485e3d..c3cb60fd40e 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -24,6 +24,7 @@ import { type LifecycleHook, createComponentInstance, getComponentPublicInstance, + handleSetupResult, setupComponent, } from './component' import { @@ -1273,9 +1274,45 @@ function baseCreateRenderer( // setup() is async. This component relies on async logic to be resolved // before proceeding if (__FEATURE_SUSPENSE__ && instance.asyncDep) { - parentSuspense && - parentSuspense.registerDep(instance, setupRenderEffect, optimized) - + if (parentSuspense) { + const hydratedEl = instance.vnode.el + parentSuspense.registerDep(instance, setupResult => { + const { vnode } = instance + if (__DEV__) { + pushWarningContext(vnode) + } + handleSetupResult(instance, setupResult, false) + if (hydratedEl) { + // vnode may have been replaced if an update happened before the + // async dep is resolved. + vnode.el = hydratedEl + } + const placeholder = !hydratedEl && instance.subTree.el + setupRenderEffect( + instance, + vnode, + // component may have been moved before resolve. + // if this is not a hydration, instance.subTree will be the comment + // placeholder. + hostParentNode(hydratedEl || instance.subTree.el!)!, + // anchor will not be used if this is hydration, so only need to + // consider the comment placeholder case. + hydratedEl ? null : getNextHostNode(instance.subTree), + parentSuspense, + namespace, + optimized, + ) + if (placeholder) { + // clean up placeholder reference + vnode.placeholder = null + hostRemove(placeholder) + } + updateHOCHostEl(instance, vnode.el) + if (__DEV__) { + popWarningContext() + } + }) + } // Give it a placeholder if this is not hydration // TODO handle self-defined fallback if (!initialVNode.el) { diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index d00e70bff32..bd0467523f2 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -775,19 +775,15 @@ export function mountComponent( !instance.asyncResolved ) { const component = instance.type - instance.suspense.registerDep( - instance as any, - (setupResult: any) => { - handleSetupResult( - setupResult, - component, - instance, - isFunction(component) ? component : component.setup, - ) - mountComponent(instance, parent, anchor) - }, - false, - ) + instance.suspense.registerDep(instance as any, setupResult => { + handleSetupResult( + setupResult, + component, + instance, + isFunction(component) ? component : component.setup, + ) + mountComponent(instance, parent, anchor) + }) return } @@ -932,6 +928,10 @@ function handleSetupResult( instance: VaporComponentInstance, setupFn: VaporSetupFn | undefined, ) { + if (__DEV__) { + pushWarningContext(instance) + } + if (__DEV__ && !isBlock(setupResult)) { if (isFunction(component)) { warn(`Functional vapor component must return a block directly.`) @@ -994,4 +994,8 @@ function handleSetupResult( warnExtraneousAttributes(instance.attrs) } } + + if (__DEV__) { + popWarningContext() + } } From f62addb753443f15306378e024ad815e3bbb6548 Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 2 Dec 2025 15:25:09 +0800 Subject: [PATCH 3/7] test: clear document body before each vdom interop test --- packages/runtime-vapor/__tests__/components/Suspense.spec.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/runtime-vapor/__tests__/components/Suspense.spec.ts b/packages/runtime-vapor/__tests__/components/Suspense.spec.ts index b13377f773f..b0a4649dc7d 100644 --- a/packages/runtime-vapor/__tests__/components/Suspense.spec.ts +++ b/packages/runtime-vapor/__tests__/components/Suspense.spec.ts @@ -4,6 +4,10 @@ import { compile, runtimeDom, runtimeVapor } from '../_utils' describe.todo('VaporSuspense', () => {}) describe('vdom interop', () => { + beforeEach(() => { + document.body.innerHTML = '' + }) + async function testSuspense( code: string, components: Record = {}, From 7f915579861e9078cdb470777b721aa67a198ffb Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 4 Dec 2025 08:59:39 +0800 Subject: [PATCH 4/7] chore: update Suspense dependency registration to use GenericComponentInstance --- packages/runtime-core/src/components/Suspense.ts | 7 +++++-- packages/runtime-vapor/src/component.ts | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts index 2cc5ba60a0c..b3463f0b353 100644 --- a/packages/runtime-core/src/components/Suspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -11,7 +11,10 @@ import { openBlock, } from '../vnode' import { ShapeFlags, isArray, isFunction, toNumber } from '@vue/shared' -import type { ComponentInternalInstance } from '../component' +import type { + ComponentInternalInstance, + GenericComponentInstance, +} from '../component' import type { Slots } from '../componentSlots' import { type ElementNamespace, @@ -430,7 +433,7 @@ export interface SuspenseBoundary { ): void next(): RendererNode | null registerDep( - instance: ComponentInternalInstance, + instance: GenericComponentInstance, onResolve: (setupResult: unknown) => void, ): void unmount(parentSuspense: SuspenseBoundary | null, doRemove?: boolean): void diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index cdccc981539..9371c1a1abd 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -802,7 +802,7 @@ export function mountComponent( !instance.asyncResolved ) { const component = instance.type - instance.suspense.registerDep(instance as any, setupResult => { + instance.suspense.registerDep(instance, setupResult => { handleSetupResult( setupResult, component, From 040154ea7174db39e9331268e45573da0c13d123 Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 4 Dec 2025 09:08:59 +0800 Subject: [PATCH 5/7] test: add `useId` helper test file --- .../__tests__/helpers/useId.spec.ts | 325 ++++++++++++++++++ 1 file changed, 325 insertions(+) create mode 100644 packages/runtime-vapor/__tests__/helpers/useId.spec.ts diff --git a/packages/runtime-vapor/__tests__/helpers/useId.spec.ts b/packages/runtime-vapor/__tests__/helpers/useId.spec.ts new file mode 100644 index 00000000000..ec78c7f2fbf --- /dev/null +++ b/packages/runtime-vapor/__tests__/helpers/useId.spec.ts @@ -0,0 +1,325 @@ +// TODO: Suspense is required to be implemented for this test to pass + +// /** +// * @vitest-environment jsdom +// */ +// import { +// type App, +// Suspense, +// createApp, +// defineAsyncComponent, +// defineComponent, +// h, +// onServerPrefetch, +// useId, +// } from 'vue' +// import { renderToString } from '@vue/server-renderer' + +// type FactoryRes = [App, Promise[]] +// type TestCaseFactory = () => FactoryRes | Promise + +// async function runOnClient(factory: TestCaseFactory) { +// const [app, deps] = await factory() +// const root = document.createElement('div') +// app.mount(root) +// await Promise.all(deps) +// await promiseWithDelay(null, 0) +// return root.innerHTML +// } + +// async function runOnServer(factory: TestCaseFactory) { +// const [app, _] = await factory() +// return (await renderToString(app)) +// .replace(//g, '') // remove fragment wrappers +// .trim() +// } + +// async function getOutput(factory: TestCaseFactory) { +// const clientResult = await runOnClient(factory) +// const serverResult = await runOnServer(factory) +// expect(serverResult).toBe(clientResult) +// return clientResult +// } + +// function promiseWithDelay(res: any, delay: number) { +// return new Promise(r => { +// setTimeout(() => r(res), delay) +// }) +// } + +// const BasicComponentWithUseId = defineComponent({ +// setup() { +// const id1 = useId() +// const id2 = useId() +// return () => [id1, ' ', id2] +// }, +// }) + +// describe('useId', () => { +// test('basic', async () => { +// expect( +// await getOutput(() => { +// const app = createApp(BasicComponentWithUseId) +// return [app, []] +// }), +// ).toBe('v-0 v-1') +// }) + +// test('with config.idPrefix', async () => { +// expect( +// await getOutput(() => { +// const app = createApp(BasicComponentWithUseId) +// app.config.idPrefix = 'foo' +// return [app, []] +// }), +// ).toBe('foo-0 foo-1') +// }) + +// test('async component', async () => { +// const factory = ( +// delay1: number, +// delay2: number, +// ): ReturnType => { +// const p1 = promiseWithDelay(BasicComponentWithUseId, delay1) +// const p2 = promiseWithDelay(BasicComponentWithUseId, delay2) +// const AsyncOne = defineAsyncComponent(() => p1) +// const AsyncTwo = defineAsyncComponent(() => p2) +// const app = createApp({ +// setup() { +// const id1 = useId() +// const id2 = useId() +// return () => [id1, ' ', id2, ' ', h(AsyncOne), ' ', h(AsyncTwo)] +// }, +// }) +// return [app, [p1, p2]] +// } + +// const expected = +// 'v-0 v-1 ' + // root +// 'v-0-0 v-0-1 ' + // inside first async subtree +// 'v-1-0 v-1-1' // inside second async subtree +// // assert different async resolution order does not affect id stable-ness +// expect(await getOutput(() => factory(0, 16))).toBe(expected) +// expect(await getOutput(() => factory(16, 0))).toBe(expected) +// }) + +// test('serverPrefetch', async () => { +// const factory = ( +// delay1: number, +// delay2: number, +// ): ReturnType => { +// const p1 = promiseWithDelay(null, delay1) +// const p2 = promiseWithDelay(null, delay2) + +// const SPOne = defineComponent({ +// async serverPrefetch() { +// await p1 +// }, +// render() { +// return h(BasicComponentWithUseId) +// }, +// }) + +// const SPTwo = defineComponent({ +// async serverPrefetch() { +// await p2 +// }, +// render() { +// return h(BasicComponentWithUseId) +// }, +// }) + +// const app = createApp({ +// setup() { +// const id1 = useId() +// const id2 = useId() +// return () => [id1, ' ', id2, ' ', h(SPOne), ' ', h(SPTwo)] +// }, +// }) +// return [app, [p1, p2]] +// } + +// const expected = +// 'v-0 v-1 ' + // root +// 'v-0-0 v-0-1 ' + // inside first async subtree +// 'v-1-0 v-1-1' // inside second async subtree +// // assert different async resolution order does not affect id stable-ness +// expect(await getOutput(() => factory(0, 16))).toBe(expected) +// expect(await getOutput(() => factory(16, 0))).toBe(expected) +// }) + +// test('components with serverPrefetch', async () => { +// const factory = (): ReturnType => { +// const SPOne = defineComponent({ +// setup() { +// onServerPrefetch(() => {}) +// return () => h(BasicComponentWithUseId) +// }, +// }) + +// const SPTwo = defineComponent({ +// render() { +// return h(BasicComponentWithUseId) +// }, +// }) + +// const app = createApp({ +// setup() { +// const id1 = useId() +// const id2 = useId() +// return () => [id1, ' ', id2, ' ', h(SPOne), ' ', h(SPTwo)] +// }, +// }) +// return [app, []] +// } + +// const expected = +// 'v-0 v-1 ' + // root +// 'v-0-0 v-0-1 ' + // inside first async subtree +// 'v-2 v-3' // inside second async subtree +// // assert different async resolution order does not affect id stable-ness +// expect(await getOutput(() => factory())).toBe(expected) +// expect(await getOutput(() => factory())).toBe(expected) +// }) + +// test('async setup()', async () => { +// const factory = ( +// delay1: number, +// delay2: number, +// ): ReturnType => { +// const p1 = promiseWithDelay(null, delay1) +// const p2 = promiseWithDelay(null, delay2) + +// const ASOne = defineComponent({ +// async setup() { +// await p1 +// return {} +// }, +// render() { +// return h(BasicComponentWithUseId) +// }, +// }) + +// const ASTwo = defineComponent({ +// async setup() { +// await p2 +// return {} +// }, +// render() { +// return h(BasicComponentWithUseId) +// }, +// }) + +// const app = createApp({ +// setup() { +// const id1 = useId() +// const id2 = useId() +// return () => +// h(Suspense, null, { +// default: h('div', [id1, ' ', id2, ' ', h(ASOne), ' ', h(ASTwo)]), +// }) +// }, +// }) +// return [app, [p1, p2]] +// } + +// const expected = +// '
' + +// 'v-0 v-1 ' + // root +// 'v-0-0 v-0-1 ' + // inside first async subtree +// 'v-1-0 v-1-1' + // inside second async subtree +// '
' +// // assert different async resolution order does not affect id stable-ness +// expect(await getOutput(() => factory(0, 16))).toBe(expected) +// expect(await getOutput(() => factory(16, 0))).toBe(expected) +// }) + +// test('deep nested', async () => { +// const factory = (): ReturnType => { +// const p = Promise.resolve() +// const One = { +// async setup() { +// const id = useId() +// await p +// return () => [id, ' ', h(Two), ' ', h(Three)] +// }, +// } +// const Two = { +// async setup() { +// const id = useId() +// await p +// return () => [id, ' ', h(Three), ' ', h(Three)] +// }, +// } +// const Three = { +// async setup() { +// const id = useId() +// return () => id +// }, +// } +// const app = createApp({ +// setup() { +// return () => +// h(Suspense, null, { +// default: h(One), +// }) +// }, +// }) +// return [app, [p]] +// } + +// const expected = +// 'v-0 ' + // One +// 'v-0-0 ' + // Two +// 'v-0-0-0 v-0-0-1 ' + // Three + Three nested in Two +// 'v-0-1' // Three after Two +// // assert different async resolution order does not affect id stable-ness +// expect(await getOutput(() => factory())).toBe(expected) +// expect(await getOutput(() => factory())).toBe(expected) +// }) + +// test('async component inside async setup, already resolved', async () => { +// const factory = async ( +// delay1: number, +// delay2: number, +// ): Promise => { +// const p1 = promiseWithDelay(null, delay1) +// const p2 = promiseWithDelay(BasicComponentWithUseId, delay2) +// const AsyncInner = defineAsyncComponent(() => p2) + +// const AsyncSetup = defineComponent({ +// async setup() { +// await p1 +// return {} +// }, +// render() { +// return h(AsyncInner) +// }, +// }) + +// const app = createApp({ +// setup() { +// const id1 = useId() +// const id2 = useId() +// return () => +// h(Suspense, null, { +// default: h('div', [id1, ' ', id2, ' ', h(AsyncSetup)]), +// }) +// }, +// }) + +// // the async component may have already been resolved +// await AsyncInner.__asyncLoader() +// return [app, [p1, p2]] +// } + +// const expected = +// '
' + +// 'v-0 v-1 ' + // root +// 'v-0-0-0 v-0-0-1' + // async component inside async setup +// '
' +// // assert different async resolution order does not affect id stable-ness +// expect(await getOutput(async () => factory(0, 16))).toBe(expected) +// expect(await getOutput(() => factory(16, 0))).toBe(expected) +// }) +// }) From af0706160b4942ca8e618d4a18931c9adff0757d Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 4 Dec 2025 09:13:41 +0800 Subject: [PATCH 6/7] chore: tweaks --- packages/runtime-vapor/src/vdomInterop.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index d08946afcaa..60b5aad1f44 100644 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -113,7 +113,7 @@ const vaporInteropImpl: Omit< const slotsRef = shallowRef(vnode.children) let prevSuspense: SuspenseBoundary | null = null - if (__FEATURE_SUSPENSE__) { + if (__FEATURE_SUSPENSE__ && parentSuspense) { prevSuspense = setParentSuspense(parentSuspense) } @@ -148,7 +148,7 @@ const vaporInteropImpl: Omit< ) } - if (__FEATURE_SUSPENSE__) { + if (__FEATURE_SUSPENSE__ && parentSuspense) { setParentSuspense(prevSuspense) } From 00b05618943c000398ee8ce7798ef3b8593bb60a Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 4 Dec 2025 09:18:05 +0800 Subject: [PATCH 7/7] test: convert `useId` tests to a todo for future implementation --- .../__tests__/helpers/useId.spec.ts | 512 +++++++++--------- 1 file changed, 244 insertions(+), 268 deletions(-) diff --git a/packages/runtime-vapor/__tests__/helpers/useId.spec.ts b/packages/runtime-vapor/__tests__/helpers/useId.spec.ts index ec78c7f2fbf..7f0e7f7e2f0 100644 --- a/packages/runtime-vapor/__tests__/helpers/useId.spec.ts +++ b/packages/runtime-vapor/__tests__/helpers/useId.spec.ts @@ -55,271 +55,247 @@ // }, // }) -// describe('useId', () => { -// test('basic', async () => { -// expect( -// await getOutput(() => { -// const app = createApp(BasicComponentWithUseId) -// return [app, []] -// }), -// ).toBe('v-0 v-1') -// }) - -// test('with config.idPrefix', async () => { -// expect( -// await getOutput(() => { -// const app = createApp(BasicComponentWithUseId) -// app.config.idPrefix = 'foo' -// return [app, []] -// }), -// ).toBe('foo-0 foo-1') -// }) - -// test('async component', async () => { -// const factory = ( -// delay1: number, -// delay2: number, -// ): ReturnType => { -// const p1 = promiseWithDelay(BasicComponentWithUseId, delay1) -// const p2 = promiseWithDelay(BasicComponentWithUseId, delay2) -// const AsyncOne = defineAsyncComponent(() => p1) -// const AsyncTwo = defineAsyncComponent(() => p2) -// const app = createApp({ -// setup() { -// const id1 = useId() -// const id2 = useId() -// return () => [id1, ' ', id2, ' ', h(AsyncOne), ' ', h(AsyncTwo)] -// }, -// }) -// return [app, [p1, p2]] -// } - -// const expected = -// 'v-0 v-1 ' + // root -// 'v-0-0 v-0-1 ' + // inside first async subtree -// 'v-1-0 v-1-1' // inside second async subtree -// // assert different async resolution order does not affect id stable-ness -// expect(await getOutput(() => factory(0, 16))).toBe(expected) -// expect(await getOutput(() => factory(16, 0))).toBe(expected) -// }) - -// test('serverPrefetch', async () => { -// const factory = ( -// delay1: number, -// delay2: number, -// ): ReturnType => { -// const p1 = promiseWithDelay(null, delay1) -// const p2 = promiseWithDelay(null, delay2) - -// const SPOne = defineComponent({ -// async serverPrefetch() { -// await p1 -// }, -// render() { -// return h(BasicComponentWithUseId) -// }, -// }) - -// const SPTwo = defineComponent({ -// async serverPrefetch() { -// await p2 -// }, -// render() { -// return h(BasicComponentWithUseId) -// }, -// }) - -// const app = createApp({ -// setup() { -// const id1 = useId() -// const id2 = useId() -// return () => [id1, ' ', id2, ' ', h(SPOne), ' ', h(SPTwo)] -// }, -// }) -// return [app, [p1, p2]] -// } - -// const expected = -// 'v-0 v-1 ' + // root -// 'v-0-0 v-0-1 ' + // inside first async subtree -// 'v-1-0 v-1-1' // inside second async subtree -// // assert different async resolution order does not affect id stable-ness -// expect(await getOutput(() => factory(0, 16))).toBe(expected) -// expect(await getOutput(() => factory(16, 0))).toBe(expected) -// }) - -// test('components with serverPrefetch', async () => { -// const factory = (): ReturnType => { -// const SPOne = defineComponent({ -// setup() { -// onServerPrefetch(() => {}) -// return () => h(BasicComponentWithUseId) -// }, -// }) - -// const SPTwo = defineComponent({ -// render() { -// return h(BasicComponentWithUseId) -// }, -// }) - -// const app = createApp({ -// setup() { -// const id1 = useId() -// const id2 = useId() -// return () => [id1, ' ', id2, ' ', h(SPOne), ' ', h(SPTwo)] -// }, -// }) -// return [app, []] -// } - -// const expected = -// 'v-0 v-1 ' + // root -// 'v-0-0 v-0-1 ' + // inside first async subtree -// 'v-2 v-3' // inside second async subtree -// // assert different async resolution order does not affect id stable-ness -// expect(await getOutput(() => factory())).toBe(expected) -// expect(await getOutput(() => factory())).toBe(expected) -// }) - -// test('async setup()', async () => { -// const factory = ( -// delay1: number, -// delay2: number, -// ): ReturnType => { -// const p1 = promiseWithDelay(null, delay1) -// const p2 = promiseWithDelay(null, delay2) - -// const ASOne = defineComponent({ -// async setup() { -// await p1 -// return {} -// }, -// render() { -// return h(BasicComponentWithUseId) -// }, -// }) - -// const ASTwo = defineComponent({ -// async setup() { -// await p2 -// return {} -// }, -// render() { -// return h(BasicComponentWithUseId) -// }, -// }) - -// const app = createApp({ -// setup() { -// const id1 = useId() -// const id2 = useId() -// return () => -// h(Suspense, null, { -// default: h('div', [id1, ' ', id2, ' ', h(ASOne), ' ', h(ASTwo)]), -// }) -// }, -// }) -// return [app, [p1, p2]] -// } - -// const expected = -// '
' + -// 'v-0 v-1 ' + // root -// 'v-0-0 v-0-1 ' + // inside first async subtree -// 'v-1-0 v-1-1' + // inside second async subtree -// '
' -// // assert different async resolution order does not affect id stable-ness -// expect(await getOutput(() => factory(0, 16))).toBe(expected) -// expect(await getOutput(() => factory(16, 0))).toBe(expected) -// }) - -// test('deep nested', async () => { -// const factory = (): ReturnType => { -// const p = Promise.resolve() -// const One = { -// async setup() { -// const id = useId() -// await p -// return () => [id, ' ', h(Two), ' ', h(Three)] -// }, -// } -// const Two = { -// async setup() { -// const id = useId() -// await p -// return () => [id, ' ', h(Three), ' ', h(Three)] -// }, -// } -// const Three = { -// async setup() { -// const id = useId() -// return () => id -// }, -// } -// const app = createApp({ -// setup() { -// return () => -// h(Suspense, null, { -// default: h(One), -// }) -// }, -// }) -// return [app, [p]] -// } - -// const expected = -// 'v-0 ' + // One -// 'v-0-0 ' + // Two -// 'v-0-0-0 v-0-0-1 ' + // Three + Three nested in Two -// 'v-0-1' // Three after Two -// // assert different async resolution order does not affect id stable-ness -// expect(await getOutput(() => factory())).toBe(expected) -// expect(await getOutput(() => factory())).toBe(expected) -// }) - -// test('async component inside async setup, already resolved', async () => { -// const factory = async ( -// delay1: number, -// delay2: number, -// ): Promise => { -// const p1 = promiseWithDelay(null, delay1) -// const p2 = promiseWithDelay(BasicComponentWithUseId, delay2) -// const AsyncInner = defineAsyncComponent(() => p2) - -// const AsyncSetup = defineComponent({ -// async setup() { -// await p1 -// return {} -// }, -// render() { -// return h(AsyncInner) -// }, -// }) - -// const app = createApp({ -// setup() { -// const id1 = useId() -// const id2 = useId() -// return () => -// h(Suspense, null, { -// default: h('div', [id1, ' ', id2, ' ', h(AsyncSetup)]), -// }) -// }, -// }) - -// // the async component may have already been resolved -// await AsyncInner.__asyncLoader() -// return [app, [p1, p2]] -// } - -// const expected = -// '
' + -// 'v-0 v-1 ' + // root -// 'v-0-0-0 v-0-0-1' + // async component inside async setup -// '
' -// // assert different async resolution order does not affect id stable-ness -// expect(await getOutput(async () => factory(0, 16))).toBe(expected) -// expect(await getOutput(() => factory(16, 0))).toBe(expected) -// }) -// }) +describe.todo('useId', () => { + // test('basic', async () => { + // expect( + // await getOutput(() => { + // const app = createApp(BasicComponentWithUseId) + // return [app, []] + // }), + // ).toBe('v-0 v-1') + // }) + // test('with config.idPrefix', async () => { + // expect( + // await getOutput(() => { + // const app = createApp(BasicComponentWithUseId) + // app.config.idPrefix = 'foo' + // return [app, []] + // }), + // ).toBe('foo-0 foo-1') + // }) + // test('async component', async () => { + // const factory = ( + // delay1: number, + // delay2: number, + // ): ReturnType => { + // const p1 = promiseWithDelay(BasicComponentWithUseId, delay1) + // const p2 = promiseWithDelay(BasicComponentWithUseId, delay2) + // const AsyncOne = defineAsyncComponent(() => p1) + // const AsyncTwo = defineAsyncComponent(() => p2) + // const app = createApp({ + // setup() { + // const id1 = useId() + // const id2 = useId() + // return () => [id1, ' ', id2, ' ', h(AsyncOne), ' ', h(AsyncTwo)] + // }, + // }) + // return [app, [p1, p2]] + // } + // const expected = + // 'v-0 v-1 ' + // root + // 'v-0-0 v-0-1 ' + // inside first async subtree + // 'v-1-0 v-1-1' // inside second async subtree + // // assert different async resolution order does not affect id stable-ness + // expect(await getOutput(() => factory(0, 16))).toBe(expected) + // expect(await getOutput(() => factory(16, 0))).toBe(expected) + // }) + // test('serverPrefetch', async () => { + // const factory = ( + // delay1: number, + // delay2: number, + // ): ReturnType => { + // const p1 = promiseWithDelay(null, delay1) + // const p2 = promiseWithDelay(null, delay2) + // const SPOne = defineComponent({ + // async serverPrefetch() { + // await p1 + // }, + // render() { + // return h(BasicComponentWithUseId) + // }, + // }) + // const SPTwo = defineComponent({ + // async serverPrefetch() { + // await p2 + // }, + // render() { + // return h(BasicComponentWithUseId) + // }, + // }) + // const app = createApp({ + // setup() { + // const id1 = useId() + // const id2 = useId() + // return () => [id1, ' ', id2, ' ', h(SPOne), ' ', h(SPTwo)] + // }, + // }) + // return [app, [p1, p2]] + // } + // const expected = + // 'v-0 v-1 ' + // root + // 'v-0-0 v-0-1 ' + // inside first async subtree + // 'v-1-0 v-1-1' // inside second async subtree + // // assert different async resolution order does not affect id stable-ness + // expect(await getOutput(() => factory(0, 16))).toBe(expected) + // expect(await getOutput(() => factory(16, 0))).toBe(expected) + // }) + // test('components with serverPrefetch', async () => { + // const factory = (): ReturnType => { + // const SPOne = defineComponent({ + // setup() { + // onServerPrefetch(() => {}) + // return () => h(BasicComponentWithUseId) + // }, + // }) + // const SPTwo = defineComponent({ + // render() { + // return h(BasicComponentWithUseId) + // }, + // }) + // const app = createApp({ + // setup() { + // const id1 = useId() + // const id2 = useId() + // return () => [id1, ' ', id2, ' ', h(SPOne), ' ', h(SPTwo)] + // }, + // }) + // return [app, []] + // } + // const expected = + // 'v-0 v-1 ' + // root + // 'v-0-0 v-0-1 ' + // inside first async subtree + // 'v-2 v-3' // inside second async subtree + // // assert different async resolution order does not affect id stable-ness + // expect(await getOutput(() => factory())).toBe(expected) + // expect(await getOutput(() => factory())).toBe(expected) + // }) + // test('async setup()', async () => { + // const factory = ( + // delay1: number, + // delay2: number, + // ): ReturnType => { + // const p1 = promiseWithDelay(null, delay1) + // const p2 = promiseWithDelay(null, delay2) + // const ASOne = defineComponent({ + // async setup() { + // await p1 + // return {} + // }, + // render() { + // return h(BasicComponentWithUseId) + // }, + // }) + // const ASTwo = defineComponent({ + // async setup() { + // await p2 + // return {} + // }, + // render() { + // return h(BasicComponentWithUseId) + // }, + // }) + // const app = createApp({ + // setup() { + // const id1 = useId() + // const id2 = useId() + // return () => + // h(Suspense, null, { + // default: h('div', [id1, ' ', id2, ' ', h(ASOne), ' ', h(ASTwo)]), + // }) + // }, + // }) + // return [app, [p1, p2]] + // } + // const expected = + // '
' + + // 'v-0 v-1 ' + // root + // 'v-0-0 v-0-1 ' + // inside first async subtree + // 'v-1-0 v-1-1' + // inside second async subtree + // '
' + // // assert different async resolution order does not affect id stable-ness + // expect(await getOutput(() => factory(0, 16))).toBe(expected) + // expect(await getOutput(() => factory(16, 0))).toBe(expected) + // }) + // test('deep nested', async () => { + // const factory = (): ReturnType => { + // const p = Promise.resolve() + // const One = { + // async setup() { + // const id = useId() + // await p + // return () => [id, ' ', h(Two), ' ', h(Three)] + // }, + // } + // const Two = { + // async setup() { + // const id = useId() + // await p + // return () => [id, ' ', h(Three), ' ', h(Three)] + // }, + // } + // const Three = { + // async setup() { + // const id = useId() + // return () => id + // }, + // } + // const app = createApp({ + // setup() { + // return () => + // h(Suspense, null, { + // default: h(One), + // }) + // }, + // }) + // return [app, [p]] + // } + // const expected = + // 'v-0 ' + // One + // 'v-0-0 ' + // Two + // 'v-0-0-0 v-0-0-1 ' + // Three + Three nested in Two + // 'v-0-1' // Three after Two + // // assert different async resolution order does not affect id stable-ness + // expect(await getOutput(() => factory())).toBe(expected) + // expect(await getOutput(() => factory())).toBe(expected) + // }) + // test('async component inside async setup, already resolved', async () => { + // const factory = async ( + // delay1: number, + // delay2: number, + // ): Promise => { + // const p1 = promiseWithDelay(null, delay1) + // const p2 = promiseWithDelay(BasicComponentWithUseId, delay2) + // const AsyncInner = defineAsyncComponent(() => p2) + // const AsyncSetup = defineComponent({ + // async setup() { + // await p1 + // return {} + // }, + // render() { + // return h(AsyncInner) + // }, + // }) + // const app = createApp({ + // setup() { + // const id1 = useId() + // const id2 = useId() + // return () => + // h(Suspense, null, { + // default: h('div', [id1, ' ', id2, ' ', h(AsyncSetup)]), + // }) + // }, + // }) + // // the async component may have already been resolved + // await AsyncInner.__asyncLoader() + // return [app, [p1, p2]] + // } + // const expected = + // '
' + + // 'v-0 v-1 ' + // root + // 'v-0-0-0 v-0-0-1' + // async component inside async setup + // '
' + // // assert different async resolution order does not affect id stable-ness + // expect(await getOutput(async () => factory(0, 16))).toBe(expected) + // expect(await getOutput(() => factory(16, 0))).toBe(expected) + // }) +})