From 4fae27c370f67a07a07388a32285d30bea4f4a11 Mon Sep 17 00:00:00 2001 From: Adrian Cerbaro Date: Thu, 17 Apr 2025 14:38:17 -0300 Subject: [PATCH 1/4] =?UTF-8?q?fix(custom-elements):=20allow=20injecting?= =?UTF-8?q?=20values=20=E2=80=8B=E2=80=8Bfrom=20app=20context=20in=20neste?= =?UTF-8?q?d=20elements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this change, when a custom element was nested within another, the values provided via `configureApp` were ignored. Now, the provides object in the app context of a custom element inherits the provides object from its parent, allowing values to be injected properly using the app context in nested custom elements. Close #13212 --- packages/runtime-core/src/apiCreateApp.ts | 18 +++++++++++++----- packages/runtime-core/src/apiInject.ts | 12 ++++++++---- packages/runtime-dom/src/apiCustomElement.ts | 17 ++++++++++++++++- 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index 748de866f75..8bda6e2bdfd 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' @@ -449,10 +449,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..21beb1d3f44 100644 --- a/packages/runtime-core/src/apiInject.ts +++ b/packages/runtime-core/src/apiInject.ts @@ -23,8 +23,10 @@ export function provide | string | number>( // own provides object using parent provides object as prototype. // this way in `inject` we can simply look up injections from direct // parent and let the prototype chain do the work. - const parentProvides = - currentInstance.parent && currentInstance.parent.provides + // #13212, custom elements inherit the provides object from appContext + const parentProvides = currentInstance.ce + ? currentInstance.appContext.provides + : currentInstance.parent && currentInstance.parent.provides if (parentProvides === provides) { provides = currentInstance.provides = Object.create(parentProvides) } @@ -59,10 +61,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/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts index aeeaeec9b9f..cd9583f33b0 100644 --- a/packages/runtime-dom/src/apiCustomElement.ts +++ b/packages/runtime-dom/src/apiCustomElement.ts @@ -292,6 +292,7 @@ export class VueElement ) { if (parent instanceof VueElement) { this._parent = parent + this._inheritParentContext() break } } @@ -316,7 +317,19 @@ export class VueElement private _setParent(parent = this._parent) { if (parent) { this._instance!.parent = parent._instance - this._instance!.provides = parent._instance!.provides + this._instance!.provides = this._instance!.appContext.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 +430,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 399dfaf7f1d207bf38d808c49686f2d1d46da0ec Mon Sep 17 00:00:00 2001 From: Adrian Cerbaro Date: Thu, 17 Apr 2025 19:25:56 -0300 Subject: [PATCH 2/4] refactor(custom-elements): remove unnecessary call to _inheritParentContext Calling `_inheritParentContext` inside `_setParent` should be sufficient. --- packages/runtime-dom/src/apiCustomElement.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts index cd9583f33b0..7b2afe566d3 100644 --- a/packages/runtime-dom/src/apiCustomElement.ts +++ b/packages/runtime-dom/src/apiCustomElement.ts @@ -292,7 +292,6 @@ export class VueElement ) { if (parent instanceof VueElement) { this._parent = parent - this._inheritParentContext() break } } From 669487882588b201cddd9e721cdc25d1ea874897 Mon Sep 17 00:00:00 2001 From: Adrian Cerbaro Date: Thu, 17 Apr 2025 19:31:50 -0300 Subject: [PATCH 3/4] test(custom-elements): inject from app context within nested elements --- .../__tests__/customElement.spec.ts | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) 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', () => { From 87cda11c292299d28d636289fe6790e257d19691 Mon Sep 17 00:00:00 2001 From: Adrian Cerbaro Date: Thu, 17 Apr 2025 23:04:11 -0300 Subject: [PATCH 4/4] fix(custom-elements): stop overriding the provides object of the internal instance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The internal instance of a custom element already inherits the app context’s provides object via the prototype chain, so there’s no need to override it or add extra logic to the `provide` function. --- packages/runtime-core/src/apiInject.ts | 6 ++---- packages/runtime-dom/src/apiCustomElement.ts | 1 - 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/runtime-core/src/apiInject.ts b/packages/runtime-core/src/apiInject.ts index 21beb1d3f44..711c5d84de8 100644 --- a/packages/runtime-core/src/apiInject.ts +++ b/packages/runtime-core/src/apiInject.ts @@ -23,10 +23,8 @@ export function provide | string | number>( // own provides object using parent provides object as prototype. // this way in `inject` we can simply look up injections from direct // parent and let the prototype chain do the work. - // #13212, custom elements inherit the provides object from appContext - const parentProvides = currentInstance.ce - ? currentInstance.appContext.provides - : currentInstance.parent && currentInstance.parent.provides + const parentProvides = + currentInstance.parent && currentInstance.parent.provides if (parentProvides === provides) { provides = currentInstance.provides = Object.create(parentProvides) } diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts index 7b2afe566d3..cd21d0d1ce1 100644 --- a/packages/runtime-dom/src/apiCustomElement.ts +++ b/packages/runtime-dom/src/apiCustomElement.ts @@ -316,7 +316,6 @@ export class VueElement private _setParent(parent = this._parent) { if (parent) { this._instance!.parent = parent._instance - this._instance!.provides = this._instance!.appContext.provides this._inheritParentContext(parent) } }