diff --git a/packages/runtime-vapor/__tests__/apiAsyncComponent.spec.ts b/packages/runtime-vapor/__tests__/apiAsyncComponent.spec.ts
new file mode 100644
index 000000000..ddc5d97d0
--- /dev/null
+++ b/packages/runtime-vapor/__tests__/apiAsyncComponent.spec.ts
@@ -0,0 +1,681 @@
+import {
+ type Component,
+ createComponent,
+ createIf,
+ defineAsyncComponent,
+ nextTick,
+ ref,
+ template,
+} from '../src'
+import { makeRender } from './_utils'
+
+const timeout = (n: number = 0) => new Promise(r => setTimeout(r, n))
+
+const define = makeRender()
+
+const defineToggleWrapper = (Comp: Component) => {
+ const toggle = ref(true)
+ const Wrapper = define({
+ render: () => {
+ const n0 = createIf(
+ () => toggle.value,
+ () => createComponent(Comp),
+ undefined,
+ )
+ return n0
+ },
+ })
+ return [toggle, Wrapper] as const
+}
+
+describe('api: defineAsyncComponent', () => {
+ test('simple usage', async () => {
+ let resolve: (comp: Component) => void
+ const Foo = defineAsyncComponent(
+ () =>
+ new Promise(r => {
+ resolve = r as any
+ }),
+ )
+ const [toggle, { render }] = defineToggleWrapper(Foo)
+ const { html } = render()
+ expect(html()).toBe('')
+ resolve!(() => template('resolved')())
+ // first time resolve, wait for macro task since there are multiple
+ // microtasks / .then() calls
+ 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')
+ })
+
+ test('with loading component', async () => {
+ let resolve: (comp: Component) => void
+ const Foo = defineAsyncComponent({
+ loader: () =>
+ new Promise(r => {
+ resolve = r as any
+ }),
+ loadingComponent: () => template('loading')(),
+ delay: 2, // defaults to 200
+ })
+ const [toggle, { render }] = defineToggleWrapper(Foo)
+ const { html } = render()
+ // due to the delay, initial mount should be empty
+ expect(html()).toBe('')
+ // loading show up after delay
+ await timeout(2)
+ 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: Component) => void
+ const Foo = defineAsyncComponent({
+ loader: () =>
+ new Promise(r => {
+ resolve = r as any
+ }),
+ loadingComponent: () => template('loading')(),
+ delay: 0,
+ })
+ const [toggle, { render }] = defineToggleWrapper(Foo)
+ const { html } = 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: Component) => void
+ let reject: (e: Error) => void
+ const Foo = defineAsyncComponent(
+ () =>
+ new Promise((_resolve, _reject) => {
+ resolve = _resolve as any
+ reject = _reject
+ }),
+ )
+ const [toggle, { render }] = defineToggleWrapper(Foo)
+ const { html, app } = render()
+ const handler = (app.config.errorHandler = vi.fn())
+ expect(html()).toBe('')
+ const err = new Error('foo')
+ reject!(err)
+ await timeout()
+ expect(handler).toHaveBeenCalled()
+ expect(handler.mock.calls[0][0]).toBe(err)
+ expect(html()).toBe('')
+ console.log('unmount')
+ toggle.value = false
+ await nextTick()
+ expect(html()).toBe('')
+ console.log('remount')
+ // errored out on previous load, toggle and mock success this time
+ toggle.value = true
+ await nextTick()
+ expect(html()).toBe('')
+ console.log('resolve')
+ // should render this time
+ await timeout()
+ resolve!(() => template('resolved')())
+ await timeout()
+ // TODO: This is failing because the sub components lifecycle is WIP
+ expect(html()).not.toBe('resolved')
+ })
+ // test('error with error component', async () => {
+ // let resolve: (comp: Component) => void
+ // let reject: (e: Error) => void
+ // const Foo = defineAsyncComponent({
+ // loader: () =>
+ // new Promise((_resolve, _reject) => {
+ // resolve = _resolve as any
+ // reject = _reject
+ // }),
+ // errorComponent: (props: { error: Error }) => props.error.message,
+ // })
+ // const toggle = ref(true)
+ // const root = nodeOps.createElement('div')
+ // const app = createApp({
+ // render: () => (toggle.value ? h(Foo) : null),
+ // })
+ // const handler = (app.config.errorHandler = vi.fn())
+ // app.mount(root)
+ // expect(serializeInner(root)).toBe('')
+ // const err = new Error('errored out')
+ // reject!(err)
+ // await timeout()
+ // expect(handler).toHaveBeenCalled()
+ // expect(serializeInner(root)).toBe('errored out')
+ // toggle.value = false
+ // await nextTick()
+ // expect(serializeInner(root)).toBe('')
+ // // errored out on previous load, toggle and mock success this time
+ // toggle.value = true
+ // await nextTick()
+ // expect(serializeInner(root)).toBe('')
+ // // should render this time
+ // resolve!(() => 'resolved')
+ // await timeout()
+ // expect(serializeInner(root)).toBe('resolved')
+ // })
+ // #2129
+ // test('error with error component, without global handler', async () => {
+ // let resolve: (comp: Component) => void
+ // let reject: (e: Error) => void
+ // const Foo = defineAsyncComponent({
+ // loader: () =>
+ // new Promise((_resolve, _reject) => {
+ // resolve = _resolve as any
+ // reject = _reject
+ // }),
+ // errorComponent: (props: { error: Error }) => props.error.message,
+ // })
+ // const toggle = ref(true)
+ // const root = nodeOps.createElement('div')
+ // const app = createApp({
+ // render: () => (toggle.value ? h(Foo) : null),
+ // })
+ // app.mount(root)
+ // expect(serializeInner(root)).toBe('')
+ // const err = new Error('errored out')
+ // reject!(err)
+ // await timeout()
+ // expect(serializeInner(root)).toBe('errored out')
+ // expect(
+ // 'Unhandled error during execution of async component loader',
+ // ).toHaveBeenWarned()
+ // toggle.value = false
+ // await nextTick()
+ // expect(serializeInner(root)).toBe('')
+ // // errored out on previous load, toggle and mock success this time
+ // toggle.value = true
+ // await nextTick()
+ // expect(serializeInner(root)).toBe('')
+ // // should render this time
+ // resolve!(() => 'resolved')
+ // await timeout()
+ // expect(serializeInner(root)).toBe('resolved')
+ // })
+ // test('error with error + loading components', async () => {
+ // let resolve: (comp: Component) => void
+ // let reject: (e: Error) => void
+ // const Foo = defineAsyncComponent({
+ // loader: () =>
+ // new Promise((_resolve, _reject) => {
+ // resolve = _resolve as any
+ // reject = _reject
+ // }),
+ // errorComponent: (props: { error: Error }) => props.error.message,
+ // loadingComponent: () => 'loading',
+ // delay: 1,
+ // })
+ // const toggle = ref(true)
+ // const root = nodeOps.createElement('div')
+ // const app = createApp({
+ // render: () => (toggle.value ? h(Foo) : null),
+ // })
+ // const handler = (app.config.errorHandler = vi.fn())
+ // app.mount(root)
+ // // due to the delay, initial mount should be empty
+ // expect(serializeInner(root)).toBe('')
+ // // loading show up after delay
+ // await timeout(1)
+ // expect(serializeInner(root)).toBe('loading')
+ // const err = new Error('errored out')
+ // reject!(err)
+ // await timeout()
+ // expect(handler).toHaveBeenCalled()
+ // expect(serializeInner(root)).toBe('errored out')
+ // toggle.value = false
+ // await nextTick()
+ // expect(serializeInner(root)).toBe('')
+ // // errored out on previous load, toggle and mock success this time
+ // toggle.value = true
+ // await nextTick()
+ // expect(serializeInner(root)).toBe('')
+ // // loading show up after delay
+ // await timeout(1)
+ // expect(serializeInner(root)).toBe('loading')
+ // // should render this time
+ // resolve!(() => 'resolved')
+ // await timeout()
+ // expect(serializeInner(root)).toBe('resolved')
+ // })
+ // test('timeout without error component', async () => {
+ // let resolve: (comp: Component) => void
+ // const Foo = defineAsyncComponent({
+ // loader: () =>
+ // new Promise(_resolve => {
+ // resolve = _resolve as any
+ // }),
+ // timeout: 1,
+ // })
+ // const root = nodeOps.createElement('div')
+ // const app = createApp({
+ // render: () => h(Foo),
+ // })
+ // const handler = vi.fn()
+ // app.config.errorHandler = handler
+ // app.mount(root)
+ // expect(serializeInner(root)).toBe('')
+ // await timeout(1)
+ // expect(handler).toHaveBeenCalled()
+ // expect(handler.mock.calls[0][0].message).toMatch(
+ // `Async component timed out after 1ms.`,
+ // )
+ // expect(serializeInner(root)).toBe('')
+ // // if it resolved after timeout, should still work
+ // resolve!(() => 'resolved')
+ // await timeout()
+ // expect(serializeInner(root)).toBe('resolved')
+ // })
+ // test('timeout with error component', async () => {
+ // let resolve: (comp: Component) => void
+ // const Foo = defineAsyncComponent({
+ // loader: () =>
+ // new Promise(_resolve => {
+ // resolve = _resolve as any
+ // }),
+ // timeout: 1,
+ // errorComponent: () => 'timed out',
+ // })
+ // const root = nodeOps.createElement('div')
+ // const app = createApp({
+ // render: () => h(Foo),
+ // })
+ // const handler = (app.config.errorHandler = vi.fn())
+ // app.mount(root)
+ // expect(serializeInner(root)).toBe('')
+ // await timeout(1)
+ // expect(handler).toHaveBeenCalled()
+ // expect(serializeInner(root)).toBe('timed out')
+ // // if it resolved after timeout, should still work
+ // resolve!(() => 'resolved')
+ // await timeout()
+ // expect(serializeInner(root)).toBe('resolved')
+ // })
+ // test('timeout with error + loading components', async () => {
+ // let resolve: (comp: Component) => void
+ // const Foo = defineAsyncComponent({
+ // loader: () =>
+ // new Promise(_resolve => {
+ // resolve = _resolve as any
+ // }),
+ // delay: 1,
+ // timeout: 16,
+ // errorComponent: () => 'timed out',
+ // loadingComponent: () => 'loading',
+ // })
+ // const root = nodeOps.createElement('div')
+ // const app = createApp({
+ // render: () => h(Foo),
+ // })
+ // const handler = (app.config.errorHandler = vi.fn())
+ // app.mount(root)
+ // expect(serializeInner(root)).toBe('')
+ // await timeout(1)
+ // expect(serializeInner(root)).toBe('loading')
+ // await timeout(16)
+ // expect(serializeInner(root)).toBe('timed out')
+ // expect(handler).toHaveBeenCalled()
+ // resolve!(() => 'resolved')
+ // await timeout()
+ // expect(serializeInner(root)).toBe('resolved')
+ // })
+ // test('timeout without error component, but with loading component', async () => {
+ // let resolve: (comp: Component) => void
+ // const Foo = defineAsyncComponent({
+ // loader: () =>
+ // new Promise(_resolve => {
+ // resolve = _resolve as any
+ // }),
+ // delay: 1,
+ // timeout: 16,
+ // loadingComponent: () => 'loading',
+ // })
+ // const root = nodeOps.createElement('div')
+ // const app = createApp({
+ // render: () => h(Foo),
+ // })
+ // const handler = vi.fn()
+ // app.config.errorHandler = handler
+ // app.mount(root)
+ // expect(serializeInner(root)).toBe('')
+ // await timeout(1)
+ // expect(serializeInner(root)).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(serializeInner(root)).toBe('loading')
+ // resolve!(() => 'resolved')
+ // await timeout()
+ // expect(serializeInner(root)).toBe('resolved')
+ // })
+ // TODO: Suspense is not implemented yet
+ // test('with suspense', async () => {
+ // let resolve: (comp: Component) => void
+ // const Foo = defineAsyncComponent(
+ // () =>
+ // new Promise(_resolve => {
+ // resolve = _resolve as any
+ // }),
+ // )
+ // const root = nodeOps.createElement('div')
+ // const app = createApp({
+ // render: () =>
+ // h(Suspense, null, {
+ // default: () => h('div', [h(Foo), ' & ', h(Foo)]),
+ // fallback: () => 'loading',
+ // }),
+ // })
+ // app.mount(root)
+ // expect(serializeInner(root)).toBe('loading')
+ // resolve!(() => 'resolved')
+ // await timeout()
+ // expect(serializeInner(root)).toBe('
resolved & resolved
')
+ // })
+ // TODO: Suspense is not implemented yet
+ // test('suspensible: false', async () => {
+ // let resolve: (comp: Component) => void
+ // const Foo = defineAsyncComponent({
+ // loader: () =>
+ // new Promise(_resolve => {
+ // resolve = _resolve as any
+ // }),
+ // suspensible: false,
+ // })
+ // const root = nodeOps.createElement('div')
+ // const app = createApp({
+ // render: () =>
+ // h(Suspense, null, {
+ // default: () => h('div', [h(Foo), ' & ', h(Foo)]),
+ // fallback: () => 'loading',
+ // }),
+ // })
+ // app.mount(root)
+ // // should not show suspense fallback
+ // expect(serializeInner(root)).toBe(' &
')
+ // resolve!(() => 'resolved')
+ // await timeout()
+ // expect(serializeInner(root)).toBe('resolved & resolved
')
+ // })
+ // test('suspense with error handling', async () => {
+ // let reject: (e: Error) => void
+ // const Foo = defineAsyncComponent(
+ // () =>
+ // new Promise((_resolve, _reject) => {
+ // reject = _reject
+ // }),
+ // )
+ // const root = nodeOps.createElement('div')
+ // const app = createApp({
+ // render: () =>
+ // h(Suspense, null, {
+ // default: () => h('div', [h(Foo), ' & ', h(Foo)]),
+ // fallback: () => 'loading',
+ // }),
+ // })
+ // const handler = (app.config.errorHandler = vi.fn())
+ // app.mount(root)
+ // expect(serializeInner(root)).toBe('loading')
+ // reject!(new Error('no'))
+ // await timeout()
+ // expect(handler).toHaveBeenCalled()
+ // expect(serializeInner(root)).toBe(' &
')
+ // })
+ // test('retry (success)', async () => {
+ // let loaderCallCount = 0
+ // let resolve: (comp: Component) => void
+ // let reject: (e: Error) => void
+ // const Foo = defineAsyncComponent({
+ // 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 = nodeOps.createElement('div')
+ // const app = createApp({
+ // render: () => h(Foo),
+ // })
+ // const handler = (app.config.errorHandler = vi.fn())
+ // app.mount(root)
+ // expect(serializeInner(root)).toBe('')
+ // expect(loaderCallCount).toBe(1)
+ // const err = new Error('foo')
+ // reject!(err)
+ // await timeout()
+ // expect(handler).not.toHaveBeenCalled()
+ // expect(loaderCallCount).toBe(2)
+ // expect(serializeInner(root)).toBe('')
+ // // should render this time
+ // resolve!(() => 'resolved')
+ // await timeout()
+ // expect(handler).not.toHaveBeenCalled()
+ // expect(serializeInner(root)).toBe('resolved')
+ // })
+ // test('retry (skipped)', async () => {
+ // let loaderCallCount = 0
+ // let reject: (e: Error) => void
+ // const Foo = defineAsyncComponent({
+ // loader: () => {
+ // loaderCallCount++
+ // return new Promise((_resolve, _reject) => {
+ // reject = _reject
+ // })
+ // },
+ // onError(error, retry, fail) {
+ // if (error.message.match(/bar/)) {
+ // retry()
+ // } else {
+ // fail()
+ // }
+ // },
+ // })
+ // const root = nodeOps.createElement('div')
+ // const app = createApp({
+ // render: () => h(Foo),
+ // })
+ // const handler = (app.config.errorHandler = vi.fn())
+ // app.mount(root)
+ // expect(serializeInner(root)).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(serializeInner(root)).toBe('')
+ // })
+ // test('retry (fail w/ max retry attempts)', async () => {
+ // let loaderCallCount = 0
+ // let reject: (e: Error) => void
+ // const Foo = defineAsyncComponent({
+ // 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 = nodeOps.createElement('div')
+ // const app = createApp({
+ // render: () => h(Foo),
+ // })
+ // const handler = (app.config.errorHandler = vi.fn())
+ // app.mount(root)
+ // expect(serializeInner(root)).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(serializeInner(root)).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(serializeInner(root)).toBe('')
+ // })
+ // test('template ref forwarding', async () => {
+ // let resolve: (comp: Component) => void
+ // const Foo = defineAsyncComponent(
+ // () =>
+ // new Promise(r => {
+ // resolve = r as any
+ // }),
+ // )
+ // const fooRef = ref(null)
+ // const toggle = ref(true)
+ // const root = nodeOps.createElement('div')
+ // createApp({
+ // render: () => (toggle.value ? h(Foo, { ref: fooRef }) : null),
+ // }).mount(root)
+ // expect(serializeInner(root)).toBe('')
+ // expect(fooRef.value).toBe(null)
+ // resolve!({
+ // data() {
+ // return {
+ // id: 'foo',
+ // }
+ // },
+ // render: () => 'resolved',
+ // })
+ // // first time resolve, wait for macro task since there are multiple
+ // // microtasks / .then() calls
+ // await timeout()
+ // expect(serializeInner(root)).toBe('resolved')
+ // expect(fooRef.value.id).toBe('foo')
+ // toggle.value = false
+ // await nextTick()
+ // expect(serializeInner(root)).toBe('')
+ // expect(fooRef.value).toBe(null)
+ // // already resolved component should update on nextTick
+ // toggle.value = true
+ // await nextTick()
+ // expect(serializeInner(root)).toBe('resolved')
+ // expect(fooRef.value.id).toBe('foo')
+ // })
+ // #3188
+ // test('the forwarded template ref should always exist when doing multi patching', async () => {
+ // let resolve: (comp: Component) => void
+ // const Foo = defineAsyncComponent(
+ // () =>
+ // new Promise(r => {
+ // resolve = r as any
+ // }),
+ // )
+ // const fooRef = ref(null)
+ // const toggle = ref(true)
+ // const updater = ref(0)
+ // const root = nodeOps.createElement('div')
+ // createApp({
+ // render: () =>
+ // toggle.value ? [h(Foo, { ref: fooRef }), updater.value] : null,
+ // }).mount(root)
+ // expect(serializeInner(root)).toBe('0')
+ // expect(fooRef.value).toBe(null)
+ // resolve!({
+ // data() {
+ // return {
+ // id: 'foo',
+ // }
+ // },
+ // render: () => 'resolved',
+ // })
+ // await timeout()
+ // expect(serializeInner(root)).toBe('resolved0')
+ // expect(fooRef.value.id).toBe('foo')
+ // updater.value++
+ // await nextTick()
+ // expect(serializeInner(root)).toBe('resolved1')
+ // expect(fooRef.value.id).toBe('foo')
+ // toggle.value = false
+ // await nextTick()
+ // expect(serializeInner(root)).toBe('')
+ // expect(fooRef.value).toBe(null)
+ // })
+ // test('with KeepAlive', async () => {
+ // const spy = vi.fn()
+ // let resolve: (comp: Component) => void
+ // const Foo = defineAsyncComponent(
+ // () =>
+ // new Promise(r => {
+ // resolve = r as any
+ // }),
+ // )
+ // const Bar = defineAsyncComponent(() => Promise.resolve(() => 'Bar'))
+ // const toggle = ref(true)
+ // const root = nodeOps.createElement('div')
+ // const app = createApp({
+ // render: () => h(KeepAlive, [toggle.value ? h(Foo) : h(Bar)]),
+ // })
+ // app.mount(root)
+ // await nextTick()
+ // resolve!({
+ // setup() {
+ // onActivated(() => {
+ // spy()
+ // })
+ // return () => 'Foo'
+ // },
+ // })
+ // await timeout()
+ // expect(serializeInner(root)).toBe('Foo')
+ // expect(spy).toBeCalledTimes(1)
+ // toggle.value = false
+ // await timeout()
+ // expect(serializeInner(root)).toBe('Bar')
+ // })
+})
diff --git a/packages/runtime-vapor/src/apiAsyncComponent.ts b/packages/runtime-vapor/src/apiAsyncComponent.ts
new file mode 100644
index 000000000..dd14f8af0
--- /dev/null
+++ b/packages/runtime-vapor/src/apiAsyncComponent.ts
@@ -0,0 +1,229 @@
+import {
+ type Component,
+ type ComponentInternalInstance,
+ currentInstance,
+} from './component'
+import { isFunction, isObject } from '@vue/shared'
+import { defineComponent } from './apiDefineComponent'
+import { warn } from './warning'
+import { ref } from '@vue/reactivity'
+import { VaporErrorCodes, handleError } from './errorHandling'
+import { createComponent } from './apiCreateComponent'
+import { createIf } from './apiCreateIf'
+
+export type AsyncComponentResolveResult = T | { default: T } // es modules
+
+export type AsyncComponentLoader = () => Promise<
+ AsyncComponentResolveResult
+>
+
+export interface AsyncComponentOptions {
+ loader: AsyncComponentLoader
+ loadingComponent?: Component
+ errorComponent?: Component
+ delay?: number
+ timeout?: number
+ suspensible?: boolean
+ onError?: (
+ error: Error,
+ retry: () => void,
+ fail: () => void,
+ attempts: number,
+ ) => any
+}
+
+export const isAsyncWrapper = (i: ComponentInternalInstance): boolean =>
+ !!i.type.__asyncLoader
+
+/*! #__NO_SIDE_EFFECTS__ */
+export function defineAsyncComponent(
+ source: AsyncComponentLoader | AsyncComponentOptions,
+): T {
+ if (isFunction(source)) {
+ source = { loader: source }
+ }
+
+ const {
+ loader,
+ loadingComponent,
+ errorComponent,
+ delay = 200,
+ timeout, // undefined = never times out
+ // suspensible = true,
+ onError: userOnError,
+ } = source
+
+ let pendingRequest: Promise | null = null
+ let resolvedComp: Component | 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
+ }))
+ )
+ }
+
+ return defineComponent({
+ name: 'AsyncComponentWrapper',
+
+ __asyncLoader: load,
+
+ get __asyncResolved() {
+ return resolvedComp
+ },
+
+ setup() {
+ const instance = currentInstance!
+
+ // already resolved
+ if (resolvedComp) {
+ return createInnerComp(resolvedComp!, instance)
+ }
+
+ const onError = (err: Error) => {
+ pendingRequest = null
+ handleError(
+ err,
+ instance,
+ VaporErrorCodes.ASYNC_COMPONENT_LOADER,
+ !errorComponent /* do not throw in dev if user provided error component */,
+ )
+ }
+
+ // TODO: handle suspense and SSR.
+ // suspense-controlled or SSR.
+ // if (
+ // (__FEATURE_SUSPENSE__ && suspensible && instance.suspense) ||
+ // (__SSR__ && isInSSRComponentSetup)
+ // ) {
+ // return load()
+ // .then(comp => {
+ // return () => createInnerComp(comp, instance)
+ // })
+ // .catch(err => {
+ // onError(err)
+ // return () =>
+ // errorComponent
+ // ? createVNode(errorComponent as ConcreteComponent, {
+ // error: err,
+ // })
+ // : null
+ // })
+ // }
+
+ 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)
+ }
+
+ load()
+ .then(() => {
+ loaded.value = true
+ // TODO: handle keep-alive.
+ // if (instance.parent && isKeepAlive(instance.parent.vnode)) {
+ // // parent is keep-alive, force update so the loaded component's
+ // // name is taken into account
+ // queueJob(instance.parent.update)
+ // }
+ })
+ .catch(err => {
+ onError(err)
+ error.value = err
+ })
+
+ return createIf(
+ () => loaded.value && resolvedComp,
+ () => {
+ return createInnerComp(resolvedComp!, instance)
+ },
+ () =>
+ createIf(
+ () => error.value && errorComponent,
+ () =>
+ createComponent(errorComponent!, [{ error: () => error.value }]),
+ () =>
+ createIf(
+ () => loadingComponent && !delayed.value,
+ () => createComponent(loadingComponent!),
+ ),
+ ),
+ )
+ },
+ }) as T
+}
+
+function createInnerComp(comp: Component, parent: ComponentInternalInstance) {
+ const { rawProps: props, rawSlots: slots } = parent
+ const innerComp = createComponent(comp, props, slots)
+ // const vnode = createVNode(comp, props, children)
+ // // ensure inner component inherits the async wrapper's ref owner
+ innerComp.refs = parent.refs
+ // vnode.ref = ref
+ // // pass the custom element callback on to the inner comp
+ // // and remove it from the async wrapper
+ // vnode.ce = ce
+ // delete parent.vnode.ce
+
+ return innerComp
+}
diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts
index 4c4b02a52..1fb0011c0 100644
--- a/packages/runtime-vapor/src/component.ts
+++ b/packages/runtime-vapor/src/component.ts
@@ -111,6 +111,17 @@ export interface ObjectComponent extends ComponentInternalOptions {
emits?: EmitsOptions
render?(ctx: any): Block
+ /**
+ * marker for AsyncComponentWrapper
+ * @internal
+ */
+ __asyncLoader?: () => Promise
+ /**
+ * the inner component resolved by the AsyncComponentWrapper
+ * @internal
+ */
+ __asyncResolved?: Component
+
name?: string
vapor?: boolean
}
@@ -175,6 +186,7 @@ export interface ComponentInternalInstance {
emit: EmitFn
emitted: Record | null
attrs: Data
+ rawSlots: RawSlots
slots: StaticSlots
refs: Data
// exposed properties via expose()
@@ -301,6 +313,7 @@ export function createComponentInstance(
emitted: null,
attrs: EMPTY_OBJ,
slots: EMPTY_OBJ,
+ rawSlots: slots || EMPTY_OBJ,
refs: EMPTY_OBJ,
// lifecycle
diff --git a/packages/runtime-vapor/src/dom/templateRef.ts b/packages/runtime-vapor/src/dom/templateRef.ts
index b3770739a..95c8bff6a 100644
--- a/packages/runtime-vapor/src/dom/templateRef.ts
+++ b/packages/runtime-vapor/src/dom/templateRef.ts
@@ -36,11 +36,14 @@ export function setRef(
if (!currentInstance) return
const { setupState, isUnmounted } = currentInstance
+ const isComponent = isVaporComponent(el)
+ // const isAsync = isComponent && isAsyncWrapper(currentInstance)
+
if (isUnmounted) {
return
}
- const refValue = isVaporComponent(el) ? el.exposed || el : el
+ const refValue = isComponent ? el.exposed || el : el
const refs =
currentInstance.refs === EMPTY_OBJ
diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts
index 96f189b03..919571957 100644
--- a/packages/runtime-vapor/src/index.ts
+++ b/packages/runtime-vapor/src/index.ts
@@ -98,6 +98,7 @@ export { on, delegate, delegateEvents, setDynamicEvents } from './dom/event'
export { setRef } from './dom/templateRef'
export { defineComponent } from './apiDefineComponent'
+export { defineAsyncComponent } from './apiAsyncComponent'
export {
type InjectionKey,
inject,