From 36c19b5a2b6b9ee2313b9f8c5fcccbca4aa1b8cd Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 18 Nov 2025 16:03:32 +0800 Subject: [PATCH 01/11] test(hmr): port hmr tests --- packages/runtime-vapor/__tests__/hmr.spec.ts | 208 ++++++++++++++++++- 1 file changed, 206 insertions(+), 2 deletions(-) diff --git a/packages/runtime-vapor/__tests__/hmr.spec.ts b/packages/runtime-vapor/__tests__/hmr.spec.ts index 21ca611263e..139f982afc1 100644 --- a/packages/runtime-vapor/__tests__/hmr.spec.ts +++ b/packages/runtime-vapor/__tests__/hmr.spec.ts @@ -1,21 +1,225 @@ // TODO: port tests from packages/runtime-core/__tests__/hmr.spec.ts -import { type HMRRuntime, ref } from '@vue/runtime-dom' +import { + type HMRRuntime, + nextTick, + ref, + toDisplayString, +} from '@vue/runtime-dom' import { makeRender } from './_utils' import { child, createComponent, + createComponentWithFallback, + createInvoker, + createSlot, + defineVaporComponent, + delegateEvents, + next, renderEffect, + setInsertionState, setText, template, + txt, + withVaporCtx, } from '@vue/runtime-vapor' declare var __VUE_HMR_RUNTIME__: HMRRuntime -const { createRecord, reload } = __VUE_HMR_RUNTIME__ +const { createRecord, rerender, reload } = __VUE_HMR_RUNTIME__ const define = makeRender() +const triggerEvent = (type: string, el: Element) => { + const event = new Event(type, { bubbles: true }) + el.dispatchEvent(event) +} +delegateEvents('click') + +beforeEach(() => { + document.body.innerHTML = '' +}) + describe('hot module replacement', () => { + test('inject global runtime', () => { + expect(createRecord).toBeDefined() + expect(rerender).toBeDefined() + expect(reload).toBeDefined() + }) + + test('createRecord', () => { + expect(createRecord('test1', {})).toBe(true) + // if id has already been created, should return false + expect(createRecord('test1', {})).toBe(false) + }) + + test('rerender', async () => { + const root = document.createElement('div') + const parentId = 'test2-parent' + const childId = 'test2-child' + document.body.appendChild(root) + + const Child = defineVaporComponent({ + __hmrId: childId, + render() { + const n1 = template('
', true)() as any + setInsertionState(n1, null, true) + createSlot('default', null) + return n1 + }, + }) + createRecord(childId, Child as any) + + const Parent = defineVaporComponent({ + __hmrId: parentId, + setup() { + const count = ref(0) + return { count } + }, + render(ctx) { + const n3 = template('
', true)() as any + const n0 = child(n3) as any + setInsertionState(n3, 1, true) + createComponent(Child, null, { + default: withVaporCtx(() => { + const n1 = template(' ')() as any + renderEffect(() => setText(n1, toDisplayString(ctx.count))) + return n1 + }), + }) + n3.$evtclick = createInvoker(() => ctx.count++) + renderEffect(() => setText(n0, toDisplayString(ctx.count))) + return n3 + }, + }) + createRecord(parentId, Parent as any) + + // render(h(Parent), root) + const { mount } = define(Parent).create() + mount(root) + expect(root.innerHTML).toBe(`
0
0
`) + + // Perform some state change. This change should be preserved after the + // re-render! + // triggerEvent(root.children[0] as TestElement, 'click') + triggerEvent('click', root.children[0]) + await nextTick() + expect(root.innerHTML).toBe(`
1
1
`) + + // Update text while preserving state + rerender(parentId, (ctx: any) => { + const n3 = template('
', true)() as any + const n0 = child(n3) as any + setInsertionState(n3, 1, true) + createComponent(Child, null, { + default: withVaporCtx(() => { + const n1 = template(' ')() as any + renderEffect(() => setText(n1, toDisplayString(ctx.count))) + return n1 + }), + }) + n3.$evtclick = createInvoker(() => ctx.count++) + renderEffect(() => setText(n0, toDisplayString(ctx.count) + '!')) + return n3 + }) + expect(root.innerHTML).toBe(`
1!
1
`) + + // Should force child update on slot content change + rerender(parentId, (ctx: any) => { + const n3 = template('
', true)() as any + const n0 = child(n3) as any + setInsertionState(n3, 1, true) + createComponent(Child, null, { + default: withVaporCtx(() => { + const n1 = template(' ')() as any + renderEffect(() => setText(n1, toDisplayString(ctx.count) + '!')) + return n1 + }), + }) + n3.$evtclick = createInvoker(() => ctx.count++) + renderEffect(() => setText(n0, toDisplayString(ctx.count) + '!')) + return n3 + }) + expect(root.innerHTML).toBe(`
1!
1!
`) + + // Should force update element children despite block optimization + rerender(parentId, (ctx: any) => { + const n5 = template('
', true)() as any + const n0 = child(n5) as any + const n1 = next(n0) as any + setInsertionState(n5, 2, true) + createComponentWithFallback(Child, null, { + default: withVaporCtx(() => { + const n2 = template(' ')() as any + renderEffect(() => setText(n2, toDisplayString(ctx.count) + '!')) + return n2 + }), + }) + const x1 = txt(n1) as any + n5.$evtclick = createInvoker(() => ctx.count++) + renderEffect(() => { + const count = ctx.count + setText(n0, toDisplayString(count)) + setText(x1, toDisplayString(count)) + }) + return n5 + }) + expect(root.innerHTML).toBe( + `
11
1!
`, + ) + + // Should force update child slot elements + rerender(parentId, (ctx: any) => { + const n2 = template('
', true)() as any + setInsertionState(n2, null, true) + createComponentWithFallback(Child, null, { + default: withVaporCtx(() => { + const n0 = template(' ')() as any + const x0 = txt(n0) as any + renderEffect(() => setText(x0, toDisplayString(ctx.count))) + return n0 + }), + }) + n2.$evtclick = createInvoker(() => ctx.count++) + return n2 + }) + expect(root.innerHTML).toBe( + `
1
`, + ) + }) + + test('reload', async () => { + // const root = nodeOps.createElement('div') + // const childId = 'test3-child' + // const unmountSpy = vi.fn() + // const mountSpy = vi.fn() + // const Child: ComponentOptions = { + // __hmrId: childId, + // data() { + // return { count: 0 } + // }, + // unmounted: unmountSpy, + // render: compileToFunction(`
{{ count }}
`), + // } + // createRecord(childId, Child) + // const Parent: ComponentOptions = { + // render: () => h(Child), + // } + // render(h(Parent), root) + // expect(serializeInner(root)).toBe(`
0
`) + // reload(childId, { + // __hmrId: childId, + // data() { + // return { count: 1 } + // }, + // mounted: mountSpy, + // render: compileToFunction(`
{{ count }}
`), + // }) + // await nextTick() + // expect(serializeInner(root)).toBe(`
1
`) + // expect(unmountSpy).toHaveBeenCalledTimes(1) + // expect(mountSpy).toHaveBeenCalledTimes(1) + }) + test('child reload + parent reload', async () => { const root = document.createElement('div') const childId = 'test1-child-reload' From ae5d0a4023f52e32d0608f87045a98f95f1fbb5d Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 18 Nov 2025 16:55:10 +0800 Subject: [PATCH 02/11] wip: refactor test --- packages/runtime-vapor/__tests__/_utils.ts | 28 ++- packages/runtime-vapor/__tests__/hmr.spec.ts | 190 +++++-------------- 2 files changed, 73 insertions(+), 145 deletions(-) diff --git a/packages/runtime-vapor/__tests__/_utils.ts b/packages/runtime-vapor/__tests__/_utils.ts index 1e056110320..13bd1c251cc 100644 --- a/packages/runtime-vapor/__tests__/_utils.ts +++ b/packages/runtime-vapor/__tests__/_utils.ts @@ -1,11 +1,16 @@ import { createVaporApp, vaporInteropPlugin } from '../src' import { type App, type Component, createApp } from '@vue/runtime-dom' -import type { VaporComponent, VaporComponentInstance } from '../src/component' +import type { + ObjectVaporComponent, + VaporComponent, + VaporComponentInstance, +} from '../src/component' import type { RawProps } from '../src/componentProps' import { compileScript, parse } from '@vue/compiler-sfc' import * as runtimeVapor from '../src' import * as runtimeDom from '@vue/runtime-dom' import * as VueServerRenderer from '@vue/server-renderer' +import { compile as compileVapor } from '@vue/compiler-vapor' export interface RenderContext { component: VaporComponent @@ -187,6 +192,27 @@ export function compile( ) } +export function compileToVaporRender( + template: string, +): ObjectVaporComponent['render'] { + let { code } = compileVapor(template, { + mode: 'module', + prefixIdentifiers: true, + hmr: true, + }) + + const transformed = code + .replace(/\bimport {/g, 'const {') + .replace(/ as _/g, ': _') + .replace(/} from ['"]vue['"];/g, '} = Vue;') + .replace(/export function render/, 'function render') + + return new Function('Vue', `${transformed}\nreturn render`)({ + ...runtimeDom, + ...runtimeVapor, + }) +} + export function shuffle(array: Array): any[] { let currentIndex = array.length let temporaryValue diff --git a/packages/runtime-vapor/__tests__/hmr.spec.ts b/packages/runtime-vapor/__tests__/hmr.spec.ts index 139f982afc1..2e8789991a4 100644 --- a/packages/runtime-vapor/__tests__/hmr.spec.ts +++ b/packages/runtime-vapor/__tests__/hmr.spec.ts @@ -1,28 +1,6 @@ -// TODO: port tests from packages/runtime-core/__tests__/hmr.spec.ts - -import { - type HMRRuntime, - nextTick, - ref, - toDisplayString, -} from '@vue/runtime-dom' -import { makeRender } from './_utils' -import { - child, - createComponent, - createComponentWithFallback, - createInvoker, - createSlot, - defineVaporComponent, - delegateEvents, - next, - renderEffect, - setInsertionState, - setText, - template, - txt, - withVaporCtx, -} from '@vue/runtime-vapor' +import { type HMRRuntime, nextTick, ref } from '@vue/runtime-dom' +import { compileToVaporRender as compileToFunction, makeRender } from './_utils' +import { defineVaporComponent, delegateEvents } from '@vue/runtime-vapor' declare var __VUE_HMR_RUNTIME__: HMRRuntime const { createRecord, rerender, reload } = __VUE_HMR_RUNTIME__ @@ -60,40 +38,24 @@ describe('hot module replacement', () => { const Child = defineVaporComponent({ __hmrId: childId, - render() { - const n1 = template('
', true)() as any - setInsertionState(n1, null, true) - createSlot('default', null) - return n1 - }, + render: compileToFunction('
'), }) createRecord(childId, Child as any) const Parent = defineVaporComponent({ __hmrId: parentId, + // @ts-expect-error ObjectVaporComponent doesn't have components + components: { Child }, setup() { const count = ref(0) return { count } }, - render(ctx) { - const n3 = template('
', true)() as any - const n0 = child(n3) as any - setInsertionState(n3, 1, true) - createComponent(Child, null, { - default: withVaporCtx(() => { - const n1 = template(' ')() as any - renderEffect(() => setText(n1, toDisplayString(ctx.count))) - return n1 - }), - }) - n3.$evtclick = createInvoker(() => ctx.count++) - renderEffect(() => setText(n0, toDisplayString(ctx.count))) - return n3 - }, + render: compileToFunction( + `
{{ count }}{{ count }}
`, + ), }) createRecord(parentId, Parent as any) - // render(h(Parent), root) const { mount } = define(Parent).create() mount(root) expect(root.innerHTML).toBe(`
0
0
`) @@ -106,82 +68,45 @@ describe('hot module replacement', () => { expect(root.innerHTML).toBe(`
1
1
`) // Update text while preserving state - rerender(parentId, (ctx: any) => { - const n3 = template('
', true)() as any - const n0 = child(n3) as any - setInsertionState(n3, 1, true) - createComponent(Child, null, { - default: withVaporCtx(() => { - const n1 = template(' ')() as any - renderEffect(() => setText(n1, toDisplayString(ctx.count))) - return n1 - }), - }) - n3.$evtclick = createInvoker(() => ctx.count++) - renderEffect(() => setText(n0, toDisplayString(ctx.count) + '!')) - return n3 - }) + rerender( + parentId, + compileToFunction( + `
{{ count }}!{{ count }}
`, + ), + ) expect(root.innerHTML).toBe(`
1!
1
`) // Should force child update on slot content change - rerender(parentId, (ctx: any) => { - const n3 = template('
', true)() as any - const n0 = child(n3) as any - setInsertionState(n3, 1, true) - createComponent(Child, null, { - default: withVaporCtx(() => { - const n1 = template(' ')() as any - renderEffect(() => setText(n1, toDisplayString(ctx.count) + '!')) - return n1 - }), - }) - n3.$evtclick = createInvoker(() => ctx.count++) - renderEffect(() => setText(n0, toDisplayString(ctx.count) + '!')) - return n3 - }) + rerender( + parentId, + compileToFunction( + `
{{ count }}!{{ count }}!
`, + ), + ) expect(root.innerHTML).toBe(`
1!
1!
`) // Should force update element children despite block optimization - rerender(parentId, (ctx: any) => { - const n5 = template('
', true)() as any - const n0 = child(n5) as any - const n1 = next(n0) as any - setInsertionState(n5, 2, true) - createComponentWithFallback(Child, null, { - default: withVaporCtx(() => { - const n2 = template(' ')() as any - renderEffect(() => setText(n2, toDisplayString(ctx.count) + '!')) - return n2 - }), - }) - const x1 = txt(n1) as any - n5.$evtclick = createInvoker(() => ctx.count++) - renderEffect(() => { - const count = ctx.count - setText(n0, toDisplayString(count)) - setText(x1, toDisplayString(count)) - }) - return n5 - }) + rerender( + parentId, + compileToFunction( + `
{{ count }}{{ count }} + {{ count }}! +
`, + ), + ) expect(root.innerHTML).toBe( `
11
1!
`, ) // Should force update child slot elements - rerender(parentId, (ctx: any) => { - const n2 = template('
', true)() as any - setInsertionState(n2, null, true) - createComponentWithFallback(Child, null, { - default: withVaporCtx(() => { - const n0 = template(' ')() as any - const x0 = txt(n0) as any - renderEffect(() => setText(x0, toDisplayString(ctx.count))) - return n0 - }), - }) - n2.$evtclick = createInvoker(() => ctx.count++) - return n2 - }) + rerender( + parentId, + compileToFunction( + `
+ {{ count }} +
`, + ), + ) expect(root.innerHTML).toBe( `
1
`, ) @@ -231,28 +156,19 @@ describe('hot module replacement', () => { const msg = ref('child') return { msg } }, - render(ctx) { - const n0 = template(`
`)() - const x0 = child(n0 as any) - renderEffect(() => setText(x0 as any, ctx.msg)) - return [n0] - }, + render: compileToFunction(`
{{ msg }}
`), }) createRecord(childId, Child as any) const { mount, component: Parent } = define({ __hmrId: parentId, + // @ts-expect-error + components: { Child }, setup() { const msg = ref('root') return { msg } }, - render(ctx) { - const n0 = createComponent(Child) - const n1 = template(`
`)() - const x0 = child(n1 as any) - renderEffect(() => setText(x0 as any, ctx.msg)) - return [n0, n1] - }, + render: compileToFunction(`
{{ msg }}
`), }).create() createRecord(parentId, Parent as any) mount(root) @@ -269,12 +185,7 @@ describe('hot module replacement', () => { const msg = ref('child changed') return { msg } }, - render(ctx: any) { - const n0 = template(`
`)() - const x0 = child(n0 as any) - renderEffect(() => setText(x0 as any, ctx.msg)) - return [n0] - }, + render: compileToFunction(`
{{ msg }}
`), }) expect(root.innerHTML).toMatchInlineSnapshot( `"
child changed
root
"`, @@ -288,12 +199,7 @@ describe('hot module replacement', () => { const msg = ref('child changed2') return { msg } }, - render(ctx: any) { - const n0 = template(`
`)() - const x0 = child(n0 as any) - renderEffect(() => setText(x0 as any, ctx.msg)) - return [n0] - }, + render: compileToFunction(`
{{ msg }}
`), }) expect(root.innerHTML).toMatchInlineSnapshot( `"
child changed2
root
"`, @@ -303,17 +209,13 @@ describe('hot module replacement', () => { reload(parentId, { __hmrId: parentId, __vapor: true, + // @ts-expect-error + components: { Child }, setup() { const msg = ref('root changed') return { msg } }, - render(ctx: any) { - const n0 = createComponent(Child) - const n1 = template(`
`)() - const x0 = child(n1 as any) - renderEffect(() => setText(x0 as any, ctx.msg)) - return [n0, n1] - }, + render: compileToFunction(`
{{ msg }}
`), }) expect(root.innerHTML).toMatchInlineSnapshot( `"
child changed2
root changed
"`, From a37e2c5241c22e9d4fd27f2eef7db009b849f01d Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 18 Nov 2025 17:01:38 +0800 Subject: [PATCH 03/11] wip: save --- packages/runtime-vapor/__tests__/hmr.spec.ts | 80 ++++++++++++-------- 1 file changed, 48 insertions(+), 32 deletions(-) diff --git a/packages/runtime-vapor/__tests__/hmr.spec.ts b/packages/runtime-vapor/__tests__/hmr.spec.ts index 2e8789991a4..a04399c40df 100644 --- a/packages/runtime-vapor/__tests__/hmr.spec.ts +++ b/packages/runtime-vapor/__tests__/hmr.spec.ts @@ -1,6 +1,16 @@ -import { type HMRRuntime, nextTick, ref } from '@vue/runtime-dom' +import { + type HMRRuntime, + nextTick, + onMounted, + onUnmounted, + ref, +} from '@vue/runtime-dom' import { compileToVaporRender as compileToFunction, makeRender } from './_utils' -import { defineVaporComponent, delegateEvents } from '@vue/runtime-vapor' +import { + createComponent, + defineVaporComponent, + delegateEvents, +} from '@vue/runtime-vapor' declare var __VUE_HMR_RUNTIME__: HMRRuntime const { createRecord, rerender, reload } = __VUE_HMR_RUNTIME__ @@ -113,36 +123,42 @@ describe('hot module replacement', () => { }) test('reload', async () => { - // const root = nodeOps.createElement('div') - // const childId = 'test3-child' - // const unmountSpy = vi.fn() - // const mountSpy = vi.fn() - // const Child: ComponentOptions = { - // __hmrId: childId, - // data() { - // return { count: 0 } - // }, - // unmounted: unmountSpy, - // render: compileToFunction(`
{{ count }}
`), - // } - // createRecord(childId, Child) - // const Parent: ComponentOptions = { - // render: () => h(Child), - // } - // render(h(Parent), root) - // expect(serializeInner(root)).toBe(`
0
`) - // reload(childId, { - // __hmrId: childId, - // data() { - // return { count: 1 } - // }, - // mounted: mountSpy, - // render: compileToFunction(`
{{ count }}
`), - // }) - // await nextTick() - // expect(serializeInner(root)).toBe(`
1
`) - // expect(unmountSpy).toHaveBeenCalledTimes(1) - // expect(mountSpy).toHaveBeenCalledTimes(1) + const root = document.createElement('div') + const childId = 'test3-child' + const unmountSpy = vi.fn() + const mountSpy = vi.fn() + const Child = defineVaporComponent({ + __hmrId: childId, + setup() { + onUnmounted(unmountSpy) + const count = ref(0) + return { count } + }, + render: compileToFunction(`
{{ count }}
`), + }) + createRecord(childId, Child as any) + + const Parent = defineVaporComponent({ + __hmrId: 'parentId', + render: () => createComponent(Child), + }) + + define(Parent).create().mount(root) + expect(root.innerHTML).toBe(`
0
`) + + reload(childId, { + __hmrId: childId, + setup() { + onMounted(mountSpy) + const count = ref(1) + return { count } + }, + render: compileToFunction(`
{{ count }}
`), + }) + await nextTick() + expect(root.innerHTML).toBe(`
1
`) + expect(unmountSpy).toHaveBeenCalledTimes(1) + expect(mountSpy).toHaveBeenCalledTimes(1) }) test('child reload + parent reload', async () => { From e6d858267f7a4f1608a6e17d454dd528286babde Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 19 Nov 2025 11:04:53 +0800 Subject: [PATCH 04/11] wip: save --- packages/runtime-vapor/__tests__/hmr.spec.ts | 893 +++++++++++++++++++ 1 file changed, 893 insertions(+) diff --git a/packages/runtime-vapor/__tests__/hmr.spec.ts b/packages/runtime-vapor/__tests__/hmr.spec.ts index a04399c40df..9ed6881dfa1 100644 --- a/packages/runtime-vapor/__tests__/hmr.spec.ts +++ b/packages/runtime-vapor/__tests__/hmr.spec.ts @@ -1,6 +1,8 @@ import { type HMRRuntime, nextTick, + onActivated, + onDeactivated, onMounted, onUnmounted, ref, @@ -161,6 +163,897 @@ describe('hot module replacement', () => { expect(mountSpy).toHaveBeenCalledTimes(1) }) + test.todo('reload KeepAlive slot', async () => { + const root = document.createElement('div') + const childId = 'test-child-keep-alive' + const unmountSpy = vi.fn() + const mountSpy = vi.fn() + const activeSpy = vi.fn() + const deactivatedSpy = vi.fn() + + const Child = defineVaporComponent({ + __hmrId: childId, + setup() { + onUnmounted(unmountSpy) + const count = ref(0) + return { count } + }, + render: compileToFunction(`
{{ count }}
`), + }) + createRecord(childId, Child as any) + + const Parent = defineVaporComponent({ + __hmrId: 'parentId', + // @ts-expect-error + components: { Child }, + setup() { + const toggle = ref(true) + return { toggle } + }, + render: compileToFunction( + `
0
`) + + reload(childId, { + __hmrId: childId, + setup() { + onMounted(mountSpy) + onUnmounted(unmountSpy) + onActivated(activeSpy) + onDeactivated(deactivatedSpy) + const count = ref(1) + return { count } + }, + render: compileToFunction(`
{{ count }}
`), + }) + await nextTick() + expect(root.innerHTML).toBe(`
1
`) + expect(unmountSpy).toHaveBeenCalledTimes(1) + expect(mountSpy).toHaveBeenCalledTimes(1) + expect(activeSpy).toHaveBeenCalledTimes(1) + expect(deactivatedSpy).toHaveBeenCalledTimes(0) + + // should not unmount when toggling + triggerEvent('click', root.children[1] as Element) + await nextTick() + expect(unmountSpy).toHaveBeenCalledTimes(1) + expect(mountSpy).toHaveBeenCalledTimes(1) + expect(activeSpy).toHaveBeenCalledTimes(1) + expect(deactivatedSpy).toHaveBeenCalledTimes(1) + + // should not mount when toggling + triggerEvent('click', root.children[1] as Element) + await nextTick() + expect(unmountSpy).toHaveBeenCalledTimes(1) + expect(mountSpy).toHaveBeenCalledTimes(1) + expect(activeSpy).toHaveBeenCalledTimes(2) + expect(deactivatedSpy).toHaveBeenCalledTimes(1) + }) + + // // #7121 + // test('reload KeepAlive slot in Transition', async () => { + // const root = nodeOps.createElement('div') + // const childId = 'test-transition-keep-alive-reload' + // const unmountSpy = vi.fn() + // const mountSpy = vi.fn() + // const activeSpy = vi.fn() + // const deactiveSpy = vi.fn() + + // const Child: ComponentOptions = { + // __hmrId: childId, + // data() { + // return { count: 0 } + // }, + // unmounted: unmountSpy, + // render: compileToFunction(`
{{ count }}
`), + // } + // createRecord(childId, Child) + + // const Parent: ComponentOptions = { + // components: { Child }, + // data() { + // return { toggle: true } + // }, + // render: compileToFunction( + // `
0
`) + + // reload(childId, { + // __hmrId: childId, + // data() { + // return { count: 1 } + // }, + // mounted: mountSpy, + // unmounted: unmountSpy, + // activated: activeSpy, + // deactivated: deactiveSpy, + // render: compileToFunction(`
{{ count }}
`), + // }) + // await nextTick() + // expect(serializeInner(root)).toBe(`
1
`) + // expect(unmountSpy).toHaveBeenCalledTimes(1) + // expect(mountSpy).toHaveBeenCalledTimes(1) + // expect(activeSpy).toHaveBeenCalledTimes(1) + // expect(deactiveSpy).toHaveBeenCalledTimes(0) + + // // should not unmount when toggling + // triggerEvent(root.children[1] as TestElement, 'click') + // await nextTick() + // expect(serializeInner(root)).toBe(``) + // expect(unmountSpy).toHaveBeenCalledTimes(1) + // expect(mountSpy).toHaveBeenCalledTimes(1) + // expect(activeSpy).toHaveBeenCalledTimes(1) + // expect(deactiveSpy).toHaveBeenCalledTimes(1) + + // // should not mount when toggling + // triggerEvent(root.children[1] as TestElement, 'click') + // await nextTick() + // expect(serializeInner(root)).toBe(`
1
`) + // expect(unmountSpy).toHaveBeenCalledTimes(1) + // expect(mountSpy).toHaveBeenCalledTimes(1) + // expect(activeSpy).toHaveBeenCalledTimes(2) + // expect(deactiveSpy).toHaveBeenCalledTimes(1) + // }) + + // test('reload KeepAlive slot in Transition with out-in', async () => { + // const root = nodeOps.createElement('div') + // const childId = 'test-transition-keep-alive-reload-with-out-in' + // const unmountSpy = vi.fn() + // const mountSpy = vi.fn() + // const activeSpy = vi.fn() + // const deactiveSpy = vi.fn() + + // const Child: ComponentOptions = { + // __hmrId: childId, + // name: 'original', + // data() { + // return { count: 0 } + // }, + // unmounted: unmountSpy, + // render: compileToFunction(`
{{ count }}
`), + // } + // createRecord(childId, Child) + + // const Parent: ComponentOptions = { + // components: { Child }, + // data() { + // return { toggle: true } + // }, + // methods: { + // // @ts-expect-error + // onLeave(_, done) { + // setTimeout(done, 0) + // }, + // }, + // render: compileToFunction( + // `
0
`) + + // reload(childId, { + // __hmrId: childId, + // name: 'updated', + // data() { + // return { count: 1 } + // }, + // mounted: mountSpy, + // unmounted: unmountSpy, + // activated: activeSpy, + // deactivated: deactiveSpy, + // render: compileToFunction(`
{{ count }}
`), + // }) + // await nextTick() + // await new Promise(r => setTimeout(r, 0)) + // expect(serializeInner(root)).toBe(`
1
`) + // expect(unmountSpy).toHaveBeenCalledTimes(1) + // expect(mountSpy).toHaveBeenCalledTimes(1) + // expect(activeSpy).toHaveBeenCalledTimes(1) + // expect(deactiveSpy).toHaveBeenCalledTimes(0) + + // // should not unmount when toggling + // triggerEvent(root.children[1] as TestElement, 'click') + // await nextTick() + // await new Promise(r => setTimeout(r, 0)) + // expect(serializeInner(root)).toBe(``) + // expect(unmountSpy).toHaveBeenCalledTimes(1) + // expect(mountSpy).toHaveBeenCalledTimes(1) + // expect(activeSpy).toHaveBeenCalledTimes(1) + // expect(deactiveSpy).toHaveBeenCalledTimes(1) + + // // should not mount when toggling + // triggerEvent(root.children[1] as TestElement, 'click') + // await nextTick() + // expect(serializeInner(root)).toBe(`
1
`) + // expect(unmountSpy).toHaveBeenCalledTimes(1) + // expect(mountSpy).toHaveBeenCalledTimes(1) + // expect(activeSpy).toHaveBeenCalledTimes(2) + // expect(deactiveSpy).toHaveBeenCalledTimes(1) + // }) + + // test('reload class component', async () => { + // const root = nodeOps.createElement('div') + // const childId = 'test4-child' + // const unmountSpy = vi.fn() + // const mountSpy = vi.fn() + + // class Child { + // static __vccOpts: ComponentOptions = { + // __hmrId: childId, + // data() { + // return { count: 0 } + // }, + // unmounted: unmountSpy, + // render: compileToFunction(`
{{ count }}
`), + // } + // } + // createRecord(childId, Child) + + // const Parent: ComponentOptions = { + // render: () => h(Child), + // } + + // render(h(Parent), root) + // expect(serializeInner(root)).toBe(`
0
`) + + // class UpdatedChild { + // static __vccOpts: ComponentOptions = { + // __hmrId: childId, + // data() { + // return { count: 1 } + // }, + // mounted: mountSpy, + // render: compileToFunction(`
{{ count }}
`), + // } + // } + + // reload(childId, UpdatedChild) + // await nextTick() + // expect(serializeInner(root)).toBe(`
1
`) + // expect(unmountSpy).toHaveBeenCalledTimes(1) + // expect(mountSpy).toHaveBeenCalledTimes(1) + // }) + + // // #6930 + // test('reload: avoid infinite recursion', async () => { + // const root = nodeOps.createElement('div') + // const childId = 'test-child-6930' + // const unmountSpy = vi.fn() + // const mountSpy = vi.fn() + + // const Child: ComponentOptions = { + // __hmrId: childId, + // data() { + // return { count: 0 } + // }, + // expose: ['count'], + // unmounted: unmountSpy, + // render: compileToFunction(`
{{ count }}
`), + // } + // createRecord(childId, Child) + + // const Parent: ComponentOptions = { + // setup() { + // const com1 = ref() + // const changeRef1 = (value: any) => (com1.value = value) + + // const com2 = ref() + // const changeRef2 = (value: any) => (com2.value = value) + + // return () => [ + // h(Child, { ref: changeRef1 }), + // h(Child, { ref: changeRef2 }), + // com1.value?.count, + // ] + // }, + // } + + // render(h(Parent), root) + // await nextTick() + // expect(serializeInner(root)).toBe(`
0
0
0`) + + // reload(childId, { + // __hmrId: childId, + // data() { + // return { count: 1 } + // }, + // mounted: mountSpy, + // render: compileToFunction(`
{{ count }}
`), + // }) + // await nextTick() + // expect(serializeInner(root)).toBe(`
1
1
1`) + // expect(unmountSpy).toHaveBeenCalledTimes(2) + // expect(mountSpy).toHaveBeenCalledTimes(2) + // }) + + // // #1156 - static nodes should retain DOM element reference across updates + // // when HMR is active + // test('static el reference', async () => { + // const root = nodeOps.createElement('div') + // const id = 'test-static-el' + + // const template = `
+ //
{{ count }}
+ // + //
` + + // const Comp: ComponentOptions = { + // __hmrId: id, + // data() { + // return { count: 0 } + // }, + // render: compileToFunction(template), + // } + // createRecord(id, Comp) + + // render(h(Comp), root) + // expect(serializeInner(root)).toBe( + // `
0
`, + // ) + + // // 1. click to trigger update + // triggerEvent((root as any).children[0].children[1], 'click') + // await nextTick() + // expect(serializeInner(root)).toBe( + // `
1
`, + // ) + + // // 2. trigger HMR + // rerender( + // id, + // compileToFunction(template.replace(`
1
`, + // ) + // }) + + // // #1157 - component should force full props update when HMR is active + // test('force update child component w/ static props', () => { + // const root = nodeOps.createElement('div') + // const parentId = 'test-force-props-parent' + // const childId = 'test-force-props-child' + + // const Child: ComponentOptions = { + // __hmrId: childId, + // props: { + // msg: String, + // }, + // render: compileToFunction(`
{{ msg }}
`), + // } + // createRecord(childId, Child) + + // const Parent: ComponentOptions = { + // __hmrId: parentId, + // components: { Child }, + // render: compileToFunction(``), + // } + // createRecord(parentId, Parent) + + // render(h(Parent), root) + // expect(serializeInner(root)).toBe(`
foo
`) + + // rerender(parentId, compileToFunction(``)) + // expect(serializeInner(root)).toBe(`
bar
`) + // }) + + // // #1305 - component should remove class + // test('remove static class from parent', () => { + // const root = nodeOps.createElement('div') + // const parentId = 'test-force-class-parent' + // const childId = 'test-force-class-child' + + // const Child: ComponentOptions = { + // __hmrId: childId, + // render: compileToFunction(`
child
`), + // } + // createRecord(childId, Child) + + // const Parent: ComponentOptions = { + // __hmrId: parentId, + // components: { Child }, + // render: compileToFunction(``), + // } + // createRecord(parentId, Parent) + + // render(h(Parent), root) + // expect(serializeInner(root)).toBe(`
child
`) + + // rerender(parentId, compileToFunction(``)) + // expect(serializeInner(root)).toBe(`
child
`) + // }) + + // test('rerender if any parent in the parent chain', () => { + // const root = nodeOps.createElement('div') + // const parent = 'test-force-props-parent-' + // const childId = 'test-force-props-child' + + // const numberOfParents = 5 + + // const Child: ComponentOptions = { + // __hmrId: childId, + // render: compileToFunction(`
child
`), + // } + // createRecord(childId, Child) + + // const components: ComponentOptions[] = [] + + // for (let i = 0; i < numberOfParents; i++) { + // const parentId = `${parent}${i}` + // const parentComp: ComponentOptions = { + // __hmrId: parentId, + // } + // components.push(parentComp) + // if (i === 0) { + // parentComp.render = compileToFunction(``) + // parentComp.components = { + // Child, + // } + // } else { + // parentComp.render = compileToFunction(``) + // parentComp.components = { + // Parent: components[i - 1], + // } + // } + + // createRecord(parentId, parentComp) + // } + + // const last = components[components.length - 1] + + // render(h(last), root) + // expect(serializeInner(root)).toBe(`
child
`) + + // rerender(last.__hmrId!, compileToFunction(``)) + // expect(serializeInner(root)).toBe(`
child
`) + // }) + + // // #3302 + // test('rerender with Teleport', () => { + // const root = nodeOps.createElement('div') + // const target = nodeOps.createElement('div') + // const parentId = 'parent-teleport' + + // const Child: ComponentOptions = { + // data() { + // return { + // // style is used to ensure that the div tag will be tracked by Teleport + // style: {}, + // target, + // } + // }, + // render: compileToFunction(` + // + //
+ // + //
+ //
+ // `), + // } + + // const Parent: ComponentOptions = { + // __hmrId: parentId, + // components: { Child }, + // render: compileToFunction(` + // + // + // + // `), + // } + // createRecord(parentId, Parent) + + // render(h(Parent), root) + // expect(serializeInner(root)).toBe( + // ``, + // ) + // expect(serializeInner(target)).toBe(`
1
`) + + // rerender( + // parentId, + // compileToFunction(` + // + // + // + // `), + // ) + // expect(serializeInner(root)).toBe( + // ``, + // ) + // expect(serializeInner(target)).toBe( + // `
1
2
`, + // ) + // }) + + // // #4174 + // test('with global mixins', async () => { + // const childId = 'hmr-global-mixin' + // const createSpy1 = vi.fn() + // const createSpy2 = vi.fn() + + // const Child: ComponentOptions = { + // __hmrId: childId, + // created: createSpy1, + // render() { + // return h('div') + // }, + // } + // createRecord(childId, Child) + + // const Parent: ComponentOptions = { + // render: () => h(Child), + // } + + // const app = createApp(Parent) + // app.mixin({}) + + // const root = nodeOps.createElement('div') + // app.mount(root) + // expect(createSpy1).toHaveBeenCalledTimes(1) + // expect(createSpy2).toHaveBeenCalledTimes(0) + + // reload(childId, { + // __hmrId: childId, + // created: createSpy2, + // render() { + // return h('div') + // }, + // }) + // await nextTick() + // expect(createSpy1).toHaveBeenCalledTimes(1) + // expect(createSpy2).toHaveBeenCalledTimes(1) + // }) + + // // #4757 + // test('rerender for component that has no active instance yet', () => { + // const id = 'no-active-instance-rerender' + // const Foo: ComponentOptions = { + // __hmrId: id, + // render: () => 'foo', + // } + + // createRecord(id, Foo) + // rerender(id, () => 'bar') + + // const root = nodeOps.createElement('div') + // render(h(Foo), root) + // expect(serializeInner(root)).toBe('bar') + // }) + + // test('reload for component that has no active instance yet', () => { + // const id = 'no-active-instance-reload' + // const Foo: ComponentOptions = { + // __hmrId: id, + // render: () => 'foo', + // } + + // createRecord(id, Foo) + // reload(id, { + // __hmrId: id, + // render: () => 'bar', + // }) + + // const root = nodeOps.createElement('div') + // render(h(Foo), root) + // expect(serializeInner(root)).toBe('bar') + // }) + + // // #7155 - force HMR on slots content update + // test('force update slot content change', () => { + // const root = nodeOps.createElement('div') + // const parentId = 'test-force-computed-parent' + // const childId = 'test-force-computed-child' + + // const Child: ComponentOptions = { + // __hmrId: childId, + // computed: { + // slotContent() { + // return this.$slots.default?.() + // }, + // }, + // render: compileToFunction(``), + // } + // createRecord(childId, Child) + + // const Parent: ComponentOptions = { + // __hmrId: parentId, + // components: { Child }, + // render: compileToFunction(`1`), + // } + // createRecord(parentId, Parent) + + // render(h(Parent), root) + // expect(serializeInner(root)).toBe(`1`) + + // rerender(parentId, compileToFunction(`2`)) + // expect(serializeInner(root)).toBe(`2`) + // }) + + // // #6978, #7138, #7114 + // test('hoisted children array inside v-for', () => { + // const root = nodeOps.createElement('div') + // const appId = 'test-app-id' + // const App: ComponentOptions = { + // __hmrId: appId, + // render: compileToFunction( + // `
+ //
1
+ //
+ //

2

+ //

3

`, + // ), + // } + // createRecord(appId, App) + + // render(h(App), root) + // expect(serializeInner(root)).toBe( + // `
1
1

2

3

`, + // ) + + // // move the

3

into the
1
+ // rerender( + // appId, + // compileToFunction( + // `
+ //
1

3

+ //
+ //

2

`, + // ), + // ) + // expect(serializeInner(root)).toBe( + // `
1

3

1

3

2

`, + // ) + // }) + + // // #11248 + // test('reload async component with multiple instances', async () => { + // const root = nodeOps.createElement('div') + // const childId = 'test-child-id' + // const Child: ComponentOptions = { + // __hmrId: childId, + // data() { + // return { count: 0 } + // }, + // render: compileToFunction(`
{{ count }}
`), + // } + // const Comp = runtimeTest.defineAsyncComponent(() => Promise.resolve(Child)) + // const appId = 'test-app-id' + // const App: ComponentOptions = { + // __hmrId: appId, + // render: () => [h(Comp), h(Comp)], + // } + // createRecord(appId, App) + + // render(h(App), root) + + // await timeout() + + // expect(serializeInner(root)).toBe(`
0
0
`) + + // // change count to 1 + // reload(childId, { + // __hmrId: childId, + // data() { + // return { count: 1 } + // }, + // render: compileToFunction(`
{{ count }}
`), + // }) + + // await timeout() + + // expect(serializeInner(root)).toBe(`
1
1
`) + // }) + + // test('reload async child wrapped in Suspense + KeepAlive', async () => { + // const id = 'async-child-reload' + // const AsyncChild: ComponentOptions = { + // __hmrId: id, + // async setup() { + // await nextTick() + // return () => 'foo' + // }, + // } + // createRecord(id, AsyncChild) + + // const appId = 'test-app-id' + // const App: ComponentOptions = { + // __hmrId: appId, + // components: { AsyncChild }, + // render: compileToFunction(` + //
+ // + // + // + // + // + //
+ // `), + // } + + // const root = nodeOps.createElement('div') + // render(h(App), root) + // expect(serializeInner(root)).toBe('
') + // await timeout() + // expect(serializeInner(root)).toBe('
foo
') + + // reload(id, { + // __hmrId: id, + // async setup() { + // await nextTick() + // return () => 'bar' + // }, + // }) + // await timeout() + // expect(serializeInner(root)).toBe('
bar
') + // }) + + // test('multi reload child wrapped in Suspense + KeepAlive', async () => { + // const id = 'test-child-reload-3' + // const Child: ComponentOptions = { + // __hmrId: id, + // setup() { + // const count = ref(0) + // return { count } + // }, + // render: compileToFunction(`
{{ count }}
`), + // } + // createRecord(id, Child) + + // const appId = 'test-app-id' + // const App: ComponentOptions = { + // __hmrId: appId, + // components: { Child }, + // render: compileToFunction(` + // + // + // + // + // + // `), + // } + + // const root = nodeOps.createElement('div') + // render(h(App), root) + // expect(serializeInner(root)).toBe('
0
') + // await timeout() + // reload(id, { + // __hmrId: id, + // setup() { + // const count = ref(1) + // return { count } + // }, + // render: compileToFunction(`
{{ count }}
`), + // }) + // await timeout() + // expect(serializeInner(root)).toBe('
1
') + + // reload(id, { + // __hmrId: id, + // setup() { + // const count = ref(2) + // return { count } + // }, + // render: compileToFunction(`
{{ count }}
`), + // }) + // await timeout() + // expect(serializeInner(root)).toBe('
2
') + // }) + + // test('rerender for nested component', () => { + // const id = 'child-nested-rerender' + // const Foo: ComponentOptions = { + // __hmrId: id, + // render() { + // return this.$slots.default() + // }, + // } + // createRecord(id, Foo) + + // const parentId = 'parent-nested-rerender' + // const Parent: ComponentOptions = { + // __hmrId: parentId, + // render() { + // return h(Foo, null, { + // default: () => this.$slots.default(), + // _: 3 /* FORWARDED */, + // }) + // }, + // } + + // const appId = 'app-nested-rerender' + // const App: ComponentOptions = { + // __hmrId: appId, + // render: () => + // h(Parent, null, { + // default: () => [ + // h(Foo, null, { + // default: () => ['foo'], + // }), + // ], + // }), + // } + // createRecord(parentId, App) + + // const root = nodeOps.createElement('div') + // render(h(App), root) + // expect(serializeInner(root)).toBe('foo') + + // rerender(id, () => 'bar') + // expect(serializeInner(root)).toBe('bar') + // }) + + // // https://github.com/vitejs/vite-plugin-vue/issues/599 + // // Both Outer and Inner are reloaded when './server.js' changes + // test('reload nested components from single update', async () => { + // const innerId = 'nested-reload-inner' + // const outerId = 'nested-reload-outer' + + // let Inner = { + // __hmrId: innerId, + // render() { + // return h('div', 'foo') + // }, + // } + // let Outer = { + // __hmrId: outerId, + // render() { + // return h(Inner) + // }, + // } + + // createRecord(innerId, Inner) + // createRecord(outerId, Outer) + + // const App = { + // render: () => h(Outer), + // } + + // const root = nodeOps.createElement('div') + // render(h(App), root) + // expect(serializeInner(root)).toBe('
foo
') + + // Inner = { + // __hmrId: innerId, + // render() { + // return h('div', 'bar') + // }, + // } + // Outer = { + // __hmrId: outerId, + // render() { + // return h(Inner) + // }, + // } + + // // trigger reload for both Outer and Inner + // reload(outerId, Outer) + // reload(innerId, Inner) + // await nextTick() + + // expect(serializeInner(root)).toBe('
bar
') + // }) + test('child reload + parent reload', async () => { const root = document.createElement('div') const childId = 'test1-child-reload' From 88ae7427289882acda4cd6640954080bdb11abcb Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 19 Nov 2025 17:18:42 +0800 Subject: [PATCH 05/11] feat: Refactor `KeepAlive` to manage fragment scope and caching via new `FragmentHooks` on `DynamicFragment`. --- packages/runtime-vapor/src/block.ts | 5 --- .../runtime-vapor/src/components/KeepAlive.ts | 34 +++++++++++--- packages/runtime-vapor/src/fragment.ts | 44 +++++++++---------- 3 files changed, 50 insertions(+), 33 deletions(-) diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index 27c9e333716..eb3a2bde2d6 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -160,11 +160,6 @@ export function remove(block: Block, parent?: ParentNode): void { if (block.anchor) remove(block.anchor, parent) if ((block as DynamicFragment).scope) { ;(block as DynamicFragment).scope!.stop() - const scopes = (block as DynamicFragment).keptAliveScopes - if (scopes) { - scopes.forEach(scope => scope.stop()) - scopes.clear() - } } } } diff --git a/packages/runtime-vapor/src/components/KeepAlive.ts b/packages/runtime-vapor/src/components/KeepAlive.ts index 573fe613b96..2442b5a2765 100644 --- a/packages/runtime-vapor/src/components/KeepAlive.ts +++ b/packages/runtime-vapor/src/components/KeepAlive.ts @@ -29,8 +29,10 @@ import { createElement } from '../dom/node' import { type DynamicFragment, type VaporFragment, + isDynamicFragment, isFragment, } from '../fragment' +import type { EffectScope } from '@vue/reactivity' export interface KeepAliveInstance extends VaporComponentInstance { activate: ( @@ -44,8 +46,6 @@ export interface KeepAliveInstance extends VaporComponentInstance { comp: VaporComponent, ) => VaporComponentInstance | VaporFragment | undefined getStorageContainer: () => ParentNode - processFragment: (fragment: DynamicFragment) => void - cacheFragment: (fragment: DynamicFragment) => void } type CacheKey = VaporComponent | VNode['type'] @@ -69,6 +69,7 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({ const cache: Cache = new Map() const keys: Keys = new Set() const storageContainer = createElement('div') + const keptAliveScopes = new Map() let current: VaporComponentInstance | VaporFragment | undefined if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { @@ -163,6 +164,8 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({ remove(cached, storageContainer) }) + keptAliveScopes.forEach(scope => scope.stop()) + keptAliveScopes.clear() }) keepAliveInstance.getStorageContainer = () => storageContainer @@ -177,7 +180,7 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({ innerCacheBlock(instance.type, instance) } - keepAliveInstance.processFragment = (frag: DynamicFragment) => { + const processFragment = (frag: DynamicFragment) => { const innerBlock = getInnerBlock(frag.nodes) if (!innerBlock) return @@ -199,7 +202,7 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({ } } - keepAliveInstance.cacheFragment = (fragment: DynamicFragment) => { + const cacheFragment = (fragment: DynamicFragment) => { const innerBlock = getInnerBlock(fragment.nodes) if (!innerBlock || !shouldCache(innerBlock)) return @@ -208,7 +211,7 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({ let key: CacheKey // find vdom interop fragment - const frag = findInteropFragment(fragment) + const frag = findInteropFragment(fragment.nodes) if (frag) { frag.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE toCache = frag @@ -256,6 +259,27 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({ children.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE } else if (isInteropFragment(children)) { children.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE + } else if (isDynamicFragment(children)) { + children.hooks = { + beforeUpdate(oldKey, newKey) { + processFragment(children) + const scope = children.scope + if (scope) { + keptAliveScopes.set(oldKey, scope) + } + }, + afterUpdate(newKey, nodes, scope) { + cacheFragment(children) + }, + getScope(key) { + const scope = keptAliveScopes.get(key) + if (scope) { + keptAliveScopes.delete(key) + return scope + } + }, + } + cacheFragment(children) } function pruneCache(filter: (name: string) => boolean) { diff --git a/packages/runtime-vapor/src/fragment.ts b/packages/runtime-vapor/src/fragment.ts index e30909ea067..347b23ffae9 100644 --- a/packages/runtime-vapor/src/fragment.ts +++ b/packages/runtime-vapor/src/fragment.ts @@ -11,16 +11,12 @@ import { remove, } from './block' import { - type GenericComponentInstance, type TransitionHooks, type VNode, - currentInstance, - isKeepAlive, queuePostFlushCb, } from '@vue/runtime-dom' import type { VaporComponentInstance } from './component' import type { NodeRef } from './apiTemplateRef' -import type { KeepAliveInstance } from './components/KeepAlive' import { applyTransitionHooks, applyTransitionLeaveHooks, @@ -33,6 +29,12 @@ import { locateHydrationNode, } from './dom/hydration' +export interface FragmentHooks { + getScope(key: any): EffectScope | undefined + beforeUpdate(oldKey: any, newKey: any): void + afterUpdate(newKey: any, nodes: Block, scope: EffectScope): void +} + export class VaporFragment implements TransitionOptions { @@ -73,8 +75,7 @@ export class DynamicFragment extends VaporFragment { current?: BlockFn fallback?: BlockFn anchorLabel?: string - inKeepAlive?: boolean - keptAliveScopes?: Map + hooks?: FragmentHooks constructor(anchorLabel?: string) { super([]) @@ -97,21 +98,17 @@ export class DynamicFragment extends VaporFragment { const prevSub = setActiveSub() const parent = isHydrating ? null : this.anchor.parentNode const transition = this.$transition - const instance = currentInstance! - this.inKeepAlive = isKeepAlive(instance) // teardown previous branch if (this.scope) { - if (this.inKeepAlive) { - ;(instance as KeepAliveInstance).processFragment(this) - if (!this.keptAliveScopes) this.keptAliveScopes = new Map() - this.keptAliveScopes.set(this.current, this.scope) + if (this.hooks) { + this.hooks.beforeUpdate(this.current, key) } else { this.scope.stop() } const mode = transition && transition.mode if (mode) { applyTransitionLeaveHooks(this.nodes, transition, () => - this.render(render, instance, transition, parent), + this.render(render, transition, parent), ) parent && remove(this.nodes, parent) if (mode === 'out-in') { @@ -123,7 +120,7 @@ export class DynamicFragment extends VaporFragment { } } - this.render(render, instance, transition, parent) + this.render(render, transition, parent) if (this.fallback) { // set fallback for nested fragments @@ -155,27 +152,22 @@ export class DynamicFragment extends VaporFragment { private render( render: BlockFn | undefined, - instance: GenericComponentInstance, transition: VaporTransitionHooks | undefined, parent: ParentNode | null, ) { if (render) { // For KeepAlive, try to reuse the keepAlive scope for this key - const scope = - this.inKeepAlive && this.keptAliveScopes - ? this.keptAliveScopes.get(this.current) - : undefined + const scope = this.hooks && this.hooks.getScope(this.current) if (scope) { this.scope = scope - this.keptAliveScopes!.delete(this.current!) - this.scope.resume() } else { this.scope = new EffectScope() } this.nodes = this.scope.run(render) || [] - if (this.inKeepAlive) { - ;(instance as KeepAliveInstance).cacheFragment(this) + + if (this.hooks) { + this.hooks.afterUpdate(this.current, this.nodes, this.scope) } if (transition) { this.$transition = applyTransitionHooks(this.nodes, transition) @@ -287,3 +279,9 @@ function findInvalidFragment(fragment: VaporFragment): VaporFragment | null { export function isFragment(val: NonNullable): val is VaporFragment { return val instanceof VaporFragment } + +export function isDynamicFragment( + val: NonNullable, +): val is DynamicFragment { + return val instanceof DynamicFragment +} From bce68d5a34b6560b519b1b6861d58fe7f5a2af95 Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 21 Nov 2025 09:40:55 +0800 Subject: [PATCH 06/11] refactor: remove `FragmentHooks` interface --- packages/runtime-vapor/src/fragment.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/runtime-vapor/src/fragment.ts b/packages/runtime-vapor/src/fragment.ts index da5754a79df..96dcf6e7dda 100644 --- a/packages/runtime-vapor/src/fragment.ts +++ b/packages/runtime-vapor/src/fragment.ts @@ -29,12 +29,6 @@ import { locateHydrationNode, } from './dom/hydration' -export interface FragmentHooks { - getScope(key: any): EffectScope | undefined - beforeUpdate(oldKey: any, newKey: any): void - afterUpdate(newKey: any, nodes: Block, scope: EffectScope): void -} - export class VaporFragment implements TransitionOptions { From d34c1ac7273f31f3f517f8038cf26a34dc231de1 Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 21 Nov 2025 10:42:42 +0800 Subject: [PATCH 07/11] refactor: simplify `__DEV__` HMR registration and remove `isSingleRoot` from async component creation. --- packages/runtime-vapor/src/apiDefineAsyncComponent.ts | 6 ++++-- packages/runtime-vapor/src/component.ts | 4 +--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/runtime-vapor/src/apiDefineAsyncComponent.ts b/packages/runtime-vapor/src/apiDefineAsyncComponent.ts index 1b7c82bbefc..48590177694 100644 --- a/packages/runtime-vapor/src/apiDefineAsyncComponent.ts +++ b/packages/runtime-vapor/src/apiDefineAsyncComponent.ts @@ -181,12 +181,14 @@ function createInnerComp( parent: VaporComponentInstance & TransitionOptions, frag?: DynamicFragment, ): VaporComponentInstance { - const { rawProps, rawSlots, isSingleRoot, appContext, $transition } = parent + const { rawProps, rawSlots, appContext, $transition } = parent const instance = createComponent( comp, rawProps, rawSlots, - isSingleRoot, + // rawProps is shared and already contains fallthrough attrs. + // so isSingleRoot should be undefined + undefined, undefined, appContext, ) diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index cae2bd7860d..d5f7eeb0f58 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -272,14 +272,12 @@ export function createComponent( const prevSlotConsumer = setCurrentSlotConsumer(null) // HMR - if (__DEV__ && component.__hmrId) { + if (__DEV__) { registerHMR(instance) instance.isSingleRoot = isSingleRoot instance.hmrRerender = hmrRerender.bind(null, instance) instance.hmrReload = hmrReload.bind(null, instance) - } - if (__DEV__) { pushWarningContext(instance) startMeasure(instance, `init`) From 3995a715b8dba3b8064f41b9c9315739c13109cc Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 21 Nov 2025 14:51:37 +0800 Subject: [PATCH 08/11] test: reload keep-alive slot --- packages/compiler-vapor/src/generators/component.ts | 4 ++-- packages/runtime-vapor/__tests__/hmr.spec.ts | 8 +++++--- packages/runtime-vapor/src/hmr.ts | 6 ++++++ 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/compiler-vapor/src/generators/component.ts b/packages/compiler-vapor/src/generators/component.ts index cd842a91887..93e7f8a6ac0 100644 --- a/packages/compiler-vapor/src/generators/component.ts +++ b/packages/compiler-vapor/src/generators/component.ts @@ -40,7 +40,7 @@ import { genEventHandler } from './event' import { genDirectiveModifiers, genDirectivesForElement } from './directive' import { genBlock } from './block' import { genModelHandler } from './vModel' -import { isBuiltInComponent } from '../utils' +import { isBuiltInComponent, isKeepAliveTag } from '../utils' export function genCreateComponent( operation: CreateComponentIRNode, @@ -460,7 +460,7 @@ function genSlotBlockWithProps(oper: SlotBlockIRNode, context: CodegenContext) { ] } - if (node.type === NodeTypes.ELEMENT) { + if (node.type === NodeTypes.ELEMENT && !isKeepAliveTag(node.tag)) { // wrap with withVaporCtx to ensure correct currentInstance inside slot blockFn = [`${context.helper('withVaporCtx')}(`, ...blockFn, `)`] } diff --git a/packages/runtime-vapor/__tests__/hmr.spec.ts b/packages/runtime-vapor/__tests__/hmr.spec.ts index 9ed6881dfa1..8ac28a0156e 100644 --- a/packages/runtime-vapor/__tests__/hmr.spec.ts +++ b/packages/runtime-vapor/__tests__/hmr.spec.ts @@ -163,8 +163,9 @@ describe('hot module replacement', () => { expect(mountSpy).toHaveBeenCalledTimes(1) }) - test.todo('reload KeepAlive slot', async () => { + test('reload KeepAlive slot', async () => { const root = document.createElement('div') + document.body.appendChild(root) const childId = 'test-child-keep-alive' const unmountSpy = vi.fn() const mountSpy = vi.fn() @@ -201,6 +202,7 @@ describe('hot module replacement', () => { reload(childId, { __hmrId: childId, + __vapor: true, setup() { onMounted(mountSpy) onUnmounted(unmountSpy) @@ -219,7 +221,7 @@ describe('hot module replacement', () => { expect(deactivatedSpy).toHaveBeenCalledTimes(0) // should not unmount when toggling - triggerEvent('click', root.children[1] as Element) + triggerEvent('click', root.children[0] as Element) await nextTick() expect(unmountSpy).toHaveBeenCalledTimes(1) expect(mountSpy).toHaveBeenCalledTimes(1) @@ -227,7 +229,7 @@ describe('hot module replacement', () => { expect(deactivatedSpy).toHaveBeenCalledTimes(1) // should not mount when toggling - triggerEvent('click', root.children[1] as Element) + triggerEvent('click', root.children[0] as Element) await nextTick() expect(unmountSpy).toHaveBeenCalledTimes(1) expect(mountSpy).toHaveBeenCalledTimes(1) diff --git a/packages/runtime-vapor/src/hmr.ts b/packages/runtime-vapor/src/hmr.ts index 4e9927ee9ab..f045595b548 100644 --- a/packages/runtime-vapor/src/hmr.ts +++ b/packages/runtime-vapor/src/hmr.ts @@ -1,4 +1,5 @@ import { + isKeepAlive, popWarningContext, pushWarningContext, setCurrentInstance, @@ -35,6 +36,11 @@ export function hmrReload( instance: VaporComponentInstance, newComp: VaporComponent, ): void { + // if parent is KeepAlive, we need to rerender it + if (instance.parent && isKeepAlive(instance.parent)) { + instance.parent.hmrRerender!() + return + } const normalized = normalizeBlock(instance.block) const parent = normalized[0].parentNode! const anchor = normalized[normalized.length - 1].nextSibling From 23511e5ea6fb86445c3f9b8a2e38fb2ee17fd11c Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 21 Nov 2025 15:11:22 +0800 Subject: [PATCH 09/11] wip: add more tests for keepalive hmr reload --- packages/runtime-vapor/__tests__/hmr.spec.ts | 287 ++++++++++--------- 1 file changed, 148 insertions(+), 139 deletions(-) diff --git a/packages/runtime-vapor/__tests__/hmr.spec.ts b/packages/runtime-vapor/__tests__/hmr.spec.ts index 8ac28a0156e..004ec551238 100644 --- a/packages/runtime-vapor/__tests__/hmr.spec.ts +++ b/packages/runtime-vapor/__tests__/hmr.spec.ts @@ -237,158 +237,167 @@ describe('hot module replacement', () => { expect(deactivatedSpy).toHaveBeenCalledTimes(1) }) - // // #7121 - // test('reload KeepAlive slot in Transition', async () => { - // const root = nodeOps.createElement('div') - // const childId = 'test-transition-keep-alive-reload' - // const unmountSpy = vi.fn() - // const mountSpy = vi.fn() - // const activeSpy = vi.fn() - // const deactiveSpy = vi.fn() + test('reload KeepAlive slot in Transition', async () => { + const root = document.createElement('div') + document.body.appendChild(root) + const childId = 'test-transition-keep-alive-reload' + const unmountSpy = vi.fn() + const mountSpy = vi.fn() + const activeSpy = vi.fn() + const deactivatedSpy = vi.fn() - // const Child: ComponentOptions = { - // __hmrId: childId, - // data() { - // return { count: 0 } - // }, - // unmounted: unmountSpy, - // render: compileToFunction(`
{{ count }}
`), - // } - // createRecord(childId, Child) + const Child = defineVaporComponent({ + __hmrId: childId, + setup() { + onUnmounted(unmountSpy) + const count = ref(0) + return { count } + }, + render: compileToFunction(`
{{ count }}
`), + }) + createRecord(childId, Child as any) - // const Parent: ComponentOptions = { - // components: { Child }, - // data() { - // return { toggle: true } - // }, - // render: compileToFunction( - // `
0
`) + define(Parent).create().mount(root) + expect(root.innerHTML).toBe(`
0
`) - // reload(childId, { - // __hmrId: childId, - // data() { - // return { count: 1 } - // }, - // mounted: mountSpy, - // unmounted: unmountSpy, - // activated: activeSpy, - // deactivated: deactiveSpy, - // render: compileToFunction(`
{{ count }}
`), - // }) - // await nextTick() - // expect(serializeInner(root)).toBe(`
1
`) - // expect(unmountSpy).toHaveBeenCalledTimes(1) - // expect(mountSpy).toHaveBeenCalledTimes(1) - // expect(activeSpy).toHaveBeenCalledTimes(1) - // expect(deactiveSpy).toHaveBeenCalledTimes(0) + reload(childId, { + __hmrId: childId, + __vapor: true, + setup() { + onMounted(mountSpy) + onUnmounted(unmountSpy) + onActivated(activeSpy) + onDeactivated(deactivatedSpy) + const count = ref(1) + return { count } + }, + render: compileToFunction(`
{{ count }}
`), + }) + await nextTick() + expect(root.innerHTML).toBe(`
1
`) + expect(unmountSpy).toHaveBeenCalledTimes(1) + expect(mountSpy).toHaveBeenCalledTimes(1) + expect(activeSpy).toHaveBeenCalledTimes(1) + expect(deactivatedSpy).toHaveBeenCalledTimes(0) - // // should not unmount when toggling - // triggerEvent(root.children[1] as TestElement, 'click') - // await nextTick() - // expect(serializeInner(root)).toBe(``) - // expect(unmountSpy).toHaveBeenCalledTimes(1) - // expect(mountSpy).toHaveBeenCalledTimes(1) - // expect(activeSpy).toHaveBeenCalledTimes(1) - // expect(deactiveSpy).toHaveBeenCalledTimes(1) + // should not unmount when toggling + triggerEvent('click', root.children[0] as Element) + await nextTick() + expect(root.innerHTML).toBe(``) + expect(unmountSpy).toHaveBeenCalledTimes(1) + expect(mountSpy).toHaveBeenCalledTimes(1) + expect(activeSpy).toHaveBeenCalledTimes(1) + expect(deactivatedSpy).toHaveBeenCalledTimes(1) - // // should not mount when toggling - // triggerEvent(root.children[1] as TestElement, 'click') - // await nextTick() - // expect(serializeInner(root)).toBe(`
1
`) - // expect(unmountSpy).toHaveBeenCalledTimes(1) - // expect(mountSpy).toHaveBeenCalledTimes(1) - // expect(activeSpy).toHaveBeenCalledTimes(2) - // expect(deactiveSpy).toHaveBeenCalledTimes(1) - // }) + // should not mount when toggling + triggerEvent('click', root.children[0] as Element) + await nextTick() + expect(root.innerHTML).toBe(`
1
`) + expect(unmountSpy).toHaveBeenCalledTimes(1) + expect(mountSpy).toHaveBeenCalledTimes(1) + expect(activeSpy).toHaveBeenCalledTimes(2) + expect(deactivatedSpy).toHaveBeenCalledTimes(1) + }) - // test('reload KeepAlive slot in Transition with out-in', async () => { - // const root = nodeOps.createElement('div') - // const childId = 'test-transition-keep-alive-reload-with-out-in' - // const unmountSpy = vi.fn() - // const mountSpy = vi.fn() - // const activeSpy = vi.fn() - // const deactiveSpy = vi.fn() + test('reload KeepAlive slot in Transition with out-in', async () => { + const root = document.createElement('div') + document.body.appendChild(root) + const childId = 'test-transition-keep-alive-reload-with-out-in' + const unmountSpy = vi.fn() + const mountSpy = vi.fn() + const activeSpy = vi.fn() + const deactivatedSpy = vi.fn() - // const Child: ComponentOptions = { - // __hmrId: childId, - // name: 'original', - // data() { - // return { count: 0 } - // }, - // unmounted: unmountSpy, - // render: compileToFunction(`
{{ count }}
`), - // } - // createRecord(childId, Child) + const Child = defineVaporComponent({ + name: 'original', + __hmrId: childId, + setup() { + onUnmounted(unmountSpy) + const count = ref(0) + return { count } + }, + render: compileToFunction(`
{{ count }}
`), + }) + createRecord(childId, Child as any) - // const Parent: ComponentOptions = { - // components: { Child }, - // data() { - // return { toggle: true } - // }, - // methods: { - // // @ts-expect-error - // onLeave(_, done) { - // setTimeout(done, 0) - // }, - // }, - // render: compileToFunction( - // `
0
`) + define(Parent).create().mount(root) + expect(root.innerHTML).toBe(`
0
`) - // reload(childId, { - // __hmrId: childId, - // name: 'updated', - // data() { - // return { count: 1 } - // }, - // mounted: mountSpy, - // unmounted: unmountSpy, - // activated: activeSpy, - // deactivated: deactiveSpy, - // render: compileToFunction(`
{{ count }}
`), - // }) - // await nextTick() - // await new Promise(r => setTimeout(r, 0)) - // expect(serializeInner(root)).toBe(`
1
`) - // expect(unmountSpy).toHaveBeenCalledTimes(1) - // expect(mountSpy).toHaveBeenCalledTimes(1) - // expect(activeSpy).toHaveBeenCalledTimes(1) - // expect(deactiveSpy).toHaveBeenCalledTimes(0) + reload(childId, { + name: 'updated', + __hmrId: childId, + __vapor: true, + setup() { + onMounted(mountSpy) + onUnmounted(unmountSpy) + onActivated(activeSpy) + onDeactivated(deactivatedSpy) + const count = ref(1) + return { count } + }, + render: compileToFunction(`
{{ count }}
`), + }) + await nextTick() + await new Promise(r => setTimeout(r, 0)) + expect(root.innerHTML).toBe(`
1
`) + expect(unmountSpy).toHaveBeenCalledTimes(1) + expect(mountSpy).toHaveBeenCalledTimes(1) + expect(activeSpy).toHaveBeenCalledTimes(1) + expect(deactivatedSpy).toHaveBeenCalledTimes(0) - // // should not unmount when toggling - // triggerEvent(root.children[1] as TestElement, 'click') - // await nextTick() - // await new Promise(r => setTimeout(r, 0)) - // expect(serializeInner(root)).toBe(``) - // expect(unmountSpy).toHaveBeenCalledTimes(1) - // expect(mountSpy).toHaveBeenCalledTimes(1) - // expect(activeSpy).toHaveBeenCalledTimes(1) - // expect(deactiveSpy).toHaveBeenCalledTimes(1) + // should not unmount when toggling + triggerEvent('click', root.children[0] as Element) + await nextTick() + await new Promise(r => setTimeout(r, 0)) + expect(root.innerHTML).toBe(``) + expect(unmountSpy).toHaveBeenCalledTimes(1) + expect(mountSpy).toHaveBeenCalledTimes(1) + expect(activeSpy).toHaveBeenCalledTimes(1) + expect(deactivatedSpy).toHaveBeenCalledTimes(1) - // // should not mount when toggling - // triggerEvent(root.children[1] as TestElement, 'click') - // await nextTick() - // expect(serializeInner(root)).toBe(`
1
`) - // expect(unmountSpy).toHaveBeenCalledTimes(1) - // expect(mountSpy).toHaveBeenCalledTimes(1) - // expect(activeSpy).toHaveBeenCalledTimes(2) - // expect(deactiveSpy).toHaveBeenCalledTimes(1) - // }) + // should not mount when toggling + triggerEvent('click', root.children[0] as Element) + await nextTick() + expect(root.innerHTML).toBe(`
1
`) + expect(unmountSpy).toHaveBeenCalledTimes(1) + expect(mountSpy).toHaveBeenCalledTimes(1) + expect(activeSpy).toHaveBeenCalledTimes(2) + expect(deactivatedSpy).toHaveBeenCalledTimes(1) + }) // test('reload class component', async () => { // const root = nodeOps.createElement('div') From 4b0d95f10b71c1390e22c40c3ae52cf35f4e9b05 Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 21 Nov 2025 17:08:54 +0800 Subject: [PATCH 10/11] test: add more tests --- packages/runtime-vapor/__tests__/_utils.ts | 7 +- packages/runtime-vapor/__tests__/hmr.spec.ts | 689 ++++++++----------- 2 files changed, 301 insertions(+), 395 deletions(-) diff --git a/packages/runtime-vapor/__tests__/_utils.ts b/packages/runtime-vapor/__tests__/_utils.ts index 13bd1c251cc..746d94a776d 100644 --- a/packages/runtime-vapor/__tests__/_utils.ts +++ b/packages/runtime-vapor/__tests__/_utils.ts @@ -10,7 +10,10 @@ import { compileScript, parse } from '@vue/compiler-sfc' import * as runtimeVapor from '../src' import * as runtimeDom from '@vue/runtime-dom' import * as VueServerRenderer from '@vue/server-renderer' -import { compile as compileVapor } from '@vue/compiler-vapor' +import { + type CompilerOptions, + compile as compileVapor, +} from '@vue/compiler-vapor' export interface RenderContext { component: VaporComponent @@ -194,11 +197,13 @@ export function compile( export function compileToVaporRender( template: string, + options?: CompilerOptions, ): ObjectVaporComponent['render'] { let { code } = compileVapor(template, { mode: 'module', prefixIdentifiers: true, hmr: true, + ...options, }) const transformed = code diff --git a/packages/runtime-vapor/__tests__/hmr.spec.ts b/packages/runtime-vapor/__tests__/hmr.spec.ts index 004ec551238..c1902c9057a 100644 --- a/packages/runtime-vapor/__tests__/hmr.spec.ts +++ b/packages/runtime-vapor/__tests__/hmr.spec.ts @@ -1,18 +1,26 @@ import { type HMRRuntime, + computed, nextTick, onActivated, onDeactivated, onMounted, onUnmounted, ref, + toDisplayString, } from '@vue/runtime-dom' import { compileToVaporRender as compileToFunction, makeRender } from './_utils' import { createComponent, + createTemplateRefSetter, defineVaporComponent, delegateEvents, + renderEffect, + setText, + template, } from '@vue/runtime-vapor' +import { BindingTypes } from '@vue/compiler-core' +import type { VaporComponent } from '../src/component' declare var __VUE_HMR_RUNTIME__: HMRRuntime const { createRecord, rerender, reload } = __VUE_HMR_RUNTIME__ @@ -399,443 +407,336 @@ describe('hot module replacement', () => { expect(deactivatedSpy).toHaveBeenCalledTimes(1) }) - // test('reload class component', async () => { - // const root = nodeOps.createElement('div') - // const childId = 'test4-child' - // const unmountSpy = vi.fn() - // const mountSpy = vi.fn() - - // class Child { - // static __vccOpts: ComponentOptions = { - // __hmrId: childId, - // data() { - // return { count: 0 } - // }, - // unmounted: unmountSpy, - // render: compileToFunction(`
{{ count }}
`), - // } - // } - // createRecord(childId, Child) - - // const Parent: ComponentOptions = { - // render: () => h(Child), - // } - - // render(h(Parent), root) - // expect(serializeInner(root)).toBe(`
0
`) - - // class UpdatedChild { - // static __vccOpts: ComponentOptions = { - // __hmrId: childId, - // data() { - // return { count: 1 } - // }, - // mounted: mountSpy, - // render: compileToFunction(`
{{ count }}
`), - // } - // } - - // reload(childId, UpdatedChild) - // await nextTick() - // expect(serializeInner(root)).toBe(`
1
`) - // expect(unmountSpy).toHaveBeenCalledTimes(1) - // expect(mountSpy).toHaveBeenCalledTimes(1) - // }) - - // // #6930 - // test('reload: avoid infinite recursion', async () => { - // const root = nodeOps.createElement('div') - // const childId = 'test-child-6930' - // const unmountSpy = vi.fn() - // const mountSpy = vi.fn() - - // const Child: ComponentOptions = { - // __hmrId: childId, - // data() { - // return { count: 0 } - // }, - // expose: ['count'], - // unmounted: unmountSpy, - // render: compileToFunction(`
{{ count }}
`), - // } - // createRecord(childId, Child) - - // const Parent: ComponentOptions = { - // setup() { - // const com1 = ref() - // const changeRef1 = (value: any) => (com1.value = value) - - // const com2 = ref() - // const changeRef2 = (value: any) => (com2.value = value) - - // return () => [ - // h(Child, { ref: changeRef1 }), - // h(Child, { ref: changeRef2 }), - // com1.value?.count, - // ] - // }, - // } - - // render(h(Parent), root) - // await nextTick() - // expect(serializeInner(root)).toBe(`
0
0
0`) - - // reload(childId, { - // __hmrId: childId, - // data() { - // return { count: 1 } - // }, - // mounted: mountSpy, - // render: compileToFunction(`
{{ count }}
`), - // }) - // await nextTick() - // expect(serializeInner(root)).toBe(`
1
1
1`) - // expect(unmountSpy).toHaveBeenCalledTimes(2) - // expect(mountSpy).toHaveBeenCalledTimes(2) - // }) + // TODO: renderEffect not re-run after child reload + // it requires parent rerender to align with vdom + test.todo('reload: avoid infinite recursion', async () => { + const root = document.createElement('div') + document.body.appendChild(root) + const childId = 'test-child-6930' + const unmountSpy = vi.fn() + const mountSpy = vi.fn() - // // #1156 - static nodes should retain DOM element reference across updates - // // when HMR is active - // test('static el reference', async () => { - // const root = nodeOps.createElement('div') - // const id = 'test-static-el' + const Child = defineVaporComponent({ + __hmrId: childId, + setup(_, { expose }) { + const count = ref(0) + expose({ + count, + }) + onUnmounted(unmountSpy) + return { count } + }, + render: compileToFunction(`
{{ count }}
`), + }) + createRecord(childId, Child as any) - // const template = `
- //
{{ count }}
- // - //
` + const Parent = defineVaporComponent({ + setup() { + const com1 = ref() + const changeRef1 = (value: any) => (com1.value = value) + const com2 = ref() + const changeRef2 = (value: any) => (com2.value = value) + const setRef = createTemplateRefSetter() + const n0 = createComponent(Child) + setRef(n0, changeRef1) + const n1 = createComponent(Child) + setRef(n1, changeRef2) + const n2 = template(' ')() as any + renderEffect(() => { + setText(n2, toDisplayString(com1.value.count)) + }) + return [n0, n1, n2] + }, + }) - // const Comp: ComponentOptions = { - // __hmrId: id, - // data() { - // return { count: 0 } - // }, - // render: compileToFunction(template), - // } - // createRecord(id, Comp) + define(Parent).create().mount(root) + await nextTick() + expect(root.innerHTML).toBe(`
0
0
0`) - // render(h(Comp), root) - // expect(serializeInner(root)).toBe( - // `
0
`, - // ) + reload(childId, { + __hmrId: childId, + __vapor: true, + setup() { + onMounted(mountSpy) + const count = ref(1) + return { count } + }, + render: compileToFunction(`
{{ count }}
`), + }) + await nextTick() + expect(root.innerHTML).toBe(`
1
1
1`) + expect(unmountSpy).toHaveBeenCalledTimes(2) + expect(mountSpy).toHaveBeenCalledTimes(2) + }) - // // 1. click to trigger update - // triggerEvent((root as any).children[0].children[1], 'click') - // await nextTick() - // expect(serializeInner(root)).toBe( - // `
1
`, - // ) - - // // 2. trigger HMR - // rerender( - // id, - // compileToFunction(template.replace(`
1
`, - // ) - // }) + test('static el reference', async () => { + const root = document.createElement('div') + document.body.appendChild(root) + const id = 'test-static-el' - // // #1157 - component should force full props update when HMR is active - // test('force update child component w/ static props', () => { - // const root = nodeOps.createElement('div') - // const parentId = 'test-force-props-parent' - // const childId = 'test-force-props-child' + const template = `
+
{{ count }}
+ +
` - // const Child: ComponentOptions = { - // __hmrId: childId, - // props: { - // msg: String, - // }, - // render: compileToFunction(`
{{ msg }}
`), - // } - // createRecord(childId, Child) + const Comp = defineVaporComponent({ + __hmrId: id, + setup() { + const count = ref(0) + return { count } + }, + render: compileToFunction(template), + }) + createRecord(id, Comp as any) - // const Parent: ComponentOptions = { - // __hmrId: parentId, - // components: { Child }, - // render: compileToFunction(``), - // } - // createRecord(parentId, Parent) + define(Comp).create().mount(root) + expect(root.innerHTML).toBe(`
0
`) - // render(h(Parent), root) - // expect(serializeInner(root)).toBe(`
foo
`) + // 1. click to trigger update + triggerEvent('click', root.children[0].children[1] as Element) + await nextTick() + expect(root.innerHTML).toBe(`
1
`) - // rerender(parentId, compileToFunction(``)) - // expect(serializeInner(root)).toBe(`
bar
`) - // }) + // 2. trigger HMR + rerender( + id, + compileToFunction(template.replace(`
1
`, + ) + }) - // // #1305 - component should remove class - // test('remove static class from parent', () => { - // const root = nodeOps.createElement('div') - // const parentId = 'test-force-class-parent' - // const childId = 'test-force-class-child' + test('force update child component w/ static props', () => { + const root = document.createElement('div') + const parentId = 'test-force-props-parent' + const childId = 'test-force-props-child' - // const Child: ComponentOptions = { - // __hmrId: childId, - // render: compileToFunction(`
child
`), - // } - // createRecord(childId, Child) + const Child = defineVaporComponent({ + __hmrId: childId, + props: { + msg: String, + }, + render: compileToFunction(`
{{ msg }}
`, { + bindingMetadata: { + msg: BindingTypes.PROPS, + }, + }), + }) + createRecord(childId, Child as any) - // const Parent: ComponentOptions = { - // __hmrId: parentId, - // components: { Child }, - // render: compileToFunction(``), - // } - // createRecord(parentId, Parent) + const Parent = defineVaporComponent({ + __hmrId: parentId, + // @ts-expect-error + components: { Child }, + render: compileToFunction(``), + }) + createRecord(parentId, Parent as any) - // render(h(Parent), root) - // expect(serializeInner(root)).toBe(`
child
`) + define(Parent).create().mount(root) + expect(root.innerHTML).toBe(`
foo
`) - // rerender(parentId, compileToFunction(``)) - // expect(serializeInner(root)).toBe(`
child
`) - // }) + rerender(parentId, compileToFunction(``)) + expect(root.innerHTML).toBe(`
bar
`) + }) - // test('rerender if any parent in the parent chain', () => { - // const root = nodeOps.createElement('div') - // const parent = 'test-force-props-parent-' - // const childId = 'test-force-props-child' + test('remove static class from parent', () => { + const root = document.createElement('div') + const parentId = 'test-force-class-parent' + const childId = 'test-force-class-child' - // const numberOfParents = 5 + const Child = defineVaporComponent({ + __hmrId: childId, + render: compileToFunction(`
child
`), + }) + createRecord(childId, Child as any) - // const Child: ComponentOptions = { - // __hmrId: childId, - // render: compileToFunction(`
child
`), - // } - // createRecord(childId, Child) - - // const components: ComponentOptions[] = [] - - // for (let i = 0; i < numberOfParents; i++) { - // const parentId = `${parent}${i}` - // const parentComp: ComponentOptions = { - // __hmrId: parentId, - // } - // components.push(parentComp) - // if (i === 0) { - // parentComp.render = compileToFunction(``) - // parentComp.components = { - // Child, - // } - // } else { - // parentComp.render = compileToFunction(``) - // parentComp.components = { - // Parent: components[i - 1], - // } - // } - - // createRecord(parentId, parentComp) - // } + const Parent = defineVaporComponent({ + __hmrId: parentId, + // @ts-expect-error + components: { Child }, + render: compileToFunction(``), + }) + createRecord(parentId, Parent as any) - // const last = components[components.length - 1] + define(Parent).create().mount(root) + expect(root.innerHTML).toBe(`
child
`) - // render(h(last), root) - // expect(serializeInner(root)).toBe(`
child
`) + rerender(parentId, compileToFunction(``)) + expect(root.innerHTML).toBe(`
child
`) + }) - // rerender(last.__hmrId!, compileToFunction(``)) - // expect(serializeInner(root)).toBe(`
child
`) - // }) + test('rerender if any parent in the parent chain', () => { + const root = document.createElement('div') + const parent = 'test-force-props-parent-' + const childId = 'test-force-props-child' - // // #3302 - // test('rerender with Teleport', () => { - // const root = nodeOps.createElement('div') - // const target = nodeOps.createElement('div') - // const parentId = 'parent-teleport' + const numberOfParents = 5 - // const Child: ComponentOptions = { - // data() { - // return { - // // style is used to ensure that the div tag will be tracked by Teleport - // style: {}, - // target, - // } - // }, - // render: compileToFunction(` - // - //
- // - //
- //
- // `), - // } + const Child = defineVaporComponent({ + __hmrId: childId, + render: compileToFunction(`
child
`), + }) + createRecord(childId, Child as any) - // const Parent: ComponentOptions = { - // __hmrId: parentId, - // components: { Child }, - // render: compileToFunction(` - // - // - // - // `), - // } - // createRecord(parentId, Parent) - - // render(h(Parent), root) - // expect(serializeInner(root)).toBe( - // ``, - // ) - // expect(serializeInner(target)).toBe(`
1
`) - - // rerender( - // parentId, - // compileToFunction(` - // - // - // - // `), - // ) - // expect(serializeInner(root)).toBe( - // ``, - // ) - // expect(serializeInner(target)).toBe( - // `
1
2
`, - // ) - // }) + const components: VaporComponent[] = [] + + for (let i = 0; i < numberOfParents; i++) { + const parentId = `${parent}${i}` + const parentComp: VaporComponent = { + __vapor: true, + __hmrId: parentId, + } + components.push(parentComp) + if (i === 0) { + parentComp.render = compileToFunction(``) + // @ts-expect-error + parentComp.components = { + Child, + } + } else { + parentComp.render = compileToFunction(``) + // @ts-expect-error + parentComp.components = { + Parent: components[i - 1], + } + } - // // #4174 - // test('with global mixins', async () => { - // const childId = 'hmr-global-mixin' - // const createSpy1 = vi.fn() - // const createSpy2 = vi.fn() + createRecord(parentId, parentComp as any) + } - // const Child: ComponentOptions = { - // __hmrId: childId, - // created: createSpy1, - // render() { - // return h('div') - // }, - // } - // createRecord(childId, Child) + const last = components[components.length - 1] - // const Parent: ComponentOptions = { - // render: () => h(Child), - // } + define(last).create().mount(root) + expect(root.innerHTML).toBe(`
child
`) - // const app = createApp(Parent) - // app.mixin({}) + rerender(last.__hmrId!, compileToFunction(``)) + expect(root.innerHTML).toBe(`
child
`) + }) - // const root = nodeOps.createElement('div') - // app.mount(root) - // expect(createSpy1).toHaveBeenCalledTimes(1) - // expect(createSpy2).toHaveBeenCalledTimes(0) + test('rerender with Teleport', () => { + const root = document.createElement('div') + const target = document.createElement('div') + document.body.appendChild(root) + document.body.appendChild(target) + const parentId = 'parent-teleport' - // reload(childId, { - // __hmrId: childId, - // created: createSpy2, - // render() { - // return h('div') - // }, - // }) - // await nextTick() - // expect(createSpy1).toHaveBeenCalledTimes(1) - // expect(createSpy2).toHaveBeenCalledTimes(1) - // }) + const Child = defineVaporComponent({ + setup() { + return { target } + }, + render: compileToFunction(` + +
+ +
+
+ `), + }) - // // #4757 - // test('rerender for component that has no active instance yet', () => { - // const id = 'no-active-instance-rerender' - // const Foo: ComponentOptions = { - // __hmrId: id, - // render: () => 'foo', - // } + const Parent = { + __vapor: true, + __hmrId: parentId, + components: { Child }, + render: compileToFunction(` + + + + `), + } + createRecord(parentId, Parent as any) - // createRecord(id, Foo) - // rerender(id, () => 'bar') + define(Parent).create().mount(root) + expect(root.innerHTML).toBe(``) + expect(target.innerHTML).toBe(`
1
`) - // const root = nodeOps.createElement('div') - // render(h(Foo), root) - // expect(serializeInner(root)).toBe('bar') - // }) + rerender( + parentId, + compileToFunction(` + + + + `), + ) + expect(root.innerHTML).toBe(``) + expect(target.innerHTML).toBe( + `
1
2
`, + ) + }) - // test('reload for component that has no active instance yet', () => { - // const id = 'no-active-instance-reload' - // const Foo: ComponentOptions = { - // __hmrId: id, - // render: () => 'foo', - // } + test('rerender for component that has no active instance yet', () => { + const id = 'no-active-instance-rerender' + const Foo = { + __vapor: true, + __hmrId: id, + render: () => template('foo')(), + } - // createRecord(id, Foo) - // reload(id, { - // __hmrId: id, - // render: () => 'bar', - // }) + createRecord(id, Foo) + rerender(id, () => template('bar')()) - // const root = nodeOps.createElement('div') - // render(h(Foo), root) - // expect(serializeInner(root)).toBe('bar') - // }) + const root = document.createElement('div') + define(Foo).create().mount(root) + expect(root.innerHTML).toBe('bar') + }) - // // #7155 - force HMR on slots content update - // test('force update slot content change', () => { - // const root = nodeOps.createElement('div') - // const parentId = 'test-force-computed-parent' - // const childId = 'test-force-computed-child' + test('reload for component that has no active instance yet', () => { + const id = 'no-active-instance-reload' + const Foo = { + __vapor: true, + __hmrId: id, + render: () => template('foo')(), + } + + createRecord(id, Foo) + reload(id, { + __hmrId: id, + render: () => template('bar')(), + }) - // const Child: ComponentOptions = { - // __hmrId: childId, - // computed: { - // slotContent() { - // return this.$slots.default?.() - // }, - // }, - // render: compileToFunction(``), - // } - // createRecord(childId, Child) + const root = document.createElement('div') + define(Foo).render({}, root) + expect(root.innerHTML).toBe('bar') + }) - // const Parent: ComponentOptions = { - // __hmrId: parentId, - // components: { Child }, - // render: compileToFunction(`1`), - // } - // createRecord(parentId, Parent) + test('force update slot content change', () => { + const root = document.createElement('div') + const parentId = 'test-force-computed-parent' + const childId = 'test-force-computed-child' - // render(h(Parent), root) - // expect(serializeInner(root)).toBe(`1`) + const Child = { + __vapor: true, + __hmrId: childId, + setup(_: any, { slots }: any) { + const slotContent = computed(() => { + return slots.default?.() + }) + return { slotContent } + }, + render: compileToFunction(``), + } + createRecord(childId, Child) - // rerender(parentId, compileToFunction(`2`)) - // expect(serializeInner(root)).toBe(`2`) - // }) + const Parent = { + __vapor: true, + __hmrId: parentId, + components: { Child }, + render: compileToFunction(`1`), + } + createRecord(parentId, Parent) - // // #6978, #7138, #7114 - // test('hoisted children array inside v-for', () => { - // const root = nodeOps.createElement('div') - // const appId = 'test-app-id' - // const App: ComponentOptions = { - // __hmrId: appId, - // render: compileToFunction( - // `
- //
1
- //
- //

2

- //

3

`, - // ), - // } - // createRecord(appId, App) + // render(h(Parent), root) + define(Parent).render({}, root) + expect(root.innerHTML).toBe(`1`) - // render(h(App), root) - // expect(serializeInner(root)).toBe( - // `
1
1

2

3

`, - // ) - - // // move the

3

into the
1
- // rerender( - // appId, - // compileToFunction( - // `
- //
1

3

- //
- //

2

`, - // ), - // ) - // expect(serializeInner(root)).toBe( - // `
1

3

1

3

2

`, - // ) - // }) + rerender(parentId, compileToFunction(`2`)) + expect(root.innerHTML).toBe(`2`) + }) // // #11248 // test('reload async component with multiple instances', async () => { From 63c51066fdf2b383626db7f8b5546cf6f8fdb31a Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 21 Nov 2025 17:39:16 +0800 Subject: [PATCH 11/11] test: add more tests --- packages/runtime-core/src/hmr.ts | 2 +- packages/runtime-vapor/__tests__/hmr.spec.ts | 481 ++++++++++--------- 2 files changed, 255 insertions(+), 228 deletions(-) diff --git a/packages/runtime-core/src/hmr.ts b/packages/runtime-core/src/hmr.ts index 8a0ed8746e2..1d35bd34692 100644 --- a/packages/runtime-core/src/hmr.ts +++ b/packages/runtime-core/src/hmr.ts @@ -97,7 +97,7 @@ function rerender(id: string, newRender?: Function): void { // this flag forces child components with slot content to update isHmrUpdating = true if (instance.vapor) { - instance.hmrRerender!() + if (!instance.isUnmounted) instance.hmrRerender!() } else { const i = instance as ComponentInternalInstance // #13771 don't update if the job is already disposed diff --git a/packages/runtime-vapor/__tests__/hmr.spec.ts b/packages/runtime-vapor/__tests__/hmr.spec.ts index c1902c9057a..23cb2049803 100644 --- a/packages/runtime-vapor/__tests__/hmr.spec.ts +++ b/packages/runtime-vapor/__tests__/hmr.spec.ts @@ -12,12 +12,15 @@ import { import { compileToVaporRender as compileToFunction, makeRender } from './_utils' import { createComponent, + createSlot, createTemplateRefSetter, + defineVaporAsyncComponent, defineVaporComponent, delegateEvents, renderEffect, setText, template, + withVaporCtx, } from '@vue/runtime-vapor' import { BindingTypes } from '@vue/compiler-core' import type { VaporComponent } from '../src/component' @@ -26,6 +29,7 @@ declare var __VUE_HMR_RUNTIME__: HMRRuntime const { createRecord, rerender, reload } = __VUE_HMR_RUNTIME__ const define = makeRender() +const timeout = (n: number = 0) => new Promise(r => setTimeout(r, n)) const triggerEvent = (type: string, el: Element) => { const event = new Event(type, { bubbles: true }) @@ -738,233 +742,256 @@ describe('hot module replacement', () => { expect(root.innerHTML).toBe(`2`) }) - // // #11248 - // test('reload async component with multiple instances', async () => { - // const root = nodeOps.createElement('div') - // const childId = 'test-child-id' - // const Child: ComponentOptions = { - // __hmrId: childId, - // data() { - // return { count: 0 } - // }, - // render: compileToFunction(`
{{ count }}
`), - // } - // const Comp = runtimeTest.defineAsyncComponent(() => Promise.resolve(Child)) - // const appId = 'test-app-id' - // const App: ComponentOptions = { - // __hmrId: appId, - // render: () => [h(Comp), h(Comp)], - // } - // createRecord(appId, App) - - // render(h(App), root) - - // await timeout() - - // expect(serializeInner(root)).toBe(`
0
0
`) - - // // change count to 1 - // reload(childId, { - // __hmrId: childId, - // data() { - // return { count: 1 } - // }, - // render: compileToFunction(`
{{ count }}
`), - // }) - - // await timeout() - - // expect(serializeInner(root)).toBe(`
1
1
`) - // }) - - // test('reload async child wrapped in Suspense + KeepAlive', async () => { - // const id = 'async-child-reload' - // const AsyncChild: ComponentOptions = { - // __hmrId: id, - // async setup() { - // await nextTick() - // return () => 'foo' - // }, - // } - // createRecord(id, AsyncChild) - - // const appId = 'test-app-id' - // const App: ComponentOptions = { - // __hmrId: appId, - // components: { AsyncChild }, - // render: compileToFunction(` - //
- // - // - // - // - // - //
- // `), - // } - - // const root = nodeOps.createElement('div') - // render(h(App), root) - // expect(serializeInner(root)).toBe('
') - // await timeout() - // expect(serializeInner(root)).toBe('
foo
') - - // reload(id, { - // __hmrId: id, - // async setup() { - // await nextTick() - // return () => 'bar' - // }, - // }) - // await timeout() - // expect(serializeInner(root)).toBe('
bar
') - // }) - - // test('multi reload child wrapped in Suspense + KeepAlive', async () => { - // const id = 'test-child-reload-3' - // const Child: ComponentOptions = { - // __hmrId: id, - // setup() { - // const count = ref(0) - // return { count } - // }, - // render: compileToFunction(`
{{ count }}
`), - // } - // createRecord(id, Child) - - // const appId = 'test-app-id' - // const App: ComponentOptions = { - // __hmrId: appId, - // components: { Child }, - // render: compileToFunction(` - // - // - // - // - // - // `), - // } - - // const root = nodeOps.createElement('div') - // render(h(App), root) - // expect(serializeInner(root)).toBe('
0
') - // await timeout() - // reload(id, { - // __hmrId: id, - // setup() { - // const count = ref(1) - // return { count } - // }, - // render: compileToFunction(`
{{ count }}
`), - // }) - // await timeout() - // expect(serializeInner(root)).toBe('
1
') - - // reload(id, { - // __hmrId: id, - // setup() { - // const count = ref(2) - // return { count } - // }, - // render: compileToFunction(`
{{ count }}
`), - // }) - // await timeout() - // expect(serializeInner(root)).toBe('
2
') - // }) - - // test('rerender for nested component', () => { - // const id = 'child-nested-rerender' - // const Foo: ComponentOptions = { - // __hmrId: id, - // render() { - // return this.$slots.default() - // }, - // } - // createRecord(id, Foo) - - // const parentId = 'parent-nested-rerender' - // const Parent: ComponentOptions = { - // __hmrId: parentId, - // render() { - // return h(Foo, null, { - // default: () => this.$slots.default(), - // _: 3 /* FORWARDED */, - // }) - // }, - // } - - // const appId = 'app-nested-rerender' - // const App: ComponentOptions = { - // __hmrId: appId, - // render: () => - // h(Parent, null, { - // default: () => [ - // h(Foo, null, { - // default: () => ['foo'], - // }), - // ], - // }), - // } - // createRecord(parentId, App) - - // const root = nodeOps.createElement('div') - // render(h(App), root) - // expect(serializeInner(root)).toBe('foo') - - // rerender(id, () => 'bar') - // expect(serializeInner(root)).toBe('bar') - // }) - - // // https://github.com/vitejs/vite-plugin-vue/issues/599 - // // Both Outer and Inner are reloaded when './server.js' changes - // test('reload nested components from single update', async () => { - // const innerId = 'nested-reload-inner' - // const outerId = 'nested-reload-outer' - - // let Inner = { - // __hmrId: innerId, - // render() { - // return h('div', 'foo') - // }, - // } - // let Outer = { - // __hmrId: outerId, - // render() { - // return h(Inner) - // }, - // } - - // createRecord(innerId, Inner) - // createRecord(outerId, Outer) - - // const App = { - // render: () => h(Outer), - // } - - // const root = nodeOps.createElement('div') - // render(h(App), root) - // expect(serializeInner(root)).toBe('
foo
') - - // Inner = { - // __hmrId: innerId, - // render() { - // return h('div', 'bar') - // }, - // } - // Outer = { - // __hmrId: outerId, - // render() { - // return h(Inner) - // }, - // } - - // // trigger reload for both Outer and Inner - // reload(outerId, Outer) - // reload(innerId, Inner) - // await nextTick() - - // expect(serializeInner(root)).toBe('
bar
') - // }) + // #11248 + test('reload async component with multiple instances', async () => { + const root = document.createElement('div') + const childId = 'test-child-id' + const Child = { + __vapor: true, + __hmrId: childId, + setup() { + const count = ref(0) + return { count } + }, + render: compileToFunction(`
{{ count }}
`), + } + const Comp = defineVaporAsyncComponent(() => Promise.resolve(Child)) + const appId = 'test-app-id' + const App = { + __hmrId: appId, + render() { + return [createComponent(Comp), createComponent(Comp)] + }, + } + createRecord(appId, App) + + define(App).render({}, root) + + await timeout() + + expect(root.innerHTML).toBe( + `
0
0
`, + ) + + // change count to 1 + reload(childId, { + __vapor: true, + __hmrId: childId, + setup() { + const count = ref(1) + return { count } + }, + render: compileToFunction(`
{{ count }}
`), + }) + + await timeout() + + expect(root.innerHTML).toBe( + `
1
1
`, + ) + }) + + test.todo('reload async child wrapped in Suspense + KeepAlive', async () => { + // const id = 'async-child-reload' + // const AsyncChild: ComponentOptions = { + // __hmrId: id, + // async setup() { + // await nextTick() + // return () => 'foo' + // }, + // } + // createRecord(id, AsyncChild) + // const appId = 'test-app-id' + // const App: ComponentOptions = { + // __hmrId: appId, + // components: { AsyncChild }, + // render: compileToFunction(` + //
+ // + // + // + // + // + //
+ // `), + // } + // const root = nodeOps.createElement('div') + // render(h(App), root) + // expect(serializeInner(root)).toBe('
') + // await timeout() + // expect(serializeInner(root)).toBe('
foo
') + // reload(id, { + // __hmrId: id, + // async setup() { + // await nextTick() + // return () => 'bar' + // }, + // }) + // await timeout() + // expect(serializeInner(root)).toBe('
bar
') + }) + + test.todo('multi reload child wrapped in Suspense + KeepAlive', async () => { + // const id = 'test-child-reload-3' + // const Child: ComponentOptions = { + // __hmrId: id, + // setup() { + // const count = ref(0) + // return { count } + // }, + // render: compileToFunction(`
{{ count }}
`), + // } + // createRecord(id, Child) + // const appId = 'test-app-id' + // const App: ComponentOptions = { + // __hmrId: appId, + // components: { Child }, + // render: compileToFunction(` + // + // + // + // + // + // `), + // } + // const root = nodeOps.createElement('div') + // render(h(App), root) + // expect(serializeInner(root)).toBe('
0
') + // await timeout() + // reload(id, { + // __hmrId: id, + // setup() { + // const count = ref(1) + // return { count } + // }, + // render: compileToFunction(`
{{ count }}
`), + // }) + // await timeout() + // expect(serializeInner(root)).toBe('
1
') + // reload(id, { + // __hmrId: id, + // setup() { + // const count = ref(2) + // return { count } + // }, + // render: compileToFunction(`
{{ count }}
`), + // }) + // await timeout() + // expect(serializeInner(root)).toBe('
2
') + }) + + test('rerender for nested component', () => { + const id = 'child-nested-rerender' + const Foo = { + __vapor: true, + __hmrId: id, + setup(_ctx: any, { slots }: any) { + return slots.default() + }, + } + createRecord(id, Foo) + + const parentId = 'parent-nested-rerender' + const Parent = { + __vapor: true, + __hmrId: parentId, + render() { + return createComponent( + Foo, + {}, + { + default: withVaporCtx(() => { + return createSlot('default') + }), + }, + ) + }, + } + + const appId = 'app-nested-rerender' + const App = { + __vapor: true, + __hmrId: appId, + render: () => + createComponent( + Parent, + {}, + { + default: withVaporCtx(() => { + return createComponent( + Foo, + {}, + { + default: () => template('foo')(), + }, + ) + }), + }, + ), + } + createRecord(parentId, App) + + const root = document.createElement('div') + define(App).render({}, root) + expect(root.innerHTML).toBe('foo') + + rerender(id, () => template('bar')()) + expect(root.innerHTML).toBe('bar') + }) + + test('reload nested components from single update', async () => { + const innerId = 'nested-reload-inner' + const outerId = 'nested-reload-outer' + + let Inner = { + __vapor: true, + __hmrId: innerId, + render() { + return template('
foo
')() + }, + } + let Outer = { + __vapor: true, + __hmrId: outerId, + render() { + return createComponent(Inner as any) + }, + } + + createRecord(innerId, Inner) + createRecord(outerId, Outer) + + const App = { + __vapor: true, + render: () => createComponent(Outer), + } + + const root = document.createElement('div') + define(App).render({}, root) + expect(root.innerHTML).toBe('
foo
') + + Inner = { + __vapor: true, + __hmrId: innerId, + render() { + return template('
bar
')() + }, + } + Outer = { + __vapor: true, + __hmrId: outerId, + render() { + return createComponent(Inner as any) + }, + } + + // trigger reload for both Outer and Inner + reload(outerId, Outer) + reload(innerId, Inner) + await nextTick() + + expect(root.innerHTML).toBe('
bar
') + }) test('child reload + parent reload', async () => { const root = document.createElement('div')