From b9910755a50c7d6c52b28c3aef20cf97810295c9 Mon Sep 17 00:00:00 2001 From: Adrian Cerbaro Date: Thu, 15 May 2025 21:07:32 -0300 Subject: [PATCH 01/94] =?UTF-8?q?fix(custom-element):=20allow=20injecting?= =?UTF-8?q?=20values=20=E2=80=8B=E2=80=8Bfrom=20app=20context=20in=20neste?= =?UTF-8?q?d=20elements=20(#13219)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit close #13212) --- packages/runtime-core/src/apiCreateApp.ts | 18 +++- packages/runtime-core/src/apiInject.ts | 6 +- .../__tests__/customElement.spec.ts | 95 +++++++++++++++++++ packages/runtime-dom/src/apiCustomElement.ts | 15 ++- 4 files changed, 126 insertions(+), 8 deletions(-) diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index cba5e4ede02..b69a1ccd540 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -22,7 +22,7 @@ import { warn } from './warning' import { type VNode, cloneVNode, createVNode } from './vnode' import type { RootHydrateFunction } from './hydration' import { devtoolsInitApp, devtoolsUnmountApp } from './devtools' -import { NO, extend, isFunction, isObject } from '@vue/shared' +import { NO, extend, hasOwn, isFunction, isObject } from '@vue/shared' import { version } from '.' import { installAppCompatProperties } from './compat/global' import type { NormalizedPropsOptions } from './componentProps' @@ -448,10 +448,18 @@ export function createAppAPI( provide(key, value) { if (__DEV__ && (key as string | symbol) in context.provides) { - warn( - `App already provides property with key "${String(key)}". ` + - `It will be overwritten with the new value.`, - ) + if (hasOwn(context.provides, key as string | symbol)) { + warn( + `App already provides property with key "${String(key)}". ` + + `It will be overwritten with the new value.`, + ) + } else { + // #13212, context.provides can inherit the provides object from parent on custom elements + warn( + `App already provides property with key "${String(key)}" inherited from its parent element. ` + + `It will be overwritten with the new value.`, + ) + } } context.provides[key as string | symbol] = value diff --git a/packages/runtime-core/src/apiInject.ts b/packages/runtime-core/src/apiInject.ts index f05d7333da6..711c5d84de8 100644 --- a/packages/runtime-core/src/apiInject.ts +++ b/packages/runtime-core/src/apiInject.ts @@ -59,10 +59,12 @@ export function inject( // to support `app.use` plugins, // fallback to appContext's `provides` if the instance is at root // #11488, in a nested createApp, prioritize using the provides from currentApp - const provides = currentApp + // #13212, for custom elements we must get injected values from its appContext + // as it already inherits the provides object from the parent element + let provides = currentApp ? currentApp._context.provides : instance - ? instance.parent == null + ? instance.parent == null || instance.ce ? instance.vnode.appContext && instance.vnode.appContext.provides : instance.parent.provides : undefined diff --git a/packages/runtime-dom/__tests__/customElement.spec.ts b/packages/runtime-dom/__tests__/customElement.spec.ts index df438d47eee..943dfdc51f7 100644 --- a/packages/runtime-dom/__tests__/customElement.spec.ts +++ b/packages/runtime-dom/__tests__/customElement.spec.ts @@ -708,6 +708,101 @@ describe('defineCustomElement', () => { `
changedA! changedB!
`, ) }) + + // #13212 + test('inherited from app context within nested elements', async () => { + const outerValues: (string | undefined)[] = [] + const innerValues: (string | undefined)[] = [] + const innerChildValues: (string | undefined)[] = [] + + const Outer = defineCustomElement( + { + setup() { + outerValues.push( + inject('shared'), + inject('outer'), + inject('inner'), + ) + }, + render() { + return h('div', [renderSlot(this.$slots, 'default')]) + }, + }, + { + configureApp(app) { + app.provide('shared', 'shared') + app.provide('outer', 'outer') + }, + }, + ) + + const Inner = defineCustomElement( + { + setup() { + // ensure values are not self-injected + provide('inner', 'inner-child') + + innerValues.push( + inject('shared'), + inject('outer'), + inject('inner'), + ) + }, + render() { + return h('div', [renderSlot(this.$slots, 'default')]) + }, + }, + { + configureApp(app) { + app.provide('outer', 'override-outer') + app.provide('inner', 'inner') + }, + }, + ) + + const InnerChild = defineCustomElement({ + setup() { + innerChildValues.push( + inject('shared'), + inject('outer'), + inject('inner'), + ) + }, + render() { + return h('div') + }, + }) + + customElements.define('provide-from-app-outer', Outer) + customElements.define('provide-from-app-inner', Inner) + customElements.define('provide-from-app-inner-child', InnerChild) + + container.innerHTML = + '' + + '' + + '' + + '' + + '' + + const outer = container.childNodes[0] as VueElement + expect(outer.shadowRoot!.innerHTML).toBe('
') + + expect('[Vue warn]: injection "inner" not found.').toHaveBeenWarnedTimes( + 1, + ) + expect( + '[Vue warn]: App already provides property with key "outer" inherited from its parent element. ' + + 'It will be overwritten with the new value.', + ).toHaveBeenWarnedTimes(1) + + expect(outerValues).toEqual(['shared', 'outer', undefined]) + expect(innerValues).toEqual(['shared', 'override-outer', 'inner']) + expect(innerChildValues).toEqual([ + 'shared', + 'override-outer', + 'inner-child', + ]) + }) }) describe('styles', () => { diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts index aeeaeec9b9f..cd21d0d1ce1 100644 --- a/packages/runtime-dom/src/apiCustomElement.ts +++ b/packages/runtime-dom/src/apiCustomElement.ts @@ -316,7 +316,18 @@ export class VueElement private _setParent(parent = this._parent) { if (parent) { this._instance!.parent = parent._instance - this._instance!.provides = parent._instance!.provides + this._inheritParentContext(parent) + } + } + + private _inheritParentContext(parent = this._parent) { + // #13212, the provides object of the app context must inherit the provides + // object from the parent element so we can inject values from both places + if (parent && this._app) { + Object.setPrototypeOf( + this._app._context.provides, + parent._instance!.provides, + ) } } @@ -417,6 +428,8 @@ export class VueElement def.name = 'VueElement' } this._app = this._createApp(def) + // inherit before configureApp to detect context overwrites + this._inheritParentContext() if (def.configureApp) { def.configureApp(this._app) } From 80055fddfb3ca1e2a44f19c7f0ffaeba00de5140 Mon Sep 17 00:00:00 2001 From: edison Date: Fri, 16 May 2025 08:22:01 +0800 Subject: [PATCH 02/94] fix(hydration): skip lazy hydration for patched components (#13283) close #13255 --- .../runtime-core/src/apiAsyncComponent.ts | 16 +++++++++- .../__tests__/e2e/hydration-strat-media.html | 10 +++++-- .../__tests__/e2e/hydrationStrategies.spec.ts | 30 +++++++++++++++++++ 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/packages/runtime-core/src/apiAsyncComponent.ts b/packages/runtime-core/src/apiAsyncComponent.ts index 199b451f66f..cb675f06e43 100644 --- a/packages/runtime-core/src/apiAsyncComponent.ts +++ b/packages/runtime-core/src/apiAsyncComponent.ts @@ -4,6 +4,7 @@ import { type ComponentOptions, type ConcreteComponent, currentInstance, + getComponentName, isInSSRComponentSetup, } from './component' import { isFunction, isObject } from '@vue/shared' @@ -121,14 +122,27 @@ export function defineAsyncComponent< __asyncLoader: load, __asyncHydrate(el, instance, hydrate) { + let patched = false const doHydrate = hydrateStrategy ? () => { - const teardown = hydrateStrategy(hydrate, cb => + const performHydrate = () => { + // skip hydration if the component has been patched + if (__DEV__ && patched) { + warn( + `Skipping lazy hydration for component '${getComponentName(resolvedComp!)}': ` + + `it was updated before lazy hydration performed.`, + ) + return + } + hydrate() + } + const teardown = hydrateStrategy(performHydrate, cb => forEachElement(el, cb), ) if (teardown) { ;(instance.bum || (instance.bum = [])).push(teardown) } + ;(instance.u || (instance.u = [])).push(() => (patched = true)) } : hydrate if (resolvedComp) { diff --git a/packages/vue/__tests__/e2e/hydration-strat-media.html b/packages/vue/__tests__/e2e/hydration-strat-media.html index c04cdb2a783..954a73d0467 100644 --- a/packages/vue/__tests__/e2e/hydration-strat-media.html +++ b/packages/vue/__tests__/e2e/hydration-strat-media.html @@ -15,13 +15,17 @@ } = Vue const Comp = { - setup() { + props: { + value: Boolean, + }, + setup(props) { const count = ref(0) onMounted(() => { console.log('hydrated') window.isHydrated = true }) return () => { + props.value return h('button', { onClick: () => count.value++ }, count.value) } }, @@ -37,7 +41,9 @@ onMounted(() => { window.isRootMounted = true }) - return () => h(AsyncComp) + + const show = (window.show = ref(true)) + return () => h(AsyncComp, { value: show.value }) }, }).mount('#app') diff --git a/packages/vue/__tests__/e2e/hydrationStrategies.spec.ts b/packages/vue/__tests__/e2e/hydrationStrategies.spec.ts index 69934d9591e..d792edf1960 100644 --- a/packages/vue/__tests__/e2e/hydrationStrategies.spec.ts +++ b/packages/vue/__tests__/e2e/hydrationStrategies.spec.ts @@ -86,6 +86,36 @@ describe('async component hydration strategies', () => { await assertHydrationSuccess() }) + // #13255 + test('media query (patched before hydration)', async () => { + const spy = vi.fn() + const currentPage = page() + currentPage.on('pageerror', spy) + + const warn: any[] = [] + currentPage.on('console', e => warn.push(e.text())) + + await goToCase('media') + await page().waitForFunction(() => window.isRootMounted) + expect(await page().evaluate(() => window.isHydrated)).toBe(false) + + // patch + await page().evaluate(() => (window.show.value = false)) + await click('button') + expect(await text('button')).toBe('1') + + // resize + await page().setViewport({ width: 400, height: 600 }) + await page().waitForFunction(() => window.isHydrated) + await assertHydrationSuccess('2') + + expect(spy).toBeCalledTimes(0) + currentPage.off('pageerror', spy) + expect( + warn.some(w => w.includes('Skipping lazy hydration for component')), + ).toBe(true) + }) + test('interaction', async () => { await goToCase('interaction') await page().waitForFunction(() => window.isRootMounted) From d37a2ac59d904ac0e3257ba552b6c04920a363f0 Mon Sep 17 00:00:00 2001 From: edison Date: Fri, 16 May 2025 08:22:37 +0800 Subject: [PATCH 03/94] fix(compiler-core): ensure mapping is added only if node source is available (#13285) close #13261 close vitejs/vite-plugin-vue#368 --- packages/compiler-core/src/codegen.ts | 6 ++-- .../__tests__/compileTemplate.spec.ts | 29 +++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts index 70116cfb61a..6b4559fabb2 100644 --- a/packages/compiler-core/src/codegen.ts +++ b/packages/compiler-core/src/codegen.ts @@ -188,7 +188,9 @@ function createCodegenContext( name = content } } - addMapping(node.loc.start, name) + if (node.loc.source) { + addMapping(node.loc.start, name) + } } if (newlineIndex === NewlineType.Unknown) { // multiple newlines, full iteration @@ -225,7 +227,7 @@ function createCodegenContext( context.column = code.length - newlineIndex } } - if (node && node.loc !== locStub) { + if (node && node.loc !== locStub && node.loc.source) { addMapping(node.loc.end) } } diff --git a/packages/compiler-sfc/__tests__/compileTemplate.spec.ts b/packages/compiler-sfc/__tests__/compileTemplate.spec.ts index 2ea1eb9d378..22623299a02 100644 --- a/packages/compiler-sfc/__tests__/compileTemplate.spec.ts +++ b/packages/compiler-sfc/__tests__/compileTemplate.spec.ts @@ -157,6 +157,35 @@ test('source map', () => { ).toMatchObject(getPositionInCode(template.content, `foobar`)) }) +test('source map: v-if generated comment should not have original position', () => { + const template = parse( + ` + + `, + { filename: 'example.vue', sourceMap: true }, + ).descriptor.template! + + const { code, map } = compile({ + filename: 'example.vue', + source: template.content, + }) + + expect(map!.sources).toEqual([`example.vue`]) + expect(map!.sourcesContent).toEqual([template.content]) + + const consumer = new SourceMapConsumer(map as RawSourceMap) + const commentNode = code.match(/_createCommentVNode\("v-if", true\)/) + expect(commentNode).not.toBeNull() + const commentPosition = getPositionInCode(code, commentNode![0]) + const originalPosition = consumer.originalPositionFor(commentPosition) + // the comment node should not be mapped to the original source + expect(originalPosition.column).toBeNull() + expect(originalPosition.line).toBeNull() + expect(originalPosition.source).toBeNull() +}) + test('should work w/ AST from descriptor', () => { const source = `