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-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__/_utils.ts b/packages/runtime-vapor/__tests__/_utils.ts index 1e056110320..746d94a776d 100644 --- a/packages/runtime-vapor/__tests__/_utils.ts +++ b/packages/runtime-vapor/__tests__/_utils.ts @@ -1,11 +1,19 @@ 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 { + type CompilerOptions, + compile as compileVapor, +} from '@vue/compiler-vapor' export interface RenderContext { component: VaporComponent @@ -187,6 +195,29 @@ 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 + .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 21ca611263e..23cb2049803 100644 --- a/packages/runtime-vapor/__tests__/hmr.spec.ts +++ b/packages/runtime-vapor/__tests__/hmr.spec.ts @@ -1,21 +1,998 @@ -// TODO: port tests from packages/runtime-core/__tests__/hmr.spec.ts - -import { type HMRRuntime, ref } from '@vue/runtime-dom' -import { makeRender } from './_utils' import { - child, + type HMRRuntime, + computed, + nextTick, + onActivated, + onDeactivated, + onMounted, + onUnmounted, + ref, + toDisplayString, +} from '@vue/runtime-dom' +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' declare var __VUE_HMR_RUNTIME__: HMRRuntime -const { createRecord, reload } = __VUE_HMR_RUNTIME__ +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 }) + 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: 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: compileToFunction( + `
{{ count }}{{ count }}
`, + ), + }) + createRecord(parentId, Parent as any) + + 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, + compileToFunction( + `
{{ count }}!{{ count }}
`, + ), + ) + expect(root.innerHTML).toBe(`
1!
1
`) + + // Should force child update on slot content change + rerender( + parentId, + compileToFunction( + `
{{ count }}!{{ count }}!
`, + ), + ) + expect(root.innerHTML).toBe(`
1!
1!
`) + + // Should force update element children despite block optimization + rerender( + parentId, + compileToFunction( + `
{{ count }}{{ count }} + {{ count }}! +
`, + ), + ) + expect(root.innerHTML).toBe( + `
11
1!
`, + ) + + // Should force update child slot elements + rerender( + parentId, + compileToFunction( + `
+ {{ count }} +
`, + ), + ) + expect(root.innerHTML).toBe( + `
1
`, + ) + }) + + test('reload', async () => { + 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('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() + 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, + __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('click', root.children[0] 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[0] as Element) + await nextTick() + expect(unmountSpy).toHaveBeenCalledTimes(1) + expect(mountSpy).toHaveBeenCalledTimes(1) + expect(activeSpy).toHaveBeenCalledTimes(2) + expect(deactivatedSpy).toHaveBeenCalledTimes(1) + }) + + 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 = 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, + __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('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('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 = 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 = defineVaporComponent({ + name: 'original', + __hmrId: childId, + setup() { + onUnmounted(unmountSpy) + const count = ref(0) + return { count } + }, + render: compileToFunction(`
{{ count }}
`), + }) + createRecord(childId, Child as any) + + const Parent = defineVaporComponent({ + // @ts-expect-error + components: { Child }, + setup() { + function onLeave(_: any, done: Function) { + setTimeout(done, 0) + } + const toggle = ref(true) + return { toggle, onLeave } + }, + render: compileToFunction( + `
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('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('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) + }) + + // 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() + + 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 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] + }, + }) + + define(Parent).create().mount(root) + await nextTick() + expect(root.innerHTML).toBe(`
0
0
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) + }) + + test('static el reference', async () => { + const root = document.createElement('div') + document.body.appendChild(root) + const id = 'test-static-el' + + const template = `
+
{{ count }}
+ +
` + + const Comp = defineVaporComponent({ + __hmrId: id, + setup() { + const count = ref(0) + return { count } + }, + render: compileToFunction(template), + }) + createRecord(id, Comp as any) + + define(Comp).create().mount(root) + expect(root.innerHTML).toBe(`
0
`) + + // 1. click to trigger update + triggerEvent('click', root.children[0].children[1] as Element) + await nextTick() + expect(root.innerHTML).toBe(`
1
`) + + // 2. trigger HMR + rerender( + id, + compileToFunction(template.replace(`
1
`, + ) + }) + + 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 = defineVaporComponent({ + __hmrId: childId, + props: { + msg: String, + }, + render: compileToFunction(`
{{ msg }}
`, { + bindingMetadata: { + msg: BindingTypes.PROPS, + }, + }), + }) + createRecord(childId, Child as any) + + const Parent = defineVaporComponent({ + __hmrId: parentId, + // @ts-expect-error + components: { Child }, + render: compileToFunction(``), + }) + createRecord(parentId, Parent as any) + + define(Parent).create().mount(root) + expect(root.innerHTML).toBe(`
foo
`) + + rerender(parentId, compileToFunction(``)) + expect(root.innerHTML).toBe(`
bar
`) + }) + + test('remove static class from parent', () => { + const root = document.createElement('div') + const parentId = 'test-force-class-parent' + const childId = 'test-force-class-child' + + const Child = defineVaporComponent({ + __hmrId: childId, + render: compileToFunction(`
child
`), + }) + createRecord(childId, Child as any) + + const Parent = defineVaporComponent({ + __hmrId: parentId, + // @ts-expect-error + components: { Child }, + render: compileToFunction(``), + }) + createRecord(parentId, Parent as any) + + define(Parent).create().mount(root) + expect(root.innerHTML).toBe(`
child
`) + + rerender(parentId, compileToFunction(``)) + expect(root.innerHTML).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' + + const numberOfParents = 5 + + const Child = defineVaporComponent({ + __hmrId: childId, + render: compileToFunction(`
child
`), + }) + createRecord(childId, Child as any) + + 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], + } + } + + createRecord(parentId, parentComp as any) + } + + const last = components[components.length - 1] + + define(last).create().mount(root) + expect(root.innerHTML).toBe(`
child
`) + + rerender(last.__hmrId!, compileToFunction(``)) + expect(root.innerHTML).toBe(`
child
`) + }) + + 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' + + const Child = defineVaporComponent({ + setup() { + return { target } + }, + render: compileToFunction(` + +
+ +
+
+ `), + }) + + const Parent = { + __vapor: true, + __hmrId: parentId, + components: { Child }, + render: compileToFunction(` + + + + `), + } + createRecord(parentId, Parent as any) + + define(Parent).create().mount(root) + expect(root.innerHTML).toBe(``) + expect(target.innerHTML).toBe(`
1
`) + + rerender( + parentId, + compileToFunction(` + + + + `), + ) + expect(root.innerHTML).toBe(``) + expect(target.innerHTML).toBe( + `
1
2
`, + ) + }) + + 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) + rerender(id, () => template('bar')()) + + const root = document.createElement('div') + define(Foo).create().mount(root) + expect(root.innerHTML).toBe('bar') + }) + + 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 root = document.createElement('div') + define(Foo).render({}, root) + expect(root.innerHTML).toBe('bar') + }) + + test('force update slot content change', () => { + const root = document.createElement('div') + const parentId = 'test-force-computed-parent' + const childId = 'test-force-computed-child' + + const Child = { + __vapor: true, + __hmrId: childId, + setup(_: any, { slots }: any) { + const slotContent = computed(() => { + return slots.default?.() + }) + return { slotContent } + }, + render: compileToFunction(``), + } + createRecord(childId, Child) + + const Parent = { + __vapor: true, + __hmrId: parentId, + components: { Child }, + render: compileToFunction(`1`), + } + createRecord(parentId, Parent) + + // render(h(Parent), root) + define(Parent).render({}, root) + expect(root.innerHTML).toBe(`1`) + + rerender(parentId, compileToFunction(`2`)) + expect(root.innerHTML).toBe(`2`) + }) + + // #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') const childId = 'test1-child-reload' @@ -27,28 +1004,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) @@ -65,12 +1033,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
"`, @@ -84,12 +1047,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
"`, @@ -99,17 +1057,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
"`, 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`) 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