]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(runtime-vapor): sync parent component block reference during HMR reload (#13866)
authoredison <daiwei521@126.com>
Wed, 24 Sep 2025 08:58:49 +0000 (16:58 +0800)
committerGitHub <noreply@github.com>
Wed, 24 Sep 2025 08:58:49 +0000 (16:58 +0800)
packages/runtime-vapor/__tests__/hmr.spec.ts [new file with mode: 0644]
packages/runtime-vapor/src/hmr.ts

diff --git a/packages/runtime-vapor/__tests__/hmr.spec.ts b/packages/runtime-vapor/__tests__/hmr.spec.ts
new file mode 100644 (file)
index 0000000..21ca611
--- /dev/null
@@ -0,0 +1,118 @@
+// 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,
+  createComponent,
+  renderEffect,
+  setText,
+  template,
+} from '@vue/runtime-vapor'
+
+declare var __VUE_HMR_RUNTIME__: HMRRuntime
+const { createRecord, reload } = __VUE_HMR_RUNTIME__
+
+const define = makeRender()
+
+describe('hot module replacement', () => {
+  test('child reload + parent reload', async () => {
+    const root = document.createElement('div')
+    const childId = 'test1-child-reload'
+    const parentId = 'test1-parent-reload'
+
+    const { component: Child } = define({
+      __hmrId: childId,
+      setup() {
+        const msg = ref('child')
+        return { msg }
+      },
+      render(ctx) {
+        const n0 = template(`<div> </div>`)()
+        const x0 = child(n0 as any)
+        renderEffect(() => setText(x0 as any, ctx.msg))
+        return [n0]
+      },
+    })
+    createRecord(childId, Child as any)
+
+    const { mount, component: Parent } = define({
+      __hmrId: parentId,
+      setup() {
+        const msg = ref('root')
+        return { msg }
+      },
+      render(ctx) {
+        const n0 = createComponent(Child)
+        const n1 = template(`<div> </div>`)()
+        const x0 = child(n1 as any)
+        renderEffect(() => setText(x0 as any, ctx.msg))
+        return [n0, n1]
+      },
+    }).create()
+    createRecord(parentId, Parent as any)
+    mount(root)
+
+    expect(root.innerHTML).toMatchInlineSnapshot(
+      `"<div>child</div><div>root</div>"`,
+    )
+
+    // reload child
+    reload(childId, {
+      __hmrId: childId,
+      __vapor: true,
+      setup() {
+        const msg = ref('child changed')
+        return { msg }
+      },
+      render(ctx: any) {
+        const n0 = template(`<div> </div>`)()
+        const x0 = child(n0 as any)
+        renderEffect(() => setText(x0 as any, ctx.msg))
+        return [n0]
+      },
+    })
+    expect(root.innerHTML).toMatchInlineSnapshot(
+      `"<div>child changed</div><div>root</div>"`,
+    )
+
+    // reload child again
+    reload(childId, {
+      __hmrId: childId,
+      __vapor: true,
+      setup() {
+        const msg = ref('child changed2')
+        return { msg }
+      },
+      render(ctx: any) {
+        const n0 = template(`<div> </div>`)()
+        const x0 = child(n0 as any)
+        renderEffect(() => setText(x0 as any, ctx.msg))
+        return [n0]
+      },
+    })
+    expect(root.innerHTML).toMatchInlineSnapshot(
+      `"<div>child changed2</div><div>root</div>"`,
+    )
+
+    // reload parent
+    reload(parentId, {
+      __hmrId: parentId,
+      __vapor: true,
+      setup() {
+        const msg = ref('root changed')
+        return { msg }
+      },
+      render(ctx: any) {
+        const n0 = createComponent(Child)
+        const n1 = template(`<div> </div>`)()
+        const x0 = child(n1 as any)
+        renderEffect(() => setText(x0 as any, ctx.msg))
+        return [n0, n1]
+      },
+    })
+    expect(root.innerHTML).toMatchInlineSnapshot(
+      `"<div>child changed2</div><div>root changed</div>"`,
+    )
+  })
+})
index c96c1afa13054e1230f9cef57a5fa9d9d04e5c19..1a88ec7dc22a659db7ae7e2227d239f21744927d 100644 (file)
@@ -12,6 +12,7 @@ import {
   mountComponent,
   unmountComponent,
 } from './component'
+import { isArray } from '@vue/shared'
 
 export function hmrRerender(instance: VaporComponentInstance): void {
   const normalized = normalizeBlock(instance.block)
@@ -34,7 +35,8 @@ export function hmrReload(
   const parent = normalized[0].parentNode!
   const anchor = normalized[normalized.length - 1].nextSibling
   unmountComponent(instance, parent)
-  const prev = setCurrentInstance(instance.parent)
+  const parentInstance = instance.parent as VaporComponentInstance | null
+  const prev = setCurrentInstance(parentInstance)
   const newInstance = createComponent(
     newComp,
     instance.rawProps,
@@ -43,4 +45,31 @@ export function hmrReload(
   )
   setCurrentInstance(...prev)
   mountComponent(newInstance, parent, anchor)
+
+  updateParentBlockOnHmrReload(parentInstance, instance, newInstance)
+}
+
+/**
+ * dev only
+ * update parentInstance.block to ensure that the correct parent and
+ * anchor are found during parentInstance HMR rerender/reload, as
+ * `normalizeBlock` relies on the current instance.block
+ */
+function updateParentBlockOnHmrReload(
+  parentInstance: VaporComponentInstance | null,
+  instance: VaporComponentInstance,
+  newInstance: VaporComponentInstance,
+): void {
+  if (parentInstance) {
+    if (parentInstance.block === instance) {
+      parentInstance.block = newInstance
+    } else if (isArray(parentInstance.block)) {
+      for (let i = 0; i < parentInstance.block.length; i++) {
+        if (parentInstance.block[i] === instance) {
+          parentInstance.block[i] = newInstance
+          break
+        }
+      }
+    }
+  }
 }