]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix: enhance HMR style handling and component style injection
authordaiwei <daiwei521@126.com>
Tue, 28 Oct 2025 07:59:58 +0000 (15:59 +0800)
committerdaiwei <daiwei521@126.com>
Tue, 28 Oct 2025 07:59:58 +0000 (15:59 +0800)
packages/runtime-core/src/hmr.ts
packages/runtime-dom/src/apiCustomElement.ts
packages/runtime-vapor/__tests__/customElement.spec.ts
packages/runtime-vapor/src/component.ts

index c8029950e2d5f027362b36ff8ef91afc6858a1f5..8a0ed8746e27e7afcb4a48bf618ae09ba71d18db 100644 (file)
@@ -123,7 +123,16 @@ function reload(id: string, newComp: HMRComponent): void {
   // create a snapshot which avoids the set being mutated during updates
   const instances = [...record.instances]
 
-  if (newComp.__vapor) {
+  if (newComp.__vapor && !instances.some(i => i.ceReload)) {
+    // For multiple instances with the same __hmrId, remove styles first before reload
+    // to avoid the second instance's style removal deleting the first instance's
+    // newly added styles (since hmrReload is synchronous)
+    for (const instance of instances) {
+      // update custom element child style
+      if (instance.root && instance.root.ce && instance !== instance.root) {
+        instance.root.ce._removeChildStyle(instance.type)
+      }
+    }
     for (const instance of instances) {
       instance.hmrReload!(newComp)
     }
index ede098715463298bd7892d0ea28aa769d9201197..341ed48e3df7a91b69a2a2ee32a15fb59ea69079 100644 (file)
@@ -466,7 +466,9 @@ export abstract class VueElementBase<
           this._styles.length = 0
         }
         this._applyStyles(newStyles)
-        this._instance = null
+        if (!this._instance!.vapor) {
+          this._instance = null
+        }
         this._update()
       }
     }
index 628e0fa0a43c9914064020bedbdb2713e3958712..6072da34a248db6a1615a1c1b278eaaabdee7a7e 100644 (file)
@@ -11,6 +11,7 @@ import {
 } from '@vue/runtime-dom'
 import {
   child,
+  createComponent,
   createComponentWithFallback,
   createSlot,
   createVaporApp,
@@ -1007,128 +1008,128 @@ describe('defineVaporCustomElement', () => {
     })
   })
 
-  // describe('styles', () => {
-  //   function assertStyles(el: VaporElement, css: string[]) {
-  //     const styles = el.shadowRoot?.querySelectorAll('style')!
-  //     expect(styles.length).toBe(css.length) // should not duplicate multiple copies from Bar
-  //     for (let i = 0; i < css.length; i++) {
-  //       expect(styles[i].textContent).toBe(css[i])
-  //     }
-  //   }
-
-  //   test('should attach styles to shadow dom', async () => {
-  //     const def = defineVaporComponent({
-  //       __hmrId: 'foo',
-  //       styles: [`div { color: red; }`],
-  //       render() {
-  //         return h('div', 'hello')
-  //       },
-  //     })
-  //     const Foo = defineVaporCustomElement(def)
-  //     customElements.define('my-el-with-styles', Foo)
-  //     container.innerHTML = `<my-el-with-styles></my-el-with-styles>`
-  //     const el = container.childNodes[0] as VaporElement
-  //     const style = el.shadowRoot?.querySelector('style')!
-  //     expect(style.textContent).toBe(`div { color: red; }`)
-
-  //     // hmr
-  //     __VUE_HMR_RUNTIME__.reload('foo', {
-  //       ...def,
-  //       styles: [`div { color: blue; }`, `div { color: yellow; }`],
-  //     } as any)
+  describe('styles', () => {
+    function assertStyles(el: VaporElement, css: string[]) {
+      const styles = el.shadowRoot?.querySelectorAll('style')!
+      expect(styles.length).toBe(css.length) // should not duplicate multiple copies from Bar
+      for (let i = 0; i < css.length; i++) {
+        expect(styles[i].textContent).toBe(css[i])
+      }
+    }
 
-  //     await nextTick()
-  //     assertStyles(el, [`div { color: blue; }`, `div { color: yellow; }`])
-  //   })
+    test('should attach styles to shadow dom', async () => {
+      const def = defineVaporComponent({
+        __hmrId: 'foo',
+        styles: [`div { color: red; }`],
+        setup() {
+          return template('<div>hello</div>', true)()
+        },
+      } as any)
+      const Foo = defineVaporCustomElement(def)
+      customElements.define('my-el-with-styles', Foo)
+      container.innerHTML = `<my-el-with-styles></my-el-with-styles>`
+      const el = container.childNodes[0] as VaporElement
+      const style = el.shadowRoot?.querySelector('style')!
+      expect(style.textContent).toBe(`div { color: red; }`)
+
+      // hmr
+      __VUE_HMR_RUNTIME__.reload('foo', {
+        ...def,
+        styles: [`div { color: blue; }`, `div { color: yellow; }`],
+      } as any)
 
-  //   test("child components should inject styles to root element's shadow root", async () => {
-  //     const Baz = () => h(Bar)
-  //     const Bar = defineVaporComponent({
-  //       __hmrId: 'bar',
-  //       styles: [`div { color: green; }`, `div { color: blue; }`],
-  //       render() {
-  //         return 'bar'
-  //       },
-  //     })
-  //     const Foo = defineVaporCustomElement({
-  //       styles: [`div { color: red; }`],
-  //       render() {
-  //         return [h(Baz), h(Baz)]
-  //       },
-  //     })
-  //     customElements.define('my-el-with-child-styles', Foo)
-  //     container.innerHTML = `<my-el-with-child-styles></my-el-with-child-styles>`
-  //     const el = container.childNodes[0] as VaporElement
+      await nextTick()
+      assertStyles(el, [`div { color: blue; }`, `div { color: yellow; }`])
+    })
 
-  //     // inject order should be child -> parent
-  //     assertStyles(el, [
-  //       `div { color: green; }`,
-  //       `div { color: blue; }`,
-  //       `div { color: red; }`,
-  //     ])
+    test("child components should inject styles to root element's shadow root", async () => {
+      const Baz = () => createComponent(Bar)
+      const Bar = defineVaporComponent({
+        __hmrId: 'bar',
+        styles: [`div { color: green; }`, `div { color: blue; }`],
+        setup() {
+          return template('bar')()
+        },
+      } as any)
+      const Foo = defineVaporCustomElement({
+        styles: [`div { color: red; }`],
+        setup() {
+          return [createComponent(Baz), createComponent(Baz)]
+        },
+      })
+      customElements.define('my-el-with-child-styles', Foo)
+      container.innerHTML = `<my-el-with-child-styles></my-el-with-child-styles>`
+      const el = container.childNodes[0] as VaporElement
+
+      // inject order should be child -> parent
+      assertStyles(el, [
+        `div { color: green; }`,
+        `div { color: blue; }`,
+        `div { color: red; }`,
+      ])
 
-  //     // hmr
-  //     __VUE_HMR_RUNTIME__.reload(Bar.__hmrId!, {
-  //       ...Bar,
-  //       styles: [`div { color: red; }`, `div { color: yellow; }`],
-  //     } as any)
+      // hmr
+      __VUE_HMR_RUNTIME__.reload(Bar.__hmrId!, {
+        ...Bar,
+        styles: [`div { color: red; }`, `div { color: yellow; }`],
+      } as any)
 
-  //     await nextTick()
-  //     assertStyles(el, [
-  //       `div { color: red; }`,
-  //       `div { color: yellow; }`,
-  //       `div { color: red; }`,
-  //     ])
+      await nextTick()
+      assertStyles(el, [
+        `div { color: red; }`,
+        `div { color: yellow; }`,
+        `div { color: red; }`,
+      ])
 
-  //     __VUE_HMR_RUNTIME__.reload(Bar.__hmrId!, {
-  //       ...Bar,
-  //       styles: [`div { color: blue; }`],
-  //     } as any)
-  //     await nextTick()
-  //     assertStyles(el, [`div { color: blue; }`, `div { color: red; }`])
-  //   })
+      __VUE_HMR_RUNTIME__.reload(Bar.__hmrId!, {
+        ...Bar,
+        styles: [`div { color: blue; }`],
+      } as any)
+      await nextTick()
+      assertStyles(el, [`div { color: blue; }`, `div { color: red; }`])
+    })
 
-  //   test("child components should not inject styles to root element's shadow root w/ shadowRoot false", async () => {
-  //     const Bar = defineVaporComponent({
-  //       styles: [`div { color: green; }`],
-  //       render() {
-  //         return 'bar'
-  //       },
-  //     })
-  //     const Baz = () => h(Bar)
-  //     const Foo = defineVaporCustomElement(
-  //       {
-  //         render() {
-  //           return [h(Baz)]
-  //         },
-  //       },
-  //       { shadowRoot: false },
-  //     )
+    // test("child components should not inject styles to root element's shadow root w/ shadowRoot false", async () => {
+    //   const Bar = defineVaporComponent({
+    //     styles: [`div { color: green; }`],
+    //     render() {
+    //       return 'bar'
+    //     },
+    //   })
+    //   const Baz = () => h(Bar)
+    //   const Foo = defineVaporCustomElement(
+    //     {
+    //       render() {
+    //         return [h(Baz)]
+    //       },
+    //     },
+    //     { shadowRoot: false },
+    //   )
 
-  //     customElements.define('my-foo-with-shadowroot-false', Foo)
-  //     container.innerHTML = `<my-foo-with-shadowroot-false></my-foo-with-shadowroot-false>`
-  //     const el = container.childNodes[0] as VaporElement
-  //     const style = el.shadowRoot?.querySelector('style')
-  //     expect(style).toBeUndefined()
-  //   })
+    //   customElements.define('my-foo-with-shadowroot-false', Foo)
+    //   container.innerHTML = `<my-foo-with-shadowroot-false></my-foo-with-shadowroot-false>`
+    //   const el = container.childNodes[0] as VaporElement
+    //   const style = el.shadowRoot?.querySelector('style')
+    //   expect(style).toBeUndefined()
+    // })
 
-  //   test('with nonce', () => {
-  //     const Foo = defineVaporCustomElement(
-  //       {
-  //         styles: [`div { color: red; }`],
-  //         render() {
-  //           return h('div', 'hello')
-  //         },
-  //       },
-  //       { nonce: 'xxx' },
-  //     )
-  //     customElements.define('my-el-with-nonce', Foo)
-  //     container.innerHTML = `<my-el-with-nonce></my-el-with-nonce>`
-  //     const el = container.childNodes[0] as VaporElement
-  //     const style = el.shadowRoot?.querySelector('style')!
-  //     expect(style.getAttribute('nonce')).toBe('xxx')
-  //   })
-  // })
+    // test('with nonce', () => {
+    //   const Foo = defineVaporCustomElement(
+    //     {
+    //       styles: [`div { color: red; }`],
+    //       render() {
+    //         return h('div', 'hello')
+    //       },
+    //     },
+    //     { nonce: 'xxx' },
+    //   )
+    //   customElements.define('my-el-with-nonce', Foo)
+    //   container.innerHTML = `<my-el-with-nonce></my-el-with-nonce>`
+    //   const el = container.childNodes[0] as VaporElement
+    //   const style = el.shadowRoot?.querySelector('style')!
+    //   expect(style.getAttribute('nonce')).toBe('xxx')
+    // })
+  })
 
   // describe('async', () => {
   //   test('should work', async () => {
index df116b2f73a0eadfe025c08c38883eb3d024f89d..8073237b427b5b6002139691c6b453775e580adc 100644 (file)
@@ -94,6 +94,7 @@ import {
   resetInsertionState,
 } from './insertionState'
 import { DynamicFragment } from './fragment'
+import type { VaporElement } from './apiDefineVaporCustomElement'
 
 export { currentInstance } from '@vue/runtime-dom'
 
@@ -706,6 +707,17 @@ export function mountComponent(
     return
   }
 
+  // custom element style injection
+  const { root, type } = instance as GenericComponentInstance
+  if (
+    root &&
+    root.ce &&
+    // @ts-expect-error _def is private
+    (root.ce as VaporElement)._def.shadowRoot !== false
+  ) {
+    root.ce!._injectChildStyle(type)
+  }
+
   if (__DEV__) {
     startMeasure(instance, `mount`)
   }