From a590cfc4b5621dae425daceb830a90566204043f Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 28 Oct 2025 15:59:58 +0800 Subject: [PATCH] fix: enhance HMR style handling and component style injection --- packages/runtime-core/src/hmr.ts | 11 +- packages/runtime-dom/src/apiCustomElement.ts | 4 +- .../__tests__/customElement.spec.ts | 227 +++++++++--------- packages/runtime-vapor/src/component.ts | 12 + 4 files changed, 139 insertions(+), 115 deletions(-) diff --git a/packages/runtime-core/src/hmr.ts b/packages/runtime-core/src/hmr.ts index c8029950e2..8a0ed8746e 100644 --- a/packages/runtime-core/src/hmr.ts +++ b/packages/runtime-core/src/hmr.ts @@ -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) } diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts index ede0987154..341ed48e3d 100644 --- a/packages/runtime-dom/src/apiCustomElement.ts +++ b/packages/runtime-dom/src/apiCustomElement.ts @@ -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() } } diff --git a/packages/runtime-vapor/__tests__/customElement.spec.ts b/packages/runtime-vapor/__tests__/customElement.spec.ts index 628e0fa0a4..6072da34a2 100644 --- a/packages/runtime-vapor/__tests__/customElement.spec.ts +++ b/packages/runtime-vapor/__tests__/customElement.spec.ts @@ -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 = `` - // 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('
hello
', true)() + }, + } as any) + const Foo = defineVaporCustomElement(def) + customElements.define('my-el-with-styles', Foo) + container.innerHTML = `` + 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 = `` - // 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 = `` + 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 = `` - // 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 = `` + // 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 = `` - // 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 = `` + // 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 () => { diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index df116b2f73..8073237b42 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -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`) } -- 2.47.3