From 9be697b38cbe85807c39c41dd5e0d5563df02589 Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 20 Mar 2025 22:17:57 +0800 Subject: [PATCH 01/40] wip: save --- .../runtime-core/src/components/Teleport.ts | 6 +- packages/runtime-core/src/index.ts | 9 + packages/runtime-vapor/src/block.ts | 4 +- packages/runtime-vapor/src/component.ts | 18 ++ .../runtime-vapor/src/components/Teleport.ts | 162 ++++++++++++++++++ packages/runtime-vapor/src/index.ts | 1 + 6 files changed, 196 insertions(+), 4 deletions(-) create mode 100644 packages/runtime-vapor/src/components/Teleport.ts diff --git a/packages/runtime-core/src/components/Teleport.ts b/packages/runtime-core/src/components/Teleport.ts index a6445df7b05..c365ad0a217 100644 --- a/packages/runtime-core/src/components/Teleport.ts +++ b/packages/runtime-core/src/components/Teleport.ts @@ -27,10 +27,10 @@ export const TeleportEndKey: unique symbol = Symbol('_vte') export const isTeleport = (type: any): boolean => type.__isTeleport -const isTeleportDisabled = (props: VNode['props']): boolean => +export const isTeleportDisabled = (props: VNode['props']): boolean => props && (props.disabled || props.disabled === '') -const isTeleportDeferred = (props: VNode['props']): boolean => +export const isTeleportDeferred = (props: VNode['props']): boolean => props && (props.defer || props.defer === '') const isTargetSVG = (target: RendererElement): boolean => @@ -39,7 +39,7 @@ const isTargetSVG = (target: RendererElement): boolean => const isTargetMathML = (target: RendererElement): boolean => typeof MathMLElement === 'function' && target instanceof MathMLElement -const resolveTarget = ( +export const resolveTarget = ( props: TeleportProps | null, select: RendererOptions['querySelector'], ): T | null => { diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index c7150e38e80..2d721b058f0 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -557,3 +557,12 @@ export { startMeasure, endMeasure } from './profiling' * @internal */ export { initFeatureFlags } from './featureFlags' +/** + * @internal + */ +export { + resolveTarget, + isTeleportDisabled, + isTeleportDeferred, + TeleportEndKey, +} from './components/Teleport' diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index b782afd38d3..6ec62da20d5 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -20,6 +20,8 @@ export type BlockFn = (...args: any[]) => Block export class VaporFragment { nodes: Block + target?: ParentNode | null + targetAnchor?: Node | null anchor?: Node insert?: (parent: ParentNode, anchor: Node | null) => void remove?: (parent?: ParentNode) => void @@ -129,7 +131,7 @@ export function insert( // TODO handle hydration for vdom interop block.insert(parent, anchor) } else { - insert(block.nodes, parent, anchor) + insert(block.nodes, block.target || parent, block.targetAnchor || anchor) } if (block.anchor) insert(block.anchor, parent, anchor) } diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 548babebf8b..7989b67a8b6 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -60,6 +60,7 @@ import { import { hmrReload, hmrRerender } from './hmr' import { isHydrating, locateHydrationNode } from './dom/hydration' import { insertionAnchor, insertionParent } from './insertionState' +import type { VaporTeleportImpl } from './components/Teleport' export { currentInstance } from '@vue/runtime-dom' @@ -92,6 +93,8 @@ export interface ObjectVaporComponent name?: string vapor?: boolean + + __isTeleport?: boolean } interface SharedInternalOptions { @@ -157,6 +160,21 @@ export function createComponent( return frag } + // teleport + if (component.__isTeleport) { + const frag = (component as typeof VaporTeleportImpl).process( + rawProps!, + rawSlots!, + ) + if (!isHydrating && _insertionParent) { + insert(frag, _insertionParent, _insertionAnchor) + } else { + frag.hydrate() + } + + return frag as any + } + if ( isSingleRoot && component.inheritAttrs !== false && diff --git a/packages/runtime-vapor/src/components/Teleport.ts b/packages/runtime-vapor/src/components/Teleport.ts new file mode 100644 index 00000000000..762d69ce81b --- /dev/null +++ b/packages/runtime-vapor/src/components/Teleport.ts @@ -0,0 +1,162 @@ +import { + TeleportEndKey, + type TeleportProps, + isTeleportDeferred, + isTeleportDisabled, + queuePostFlushCb, + resolveTarget, + warn, +} from '@vue/runtime-dom' +import { + type Block, + type BlockFn, + VaporFragment, + insert, + remove, +} from '../block' +import { createComment, createTextNode, querySelector } from '../dom/node' +import type { LooseRawProps, LooseRawSlots } from '../component' +import { rawPropsProxyHandlers } from '../componentProps' +import { renderEffect } from '../renderEffect' + +export const VaporTeleportImpl = { + name: 'VaporTeleport', + __isTeleport: true, + __vapor: true, + + process(props: LooseRawProps, slots: LooseRawSlots): TeleportFragment { + const children = slots.default && (slots.default as BlockFn)() + const frag = __DEV__ + ? new TeleportFragment('teleport') + : new TeleportFragment() + + const resolvedProps = new Proxy( + props, + rawPropsProxyHandlers, + ) as any as TeleportProps + + renderEffect(() => frag.update(resolvedProps, children)) + + frag.remove = parent => { + const { + nodes, + target, + cachedTargetAnchor, + targetStart, + placeholder, + mainAnchor, + } = frag + + remove(nodes, target || parent) + + // remove anchors + if (targetStart) { + let parentNode = targetStart.parentNode! + remove(targetStart!, parentNode) + remove(cachedTargetAnchor!, parentNode) + } + if (placeholder && placeholder.isConnected) { + remove(placeholder!, parent) + remove(mainAnchor!, parent) + } + } + + return frag + }, +} + +export class TeleportFragment extends VaporFragment { + anchor: Node + target?: ParentNode | null + targetStart?: Node | null + targetAnchor?: Node | null + cachedTargetAnchor?: Node + mainAnchor?: Node + placeholder?: Node + + constructor(anchorLabel?: string) { + super([]) + this.anchor = + __DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode() + } + + update(props: TeleportProps, children: Block): void { + this.nodes = children + const parent = this.anchor.parentNode + + if (!this.mainAnchor) { + this.mainAnchor = __DEV__ + ? createComment('teleport end') + : createTextNode() + } + if (!this.placeholder) { + this.placeholder = __DEV__ + ? createComment('teleport start') + : createTextNode() + } + if (parent) { + insert(this.placeholder, parent, this.anchor) + insert(this.mainAnchor, parent, this.anchor) + } + + const disabled = isTeleportDisabled(props) + if (disabled) { + this.target = this.anchor.parentNode + this.targetAnchor = parent ? this.mainAnchor : null + } else { + const target = (this.target = resolveTarget( + props, + querySelector, + ) as ParentNode) + if (target) { + if ( + // initial mount + !this.targetStart || + // target changed + this.targetStart.parentNode !== target + ) { + ;[this.targetAnchor, this.targetStart] = prepareAnchor(target) + this.cachedTargetAnchor = this.targetAnchor + } else { + // re-mount or target not changed, use cached target anchor + this.targetAnchor = this.cachedTargetAnchor + } + } else if (__DEV__) { + warn('Invalid Teleport target on mount:', target, `(${typeof target})`) + } + } + + const mountToTarget = () => { + insert(this.nodes, this.target!, this.targetAnchor) + } + + if (parent) { + if (isTeleportDeferred(props)) { + queuePostFlushCb(mountToTarget) + } else { + mountToTarget() + } + } + } + + hydrate(): void { + // TODO + } +} + +function prepareAnchor(target: ParentNode | null) { + const targetStart = createTextNode('targetStart') + const targetAnchor = createTextNode('targetAnchor') + + // attach a special property, so we can skip teleported content in + // renderer's nextSibling search + // @ts-expect-error + targetStart[TeleportEndKey] = targetAnchor + + if (target) { + insert(targetStart, target) + insert(targetAnchor, target) + } + + return [targetAnchor, targetStart] +} diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index 682532fa4d8..4b55949a63a 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -3,6 +3,7 @@ export { createVaporApp, createVaporSSRApp } from './apiCreateApp' export { defineVaporComponent } from './apiDefineComponent' 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' From 257138810f51802fdb4bfb0bb7068b84f309e9eb Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 21 Mar 2025 14:43:27 +0800 Subject: [PATCH 02/40] wip: save --- .../__tests__/components/Teleport.spec.ts | 252 ++++++++++++++++++ .../runtime-vapor/src/components/Teleport.ts | 121 +++++---- 2 files changed, 312 insertions(+), 61 deletions(-) create mode 100644 packages/runtime-vapor/__tests__/components/Teleport.spec.ts diff --git a/packages/runtime-vapor/__tests__/components/Teleport.spec.ts b/packages/runtime-vapor/__tests__/components/Teleport.spec.ts new file mode 100644 index 00000000000..373d170bd55 --- /dev/null +++ b/packages/runtime-vapor/__tests__/components/Teleport.spec.ts @@ -0,0 +1,252 @@ +import { + type LooseRawProps, + type VaporComponent, + createComponent as originalCreateComponent, +} from '../../src/component' +import { VaporTeleport, template } from '@vue/runtime-vapor' + +import { makeRender } from '../_utils' +import { nextTick, onBeforeUnmount, onUnmounted, ref, shallowRef } from 'vue' + +const define = makeRender() + +describe('renderer: VaporTeleport', () => { + describe('eager mode', () => { + runSharedTests(false) + }) + + describe('defer mode', () => { + runSharedTests(true) + }) +}) + +function runSharedTests(deferMode: boolean): void { + const createComponent = deferMode + ? ( + component: VaporComponent, + rawProps?: LooseRawProps | null, + ...args: any[] + ) => { + if (component === VaporTeleport) { + rawProps!.defer = () => true + } + return originalCreateComponent(component, rawProps, ...args) + } + : originalCreateComponent + + test('should work', () => { + const target = document.createElement('div') + const root = document.createElement('div') + + const { mount } = define({ + setup() { + const n0 = createComponent( + VaporTeleport, + { + to: () => target, + }, + { + default: () => template('
teleported
')(), + }, + ) + const n1 = template('
root
')() + return [n0, n1] + }, + }).create() + mount(root) + + expect(root.innerHTML).toBe('
root
') + expect(target.innerHTML).toBe('
teleported
') + }) + + test.todo('should work with SVG', async () => {}) + + test('should update target', async () => { + const targetA = document.createElement('div') + const targetB = document.createElement('div') + const target = ref(targetA) + const root = document.createElement('div') + + const { mount } = define({ + setup() { + const n0 = createComponent( + VaporTeleport, + { + to: () => target.value, + }, + { + default: () => template('
teleported
')(), + }, + ) + const n1 = template('
root
')() + return [n0, n1] + }, + }).create() + mount(root) + + expect(root.innerHTML).toBe('
root
') + expect(targetA.innerHTML).toBe('
teleported
') + expect(targetB.innerHTML).toBe('') + + target.value = targetB + await nextTick() + + expect(root.innerHTML).toBe('
root
') + expect(targetA.innerHTML).toBe('') + expect(targetB.innerHTML).toBe('
teleported
') + }) + + test('should update children', async () => { + const target = document.createElement('div') + const root = document.createElement('div') + const children = shallowRef([template('
teleported
')()]) + + const { mount } = define({ + setup() { + const n0 = createComponent( + VaporTeleport, + { + to: () => target, + }, + { + default: () => children.value, + }, + ) + const n1 = template('
root
')() + return [n0, n1] + }, + }).create() + mount(root) + + expect(target.innerHTML).toBe('
teleported
') + + children.value = [template('')()] + await nextTick() + expect(target.innerHTML).toBe('') + + children.value = [template('teleported')()] + await nextTick() + expect(target.innerHTML).toBe('teleported') + }) + + test('should remove children when unmounted', async () => { + const target = document.createElement('div') + const root = document.createElement('div') + + function testUnmount(props: any) { + const { app } = define({ + setup() { + const n0 = createComponent(VaporTeleport, props, { + default: () => template('
teleported
')(), + }) + const n1 = template('
root
')() + return [n0, n1] + }, + }).create() + app.mount(root) + + expect(target.innerHTML).toBe( + props.disabled() ? '' : '
teleported
', + ) + + app.unmount() + expect(target.innerHTML).toBe('') + expect(target.children.length).toBe(0) + } + + testUnmount({ to: () => target, disabled: () => false }) + testUnmount({ to: () => target, disabled: () => true }) + testUnmount({ to: () => null, disabled: () => true }) + }) + + test('component with multi roots should be removed when unmounted', async () => { + const target = document.createElement('div') + const root = document.createElement('div') + + const { component: Comp } = define({ + setup() { + return [template('

')(), template('

')()] + }, + }) + + const { app } = define({ + setup() { + const n0 = createComponent( + VaporTeleport, + { + to: () => target, + }, + { + default: () => createComponent(Comp), + }, + ) + const n1 = template('

root
')() + return [n0, n1] + }, + }).create() + + app.mount(root) + expect(target.innerHTML).toBe('

') + + app.unmount() + expect(target.innerHTML).toBe('') + }) + + test.todo( + 'descendent component should be unmounted when teleport is disabled and unmounted', + async () => { + const root = document.createElement('div') + const beforeUnmount = vi.fn() + const unmounted = vi.fn() + const { component: Comp } = define({ + setup() { + onBeforeUnmount(beforeUnmount) + onUnmounted(unmounted) + return [template('

')(), template('

')()] + }, + }) + + const { app } = define({ + setup() { + const n0 = createComponent( + VaporTeleport, + { + to: () => null, + disabled: () => true, + }, + { + default: () => createComponent(Comp), + }, + ) + return [n0] + }, + }).create() + app.mount(root) + + expect(beforeUnmount).toHaveBeenCalledTimes(0) + expect(unmounted).toHaveBeenCalledTimes(0) + + app.unmount() + expect(beforeUnmount).toHaveBeenCalledTimes(1) + expect(unmounted).toHaveBeenCalledTimes(1) + }, + ) + + test.todo('multiple teleport with same target', async () => {}) + test.todo('should work when using template ref as target', async () => {}) + test.todo('disabled', async () => {}) + test.todo('moving teleport while enabled', async () => {}) + test.todo('moving teleport while disabled', async () => {}) + test.todo('should work with block tree', async () => {}) + test.todo( + `the dir hooks of the Teleport's children should be called correctly`, + async () => {}, + ) + test.todo( + `ensure that target changes when disabled are updated correctly when enabled`, + async () => {}, + ) + test.todo('toggle sibling node inside target node', async () => {}) + test.todo('unmount previous sibling node inside target node', async () => {}) + test.todo('accessing template refs inside teleport', async () => {}) +} diff --git a/packages/runtime-vapor/src/components/Teleport.ts b/packages/runtime-vapor/src/components/Teleport.ts index 762d69ce81b..e12ad4d87ec 100644 --- a/packages/runtime-vapor/src/components/Teleport.ts +++ b/packages/runtime-vapor/src/components/Teleport.ts @@ -18,6 +18,7 @@ import { createComment, createTextNode, querySelector } from '../dom/node' import type { LooseRawProps, LooseRawSlots } from '../component' import { rawPropsProxyHandlers } from '../componentProps' import { renderEffect } from '../renderEffect' +import { extend } from '@vue/shared' export const VaporTeleportImpl = { name: 'VaporTeleport', @@ -25,7 +26,6 @@ export const VaporTeleportImpl = { __vapor: true, process(props: LooseRawProps, slots: LooseRawSlots): TeleportFragment { - const children = slots.default && (slots.default as BlockFn)() const frag = __DEV__ ? new TeleportFragment('teleport') : new TeleportFragment() @@ -35,31 +35,11 @@ export const VaporTeleportImpl = { rawPropsProxyHandlers, ) as any as TeleportProps - renderEffect(() => frag.update(resolvedProps, children)) - - frag.remove = parent => { - const { - nodes, - target, - cachedTargetAnchor, - targetStart, - placeholder, - mainAnchor, - } = frag - - remove(nodes, target || parent) - - // remove anchors - if (targetStart) { - let parentNode = targetStart.parentNode! - remove(targetStart!, parentNode) - remove(cachedTargetAnchor!, parentNode) - } - if (placeholder && placeholder.isConnected) { - remove(placeholder!, parent) - remove(mainAnchor!, parent) - } - } + renderEffect(() => { + const children = slots.default && (slots.default as BlockFn)() + // access the props to trigger tracking + frag.update(extend({}, resolvedProps), children) + }) return frag }, @@ -67,12 +47,10 @@ export const VaporTeleportImpl = { export class TeleportFragment extends VaporFragment { anchor: Node - target?: ParentNode | null targetStart?: Node | null - targetAnchor?: Node | null - cachedTargetAnchor?: Node mainAnchor?: Node placeholder?: Node + currentParent?: ParentNode | null constructor(anchorLabel?: string) { super([]) @@ -81,33 +59,21 @@ export class TeleportFragment extends VaporFragment { } update(props: TeleportProps, children: Block): void { + // teardown previous + if (this.currentParent && this.nodes) { + remove(this.nodes, this.currentParent) + } + this.nodes = children + const disabled = isTeleportDisabled(props) const parent = this.anchor.parentNode - if (!this.mainAnchor) { - this.mainAnchor = __DEV__ - ? createComment('teleport end') - : createTextNode() - } - if (!this.placeholder) { - this.placeholder = __DEV__ - ? createComment('teleport start') - : createTextNode() - } - if (parent) { - insert(this.placeholder, parent, this.anchor) - insert(this.mainAnchor, parent, this.anchor) + const mount = (parent: ParentNode, anchor: Node | null) => { + insert(this.nodes, (this.currentParent = parent), anchor) } - const disabled = isTeleportDisabled(props) - if (disabled) { - this.target = this.anchor.parentNode - this.targetAnchor = parent ? this.mainAnchor : null - } else { - const target = (this.target = resolveTarget( - props, - querySelector, - ) as ParentNode) + const mountToTarget = () => { + const target = (this.target = resolveTarget(props, querySelector)) if (target) { if ( // initial mount @@ -116,21 +82,38 @@ export class TeleportFragment extends VaporFragment { this.targetStart.parentNode !== target ) { ;[this.targetAnchor, this.targetStart] = prepareAnchor(target) - this.cachedTargetAnchor = this.targetAnchor - } else { - // re-mount or target not changed, use cached target anchor - this.targetAnchor = this.cachedTargetAnchor } + + mount(target, this.targetAnchor!) } else if (__DEV__) { - warn('Invalid Teleport target on mount:', target, `(${typeof target})`) + warn( + `Invalid Teleport target on ${this.targetStart ? 'update' : 'mount'}:`, + target, + `(${typeof target})`, + ) } } - const mountToTarget = () => { - insert(this.nodes, this.target!, this.targetAnchor) + if (parent && disabled) { + if (!this.mainAnchor) { + this.mainAnchor = __DEV__ + ? createComment('teleport end') + : createTextNode() + } + if (!this.placeholder) { + this.placeholder = __DEV__ + ? createComment('teleport start') + : createTextNode() + } + if (!this.mainAnchor.isConnected) { + insert(this.placeholder, parent, this.anchor) + insert(this.mainAnchor, parent, this.anchor) + } + + mount(parent, this.mainAnchor) } - if (parent) { + if (!disabled) { if (isTeleportDeferred(props)) { queuePostFlushCb(mountToTarget) } else { @@ -139,14 +122,30 @@ export class TeleportFragment extends VaporFragment { } } + remove = (parent: ParentNode | undefined): void => { + // remove nodes + remove(this.nodes, this.currentParent || parent) + + // remove anchors + if (this.targetStart) { + let parentNode = this.targetStart.parentNode! + remove(this.targetStart!, parentNode) + remove(this.targetAnchor!, parentNode) + } + if (this.placeholder) { + remove(this.placeholder!, parent) + remove(this.mainAnchor!, parent) + } + } + hydrate(): void { // TODO } } function prepareAnchor(target: ParentNode | null) { - const targetStart = createTextNode('targetStart') - const targetAnchor = createTextNode('targetAnchor') + const targetStart = createTextNode('') + const targetAnchor = createTextNode('') // attach a special property, so we can skip teleported content in // renderer's nextSibling search From c0cd7fc810ec20a1241842563c5a5084ebf26722 Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 21 Mar 2025 15:42:17 +0800 Subject: [PATCH 03/40] wip: add tests --- .../__tests__/components/Teleport.spec.ts | 256 +++++++++++++++--- .../runtime-vapor/src/components/Teleport.ts | 6 +- 2 files changed, 218 insertions(+), 44 deletions(-) diff --git a/packages/runtime-vapor/__tests__/components/Teleport.spec.ts b/packages/runtime-vapor/__tests__/components/Teleport.spec.ts index 373d170bd55..bc77be2dc01 100644 --- a/packages/runtime-vapor/__tests__/components/Teleport.spec.ts +++ b/packages/runtime-vapor/__tests__/components/Teleport.spec.ts @@ -3,7 +3,12 @@ import { type VaporComponent, createComponent as originalCreateComponent, } from '../../src/component' -import { VaporTeleport, template } from '@vue/runtime-vapor' +import { + VaporTeleport, + createTemplateRefSetter, + setInsertionState, + template, +} from '@vue/runtime-vapor' import { makeRender } from '../_utils' import { nextTick, onBeforeUnmount, onUnmounted, ref, shallowRef } from 'vue' @@ -192,50 +197,219 @@ function runSharedTests(deferMode: boolean): void { expect(target.innerHTML).toBe('') }) - test.todo( - 'descendent component should be unmounted when teleport is disabled and unmounted', - async () => { - const root = document.createElement('div') - const beforeUnmount = vi.fn() - const unmounted = vi.fn() - const { component: Comp } = define({ - setup() { - onBeforeUnmount(beforeUnmount) - onUnmounted(unmounted) - return [template('

')(), template('

')()] - }, - }) + test('descendent component should be unmounted when teleport is disabled and unmounted', async () => { + const root = document.createElement('div') + const beforeUnmount = vi.fn() + const unmounted = vi.fn() + const { component: Comp } = define({ + setup() { + onBeforeUnmount(beforeUnmount) + onUnmounted(unmounted) + return [template('

')(), template('

')()] + }, + }) - const { app } = define({ - setup() { - const n0 = createComponent( - VaporTeleport, - { - to: () => null, - disabled: () => true, - }, - { - default: () => createComponent(Comp), - }, - ) - return [n0] - }, - }).create() - app.mount(root) + const { app } = define({ + setup() { + const n0 = createComponent( + VaporTeleport, + { + to: () => null, + disabled: () => true, + }, + { + default: () => createComponent(Comp), + }, + ) + return [n0] + }, + }).create() + app.mount(root) - expect(beforeUnmount).toHaveBeenCalledTimes(0) - expect(unmounted).toHaveBeenCalledTimes(0) + expect(beforeUnmount).toHaveBeenCalledTimes(0) + expect(unmounted).toHaveBeenCalledTimes(0) - app.unmount() - expect(beforeUnmount).toHaveBeenCalledTimes(1) - expect(unmounted).toHaveBeenCalledTimes(1) - }, - ) + app.unmount() + await nextTick() + expect(beforeUnmount).toHaveBeenCalledTimes(1) + expect(unmounted).toHaveBeenCalledTimes(1) + }) + + test('multiple teleport with same target', async () => { + const target = document.createElement('div') + const root = document.createElement('div') + + const child1 = shallowRef(template('

one
')()) + const child2 = shallowRef(template('two')()) + + const { mount } = define({ + setup() { + const n0 = template('
')() + setInsertionState(n0 as any) + createComponent( + VaporTeleport, + { + to: () => target, + }, + { + default: () => child1.value, + }, + ) + createComponent( + VaporTeleport, + { + to: () => target, + }, + { + default: () => child2.value, + }, + ) + return [n0] + }, + }).create() + mount(root) + expect(root.innerHTML).toBe('
') + expect(target.innerHTML).toBe('
one
two') + + // update existing content + child1.value = [ + template('
one
')(), + template('
two
')(), + ] as any + child2.value = [template('three')()] as any + await nextTick() + expect(target.innerHTML).toBe('
one
two
three') + + // toggling + child1.value = [] as any + await nextTick() + expect(root.innerHTML).toBe('
') + expect(target.innerHTML).toBe('three') + + // toggle back + child1.value = [ + template('
one
')(), + template('
two
')(), + ] as any + child2.value = [template('three')()] as any + await nextTick() + expect(root.innerHTML).toBe('
') + // should append + expect(target.innerHTML).toBe('
one
two
three') + + // toggle the other teleport + child2.value = [] as any + await nextTick() + expect(root.innerHTML).toBe('
') + expect(target.innerHTML).toBe('
one
two
') + }) + + test('should work when using template ref as target', async () => { + const root = document.createElement('div') + const target = ref(null) + const disabled = ref(true) + + const { mount } = define({ + setup() { + const setTemplateRef = createTemplateRefSetter() + const n0 = template('
')() as any + setTemplateRef(n0, target) + + const n1 = createComponent( + VaporTeleport, + { + to: () => target.value, + disabled: () => disabled.value, + }, + { + default: () => template('
teleported
')(), + }, + ) + return [n0, n1] + }, + }).create() + mount(root) + + expect(root.innerHTML).toBe( + '
teleported
', + ) + disabled.value = false + await nextTick() + expect(root.innerHTML).toBe( + '
teleported
', + ) + }) + + test('disabled', async () => { + const target = document.createElement('div') + const root = document.createElement('div') + + const disabled = ref(false) + const { mount } = define({ + setup() { + const n0 = createComponent( + VaporTeleport, + { + to: () => target, + disabled: () => disabled.value, + }, + { + default: () => template('
teleported
')(), + }, + ) + const n1 = template('
root
')() + return [n0, n1] + }, + }).create() + mount(root) + + expect(root.innerHTML).toBe('
root
') + expect(target.innerHTML).toBe('
teleported
') + + disabled.value = true + await nextTick() + expect(root.innerHTML).toBe( + '
teleported
root
', + ) + expect(target.innerHTML).toBe('') + + // toggle back + disabled.value = false + await nextTick() + expect(root.innerHTML).toBe( + '
root
', + ) + expect(target.innerHTML).toBe('
teleported
') + }) + + test.todo('moving teleport while enabled', async () => { + const target = document.createElement('div') + const root = document.createElement('div') + + const child1 = createComponent( + VaporTeleport, + { to: () => target }, + { default: () => template('
teleported
')() }, + ) + const child2 = template('
root
')() + + const children = shallowRef([child1, child2]) + const { mount } = define({ + setup() { + return children.value + }, + }).create() + mount(root) + + expect(root.innerHTML).toBe('
root
') + expect(target.innerHTML).toBe('
teleported
') + + children.value = [child2, child1] + await nextTick() + expect(root.innerHTML).toBe('
root
') + expect(target.innerHTML).toBe('
teleported
') + }) - test.todo('multiple teleport with same target', async () => {}) - test.todo('should work when using template ref as target', async () => {}) - test.todo('disabled', async () => {}) - test.todo('moving teleport while enabled', async () => {}) test.todo('moving teleport while disabled', async () => {}) test.todo('should work with block tree', async () => {}) test.todo( diff --git a/packages/runtime-vapor/src/components/Teleport.ts b/packages/runtime-vapor/src/components/Teleport.ts index e12ad4d87ec..b0da3454036 100644 --- a/packages/runtime-vapor/src/components/Teleport.ts +++ b/packages/runtime-vapor/src/components/Teleport.ts @@ -59,14 +59,14 @@ export class TeleportFragment extends VaporFragment { } update(props: TeleportProps, children: Block): void { + const parent = this.anchor.parentNode // teardown previous - if (this.currentParent && this.nodes) { - remove(this.nodes, this.currentParent) + if (this.nodes && (parent || this.currentParent)) { + remove(this.nodes, this.currentParent! || parent) } this.nodes = children const disabled = isTeleportDisabled(props) - const parent = this.anchor.parentNode const mount = (parent: ParentNode, anchor: Node | null) => { insert(this.nodes, (this.currentParent = parent), anchor) From b945079643d15ccd34f167a0aebaa11cd78793fa Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 21 Mar 2025 16:23:27 +0800 Subject: [PATCH 04/40] wip: update tests --- packages/runtime-vapor/src/components/Teleport.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/runtime-vapor/src/components/Teleport.ts b/packages/runtime-vapor/src/components/Teleport.ts index b0da3454036..1402349e1a4 100644 --- a/packages/runtime-vapor/src/components/Teleport.ts +++ b/packages/runtime-vapor/src/components/Teleport.ts @@ -61,8 +61,8 @@ export class TeleportFragment extends VaporFragment { update(props: TeleportProps, children: Block): void { const parent = this.anchor.parentNode // teardown previous - if (this.nodes && (parent || this.currentParent)) { - remove(this.nodes, this.currentParent! || parent) + if (this.nodes && (this.currentParent || parent)) { + remove(this.nodes, (this.currentParent || parent)!) } this.nodes = children From dd18528023fb224cb80781584b4fa4d0cb3a359f Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 21 Mar 2025 17:47:43 +0800 Subject: [PATCH 05/40] wip: save --- .../runtime-vapor/src/components/Teleport.ts | 41 +++++++++++++++---- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/packages/runtime-vapor/src/components/Teleport.ts b/packages/runtime-vapor/src/components/Teleport.ts index 1402349e1a4..4b029b64121 100644 --- a/packages/runtime-vapor/src/components/Teleport.ts +++ b/packages/runtime-vapor/src/components/Teleport.ts @@ -35,10 +35,17 @@ export const VaporTeleportImpl = { rawPropsProxyHandlers, ) as any as TeleportProps + let children: Block + + renderEffect(() => { + frag.updateChildren( + (children = slots.default && (slots.default as BlockFn)()), + ) + }) + renderEffect(() => { - const children = slots.default && (slots.default as BlockFn)() // access the props to trigger tracking - frag.update(extend({}, resolvedProps), children) + frag.update(extend({}, resolvedProps), children!) }) return frag @@ -50,7 +57,8 @@ export class TeleportFragment extends VaporFragment { targetStart?: Node | null mainAnchor?: Node placeholder?: Node - currentParent?: ParentNode | null + container?: ParentNode | null + currentAnchor?: Node | null constructor(anchorLabel?: string) { super([]) @@ -58,18 +66,33 @@ export class TeleportFragment extends VaporFragment { __DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode() } - update(props: TeleportProps, children: Block): void { + updateChildren(children: Block): void { const parent = this.anchor.parentNode + if (!parent) return + + const container = this.container || parent + // teardown previous - if (this.nodes && (this.currentParent || parent)) { - remove(this.nodes, (this.currentParent || parent)!) - } + remove(this.nodes, container) + + insert( + (this.nodes = children), + container, + this.currentAnchor || this.anchor, + ) + } + update(props: TeleportProps, children: Block): void { + const parent = this.anchor.parentNode this.nodes = children const disabled = isTeleportDisabled(props) const mount = (parent: ParentNode, anchor: Node | null) => { - insert(this.nodes, (this.currentParent = parent), anchor) + insert( + this.nodes, + (this.container = parent), + (this.currentAnchor = anchor), + ) } const mountToTarget = () => { @@ -124,7 +147,7 @@ export class TeleportFragment extends VaporFragment { remove = (parent: ParentNode | undefined): void => { // remove nodes - remove(this.nodes, this.currentParent || parent) + remove(this.nodes, this.container || parent) // remove anchors if (this.targetStart) { From 5c8f7ed2add8e6022e744912225d7a5ba381d963 Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 21 Mar 2025 21:53:35 +0800 Subject: [PATCH 06/40] wip: save --- .../runtime-vapor/src/components/Teleport.ts | 121 +++++++++--------- 1 file changed, 64 insertions(+), 57 deletions(-) diff --git a/packages/runtime-vapor/src/components/Teleport.ts b/packages/runtime-vapor/src/components/Teleport.ts index 4b029b64121..823f6c5cca2 100644 --- a/packages/runtime-vapor/src/components/Teleport.ts +++ b/packages/runtime-vapor/src/components/Teleport.ts @@ -30,13 +30,7 @@ export const VaporTeleportImpl = { ? new TeleportFragment('teleport') : new TeleportFragment() - const resolvedProps = new Proxy( - props, - rawPropsProxyHandlers, - ) as any as TeleportProps - let children: Block - renderEffect(() => { frag.updateChildren( (children = slots.default && (slots.default as BlockFn)()), @@ -44,21 +38,28 @@ export const VaporTeleportImpl = { }) renderEffect(() => { - // access the props to trigger tracking - frag.update(extend({}, resolvedProps), children!) + frag.update( + // access the props to trigger tracking + extend( + {}, + new Proxy(props, rawPropsProxyHandlers) as any as TeleportProps, + ), + children!, + ) }) return frag }, } -export class TeleportFragment extends VaporFragment { +class TeleportFragment extends VaporFragment { anchor: Node - targetStart?: Node | null - mainAnchor?: Node - placeholder?: Node - container?: ParentNode | null - currentAnchor?: Node | null + + private targetStart?: Node + private mainAnchor?: Node + private placeholder?: Node + private mountContainer?: ParentNode | null + private mountAnchor?: Node | null constructor(anchorLabel?: string) { super([]) @@ -66,32 +67,37 @@ export class TeleportFragment extends VaporFragment { __DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode() } - updateChildren(children: Block): void { - const parent = this.anchor.parentNode - if (!parent) return + get currentParent(): ParentNode { + return (this.mountContainer || this.parent)! + } - const container = this.container || parent + get currentAnchor(): Node | null { + return this.mountAnchor || this.anchor + } - // teardown previous - remove(this.nodes, container) + get parent(): ParentNode | null { + return this.anchor.parentNode + } + + updateChildren(children: Block): void { + // not mounted yet, early return + if (!this.parent) return - insert( - (this.nodes = children), - container, - this.currentAnchor || this.anchor, - ) + // teardown previous children + remove(this.nodes, this.currentParent) + + // mount new + insert((this.nodes = children), this.currentParent, this.currentAnchor) } update(props: TeleportProps, children: Block): void { - const parent = this.anchor.parentNode this.nodes = children - const disabled = isTeleportDisabled(props) const mount = (parent: ParentNode, anchor: Node | null) => { insert( this.nodes, - (this.container = parent), - (this.currentAnchor = anchor), + (this.mountContainer = parent), + (this.mountAnchor = anchor), ) } @@ -99,10 +105,10 @@ export class TeleportFragment extends VaporFragment { const target = (this.target = resolveTarget(props, querySelector)) if (target) { if ( - // initial mount - !this.targetStart || + // initial mount into target + !this.targetAnchor || // target changed - this.targetStart.parentNode !== target + this.targetAnchor.parentNode !== target ) { ;[this.targetAnchor, this.targetStart] = prepareAnchor(target) } @@ -110,33 +116,36 @@ export class TeleportFragment extends VaporFragment { mount(target, this.targetAnchor!) } else if (__DEV__) { warn( - `Invalid Teleport target on ${this.targetStart ? 'update' : 'mount'}:`, + `Invalid Teleport target on ${this.targetAnchor ? 'update' : 'mount'}:`, target, `(${typeof target})`, ) } } - if (parent && disabled) { - if (!this.mainAnchor) { - this.mainAnchor = __DEV__ - ? createComment('teleport end') - : createTextNode() - } - if (!this.placeholder) { - this.placeholder = __DEV__ - ? createComment('teleport start') - : createTextNode() - } - if (!this.mainAnchor.isConnected) { - insert(this.placeholder, parent, this.anchor) - insert(this.mainAnchor, parent, this.anchor) - } + // mount into main container + if (isTeleportDisabled(props)) { + if (this.parent) { + if (!this.mainAnchor) { + this.mainAnchor = __DEV__ + ? createComment('teleport end') + : createTextNode() + } + if (!this.placeholder) { + this.placeholder = __DEV__ + ? createComment('teleport start') + : createTextNode() + } + if (!this.mainAnchor.isConnected) { + insert(this.placeholder, this.parent, this.anchor) + insert(this.mainAnchor, this.parent, this.anchor) + } - mount(parent, this.mainAnchor) + mount(this.parent, this.mainAnchor) + } } - - if (!disabled) { + // mount into target container + else { if (isTeleportDeferred(props)) { queuePostFlushCb(mountToTarget) } else { @@ -147,13 +156,12 @@ export class TeleportFragment extends VaporFragment { remove = (parent: ParentNode | undefined): void => { // remove nodes - remove(this.nodes, this.container || parent) + remove(this.nodes, this.currentParent) // remove anchors if (this.targetStart) { - let parentNode = this.targetStart.parentNode! - remove(this.targetStart!, parentNode) - remove(this.targetAnchor!, parentNode) + remove(this.targetStart!, this.target!) + remove(this.targetAnchor!, this.target!) } if (this.placeholder) { remove(this.placeholder!, parent) @@ -167,12 +175,11 @@ export class TeleportFragment extends VaporFragment { } function prepareAnchor(target: ParentNode | null) { - const targetStart = createTextNode('') + const targetStart = createTextNode('') as Text & { [TeleportEndKey]: Node } const targetAnchor = createTextNode('') // attach a special property, so we can skip teleported content in // renderer's nextSibling search - // @ts-expect-error targetStart[TeleportEndKey] = targetAnchor if (target) { From 17317c5bd588c2e116ad504f7edf063da5407b1c Mon Sep 17 00:00:00 2001 From: daiwei Date: Mon, 24 Mar 2025 13:55:17 +0800 Subject: [PATCH 07/40] wip: port tests --- .../__tests__/components/Teleport.spec.ts | 275 +++++++++++++++++- 1 file changed, 261 insertions(+), 14 deletions(-) diff --git a/packages/runtime-vapor/__tests__/components/Teleport.spec.ts b/packages/runtime-vapor/__tests__/components/Teleport.spec.ts index bc77be2dc01..86b1ec62cf1 100644 --- a/packages/runtime-vapor/__tests__/components/Teleport.spec.ts +++ b/packages/runtime-vapor/__tests__/components/Teleport.spec.ts @@ -4,14 +4,24 @@ import { createComponent as originalCreateComponent, } from '../../src/component' import { + type VaporDirective, VaporTeleport, + createIf, createTemplateRefSetter, setInsertionState, template, + withVaporDirectives, } from '@vue/runtime-vapor' import { makeRender } from '../_utils' -import { nextTick, onBeforeUnmount, onUnmounted, ref, shallowRef } from 'vue' +import { + nextTick, + onBeforeUnmount, + onMounted, + onUnmounted, + ref, + shallowRef, +} from 'vue' const define = makeRender() @@ -410,17 +420,254 @@ function runSharedTests(deferMode: boolean): void { expect(target.innerHTML).toBe('
teleported
') }) - test.todo('moving teleport while disabled', async () => {}) - test.todo('should work with block tree', async () => {}) - test.todo( - `the dir hooks of the Teleport's children should be called correctly`, - async () => {}, - ) - test.todo( - `ensure that target changes when disabled are updated correctly when enabled`, - async () => {}, - ) - test.todo('toggle sibling node inside target node', async () => {}) - test.todo('unmount previous sibling node inside target node', async () => {}) - test.todo('accessing template refs inside teleport', async () => {}) + test.todo('moving teleport while disabled', async () => { + const target = document.createElement('div') + const root = document.createElement('div') + + const child1 = createComponent( + VaporTeleport, + { to: () => target, disabled: () => true }, + { default: () => template('
teleported
')() }, + ) + const child2 = template('
root
')() + + const children = shallowRef([child1, child2]) + const { mount } = define({ + setup() { + return children.value + }, + }).create() + mount(root) + + expect(root.innerHTML).toBe( + '
teleported
root
', + ) + expect(target.innerHTML).toBe('') + + children.value = [child2, child1] + await nextTick() + expect(root.innerHTML).toBe( + '
root
teleported
', + ) + expect(target.innerHTML).toBe('') + }) + + test(`the dir hooks of the Teleport's children should be called correctly`, async () => { + const target = document.createElement('div') + const root = document.createElement('div') + const toggle = ref(true) + + const spy = vi.fn() + const teardown = vi.fn() + const dir: VaporDirective = vi.fn((el, source) => { + spy() + return teardown + }) + + const { mount } = define({ + setup() { + return createComponent( + VaporTeleport, + { + to: () => target, + }, + { + default: () => { + return createIf( + () => toggle.value, + () => { + const n1 = template('
foo
')() as any + withVaporDirectives(n1, [[dir]]) + return n1 + }, + ) + }, + }, + ) + }, + }).create() + + mount(root) + expect(root.innerHTML).toBe('') + expect(target.innerHTML).toBe('
foo
') + expect(spy).toHaveBeenCalledTimes(1) + expect(teardown).not.toHaveBeenCalled() + + toggle.value = false + await nextTick() + expect(root.innerHTML).toBe('') + expect(target.innerHTML).toBe('') + expect(spy).toHaveBeenCalledTimes(1) + expect(teardown).toHaveBeenCalledTimes(1) + }) + + test(`ensure that target changes when disabled are updated correctly when enabled`, async () => { + const root = document.createElement('div') + const target1 = document.createElement('div') + const target2 = document.createElement('div') + const target3 = document.createElement('div') + const target = ref(target1) + const disabled = ref(true) + + const { mount } = define({ + setup() { + return createComponent( + VaporTeleport, + { + to: () => target.value, + disabled: () => disabled.value, + }, + { + default: () => template('
teleported
')(), + }, + ) + }, + }).create() + mount(root) + + disabled.value = false + await nextTick() + expect(target1.innerHTML).toBe('
teleported
') + expect(target2.innerHTML).toBe('') + expect(target3.innerHTML).toBe('') + + disabled.value = true + await nextTick() + target.value = target2 + await nextTick() + expect(target1.innerHTML).toBe('') + expect(target2.innerHTML).toBe('') + expect(target3.innerHTML).toBe('') + + target.value = target3 + await nextTick() + expect(target1.innerHTML).toBe('') + expect(target2.innerHTML).toBe('') + expect(target3.innerHTML).toBe('') + + disabled.value = false + await nextTick() + expect(target1.innerHTML).toBe('') + expect(target2.innerHTML).toBe('') + expect(target3.innerHTML).toBe('
teleported
') + }) + + test('toggle sibling node inside target node', async () => { + const root = document.createElement('div') + const show = ref(false) + const { mount } = define({ + setup() { + return createIf( + () => show.value, + () => { + return createComponent( + VaporTeleport, + { + to: () => root, + }, + { + default: () => template('
teleported
')(), + }, + ) + }, + () => { + return template('
foo
')() + }, + ) + }, + }).create() + + mount(root) + expect(root.innerHTML).toBe('
foo
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('
teleported
') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
foo
') + }) + + test('unmount previous sibling node inside target node', async () => { + const root = document.createElement('div') + const parentShow = ref(false) + const childShow = ref(true) + + const { component: Comp } = define({ + setup() { + return createComponent( + VaporTeleport, + { to: () => root }, + { + default: () => { + return template('
foo
')() + }, + }, + ) + }, + }) + + const { mount } = define({ + setup() { + return createIf( + () => parentShow.value, + () => + createIf( + () => childShow.value, + () => createComponent(Comp), + () => template('bar')(), + ), + () => template('foo')(), + ) + }, + }).create() + + mount(root) + expect(root.innerHTML).toBe('foo') + + parentShow.value = true + await nextTick() + expect(root.innerHTML).toBe( + '
foo
', + ) + + parentShow.value = false + await nextTick() + expect(root.innerHTML).toBe('foo') + }) + + test('accessing template refs inside teleport', async () => { + const target = document.createElement('div') + const tRef = ref() + let tRefInMounted + + const { mount } = define({ + setup() { + onMounted(() => { + tRefInMounted = tRef.value + }) + const n1 = createComponent( + VaporTeleport, + { + to: () => target, + }, + { + default: () => { + const setTemplateRef = createTemplateRefSetter() + const n0 = template('
teleported
')() as any + setTemplateRef(n0, tRef) + return n0 + }, + }, + ) + return n1 + }, + }).create() + mount(target) + + const child = target.children[0] + expect(child.outerHTML).toBe(`
teleported
`) + expect(tRefInMounted).toBe(child) + }) } From b232e456f444551df4a5df2d4bbce125cece2303 Mon Sep 17 00:00:00 2001 From: daiwei Date: Mon, 24 Mar 2025 14:17:52 +0800 Subject: [PATCH 08/40] wip: remove unnecessary tests --- .../__tests__/components/Teleport.spec.ts | 60 ------------------- 1 file changed, 60 deletions(-) diff --git a/packages/runtime-vapor/__tests__/components/Teleport.spec.ts b/packages/runtime-vapor/__tests__/components/Teleport.spec.ts index 86b1ec62cf1..27c1682fa4f 100644 --- a/packages/runtime-vapor/__tests__/components/Teleport.spec.ts +++ b/packages/runtime-vapor/__tests__/components/Teleport.spec.ts @@ -392,66 +392,6 @@ function runSharedTests(deferMode: boolean): void { expect(target.innerHTML).toBe('
teleported
') }) - test.todo('moving teleport while enabled', async () => { - const target = document.createElement('div') - const root = document.createElement('div') - - const child1 = createComponent( - VaporTeleport, - { to: () => target }, - { default: () => template('
teleported
')() }, - ) - const child2 = template('
root
')() - - const children = shallowRef([child1, child2]) - const { mount } = define({ - setup() { - return children.value - }, - }).create() - mount(root) - - expect(root.innerHTML).toBe('
root
') - expect(target.innerHTML).toBe('
teleported
') - - children.value = [child2, child1] - await nextTick() - expect(root.innerHTML).toBe('
root
') - expect(target.innerHTML).toBe('
teleported
') - }) - - test.todo('moving teleport while disabled', async () => { - const target = document.createElement('div') - const root = document.createElement('div') - - const child1 = createComponent( - VaporTeleport, - { to: () => target, disabled: () => true }, - { default: () => template('
teleported
')() }, - ) - const child2 = template('
root
')() - - const children = shallowRef([child1, child2]) - const { mount } = define({ - setup() { - return children.value - }, - }).create() - mount(root) - - expect(root.innerHTML).toBe( - '
teleported
root
', - ) - expect(target.innerHTML).toBe('') - - children.value = [child2, child1] - await nextTick() - expect(root.innerHTML).toBe( - '
root
teleported
', - ) - expect(target.innerHTML).toBe('') - }) - test(`the dir hooks of the Teleport's children should be called correctly`, async () => { const target = document.createElement('div') const root = document.createElement('div') From 97e6174c41097f59c1a249ce2516a263c5c4321e Mon Sep 17 00:00:00 2001 From: daiwei Date: Mon, 24 Mar 2025 14:27:46 +0800 Subject: [PATCH 09/40] wip: handle vapor teleport --- packages/compiler-vapor/src/generators/component.ts | 10 +++++++++- .../compiler-vapor/src/transforms/transformElement.ts | 8 +++++++- packages/compiler-vapor/src/utils.ts | 11 +++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/compiler-vapor/src/generators/component.ts b/packages/compiler-vapor/src/generators/component.ts index 7c232db754b..7dbf8c9a2f1 100644 --- a/packages/compiler-vapor/src/generators/component.ts +++ b/packages/compiler-vapor/src/generators/component.ts @@ -39,6 +39,7 @@ import { genEventHandler } from './event' import { genDirectiveModifiers, genDirectivesForElement } from './directive' import { genBlock } from './block' import { genModelHandler } from './vModel' +import { isBuiltInComponent } from '../utils' export function genCreateComponent( operation: CreateComponentIRNode, @@ -92,8 +93,15 @@ export function genCreateComponent( } else if (operation.asset) { return toValidAssetId(operation.tag, 'component') } else { + const { tag } = operation + const builtInTag = isBuiltInComponent(tag) + if (builtInTag) { + // @ts-expect-error + helper(builtInTag) + return `_${builtInTag}` + } return genExpression( - extend(createSimpleExpression(operation.tag, false), { ast: null }), + extend(createSimpleExpression(tag, false), { ast: null }), context, ) } diff --git a/packages/compiler-vapor/src/transforms/transformElement.ts b/packages/compiler-vapor/src/transforms/transformElement.ts index dceb3fd6121..07f88ae0c58 100644 --- a/packages/compiler-vapor/src/transforms/transformElement.ts +++ b/packages/compiler-vapor/src/transforms/transformElement.ts @@ -36,7 +36,7 @@ import { type VaporDirectiveNode, } from '../ir' import { EMPTY_EXPRESSION } from './utils' -import { findProp } from '../utils' +import { findProp, isBuiltInComponent } from '../utils' export const isReservedProp: (key: string) => boolean = /*#__PURE__*/ makeMap( // the leading comma is intentional so empty string "" is also included @@ -109,6 +109,12 @@ function transformComponentElement( asset = false } + const builtInTag = isBuiltInComponent(tag) + if (builtInTag) { + tag = builtInTag + asset = false + } + const dotIndex = tag.indexOf('.') if (dotIndex > 0) { const ns = resolveSetupReference(tag.slice(0, dotIndex), context) diff --git a/packages/compiler-vapor/src/utils.ts b/packages/compiler-vapor/src/utils.ts index 728281914fd..9b99ef869cf 100644 --- a/packages/compiler-vapor/src/utils.ts +++ b/packages/compiler-vapor/src/utils.ts @@ -88,3 +88,14 @@ export function getLiteralExpressionValue( } return exp.isStatic ? exp.content : null } + +export function isTeleportTag(tag: string): boolean { + tag = tag.toLowerCase() + return tag === 'teleport' || tag === 'vaporteleport' +} + +export function isBuiltInComponent(tag: string): string | undefined { + if (isTeleportTag(tag)) { + return 'VaporTeleport' + } +} From 33830a07450c50f89b1b90faa0fa6b6a46810e82 Mon Sep 17 00:00:00 2001 From: daiwei Date: Mon, 24 Mar 2025 15:11:00 +0800 Subject: [PATCH 10/40] wip: save --- packages/runtime-vapor/src/components/Teleport.ts | 11 +++++++++++ packages/runtime-vapor/src/index.ts | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/runtime-vapor/src/components/Teleport.ts b/packages/runtime-vapor/src/components/Teleport.ts index 823f6c5cca2..ba47442500e 100644 --- a/packages/runtime-vapor/src/components/Teleport.ts +++ b/packages/runtime-vapor/src/components/Teleport.ts @@ -189,3 +189,14 @@ function prepareAnchor(target: ParentNode | null) { return [targetAnchor, targetStart] } + +export const VaporTeleport = VaporTeleportImpl as unknown as { + __vapor: true + __isTeleport: true + new (): { + $props: TeleportProps + $slots: { + default(): Block + } + } +} diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index 4b55949a63a..2edceb54912 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -3,7 +3,7 @@ export { createVaporApp, createVaporSSRApp } from './apiCreateApp' export { defineVaporComponent } from './apiDefineComponent' export { vaporInteropPlugin } from './vdomInterop' export type { VaporDirective } from './directives/custom' -export { VaporTeleportImpl as VaporTeleport } from './components/Teleport' +export { VaporTeleport } from './components/Teleport' // compiler-use only export { insert, prepend, remove, isFragment, VaporFragment } from './block' From 098f50d5a173f2ba24b1053804b675c1eb1cc8a8 Mon Sep 17 00:00:00 2001 From: daiwei Date: Mon, 24 Mar 2025 17:28:11 +0800 Subject: [PATCH 11/40] wip: handing teleport hmr updating --- packages/runtime-vapor/src/component.ts | 8 ++++++++ packages/runtime-vapor/src/hmr.ts | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 7989b67a8b6..2855fa9ce96 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -172,6 +172,13 @@ export function createComponent( frag.hydrate() } + if (__DEV__) { + const instance = currentInstance as VaporComponentInstance + ;(instance!.hmrEffects || (instance!.hmrEffects = [])).push(() => + frag.remove(frag.anchor.parentNode!), + ) + } + return frag as any } @@ -389,6 +396,7 @@ export class VaporComponentInstance implements GenericComponentInstance { devtoolsRawSetupState?: any hmrRerender?: () => void hmrReload?: (newComp: VaporComponent) => void + hmrEffects?: (() => void)[] propsOptions?: NormalizedPropsOptions emitsOptions?: ObjectEmitsOptions | null isSingleRoot?: boolean diff --git a/packages/runtime-vapor/src/hmr.ts b/packages/runtime-vapor/src/hmr.ts index 741f385861d..c960a2610cf 100644 --- a/packages/runtime-vapor/src/hmr.ts +++ b/packages/runtime-vapor/src/hmr.ts @@ -19,6 +19,10 @@ export function hmrRerender(instance: VaporComponentInstance): void { const parent = normalized[0].parentNode! const anchor = normalized[normalized.length - 1].nextSibling remove(instance.block, parent) + if (instance.hmrEffects) { + instance.hmrEffects.forEach(e => e()) + instance.hmrEffects.length = 0 + } const prev = currentInstance simpleSetCurrentInstance(instance) pushWarningContext(instance) @@ -36,6 +40,10 @@ export function hmrReload( const parent = normalized[0].parentNode! const anchor = normalized[normalized.length - 1].nextSibling unmountComponent(instance, parent) + if (instance.hmrEffects) { + instance.hmrEffects.forEach(e => e()) + instance.hmrEffects.length = 0 + } const prev = currentInstance simpleSetCurrentInstance(instance.parent) const newInstance = createComponent( From 51ca617632adf28d91df4fc6a0fa3ddbd4002faa Mon Sep 17 00:00:00 2001 From: daiwei Date: Mon, 24 Mar 2025 21:14:46 +0800 Subject: [PATCH 12/40] test: add e2e tests for vdom interop --- .../vapor-e2e-test/__tests__/teleport.spec.ts | 61 +++++++++++++++++++ .../vapor-e2e-test/__tests__/todomvc.spec.ts | 3 +- .../__tests__/vdomInterop.spec.ts | 3 +- packages-private/vapor-e2e-test/index.html | 1 + .../vapor-e2e-test/teleport/App.vue | 17 ++++++ .../teleport/components/VdomComp.vue | 7 +++ .../vapor-e2e-test/teleport/index.html | 2 + .../vapor-e2e-test/teleport/main.ts | 5 ++ packages-private/vapor-e2e-test/utils.ts | 6 ++ .../vapor-e2e-test/vite.config.ts | 1 + 10 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 packages-private/vapor-e2e-test/__tests__/teleport.spec.ts create mode 100644 packages-private/vapor-e2e-test/teleport/App.vue create mode 100644 packages-private/vapor-e2e-test/teleport/components/VdomComp.vue create mode 100644 packages-private/vapor-e2e-test/teleport/index.html create mode 100644 packages-private/vapor-e2e-test/teleport/main.ts create mode 100644 packages-private/vapor-e2e-test/utils.ts diff --git a/packages-private/vapor-e2e-test/__tests__/teleport.spec.ts b/packages-private/vapor-e2e-test/__tests__/teleport.spec.ts new file mode 100644 index 00000000000..dc3efa13a47 --- /dev/null +++ b/packages-private/vapor-e2e-test/__tests__/teleport.spec.ts @@ -0,0 +1,61 @@ +import path from 'node:path' +import { + E2E_TIMEOUT, + setupPuppeteer, +} from '../../../packages/vue/__tests__/e2e/e2eUtils' +import connect from 'connect' +import sirv from 'sirv' +import { nextTick } from 'vue' +import { ports } from '../utils' +const { page, click, html } = setupPuppeteer() + +describe('vdom / vapor interop', () => { + let server: any + const port = ports.teleport + beforeAll(() => { + server = connect() + .use(sirv(path.resolve(import.meta.dirname, '../dist'))) + .listen(port) + process.on('SIGTERM', () => server && server.close()) + }) + + afterAll(() => { + server.close() + }) + + beforeEach(async () => { + const baseUrl = `http://localhost:${port}/teleport/` + await page().goto(baseUrl) + await page().waitForSelector('#app') + }) + + describe('vapor teleport', () => { + test( + 'render vdom component', + async () => { + const targetSelector = '.target' + const testSelector = '.interop-render-vdom-comp' + const containerSelector = `${testSelector} > div` + const btnSelector = `${testSelector} > button` + + // teleport is disabled + expect(await html(containerSelector)).toBe('

vdom comp

') + expect(await html(targetSelector)).toBe('') + + // enable teleport + await click(btnSelector) + await nextTick() + + expect(await html(containerSelector)).toBe('') + expect(await html(targetSelector)).toBe('

vdom comp

') + + // disable teleport + await click(btnSelector) + await nextTick() + expect(await html(containerSelector)).toBe('

vdom comp

') + expect(await html(targetSelector)).toBe('') + }, + E2E_TIMEOUT, + ) + }) +}) diff --git a/packages-private/vapor-e2e-test/__tests__/todomvc.spec.ts b/packages-private/vapor-e2e-test/__tests__/todomvc.spec.ts index 3de8392e5e2..035691fd69b 100644 --- a/packages-private/vapor-e2e-test/__tests__/todomvc.spec.ts +++ b/packages-private/vapor-e2e-test/__tests__/todomvc.spec.ts @@ -5,6 +5,7 @@ import { } from '../../../packages/vue/__tests__/e2e/e2eUtils' import connect from 'connect' import sirv from 'sirv' +import { ports } from '../utils' describe('e2e: todomvc', () => { const { @@ -23,7 +24,7 @@ describe('e2e: todomvc', () => { } = setupPuppeteer() let server: any - const port = '8194' + const port = ports.todomvc beforeAll(() => { server = connect() .use(sirv(path.resolve(import.meta.dirname, '../dist'))) diff --git a/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts b/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts index 360f48085a1..a3069d1aed2 100644 --- a/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts +++ b/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts @@ -5,12 +5,13 @@ import { } from '../../../packages/vue/__tests__/e2e/e2eUtils' import connect from 'connect' import sirv from 'sirv' +import { ports } from '../utils' describe('vdom / vapor interop', () => { const { page, click, text, enterValue } = setupPuppeteer() let server: any - const port = '8193' + const port = ports.vdomInterop beforeAll(() => { server = connect() .use(sirv(path.resolve(import.meta.dirname, '../dist'))) diff --git a/packages-private/vapor-e2e-test/index.html b/packages-private/vapor-e2e-test/index.html index 7dc205e5ab0..bb1234e8e10 100644 --- a/packages-private/vapor-e2e-test/index.html +++ b/packages-private/vapor-e2e-test/index.html @@ -1,2 +1,3 @@ VDOM / Vapor interop Vapor TodoMVC +Vapor Teleport diff --git a/packages-private/vapor-e2e-test/teleport/App.vue b/packages-private/vapor-e2e-test/teleport/App.vue new file mode 100644 index 00000000000..d2aeba8e1f9 --- /dev/null +++ b/packages-private/vapor-e2e-test/teleport/App.vue @@ -0,0 +1,17 @@ + + + diff --git a/packages-private/vapor-e2e-test/teleport/components/VdomComp.vue b/packages-private/vapor-e2e-test/teleport/components/VdomComp.vue new file mode 100644 index 00000000000..6eba9134ef6 --- /dev/null +++ b/packages-private/vapor-e2e-test/teleport/components/VdomComp.vue @@ -0,0 +1,7 @@ + + + diff --git a/packages-private/vapor-e2e-test/teleport/index.html b/packages-private/vapor-e2e-test/teleport/index.html new file mode 100644 index 00000000000..79052a023ba --- /dev/null +++ b/packages-private/vapor-e2e-test/teleport/index.html @@ -0,0 +1,2 @@ + +
diff --git a/packages-private/vapor-e2e-test/teleport/main.ts b/packages-private/vapor-e2e-test/teleport/main.ts new file mode 100644 index 00000000000..2e962efe731 --- /dev/null +++ b/packages-private/vapor-e2e-test/teleport/main.ts @@ -0,0 +1,5 @@ +import { createVaporApp, vaporInteropPlugin } from 'vue' +import App from './App.vue' +import 'todomvc-app-css/index.css' + +createVaporApp(App).use(vaporInteropPlugin).mount('#app') diff --git a/packages-private/vapor-e2e-test/utils.ts b/packages-private/vapor-e2e-test/utils.ts new file mode 100644 index 00000000000..a42064b7050 --- /dev/null +++ b/packages-private/vapor-e2e-test/utils.ts @@ -0,0 +1,6 @@ +// make sure these ports are unique +export const ports = { + vdomInterop: 8193, + todomvc: 8194, + teleport: 8195, +} diff --git a/packages-private/vapor-e2e-test/vite.config.ts b/packages-private/vapor-e2e-test/vite.config.ts index 1e29a4dbd13..a2816f4b6db 100644 --- a/packages-private/vapor-e2e-test/vite.config.ts +++ b/packages-private/vapor-e2e-test/vite.config.ts @@ -14,6 +14,7 @@ export default defineConfig({ input: { interop: resolve(import.meta.dirname, 'interop/index.html'), todomvc: resolve(import.meta.dirname, 'todomvc/index.html'), + teleport: resolve(import.meta.dirname, 'teleport/index.html'), }, }, }, From 90c2e20e169251d05d78a51e8e0e5d63eee89768 Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 25 Mar 2025 11:10:50 +0800 Subject: [PATCH 13/40] wip: hmr updating --- packages/runtime-vapor/src/component.ts | 7 ++- .../runtime-vapor/src/components/Teleport.ts | 55 +++++++++++++------ 2 files changed, 42 insertions(+), 20 deletions(-) diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 2855fa9ce96..4d5c6d0b782 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -172,11 +172,12 @@ export function createComponent( frag.hydrate() } + // remove the teleport content from the parent tree for HMR updates if (__DEV__) { const instance = currentInstance as VaporComponentInstance - ;(instance!.hmrEffects || (instance!.hmrEffects = [])).push(() => - frag.remove(frag.anchor.parentNode!), - ) + ;(instance!.hmrEffects || (instance!.hmrEffects = [])).push(() => { + frag.remove(frag.anchor.parentNode!) + }) } return frag as any diff --git a/packages/runtime-vapor/src/components/Teleport.ts b/packages/runtime-vapor/src/components/Teleport.ts index ba47442500e..8a036578204 100644 --- a/packages/runtime-vapor/src/components/Teleport.ts +++ b/packages/runtime-vapor/src/components/Teleport.ts @@ -19,6 +19,7 @@ import type { LooseRawProps, LooseRawSlots } from '../component' import { rawPropsProxyHandlers } from '../componentProps' import { renderEffect } from '../renderEffect' import { extend } from '@vue/shared' +import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity' export const VaporTeleportImpl = { name: 'VaporTeleport', @@ -30,23 +31,28 @@ export const VaporTeleportImpl = { ? new TeleportFragment('teleport') : new TeleportFragment() - let children: Block - renderEffect(() => { - frag.updateChildren( - (children = slots.default && (slots.default as BlockFn)()), - ) - }) - - renderEffect(() => { - frag.update( - // access the props to trigger tracking - extend( - {}, - new Proxy(props, rawPropsProxyHandlers) as any as TeleportProps, - ), - children!, - ) + pauseTracking() + const scope = (frag.scope = new EffectScope()) + scope!.run(() => { + let children: Block + renderEffect(() => { + frag.updateChildren( + (children = slots.default && (slots.default as BlockFn)()), + ) + }) + + renderEffect(() => { + frag.update( + // access the props to trigger tracking + extend( + {}, + new Proxy(props, rawPropsProxyHandlers) as any as TeleportProps, + ), + children!, + ) + }) }) + resetTracking() return frag }, @@ -54,6 +60,7 @@ export const VaporTeleportImpl = { class TeleportFragment extends VaporFragment { anchor: Node + scope: EffectScope | undefined private targetStart?: Node private mainAnchor?: Node @@ -155,17 +162,31 @@ class TeleportFragment extends VaporFragment { } remove = (parent: ParentNode | undefined): void => { + // stop effect scope + if (this.scope) { + this.scope.stop() + this.scope = undefined + } + // remove nodes - remove(this.nodes, this.currentParent) + if (this.nodes) { + remove(this.nodes, this.currentParent) + this.nodes = [] + } // remove anchors if (this.targetStart) { remove(this.targetStart!, this.target!) + this.targetStart = undefined remove(this.targetAnchor!, this.target!) + this.targetAnchor = undefined } + if (this.placeholder) { remove(this.placeholder!, parent) + this.placeholder = undefined remove(this.mainAnchor!, parent) + this.mainAnchor = undefined } } From ec76aec619a0006054a60ecf689dd6c49045c176 Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 25 Mar 2025 17:21:50 +0800 Subject: [PATCH 14/40] wip: test hmr updating --- packages/runtime-core/src/hmr.ts | 2 +- .../__tests__/components/Teleport.spec.ts | 146 +++++++++++++++++- packages/runtime-vapor/src/block.ts | 6 +- .../runtime-vapor/src/components/Teleport.ts | 7 + packages/runtime-vapor/src/hmr.ts | 1 + 5 files changed, 156 insertions(+), 6 deletions(-) diff --git a/packages/runtime-core/src/hmr.ts b/packages/runtime-core/src/hmr.ts index ed5d8b081a0..acc9593d137 100644 --- a/packages/runtime-core/src/hmr.ts +++ b/packages/runtime-core/src/hmr.ts @@ -119,7 +119,7 @@ function reload(id: string, newComp: HMRComponent): void { // create a snapshot which avoids the set being mutated during updates const instances = [...record.instances] - if (newComp.vapor) { + if (newComp.__vapor) { for (const instance of instances) { instance.hmrReload!(newComp) } diff --git a/packages/runtime-vapor/__tests__/components/Teleport.spec.ts b/packages/runtime-vapor/__tests__/components/Teleport.spec.ts index 27c1682fa4f..cae00f75fd7 100644 --- a/packages/runtime-vapor/__tests__/components/Teleport.spec.ts +++ b/packages/runtime-vapor/__tests__/components/Teleport.spec.ts @@ -1,7 +1,7 @@ import { type LooseRawProps, type VaporComponent, - createComponent as originalCreateComponent, + createComponent as createComp, } from '../../src/component' import { type VaporDirective, @@ -12,7 +12,6 @@ import { template, withVaporDirectives, } from '@vue/runtime-vapor' - import { makeRender } from '../_utils' import { nextTick, @@ -23,6 +22,10 @@ import { shallowRef, } from 'vue' +import type { HMRRuntime } from '@vue/runtime-dom' +declare var __VUE_HMR_RUNTIME__: HMRRuntime +const { createRecord, rerender, reload } = __VUE_HMR_RUNTIME__ + const define = makeRender() describe('renderer: VaporTeleport', () => { @@ -33,6 +36,141 @@ describe('renderer: VaporTeleport', () => { describe('defer mode', () => { runSharedTests(true) }) + + describe('HMR', () => { + test('rerender', async () => { + const target = document.createElement('div') + const root = document.createElement('div') + const childId = 'test1-child' + const parentId = 'test1-parent' + + const { component: Child } = define({ + __hmrId: childId, + render() { + return template('
teleported
')() + }, + }) + createRecord(childId, Child as any) + + const { mount, component: Parent } = define({ + __hmrId: parentId, + render() { + const n0 = createComp( + VaporTeleport, + { + to: () => target, + }, + { + default: () => createComp(Child), + }, + ) + const n1 = template('
root
')() + return [n0, n1] + }, + }).create() + createRecord(parentId, Parent as any) + mount(root) + + expect(root.innerHTML).toBe('
root
') + expect(target.innerHTML).toBe('
teleported
') + + // rerender child + rerender(childId, () => { + return template('
teleported 2
')() + }) + + expect(root.innerHTML).toBe('
root
') + expect(target.innerHTML).toBe('
teleported 2
') + + // rerender parent + rerender(parentId, () => { + const n0 = createComp( + VaporTeleport, + { + to: () => target, + }, + { + default: () => createComp(Child), + }, + ) + const n1 = template('
root 2
')() + return [n0, n1] + }) + + expect(root.innerHTML).toBe('
root 2
') + expect(target.innerHTML).toBe('
teleported 2
') + }) + + test('reload', async () => { + const target = document.createElement('div') + const root = document.createElement('div') + const childId = 'test2-child' + const parentId = 'test2-parent' + + const { component: Child } = define({ + __hmrId: childId, + render() { + return template('
teleported
')() + }, + }) + createRecord(childId, Child as any) + + const { mount, component: Parent } = define({ + __hmrId: parentId, + render() { + const n0 = createComp( + VaporTeleport, + { + to: () => target, + }, + { + default: () => createComp(Child), + }, + ) + const n1 = template('
root
')() + return [n0, n1] + }, + }).create() + createRecord(parentId, Parent as any) + mount(root) + + expect(root.innerHTML).toBe('
root
') + expect(target.innerHTML).toBe('
teleported
') + + // reload child + reload(childId, { + __hmrId: childId, + __vapor: true, + render() { + return template('
teleported 2
')() + }, + }) + expect(root.innerHTML).toBe('
root
') + expect(target.innerHTML).toBe('
teleported 2
') + + // reload parent + reload(parentId, { + __hmrId: parentId, + __vapor: true, + render() { + const n0 = createComp( + VaporTeleport, + { + to: () => target, + }, + { + default: () => createComp(Child), + }, + ) + const n1 = template('
root 2
')() + return [n0, n1] + }, + }) + + expect(root.innerHTML).toBe('
root 2
') + expect(target.innerHTML).toBe('
teleported 2
') + }) + }) }) function runSharedTests(deferMode: boolean): void { @@ -45,9 +183,9 @@ function runSharedTests(deferMode: boolean): void { if (component === VaporTeleport) { rawProps!.defer = () => true } - return originalCreateComponent(component, rawProps, ...args) + return createComp(component, rawProps, ...args) } - : originalCreateComponent + : createComp test('should work', () => { const target = document.createElement('div') diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index 6ec62da20d5..17ae7190923 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -184,7 +184,11 @@ export function normalizeBlock(block: Block): Node[] { } else if (isVaporComponent(block)) { nodes.push(...normalizeBlock(block.block!)) } else { - nodes.push(...normalizeBlock(block.nodes)) + if ((block as any).getNodes) { + nodes.push(...normalizeBlock((block as any).getNodes())) + } else { + nodes.push(...normalizeBlock(block.nodes)) + } block.anchor && nodes.push(block.anchor) } return nodes diff --git a/packages/runtime-vapor/src/components/Teleport.ts b/packages/runtime-vapor/src/components/Teleport.ts index 8a036578204..6c0b089bf00 100644 --- a/packages/runtime-vapor/src/components/Teleport.ts +++ b/packages/runtime-vapor/src/components/Teleport.ts @@ -54,6 +54,13 @@ export const VaporTeleportImpl = { }) resetTracking() + if (__DEV__) { + // TODO + ;(frag as any).getNodes = () => { + return frag.parent !== frag.currentParent ? [] : frag.nodes + } + } + return frag }, } diff --git a/packages/runtime-vapor/src/hmr.ts b/packages/runtime-vapor/src/hmr.ts index c960a2610cf..5df5d2a469d 100644 --- a/packages/runtime-vapor/src/hmr.ts +++ b/packages/runtime-vapor/src/hmr.ts @@ -54,4 +54,5 @@ export function hmrReload( ) simpleSetCurrentInstance(prev, instance.parent) mountComponent(newInstance, parent, anchor) + instance.block = newInstance.block } From f3154238bb112e2680f402ba56fe184598bf283b Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 25 Mar 2025 21:25:31 +0800 Subject: [PATCH 15/40] test: remove vapor mark --- .../vapor-e2e-test/teleport/components/VdomComp.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages-private/vapor-e2e-test/teleport/components/VdomComp.vue b/packages-private/vapor-e2e-test/teleport/components/VdomComp.vue index 6eba9134ef6..2c7a626f21a 100644 --- a/packages-private/vapor-e2e-test/teleport/components/VdomComp.vue +++ b/packages-private/vapor-e2e-test/teleport/components/VdomComp.vue @@ -1,4 +1,4 @@ - From 16b30d8ce5f75969b223c2ec64c4b6b49cb8fa96 Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 26 Mar 2025 08:21:58 +0800 Subject: [PATCH 16/40] test: port more tests --- .../__tests__/components/Teleport.spec.ts | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/packages/runtime-vapor/__tests__/components/Teleport.spec.ts b/packages/runtime-vapor/__tests__/components/Teleport.spec.ts index cae00f75fd7..0bf21bb7656 100644 --- a/packages/runtime-vapor/__tests__/components/Teleport.spec.ts +++ b/packages/runtime-vapor/__tests__/components/Teleport.spec.ts @@ -6,9 +6,13 @@ import { import { type VaporDirective, VaporTeleport, + child, createIf, createTemplateRefSetter, + defineVaporComponent, + renderEffect, setInsertionState, + setText, template, withVaporDirectives, } from '@vue/runtime-vapor' @@ -35,6 +39,86 @@ describe('renderer: VaporTeleport', () => { describe('defer mode', () => { runSharedTests(true) + + test('should be able to target content appearing later than the teleport with defer', () => { + const root = document.createElement('div') + document.body.appendChild(root) + + const { mount } = define({ + setup() { + const n1 = createComp( + VaporTeleport, + { + to: () => '#target', + defer: () => true, + }, + { + default: () => template('
teleported
')(), + }, + ) + const n2 = template('
')() + return [n1, n2] + }, + }).create() + mount(root) + + expect(root.innerHTML).toBe( + '
teleported
', + ) + }) + + test.todo('defer mode should work inside suspense', () => {}) + + test('update before mounted with defer', async () => { + const root = document.createElement('div') + document.body.appendChild(root) + + const show = ref(false) + const foo = ref('foo') + const Header = defineVaporComponent({ + props: { foo: String }, + setup(props) { + const n0 = template(`
`)() + const x0 = child(n0 as any) + renderEffect(() => setText(x0 as any, props.foo)) + return [n0] + }, + }) + const Footer = defineVaporComponent({ + setup() { + foo.value = 'bar' + return template('
Footer
')() + }, + }) + + const { mount } = define({ + setup() { + return createIf( + () => show.value, + () => { + const n1 = createComp( + VaporTeleport, + { to: () => '#targetId', defer: () => true }, + { default: () => createComp(Header, { foo: () => foo.value }) }, + ) + const n2 = createComp(Footer) + const n3 = template('
')() + return [n1, n2, n3] + }, + () => template('
')(), + ) + }, + }).create() + mount(root) + + expect(root.innerHTML).toBe('
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe( + `
Footer
bar
`, + ) + }) }) describe('HMR', () => { From b6468562e6e211519b4bd3fa3fa286cfcbef4a0f Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 26 Mar 2025 14:01:23 +0800 Subject: [PATCH 17/40] wip: save --- .../__tests__/components/Teleport.spec.ts | 184 +++++++++++++++++- 1 file changed, 174 insertions(+), 10 deletions(-) diff --git a/packages/runtime-vapor/__tests__/components/Teleport.spec.ts b/packages/runtime-vapor/__tests__/components/Teleport.spec.ts index 0bf21bb7656..20f5105472b 100644 --- a/packages/runtime-vapor/__tests__/components/Teleport.spec.ts +++ b/packages/runtime-vapor/__tests__/components/Teleport.spec.ts @@ -193,25 +193,40 @@ describe('renderer: VaporTeleport', () => { const { component: Child } = define({ __hmrId: childId, - render() { - return template('
teleported
')() + setup() { + const msg = ref('teleported') + return { msg } + }, + render(ctx) { + const n0 = template(`
`)() + const x0 = child(n0 as any) + renderEffect(() => setText(x0 as any, ctx.msg)) + return [n0] }, }) createRecord(childId, Child as any) const { mount, component: Parent } = define({ __hmrId: parentId, - render() { + setup() { + const msg = ref('root') + const disabled = ref(false) + return { msg, disabled } + }, + render(ctx) { const n0 = createComp( VaporTeleport, { to: () => target, + disabled: () => ctx.disabled, }, { default: () => createComp(Child), }, ) - const n1 = template('
root
')() + const n1 = template(`
`)() + const x0 = child(n1 as any) + renderEffect(() => setText(x0 as any, ctx.msg)) return [n0, n1] }, }).create() @@ -221,38 +236,187 @@ describe('renderer: VaporTeleport', () => { expect(root.innerHTML).toBe('
root
') expect(target.innerHTML).toBe('
teleported
') - // reload child + // reload child by changing msg reload(childId, { __hmrId: childId, __vapor: true, - render() { - return template('
teleported 2
')() + setup() { + const msg = ref('teleported 2') + return { msg } + }, + render(ctx: any) { + const n0 = template(`
`)() + const x0 = child(n0 as any) + renderEffect(() => setText(x0 as any, ctx.msg)) + return [n0] }, }) expect(root.innerHTML).toBe('
root
') expect(target.innerHTML).toBe('
teleported 2
') - // reload parent + // reload parent by changing msg reload(parentId, { __hmrId: parentId, __vapor: true, - render() { + setup() { + const msg = ref('root 2') + const disabled = ref(false) + return { msg, disabled } + }, + render(ctx: any) { const n0 = createComp( VaporTeleport, { to: () => target, + disabled: () => ctx.disabled, }, { default: () => createComp(Child), }, ) - const n1 = template('
root 2
')() + const n1 = template(`
`)() + const x0 = child(n1 as any) + renderEffect(() => setText(x0 as any, ctx.msg)) return [n0, n1] }, }) expect(root.innerHTML).toBe('
root 2
') expect(target.innerHTML).toBe('
teleported 2
') + + // reload parent again by changing disabled + reload(parentId, { + __hmrId: parentId, + __vapor: true, + setup() { + const msg = ref('root 2') + const disabled = ref(true) + return { msg, disabled } + }, + render(ctx: any) { + const n0 = createComp( + VaporTeleport, + { + to: () => target, + disabled: () => ctx.disabled, + }, + { + default: () => createComp(Child), + }, + ) + const n1 = template(`
`)() + const x0 = child(n1 as any) + renderEffect(() => setText(x0 as any, ctx.msg)) + return [n0, n1] + }, + }) + + expect(root.innerHTML).toBe( + '
teleported 2
root 2
', + ) + expect(target.innerHTML).toBe('') + }) + + test.todo('reload child + toggle disabled', async () => { + const target = document.createElement('div') + const root = document.createElement('div') + const childId = 'test3-child' + const parentId = 'test3-parent' + + const disabled = ref(true) + const { component: Child } = define({ + __hmrId: childId, + setup() { + const msg = ref('teleported') + return { msg } + }, + render(ctx) { + const n0 = template(`
`)() + const x0 = child(n0 as any) + renderEffect(() => setText(x0 as any, ctx.msg)) + return [n0] + }, + }) + createRecord(childId, Child as any) + + const { mount, component: Parent } = define({ + __hmrId: parentId, + setup() { + const msg = ref('root') + return { msg, disabled } + }, + render(ctx) { + const n0 = createComp( + VaporTeleport, + { + to: () => target, + disabled: () => ctx.disabled, + }, + { + default: () => createComp(Child), + }, + ) + const n1 = template(`
`)() + const x0 = child(n1 as any) + renderEffect(() => setText(x0 as any, ctx.msg)) + return [n0, n1] + }, + }).create() + createRecord(parentId, Parent as any) + mount(root) + + expect(root.innerHTML).toBe( + '
teleported
root
', + ) + expect(target.innerHTML).toBe('') + + // reload child by changing msg + reload(childId, { + __hmrId: childId, + __vapor: true, + setup() { + const msg = ref('teleported 2') + return { msg } + }, + render(ctx: any) { + const n0 = template(`
`)() + const x0 = child(n0 as any) + renderEffect(() => setText(x0 as any, ctx.msg)) + return [n0] + }, + }) + expect(root.innerHTML).toBe( + '
teleported 2
root
', + ) + expect(target.innerHTML).toBe('') + + // reload child again by changing msg + reload(childId, { + __hmrId: childId, + __vapor: true, + setup() { + const msg = ref('teleported 3') + return { msg } + }, + render(ctx: any) { + const n0 = template(`
`)() + const x0 = child(n0 as any) + renderEffect(() => setText(x0 as any, ctx.msg)) + return [n0] + }, + }) + expect(root.innerHTML).toBe( + '
teleported 3
root
', + ) + expect(target.innerHTML).toBe('') + + //bug: child reload not update teleport fragment's nodes + + // toggle disabled + disabled.value = false + await nextTick() + expect(root.innerHTML).toBe('
root
') + expect(target.innerHTML).toBe('
teleported 3
') }) }) }) From b474ce0a1e7d39d1884b23c44d684036a85fb3be Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 26 Mar 2025 17:30:32 +0800 Subject: [PATCH 18/40] wip: fix teleport root component hmr reload --- .../__tests__/components/Teleport.spec.ts | 4 +- packages/runtime-vapor/src/block.ts | 5 +- packages/runtime-vapor/src/component.ts | 13 ++++- .../runtime-vapor/src/components/Teleport.ts | 53 ++++++++++++++++--- packages/runtime-vapor/src/hmr.ts | 3 +- 5 files changed, 62 insertions(+), 16 deletions(-) diff --git a/packages/runtime-vapor/__tests__/components/Teleport.spec.ts b/packages/runtime-vapor/__tests__/components/Teleport.spec.ts index 20f5105472b..d5ff4142249 100644 --- a/packages/runtime-vapor/__tests__/components/Teleport.spec.ts +++ b/packages/runtime-vapor/__tests__/components/Teleport.spec.ts @@ -317,7 +317,7 @@ describe('renderer: VaporTeleport', () => { expect(target.innerHTML).toBe('') }) - test.todo('reload child + toggle disabled', async () => { + test('reload child + toggle disabled', async () => { const target = document.createElement('div') const root = document.createElement('div') const childId = 'test3-child' @@ -410,8 +410,6 @@ describe('renderer: VaporTeleport', () => { ) expect(target.innerHTML).toBe('') - //bug: child reload not update teleport fragment's nodes - // toggle disabled disabled.value = false await nextTick() diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index 17ae7190923..332595aac48 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -25,6 +25,7 @@ export class VaporFragment { anchor?: Node insert?: (parent: ParentNode, anchor: Node | null) => void remove?: (parent?: ParentNode) => void + getNodes?: () => Block constructor(nodes: Block) { this.nodes = nodes @@ -184,8 +185,8 @@ export function normalizeBlock(block: Block): Node[] { } else if (isVaporComponent(block)) { nodes.push(...normalizeBlock(block.block!)) } else { - if ((block as any).getNodes) { - nodes.push(...normalizeBlock((block as any).getNodes())) + if (block.getNodes) { + nodes.push(...normalizeBlock(block.getNodes())) } else { nodes.push(...normalizeBlock(block.nodes)) } diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 4d5c6d0b782..208b2d77872 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -60,7 +60,11 @@ import { import { hmrReload, hmrRerender } from './hmr' import { isHydrating, locateHydrationNode } from './dom/hydration' import { insertionAnchor, insertionParent } from './insertionState' -import type { VaporTeleportImpl } from './components/Teleport' +import { + type VaporTeleportImpl, + instanceToTeleportMap, + teleportStack, +} from './components/Teleport' export { currentInstance } from '@vue/runtime-dom' @@ -209,6 +213,11 @@ export function createComponent( ) if (__DEV__) { + let teleport = teleportStack[teleportStack.length - 1] + if (teleport) { + instanceToTeleportMap.set(instance, teleport) + } + pushWarningContext(instance) startMeasure(instance, `init`) @@ -296,7 +305,7 @@ export function createComponent( onScopeDispose(() => unmountComponent(instance), true) if (!isHydrating && _insertionParent) { - insert(instance.block, _insertionParent, _insertionAnchor) + mountComponent(instance, _insertionParent, _insertionAnchor) } return instance diff --git a/packages/runtime-vapor/src/components/Teleport.ts b/packages/runtime-vapor/src/components/Teleport.ts index 6c0b089bf00..ab900951da6 100644 --- a/packages/runtime-vapor/src/components/Teleport.ts +++ b/packages/runtime-vapor/src/components/Teleport.ts @@ -15,12 +15,45 @@ import { remove, } from '../block' import { createComment, createTextNode, querySelector } from '../dom/node' -import type { LooseRawProps, LooseRawSlots } from '../component' +import type { + LooseRawProps, + LooseRawSlots, + VaporComponentInstance, +} from '../component' import { rawPropsProxyHandlers } from '../componentProps' import { renderEffect } from '../renderEffect' -import { extend } from '@vue/shared' +import { extend, isArray } from '@vue/shared' import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity' +export const teleportStack: TeleportFragment[] = [] +export const instanceToTeleportMap: WeakMap< + VaporComponentInstance, + TeleportFragment +> = __DEV__ ? new WeakMap() : (null as any) + +/** + * dev only. + * when the **root** child component updates, synchronously update + * the TeleportFragment's children and nodes. + */ +export function handleTeleportChildrenHmrReload( + instance: VaporComponentInstance, + newInstance: VaporComponentInstance, +): void { + const teleport = instanceToTeleportMap.get(instance) + if (teleport) { + instanceToTeleportMap.set(newInstance, teleport) + if (teleport.nodes === instance) { + teleport.children = teleport.nodes = newInstance + } else if (isArray(teleport.nodes)) { + const i = teleport.nodes.indexOf(instance) + if (i > -1) { + ;(teleport.children as Block[])[i] = teleport.nodes[i] = newInstance + } + } + } +} + export const VaporTeleportImpl = { name: 'VaporTeleport', __isTeleport: true, @@ -34,11 +67,12 @@ export const VaporTeleportImpl = { pauseTracking() const scope = (frag.scope = new EffectScope()) scope!.run(() => { - let children: Block renderEffect(() => { + teleportStack.push(frag) frag.updateChildren( - (children = slots.default && (slots.default as BlockFn)()), + (frag.children = slots.default && (slots.default as BlockFn)()), ) + teleportStack.pop() }) renderEffect(() => { @@ -48,15 +82,17 @@ export const VaporTeleportImpl = { {}, new Proxy(props, rawPropsProxyHandlers) as any as TeleportProps, ), - children!, + frag.children!, ) }) }) resetTracking() if (__DEV__) { - // TODO - ;(frag as any).getNodes = () => { + // used in normalizeBlock to get the nodes of a TeleportFragment + // during hmr update. return empty array if the teleport content + // is mounted into the target container. + frag.getNodes = () => { return frag.parent !== frag.currentParent ? [] : frag.nodes } } @@ -68,6 +104,7 @@ export const VaporTeleportImpl = { class TeleportFragment extends VaporFragment { anchor: Node scope: EffectScope | undefined + children: Block | undefined private targetStart?: Node private mainAnchor?: Node @@ -178,7 +215,7 @@ class TeleportFragment extends VaporFragment { // remove nodes if (this.nodes) { remove(this.nodes, this.currentParent) - this.nodes = [] + this.children = this.nodes = [] } // remove anchors diff --git a/packages/runtime-vapor/src/hmr.ts b/packages/runtime-vapor/src/hmr.ts index 5df5d2a469d..ba669360491 100644 --- a/packages/runtime-vapor/src/hmr.ts +++ b/packages/runtime-vapor/src/hmr.ts @@ -13,6 +13,7 @@ import { mountComponent, unmountComponent, } from './component' +import { handleTeleportChildrenHmrReload } from './components/Teleport' export function hmrRerender(instance: VaporComponentInstance): void { const normalized = normalizeBlock(instance.block) @@ -54,5 +55,5 @@ export function hmrReload( ) simpleSetCurrentInstance(prev, instance.parent) mountComponent(newInstance, parent, anchor) - instance.block = newInstance.block + handleTeleportChildrenHmrReload(instance, newInstance) } From c1547b580ca68da28e852af1944e29eae2ca8b3c Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 26 Mar 2025 21:09:15 +0800 Subject: [PATCH 19/40] wip: save --- packages/runtime-vapor/src/components/Teleport.ts | 10 ++++++---- packages/runtime-vapor/src/hmr.ts | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/runtime-vapor/src/components/Teleport.ts b/packages/runtime-vapor/src/components/Teleport.ts index ab900951da6..040914ef984 100644 --- a/packages/runtime-vapor/src/components/Teleport.ts +++ b/packages/runtime-vapor/src/components/Teleport.ts @@ -25,18 +25,20 @@ import { renderEffect } from '../renderEffect' import { extend, isArray } from '@vue/shared' import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity' -export const teleportStack: TeleportFragment[] = [] +export const teleportStack: TeleportFragment[] = __DEV__ + ? ([] as TeleportFragment[]) + : (undefined as any) export const instanceToTeleportMap: WeakMap< VaporComponentInstance, TeleportFragment -> = __DEV__ ? new WeakMap() : (null as any) +> = __DEV__ ? new WeakMap() : (undefined as any) /** * dev only. - * when the **root** child component updates, synchronously update + * when the root child component updates, synchronously update * the TeleportFragment's children and nodes. */ -export function handleTeleportChildrenHmrReload( +export function handleTeleportRootComponentHmrReload( instance: VaporComponentInstance, newInstance: VaporComponentInstance, ): void { diff --git a/packages/runtime-vapor/src/hmr.ts b/packages/runtime-vapor/src/hmr.ts index ba669360491..d72800cf53e 100644 --- a/packages/runtime-vapor/src/hmr.ts +++ b/packages/runtime-vapor/src/hmr.ts @@ -13,7 +13,7 @@ import { mountComponent, unmountComponent, } from './component' -import { handleTeleportChildrenHmrReload } from './components/Teleport' +import { handleTeleportRootComponentHmrReload } from './components/Teleport' export function hmrRerender(instance: VaporComponentInstance): void { const normalized = normalizeBlock(instance.block) @@ -55,5 +55,5 @@ export function hmrReload( ) simpleSetCurrentInstance(prev, instance.parent) mountComponent(newInstance, parent, anchor) - handleTeleportChildrenHmrReload(instance, newInstance) + handleTeleportRootComponentHmrReload(instance, newInstance) } From 5b933f582c4a0450f980af3668a9605c3248a88d Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 26 Mar 2025 21:40:13 +0800 Subject: [PATCH 20/40] wip: extract VaporFragment into a separate file to resolve the circular dependency caused by TeleportFragment --- .../vapor-e2e-test/__tests__/teleport.spec.ts | 61 ++++++++-------- .../runtime-vapor/__tests__/block.spec.ts | 9 +-- .../src/apiCreateDynamicComponent.ts | 2 +- packages/runtime-vapor/src/apiCreateFor.ts | 8 +- packages/runtime-vapor/src/apiCreateIf.ts | 3 +- packages/runtime-vapor/src/block.ts | 73 ++----------------- packages/runtime-vapor/src/componentSlots.ts | 3 +- .../runtime-vapor/src/components/Teleport.ts | 13 +--- .../runtime-vapor/src/directives/vShow.ts | 3 +- packages/runtime-vapor/src/fragment.ts | 69 ++++++++++++++++++ packages/runtime-vapor/src/index.ts | 4 +- packages/runtime-vapor/src/vdomInterop.ts | 3 +- 12 files changed, 125 insertions(+), 126 deletions(-) create mode 100644 packages/runtime-vapor/src/fragment.ts diff --git a/packages-private/vapor-e2e-test/__tests__/teleport.spec.ts b/packages-private/vapor-e2e-test/__tests__/teleport.spec.ts index dc3efa13a47..ce383dadf1c 100644 --- a/packages-private/vapor-e2e-test/__tests__/teleport.spec.ts +++ b/packages-private/vapor-e2e-test/__tests__/teleport.spec.ts @@ -9,7 +9,7 @@ import { nextTick } from 'vue' import { ports } from '../utils' const { page, click, html } = setupPuppeteer() -describe('vdom / vapor interop', () => { +describe('vapor teleport', () => { let server: any const port = ports.teleport beforeAll(() => { @@ -29,33 +29,34 @@ describe('vdom / vapor interop', () => { await page().waitForSelector('#app') }) - describe('vapor teleport', () => { - test( - 'render vdom component', - async () => { - const targetSelector = '.target' - const testSelector = '.interop-render-vdom-comp' - const containerSelector = `${testSelector} > div` - const btnSelector = `${testSelector} > button` - - // teleport is disabled - expect(await html(containerSelector)).toBe('

vdom comp

') - expect(await html(targetSelector)).toBe('') - - // enable teleport - await click(btnSelector) - await nextTick() - - expect(await html(containerSelector)).toBe('') - expect(await html(targetSelector)).toBe('

vdom comp

') - - // disable teleport - await click(btnSelector) - await nextTick() - expect(await html(containerSelector)).toBe('

vdom comp

') - expect(await html(targetSelector)).toBe('') - }, - E2E_TIMEOUT, - ) - }) + test( + 'render vdom component', + async () => { + const targetSelector = '.target' + const testSelector = '.interop-render-vdom-comp' + const containerSelector = `${testSelector} > div` + const btnSelector = `${testSelector} > button` + + const tt = await html('#app') + console.log(tt) + + // teleport is disabled + expect(await html(containerSelector)).toBe('

vdom comp

') + expect(await html(targetSelector)).toBe('') + + // enable teleport + await click(btnSelector) + await nextTick() + + expect(await html(containerSelector)).toBe('') + expect(await html(targetSelector)).toBe('

vdom comp

') + + // disable teleport + await click(btnSelector) + await nextTick() + expect(await html(containerSelector)).toBe('

vdom comp

') + expect(await html(targetSelector)).toBe('') + }, + E2E_TIMEOUT, + ) }) diff --git a/packages/runtime-vapor/__tests__/block.spec.ts b/packages/runtime-vapor/__tests__/block.spec.ts index 9f76c7f0333..f0144dee3df 100644 --- a/packages/runtime-vapor/__tests__/block.spec.ts +++ b/packages/runtime-vapor/__tests__/block.spec.ts @@ -1,10 +1,5 @@ -import { - VaporFragment, - insert, - normalizeBlock, - prepend, - remove, -} from '../src/block' +import { insert, normalizeBlock, prepend, remove } from '../src/block' +import { VaporFragment } from '../src/fragment' const node1 = document.createTextNode('node1') const node2 = document.createTextNode('node2') diff --git a/packages/runtime-vapor/src/apiCreateDynamicComponent.ts b/packages/runtime-vapor/src/apiCreateDynamicComponent.ts index 2126611d718..abdc1a1cf24 100644 --- a/packages/runtime-vapor/src/apiCreateDynamicComponent.ts +++ b/packages/runtime-vapor/src/apiCreateDynamicComponent.ts @@ -1,9 +1,9 @@ import { resolveDynamicComponent } from '@vue/runtime-dom' -import { DynamicFragment, type VaporFragment } from './block' import { createComponentWithFallback } from './component' import { renderEffect } from './renderEffect' import type { RawProps } from './componentProps' import type { RawSlots } from './componentSlots' +import { DynamicFragment, type VaporFragment } from './fragment' export function createDynamicComponent( getter: () => any, diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index 0cd8317532f..ca976aa8ccd 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@ -11,12 +11,7 @@ import { } from '@vue/reactivity' import { getSequence, isArray, isObject, isString } from '@vue/shared' import { createComment, createTextNode } from './dom/node' -import { - type Block, - VaporFragment, - insert, - remove as removeBlock, -} from './block' +import { type Block, insert, remove as removeBlock } from './block' import { warn } from '@vue/runtime-dom' import { currentInstance, isVaporComponent } from './component' import type { DynamicSlot } from './componentSlots' @@ -24,6 +19,7 @@ import { renderEffect } from './renderEffect' import { VaporVForFlags } from '../../shared/src/vaporFlags' import { isHydrating, locateHydrationNode } from './dom/hydration' import { insertionAnchor, insertionParent } from './insertionState' +import { VaporFragment } from './fragment' class ForBlock extends VaporFragment { scope: EffectScope | undefined diff --git a/packages/runtime-vapor/src/apiCreateIf.ts b/packages/runtime-vapor/src/apiCreateIf.ts index 71bfa32d5d3..e83b251d069 100644 --- a/packages/runtime-vapor/src/apiCreateIf.ts +++ b/packages/runtime-vapor/src/apiCreateIf.ts @@ -1,7 +1,8 @@ -import { type Block, type BlockFn, DynamicFragment, insert } from './block' +import { type Block, type BlockFn, insert } from './block' import { isHydrating, locateHydrationNode } from './dom/hydration' import { insertionAnchor, insertionParent } from './insertionState' import { renderEffect } from './renderEffect' +import { DynamicFragment } from './fragment' export function createIf( condition: () => any, diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index 332595aac48..f1791904ce8 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -5,9 +5,12 @@ import { mountComponent, unmountComponent, } from './component' -import { createComment, createTextNode } from './dom/node' -import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity' import { isHydrating } from './dom/hydration' +import { + type DynamicFragment, + type VaporFragment, + isFragment, +} from './fragment' export type Block = | Node @@ -18,72 +21,6 @@ export type Block = export type BlockFn = (...args: any[]) => Block -export class VaporFragment { - nodes: Block - target?: ParentNode | null - targetAnchor?: Node | null - anchor?: Node - insert?: (parent: ParentNode, anchor: Node | null) => void - remove?: (parent?: ParentNode) => void - getNodes?: () => Block - - constructor(nodes: Block) { - this.nodes = nodes - } -} - -export class DynamicFragment extends VaporFragment { - anchor: Node - scope: EffectScope | undefined - current?: BlockFn - fallback?: BlockFn - - constructor(anchorLabel?: string) { - super([]) - 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 - - // teardown previous branch - if (this.scope) { - this.scope.stop() - parent && remove(this.nodes, parent) - } - - if (render) { - this.scope = new EffectScope() - this.nodes = this.scope.run(render) || [] - if (parent) insert(this.nodes, parent, this.anchor) - } else { - this.scope = undefined - this.nodes = [] - } - - 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() - } -} - -export function isFragment(val: NonNullable): val is VaporFragment { - return val instanceof VaporFragment -} - export function isBlock(val: NonNullable): val is Block { return ( val instanceof Node || diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index 74296e09466..3d17e5c0a5b 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -1,11 +1,12 @@ import { EMPTY_OBJ, NO, hasOwn, isArray, isFunction } from '@vue/shared' -import { type Block, type BlockFn, DynamicFragment, insert } from './block' +import { type Block, type BlockFn, insert } from './block' import { rawPropsProxyHandlers } from './componentProps' import { currentInstance, isRef } from '@vue/runtime-dom' import type { LooseRawProps, VaporComponentInstance } from './component' import { renderEffect } from './renderEffect' import { insertionAnchor, insertionParent } from './insertionState' import { isHydrating, locateHydrationNode } from './dom/hydration' +import { DynamicFragment } from './fragment' export type RawSlots = Record & { $?: DynamicSlotSource[] diff --git a/packages/runtime-vapor/src/components/Teleport.ts b/packages/runtime-vapor/src/components/Teleport.ts index 040914ef984..65fdc53e0ac 100644 --- a/packages/runtime-vapor/src/components/Teleport.ts +++ b/packages/runtime-vapor/src/components/Teleport.ts @@ -7,13 +7,7 @@ import { resolveTarget, warn, } from '@vue/runtime-dom' -import { - type Block, - type BlockFn, - VaporFragment, - insert, - remove, -} from '../block' +import { type Block, type BlockFn, insert, remove } from '../block' import { createComment, createTextNode, querySelector } from '../dom/node' import type { LooseRawProps, @@ -24,6 +18,7 @@ import { rawPropsProxyHandlers } from '../componentProps' import { renderEffect } from '../renderEffect' import { extend, isArray } from '@vue/shared' import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity' +import { VaporFragment } from '../fragment' export const teleportStack: TeleportFragment[] = __DEV__ ? ([] as TeleportFragment[]) @@ -70,11 +65,11 @@ export const VaporTeleportImpl = { const scope = (frag.scope = new EffectScope()) scope!.run(() => { renderEffect(() => { - teleportStack.push(frag) + __DEV__ && teleportStack.push(frag) frag.updateChildren( (frag.children = slots.default && (slots.default as BlockFn)()), ) - teleportStack.pop() + __DEV__ && teleportStack.pop() }) renderEffect(() => { diff --git a/packages/runtime-vapor/src/directives/vShow.ts b/packages/runtime-vapor/src/directives/vShow.ts index ac4c066b71d..b0fc22c14c8 100644 --- a/packages/runtime-vapor/src/directives/vShow.ts +++ b/packages/runtime-vapor/src/directives/vShow.ts @@ -6,8 +6,9 @@ import { } from '@vue/runtime-dom' import { renderEffect } from '../renderEffect' import { isVaporComponent } from '../component' -import { type Block, DynamicFragment } from '../block' +import type { Block } from '../block' import { isArray } from '@vue/shared' +import { DynamicFragment } from '../fragment' export function applyVShow(target: Block, source: () => any): void { if (isVaporComponent(target)) { diff --git a/packages/runtime-vapor/src/fragment.ts b/packages/runtime-vapor/src/fragment.ts new file mode 100644 index 00000000000..3e4fcb221c3 --- /dev/null +++ b/packages/runtime-vapor/src/fragment.ts @@ -0,0 +1,69 @@ +import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity' +import { createComment, createTextNode } from './dom/node' +import { type Block, type BlockFn, insert, isValidBlock, remove } from './block' + +export class VaporFragment { + nodes: Block + target?: ParentNode | null + targetAnchor?: Node | null + anchor?: Node + insert?: (parent: ParentNode, anchor: Node | null) => void + remove?: (parent?: ParentNode) => void + getNodes?: () => Block + + constructor(nodes: Block) { + this.nodes = nodes + } +} + +export class DynamicFragment extends VaporFragment { + anchor: Node + scope: EffectScope | undefined + current?: BlockFn + fallback?: BlockFn + + constructor(anchorLabel?: string) { + super([]) + 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 + + // teardown previous branch + if (this.scope) { + this.scope.stop() + parent && remove(this.nodes, parent) + } + + if (render) { + this.scope = new EffectScope() + this.nodes = this.scope.run(render) || [] + if (parent) insert(this.nodes, parent, this.anchor) + } else { + this.scope = undefined + this.nodes = [] + } + + 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() + } +} + +export function isFragment(val: NonNullable): val is VaporFragment { + return val instanceof VaporFragment +} diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index 2edceb54912..c2716059df2 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -6,7 +6,7 @@ export type { VaporDirective } from './directives/custom' export { 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' @@ -43,3 +43,5 @@ export { applyDynamicModel, } from './directives/vModel' export { withVaporDirectives } from './directives/custom' +export { isFragment } from './fragment' +export { VaporFragment } from './fragment' diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index 77228fd72a0..2249bbb2fdc 100644 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -26,13 +26,14 @@ import { mountComponent, unmountComponent, } from './component' -import { type Block, VaporFragment, insert, remove } from './block' +import { type Block, insert, remove } from './block' import { EMPTY_OBJ, extend, isFunction } from '@vue/shared' import { type RawProps, rawPropsProxyHandlers } from './componentProps' import type { RawSlots, VaporSlot } from './componentSlots' import { renderEffect } from './renderEffect' import { createTextNode } from './dom/node' import { optimizePropertyLookup } from './dom/prop' +import { VaporFragment } from './fragment' // mounting vapor components and slots in vdom const vaporInteropImpl: Omit< From ba6577fac1b61854ba35d21d9d5c407ea30185a1 Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 27 Mar 2025 11:42:36 +0800 Subject: [PATCH 21/40] wip: add more hmr tests + refactor --- .../__tests__/components/Teleport.spec.ts | 189 +++++++++++++++++- packages/runtime-vapor/src/component.ts | 10 +- .../runtime-vapor/src/components/Teleport.ts | 96 +++++---- packages/runtime-vapor/src/hmr.ts | 10 +- packages/runtime-vapor/src/renderEffect.ts | 6 +- 5 files changed, 234 insertions(+), 77 deletions(-) diff --git a/packages/runtime-vapor/__tests__/components/Teleport.spec.ts b/packages/runtime-vapor/__tests__/components/Teleport.spec.ts index d5ff4142249..6863d398bfc 100644 --- a/packages/runtime-vapor/__tests__/components/Teleport.spec.ts +++ b/packages/runtime-vapor/__tests__/components/Teleport.spec.ts @@ -122,11 +122,11 @@ describe('renderer: VaporTeleport', () => { }) describe('HMR', () => { - test('rerender', async () => { + test('rerender child + rerender parent', async () => { const target = document.createElement('div') const root = document.createElement('div') - const childId = 'test1-child' - const parentId = 'test1-parent' + const childId = 'test1-child-rerender' + const parentId = 'test1-parent-rerender' const { component: Child } = define({ __hmrId: childId, @@ -185,11 +185,78 @@ describe('renderer: VaporTeleport', () => { expect(target.innerHTML).toBe('
teleported 2
') }) - test('reload', async () => { + test('parent rerender + toggle disabled', async () => { const target = document.createElement('div') const root = document.createElement('div') - const childId = 'test2-child' - const parentId = 'test2-parent' + const parentId = 'test3-parent-rerender' + const disabled = ref(true) + + const Child = defineVaporComponent({ + render() { + return template('
teleported
')() + }, + }) + + const { mount, component: Parent } = define({ + __hmrId: parentId, + render() { + const n2 = template('
root
', true)() as any + setInsertionState(n2, 0) + createComp( + VaporTeleport, + { + to: () => target, + disabled: () => disabled.value, + }, + { + default: () => createComp(Child), + }, + ) + return n2 + }, + }).create() + createRecord(parentId, Parent as any) + mount(root) + + expect(root.innerHTML).toBe( + '
teleported
root
', + ) + expect(target.innerHTML).toBe('') + + // rerender parent + rerender(parentId, () => { + const n2 = template('
root 2
', true)() as any + setInsertionState(n2, 0) + createComp( + VaporTeleport, + { + to: () => target, + disabled: () => disabled.value, + }, + { + default: () => createComp(Child), + }, + ) + return n2 + }) + + expect(root.innerHTML).toBe( + '
teleported
root 2
', + ) + expect(target.innerHTML).toBe('') + + // toggle disabled + disabled.value = false + await nextTick() + expect(root.innerHTML).toBe('
root 2
') + expect(target.innerHTML).toBe('
teleported
') + }) + + test('reload child + reload parent', async () => { + const target = document.createElement('div') + const root = document.createElement('div') + const childId = 'test1-child-reload' + const parentId = 'test1-parent-reload' const { component: Child } = define({ __hmrId: childId, @@ -317,11 +384,11 @@ describe('renderer: VaporTeleport', () => { expect(target.innerHTML).toBe('') }) - test('reload child + toggle disabled', async () => { + test('reload single root child + toggle disabled', async () => { const target = document.createElement('div') const root = document.createElement('div') - const childId = 'test3-child' - const parentId = 'test3-parent' + const childId = 'test2-child-reload' + const parentId = 'test2-parent-reload' const disabled = ref(true) const { component: Child } = define({ @@ -353,6 +420,7 @@ describe('renderer: VaporTeleport', () => { disabled: () => ctx.disabled, }, { + // with single root child default: () => createComp(Child), }, ) @@ -416,6 +484,109 @@ describe('renderer: VaporTeleport', () => { expect(root.innerHTML).toBe('
root
') expect(target.innerHTML).toBe('
teleported 3
') }) + + test('reload multiple root children + toggle disabled', async () => { + const target = document.createElement('div') + const root = document.createElement('div') + const childId = 'test3-child-reload' + const parentId = 'test3-parent-reload' + + const disabled = ref(true) + const { component: Child } = define({ + __hmrId: childId, + setup() { + const msg = ref('teleported') + return { msg } + }, + render(ctx) { + const n0 = template(`
`)() + const x0 = child(n0 as any) + renderEffect(() => setText(x0 as any, ctx.msg)) + return [n0] + }, + }) + createRecord(childId, Child as any) + + const { mount, component: Parent } = define({ + __hmrId: parentId, + setup() { + const msg = ref('root') + return { msg, disabled } + }, + render(ctx) { + const n0 = createComp( + VaporTeleport, + { + to: () => target, + disabled: () => ctx.disabled, + }, + { + default: () => { + // with multiple root children + return [createComp(Child), template(`child`)()] + }, + }, + ) + const n1 = template(`
`)() + const x0 = child(n1 as any) + renderEffect(() => setText(x0 as any, ctx.msg)) + return [n0, n1] + }, + }).create() + createRecord(parentId, Parent as any) + mount(root) + + expect(root.innerHTML).toBe( + '
teleported
child
root
', + ) + expect(target.innerHTML).toBe('') + + // reload child by changing msg + reload(childId, { + __hmrId: childId, + __vapor: true, + setup() { + const msg = ref('teleported 2') + return { msg } + }, + render(ctx: any) { + const n0 = template(`
`)() + const x0 = child(n0 as any) + renderEffect(() => setText(x0 as any, ctx.msg)) + return [n0] + }, + }) + expect(root.innerHTML).toBe( + '
teleported 2
child
root
', + ) + expect(target.innerHTML).toBe('') + + // reload child again by changing msg + reload(childId, { + __hmrId: childId, + __vapor: true, + setup() { + const msg = ref('teleported 3') + return { msg } + }, + render(ctx: any) { + const n0 = template(`
`)() + const x0 = child(n0 as any) + renderEffect(() => setText(x0 as any, ctx.msg)) + return [n0] + }, + }) + expect(root.innerHTML).toBe( + '
teleported 3
child
root
', + ) + expect(target.innerHTML).toBe('') + + // toggle disabled + disabled.value = false + await nextTick() + expect(root.innerHTML).toBe('
root
') + expect(target.innerHTML).toBe('
teleported 3
child') + }) }) }) diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 208b2d77872..209bf135045 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -176,14 +176,6 @@ export function createComponent( frag.hydrate() } - // remove the teleport content from the parent tree for HMR updates - if (__DEV__) { - const instance = currentInstance as VaporComponentInstance - ;(instance!.hmrEffects || (instance!.hmrEffects = [])).push(() => { - frag.remove(frag.anchor.parentNode!) - }) - } - return frag as any } @@ -405,8 +397,8 @@ export class VaporComponentInstance implements GenericComponentInstance { setupState?: Record devtoolsRawSetupState?: any hmrRerender?: () => void + hmrRerenderEffects?: (() => void)[] hmrReload?: (newComp: VaporComponent) => void - hmrEffects?: (() => void)[] propsOptions?: NormalizedPropsOptions emitsOptions?: ObjectEmitsOptions | null isSingleRoot?: boolean diff --git a/packages/runtime-vapor/src/components/Teleport.ts b/packages/runtime-vapor/src/components/Teleport.ts index 65fdc53e0ac..1af56e65b55 100644 --- a/packages/runtime-vapor/src/components/Teleport.ts +++ b/packages/runtime-vapor/src/components/Teleport.ts @@ -1,6 +1,7 @@ import { TeleportEndKey, type TeleportProps, + currentInstance, isTeleportDeferred, isTeleportDisabled, queuePostFlushCb, @@ -17,7 +18,6 @@ import type { import { rawPropsProxyHandlers } from '../componentProps' import { renderEffect } from '../renderEffect' import { extend, isArray } from '@vue/shared' -import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity' import { VaporFragment } from '../fragment' export const teleportStack: TeleportFragment[] = __DEV__ @@ -29,9 +29,9 @@ export const instanceToTeleportMap: WeakMap< > = __DEV__ ? new WeakMap() : (undefined as any) /** - * dev only. + * dev only * when the root child component updates, synchronously update - * the TeleportFragment's children and nodes. + * the TeleportFragment's nodes. */ export function handleTeleportRootComponentHmrReload( instance: VaporComponentInstance, @@ -41,12 +41,10 @@ export function handleTeleportRootComponentHmrReload( if (teleport) { instanceToTeleportMap.set(newInstance, teleport) if (teleport.nodes === instance) { - teleport.children = teleport.nodes = newInstance + teleport.nodes = newInstance } else if (isArray(teleport.nodes)) { const i = teleport.nodes.indexOf(instance) - if (i > -1) { - ;(teleport.children as Block[])[i] = teleport.nodes[i] = newInstance - } + if (i !== -1) teleport.nodes[i] = newInstance } } } @@ -61,37 +59,42 @@ export const VaporTeleportImpl = { ? new TeleportFragment('teleport') : new TeleportFragment() - pauseTracking() - const scope = (frag.scope = new EffectScope()) - scope!.run(() => { - renderEffect(() => { - __DEV__ && teleportStack.push(frag) - frag.updateChildren( - (frag.children = slots.default && (slots.default as BlockFn)()), - ) - __DEV__ && teleportStack.pop() - }) + const updateChildrenEffect = renderEffect(() => { + __DEV__ && teleportStack.push(frag) + frag.updateChildren(slots.default && (slots.default as BlockFn)()) + __DEV__ && teleportStack.pop() + }) - renderEffect(() => { - frag.update( - // access the props to trigger tracking - extend( - {}, - new Proxy(props, rawPropsProxyHandlers) as any as TeleportProps, - ), - frag.children!, - ) - }) + const updateEffect = renderEffect(() => { + frag.update( + // access the props to trigger tracking + extend( + {}, + new Proxy(props, rawPropsProxyHandlers) as any as TeleportProps, + ), + ) }) - resetTracking() if (__DEV__) { - // used in normalizeBlock to get the nodes of a TeleportFragment - // during hmr update. return empty array if the teleport content - // is mounted into the target container. + // used in `normalizeBlock` to get nodes of TeleportFragment during + // HMR updates. returns empty array if content is mounted in target + // container to prevent incorrect parent node lookup. frag.getNodes = () => { return frag.parent !== frag.currentParent ? [] : frag.nodes } + + // for HMR re-render + const instance = currentInstance as VaporComponentInstance + ;( + instance!.hmrRerenderEffects || (instance!.hmrRerenderEffects = []) + ).push(() => { + // remove the teleport content + frag.remove(frag.anchor.parentNode!) + + // stop effects + updateChildrenEffect.stop() + updateEffect.stop() + }) } return frag @@ -100,8 +103,6 @@ export const VaporTeleportImpl = { class TeleportFragment extends VaporFragment { anchor: Node - scope: EffectScope | undefined - children: Block | undefined private targetStart?: Node private mainAnchor?: Node @@ -128,19 +129,18 @@ class TeleportFragment extends VaporFragment { } updateChildren(children: Block): void { - // not mounted yet, early return - if (!this.parent) return - - // teardown previous children - remove(this.nodes, this.currentParent) - - // mount new - insert((this.nodes = children), this.currentParent, this.currentAnchor) + // not mounted yet + if (!this.parent) { + this.nodes = children + } else { + // teardown previous nodes + remove(this.nodes, this.currentParent) + // mount new nodes + insert((this.nodes = children), this.currentParent, this.currentAnchor) + } } - update(props: TeleportProps, children: Block): void { - this.nodes = children - + update(props: TeleportProps): void { const mount = (parent: ParentNode, anchor: Node | null) => { insert( this.nodes, @@ -203,16 +203,10 @@ class TeleportFragment extends VaporFragment { } remove = (parent: ParentNode | undefined): void => { - // stop effect scope - if (this.scope) { - this.scope.stop() - this.scope = undefined - } - // remove nodes if (this.nodes) { remove(this.nodes, this.currentParent) - this.children = this.nodes = [] + this.nodes = [] } // remove anchors diff --git a/packages/runtime-vapor/src/hmr.ts b/packages/runtime-vapor/src/hmr.ts index d72800cf53e..63e5376896a 100644 --- a/packages/runtime-vapor/src/hmr.ts +++ b/packages/runtime-vapor/src/hmr.ts @@ -20,9 +20,9 @@ export function hmrRerender(instance: VaporComponentInstance): void { const parent = normalized[0].parentNode! const anchor = normalized[normalized.length - 1].nextSibling remove(instance.block, parent) - if (instance.hmrEffects) { - instance.hmrEffects.forEach(e => e()) - instance.hmrEffects.length = 0 + if (instance.hmrRerenderEffects) { + instance.hmrRerenderEffects.forEach(e => e()) + instance.hmrRerenderEffects.length = 0 } const prev = currentInstance simpleSetCurrentInstance(instance) @@ -41,10 +41,6 @@ export function hmrReload( const parent = normalized[0].parentNode! const anchor = normalized[normalized.length - 1].nextSibling unmountComponent(instance, parent) - if (instance.hmrEffects) { - instance.hmrEffects.forEach(e => e()) - instance.hmrEffects.length = 0 - } const prev = currentInstance simpleSetCurrentInstance(instance.parent) const newInstance = createComponent( diff --git a/packages/runtime-vapor/src/renderEffect.ts b/packages/runtime-vapor/src/renderEffect.ts index a9fa9b33562..227d7933e78 100644 --- a/packages/runtime-vapor/src/renderEffect.ts +++ b/packages/runtime-vapor/src/renderEffect.ts @@ -11,7 +11,10 @@ import { import { type VaporComponentInstance, isVaporComponent } from './component' import { invokeArrayFns } from '@vue/shared' -export function renderEffect(fn: () => void, noLifecycle = false): void { +export function renderEffect( + fn: () => void, + noLifecycle = false, +): ReactiveEffect { const instance = currentInstance as VaporComponentInstance | null const scope = getCurrentScope() if (__DEV__ && !__TEST__ && !scope && !isVaporComponent(instance)) { @@ -66,5 +69,6 @@ export function renderEffect(fn: () => void, noLifecycle = false): void { effect.scheduler = () => queueJob(job) effect.run() + return effect // TODO recurse handling } From 7ab1a30a40de234e24ec8a91bb12a0b81921f62a Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 27 Mar 2025 13:43:08 +0800 Subject: [PATCH 22/40] wip: refactor --- packages/runtime-vapor/src/component.ts | 11 +-- .../runtime-vapor/src/components/Teleport.ts | 80 ++++++++++--------- 2 files changed, 44 insertions(+), 47 deletions(-) diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 209bf135045..0b051420a54 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -60,11 +60,7 @@ import { import { hmrReload, hmrRerender } from './hmr' import { isHydrating, locateHydrationNode } from './dom/hydration' import { insertionAnchor, insertionParent } from './insertionState' -import { - type VaporTeleportImpl, - instanceToTeleportMap, - teleportStack, -} from './components/Teleport' +import type { VaporTeleportImpl } from './components/Teleport' export { currentInstance } from '@vue/runtime-dom' @@ -205,11 +201,6 @@ export function createComponent( ) if (__DEV__) { - let teleport = teleportStack[teleportStack.length - 1] - if (teleport) { - instanceToTeleportMap.set(instance, teleport) - } - pushWarningContext(instance) startMeasure(instance, `init`) diff --git a/packages/runtime-vapor/src/components/Teleport.ts b/packages/runtime-vapor/src/components/Teleport.ts index 1af56e65b55..c16d0b06342 100644 --- a/packages/runtime-vapor/src/components/Teleport.ts +++ b/packages/runtime-vapor/src/components/Teleport.ts @@ -10,44 +10,19 @@ import { } from '@vue/runtime-dom' import { type Block, type BlockFn, insert, remove } from '../block' import { createComment, createTextNode, querySelector } from '../dom/node' -import type { - LooseRawProps, - LooseRawSlots, - VaporComponentInstance, +import { + type LooseRawProps, + type LooseRawSlots, + type VaporComponentInstance, + isVaporComponent, } from '../component' import { rawPropsProxyHandlers } from '../componentProps' import { renderEffect } from '../renderEffect' import { extend, isArray } from '@vue/shared' import { VaporFragment } from '../fragment' -export const teleportStack: TeleportFragment[] = __DEV__ - ? ([] as TeleportFragment[]) - : (undefined as any) -export const instanceToTeleportMap: WeakMap< - VaporComponentInstance, - TeleportFragment -> = __DEV__ ? new WeakMap() : (undefined as any) - -/** - * dev only - * when the root child component updates, synchronously update - * the TeleportFragment's nodes. - */ -export function handleTeleportRootComponentHmrReload( - instance: VaporComponentInstance, - newInstance: VaporComponentInstance, -): void { - const teleport = instanceToTeleportMap.get(instance) - if (teleport) { - instanceToTeleportMap.set(newInstance, teleport) - if (teleport.nodes === instance) { - teleport.nodes = newInstance - } else if (isArray(teleport.nodes)) { - const i = teleport.nodes.indexOf(instance) - if (i !== -1) teleport.nodes[i] = newInstance - } - } -} +const instanceToTeleportMap: WeakMap = + __DEV__ ? new WeakMap() : (undefined as any) export const VaporTeleportImpl = { name: 'VaporTeleport', @@ -59,11 +34,9 @@ export const VaporTeleportImpl = { ? new TeleportFragment('teleport') : new TeleportFragment() - const updateChildrenEffect = renderEffect(() => { - __DEV__ && teleportStack.push(frag) - frag.updateChildren(slots.default && (slots.default as BlockFn)()) - __DEV__ && teleportStack.pop() - }) + const updateChildrenEffect = renderEffect(() => + frag.updateChildren(slots.default && (slots.default as BlockFn)()), + ) const updateEffect = renderEffect(() => { frag.update( @@ -138,6 +111,18 @@ class TeleportFragment extends VaporFragment { // mount new nodes insert((this.nodes = children), this.currentParent, this.currentAnchor) } + + if (__DEV__) { + if (isVaporComponent(children)) { + instanceToTeleportMap.set(children, this) + } else if (isArray(children)) { + children.forEach(node => { + if (isVaporComponent(node)) { + instanceToTeleportMap.set(node, this) + } + }) + } + } } update(props: TeleportProps): void { @@ -256,3 +241,24 @@ export const VaporTeleport = VaporTeleportImpl as unknown as { } } } + +/** + * dev only + * when the root child component updates, synchronously update + * the TeleportFragment's nodes. + */ +export function handleTeleportRootComponentHmrReload( + instance: VaporComponentInstance, + newInstance: VaporComponentInstance, +): void { + const teleport = instanceToTeleportMap.get(instance) + if (teleport) { + instanceToTeleportMap.set(newInstance, teleport) + if (teleport.nodes === instance) { + teleport.nodes = newInstance + } else if (isArray(teleport.nodes)) { + const i = teleport.nodes.indexOf(instance) + if (i !== -1) teleport.nodes[i] = newInstance + } + } +} From fd6f1632461348d75a9677a65b8832a6ae1a1b8f Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 27 Mar 2025 14:21:29 +0800 Subject: [PATCH 23/40] wip: save --- .../runtime-vapor/src/components/Teleport.ts | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/packages/runtime-vapor/src/components/Teleport.ts b/packages/runtime-vapor/src/components/Teleport.ts index c16d0b06342..1e4a12f835a 100644 --- a/packages/runtime-vapor/src/components/Teleport.ts +++ b/packages/runtime-vapor/src/components/Teleport.ts @@ -56,18 +56,29 @@ export const VaporTeleportImpl = { return frag.parent !== frag.currentParent ? [] : frag.nodes } - // for HMR re-render + // for HMR rerender const instance = currentInstance as VaporComponentInstance ;( instance!.hmrRerenderEffects || (instance!.hmrRerenderEffects = []) ).push(() => { // remove the teleport content - frag.remove(frag.anchor.parentNode!) + frag.remove() // stop effects updateChildrenEffect.stop() updateEffect.stop() }) + + // for HMR reload + const nodes = frag.nodes + if (isVaporComponent(nodes)) { + instanceToTeleportMap.set(nodes, frag) + } else if (isArray(nodes)) { + nodes.forEach( + node => + isVaporComponent(node) && instanceToTeleportMap.set(node, frag), + ) + } } return frag @@ -105,24 +116,13 @@ class TeleportFragment extends VaporFragment { // not mounted yet if (!this.parent) { this.nodes = children - } else { - // teardown previous nodes - remove(this.nodes, this.currentParent) - // mount new nodes - insert((this.nodes = children), this.currentParent, this.currentAnchor) + return } - if (__DEV__) { - if (isVaporComponent(children)) { - instanceToTeleportMap.set(children, this) - } else if (isArray(children)) { - children.forEach(node => { - if (isVaporComponent(node)) { - instanceToTeleportMap.set(node, this) - } - }) - } - } + // teardown previous nodes + remove(this.nodes, this.currentParent) + // mount new nodes + insert((this.nodes = children), this.currentParent, this.currentAnchor) } update(props: TeleportProps): void { @@ -187,7 +187,7 @@ class TeleportFragment extends VaporFragment { } } - remove = (parent: ParentNode | undefined): void => { + remove = (parent: ParentNode | undefined = this.parent!): void => { // remove nodes if (this.nodes) { remove(this.nodes, this.currentParent) @@ -244,8 +244,9 @@ export const VaporTeleport = VaporTeleportImpl as unknown as { /** * dev only - * when the root child component updates, synchronously update - * the TeleportFragment's nodes. + * during root component HMR reload, since the old component will be unmounted + * and a new one will be mounted, we need to update the teleport's nodes + * to ensure they are up to date. */ export function handleTeleportRootComponentHmrReload( instance: VaporComponentInstance, From 4de819c267794a3e57cd357f9013b10e83677982 Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 27 Mar 2025 14:36:02 +0800 Subject: [PATCH 24/40] wip: refactor --- packages/runtime-vapor/src/component.ts | 11 +++-------- packages/runtime-vapor/src/components/Teleport.ts | 8 +++++++- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 0b051420a54..672bb75dc1e 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -60,7 +60,7 @@ import { import { hmrReload, hmrRerender } from './hmr' import { isHydrating, locateHydrationNode } from './dom/hydration' import { insertionAnchor, insertionParent } from './insertionState' -import type { VaporTeleportImpl } from './components/Teleport' +import { isVaporTeleport } from './components/Teleport' export { currentInstance } from '@vue/runtime-dom' @@ -93,8 +93,6 @@ export interface ObjectVaporComponent name?: string vapor?: boolean - - __isTeleport?: boolean } interface SharedInternalOptions { @@ -161,11 +159,8 @@ export function createComponent( } // teleport - if (component.__isTeleport) { - const frag = (component as typeof VaporTeleportImpl).process( - rawProps!, - rawSlots!, - ) + if (isVaporTeleport(component)) { + const frag = component.process(rawProps!, rawSlots!) if (!isHydrating && _insertionParent) { insert(frag, _insertionParent, _insertionAnchor) } else { diff --git a/packages/runtime-vapor/src/components/Teleport.ts b/packages/runtime-vapor/src/components/Teleport.ts index 1e4a12f835a..50ddceb62ec 100644 --- a/packages/runtime-vapor/src/components/Teleport.ts +++ b/packages/runtime-vapor/src/components/Teleport.ts @@ -85,7 +85,7 @@ export const VaporTeleportImpl = { }, } -class TeleportFragment extends VaporFragment { +export class TeleportFragment extends VaporFragment { anchor: Node private targetStart?: Node @@ -242,6 +242,12 @@ export const VaporTeleport = VaporTeleportImpl as unknown as { } } +export function isVaporTeleport( + value: unknown, +): value is typeof VaporTeleportImpl { + return value === VaporTeleportImpl +} + /** * dev only * during root component HMR reload, since the old component will be unmounted From 177c6a61e63f7375bac187ddac2407d951aa80d4 Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 27 Mar 2025 14:51:05 +0800 Subject: [PATCH 25/40] wip: save --- packages/runtime-vapor/src/components/Teleport.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/runtime-vapor/src/components/Teleport.ts b/packages/runtime-vapor/src/components/Teleport.ts index 50ddceb62ec..7268a631f21 100644 --- a/packages/runtime-vapor/src/components/Teleport.ts +++ b/packages/runtime-vapor/src/components/Teleport.ts @@ -208,6 +208,9 @@ export class TeleportFragment extends VaporFragment { remove(this.mainAnchor!, parent) this.mainAnchor = undefined } + + this.mountContainer = undefined + this.mountAnchor = undefined } hydrate(): void { From 5574fbf52151d3d423d9c308be75c90dd995246b Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 27 Mar 2025 14:58:47 +0800 Subject: [PATCH 26/40] wip: refactor --- packages/runtime-core/src/index.ts | 3 +-- .../runtime-vapor/src/components/Teleport.ts | 24 ++++--------------- 2 files changed, 5 insertions(+), 22 deletions(-) diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 2d721b058f0..b3811e929fa 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -561,8 +561,7 @@ export { initFeatureFlags } from './featureFlags' * @internal */ export { - resolveTarget, + resolveTarget as resolveTeleportTarget, isTeleportDisabled, isTeleportDeferred, - TeleportEndKey, } from './components/Teleport' diff --git a/packages/runtime-vapor/src/components/Teleport.ts b/packages/runtime-vapor/src/components/Teleport.ts index 7268a631f21..8f746887090 100644 --- a/packages/runtime-vapor/src/components/Teleport.ts +++ b/packages/runtime-vapor/src/components/Teleport.ts @@ -1,11 +1,10 @@ import { - TeleportEndKey, type TeleportProps, currentInstance, isTeleportDeferred, isTeleportDisabled, queuePostFlushCb, - resolveTarget, + resolveTeleportTarget, warn, } from '@vue/runtime-dom' import { type Block, type BlockFn, insert, remove } from '../block' @@ -135,7 +134,7 @@ export class TeleportFragment extends VaporFragment { } const mountToTarget = () => { - const target = (this.target = resolveTarget(props, querySelector)) + const target = (this.target = resolveTeleportTarget(props, querySelector)) if (target) { if ( // initial mount into target @@ -143,7 +142,8 @@ export class TeleportFragment extends VaporFragment { // target changed this.targetAnchor.parentNode !== target ) { - ;[this.targetAnchor, this.targetStart] = prepareAnchor(target) + insert((this.targetStart = createTextNode('')), target) + insert((this.targetAnchor = createTextNode('')), target) } mount(target, this.targetAnchor!) @@ -218,22 +218,6 @@ export class TeleportFragment extends VaporFragment { } } -function prepareAnchor(target: ParentNode | null) { - const targetStart = createTextNode('') as Text & { [TeleportEndKey]: Node } - const targetAnchor = createTextNode('') - - // attach a special property, so we can skip teleported content in - // renderer's nextSibling search - targetStart[TeleportEndKey] = targetAnchor - - if (target) { - insert(targetStart, target) - insert(targetAnchor, target) - } - - return [targetAnchor, targetStart] -} - export const VaporTeleport = VaporTeleportImpl as unknown as { __vapor: true __isTeleport: true From ba9db343792ef00632b7e537784367ce8051e6da Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 27 Mar 2025 15:33:02 +0800 Subject: [PATCH 27/40] test: add e2e tests for vdom teleport vapor interop --- .../__tests__/vdomInterop.spec.ts | 42 ++++++++++++++++--- .../vapor-e2e-test/interop/App.vue | 16 ++++++- .../interop/components/SimpleVaporComp.vue | 6 +++ .../interop/{ => components}/VaporComp.vue | 0 .../interop/{ => components}/VdomComp.vue | 0 5 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 packages-private/vapor-e2e-test/interop/components/SimpleVaporComp.vue rename packages-private/vapor-e2e-test/interop/{ => components}/VaporComp.vue (100%) rename packages-private/vapor-e2e-test/interop/{ => components}/VdomComp.vue (100%) diff --git a/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts b/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts index a3069d1aed2..734c9fde190 100644 --- a/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts +++ b/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts @@ -6,10 +6,10 @@ import { import connect from 'connect' import sirv from 'sirv' import { ports } from '../utils' +import { nextTick } from 'vue' +const { page, click, text, enterValue, html } = setupPuppeteer() describe('vdom / vapor interop', () => { - const { page, click, text, enterValue } = setupPuppeteer() - let server: any const port = ports.vdomInterop beforeAll(() => { @@ -19,6 +19,12 @@ describe('vdom / vapor interop', () => { process.on('SIGTERM', () => server && server.close()) }) + beforeEach(async () => { + const baseUrl = `http://localhost:${port}/interop/` + await page().goto(baseUrl) + await page().waitForSelector('#app') + }) + afterAll(() => { server.close() }) @@ -26,9 +32,6 @@ describe('vdom / vapor interop', () => { 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') @@ -82,4 +85,33 @@ 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('') + }) + }) }) diff --git a/packages-private/vapor-e2e-test/interop/App.vue b/packages-private/vapor-e2e-test/interop/App.vue index 772a6989dd7..dcdd5f99ace 100644 --- a/packages-private/vapor-e2e-test/interop/App.vue +++ b/packages-private/vapor-e2e-test/interop/App.vue @@ -1,9 +1,11 @@ diff --git a/packages-private/vapor-e2e-test/interop/components/SimpleVaporComp.vue b/packages-private/vapor-e2e-test/interop/components/SimpleVaporComp.vue new file mode 100644 index 00000000000..d2d7be5680d --- /dev/null +++ b/packages-private/vapor-e2e-test/interop/components/SimpleVaporComp.vue @@ -0,0 +1,6 @@ + + diff --git a/packages-private/vapor-e2e-test/interop/VaporComp.vue b/packages-private/vapor-e2e-test/interop/components/VaporComp.vue similarity index 100% rename from packages-private/vapor-e2e-test/interop/VaporComp.vue rename to packages-private/vapor-e2e-test/interop/components/VaporComp.vue 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 From 43d279932a10b94dbb0bc70cd0a297563b0d373a Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 27 Mar 2025 07:34:00 +0000 Subject: [PATCH 28/40] [autofix.ci] apply automated fixes --- .../vapor-e2e-test/interop/components/SimpleVaporComp.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages-private/vapor-e2e-test/interop/components/SimpleVaporComp.vue b/packages-private/vapor-e2e-test/interop/components/SimpleVaporComp.vue index d2d7be5680d..65661740cd9 100644 --- a/packages-private/vapor-e2e-test/interop/components/SimpleVaporComp.vue +++ b/packages-private/vapor-e2e-test/interop/components/SimpleVaporComp.vue @@ -2,5 +2,5 @@ const msg = 'vapor comp' From 9ab8e4c0c98087fe2758380c194259aebcb3f765 Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 1 Apr 2025 21:03:42 +0800 Subject: [PATCH 29/40] chore: update lockfile --- pnpm-lock.yaml | 582 +++++++++++++++++++++++++++---------------------- 1 file changed, 323 insertions(+), 259 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bc4e5a164ba..c72eaa1ab19 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,8 +13,8 @@ catalogs: specifier: ^7.26.10 version: 7.26.10 '@vitejs/plugin-vue': - specifier: ^5.2.3 - version: 5.2.3 + specifier: https://pkg.pr.new/@vitejs/plugin-vue@c156992 + version: 5.2.1 estree-walker: specifier: ^2.0.2 version: 2.0.2 @@ -25,8 +25,8 @@ catalogs: specifier: ^1.2.1 version: 1.2.1 vite: - specifier: ^5.4.14 - version: 5.4.14 + specifier: ^6.1.0 + version: 6.2.4 importers: @@ -69,14 +69,14 @@ importers: specifier: ^6.1.4 version: 6.1.4 '@vitest/coverage-v8': - specifier: ^3.0.2 - version: 3.0.2(vitest@3.0.2) + specifier: ^3.0.9 + version: 3.0.9(vitest@3.0.9) '@vitest/eslint-plugin': - specifier: ^1.1.25 - version: 1.1.25(@typescript-eslint/utils@8.20.0(eslint@9.18.0)(typescript@5.6.2))(eslint@9.18.0)(typescript@5.6.2)(vitest@3.0.2) + specifier: ^1.1.38 + version: 1.1.38(@typescript-eslint/utils@8.27.0(eslint@9.23.0)(typescript@5.6.2))(eslint@9.23.0)(typescript@5.6.2)(vitest@3.0.9) '@vitest/ui': specifier: ^3.0.2 - version: 3.0.4(vitest@3.0.2) + version: 3.1.1(vitest@3.0.9) '@vue/consolidate': specifier: 1.0.0 version: 1.0.0 @@ -178,16 +178,16 @@ importers: version: 8.27.0(eslint@9.23.0)(typescript@5.6.2) vite: specifier: 'catalog:' - version: 6.1.0(@types/node@22.10.7)(sass@1.83.4)(terser@5.33.0)(yaml@2.6.1) + version: 6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) vitest: - specifier: ^3.0.2 - version: 3.0.2(@types/node@22.10.7)(@vitest/ui@3.0.4)(jsdom@26.0.0)(sass@1.83.4)(terser@5.33.0)(yaml@2.6.1) + specifier: ^3.0.9 + version: 3.0.9(@types/node@22.13.13)(@vitest/ui@3.1.1)(jsdom@26.0.0)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) packages-private/benchmark: dependencies: '@vitejs/plugin-vue': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-vue@c156992(vite@6.1.0(@types/node@22.10.7)(sass@1.83.4)(terser@5.33.0)(yaml@2.6.1))(vue@3.5.13(typescript@5.6.2)) + version: https://pkg.pr.new/@vitejs/plugin-vue@c156992(vite@6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.2)) connect: specifier: ^3.7.0 version: 3.7.0 @@ -196,7 +196,7 @@ importers: version: 2.0.4 vite: specifier: 'catalog:' - version: 6.1.0(@types/node@22.10.7)(sass@1.83.4)(terser@5.33.0)(yaml@2.6.1) + version: 6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) devDependencies: '@types/connect': specifier: ^3.4.38 @@ -227,26 +227,26 @@ importers: dependencies: '@vueuse/core': specifier: ^11.1.0 - version: 11.1.0(vue@packages+vue) + version: 11.3.0(vue@packages+vue) vue: specifier: workspace:* version: link:../../packages/vue devDependencies: '@vitejs/plugin-vue': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-vue@c156992(vite@6.1.0(@types/node@22.10.7)(sass@1.83.4)(terser@5.33.0)(yaml@2.6.1))(vue@packages+vue) + version: https://pkg.pr.new/@vitejs/plugin-vue@c156992(vite@6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0))(vue@packages+vue) '@vue/compiler-sfc': specifier: workspace:* version: link:../../packages/compiler-sfc vite: specifier: 'catalog:' - version: 6.1.0(@types/node@22.10.7)(sass@1.83.4)(terser@5.33.0)(yaml@2.6.1) + version: 6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) vite-hyper-config: specifier: ^0.4.0 - version: 0.4.0(@types/node@22.10.7)(sass@1.83.4)(terser@5.33.0)(vite@6.1.0(@types/node@22.10.7)(sass@1.83.4)(terser@5.33.0)(yaml@2.6.1)) + version: 0.4.1(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(vite@6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0)) vite-plugin-inspect: specifier: ^0.8.7 - version: 0.8.7(rollup@4.31.0)(vite@6.1.0(@types/node@22.10.7)(sass@1.83.4)(terser@5.33.0)(yaml@2.6.1)) + version: 0.8.7(rollup@4.37.0)(vite@6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0)) packages-private/sfc-playground: dependencies: @@ -265,10 +265,10 @@ importers: devDependencies: '@vitejs/plugin-vue': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-vue@c156992(vite@6.1.0(@types/node@22.10.7)(sass@1.83.4)(terser@5.33.0)(yaml@2.6.1))(vue@packages+vue) + version: https://pkg.pr.new/@vitejs/plugin-vue@c156992(vite@6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0))(vue@packages+vue) vite: specifier: 'catalog:' - version: 6.1.0(@types/node@22.10.7)(sass@1.83.4)(terser@5.33.0)(yaml@2.6.1) + version: 6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) packages-private/template-explorer: dependencies: @@ -289,7 +289,7 @@ importers: version: 3.4.38 '@vitejs/plugin-vue': specifier: 'catalog:' - version: https://pkg.pr.new/@vitejs/plugin-vue@c156992(vite@6.1.0(@types/node@22.10.7)(sass@1.83.4)(terser@5.33.0)(yaml@2.6.1))(vue@packages+vue) + version: https://pkg.pr.new/@vitejs/plugin-vue@c156992(vite@6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0))(vue@packages+vue) connect: specifier: ^3.7.0 version: 3.7.0 @@ -298,7 +298,7 @@ importers: version: 2.0.4 vite: specifier: 'catalog:' - version: 6.1.0(@types/node@22.10.7)(sass@1.83.4)(terser@5.33.0)(yaml@2.6.1) + version: 6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) vue: specifier: workspace:* version: link:../../packages/vue @@ -307,10 +307,10 @@ importers: devDependencies: '@vitejs/plugin-vue': specifier: 'catalog:' - version: 5.2.3(vite@5.4.14(@types/node@22.13.13)(sass@1.86.0))(vue@packages+vue) + version: https://pkg.pr.new/@vitejs/plugin-vue@c156992(vite@6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0))(vue@packages+vue) vite: specifier: 'catalog:' - version: 5.4.14(@types/node@22.13.13)(sass@1.86.0) + version: 6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) vue: specifier: workspace:* version: link:../../packages/vue @@ -1115,6 +1115,9 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@polka/url@1.0.0-next.28': + resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} + '@puppeteer/browsers@2.8.0': resolution: {integrity: sha512-yTwt2KWRmCQAfhvbCRjebaSX8pV1//I0Y3g+A7f/eS7gf0l4eRJoUCvcYdVtboeU4CTOZQuqYbZNS8aBYb8ROQ==} engines: {node: '>=18'} @@ -1183,100 +1186,53 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.34.2': - resolution: {integrity: sha512-6Fyg9yQbwJR+ykVdT9sid1oc2ewejS6h4wzQltmJfSW53N60G/ah9pngXGANdy9/aaE/TcUFpWosdm7JXS1WTQ==} - cpu: [arm] - os: [android] - '@rollup/rollup-android-arm-eabi@4.37.0': resolution: {integrity: sha512-l7StVw6WAa8l3vA1ov80jyetOAEo1FtHvZDbzXDO/02Sq/QVvqlHkYoFwDJPIMj0GKiistsBudfx5tGFnwYWDQ==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.34.2': - resolution: {integrity: sha512-K5GfWe+vtQ3kyEbihrimM38UgX57UqHp+oME7X/EX9Im6suwZfa7Hsr8AtzbJvukTpwMGs+4s29YMSO3rwWtsw==} - cpu: [arm64] - os: [android] - '@rollup/rollup-android-arm64@4.37.0': resolution: {integrity: sha512-6U3SlVyMxezt8Y+/iEBcbp945uZjJwjZimu76xoG7tO1av9VO691z8PkhzQ85ith2I8R2RddEPeSfcbyPfD4hA==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.34.2': - resolution: {integrity: sha512-PSN58XG/V/tzqDb9kDGutUruycgylMlUE59f40ny6QIRNsTEIZsrNQTJKUN2keMMSmlzgunMFqyaGLmly39sug==} - cpu: [arm64] - os: [darwin] - '@rollup/rollup-darwin-arm64@4.37.0': resolution: {integrity: sha512-+iTQ5YHuGmPt10NTzEyMPbayiNTcOZDWsbxZYR1ZnmLnZxG17ivrPSWFO9j6GalY0+gV3Jtwrrs12DBscxnlYA==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.34.2': - resolution: {integrity: sha512-gQhK788rQJm9pzmXyfBB84VHViDERhAhzGafw+E5mUpnGKuxZGkMVDa3wgDFKT6ukLC5V7QTifzsUKdNVxp5qQ==} - cpu: [x64] - os: [darwin] - '@rollup/rollup-darwin-x64@4.37.0': resolution: {integrity: sha512-m8W2UbxLDcmRKVjgl5J/k4B8d7qX2EcJve3Sut7YGrQoPtCIQGPH5AMzuFvYRWZi0FVS0zEY4c8uttPfX6bwYQ==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.34.2': - resolution: {integrity: sha512-eiaHgQwGPpxLC3+zTAcdKl4VsBl3r0AiJOd1Um/ArEzAjN/dbPK1nROHrVkdnoE6p7Svvn04w3f/jEZSTVHunA==} - cpu: [arm64] - os: [freebsd] - '@rollup/rollup-freebsd-arm64@4.37.0': resolution: {integrity: sha512-FOMXGmH15OmtQWEt174v9P1JqqhlgYge/bUjIbiVD1nI1NeJ30HYT9SJlZMqdo1uQFyt9cz748F1BHghWaDnVA==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.34.2': - resolution: {integrity: sha512-lhdiwQ+jf8pewYOTG4bag0Qd68Jn1v2gO1i0mTuiD+Qkt5vNfHVK/jrT7uVvycV8ZchlzXp5HDVmhpzjC6mh0g==} - cpu: [x64] - os: [freebsd] - '@rollup/rollup-freebsd-x64@4.37.0': resolution: {integrity: sha512-SZMxNttjPKvV14Hjck5t70xS3l63sbVwl98g3FlVVx2YIDmfUIy29jQrsw06ewEYQ8lQSuY9mpAPlmgRD2iSsA==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.34.2': - resolution: {integrity: sha512-lfqTpWjSvbgQP1vqGTXdv+/kxIznKXZlI109WkIFPbud41bjigjNmOAAKoazmRGx+k9e3rtIdbq2pQZPV1pMig==} - cpu: [arm] - os: [linux] - '@rollup/rollup-linux-arm-gnueabihf@4.37.0': resolution: {integrity: sha512-hhAALKJPidCwZcj+g+iN+38SIOkhK2a9bqtJR+EtyxrKKSt1ynCBeqrQy31z0oWU6thRZzdx53hVgEbRkuI19w==} cpu: [arm] os: [linux] - - '@rollup/rollup-linux-arm-musleabihf@4.34.2': - resolution: {integrity: sha512-RGjqULqIurqqv+NJTyuPgdZhka8ImMLB32YwUle2BPTDqDoXNgwFjdjQC59FbSk08z0IqlRJjrJ0AvDQ5W5lpw==} - cpu: [arm] - os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.37.0': resolution: {integrity: sha512-jUb/kmn/Gd8epbHKEqkRAxq5c2EwRt0DqhSGWjPFxLeFvldFdHQs/n8lQ9x85oAeVb6bHcS8irhTJX2FCOd8Ag==} cpu: [arm] os: [linux] - - '@rollup/rollup-linux-arm64-gnu@4.34.2': - resolution: {integrity: sha512-ZvkPiheyXtXlFqHpsdgscx+tZ7hoR59vOettvArinEspq5fxSDSgfF+L5wqqJ9R4t+n53nyn0sKxeXlik7AY9Q==} - cpu: [arm64] - os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.37.0': resolution: {integrity: sha512-oNrJxcQT9IcbcmKlkF+Yz2tmOxZgG9D9GRq+1OE6XCQwCVwxixYAa38Z8qqPzQvzt1FCfmrHX03E0pWoXm1DqA==} cpu: [arm64] os: [linux] - - '@rollup/rollup-linux-arm64-musl@4.34.2': - resolution: {integrity: sha512-UlFk+E46TZEoxD9ufLKDBzfSG7Ki03fo6hsNRRRHF+KuvNZ5vd1RRVQm8YZlGsjcJG8R252XFK0xNPay+4WV7w==} - cpu: [arm64] - os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.37.0': resolution: {integrity: sha512-pfxLBMls+28Ey2enpX3JvjEjaJMBX5XlPCZNGxj4kdJyHduPBXtxYeb8alo0a7bqOoWZW2uKynhHxF/MWoHaGQ==} @@ -1284,96 +1240,58 @@ packages: os: [linux] libc: [musl] - '@rollup/rollup-linux-loongarch64-gnu@4.34.2': - resolution: {integrity: sha512-hJhfsD9ykx59jZuuoQgYT1GEcNNi3RCoEmbo5OGfG8RlHOiVS7iVNev9rhLKh7UBYq409f4uEw0cclTXx8nh8Q==} - cpu: [loong64] - os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.37.0': resolution: {integrity: sha512-yCE0NnutTC/7IGUq/PUHmoeZbIwq3KRh02e9SfFh7Vmc1Z7atuJRYWhRME5fKgT8aS20mwi1RyChA23qSyRGpA==} cpu: [loong64] os: [linux] - - '@rollup/rollup-linux-powerpc64le-gnu@4.34.2': - resolution: {integrity: sha512-g/O5IpgtrQqPegvqopvmdCF9vneLE7eqYfdPWW8yjPS8f63DNam3U4ARL1PNNB64XHZDHKpvO2Giftf43puB8Q==} - cpu: [ppc64] - os: [linux] + libc: [glibc] '@rollup/rollup-linux-powerpc64le-gnu@4.37.0': resolution: {integrity: sha512-NxcICptHk06E2Lh3a4Pu+2PEdZ6ahNHuK7o6Np9zcWkrBMuv21j10SQDJW3C9Yf/A/P7cutWoC/DptNLVsZ0VQ==} cpu: [ppc64] os: [linux] - - '@rollup/rollup-linux-riscv64-gnu@4.34.2': - resolution: {integrity: sha512-bSQijDC96M6PuooOuXHpvXUYiIwsnDmqGU8+br2U7iPoykNi9JtMUpN7K6xml29e0evK0/g0D1qbAUzWZFHY5Q==} - cpu: [riscv64] - os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.37.0': resolution: {integrity: sha512-PpWwHMPCVpFZLTfLq7EWJWvrmEuLdGn1GMYcm5MV7PaRgwCEYJAwiN94uBuZev0/J/hFIIJCsYw4nLmXA9J7Pw==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.37.0': resolution: {integrity: sha512-DTNwl6a3CfhGTAOYZ4KtYbdS8b+275LSLqJVJIrPa5/JuIufWWZ/QFvkxp52gpmguN95eujrM68ZG+zVxa8zHA==} cpu: [riscv64] os: [linux] - - '@rollup/rollup-linux-s390x-gnu@4.34.2': - resolution: {integrity: sha512-49TtdeVAsdRuiUHXPrFVucaP4SivazetGUVH8CIxVsNsaPHV4PFkpLmH9LeqU/R4Nbgky9lzX5Xe1NrzLyraVA==} - cpu: [s390x] - os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.37.0': resolution: {integrity: sha512-hZDDU5fgWvDdHFuExN1gBOhCuzo/8TMpidfOR+1cPZJflcEzXdCy1LjnklQdW8/Et9sryOPJAKAQRw8Jq7Tg+A==} cpu: [s390x] os: [linux] - - '@rollup/rollup-linux-x64-gnu@4.34.2': - resolution: {integrity: sha512-j+jFdfOycLIQ7FWKka9Zd3qvsIyugg5LeZuHF6kFlXo6MSOc6R1w37YUVy8VpAKd81LMWGi5g9J25P09M0SSIw==} - cpu: [x64] - os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.37.0': resolution: {integrity: sha512-pKivGpgJM5g8dwj0ywBwe/HeVAUSuVVJhUTa/URXjxvoyTT/AxsLTAbkHkDHG7qQxLoW2s3apEIl26uUe08LVQ==} cpu: [x64] os: [linux] - - '@rollup/rollup-linux-x64-musl@4.34.2': - resolution: {integrity: sha512-aDPHyM/D2SpXfSNCVWCxyHmOqN9qb7SWkY1+vaXqMNMXslZYnwh9V/UCudl6psyG0v6Ukj7pXanIpfZwCOEMUg==} - cpu: [x64] - os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.37.0': resolution: {integrity: sha512-E2lPrLKE8sQbY/2bEkVTGDEk4/49UYRVWgj90MY8yPjpnGBQ+Xi1Qnr7b7UIWw1NOggdFQFOLZ8+5CzCiz143w==} cpu: [x64] os: [linux] - - '@rollup/rollup-win32-arm64-msvc@4.34.2': - resolution: {integrity: sha512-LQRkCyUBnAo7r8dbEdtNU08EKLCJMgAk2oP5H3R7BnUlKLqgR3dUjrLBVirmc1RK6U6qhtDw29Dimeer8d5hzQ==} - cpu: [arm64] - os: [win32] + libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.37.0': resolution: {integrity: sha512-Jm7biMazjNzTU4PrQtr7VS8ibeys9Pn29/1bm4ph7CP2kf21950LgN+BaE2mJ1QujnvOc6p54eWWiVvn05SOBg==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.34.2': - resolution: {integrity: sha512-wt8OhpQUi6JuPFkm1wbVi1BByeag87LDFzeKSXzIdGcX4bMLqORTtKxLoCbV57BHYNSUSOKlSL4BYYUghainYA==} - cpu: [ia32] - os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.37.0': resolution: {integrity: sha512-e3/1SFm1OjefWICB2Ucstg2dxYDkDTZGDYgwufcbsxTHyqQps1UQf33dFEChBNmeSsTOyrjw2JJq0zbG5GF6RA==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.34.2': - resolution: {integrity: sha512-rUrqINax0TvrPBXrFKg0YbQx18NpPN3NNrgmaao9xRNbTwek7lOXObhx8tQy8gelmQ/gLaGy1WptpU2eKJZImg==} - cpu: [x64] - os: [win32] - '@rollup/rollup-win32-x64-msvc@4.37.0': resolution: {integrity: sha512-LWbXUBwn/bcLx2sSsqy7pK5o+Nr+VCoRoAohfJ5C/aBio9nfJmGQqHAhU6pwxV/RmyTk5AqdySma7uwWGlmeuA==} cpu: [x64] @@ -1464,6 +1382,9 @@ packages: '@tybys/wasm-util@0.9.0': resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/doctrine@0.0.9': resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} @@ -1571,21 +1492,25 @@ packages: resolution: {integrity: sha512-fp4Azi8kHz6TX8SFmKfyScZrMLfp++uRm2srpqRjsRZIIBzH74NtSkdEUHImR4G7f7XJ+sVZjCc6KDDK04YEpQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@unrs/rspack-resolver-binding-linux-arm64-musl@1.2.2': resolution: {integrity: sha512-gMiG3DCFioJxdGBzhlL86KcFgt9HGz0iDhw0YVYPsShItpN5pqIkNrI+L/Q/0gfDiGrfcE0X3VANSYIPmqEAlQ==} cpu: [arm64] os: [linux] + libc: [musl] '@unrs/rspack-resolver-binding-linux-x64-gnu@1.2.2': resolution: {integrity: sha512-n/4n2CxaUF9tcaJxEaZm+lqvaw2gflfWQ1R9I7WQgYkKEKbRKbpG/R3hopYdUmLSRI4xaW1Cy0Bz40eS2Yi4Sw==} cpu: [x64] os: [linux] + libc: [glibc] '@unrs/rspack-resolver-binding-linux-x64-musl@1.2.2': resolution: {integrity: sha512-cHyhAr6rlYYbon1L2Ag449YCj3p6XMfcYTP0AQX+KkQo025d1y/VFtPWvjMhuEsE2lLvtHm7GdJozj6BOMtzVg==} cpu: [x64] os: [linux] + libc: [musl] '@unrs/rspack-resolver-binding-wasm32-wasi@1.2.2': resolution: {integrity: sha512-eogDKuICghDLGc32FtP+WniG38IB1RcGOGz0G3z8406dUdjJvxfHGuGs/dSlM9YEp/v0lEqhJ4mBu6X2nL9pog==} @@ -1602,8 +1527,9 @@ packages: cpu: [x64] os: [win32] - '@vitejs/plugin-vue@5.2.3': - resolution: {integrity: sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg==} + '@vitejs/plugin-vue@https://pkg.pr.new/@vitejs/plugin-vue@c156992': + resolution: {tarball: https://pkg.pr.new/@vitejs/plugin-vue@c156992} + version: 5.2.1 engines: {node: ^18.0.0 || >=20.0.0} peerDependencies: vite: ^5.0.0 || ^6.0.0 @@ -1648,6 +1574,9 @@ packages: '@vitest/pretty-format@3.0.9': resolution: {integrity: sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==} + '@vitest/pretty-format@3.1.1': + resolution: {integrity: sha512-dg0CIzNx+hMMYfNmSqJlLSXEmnNhMswcn3sXO7Tpldr0LiGmg3eXdLLhwkv2ZqgHb/d5xg5F7ezNFRA1fA13yA==} + '@vitest/runner@3.0.9': resolution: {integrity: sha512-NX9oUXgF9HPfJSwl8tUZCMP1oGx2+Sf+ru6d05QjzQz4OwWg0psEzwY6VexP2tTHWdOkhKHUIZH+fS6nA7jfOw==} @@ -1657,11 +1586,16 @@ packages: '@vitest/spy@3.0.9': resolution: {integrity: sha512-/CcK2UDl0aQ2wtkp3YVWldrpLRNCfVcIOFGlVGKO4R5eajsH393Z1yiXLVQ7vWsj26JOEjeZI0x5sm5P4OGUNQ==} + '@vitest/ui@3.1.1': + resolution: {integrity: sha512-2HpiRIYg3dlvAJBV9RtsVswFgUSJK4Sv7QhpxoP0eBGkYwzGIKP34PjaV00AULQi9Ovl6LGyZfsetxDWY5BQdQ==} + peerDependencies: + vitest: 3.1.1 + '@vitest/utils@3.0.9': resolution: {integrity: sha512-ilHM5fHhZ89MCp5aAaM9uhfl1c2JdxVxl3McqsdVyVNN6JffnEen8UMCdRTzOhGXNQGo5GNL9QugHrz727Wnng==} - '@vitest/utils@3.0.4': - resolution: {integrity: sha512-8BqC1ksYsHtbWH+DfpOAKrFw3jl3Uf9J7yeFh85Pz52IWuh1hBBtyfEbRNNZNjl8H8A5yMLH9/t+k7HIKzQcZQ==} + '@vitest/utils@3.1.1': + resolution: {integrity: sha512-1XIjflyaU2k3HMArJ50bwSh3wKWPD6Q47wz/NUSmRV0zNywPc4w79ARjg/i/aNINHwA+mIALhUVqD9/aUvZNgg==} '@vue/compiler-core@3.5.13': resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==} @@ -1679,9 +1613,35 @@ packages: resolution: {integrity: sha512-oTyUE+QHIzLw2PpV14GD/c7EohDyP64xCniWTcqcEmTd699eFqTIwOmtDYjcO1j3QgdXoJEoWv1/cCdLrRoOfg==} engines: {node: '>= 0.12.0'} + '@vue/reactivity@3.5.13': + resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==} + '@vue/repl@4.5.1': resolution: {integrity: sha512-YYXvFue2GOrZ6EWnoA8yQVKzdCIn45+tpwJHzMof1uwrgyYAVY9ynxCsDYeAuWcpaAeylg/nybhFuqiFy2uvYA==} + '@vue/runtime-core@3.5.13': + resolution: {integrity: sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==} + + '@vue/runtime-dom@3.5.13': + resolution: {integrity: sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==} + + '@vue/server-renderer@3.5.13': + resolution: {integrity: sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==} + peerDependencies: + vue: 3.5.13 + + '@vue/shared@3.5.13': + resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==} + + '@vueuse/core@11.3.0': + resolution: {integrity: sha512-7OC4Rl1f9G8IT6rUfi9JrKiXy4bfmHhZ5x2Ceojy0jnd3mHNEvV4JaRygH362ror6/NZ+Nl+n13LPzGiPN8cKA==} + + '@vueuse/metadata@11.3.0': + resolution: {integrity: sha512-pwDnDspTqtTo2HwfLw4Rp6yywuuBdYnPYDq+mO38ZYKGebCUQC/nVj/PXSiK9HX5otxLz8Fn7ECPbjiRz2CC3g==} + + '@vueuse/shared@11.3.0': + resolution: {integrity: sha512-P8gSSWQeucH5821ek2mn/ciCk+MS/zoRKqdQIM3bHq6p7GXDAJLmnRRKmF5F65sAVJIfzQlwR3aDzwCn10s8hA==} + '@zeit/schemas@2.36.0': resolution: {integrity: sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==} @@ -1825,6 +1785,13 @@ packages: buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + bytes@3.0.0: resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} engines: {node: '>= 0.8'} @@ -2396,8 +2363,8 @@ packages: flatted@3.3.1: resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} - flatted@3.3.2: - resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==} + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} foreground-child@3.3.0: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} @@ -3098,6 +3065,9 @@ packages: path-to-regexp@3.3.0: resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==} + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -3167,10 +3137,6 @@ packages: postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.5.1: - resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==} - engines: {node: ^10 || ^12 || >=14} - postcss@8.5.3: resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} engines: {node: ^10 || ^12 || >=14} @@ -3351,11 +3317,6 @@ packages: peerDependencies: rollup: ^1.20.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 - rollup@4.34.2: - resolution: {integrity: sha512-sBDUoxZEaqLu9QeNalL8v3jw6WjPku4wfZGyTU7l7m1oC+rpRihXc/n/H+4148ZkGz5Xli8CHMns//fFGKvpIQ==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - rollup@4.37.0: resolution: {integrity: sha512-iAtQy/L4QFU+rTJ1YUjXqJOJzuwEghqWzCEYD2FEghT7Gsy1VdABntrO4CLopA5IkflTyqNiLNwPcOJ3S7UKLg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -3367,6 +3328,10 @@ packages: rspack-resolver@1.2.2: resolution: {integrity: sha512-Fwc19jMBA3g+fxDJH2B4WxwZjE0VaaOL7OX/A4Wn5Zv7bOD/vyPZhzXfaO73Xc2GAlfi96g5fGUa378WbIGfFw==} + run-applescript@7.0.0: + resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==} + engines: {node: '>=18'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -3437,8 +3402,8 @@ packages: resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} engines: {node: '>= 10'} - sirv@3.0.0: - resolution: {integrity: sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==} + sirv@3.0.1: + resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==} engines: {node: '>=18'} slice-ansi@5.0.0: @@ -3596,8 +3561,8 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyglobby@0.2.10: - resolution: {integrity: sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==} + tinyglobby@0.2.12: + resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==} engines: {node: '>=12.0.0'} tinypool@1.0.2: @@ -3699,6 +3664,10 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + unplugin-utils@0.2.4: resolution: {integrity: sha512-8U/MtpkPkkk3Atewj1+RcKIjb5WBimZ/WSLhhR3w6SsIj8XJuKTacSP8g+2JhfSGw0Cb125Y+2zA/IzJZDVbhA==} engines: {node: '>=18.12.0'} @@ -3723,15 +3692,67 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vite-hyper-config@0.4.1: + resolution: {integrity: sha512-w9D4g0+5Km8XCgkBY/BZrXZAl8FF2q1UpDXT/Fsm6VLEU5tkkzDCko8fjLPOaSbvirUJgbY5OsD5wuuZ6581Fg==} + engines: {node: '>=18.0.0'} + peerDependencies: + vite: ^4.0.0 || ^5.0.0 || ^6.0.0 + + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + vite-node@3.0.9: resolution: {integrity: sha512-w3Gdx7jDcuT9cNn9jExXgOyKmf5UOTb6WMHz8LGAm54eS1Elf5OuBhCxl6zJxGhEeIkgsE1WbHuoL0mj/UXqXg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true + vite-plugin-inspect@0.8.7: + resolution: {integrity: sha512-/XXou3MVc13A5O9/2Nd6xczjrUwt7ZyI9h8pTnUMkr5SshLcb0PJUOVq2V+XVkdeU4njsqAtmK87THZuO2coGA==} + engines: {node: '>=14'} + peerDependencies: + '@nuxt/kit': '*' + vite: ^3.1.0 || ^4.0.0 || ^5.0.0-0 + peerDependenciesMeta: + '@nuxt/kit': + optional: true + vite@5.4.14: resolution: {integrity: sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vite@6.2.4: + resolution: {integrity: sha512-veHMSew8CcRzhL5o8ONjy8gkfmFJAd5Ac16oxBUjlwgX3Gq2Wqr+qNC3TjPIpy7TPV/KporLga5GT9HqdrCizw==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true peerDependencies: '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 jiti: '>=1.21.0' @@ -4355,6 +4376,8 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@polka/url@1.0.0-next.28': {} + '@puppeteer/browsers@2.8.0': dependencies: debug: 4.4.0 @@ -4422,120 +4445,63 @@ snapshots: optionalDependencies: rollup: 4.37.0 - '@rollup/rollup-android-arm-eabi@4.34.2': - optional: true - '@rollup/rollup-android-arm-eabi@4.37.0': optional: true - '@rollup/rollup-android-arm64@4.34.2': - optional: true - '@rollup/rollup-android-arm64@4.37.0': optional: true - '@rollup/rollup-darwin-arm64@4.34.2': - optional: true - '@rollup/rollup-darwin-arm64@4.37.0': optional: true - '@rollup/rollup-darwin-x64@4.34.2': - optional: true - '@rollup/rollup-darwin-x64@4.37.0': optional: true - '@rollup/rollup-freebsd-arm64@4.34.2': - optional: true - '@rollup/rollup-freebsd-arm64@4.37.0': optional: true - '@rollup/rollup-freebsd-x64@4.34.2': - optional: true - '@rollup/rollup-freebsd-x64@4.37.0': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.34.2': - optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.37.0': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.34.2': - optional: true - '@rollup/rollup-linux-arm-musleabihf@4.37.0': optional: true - '@rollup/rollup-linux-arm64-gnu@4.34.2': - optional: true - '@rollup/rollup-linux-arm64-gnu@4.37.0': optional: true - '@rollup/rollup-linux-arm64-musl@4.34.2': - optional: true - '@rollup/rollup-linux-arm64-musl@4.37.0': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.34.2': - optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.37.0': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.34.2': - optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.37.0': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.34.2': - optional: true - '@rollup/rollup-linux-riscv64-gnu@4.37.0': optional: true '@rollup/rollup-linux-riscv64-musl@4.37.0': optional: true - '@rollup/rollup-linux-s390x-gnu@4.34.2': - optional: true - '@rollup/rollup-linux-s390x-gnu@4.37.0': optional: true - '@rollup/rollup-linux-x64-gnu@4.34.2': - optional: true - '@rollup/rollup-linux-x64-gnu@4.37.0': optional: true - '@rollup/rollup-linux-x64-musl@4.34.2': - optional: true - '@rollup/rollup-linux-x64-musl@4.37.0': optional: true - '@rollup/rollup-win32-arm64-msvc@4.34.2': - optional: true - '@rollup/rollup-win32-arm64-msvc@4.37.0': optional: true - '@rollup/rollup-win32-ia32-msvc@4.34.2': - optional: true - '@rollup/rollup-win32-ia32-msvc@4.37.0': optional: true - '@rollup/rollup-win32-x64-msvc@4.34.2': - optional: true - '@rollup/rollup-win32-x64-msvc@4.37.0': optional: true @@ -4598,6 +4564,10 @@ snapshots: tslib: 2.8.1 optional: true + '@types/connect@3.4.38': + dependencies: + '@types/node': 22.13.13 + '@types/doctrine@0.0.9': {} '@types/estree@1.0.6': {} @@ -4741,12 +4711,17 @@ snapshots: '@unrs/rspack-resolver-binding-win32-x64-msvc@1.2.2': optional: true - '@vitejs/plugin-vue@5.2.3(vite@5.4.14(@types/node@22.13.13)(sass@1.86.0))(vue@packages+vue)': + '@vitejs/plugin-vue@https://pkg.pr.new/@vitejs/plugin-vue@c156992(vite@6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.6.2))': dependencies: - vite: 5.4.14(@types/node@22.13.13)(sass@1.86.0) + vite: 6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) + vue: 3.5.13(typescript@5.6.2) + + '@vitejs/plugin-vue@https://pkg.pr.new/@vitejs/plugin-vue@c156992(vite@6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0))(vue@packages+vue)': + dependencies: + vite: 6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) vue: link:packages/vue - '@vitest/coverage-v8@3.0.9(vitest@3.0.9(@types/node@22.13.13)(jsdom@26.0.0)(sass@1.86.0))': + '@vitest/coverage-v8@3.0.9(vitest@3.0.9)': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -4760,17 +4735,17 @@ snapshots: std-env: 3.8.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.0.9(@types/node@22.13.13)(jsdom@26.0.0)(sass@1.86.0) + vitest: 3.0.9(@types/node@22.13.13)(@vitest/ui@3.1.1)(jsdom@26.0.0)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) transitivePeerDependencies: - supports-color - '@vitest/eslint-plugin@1.1.38(@typescript-eslint/utils@8.27.0(eslint@9.23.0)(typescript@5.6.2))(eslint@9.23.0)(typescript@5.6.2)(vitest@3.0.9(@types/node@22.13.13)(jsdom@26.0.0)(sass@1.86.0))': + '@vitest/eslint-plugin@1.1.38(@typescript-eslint/utils@8.27.0(eslint@9.23.0)(typescript@5.6.2))(eslint@9.23.0)(typescript@5.6.2)(vitest@3.0.9)': dependencies: '@typescript-eslint/utils': 8.27.0(eslint@9.23.0)(typescript@5.6.2) eslint: 9.23.0 optionalDependencies: typescript: 5.6.2 - vitest: 3.0.9(@types/node@22.13.13)(jsdom@26.0.0)(sass@1.86.0) + vitest: 3.0.9(@types/node@22.13.13)(@vitest/ui@3.1.1)(jsdom@26.0.0)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) '@vitest/expect@3.0.9': dependencies: @@ -4779,18 +4754,22 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.0.9(vite@5.4.14(@types/node@22.13.13)(sass@1.86.0))': + '@vitest/mocker@3.0.9(vite@6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0))': dependencies: '@vitest/spy': 3.0.9 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 5.4.14(@types/node@22.13.13)(sass@1.86.0) + vite: 6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) '@vitest/pretty-format@3.0.9': dependencies: tinyrainbow: 2.0.0 + '@vitest/pretty-format@3.1.1': + dependencies: + tinyrainbow: 2.0.0 + '@vitest/runner@3.0.9': dependencies: '@vitest/utils': 3.0.9 @@ -4806,21 +4785,32 @@ snapshots: dependencies: tinyspy: 3.0.2 + '@vitest/ui@3.1.1(vitest@3.0.9)': + dependencies: + '@vitest/utils': 3.1.1 + fflate: 0.8.2 + flatted: 3.3.3 + pathe: 2.0.3 + sirv: 3.0.1 + tinyglobby: 0.2.12 + tinyrainbow: 2.0.0 + vitest: 3.0.9(@types/node@22.13.13)(@vitest/ui@3.1.1)(jsdom@26.0.0)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) + '@vitest/utils@3.0.9': dependencies: '@vitest/pretty-format': 3.0.9 loupe: 3.1.3 tinyrainbow: 2.0.0 - '@vitest/utils@3.0.4': + '@vitest/utils@3.1.1': dependencies: - '@vitest/pretty-format': 3.0.4 - loupe: 3.1.2 + '@vitest/pretty-format': 3.1.1 + loupe: 3.1.3 tinyrainbow: 2.0.0 '@vue/compiler-core@3.5.13': dependencies: - '@babel/parser': 7.26.2 + '@babel/parser': 7.26.10 '@vue/shared': 3.5.13 entities: 4.5.0 estree-walker: 2.0.2 @@ -4833,14 +4823,14 @@ snapshots: '@vue/compiler-sfc@3.5.13': dependencies: - '@babel/parser': 7.26.2 + '@babel/parser': 7.26.10 '@vue/compiler-core': 3.5.13 '@vue/compiler-dom': 3.5.13 '@vue/compiler-ssr': 3.5.13 '@vue/shared': 3.5.13 estree-walker: 2.0.2 magic-string: 0.30.17 - postcss: 8.5.1 + postcss: 8.5.3 source-map-js: 1.2.1 '@vue/compiler-ssr@3.5.13': @@ -4850,8 +4840,51 @@ snapshots: '@vue/consolidate@1.0.0': {} + '@vue/reactivity@3.5.13': + dependencies: + '@vue/shared': 3.5.13 + '@vue/repl@4.5.1': {} + '@vue/runtime-core@3.5.13': + dependencies: + '@vue/reactivity': 3.5.13 + '@vue/shared': 3.5.13 + + '@vue/runtime-dom@3.5.13': + dependencies: + '@vue/reactivity': 3.5.13 + '@vue/runtime-core': 3.5.13 + '@vue/shared': 3.5.13 + csstype: 3.1.3 + + '@vue/server-renderer@3.5.13(vue@3.5.13(typescript@5.6.2))': + dependencies: + '@vue/compiler-ssr': 3.5.13 + '@vue/shared': 3.5.13 + vue: 3.5.13(typescript@5.6.2) + + '@vue/shared@3.5.13': {} + + '@vueuse/core@11.3.0(vue@packages+vue)': + dependencies: + '@types/web-bluetooth': 0.0.20 + '@vueuse/metadata': 11.3.0 + '@vueuse/shared': 11.3.0(vue@packages+vue) + vue-demi: 0.14.10(vue@packages+vue) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@vueuse/metadata@11.3.0': {} + + '@vueuse/shared@11.3.0(vue@packages+vue)': + dependencies: + vue-demi: 0.14.10(vue@packages+vue) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + '@zeit/schemas@2.36.0': {} accepts@1.3.8: @@ -4988,6 +5021,13 @@ snapshots: buffer-crc32@0.2.13: {} + buffer-from@1.1.2: + optional: true + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.0.0 + bytes@3.0.0: {} cac@6.7.14: {} @@ -5636,7 +5676,7 @@ snapshots: flatted@3.3.1: {} - flatted@3.3.2: {} + flatted@3.3.3: {} foreground-child@3.3.0: dependencies: @@ -6326,6 +6366,8 @@ snapshots: path-to-regexp@3.3.0: {} + pathe@1.1.2: {} + pathe@2.0.3: {} pathval@2.0.0: {} @@ -6387,12 +6429,6 @@ snapshots: postcss-value-parser@4.2.0: {} - postcss@8.5.1: - dependencies: - nanoid: 3.3.8 - picocolors: 1.1.1 - source-map-js: 1.2.1 - postcss@8.5.3: dependencies: nanoid: 3.3.8 @@ -6634,31 +6670,6 @@ snapshots: '@rollup/plugin-inject': 5.0.5(rollup@4.37.0) rollup: 4.37.0 - rollup@4.34.2: - dependencies: - '@types/estree': 1.0.6 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.34.2 - '@rollup/rollup-android-arm64': 4.34.2 - '@rollup/rollup-darwin-arm64': 4.34.2 - '@rollup/rollup-darwin-x64': 4.34.2 - '@rollup/rollup-freebsd-arm64': 4.34.2 - '@rollup/rollup-freebsd-x64': 4.34.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.34.2 - '@rollup/rollup-linux-arm-musleabihf': 4.34.2 - '@rollup/rollup-linux-arm64-gnu': 4.34.2 - '@rollup/rollup-linux-arm64-musl': 4.34.2 - '@rollup/rollup-linux-loongarch64-gnu': 4.34.2 - '@rollup/rollup-linux-powerpc64le-gnu': 4.34.2 - '@rollup/rollup-linux-riscv64-gnu': 4.34.2 - '@rollup/rollup-linux-s390x-gnu': 4.34.2 - '@rollup/rollup-linux-x64-gnu': 4.34.2 - '@rollup/rollup-linux-x64-musl': 4.34.2 - '@rollup/rollup-win32-arm64-msvc': 4.34.2 - '@rollup/rollup-win32-ia32-msvc': 4.34.2 - '@rollup/rollup-win32-x64-msvc': 4.34.2 - fsevents: 2.3.3 - rollup@4.37.0: dependencies: '@types/estree': 1.0.6 @@ -6701,6 +6712,8 @@ snapshots: '@unrs/rspack-resolver-binding-win32-arm64-msvc': 1.2.2 '@unrs/rspack-resolver-binding-win32-x64-msvc': 1.2.2 + run-applescript@7.0.0: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -6780,13 +6793,13 @@ snapshots: sirv@2.0.4: dependencies: - '@polka/url': 1.0.0-next.25 + '@polka/url': 1.0.0-next.28 mrmime: 2.0.0 totalist: 3.0.1 - sirv@3.0.0: + sirv@3.0.1: dependencies: - '@polka/url': 1.0.0-next.25 + '@polka/url': 1.0.0-next.28 mrmime: 2.0.0 totalist: 3.0.1 @@ -6953,7 +6966,7 @@ snapshots: tinyexec@0.3.2: {} - tinyglobby@0.2.10: + tinyglobby@0.2.12: dependencies: fdir: 6.4.3(picomatch@4.0.2) picomatch: 4.0.2 @@ -7027,6 +7040,8 @@ snapshots: universalify@2.0.1: {} + unpipe@1.0.0: {} + unplugin-utils@0.2.4: dependencies: pathe: 2.0.3 @@ -7052,13 +7067,48 @@ snapshots: vary@1.1.2: {} - vite-node@3.0.9(@types/node@22.13.13)(sass@1.86.0): + vite-hyper-config@0.4.1(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(vite@6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0)): + dependencies: + cac: 6.7.14 + picocolors: 1.1.1 + vite: 6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) + vite-node: 2.1.9(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite-node@2.1.9(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0): + dependencies: + cac: 6.7.14 + debug: 4.4.0 + es-module-lexer: 1.6.0 + pathe: 1.1.2 + vite: 5.4.14(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite-node@3.0.9(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0): dependencies: cac: 6.7.14 debug: 4.4.0 es-module-lexer: 1.6.0 pathe: 2.0.3 - vite: 5.4.14(@types/node@22.13.13)(sass@1.86.0) + vite: 6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) transitivePeerDependencies: - '@types/node' - jiti @@ -7073,10 +7123,10 @@ snapshots: - tsx - yaml - vite-plugin-inspect@0.8.7(rollup@4.31.0)(vite@6.1.0(@types/node@22.10.7)(sass@1.83.4)(terser@5.33.0)(yaml@2.6.1)): + vite-plugin-inspect@0.8.7(rollup@4.37.0)(vite@6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0)): dependencies: '@antfu/utils': 0.7.10 - '@rollup/pluginutils': 5.1.0(rollup@4.31.0) + '@rollup/pluginutils': 5.1.0(rollup@4.37.0) debug: 4.4.0 error-stack-parser-es: 0.1.5 fs-extra: 11.2.0 @@ -7084,25 +7134,38 @@ snapshots: perfect-debounce: 1.0.0 picocolors: 1.1.1 sirv: 2.0.4 - vite: 6.1.0(@types/node@22.10.7)(sass@1.83.4)(terser@5.33.0)(yaml@2.6.1) + vite: 6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) transitivePeerDependencies: - rollup - supports-color - vite@5.4.14(@types/node@22.13.13)(sass@1.86.0): + vite@5.4.14(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0): dependencies: - esbuild: 0.24.2 - postcss: 8.5.1 - rollup: 4.34.2 + esbuild: 0.21.5 + postcss: 8.5.3 + rollup: 4.37.0 optionalDependencies: '@types/node': 22.13.13 fsevents: 2.3.3 sass: 1.86.0 + terser: 5.33.0 + + vite@6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0): + dependencies: + esbuild: 0.25.1 + postcss: 8.5.3 + rollup: 4.37.0 + optionalDependencies: + '@types/node': 22.13.13 + fsevents: 2.3.3 + sass: 1.86.0 + terser: 5.33.0 + yaml: 2.7.0 - vitest@3.0.9(@types/node@22.13.13)(jsdom@26.0.0)(sass@1.86.0): + vitest@3.0.9(@types/node@22.13.13)(@vitest/ui@3.1.1)(jsdom@26.0.0)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0): dependencies: '@vitest/expect': 3.0.9 - '@vitest/mocker': 3.0.9(vite@5.4.14(@types/node@22.13.13)(sass@1.86.0)) + '@vitest/mocker': 3.0.9(vite@6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0)) '@vitest/pretty-format': 3.0.9 '@vitest/runner': 3.0.9 '@vitest/snapshot': 3.0.9 @@ -7118,11 +7181,12 @@ snapshots: tinyexec: 0.3.2 tinypool: 1.0.2 tinyrainbow: 2.0.0 - vite: 5.4.14(@types/node@22.13.13)(sass@1.86.0) - vite-node: 3.0.9(@types/node@22.13.13)(sass@1.86.0) + vite: 6.2.4(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) + vite-node: 3.0.9(@types/node@22.13.13)(sass@1.86.0)(terser@5.33.0)(yaml@2.7.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.13.13 + '@vitest/ui': 3.1.1(vitest@3.0.9) jsdom: 26.0.0 transitivePeerDependencies: - jiti From 016596c429b1204b73c4b3b356bfcc7448e1a624 Mon Sep 17 00:00:00 2001 From: daiwei Date: Mon, 7 Apr 2025 11:35:15 +0800 Subject: [PATCH 30/40] chore: update --- packages/runtime-vapor/src/components/Teleport.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/runtime-vapor/src/components/Teleport.ts b/packages/runtime-vapor/src/components/Teleport.ts index 8f746887090..ca1f184d5dd 100644 --- a/packages/runtime-vapor/src/components/Teleport.ts +++ b/packages/runtime-vapor/src/components/Teleport.ts @@ -29,10 +29,7 @@ export const VaporTeleportImpl = { __vapor: true, process(props: LooseRawProps, slots: LooseRawSlots): TeleportFragment { - const frag = __DEV__ - ? new TeleportFragment('teleport') - : new TeleportFragment() - + const frag = new TeleportFragment() const updateChildrenEffect = renderEffect(() => frag.updateChildren(slots.default && (slots.default as BlockFn)()), ) @@ -93,10 +90,9 @@ export class TeleportFragment extends VaporFragment { private mountContainer?: ParentNode | null private mountAnchor?: Node | null - constructor(anchorLabel?: string) { + constructor() { super([]) - this.anchor = - __DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode() + this.anchor = __DEV__ ? createComment('teleport') : createTextNode() } get currentParent(): ParentNode { From d9772db54e71910c13427bb78c11b587a0919a1f Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 10 Apr 2025 15:56:28 +0800 Subject: [PATCH 31/40] chore: update --- packages/runtime-vapor/src/components/Teleport.ts | 11 ----------- packages/runtime-vapor/src/index.ts | 2 +- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/packages/runtime-vapor/src/components/Teleport.ts b/packages/runtime-vapor/src/components/Teleport.ts index ca1f184d5dd..dc4dab68cdb 100644 --- a/packages/runtime-vapor/src/components/Teleport.ts +++ b/packages/runtime-vapor/src/components/Teleport.ts @@ -214,17 +214,6 @@ export class TeleportFragment extends VaporFragment { } } -export const VaporTeleport = VaporTeleportImpl as unknown as { - __vapor: true - __isTeleport: true - new (): { - $props: TeleportProps - $slots: { - default(): Block - } - } -} - export function isVaporTeleport( value: unknown, ): value is typeof VaporTeleportImpl { diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index c2716059df2..051944443ad 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -3,7 +3,7 @@ export { createVaporApp, createVaporSSRApp } from './apiCreateApp' export { defineVaporComponent } from './apiDefineComponent' export { vaporInteropPlugin } from './vdomInterop' export type { VaporDirective } from './directives/custom' -export { VaporTeleport } from './components/Teleport' +export { VaporTeleportImpl as VaporTeleport } from './components/Teleport' // compiler-use only export { insert, prepend, remove } from './block' From e01189fcdaaab841d3e25a447825956709ef53ee Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 16 Jul 2025 22:07:08 +0800 Subject: [PATCH 32/40] fix: update SchedulerJob export --- packages/runtime-core/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index a1a500e9539..2664c16c9fc 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -345,6 +345,7 @@ export type { HydrationStrategyFactory, } from './hydrationStrategies' export type { HMRRuntime } from './hmr' +export type { SchedulerJob } from './scheduler' // Internal API ---------------------------------------------------------------- @@ -521,7 +522,7 @@ export { baseEmit, isEmitListener } from './componentEmits' /** * @internal */ -export { type SchedulerJob, queueJob, flushOnAppMount } from './scheduler' +export { queueJob, flushOnAppMount } from './scheduler' /** * @internal */ From 99c38288ec1ca61bac60120d45501b5c9b773e7c Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 10 Sep 2025 16:00:15 +0800 Subject: [PATCH 33/40] refactor: teleport --- .../__tests__/components/Teleport.spec.ts | 161 ++++++++++++++---- packages/runtime-vapor/src/block.ts | 6 +- .../runtime-vapor/src/components/Teleport.ts | 62 +++---- packages/runtime-vapor/src/fragment.ts | 2 - 4 files changed, 160 insertions(+), 71 deletions(-) diff --git a/packages/runtime-vapor/__tests__/components/Teleport.spec.ts b/packages/runtime-vapor/__tests__/components/Teleport.spec.ts index bad5208726c..d4282ab8a6e 100644 --- a/packages/runtime-vapor/__tests__/components/Teleport.spec.ts +++ b/packages/runtime-vapor/__tests__/components/Teleport.spec.ts @@ -2,6 +2,7 @@ import { type LooseRawProps, type VaporComponent, createComponent as createComp, + createComponent, } from '../../src/component' import { type VaporDirective, @@ -9,15 +10,18 @@ import { child, createIf, createTemplateRefSetter, + createVaporApp, defineVaporComponent, renderEffect, setInsertionState, setText, template, + vaporInteropPlugin, withVaporDirectives, } from '@vue/runtime-vapor' import { makeRender } from '../_utils' import { + h, nextTick, onBeforeUnmount, onMounted, @@ -63,7 +67,7 @@ describe('renderer: VaporTeleport', () => { mount(root) expect(root.innerHTML).toBe( - '
teleported
', + '
teleported
', ) }) @@ -116,7 +120,7 @@ describe('renderer: VaporTeleport', () => { show.value = true await nextTick() expect(root.innerHTML).toBe( - `
Footer
bar
`, + `
Footer
bar
`, ) }) }) @@ -155,7 +159,9 @@ describe('renderer: VaporTeleport', () => { createRecord(parentId, Parent as any) mount(root) - expect(root.innerHTML).toBe('
root
') + expect(root.innerHTML).toBe( + '
root
', + ) expect(target.innerHTML).toBe('
teleported
') // rerender child @@ -163,7 +169,9 @@ describe('renderer: VaporTeleport', () => { return template('
teleported 2
')() }) - expect(root.innerHTML).toBe('
root
') + expect(root.innerHTML).toBe( + '
root
', + ) expect(target.innerHTML).toBe('
teleported 2
') // rerender parent @@ -181,7 +189,9 @@ describe('renderer: VaporTeleport', () => { return [n0, n1] }) - expect(root.innerHTML).toBe('
root 2
') + expect(root.innerHTML).toBe( + '
root 2
', + ) expect(target.innerHTML).toBe('
teleported 2
') }) @@ -219,7 +229,7 @@ describe('renderer: VaporTeleport', () => { mount(root) expect(root.innerHTML).toBe( - '
teleported
root
', + '
teleported
root
', ) expect(target.innerHTML).toBe('') @@ -241,14 +251,16 @@ describe('renderer: VaporTeleport', () => { }) expect(root.innerHTML).toBe( - '
teleported
root 2
', + '
teleported
root 2
', ) expect(target.innerHTML).toBe('') // toggle disabled disabled.value = false await nextTick() - expect(root.innerHTML).toBe('
root 2
') + expect(root.innerHTML).toBe( + '
root 2
', + ) expect(target.innerHTML).toBe('
teleported
') }) @@ -300,7 +312,9 @@ describe('renderer: VaporTeleport', () => { createRecord(parentId, Parent as any) mount(root) - expect(root.innerHTML).toBe('
root
') + expect(root.innerHTML).toBe( + '
root
', + ) expect(target.innerHTML).toBe('
teleported
') // reload child by changing msg @@ -318,7 +332,9 @@ describe('renderer: VaporTeleport', () => { return [n0] }, }) - expect(root.innerHTML).toBe('
root
') + expect(root.innerHTML).toBe( + '
root
', + ) expect(target.innerHTML).toBe('
teleported 2
') // reload parent by changing msg @@ -348,7 +364,9 @@ describe('renderer: VaporTeleport', () => { }, }) - expect(root.innerHTML).toBe('
root 2
') + expect(root.innerHTML).toBe( + '
root 2
', + ) expect(target.innerHTML).toBe('
teleported 2
') // reload parent again by changing disabled @@ -379,7 +397,7 @@ describe('renderer: VaporTeleport', () => { }) expect(root.innerHTML).toBe( - '
teleported 2
root 2
', + '
teleported 2
root 2
', ) expect(target.innerHTML).toBe('') }) @@ -434,7 +452,7 @@ describe('renderer: VaporTeleport', () => { mount(root) expect(root.innerHTML).toBe( - '
teleported
root
', + '
teleported
root
', ) expect(target.innerHTML).toBe('') @@ -454,7 +472,7 @@ describe('renderer: VaporTeleport', () => { }, }) expect(root.innerHTML).toBe( - '
teleported 2
root
', + '
teleported 2
root
', ) expect(target.innerHTML).toBe('') @@ -474,14 +492,16 @@ describe('renderer: VaporTeleport', () => { }, }) expect(root.innerHTML).toBe( - '
teleported 3
root
', + '
teleported 3
root
', ) expect(target.innerHTML).toBe('') // toggle disabled disabled.value = false await nextTick() - expect(root.innerHTML).toBe('
root
') + expect(root.innerHTML).toBe( + '
root
', + ) expect(target.innerHTML).toBe('
teleported 3
') }) @@ -537,7 +557,7 @@ describe('renderer: VaporTeleport', () => { mount(root) expect(root.innerHTML).toBe( - '
teleported
child
root
', + '
teleported
child
root
', ) expect(target.innerHTML).toBe('') @@ -557,7 +577,7 @@ describe('renderer: VaporTeleport', () => { }, }) expect(root.innerHTML).toBe( - '
teleported 2
child
root
', + '
teleported 2
child
root
', ) expect(target.innerHTML).toBe('') @@ -577,17 +597,68 @@ describe('renderer: VaporTeleport', () => { }, }) expect(root.innerHTML).toBe( - '
teleported 3
child
root
', + '
teleported 3
child
root
', ) expect(target.innerHTML).toBe('') // toggle disabled disabled.value = false await nextTick() - expect(root.innerHTML).toBe('
root
') + expect(root.innerHTML).toBe( + '
root
', + ) expect(target.innerHTML).toBe('
teleported 3
child') }) }) + + describe('VDOM interop', () => { + test('render vdom component', async () => { + const target = document.createElement('div') + const root = document.createElement('div') + + const VDOMComp = { + setup() { + return () => h('h1', null, 'vdom comp') + }, + } + + const disabled = ref(true) + const App = defineVaporComponent({ + setup() { + const n1 = createComponent( + VaporTeleport, + { + to: () => target, + defer: () => '', + disabled: () => disabled.value, + }, + { + default: () => { + const n0 = createComponent(VDOMComp) + return n0 + }, + }, + true, + ) + return n1 + }, + }) + + const app = createVaporApp(App) + app.use(vaporInteropPlugin) + app.mount(root) + + expect(target.innerHTML).toBe('') + expect(root.innerHTML).toBe( + '

vdom comp

', + ) + + disabled.value = false + await nextTick() + expect(root.innerHTML).toBe('') + expect(target.innerHTML).toBe('

vdom comp

') + }) + }) }) function runSharedTests(deferMode: boolean): void { @@ -625,7 +696,9 @@ function runSharedTests(deferMode: boolean): void { }).create() mount(root) - expect(root.innerHTML).toBe('
root
') + expect(root.innerHTML).toBe( + '
root
', + ) expect(target.innerHTML).toBe('
teleported
') }) @@ -654,14 +727,18 @@ function runSharedTests(deferMode: boolean): void { }).create() mount(root) - expect(root.innerHTML).toBe('
root
') + expect(root.innerHTML).toBe( + '
root
', + ) expect(targetA.innerHTML).toBe('
teleported
') expect(targetB.innerHTML).toBe('') target.value = targetB await nextTick() - expect(root.innerHTML).toBe('
root
') + expect(root.innerHTML).toBe( + '
root
', + ) expect(targetA.innerHTML).toBe('') expect(targetB.innerHTML).toBe('
teleported
') }) @@ -834,7 +911,9 @@ function runSharedTests(deferMode: boolean): void { }, }).create() mount(root) - expect(root.innerHTML).toBe('
') + expect(root.innerHTML).toBe( + '
', + ) expect(target.innerHTML).toBe('
one
two') // update existing content @@ -849,7 +928,9 @@ function runSharedTests(deferMode: boolean): void { // toggling child1.value = [] as any await nextTick() - expect(root.innerHTML).toBe('
') + expect(root.innerHTML).toBe( + '
', + ) expect(target.innerHTML).toBe('three') // toggle back @@ -859,14 +940,18 @@ function runSharedTests(deferMode: boolean): void { ] as any child2.value = [template('three')()] as any await nextTick() - expect(root.innerHTML).toBe('
') + expect(root.innerHTML).toBe( + '
', + ) // should append expect(target.innerHTML).toBe('
one
two
three') // toggle the other teleport child2.value = [] as any await nextTick() - expect(root.innerHTML).toBe('
') + expect(root.innerHTML).toBe( + '
', + ) expect(target.innerHTML).toBe('
one
two
') }) @@ -897,12 +982,12 @@ function runSharedTests(deferMode: boolean): void { mount(root) expect(root.innerHTML).toBe( - '
teleported
', + '
teleported
', ) disabled.value = false await nextTick() expect(root.innerHTML).toBe( - '
teleported
', + '
teleported
', ) }) @@ -929,13 +1014,15 @@ function runSharedTests(deferMode: boolean): void { }).create() mount(root) - expect(root.innerHTML).toBe('
root
') + expect(root.innerHTML).toBe( + '
root
', + ) expect(target.innerHTML).toBe('
teleported
') disabled.value = true await nextTick() expect(root.innerHTML).toBe( - '
teleported
root
', + '
teleported
root
', ) expect(target.innerHTML).toBe('') @@ -943,7 +1030,7 @@ function runSharedTests(deferMode: boolean): void { disabled.value = false await nextTick() expect(root.innerHTML).toBe( - '
root
', + '
root
', ) expect(target.innerHTML).toBe('
teleported
') }) @@ -984,14 +1071,14 @@ function runSharedTests(deferMode: boolean): void { }).create() mount(root) - expect(root.innerHTML).toBe('') + expect(root.innerHTML).toBe('') expect(target.innerHTML).toBe('
foo
') expect(spy).toHaveBeenCalledTimes(1) expect(teardown).not.toHaveBeenCalled() toggle.value = false await nextTick() - expect(root.innerHTML).toBe('') + expect(root.innerHTML).toBe('') expect(target.innerHTML).toBe('') expect(spy).toHaveBeenCalledTimes(1) expect(teardown).toHaveBeenCalledTimes(1) @@ -1078,7 +1165,9 @@ function runSharedTests(deferMode: boolean): void { show.value = true await nextTick() - expect(root.innerHTML).toBe('
teleported
') + expect(root.innerHTML).toBe( + '
teleported
', + ) show.value = false await nextTick() @@ -1125,7 +1214,7 @@ function runSharedTests(deferMode: boolean): void { parentShow.value = true await nextTick() expect(root.innerHTML).toBe( - '
foo
', + '
foo
', ) parentShow.value = false diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index b404555770b..f6ebb756b95 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -64,14 +64,14 @@ export function insert( insert(b, parent, anchor) } } else { + if (block.anchor) insert(block.anchor, parent, anchor) // fragment if (block.insert) { // TODO handle hydration for vdom interop - block.insert(parent, anchor) + block.insert(parent, block.anchor || anchor) } else { - insert(block.nodes, block.target || parent, block.targetAnchor || anchor) + insert(block.nodes, parent, block.anchor || anchor) } - if (block.anchor) insert(block.anchor, parent, anchor) } } diff --git a/packages/runtime-vapor/src/components/Teleport.ts b/packages/runtime-vapor/src/components/Teleport.ts index dc4dab68cdb..29662f50593 100644 --- a/packages/runtime-vapor/src/components/Teleport.ts +++ b/packages/runtime-vapor/src/components/Teleport.ts @@ -35,13 +35,12 @@ export const VaporTeleportImpl = { ) const updateEffect = renderEffect(() => { - frag.update( - // access the props to trigger tracking - extend( - {}, - new Proxy(props, rawPropsProxyHandlers) as any as TeleportProps, - ), + // access the props to trigger tracking + frag.props = extend( + {}, + new Proxy(props, rawPropsProxyHandlers) as any as TeleportProps, ) + frag.update() }) if (__DEV__) { @@ -82,7 +81,10 @@ export const VaporTeleportImpl = { } export class TeleportFragment extends VaporFragment { + target?: ParentNode | null + targetAnchor?: Node | null anchor: Node + props?: TeleportProps private targetStart?: Node private mainAnchor?: Node @@ -92,7 +94,7 @@ export class TeleportFragment extends VaporFragment { constructor() { super([]) - this.anchor = __DEV__ ? createComment('teleport') : createTextNode() + this.anchor = createTextNode() } get currentParent(): ParentNode { @@ -104,7 +106,7 @@ export class TeleportFragment extends VaporFragment { } get parent(): ParentNode | null { - return this.anchor.parentNode + return this.anchor && this.anchor.parentNode } updateChildren(children: Block): void { @@ -120,7 +122,10 @@ export class TeleportFragment extends VaporFragment { insert((this.nodes = children), this.currentParent, this.currentAnchor) } - update(props: TeleportProps): void { + update(): void { + // not mounted yet + if (!this.parent) return + const mount = (parent: ParentNode, anchor: Node | null) => { insert( this.nodes, @@ -130,7 +135,10 @@ export class TeleportFragment extends VaporFragment { } const mountToTarget = () => { - const target = (this.target = resolveTeleportTarget(props, querySelector)) + const target = (this.target = resolveTeleportTarget( + this.props!, + querySelector, + )) if (target) { if ( // initial mount into target @@ -153,29 +161,12 @@ export class TeleportFragment extends VaporFragment { } // mount into main container - if (isTeleportDisabled(props)) { - if (this.parent) { - if (!this.mainAnchor) { - this.mainAnchor = __DEV__ - ? createComment('teleport end') - : createTextNode() - } - if (!this.placeholder) { - this.placeholder = __DEV__ - ? createComment('teleport start') - : createTextNode() - } - if (!this.mainAnchor.isConnected) { - insert(this.placeholder, this.parent, this.anchor) - insert(this.mainAnchor, this.parent, this.anchor) - } - - mount(this.parent, this.mainAnchor) - } + if (isTeleportDisabled(this.props!)) { + mount(this.parent, this.mainAnchor!) } // mount into target container else { - if (isTeleportDeferred(props)) { + if (isTeleportDeferred(this.props!)) { queuePostFlushCb(mountToTarget) } else { mountToTarget() @@ -183,6 +174,17 @@ export class TeleportFragment extends VaporFragment { } } + insert = (container: ParentNode, anchor: Node | null): void => { + // insert anchors in the main view + this.placeholder = __DEV__ + ? createComment('teleport start') + : createTextNode() + this.mainAnchor = __DEV__ ? createComment('teleport end') : createTextNode() + insert(this.placeholder, container, anchor) + insert(this.mainAnchor, container, anchor) + this.update() + } + remove = (parent: ParentNode | undefined = this.parent!): void => { // remove nodes if (this.nodes) { diff --git a/packages/runtime-vapor/src/fragment.ts b/packages/runtime-vapor/src/fragment.ts index 0b1bc3a3633..a34f7474ebb 100644 --- a/packages/runtime-vapor/src/fragment.ts +++ b/packages/runtime-vapor/src/fragment.ts @@ -4,8 +4,6 @@ import { type Block, type BlockFn, insert, isValidBlock, remove } from './block' export class VaporFragment { nodes: Block - target?: ParentNode | null - targetAnchor?: Node | null anchor?: Node insert?: (parent: ParentNode, anchor: Node | null) => void remove?: (parent?: ParentNode) => void From cec32be878ad2b240348a1acd1bebb92255f7d92 Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 10 Sep 2025 16:11:08 +0800 Subject: [PATCH 34/40] chore: remove teleport e2e tests --- .../vapor-e2e-test/__tests__/teleport.spec.ts | 62 ------------------- .../vapor-e2e-test/__tests__/todomvc.spec.ts | 3 +- .../__tests__/vdomInterop.spec.ts | 45 ++------------ packages-private/vapor-e2e-test/index.html | 1 - .../vapor-e2e-test/interop/App.vue | 16 +---- .../interop/{components => }/VaporComp.vue | 13 ++-- .../interop/{components => }/VdomComp.vue | 0 .../interop/components/SimpleVaporComp.vue | 6 -- .../vapor-e2e-test/teleport/App.vue | 17 ----- .../teleport/components/VdomComp.vue | 7 --- .../vapor-e2e-test/teleport/index.html | 2 - .../vapor-e2e-test/teleport/main.ts | 5 -- packages-private/vapor-e2e-test/utils.ts | 6 -- .../vapor-e2e-test/vite.config.ts | 1 - 14 files changed, 12 insertions(+), 172 deletions(-) delete mode 100644 packages-private/vapor-e2e-test/__tests__/teleport.spec.ts rename packages-private/vapor-e2e-test/interop/{components => }/VaporComp.vue (80%) rename packages-private/vapor-e2e-test/interop/{components => }/VdomComp.vue (100%) delete mode 100644 packages-private/vapor-e2e-test/interop/components/SimpleVaporComp.vue delete mode 100644 packages-private/vapor-e2e-test/teleport/App.vue delete mode 100644 packages-private/vapor-e2e-test/teleport/components/VdomComp.vue delete mode 100644 packages-private/vapor-e2e-test/teleport/index.html delete mode 100644 packages-private/vapor-e2e-test/teleport/main.ts delete mode 100644 packages-private/vapor-e2e-test/utils.ts diff --git a/packages-private/vapor-e2e-test/__tests__/teleport.spec.ts b/packages-private/vapor-e2e-test/__tests__/teleport.spec.ts deleted file mode 100644 index ce383dadf1c..00000000000 --- a/packages-private/vapor-e2e-test/__tests__/teleport.spec.ts +++ /dev/null @@ -1,62 +0,0 @@ -import path from 'node:path' -import { - E2E_TIMEOUT, - setupPuppeteer, -} from '../../../packages/vue/__tests__/e2e/e2eUtils' -import connect from 'connect' -import sirv from 'sirv' -import { nextTick } from 'vue' -import { ports } from '../utils' -const { page, click, html } = setupPuppeteer() - -describe('vapor teleport', () => { - let server: any - const port = ports.teleport - beforeAll(() => { - server = connect() - .use(sirv(path.resolve(import.meta.dirname, '../dist'))) - .listen(port) - process.on('SIGTERM', () => server && server.close()) - }) - - afterAll(() => { - server.close() - }) - - beforeEach(async () => { - const baseUrl = `http://localhost:${port}/teleport/` - await page().goto(baseUrl) - await page().waitForSelector('#app') - }) - - test( - 'render vdom component', - async () => { - const targetSelector = '.target' - const testSelector = '.interop-render-vdom-comp' - const containerSelector = `${testSelector} > div` - const btnSelector = `${testSelector} > button` - - const tt = await html('#app') - console.log(tt) - - // teleport is disabled - expect(await html(containerSelector)).toBe('

vdom comp

') - expect(await html(targetSelector)).toBe('') - - // enable teleport - await click(btnSelector) - await nextTick() - - expect(await html(containerSelector)).toBe('') - expect(await html(targetSelector)).toBe('

vdom comp

') - - // disable teleport - await click(btnSelector) - await nextTick() - expect(await html(containerSelector)).toBe('

vdom comp

') - expect(await html(targetSelector)).toBe('') - }, - E2E_TIMEOUT, - ) -}) diff --git a/packages-private/vapor-e2e-test/__tests__/todomvc.spec.ts b/packages-private/vapor-e2e-test/__tests__/todomvc.spec.ts index 035691fd69b..3de8392e5e2 100644 --- a/packages-private/vapor-e2e-test/__tests__/todomvc.spec.ts +++ b/packages-private/vapor-e2e-test/__tests__/todomvc.spec.ts @@ -5,7 +5,6 @@ import { } from '../../../packages/vue/__tests__/e2e/e2eUtils' import connect from 'connect' import sirv from 'sirv' -import { ports } from '../utils' describe('e2e: todomvc', () => { const { @@ -24,7 +23,7 @@ describe('e2e: todomvc', () => { } = setupPuppeteer() let server: any - const port = ports.todomvc + const port = '8194' beforeAll(() => { server = connect() .use(sirv(path.resolve(import.meta.dirname, '../dist'))) diff --git a/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts b/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts index 734c9fde190..360f48085a1 100644 --- a/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts +++ b/packages-private/vapor-e2e-test/__tests__/vdomInterop.spec.ts @@ -5,13 +5,12 @@ import { } from '../../../packages/vue/__tests__/e2e/e2eUtils' import connect from 'connect' import sirv from 'sirv' -import { ports } from '../utils' -import { nextTick } from 'vue' -const { page, click, text, enterValue, html } = setupPuppeteer() describe('vdom / vapor interop', () => { + const { page, click, text, enterValue } = setupPuppeteer() + let server: any - const port = ports.vdomInterop + const port = '8193' beforeAll(() => { server = connect() .use(sirv(path.resolve(import.meta.dirname, '../dist'))) @@ -19,12 +18,6 @@ describe('vdom / vapor interop', () => { process.on('SIGTERM', () => server && server.close()) }) - beforeEach(async () => { - const baseUrl = `http://localhost:${port}/interop/` - await page().goto(baseUrl) - await page().waitForSelector('#app') - }) - afterAll(() => { server.close() }) @@ -32,6 +25,9 @@ describe('vdom / vapor interop', () => { 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') @@ -85,33 +81,4 @@ 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('') - }) - }) }) diff --git a/packages-private/vapor-e2e-test/index.html b/packages-private/vapor-e2e-test/index.html index bb1234e8e10..7dc205e5ab0 100644 --- a/packages-private/vapor-e2e-test/index.html +++ b/packages-private/vapor-e2e-test/index.html @@ -1,3 +1,2 @@ VDOM / Vapor interop Vapor TodoMVC -Vapor Teleport diff --git a/packages-private/vapor-e2e-test/interop/App.vue b/packages-private/vapor-e2e-test/interop/App.vue index dcdd5f99ace..772a6989dd7 100644 --- a/packages-private/vapor-e2e-test/interop/App.vue +++ b/packages-private/vapor-e2e-test/interop/App.vue @@ -1,11 +1,9 @@ diff --git a/packages-private/vapor-e2e-test/interop/components/VaporComp.vue b/packages-private/vapor-e2e-test/interop/VaporComp.vue similarity index 80% rename from packages-private/vapor-e2e-test/interop/components/VaporComp.vue rename to packages-private/vapor-e2e-test/interop/VaporComp.vue index 88a60c782c0..4ebb58b9ce7 100644 --- a/packages-private/vapor-e2e-test/interop/components/VaporComp.vue +++ b/packages-private/vapor-e2e-test/interop/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/components/VdomComp.vue b/packages-private/vapor-e2e-test/interop/VdomComp.vue similarity index 100% rename from packages-private/vapor-e2e-test/interop/components/VdomComp.vue rename to packages-private/vapor-e2e-test/interop/VdomComp.vue diff --git a/packages-private/vapor-e2e-test/interop/components/SimpleVaporComp.vue b/packages-private/vapor-e2e-test/interop/components/SimpleVaporComp.vue deleted file mode 100644 index 65661740cd9..00000000000 --- a/packages-private/vapor-e2e-test/interop/components/SimpleVaporComp.vue +++ /dev/null @@ -1,6 +0,0 @@ - - diff --git a/packages-private/vapor-e2e-test/teleport/App.vue b/packages-private/vapor-e2e-test/teleport/App.vue deleted file mode 100644 index d2aeba8e1f9..00000000000 --- a/packages-private/vapor-e2e-test/teleport/App.vue +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/packages-private/vapor-e2e-test/teleport/components/VdomComp.vue b/packages-private/vapor-e2e-test/teleport/components/VdomComp.vue deleted file mode 100644 index 2c7a626f21a..00000000000 --- a/packages-private/vapor-e2e-test/teleport/components/VdomComp.vue +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/packages-private/vapor-e2e-test/teleport/index.html b/packages-private/vapor-e2e-test/teleport/index.html deleted file mode 100644 index 79052a023ba..00000000000 --- a/packages-private/vapor-e2e-test/teleport/index.html +++ /dev/null @@ -1,2 +0,0 @@ - -
diff --git a/packages-private/vapor-e2e-test/teleport/main.ts b/packages-private/vapor-e2e-test/teleport/main.ts deleted file mode 100644 index 2e962efe731..00000000000 --- a/packages-private/vapor-e2e-test/teleport/main.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { createVaporApp, vaporInteropPlugin } from 'vue' -import App from './App.vue' -import 'todomvc-app-css/index.css' - -createVaporApp(App).use(vaporInteropPlugin).mount('#app') diff --git a/packages-private/vapor-e2e-test/utils.ts b/packages-private/vapor-e2e-test/utils.ts deleted file mode 100644 index a42064b7050..00000000000 --- a/packages-private/vapor-e2e-test/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -// make sure these ports are unique -export const ports = { - vdomInterop: 8193, - todomvc: 8194, - teleport: 8195, -} diff --git a/packages-private/vapor-e2e-test/vite.config.ts b/packages-private/vapor-e2e-test/vite.config.ts index a2816f4b6db..1e29a4dbd13 100644 --- a/packages-private/vapor-e2e-test/vite.config.ts +++ b/packages-private/vapor-e2e-test/vite.config.ts @@ -14,7 +14,6 @@ export default defineConfig({ input: { interop: resolve(import.meta.dirname, 'interop/index.html'), todomvc: resolve(import.meta.dirname, 'todomvc/index.html'), - teleport: resolve(import.meta.dirname, 'teleport/index.html'), }, }, }, From 5443a2440b0894aaf4cfba0c881b208bc4806932 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 10 Sep 2025 08:12:16 +0000 Subject: [PATCH 35/40] [autofix.ci] apply automated fixes --- packages-private/vapor-e2e-test/interop/VaporComp.vue | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages-private/vapor-e2e-test/interop/VaporComp.vue b/packages-private/vapor-e2e-test/interop/VaporComp.vue index 4ebb58b9ce7..09b08154ae3 100644 --- a/packages-private/vapor-e2e-test/interop/VaporComp.vue +++ b/packages-private/vapor-e2e-test/interop/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')
- From dd189d4e9e4e6520785d4ad24d381a6ba4ddfe46 Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 11 Sep 2025 14:34:35 +0800 Subject: [PATCH 36/40] wip: refactor --- packages/reactivity/src/effectScope.ts | 7 ++ packages/runtime-vapor/src/block.ts | 9 ++- packages/runtime-vapor/src/component.ts | 4 +- .../runtime-vapor/src/components/Teleport.ts | 53 ++------------- packages/runtime-vapor/src/hmr.ts | 66 +++++++++++++++++-- packages/runtime-vapor/src/renderEffect.ts | 7 +- 6 files changed, 82 insertions(+), 64 deletions(-) diff --git a/packages/reactivity/src/effectScope.ts b/packages/reactivity/src/effectScope.ts index 36c9b85e8d7..efe9b0fd915 100644 --- a/packages/reactivity/src/effectScope.ts +++ b/packages/reactivity/src/effectScope.ts @@ -87,6 +87,13 @@ export class EffectScope implements ReactiveNode { if (sub !== undefined) { unlink(sub) } + this.cleanup() + } + + /** + * @internal + */ + cleanup(): void { cleanup(this) } } diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index f6ebb756b95..9bc39a84284 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -64,13 +64,16 @@ export function insert( insert(b, parent, anchor) } } else { - if (block.anchor) insert(block.anchor, parent, anchor) + if (block.anchor) { + insert(block.anchor, parent, anchor) + anchor = block.anchor + } // fragment if (block.insert) { // TODO handle hydration for vdom interop - block.insert(parent, block.anchor || anchor) + block.insert(parent, anchor) } else { - insert(block.nodes, parent, block.anchor || anchor) + insert(block.nodes, parent, anchor) } } } diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 85c13da336a..e212109fa33 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -58,7 +58,7 @@ import { } from './componentSlots' import { hmrReload, hmrRerender } from './hmr' import { isHydrating, locateHydrationNode } from './dom/hydration' -import { isVaporTeleport } from './components/Teleport' +import { type TeleportFragment, isVaporTeleport } from './components/Teleport' import { insertionAnchor, insertionParent, @@ -405,8 +405,8 @@ export class VaporComponentInstance implements GenericComponentInstance { setupState?: Record devtoolsRawSetupState?: any hmrRerender?: () => void - hmrRerenderEffects?: (() => void)[] hmrReload?: (newComp: VaporComponent) => void + parentTeleport?: TeleportFragment | null propsOptions?: NormalizedPropsOptions emitsOptions?: ObjectEmitsOptions | null isSingleRoot?: boolean diff --git a/packages/runtime-vapor/src/components/Teleport.ts b/packages/runtime-vapor/src/components/Teleport.ts index 29662f50593..7ba6ff12b98 100644 --- a/packages/runtime-vapor/src/components/Teleport.ts +++ b/packages/runtime-vapor/src/components/Teleport.ts @@ -1,8 +1,8 @@ import { type TeleportProps, - currentInstance, isTeleportDeferred, isTeleportDisabled, + onScopeDispose, queuePostFlushCb, resolveTeleportTarget, warn, @@ -12,7 +12,6 @@ import { createComment, createTextNode, querySelector } from '../dom/node' import { type LooseRawProps, type LooseRawSlots, - type VaporComponentInstance, isVaporComponent, } from '../component' import { rawPropsProxyHandlers } from '../componentProps' @@ -20,9 +19,6 @@ import { renderEffect } from '../renderEffect' import { extend, isArray } from '@vue/shared' import { VaporFragment } from '../fragment' -const instanceToTeleportMap: WeakMap = - __DEV__ ? new WeakMap() : (undefined as any) - export const VaporTeleportImpl = { name: 'VaporTeleport', __isTeleport: true, @@ -30,11 +26,11 @@ export const VaporTeleportImpl = { process(props: LooseRawProps, slots: LooseRawSlots): TeleportFragment { const frag = new TeleportFragment() - const updateChildrenEffect = renderEffect(() => + renderEffect(() => frag.updateChildren(slots.default && (slots.default as BlockFn)()), ) - const updateEffect = renderEffect(() => { + renderEffect(() => { // access the props to trigger tracking frag.props = extend( {}, @@ -43,6 +39,8 @@ export const VaporTeleportImpl = { frag.update() }) + onScopeDispose(frag.remove) + if (__DEV__) { // used in `normalizeBlock` to get nodes of TeleportFragment during // HMR updates. returns empty array if content is mounted in target @@ -51,27 +49,12 @@ export const VaporTeleportImpl = { return frag.parent !== frag.currentParent ? [] : frag.nodes } - // for HMR rerender - const instance = currentInstance as VaporComponentInstance - ;( - instance!.hmrRerenderEffects || (instance!.hmrRerenderEffects = []) - ).push(() => { - // remove the teleport content - frag.remove() - - // stop effects - updateChildrenEffect.stop() - updateEffect.stop() - }) - - // for HMR reload const nodes = frag.nodes if (isVaporComponent(nodes)) { - instanceToTeleportMap.set(nodes, frag) + nodes.parentTeleport = frag } else if (isArray(nodes)) { nodes.forEach( - node => - isVaporComponent(node) && instanceToTeleportMap.set(node, frag), + node => isVaporComponent(node) && (node.parentTeleport = frag), ) } } @@ -221,25 +204,3 @@ export function isVaporTeleport( ): value is typeof VaporTeleportImpl { return value === VaporTeleportImpl } - -/** - * dev only - * during root component HMR reload, since the old component will be unmounted - * and a new one will be mounted, we need to update the teleport's nodes - * to ensure they are up to date. - */ -export function handleTeleportRootComponentHmrReload( - instance: VaporComponentInstance, - newInstance: VaporComponentInstance, -): void { - const teleport = instanceToTeleportMap.get(instance) - if (teleport) { - instanceToTeleportMap.set(newInstance, teleport) - if (teleport.nodes === instance) { - teleport.nodes = newInstance - } else if (isArray(teleport.nodes)) { - const i = teleport.nodes.indexOf(instance) - if (i !== -1) teleport.nodes[i] = newInstance - } - } -} diff --git a/packages/runtime-vapor/src/hmr.ts b/packages/runtime-vapor/src/hmr.ts index 17b1bd0f237..3b96b114978 100644 --- a/packages/runtime-vapor/src/hmr.ts +++ b/packages/runtime-vapor/src/hmr.ts @@ -12,17 +12,14 @@ import { mountComponent, unmountComponent, } from './component' -import { handleTeleportRootComponentHmrReload } from './components/Teleport' +import { isArray } from '@vue/shared' export function hmrRerender(instance: VaporComponentInstance): void { const normalized = normalizeBlock(instance.block) const parent = normalized[0].parentNode! const anchor = normalized[normalized.length - 1].nextSibling remove(instance.block, parent) - if (instance.hmrRerenderEffects) { - instance.hmrRerenderEffects.forEach(e => e()) - instance.hmrRerenderEffects.length = 0 - } + instance.scope.cleanup() const prev = setCurrentInstance(instance) pushWarningContext(instance) devRender(instance) @@ -39,7 +36,8 @@ export function hmrReload( const parent = normalized[0].parentNode! const anchor = normalized[normalized.length - 1].nextSibling unmountComponent(instance, parent) - const prev = setCurrentInstance(instance.parent) + const parentInstance = instance.parent as VaporComponentInstance | null + const prev = setCurrentInstance(parentInstance) const newInstance = createComponent( newComp, instance.rawProps, @@ -48,5 +46,59 @@ export function hmrReload( ) setCurrentInstance(...prev) mountComponent(newInstance, parent, anchor) - handleTeleportRootComponentHmrReload(instance, newInstance) + + updateParentBlockOnHmrReload(parentInstance, instance, newInstance) + updateParentTeleportOnHmrReload(instance, newInstance) +} + +/** + * dev only + * update parentInstance.block to ensure that the correct parent and + * anchor are found during parentInstance HMR rerender/reload, as + * `normalizeBlock` relies on the current instance.block + */ +function updateParentBlockOnHmrReload( + parentInstance: VaporComponentInstance | null, + instance: VaporComponentInstance, + newInstance: VaporComponentInstance, +): void { + if (parentInstance) { + if (parentInstance.block === instance) { + parentInstance.block = newInstance + } else if (isArray(parentInstance.block)) { + for (let i = 0; i < parentInstance.block.length; i++) { + if (parentInstance.block[i] === instance) { + parentInstance.block[i] = newInstance + break + } + } + } + } +} + +/** + * dev only + * during root component HMR reload, since the old component will be unmounted + * and a new one will be mounted, we need to update the teleport's nodes + * to ensure that the correct parent and anchor are found during parentInstance + * HMR rerender/reload, as `normalizeBlock` relies on the current instance.block + */ +export function updateParentTeleportOnHmrReload( + instance: VaporComponentInstance, + newInstance: VaporComponentInstance, +): void { + const teleport = instance.parentTeleport + if (teleport) { + newInstance.parentTeleport = teleport + if (teleport.nodes === instance) { + teleport.nodes = newInstance + } else if (isArray(teleport.nodes)) { + for (let i = 0; i < teleport.nodes.length; i++) { + if (teleport.nodes[i] === instance) { + teleport.nodes[i] = newInstance + break + } + } + } + } } diff --git a/packages/runtime-vapor/src/renderEffect.ts b/packages/runtime-vapor/src/renderEffect.ts index 6da69ee2cf1..ac34e8863d2 100644 --- a/packages/runtime-vapor/src/renderEffect.ts +++ b/packages/runtime-vapor/src/renderEffect.ts @@ -82,15 +82,10 @@ class RenderEffect extends ReactiveEffect { } } -export function renderEffect( - fn: () => void, - noLifecycle = false, -): RenderEffect { +export function renderEffect(fn: () => void, noLifecycle = false): void { const effect = new RenderEffect(fn) if (noLifecycle) { effect.fn = fn } effect.run() - - return effect } From 96088ed5c36f5c9e1afdd4ead2dba21e5fc2cdd5 Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 11 Sep 2025 16:51:19 +0800 Subject: [PATCH 37/40] chore: update --- packages/runtime-vapor/src/components/Teleport.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/runtime-vapor/src/components/Teleport.ts b/packages/runtime-vapor/src/components/Teleport.ts index 7ba6ff12b98..9c1a2ea2d0f 100644 --- a/packages/runtime-vapor/src/components/Teleport.ts +++ b/packages/runtime-vapor/src/components/Teleport.ts @@ -39,9 +39,8 @@ export const VaporTeleportImpl = { frag.update() }) - onScopeDispose(frag.remove) - if (__DEV__) { + onScopeDispose(frag.remove) // used in `normalizeBlock` to get nodes of TeleportFragment during // HMR updates. returns empty array if content is mounted in target // container to prevent incorrect parent node lookup. From 0dc8963e0bc9420231adcdd767316a1747ad2c95 Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 12 Sep 2025 21:16:36 +0800 Subject: [PATCH 38/40] chore: update --- packages/reactivity/src/effectScope.ts | 7 ------- .../runtime-vapor/__tests__/components/Teleport.spec.ts | 2 +- packages/runtime-vapor/src/hmr.ts | 1 - 3 files changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/reactivity/src/effectScope.ts b/packages/reactivity/src/effectScope.ts index efe9b0fd915..36c9b85e8d7 100644 --- a/packages/reactivity/src/effectScope.ts +++ b/packages/reactivity/src/effectScope.ts @@ -87,13 +87,6 @@ export class EffectScope implements ReactiveNode { if (sub !== undefined) { unlink(sub) } - this.cleanup() - } - - /** - * @internal - */ - cleanup(): void { cleanup(this) } } diff --git a/packages/runtime-vapor/__tests__/components/Teleport.spec.ts b/packages/runtime-vapor/__tests__/components/Teleport.spec.ts index d4282ab8a6e..45a2e5858c0 100644 --- a/packages/runtime-vapor/__tests__/components/Teleport.spec.ts +++ b/packages/runtime-vapor/__tests__/components/Teleport.spec.ts @@ -195,7 +195,7 @@ describe('renderer: VaporTeleport', () => { expect(target.innerHTML).toBe('
teleported 2
') }) - test('parent rerender + toggle disabled', async () => { + test.todo('parent rerender + toggle disabled', async () => { const target = document.createElement('div') const root = document.createElement('div') const parentId = 'test3-parent-rerender' diff --git a/packages/runtime-vapor/src/hmr.ts b/packages/runtime-vapor/src/hmr.ts index 3b96b114978..8c5b3715eda 100644 --- a/packages/runtime-vapor/src/hmr.ts +++ b/packages/runtime-vapor/src/hmr.ts @@ -19,7 +19,6 @@ export function hmrRerender(instance: VaporComponentInstance): void { const parent = normalized[0].parentNode! const anchor = normalized[normalized.length - 1].nextSibling remove(instance.block, parent) - instance.scope.cleanup() const prev = setCurrentInstance(instance) pushWarningContext(instance) devRender(instance) From e5e6ee580b35dfd7defc85fd18d8b8c9f0cefd57 Mon Sep 17 00:00:00 2001 From: daiwei Date: Mon, 15 Sep 2025 16:20:43 +0800 Subject: [PATCH 39/40] test: add hydration benchmark for Vapor and VDOM --- .../__tests__/hydration.bench.ts | 225 ++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 packages/runtime-vapor/__tests__/hydration.bench.ts diff --git a/packages/runtime-vapor/__tests__/hydration.bench.ts b/packages/runtime-vapor/__tests__/hydration.bench.ts new file mode 100644 index 00000000000..56cd1ccf3a3 --- /dev/null +++ b/packages/runtime-vapor/__tests__/hydration.bench.ts @@ -0,0 +1,225 @@ +/** + * @vitest-environment jsdom + */ +import { bench, describe } from 'vitest' +import { + ssrInterpolate as _ssrInterpolate, + ssrRenderAttrs as _ssrRenderAttrs, +} from '@vue/server-renderer' +import { + Fragment as _Fragment, + createBlock as _createBlock, + createCommentVNode as _createCommentVNode, + createElementBlock as _createElementBlock, + createElementVNode as _createElementVNode, + createVNode as _createVNode, + openBlock as _openBlock, + renderList as _renderList, + renderSlot as _renderSlot, + toDisplayString as _toDisplayString, + vModelText as _vModelText, + withCtx as _withCtx, + withDirectives as _withDirectives, + createSSRApp, + ref, +} from '@vue/runtime-dom' +import { + applyTextModel as _applyTextModel, + child as _child, + createComponent as _createComponent, + createFor as _createFor, + createIf as _createIf, + createSlot as _createSlot, + next as _next, + renderEffect as _renderEffect, + setInsertionState as _setInsertionState, + setText as _setText, + template as _template, + createVaporSSRApp, +} from '@vue/runtime-vapor' + +describe('hydration benchmark', () => { + { + let html = `

Hello World!

1
2
3
4
5
` + + const t0 = _template('

') + const t1 = _template('
') + const t2 = _template('
', true) + + const VaporChild = { + __vapor: true, + setup() { + const msg = ref('Hello World!') + const show = ref(true) + + const n9 = t2() as any + const n0 = _child(n9) as any + const n8 = _next(n0) as any + const n7 = _next(n8) as any + _setInsertionState(n9, n8) + _createIf( + () => show.value, + () => { + const n3 = t0() as any + _applyTextModel( + n3, + () => msg.value, + _value => (msg.value = _value), + ) + return n3 + }, + ) + _setInsertionState(n7) + _createFor( + () => 5, + _for_item0 => { + const n6 = t1() as any + const x6 = _child(n6) as any + _renderEffect(() => + _setText(x6, _toDisplayString(_for_item0.value)), + ) + return n6 + }, + undefined, + 5, + ) + const x0 = _child(n0) as any + _renderEffect(() => _setText(x0, _toDisplayString(msg.value))) + return n9 + }, + } + + const tt0 = _template(' ') + const tt1 = _template('
', true) + + const VaporApp = { + __vapor: true, + setup() { + const msg = ref('hi') + const n2 = tt1() as any + _setInsertionState(n2) + _createComponent(VaporChild, null, { + default: () => { + const n0 = tt0() as any + const x0 = _child(n0) as any + _renderEffect(() => _setText(x0, _toDisplayString(msg.value))) + return n0 + }, + }) + return n2 + }, + } + + const VdomChild = { + setup() { + const msg = ref('Hello World!') + const show = ref(true) + + // @ts-expect-error + return (_ctx, _cache) => { + return ( + _openBlock(), + _createElementBlock('div', null, [ + _createElementVNode( + 'h1', + null, + _toDisplayString(msg.value), + 1 /* TEXT */, + ), + show.value + ? _withDirectives( + (_openBlock(), + _createElementBlock( + 'input', + { + key: 0, + 'onUpdate:modelValue': + _cache[0] || + (_cache[0] = ($event: any) => (msg.value = $event)), + }, + null, + 512 /* NEED_PATCH */, + )), + [[_vModelText, msg.value]], + ) + : _createCommentVNode('v-if', true), + _createElementVNode('div', null, [ + (_openBlock(), + _createElementBlock( + _Fragment, + null, + _renderList(5, item => { + return _createElementVNode( + 'div', + null, + _toDisplayString(item), + 1 /* TEXT */, + ) + }), + 64 /* STABLE_FRAGMENT */, + )), + ]), + _cache[1] || + (_cache[1] = _createElementVNode( + 'span', + null, + null, + -1 /* CACHED */, + )), + ]) + ) + } + }, + } + + const VdomApp = { + setup() { + const msg = ref('hi') + return () => { + return ( + _openBlock(), + _createElementBlock('div', null, [ + _createVNode(VdomChild, null, { + default: _withCtx(() => [ + _createElementVNode( + 'span', + null, + _toDisplayString(msg.value), + 1 /* TEXT */, + ), + ]), + _: 1 /* STABLE */, + }), + ]) + ) + } + }, + } + + bench('vapor', () => { + try { + __DEV__ = false + const container = document.createElement('div') + container.innerHTML = html + + const app = createVaporSSRApp(VaporApp) + app.mount(container) + } finally { + __DEV__ = true + } + }) + + bench('vdom', () => { + try { + __DEV__ = false + const container = document.createElement('div') + container.innerHTML = html + + const app = createSSRApp(VdomApp) + app.mount(container) + } finally { + __DEV__ = true + } + }) + } +}) From e69259a471feebc703254d93da6a4a6ebf1ddada Mon Sep 17 00:00:00 2001 From: daiwei Date: Mon, 15 Sep 2025 20:32:24 +0800 Subject: [PATCH 40/40] chore: remove hydration bench --- .../__tests__/hydration.bench.ts | 225 ------------------ 1 file changed, 225 deletions(-) delete mode 100644 packages/runtime-vapor/__tests__/hydration.bench.ts diff --git a/packages/runtime-vapor/__tests__/hydration.bench.ts b/packages/runtime-vapor/__tests__/hydration.bench.ts deleted file mode 100644 index 56cd1ccf3a3..00000000000 --- a/packages/runtime-vapor/__tests__/hydration.bench.ts +++ /dev/null @@ -1,225 +0,0 @@ -/** - * @vitest-environment jsdom - */ -import { bench, describe } from 'vitest' -import { - ssrInterpolate as _ssrInterpolate, - ssrRenderAttrs as _ssrRenderAttrs, -} from '@vue/server-renderer' -import { - Fragment as _Fragment, - createBlock as _createBlock, - createCommentVNode as _createCommentVNode, - createElementBlock as _createElementBlock, - createElementVNode as _createElementVNode, - createVNode as _createVNode, - openBlock as _openBlock, - renderList as _renderList, - renderSlot as _renderSlot, - toDisplayString as _toDisplayString, - vModelText as _vModelText, - withCtx as _withCtx, - withDirectives as _withDirectives, - createSSRApp, - ref, -} from '@vue/runtime-dom' -import { - applyTextModel as _applyTextModel, - child as _child, - createComponent as _createComponent, - createFor as _createFor, - createIf as _createIf, - createSlot as _createSlot, - next as _next, - renderEffect as _renderEffect, - setInsertionState as _setInsertionState, - setText as _setText, - template as _template, - createVaporSSRApp, -} from '@vue/runtime-vapor' - -describe('hydration benchmark', () => { - { - let html = `

Hello World!

1
2
3
4
5
` - - const t0 = _template('

') - const t1 = _template('
') - const t2 = _template('
', true) - - const VaporChild = { - __vapor: true, - setup() { - const msg = ref('Hello World!') - const show = ref(true) - - const n9 = t2() as any - const n0 = _child(n9) as any - const n8 = _next(n0) as any - const n7 = _next(n8) as any - _setInsertionState(n9, n8) - _createIf( - () => show.value, - () => { - const n3 = t0() as any - _applyTextModel( - n3, - () => msg.value, - _value => (msg.value = _value), - ) - return n3 - }, - ) - _setInsertionState(n7) - _createFor( - () => 5, - _for_item0 => { - const n6 = t1() as any - const x6 = _child(n6) as any - _renderEffect(() => - _setText(x6, _toDisplayString(_for_item0.value)), - ) - return n6 - }, - undefined, - 5, - ) - const x0 = _child(n0) as any - _renderEffect(() => _setText(x0, _toDisplayString(msg.value))) - return n9 - }, - } - - const tt0 = _template(' ') - const tt1 = _template('
', true) - - const VaporApp = { - __vapor: true, - setup() { - const msg = ref('hi') - const n2 = tt1() as any - _setInsertionState(n2) - _createComponent(VaporChild, null, { - default: () => { - const n0 = tt0() as any - const x0 = _child(n0) as any - _renderEffect(() => _setText(x0, _toDisplayString(msg.value))) - return n0 - }, - }) - return n2 - }, - } - - const VdomChild = { - setup() { - const msg = ref('Hello World!') - const show = ref(true) - - // @ts-expect-error - return (_ctx, _cache) => { - return ( - _openBlock(), - _createElementBlock('div', null, [ - _createElementVNode( - 'h1', - null, - _toDisplayString(msg.value), - 1 /* TEXT */, - ), - show.value - ? _withDirectives( - (_openBlock(), - _createElementBlock( - 'input', - { - key: 0, - 'onUpdate:modelValue': - _cache[0] || - (_cache[0] = ($event: any) => (msg.value = $event)), - }, - null, - 512 /* NEED_PATCH */, - )), - [[_vModelText, msg.value]], - ) - : _createCommentVNode('v-if', true), - _createElementVNode('div', null, [ - (_openBlock(), - _createElementBlock( - _Fragment, - null, - _renderList(5, item => { - return _createElementVNode( - 'div', - null, - _toDisplayString(item), - 1 /* TEXT */, - ) - }), - 64 /* STABLE_FRAGMENT */, - )), - ]), - _cache[1] || - (_cache[1] = _createElementVNode( - 'span', - null, - null, - -1 /* CACHED */, - )), - ]) - ) - } - }, - } - - const VdomApp = { - setup() { - const msg = ref('hi') - return () => { - return ( - _openBlock(), - _createElementBlock('div', null, [ - _createVNode(VdomChild, null, { - default: _withCtx(() => [ - _createElementVNode( - 'span', - null, - _toDisplayString(msg.value), - 1 /* TEXT */, - ), - ]), - _: 1 /* STABLE */, - }), - ]) - ) - } - }, - } - - bench('vapor', () => { - try { - __DEV__ = false - const container = document.createElement('div') - container.innerHTML = html - - const app = createVaporSSRApp(VaporApp) - app.mount(container) - } finally { - __DEV__ = true - } - }) - - bench('vdom', () => { - try { - __DEV__ = false - const container = document.createElement('div') - container.innerHTML = html - - const app = createSSRApp(VdomApp) - app.mount(container) - } finally { - __DEV__ = true - } - }) - } -})