From: daiwei Date: Mon, 23 Jun 2025 07:53:10 +0000 (+0800) Subject: chore: Merge branch 'edison/feat/vaporAsyncComponent' into edison/testVapor X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=d922a9a15363514e92d0d3729be1f8ae86582b83;p=thirdparty%2Fvuejs%2Fcore.git chore: Merge branch 'edison/feat/vaporAsyncComponent' into edison/testVapor --- d922a9a15363514e92d0d3729be1f8ae86582b83 diff --cc packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts index e4959121ca,32461df61a..33d7502b3a --- a/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts +++ b/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts @@@ -264,33 -100,5 +264,47 @@@ describe('vdom / vapor interop', () => }, E2E_TIMEOUT, ) + describe('teleport', () => { + const testSelector = '.teleport' + test('render vapor component', async () => { + const targetSelector = `${testSelector} .teleport-target` + const containerSelector = `${testSelector} .render-vapor-comp` + const buttonSelector = `${containerSelector} button` + + // teleport is disabled by default + expect(await html(containerSelector)).toBe( + `
vapor comp
`, + ) + expect(await html(targetSelector)).toBe('') + + // disabled -> enabled + await click(buttonSelector) + await nextTick() + expect(await html(containerSelector)).toBe(``) + expect(await html(targetSelector)).toBe('
vapor comp
') + + // enabled -> disabled + await click(buttonSelector) + await nextTick() + expect(await html(containerSelector)).toBe( + `
vapor comp
`, + ) + expect(await html(targetSelector)).toBe('') + }) + }) ++ 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 --cc packages-private/vapor-e2e-test/interop/App.vue index 7bfdd6abf0,c8c6c945da..e50c86d2da --- a/packages-private/vapor-e2e-test/interop/App.vue +++ b/packages-private/vapor-e2e-test/interop/App.vue @@@ -1,25 -1,23 +1,39 @@@ diff --cc packages/runtime-core/src/index.ts index a4063a0666,920e64eac6..01a123e7dc --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@@ -569,19 -560,15 +569,28 @@@ export { initFeatureFlags } from './fea /** * @internal */ +export { performTransitionEnter, performTransitionLeave } from './renderer' +/** + * @internal + */ +export { ensureVaporSlotFallback } from './helpers/renderSlot' +/** + * @internal + */ +export { + resolveTarget as resolveTeleportTarget, + isTeleportDisabled, + isTeleportDeferred, +} from './components/Teleport' + export { + createAsyncComponentContext, + useAsyncComponentState, + isAsyncWrapper, + } from './apiAsyncComponent' + /** + * @internal + */ + export { markAsyncBoundary } from './helpers/useId' /** * @internal */ diff --cc packages/runtime-vapor/src/apiDefineAsyncComponent.ts index 0000000000,ddd91c06c8..e609dfa795 mode 000000,100644..100644 --- a/packages/runtime-vapor/src/apiDefineAsyncComponent.ts +++ b/packages/runtime-vapor/src/apiDefineAsyncComponent.ts @@@ -1,0 -1,137 +1,139 @@@ + 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' ++import { DynamicFragment } from './fragment' + + /*! #__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', + + __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 render + if (loaded.value && resolvedComp) { + render = () => createInnerComp(resolvedComp!, instance, frag) + } else if (error.value && errorComponent) { + render = () => + createComponent(errorComponent, { error: () => error.value }) + } else if (loadingComponent && !delayed.value) { + render = () => createComponent(loadingComponent) + } + frag.update(render) + }) + + return frag + }, + }) as T + } + + function createInnerComp( + comp: VaporComponent, + parent: VaporComponentInstance, + frag?: DynamicFragment, + ): VaporComponentInstance { + const { rawProps, rawSlots, isSingleRoot, appContext } = parent + const instance = createComponent( + comp, + rawProps, + rawSlots, + isSingleRoot, ++ undefined, ++ undefined, + appContext, + ) + + // set ref + 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 instance + } diff --cc packages/runtime-vapor/src/fragment.ts index 258e848b04,0000000000..1e4328ac7f mode 100644,000000..100644 --- a/packages/runtime-vapor/src/fragment.ts +++ b/packages/runtime-vapor/src/fragment.ts @@@ -1,140 -1,0 +1,142 @@@ +import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity' +import { createComment, createTextNode } from './dom/node' +import { + type Block, + type BlockFn, + type TransitionOptions, + type VaporTransitionHooks, + insert, + isValidBlock, + remove, +} from './block' +import type { TransitionHooks } from '@vue/runtime-dom' +import { + currentHydrationNode, + isComment, + isHydrating, + locateHydrationNode, + locateVaporFragmentAnchor, +} from './dom/hydration' +import { + applyTransitionHooks, + applyTransitionLeaveHooks, +} from './components/Transition' ++import type { VaporComponentInstance } from './component' + +export class VaporFragment implements TransitionOptions { + $key?: any + $transition?: VaporTransitionHooks | undefined + nodes: Block + anchor?: Node + insert?: ( + parent: ParentNode, + anchor: Node | null, + transitionHooks?: TransitionHooks, + ) => void + remove?: (parent?: ParentNode, transitionHooks?: TransitionHooks) => void + fallback?: BlockFn + + target?: ParentNode | null + targetAnchor?: Node | null + getNodes?: () => Block ++ setRef?: (comp: VaporComponentInstance) => void + + constructor(nodes: Block) { + this.nodes = nodes + } +} + +export class DynamicFragment extends VaporFragment { + anchor!: Node + scope: EffectScope | undefined + current?: BlockFn + fallback?: BlockFn + /** + * slot only + * indicates forwarded slot + */ + forwarded?: boolean + + constructor(anchorLabel?: string) { + super([]) + if (isHydrating) { + locateHydrationNode(true) + this.hydrate(anchorLabel!) + } else { + this.anchor = + __DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode() + } + } + + update(render?: BlockFn, key: any = render): void { + if (key === this.current) { + return + } + this.current = key + + pauseTracking() + const parent = this.anchor.parentNode + const transition = this.$transition + const renderBranch = () => { + if (render) { + this.scope = new EffectScope() + this.nodes = this.scope.run(render) || [] + if (transition) { + this.$transition = applyTransitionHooks(this.nodes, transition) + } + if (parent) insert(this.nodes, parent, this.anchor) + } else { + this.scope = undefined + this.nodes = [] + } + } + + // teardown previous branch + if (this.scope) { + this.scope.stop() + const mode = transition && transition.mode + if (mode) { + applyTransitionLeaveHooks(this.nodes, transition, renderBranch) + parent && remove(this.nodes, parent) + if (mode === 'out-in') { + resetTracking() + return + } + } else { + parent && remove(this.nodes, parent) + } + } + + renderBranch() + + if (this.fallback && !isValidBlock(this.nodes)) { + parent && remove(this.nodes, parent) + this.nodes = + (this.scope || (this.scope = new EffectScope())).run(this.fallback) || + [] + parent && insert(this.nodes, parent, this.anchor) + } + + resetTracking() + } + + hydrate(label: string): void { + // for `v-if="false"` the node will be an empty comment, use it as the anchor. + // otherwise, find next sibling vapor fragment anchor + if (isComment(currentHydrationNode!, '')) { + this.anchor = currentHydrationNode + } else { + const anchor = locateVaporFragmentAnchor(currentHydrationNode!, label)! + if (anchor) { + this.anchor = anchor + } else if (__DEV__) { + // this should not happen + throw new Error(`${label} fragment anchor node was not found.`) + } + } + } +} + +export function isFragment(val: NonNullable): val is VaporFragment { + return val instanceof VaporFragment +} diff --cc packages/runtime-vapor/src/index.ts index ef2b6188b7,7cd81c3e10..f02063da1c --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@@ -1,12 -1,12 +1,13 @@@ // 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' +export { VaporTeleportImpl as VaporTeleport } from './components/Teleport' // compiler-use only -export { insert, prepend, remove, isFragment, VaporFragment } from './block' +export { insert, prepend, remove } from './block' export { setInsertionState } from './insertionState' export { createComponent, createComponentWithFallback } from './component' export { renderEffect } from './renderEffect'