From 8d77c1fd1c0b76381024030c7e36cdac9042f8f0 Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 18 Mar 2025 11:15:24 +0800 Subject: [PATCH 1/9] wip: save --- .../runtime-core/src/apiAsyncComponent.ts | 234 +++++++++++------- packages/runtime-core/src/index.ts | 11 + .../__tests__/apiDefineAsyncComponent.spec.ts | 48 ++++ .../src/apiDefineAsyncComponent.ts | 121 +++++++++ packages/runtime-vapor/src/index.ts | 1 + 5 files changed, 319 insertions(+), 96 deletions(-) create mode 100644 packages/runtime-vapor/__tests__/apiDefineAsyncComponent.spec.ts create mode 100644 packages/runtime-vapor/src/apiDefineAsyncComponent.ts diff --git a/packages/runtime-core/src/apiAsyncComponent.ts b/packages/runtime-core/src/apiAsyncComponent.ts index 07e7fc67fef..8eee833ac4b 100644 --- a/packages/runtime-core/src/apiAsyncComponent.ts +++ b/packages/runtime-core/src/apiAsyncComponent.ts @@ -12,7 +12,7 @@ import type { ComponentPublicInstance } from './componentPublicInstance' import { type VNode, createVNode } from './vnode' import { defineComponent } from './apiDefineComponent' import { warn } from './warning' -import { ref } from '@vue/reactivity' +import { type Ref, ref } from '@vue/reactivity' import { ErrorCodes, handleError } from './errorHandling' import { isKeepAlive } from './components/KeepAlive' import { markAsyncBoundary } from './helpers/useId' @@ -24,10 +24,10 @@ export type AsyncComponentLoader = () => Promise< AsyncComponentResolveResult > -export interface AsyncComponentOptions { +export interface AsyncComponentOptions { loader: AsyncComponentLoader - loadingComponent?: Component - errorComponent?: Component + loadingComponent?: C + errorComponent?: C delay?: number timeout?: number suspensible?: boolean @@ -46,75 +46,20 @@ export const isAsyncWrapper = (i: GenericComponentInstance | VNode): boolean => /*! #__NO_SIDE_EFFECTS__ */ export function defineAsyncComponent< T extends Component = { new (): ComponentPublicInstance }, ->(source: AsyncComponentLoader | AsyncComponentOptions): T { - if (isFunction(source)) { - source = { loader: source } - } - +>(source: AsyncComponentLoader | AsyncComponentOptions): T { const { - loader, - loadingComponent, - errorComponent, - delay = 200, - hydrate: hydrateStrategy, - timeout, // undefined = never times out - suspensible = true, - onError: userOnError, - } = source - - let pendingRequest: Promise | null = null - let resolvedComp: ConcreteComponent | undefined - - let retries = 0 - const retry = () => { - retries++ - pendingRequest = null - return load() - } - - const load = (): Promise => { - let thisRequest: Promise - return ( - pendingRequest || - (thisRequest = pendingRequest = - loader() - .catch(err => { - err = err instanceof Error ? err : new Error(String(err)) - if (userOnError) { - return new Promise((resolve, reject) => { - const userRetry = () => resolve(retry()) - const userFail = () => reject(err) - userOnError(err, userRetry, userFail, retries + 1) - }) - } else { - throw err - } - }) - .then((comp: any) => { - if (thisRequest !== pendingRequest && pendingRequest) { - return pendingRequest - } - if (__DEV__ && !comp) { - warn( - `Async component loader resolved to undefined. ` + - `If you are using retry(), make sure to return its return value.`, - ) - } - // interop module default - if ( - comp && - (comp.__esModule || comp[Symbol.toStringTag] === 'Module') - ) { - comp = comp.default - } - if (__DEV__ && comp && !isObject(comp) && !isFunction(comp)) { - throw new Error(`Invalid async component load result: ${comp}`) - } - resolvedComp = comp - return comp - })) - ) - } + load, + getResolvedComp, + setPendingRequest, + source: { + loadingComponent, + errorComponent, + delay, + hydrate: hydrateStrategy, + timeout, + suspensible = true, + }, + } = createAsyncComponentContext(source) return defineComponent({ name: 'AsyncComponentWrapper', @@ -132,7 +77,7 @@ export function defineAsyncComponent< } } : hydrate - if (resolvedComp) { + if (getResolvedComp()) { doHydrate() } else { load().then(() => !instance.isUnmounted && doHydrate()) @@ -140,7 +85,7 @@ export function defineAsyncComponent< }, get __asyncResolved() { - return resolvedComp + return getResolvedComp() }, setup() { @@ -148,12 +93,13 @@ export function defineAsyncComponent< markAsyncBoundary(instance) // already resolved + let resolvedComp = getResolvedComp() if (resolvedComp) { return () => createInnerComp(resolvedComp!, instance) } const onError = (err: Error) => { - pendingRequest = null + setPendingRequest(null) handleError( err, instance, @@ -182,27 +128,11 @@ export function defineAsyncComponent< }) } - const loaded = ref(false) - const error = ref() - const delayed = ref(!!delay) - - if (delay) { - setTimeout(() => { - delayed.value = false - }, delay) - } - - if (timeout != null) { - setTimeout(() => { - if (!loaded.value && !error.value) { - const err = new Error( - `Async component timed out after ${timeout}ms.`, - ) - onError(err) - error.value = err - } - }, timeout) - } + const { loaded, error, delayed } = useAsyncComponentState( + delay, + timeout, + onError, + ) load() .then(() => { @@ -223,6 +153,7 @@ export function defineAsyncComponent< }) return () => { + resolvedComp = getResolvedComp() if (loaded.value && resolvedComp) { return createInnerComp(resolvedComp, instance) } else if (error.value && errorComponent) { @@ -252,3 +183,114 @@ function createInnerComp( return vnode } + +type AsyncComponentContext = { + load: () => Promise + source: AsyncComponentOptions + getResolvedComp: () => C | undefined + setPendingRequest: (request: Promise | null) => void +} + +// shared between core and vapor +export function createAsyncComponentContext( + source: AsyncComponentLoader | AsyncComponentOptions, +): AsyncComponentContext { + if (isFunction(source)) { + source = { loader: source } + } + + const { loader, onError: userOnError } = source + let pendingRequest: Promise | null = null + let resolvedComp: C | undefined + + let retries = 0 + const retry = () => { + retries++ + pendingRequest = null + return load() + } + + const load = (): Promise => { + let thisRequest: Promise + return ( + pendingRequest || + (thisRequest = pendingRequest = + loader() + .catch(err => { + err = err instanceof Error ? err : new Error(String(err)) + if (userOnError) { + return new Promise((resolve, reject) => { + const userRetry = () => resolve(retry()) + const userFail = () => reject(err) + userOnError(err, userRetry, userFail, retries + 1) + }) + } else { + throw err + } + }) + .then((comp: any) => { + if (thisRequest !== pendingRequest && pendingRequest) { + return pendingRequest + } + if (__DEV__ && !comp) { + warn( + `Async component loader resolved to undefined. ` + + `If you are using retry(), make sure to return its return value.`, + ) + } + if ( + comp && + (comp.__esModule || comp[Symbol.toStringTag] === 'Module') + ) { + comp = comp.default + } + if (__DEV__ && comp && !isObject(comp) && !isFunction(comp)) { + throw new Error(`Invalid async component load result: ${comp}`) + } + resolvedComp = comp + return comp + })) + ) + } + + return { + load, + source, + getResolvedComp: () => resolvedComp, + setPendingRequest: (request: Promise | null) => + (pendingRequest = request), + } +} + +// shared between core and vapor +export const useAsyncComponentState = ( + delay: number | undefined, + timeout: number | undefined, + onError: (err: Error) => void, +): { + loaded: Ref + error: Ref + delayed: Ref +} => { + const loaded = ref(false) + const error = ref() + const delayed = ref(!!delay) + + if (delay) { + setTimeout(() => { + delayed.value = false + }, delay) + } + + if (timeout != null) { + setTimeout(() => { + if (!loaded.value && !error.value) { + const err = new Error(`Async component timed out after ${timeout}ms.`) + onError(err) + error.value = err + } + }, timeout) + } + + return { loaded, error, delayed } +} diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index c7150e38e80..e21b179a219 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -557,3 +557,14 @@ export { startMeasure, endMeasure } from './profiling' * @internal */ export { initFeatureFlags } from './featureFlags' +/** + * @internal + */ +export { + createAsyncComponentContext, + useAsyncComponentState, +} from './apiAsyncComponent' +/** + * @internal + */ +export { markAsyncBoundary } from './helpers/useId' diff --git a/packages/runtime-vapor/__tests__/apiDefineAsyncComponent.spec.ts b/packages/runtime-vapor/__tests__/apiDefineAsyncComponent.spec.ts new file mode 100644 index 00000000000..ef16160b5df --- /dev/null +++ b/packages/runtime-vapor/__tests__/apiDefineAsyncComponent.spec.ts @@ -0,0 +1,48 @@ +import { nextTick, ref } from '@vue/runtime-dom' +import { type VaporComponent, createComponent } from '../src/component' +import { defineVaporAsyncComponent } from '../src/apiDefineAsyncComponent' +import { makeRender } from './_utils' +import { createIf, template } from '@vue/runtime-vapor' + +const timeout = (n: number = 0) => new Promise(r => setTimeout(r, n)) + +const define = makeRender() + +describe('api: defineAsyncComponent', () => { + test('simple usage', async () => { + let resolve: (comp: VaporComponent) => void + const Foo = defineVaporAsyncComponent( + () => + new Promise(r => { + resolve = r as any + }), + ) + + const toggle = ref(true) + const { html } = define({ + setup() { + return createIf( + () => toggle.value, + () => { + return createComponent(Foo) + }, + ) + }, + }).render() + + expect(html()).toBe('') + resolve!(() => template('resolved')()) + + await timeout() + expect(html()).toBe('resolved') + + toggle.value = false + await nextTick() + expect(html()).toBe('') + + // already resolved component should update on nextTick + toggle.value = true + await nextTick() + expect(html()).toBe('resolved') + }) +}) diff --git a/packages/runtime-vapor/src/apiDefineAsyncComponent.ts b/packages/runtime-vapor/src/apiDefineAsyncComponent.ts new file mode 100644 index 00000000000..3de8e3f2ab3 --- /dev/null +++ b/packages/runtime-vapor/src/apiDefineAsyncComponent.ts @@ -0,0 +1,121 @@ +import { + type AsyncComponentLoader, + type AsyncComponentOptions, + ErrorCodes, + createAsyncComponentContext, + currentInstance, + handleError, + markAsyncBoundary, + useAsyncComponentState, +} from '@vue/runtime-dom' +import { defineVaporComponent } from './apiDefineComponent' +import { + type VaporComponent, + type VaporComponentInstance, + createComponent, +} from './component' +import { DynamicFragment } from './block' +import { renderEffect } from './renderEffect' + +/*! #__NO_SIDE_EFFECTS__ */ +export function defineVaporAsyncComponent( + source: AsyncComponentLoader | AsyncComponentOptions, +): T { + const { + load, + getResolvedComp, + setPendingRequest, + source: { + loadingComponent, + errorComponent, + delay, + // hydrate: hydrateStrategy, + timeout, + // suspensible = true, + }, + } = createAsyncComponentContext(source) + + return defineVaporComponent({ + name: 'VaporAsyncComponentWrapper', + + // @ts-expect-error + __asyncLoader: load, + + // __asyncHydrate(el, instance, hydrate) { + // // TODO async hydrate + // }, + + get __asyncResolved() { + return getResolvedComp() + }, + + setup() { + const instance = currentInstance as VaporComponentInstance + markAsyncBoundary(instance) + + const frag = __DEV__ + ? new DynamicFragment('async component') + : new DynamicFragment() + + // already resolved + let resolvedComp = getResolvedComp() + if (resolvedComp) { + frag.update(() => createInnerComp(resolvedComp!, instance)) + return frag + } + + const onError = (err: Error) => { + setPendingRequest(null) + handleError( + err, + instance, + ErrorCodes.ASYNC_COMPONENT_LOADER, + !errorComponent /* do not throw in dev if user provided error component */, + ) + } + + // TODO suspense-controlled or SSR. + + const { loaded, error, delayed } = useAsyncComponentState( + delay, + timeout, + onError, + ) + + load() + .then(() => { + loaded.value = true + // TODO parent is keep-alive, force update so the loaded component's + // name is taken into account + }) + .catch(err => { + onError(err) + error.value = err + }) + + renderEffect(() => { + resolvedComp = getResolvedComp() + let blockFn + if (loaded.value && resolvedComp) { + blockFn = () => createInnerComp(resolvedComp!, instance) + } else if (error.value && errorComponent) { + blockFn = () => + createComponent(errorComponent, { error: () => error.value }) + } else if (loadingComponent && !delayed.value) { + blockFn = () => createComponent(loadingComponent) + } + frag.update(blockFn) + }) + + return frag + }, + }) as T +} + +function createInnerComp( + comp: VaporComponent, + parent: VaporComponentInstance, +): VaporComponentInstance { + const { rawProps, rawSlots, isSingleRoot, appContext } = parent + return createComponent(comp, rawProps, rawSlots, isSingleRoot, appContext) +} diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index 682532fa4d8..7cd81c3e102 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -1,6 +1,7 @@ // public APIs export { createVaporApp, createVaporSSRApp } from './apiCreateApp' export { defineVaporComponent } from './apiDefineComponent' +export { defineVaporAsyncComponent } from './apiDefineAsyncComponent' export { vaporInteropPlugin } from './vdomInterop' export type { VaporDirective } from './directives/custom' From 58cfb6634165a06416e987d34f864efdadbcd4b3 Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 18 Mar 2025 15:24:45 +0800 Subject: [PATCH 2/9] test: port tests --- .../__tests__/apiDefineAsyncComponent.spec.ts | 652 +++++++++++++++++- 1 file changed, 651 insertions(+), 1 deletion(-) diff --git a/packages/runtime-vapor/__tests__/apiDefineAsyncComponent.spec.ts b/packages/runtime-vapor/__tests__/apiDefineAsyncComponent.spec.ts index ef16160b5df..4c76d8dbc99 100644 --- a/packages/runtime-vapor/__tests__/apiDefineAsyncComponent.spec.ts +++ b/packages/runtime-vapor/__tests__/apiDefineAsyncComponent.spec.ts @@ -2,7 +2,7 @@ import { nextTick, ref } from '@vue/runtime-dom' import { type VaporComponent, createComponent } from '../src/component' import { defineVaporAsyncComponent } from '../src/apiDefineAsyncComponent' import { makeRender } from './_utils' -import { createIf, template } from '@vue/runtime-vapor' +import { createIf, createTemplateRefSetter, template } from '@vue/runtime-vapor' const timeout = (n: number = 0) => new Promise(r => setTimeout(r, n)) @@ -45,4 +45,654 @@ describe('api: defineAsyncComponent', () => { await nextTick() expect(html()).toBe('resolved') }) + + test('with loading component', async () => { + let resolve: (comp: VaporComponent) => void + const Foo = defineVaporAsyncComponent({ + loader: () => + new Promise(r => { + resolve = r as any + }), + loadingComponent: () => template('loading')(), + delay: 1, // defaults to 200 + }) + + const toggle = ref(true) + const { html } = define({ + setup() { + return createIf( + () => toggle.value, + () => { + return createComponent(Foo) + }, + ) + }, + }).render() + + // due to the delay, initial mount should be empty + expect(html()).toBe('') + + // loading show up after delay + await timeout(1) + expect(html()).toBe('loading') + + resolve!(() => template('resolved')()) + await timeout() + expect(html()).toBe('resolved') + + toggle.value = false + await nextTick() + expect(html()).toBe('') + + // already resolved component should update on nextTick without loading + // state + toggle.value = true + await nextTick() + expect(html()).toBe('resolved') + }) + + test('with loading component + explicit delay (0)', async () => { + let resolve: (comp: VaporComponent) => void + const Foo = defineVaporAsyncComponent({ + loader: () => + new Promise(r => { + resolve = r as any + }), + loadingComponent: () => template('loading')(), + delay: 0, + }) + + const toggle = ref(true) + const { html } = define({ + setup() { + return createIf( + () => toggle.value, + () => { + return createComponent(Foo) + }, + ) + }, + }).render() + + // with delay: 0, should show loading immediately + expect(html()).toBe('loading') + + resolve!(() => template('resolved')()) + await timeout() + expect(html()).toBe('resolved') + + toggle.value = false + await nextTick() + expect(html()).toBe('') + + // already resolved component should update on nextTick without loading + // state + toggle.value = true + await nextTick() + expect(html()).toBe('resolved') + }) + + test('error without error component', async () => { + let resolve: (comp: VaporComponent) => void + let reject: (e: Error) => void + const Foo = defineVaporAsyncComponent( + () => + new Promise((_resolve, _reject) => { + resolve = _resolve as any + reject = _reject + }), + ) + + const toggle = ref(true) + const { app, mount } = define({ + setup() { + return createIf( + () => toggle.value, + () => { + return createComponent(Foo) + }, + ) + }, + }).create() + + const handler = (app.config.errorHandler = vi.fn()) + const root = document.createElement('div') + mount(root) + expect(root.innerHTML).toBe('') + + const err = new Error('foo') + reject!(err) + await timeout() + expect(handler).toHaveBeenCalled() + expect(handler.mock.calls[0][0]).toBe(err) + expect(root.innerHTML).toBe('') + + toggle.value = false + await nextTick() + expect(root.innerHTML).toBe('') + + // errored out on previous load, toggle and mock success this time + toggle.value = true + await nextTick() + expect(root.innerHTML).toBe('') + + // should render this time + resolve!(() => template('resolved')()) + await timeout() + expect(root.innerHTML).toBe('resolved') + }) + + test('error with error component', async () => { + let resolve: (comp: VaporComponent) => void + let reject: (e: Error) => void + const Foo = defineVaporAsyncComponent({ + loader: () => + new Promise((_resolve, _reject) => { + resolve = _resolve as any + reject = _reject + }), + errorComponent: (props: { error: Error }) => + template(props.error.message)(), + }) + + const toggle = ref(true) + const { app, mount } = define({ + setup() { + return createIf( + () => toggle.value, + () => { + return createComponent(Foo) + }, + ) + }, + }).create() + const handler = (app.config.errorHandler = vi.fn()) + const root = document.createElement('div') + mount(root) + expect(root.innerHTML).toBe('') + + const err = new Error('errored out') + reject!(err) + await timeout() + expect(handler).toHaveBeenCalled() + expect(root.innerHTML).toBe('errored out') + + toggle.value = false + await nextTick() + expect(root.innerHTML).toBe('') + + // errored out on previous load, toggle and mock success this time + toggle.value = true + await nextTick() + expect(root.innerHTML).toBe('') + + // should render this time + resolve!(() => template('resolved')()) + await timeout() + expect(root.innerHTML).toBe('resolved') + }) + + test('error with error component, without global handler', async () => { + let resolve: (comp: VaporComponent) => void + let reject: (e: Error) => void + const Foo = defineVaporAsyncComponent({ + loader: () => + new Promise((_resolve, _reject) => { + resolve = _resolve as any + reject = _reject + }), + errorComponent: (props: { error: Error }) => + template(props.error.message)(), + }) + + const toggle = ref(true) + const { mount } = define({ + setup() { + return createIf( + () => toggle.value, + () => { + return createComponent(Foo) + }, + ) + }, + }).create() + const root = document.createElement('div') + mount(root) + expect(root.innerHTML).toBe('') + + const err = new Error('errored out') + reject!(err) + await timeout() + expect(root.innerHTML).toBe('errored out') + expect( + 'Unhandled error during execution of async component loader', + ).toHaveBeenWarned() + + toggle.value = false + await nextTick() + expect(root.innerHTML).toBe('') + + // errored out on previous load, toggle and mock success this time + toggle.value = true + await nextTick() + expect(root.innerHTML).toBe('') + + // should render this time + resolve!(() => template('resolved')()) + await timeout() + expect(root.innerHTML).toBe('resolved') + }) + + test('error with error + loading components', async () => { + let resolve: (comp: VaporComponent) => void + let reject: (e: Error) => void + const Foo = defineVaporAsyncComponent({ + loader: () => + new Promise((_resolve, _reject) => { + resolve = _resolve as any + reject = _reject + }), + errorComponent: (props: { error: Error }) => + template(props.error.message)(), + loadingComponent: () => template('loading')(), + delay: 1, + }) + + const toggle = ref(true) + const { app, mount } = define({ + setup() { + return createIf( + () => toggle.value, + () => { + return createComponent(Foo) + }, + ) + }, + }).create() + const handler = (app.config.errorHandler = vi.fn()) + const root = document.createElement('div') + mount(root) + + // due to the delay, initial mount should be empty + expect(root.innerHTML).toBe('') + + // loading show up after delay + await timeout(1) + expect(root.innerHTML).toBe('loading') + + const err = new Error('errored out') + reject!(err) + await timeout() + expect(handler).toHaveBeenCalled() + expect(root.innerHTML).toBe('errored out') + + toggle.value = false + await nextTick() + expect(root.innerHTML).toBe('') + + // errored out on previous load, toggle and mock success this time + toggle.value = true + await nextTick() + expect(root.innerHTML).toBe('') + + // loading show up after delay + await timeout(1) + expect(root.innerHTML).toBe('loading') + + // should render this time + resolve!(() => template('resolved')()) + await timeout() + expect(root.innerHTML).toBe('resolved') + }) + + test('timeout without error component', async () => { + let resolve: (comp: VaporComponent) => void + const Foo = defineVaporAsyncComponent({ + loader: () => + new Promise(_resolve => { + resolve = _resolve as any + }), + timeout: 1, + }) + + const { app, mount } = define({ + setup() { + return createComponent(Foo) + }, + }).create() + const handler = vi.fn() + app.config.errorHandler = handler + + const root = document.createElement('div') + mount(root) + expect(root.innerHTML).toBe('') + + await timeout(1) + expect(handler).toHaveBeenCalled() + expect(handler.mock.calls[0][0].message).toMatch( + `Async component timed out after 1ms.`, + ) + expect(root.innerHTML).toBe('') + + // if it resolved after timeout, should still work + resolve!(() => template('resolved')()) + await timeout() + expect(root.innerHTML).toBe('resolved') + }) + + test('timeout with error component', async () => { + let resolve: (comp: VaporComponent) => void + const Foo = defineVaporAsyncComponent({ + loader: () => + new Promise(_resolve => { + resolve = _resolve as any + }), + timeout: 1, + errorComponent: () => template('timed out')(), + }) + + const root = document.createElement('div') + const { app, mount } = define({ + setup() { + return createComponent(Foo) + }, + }).create() + + const handler = (app.config.errorHandler = vi.fn()) + mount(root) + expect(root.innerHTML).toBe('') + + await timeout(1) + expect(handler).toHaveBeenCalled() + expect(root.innerHTML).toBe('timed out') + + // if it resolved after timeout, should still work + resolve!(() => template('resolved')()) + await timeout() + expect(root.innerHTML).toBe('resolved') + }) + + test('timeout with error + loading components', async () => { + let resolve: (comp: VaporComponent) => void + const Foo = defineVaporAsyncComponent({ + loader: () => + new Promise(_resolve => { + resolve = _resolve as any + }), + delay: 1, + timeout: 16, + errorComponent: () => template('timed out')(), + loadingComponent: () => template('loading')(), + }) + + const root = document.createElement('div') + const { app, mount } = define({ + setup() { + return createComponent(Foo) + }, + }).create() + const handler = (app.config.errorHandler = vi.fn()) + mount(root) + expect(root.innerHTML).toBe('') + await timeout(1) + expect(root.innerHTML).toBe('loading') + + await timeout(16) + expect(root.innerHTML).toBe('timed out') + expect(handler).toHaveBeenCalled() + + resolve!(() => template('resolved')()) + await timeout() + expect(root.innerHTML).toBe('resolved') + }) + + test('timeout without error component, but with loading component', async () => { + let resolve: (comp: VaporComponent) => void + const Foo = defineVaporAsyncComponent({ + loader: () => + new Promise(_resolve => { + resolve = _resolve as any + }), + delay: 1, + timeout: 16, + loadingComponent: () => template('loading')(), + }) + + const root = document.createElement('div') + const { app, mount } = define({ + setup() { + return createComponent(Foo) + }, + }).create() + const handler = vi.fn() + app.config.errorHandler = handler + mount(root) + expect(root.innerHTML).toBe('') + await timeout(1) + expect(root.innerHTML).toBe('loading') + + await timeout(16) + expect(handler).toHaveBeenCalled() + expect(handler.mock.calls[0][0].message).toMatch( + `Async component timed out after 16ms.`, + ) + // should still display loading + expect(root.innerHTML).toBe('loading') + + resolve!(() => template('resolved')()) + await timeout() + expect(root.innerHTML).toBe('resolved') + }) + + test.todo('with suspense', async () => {}) + + test.todo('suspensible: false', async () => {}) + + test.todo('suspense with error handling', async () => {}) + + test('retry (success)', async () => { + let loaderCallCount = 0 + let resolve: (comp: VaporComponent) => void + let reject: (e: Error) => void + + const Foo = defineVaporAsyncComponent({ + loader: () => { + loaderCallCount++ + return new Promise((_resolve, _reject) => { + resolve = _resolve as any + reject = _reject + }) + }, + onError(error, retry, fail) { + if (error.message.match(/foo/)) { + retry() + } else { + fail() + } + }, + }) + + const root = document.createElement('div') + const { app, mount } = define({ + setup() { + return createComponent(Foo) + }, + }).create() + + const handler = (app.config.errorHandler = vi.fn()) + mount(root) + expect(root.innerHTML).toBe('') + expect(loaderCallCount).toBe(1) + + const err = new Error('foo') + reject!(err) + await timeout() + expect(handler).not.toHaveBeenCalled() + expect(loaderCallCount).toBe(2) + expect(root.innerHTML).toBe('') + + // should render this time + resolve!(() => template('resolved')()) + await timeout() + expect(handler).not.toHaveBeenCalled() + expect(root.innerHTML).toBe('resolved') + }) + + test('retry (skipped)', async () => { + let loaderCallCount = 0 + let reject: (e: Error) => void + + const Foo = defineVaporAsyncComponent({ + loader: () => { + loaderCallCount++ + return new Promise((_resolve, _reject) => { + reject = _reject + }) + }, + onError(error, retry, fail) { + if (error.message.match(/bar/)) { + retry() + } else { + fail() + } + }, + }) + + const root = document.createElement('div') + const { app, mount } = define({ + setup() { + return createComponent(Foo) + }, + }).create() + + const handler = (app.config.errorHandler = vi.fn()) + mount(root) + expect(root.innerHTML).toBe('') + expect(loaderCallCount).toBe(1) + + const err = new Error('foo') + reject!(err) + await timeout() + // should fail because retryWhen returns false + expect(handler).toHaveBeenCalled() + expect(handler.mock.calls[0][0]).toBe(err) + expect(loaderCallCount).toBe(1) + expect(root.innerHTML).toBe('') + }) + + test('retry (fail w/ max retry attempts)', async () => { + let loaderCallCount = 0 + let reject: (e: Error) => void + + const Foo = defineVaporAsyncComponent({ + loader: () => { + loaderCallCount++ + return new Promise((_resolve, _reject) => { + reject = _reject + }) + }, + onError(error, retry, fail, attempts) { + if (error.message.match(/foo/) && attempts <= 1) { + retry() + } else { + fail() + } + }, + }) + + const root = document.createElement('div') + const { app, mount } = define({ + setup() { + return createComponent(Foo) + }, + }).create() + + const handler = (app.config.errorHandler = vi.fn()) + mount(root) + expect(root.innerHTML).toBe('') + expect(loaderCallCount).toBe(1) + + // first retry + const err = new Error('foo') + reject!(err) + await timeout() + expect(handler).not.toHaveBeenCalled() + expect(loaderCallCount).toBe(2) + expect(root.innerHTML).toBe('') + + // 2nd retry, should fail due to reaching maxRetries + reject!(err) + await timeout() + expect(handler).toHaveBeenCalled() + expect(handler.mock.calls[0][0]).toBe(err) + expect(loaderCallCount).toBe(2) + expect(root.innerHTML).toBe('') + }) + + test.todo('template ref forwarding', async () => { + let resolve: (comp: VaporComponent) => void + const Foo = defineVaporAsyncComponent( + () => + new Promise(r => { + resolve = r as any + }), + ) + + const fooRef = ref(null) + const toggle = ref(true) + const root = document.createElement('div') + const { mount } = define({ + setup() { + return { fooRef, toggle } + }, + render() { + return createIf( + () => toggle.value, + () => { + const setTemplateRef = createTemplateRefSetter() + const n0 = createComponent(Foo, null, null, true) + setTemplateRef(n0, 'fooRef') + return n0 + }, + ) + }, + }).create() + mount(root) + expect(root.innerHTML).toBe('') + expect(fooRef.value).toBe(null) + + resolve!({ + setup: (props, { expose }) => { + expose({ + id: 'foo', + }) + return template('resolved')() + }, + }) + // first time resolve, wait for macro task since there are multiple + // microtasks / .then() calls + await timeout() + expect(root.innerHTML).toBe('resolved') + expect(fooRef.value.id).toBe('foo') + + toggle.value = false + await nextTick() + expect(root.innerHTML).toBe('') + expect(fooRef.value).toBe(null) + + // already resolved component should update on nextTick + toggle.value = true + await nextTick() + expect(root.innerHTML).toBe('resolved') + expect(fooRef.value.id).toBe('foo') + }) + + test.todo( + 'the forwarded template ref should always exist when doing multi patching', + async () => {}, + ) + + test.todo('with KeepAlive', async () => {}) + + test.todo('with KeepAlive + include', async () => {}) }) From 367924ca9e4d8b14f3ac08a9db2e016944709e5f Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 18 Mar 2025 17:17:53 +0800 Subject: [PATCH 3/9] wip: handle template ref forwarding --- packages/runtime-core/src/index.ts | 1 + .../__tests__/apiDefineAsyncComponent.spec.ts | 2 +- .../src/apiDefineAsyncComponent.ts | 15 ++++++++++++--- packages/runtime-vapor/src/apiTemplateRef.ts | 18 +++++++++++++++++- packages/runtime-vapor/src/block.ts | 2 ++ packages/runtime-vapor/src/component.ts | 2 ++ 6 files changed, 35 insertions(+), 5 deletions(-) diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index e21b179a219..62677d732b8 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -563,6 +563,7 @@ export { initFeatureFlags } from './featureFlags' export { createAsyncComponentContext, useAsyncComponentState, + isAsyncWrapper, } from './apiAsyncComponent' /** * @internal diff --git a/packages/runtime-vapor/__tests__/apiDefineAsyncComponent.spec.ts b/packages/runtime-vapor/__tests__/apiDefineAsyncComponent.spec.ts index 4c76d8dbc99..b488b33dca7 100644 --- a/packages/runtime-vapor/__tests__/apiDefineAsyncComponent.spec.ts +++ b/packages/runtime-vapor/__tests__/apiDefineAsyncComponent.spec.ts @@ -629,7 +629,7 @@ describe('api: defineAsyncComponent', () => { expect(root.innerHTML).toBe('') }) - test.todo('template ref forwarding', async () => { + test('template ref forwarding', async () => { let resolve: (comp: VaporComponent) => void const Foo = defineVaporAsyncComponent( () => diff --git a/packages/runtime-vapor/src/apiDefineAsyncComponent.ts b/packages/runtime-vapor/src/apiDefineAsyncComponent.ts index 3de8e3f2ab3..1a97731cb0e 100644 --- a/packages/runtime-vapor/src/apiDefineAsyncComponent.ts +++ b/packages/runtime-vapor/src/apiDefineAsyncComponent.ts @@ -38,7 +38,6 @@ export function defineVaporAsyncComponent( return defineVaporComponent({ name: 'VaporAsyncComponentWrapper', - // @ts-expect-error __asyncLoader: load, // __asyncHydrate(el, instance, hydrate) { @@ -97,7 +96,7 @@ export function defineVaporAsyncComponent( resolvedComp = getResolvedComp() let blockFn if (loaded.value && resolvedComp) { - blockFn = () => createInnerComp(resolvedComp!, instance) + blockFn = () => createInnerComp(resolvedComp!, instance, frag) } else if (error.value && errorComponent) { blockFn = () => createComponent(errorComponent, { error: () => error.value }) @@ -115,7 +114,17 @@ export function defineVaporAsyncComponent( function createInnerComp( comp: VaporComponent, parent: VaporComponentInstance, + frag?: DynamicFragment, ): VaporComponentInstance { const { rawProps, rawSlots, isSingleRoot, appContext } = parent - return createComponent(comp, rawProps, rawSlots, isSingleRoot, appContext) + const i = createComponent(comp, rawProps, rawSlots, isSingleRoot, appContext) + // set ref + frag && frag.setRef && frag.setRef(i) + + // TODO custom element + // pass the custom element callback on to the inner comp + // and remove it from the async wrapper + // i.ce = ce + // delete parent.ce + return i } diff --git a/packages/runtime-vapor/src/apiTemplateRef.ts b/packages/runtime-vapor/src/apiTemplateRef.ts index c5a6c5fb2b6..948d008535b 100644 --- a/packages/runtime-vapor/src/apiTemplateRef.ts +++ b/packages/runtime-vapor/src/apiTemplateRef.ts @@ -7,8 +7,10 @@ import { } from './component' import { ErrorCodes, + type GenericComponentInstance, type SchedulerJob, callWithErrorHandling, + isAsyncWrapper, queuePostFlushCb, warn, } from '@vue/runtime-dom' @@ -20,6 +22,7 @@ import { isString, remove, } from '@vue/shared' +import type { DynamicFragment } from './block' export type NodeRef = string | Ref | ((ref: Element) => void) export type RefEl = Element | VaporComponentInstance @@ -47,9 +50,22 @@ export function setRef( refFor = false, ): NodeRef | undefined { if (!instance || instance.isUnmounted) return + const isVaporComp = isVaporComponent(el) + if (isVaporComp && isAsyncWrapper(el as GenericComponentInstance)) { + if (!(el as VaporComponentInstance).type.__asyncResolved) { + const frag = (el as VaporComponentInstance).block as DynamicFragment + frag.setRef = (el: RefEl) => setRef(instance, el, ref, oldRef, refFor) + return + } else { + el = ((el as VaporComponentInstance).block as DynamicFragment) + .nodes as RefEl + } + } const setupState: any = __DEV__ ? instance.setupState || {} : null - const refValue = isVaporComponent(el) ? getExposed(el) || el : el + const refValue = isVaporComp + ? getExposed(el as VaporComponentInstance) || el + : el const refs = instance.refs === EMPTY_OBJ ? (instance.refs = {}) : instance.refs diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index b782afd38d3..a0bdcd94659 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -8,6 +8,7 @@ import { import { createComment, createTextNode } from './dom/node' import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity' import { isHydrating } from './dom/hydration' +import type { RefEl } from './apiTemplateRef' export type Block = | Node @@ -23,6 +24,7 @@ export class VaporFragment { anchor?: Node insert?: (parent: ParentNode, anchor: Node | null) => void remove?: (parent?: ParentNode) => void + setRef?: (el: RefEl) => void constructor(nodes: Block) { this.nodes = nodes diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 548babebf8b..4dfc6a09b1d 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -92,6 +92,8 @@ export interface ObjectVaporComponent name?: string vapor?: boolean + __asyncLoader?: () => Promise + __asyncResolved?: VaporComponent } interface SharedInternalOptions { From cbdfdabdf2fa9f5915a8a2f7cc252c04f4b1df9f Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 18 Mar 2025 17:30:57 +0800 Subject: [PATCH 4/9] wip: update --- packages/runtime-vapor/src/apiTemplateRef.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/runtime-vapor/src/apiTemplateRef.ts b/packages/runtime-vapor/src/apiTemplateRef.ts index 948d008535b..e5a58537a9a 100644 --- a/packages/runtime-vapor/src/apiTemplateRef.ts +++ b/packages/runtime-vapor/src/apiTemplateRef.ts @@ -7,7 +7,6 @@ import { } from './component' import { ErrorCodes, - type GenericComponentInstance, type SchedulerJob, callWithErrorHandling, isAsyncWrapper, @@ -50,16 +49,19 @@ export function setRef( refFor = false, ): NodeRef | undefined { if (!instance || instance.isUnmounted) return + const isVaporComp = isVaporComponent(el) - if (isVaporComp && isAsyncWrapper(el as GenericComponentInstance)) { - if (!(el as VaporComponentInstance).type.__asyncResolved) { - const frag = (el as VaporComponentInstance).block as DynamicFragment - frag.setRef = (el: RefEl) => setRef(instance, el, ref, oldRef, refFor) + if (isVaporComp && isAsyncWrapper(el as VaporComponentInstance)) { + const i = el as VaporComponentInstance + const frag = i.block as DynamicFragment + // async component not resolved yet + if (!i.type.__asyncResolved) { + frag.setRef = n => setRef(instance, n, ref, oldRef, refFor) return - } else { - el = ((el as VaporComponentInstance).block as DynamicFragment) - .nodes as RefEl } + + // set ref to the inner component instead + el = frag.nodes as VaporComponentInstance } const setupState: any = __DEV__ ? instance.setupState || {} : null From 786732459844a472ba56d64855044aee88e3c8fe Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 18 Mar 2025 20:48:30 +0800 Subject: [PATCH 5/9] wip: add more tests --- .../__tests__/apiDefineAsyncComponent.spec.ts | 88 ++++++++++++++++--- 1 file changed, 77 insertions(+), 11 deletions(-) diff --git a/packages/runtime-vapor/__tests__/apiDefineAsyncComponent.spec.ts b/packages/runtime-vapor/__tests__/apiDefineAsyncComponent.spec.ts index b488b33dca7..fa7f481707c 100644 --- a/packages/runtime-vapor/__tests__/apiDefineAsyncComponent.spec.ts +++ b/packages/runtime-vapor/__tests__/apiDefineAsyncComponent.spec.ts @@ -2,7 +2,13 @@ import { nextTick, ref } from '@vue/runtime-dom' import { type VaporComponent, createComponent } from '../src/component' import { defineVaporAsyncComponent } from '../src/apiDefineAsyncComponent' import { makeRender } from './_utils' -import { createIf, createTemplateRefSetter, template } from '@vue/runtime-vapor' +import { + createIf, + createTemplateRefSetter, + renderEffect, + template, +} from '@vue/runtime-vapor' +import { setElementText } from '../src/dom/prop' const timeout = (n: number = 0) => new Promise(r => setTimeout(r, n)) @@ -484,12 +490,6 @@ describe('api: defineAsyncComponent', () => { expect(root.innerHTML).toBe('resolved') }) - test.todo('with suspense', async () => {}) - - test.todo('suspensible: false', async () => {}) - - test.todo('suspense with error handling', async () => {}) - test('retry (success)', async () => { let loaderCallCount = 0 let resolve: (comp: VaporComponent) => void @@ -687,10 +687,76 @@ describe('api: defineAsyncComponent', () => { expect(fooRef.value.id).toBe('foo') }) - test.todo( - 'the forwarded template ref should always exist when doing multi patching', - async () => {}, - ) + test('the forwarded template ref should always exist when doing multi patching', async () => { + let resolve: (comp: VaporComponent) => void + const Foo = defineVaporAsyncComponent( + () => + new Promise(r => { + resolve = r as any + }), + ) + + const fooRef = ref(null) + const toggle = ref(true) + const updater = ref(0) + + const root = document.createElement('div') + const { mount } = define({ + setup() { + return { fooRef, toggle, updater } + }, + render() { + return createIf( + () => toggle.value, + () => { + const setTemplateRef = createTemplateRefSetter() + const n0 = createComponent(Foo, null, null, true) + setTemplateRef(n0, 'fooRef') + const n1 = template(``)() + renderEffect(() => setElementText(n1, updater.value)) + return [n0, n1] + }, + ) + }, + }).create() + mount(root) + + expect(root.innerHTML).toBe('0') + expect(fooRef.value).toBe(null) + + resolve!({ + setup: (props, { expose }) => { + expose({ + id: 'foo', + }) + return template('resolved')() + }, + }) + + await timeout() + expect(root.innerHTML).toBe( + 'resolved0', + ) + expect(fooRef.value.id).toBe('foo') + + updater.value++ + await nextTick() + expect(root.innerHTML).toBe( + 'resolved1', + ) + expect(fooRef.value.id).toBe('foo') + + toggle.value = false + await nextTick() + expect(root.innerHTML).toBe('') + expect(fooRef.value).toBe(null) + }) + + test.todo('with suspense', async () => {}) + + test.todo('suspensible: false', async () => {}) + + test.todo('suspense with error handling', async () => {}) test.todo('with KeepAlive', async () => {}) From 9c6e49f9836d07435ecd6d36e6e11ec588695a6a Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 19 Mar 2025 08:40:55 +0800 Subject: [PATCH 6/9] chore: update --- .../src/apiDefineAsyncComponent.ts | 23 ++++++++++++------- packages/runtime-vapor/src/apiTemplateRef.ts | 2 +- packages/runtime-vapor/src/block.ts | 3 +-- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/runtime-vapor/src/apiDefineAsyncComponent.ts b/packages/runtime-vapor/src/apiDefineAsyncComponent.ts index 1a97731cb0e..ddd91c06c8b 100644 --- a/packages/runtime-vapor/src/apiDefineAsyncComponent.ts +++ b/packages/runtime-vapor/src/apiDefineAsyncComponent.ts @@ -94,16 +94,16 @@ export function defineVaporAsyncComponent( renderEffect(() => { resolvedComp = getResolvedComp() - let blockFn + let render if (loaded.value && resolvedComp) { - blockFn = () => createInnerComp(resolvedComp!, instance, frag) + render = () => createInnerComp(resolvedComp!, instance, frag) } else if (error.value && errorComponent) { - blockFn = () => + render = () => createComponent(errorComponent, { error: () => error.value }) } else if (loadingComponent && !delayed.value) { - blockFn = () => createComponent(loadingComponent) + render = () => createComponent(loadingComponent) } - frag.update(blockFn) + frag.update(render) }) return frag @@ -117,14 +117,21 @@ function createInnerComp( frag?: DynamicFragment, ): VaporComponentInstance { const { rawProps, rawSlots, isSingleRoot, appContext } = parent - const i = createComponent(comp, rawProps, rawSlots, isSingleRoot, appContext) + const instance = createComponent( + comp, + rawProps, + rawSlots, + isSingleRoot, + appContext, + ) + // set ref - frag && frag.setRef && frag.setRef(i) + frag && frag.setRef && frag.setRef(instance) // TODO custom element // pass the custom element callback on to the inner comp // and remove it from the async wrapper // i.ce = ce // delete parent.ce - return i + return instance } diff --git a/packages/runtime-vapor/src/apiTemplateRef.ts b/packages/runtime-vapor/src/apiTemplateRef.ts index e5a58537a9a..1ec0d65ef10 100644 --- a/packages/runtime-vapor/src/apiTemplateRef.ts +++ b/packages/runtime-vapor/src/apiTemplateRef.ts @@ -56,7 +56,7 @@ export function setRef( const frag = i.block as DynamicFragment // async component not resolved yet if (!i.type.__asyncResolved) { - frag.setRef = n => setRef(instance, n, ref, oldRef, refFor) + frag.setRef = i => setRef(instance, i, ref, oldRef, refFor) return } diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index a0bdcd94659..ca2b97a0681 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -8,7 +8,6 @@ import { import { createComment, createTextNode } from './dom/node' import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity' import { isHydrating } from './dom/hydration' -import type { RefEl } from './apiTemplateRef' export type Block = | Node @@ -24,7 +23,7 @@ export class VaporFragment { anchor?: Node insert?: (parent: ParentNode, anchor: Node | null) => void remove?: (parent?: ParentNode) => void - setRef?: (el: RefEl) => void + setRef?: (comp: VaporComponentInstance) => void constructor(nodes: Block) { this.nodes = nodes From 8b73edf0f0afc1cfa666f830e117be679f9fcc20 Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 19 Mar 2025 10:48:48 +0800 Subject: [PATCH 7/9] test: add e2e tests --- .../__tests__/vdomInterop.spec.ts | 30 +++++++++++++++---- .../vapor-e2e-test/interop/App.vue | 26 ++++++++++++++-- .../interop/{ => components}/VaporComp.vue | 13 +++----- .../interop/{ => components}/VdomComp.vue | 0 .../interop/components/VdomFoo.vue | 9 ++++++ packages/runtime-vapor/src/componentProps.ts | 2 +- 6 files changed, 63 insertions(+), 17 deletions(-) rename packages-private/vapor-e2e-test/interop/{ => components}/VaporComp.vue (80%) rename packages-private/vapor-e2e-test/interop/{ => components}/VdomComp.vue (100%) create mode 100644 packages-private/vapor-e2e-test/interop/components/VdomFoo.vue diff --git a/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts b/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts index 360f48085a1..a79c0d037cb 100644 --- a/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts +++ b/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts @@ -2,13 +2,15 @@ import path from 'node:path' import { E2E_TIMEOUT, setupPuppeteer, + timeout, } from '../../../packages/vue/__tests__/e2e/e2eUtils' import connect from 'connect' import sirv from 'sirv' +const { page, click, text, enterValue, html } = setupPuppeteer() -describe('vdom / vapor interop', () => { - const { page, click, text, enterValue } = setupPuppeteer() +const duration = process.env.CI ? 200 : 50 +describe('vdom / vapor interop', () => { let server: any const port = '8193' beforeAll(() => { @@ -22,12 +24,15 @@ describe('vdom / vapor interop', () => { server.close() }) + beforeEach(async () => { + const baseUrl = `http://localhost:${port}/interop/` + await page().goto(baseUrl) + await page().waitForSelector('#app') + }) + test( 'should work', async () => { - const baseUrl = `http://localhost:${port}/interop/` - await page().goto(baseUrl) - expect(await text('.vapor > h2')).toContain('Vapor component in VDOM') expect(await text('.vapor-prop')).toContain('hello') @@ -81,4 +86,19 @@ describe('vdom / vapor interop', () => { }, E2E_TIMEOUT, ) + + describe('async component', () => { + const container = '.async-component-interop' + test( + 'with-vdom-inner-component', + async () => { + const testContainer = `${container} .with-vdom-component` + expect(await html(testContainer)).toBe('loading...') + + await timeout(duration) + expect(await html(testContainer)).toBe('
foo
') + }, + E2E_TIMEOUT, + ) + }) }) diff --git a/packages-private/vapor-e2e-test/interop/App.vue b/packages-private/vapor-e2e-test/interop/App.vue index 772a6989dd7..4811d00e643 100644 --- a/packages-private/vapor-e2e-test/interop/App.vue +++ b/packages-private/vapor-e2e-test/interop/App.vue @@ -1,9 +1,23 @@ diff --git a/packages-private/vapor-e2e-test/interop/VaporComp.vue b/packages-private/vapor-e2e-test/interop/components/VaporComp.vue similarity index 80% rename from packages-private/vapor-e2e-test/interop/VaporComp.vue rename to packages-private/vapor-e2e-test/interop/components/VaporComp.vue index 88a60c782c0..4ebb58b9ce7 100644 --- a/packages-private/vapor-e2e-test/interop/VaporComp.vue +++ b/packages-private/vapor-e2e-test/interop/components/VaporComp.vue @@ -20,24 +20,19 @@ const slotProp = ref('slot prop')

vdom slots in vapor component

-
- #default: + #default: +
#test: fallback content
- diff --git a/packages-private/vapor-e2e-test/interop/VdomComp.vue b/packages-private/vapor-e2e-test/interop/components/VdomComp.vue similarity index 100% rename from packages-private/vapor-e2e-test/interop/VdomComp.vue rename to packages-private/vapor-e2e-test/interop/components/VdomComp.vue diff --git a/packages-private/vapor-e2e-test/interop/components/VdomFoo.vue b/packages-private/vapor-e2e-test/interop/components/VdomFoo.vue new file mode 100644 index 00000000000..0b15f81ca30 --- /dev/null +++ b/packages-private/vapor-e2e-test/interop/components/VdomFoo.vue @@ -0,0 +1,9 @@ + + + diff --git a/packages/runtime-vapor/src/componentProps.ts b/packages/runtime-vapor/src/componentProps.ts index a5e9daad229..fcff365c7a5 100644 --- a/packages/runtime-vapor/src/componentProps.ts +++ b/packages/runtime-vapor/src/componentProps.ts @@ -182,7 +182,7 @@ export function getAttrFromRawProps(rawProps: RawProps, key: string): unknown { source = dynamicSources[i] isDynamic = isFunction(source) source = isDynamic ? (source as Function)() : source - if (hasOwn(source, key)) { + if (source && hasOwn(source, key)) { const value = isDynamic ? source[key] : source[key]() if (merged) { merged.push(value) From ed9b36a25e604a1fa70577f87f9bb7c0aa93bc8a Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 19 Mar 2025 02:49:52 +0000 Subject: [PATCH 8/9] [autofix.ci] apply automated fixes --- .../vapor-e2e-test/interop/components/VaporComp.vue | 10 ++++++++-- .../vapor-e2e-test/interop/components/VdomFoo.vue | 8 ++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages-private/vapor-e2e-test/interop/components/VaporComp.vue b/packages-private/vapor-e2e-test/interop/components/VaporComp.vue index 4ebb58b9ce7..09b08154ae3 100644 --- a/packages-private/vapor-e2e-test/interop/components/VaporComp.vue +++ b/packages-private/vapor-e2e-test/interop/components/VaporComp.vue @@ -20,7 +20,10 @@ const slotProp = ref('slot prop')

vdom slots in vapor component

-
@@ -32,7 +35,10 @@ const slotProp = ref('slot prop')
- diff --git a/packages-private/vapor-e2e-test/interop/components/VdomFoo.vue b/packages-private/vapor-e2e-test/interop/components/VdomFoo.vue index 0b15f81ca30..ee13cfbb1ab 100644 --- a/packages-private/vapor-e2e-test/interop/components/VdomFoo.vue +++ b/packages-private/vapor-e2e-test/interop/components/VdomFoo.vue @@ -1,9 +1,5 @@ - + From 307f7578a7d35db9975d1e8161d1a20110b50853 Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 19 Mar 2025 10:53:07 +0800 Subject: [PATCH 9/9] test: update --- packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts | 2 +- packages-private/vapor-e2e-test/interop/App.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts b/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts index a79c0d037cb..32461df61af 100644 --- a/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts +++ b/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts @@ -96,7 +96,7 @@ describe('vdom / vapor interop', () => { expect(await html(testContainer)).toBe('loading...') await timeout(duration) - expect(await html(testContainer)).toBe('
foo
') + expect(await html(testContainer)).toBe('
foo
') }, E2E_TIMEOUT, ) diff --git a/packages-private/vapor-e2e-test/interop/App.vue b/packages-private/vapor-e2e-test/interop/App.vue index 4811d00e643..c8c6c945da1 100644 --- a/packages-private/vapor-e2e-test/interop/App.vue +++ b/packages-private/vapor-e2e-test/interop/App.vue @@ -6,7 +6,7 @@ import VdomFoo from './components/VdomFoo.vue' const msg = ref('hello') const passSlot = ref(true) -const duration = typeof process !== undefined && process.env.CI ? 200 : 50 +const duration = typeof process !== 'undefined' && process.env.CI ? 200 : 50 const AsyncVDomFoo = defineVaporAsyncComponent({ loader: () => {