From b233bbf9ba57f0375216667cfec548b6d2c469d5 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Mon, 2 Jan 2023 18:04:43 +0100 Subject: [PATCH 1/3] feat(app): app.runWithContext() Allows accessing globals provided at the app level with `app.provide()` via a regular `inject()` call as long as you have access to the application. Useful for Pinia, vue-router, Nuxt, Quasar, and other advanced use cases. - https://github.com/vuejs/pinia/issues/1784 --- .../__tests__/apiCreateApp.spec.ts | 16 ++++++++++++++ packages/runtime-core/src/apiCreateApp.ts | 22 +++++++++++++++++++ packages/runtime-core/src/apiInject.ts | 12 ++++++---- 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/packages/runtime-core/__tests__/apiCreateApp.spec.ts b/packages/runtime-core/__tests__/apiCreateApp.spec.ts index 7cfce51c161..699d0be6d50 100644 --- a/packages/runtime-core/__tests__/apiCreateApp.spec.ts +++ b/packages/runtime-core/__tests__/apiCreateApp.spec.ts @@ -110,6 +110,22 @@ describe('api: createApp', () => { expect(`App already provides property with key "bar".`).toHaveBeenWarned() }) + test('runWithContext', () => { + const app = createApp({ + setup() { + provide('foo', 'should not be seen') + return () => h('div') + } + }) + app.provide('foo', 1) + + expect(app.runWithContext(() => inject('foo'))).toBe(1) + + // ensure the context is restored + inject('foo') + expect('inject() can only be used inside setup').toHaveBeenWarned() + }) + test('component', () => { const Root = { // local override diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index 05c7ce31539..7a0e33492b4 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -51,6 +51,14 @@ export interface App { unmount(): void provide(key: InjectionKey | string, value: T): this + /** + * Runs a function with the app as active instance. This allows using of `inject()` within the function to get access + * to variables provided via `app.provide()`. + * + * @param fn - function to run with the app as active instance + */ + runWithContext(fn: () => T): T + // internal, but we need to expose these for the server-renderer and devtools _uid: number _component: ConcreteComponent @@ -370,6 +378,15 @@ export function createAppAPI( context.provides[key as string | symbol] = value return app + }, + + runWithContext(fn) { + currentApp = this + try { + return fn() + } finally { + currentApp = null + } } }) @@ -380,3 +397,8 @@ export function createAppAPI( return app } } + +/** + * @internal Used to identify the current app when using `inject()` within `app.runWithContext()`. + */ +export let currentApp: App | null = null diff --git a/packages/runtime-core/src/apiInject.ts b/packages/runtime-core/src/apiInject.ts index c5c47876cb3..65722739135 100644 --- a/packages/runtime-core/src/apiInject.ts +++ b/packages/runtime-core/src/apiInject.ts @@ -1,6 +1,7 @@ import { isFunction } from '@vue/shared' import { currentInstance } from './component' import { currentRenderingInstance } from './componentRenderContext' +import { App, currentApp } from './apiCreateApp' import { warn } from './warning' export interface InjectionKey extends Symbol {} @@ -44,14 +45,17 @@ export function inject( treatDefaultAsFactory = false ) { // fallback to `currentRenderingInstance` so that this can be called in - // a functional component - const instance = currentInstance || currentRenderingInstance + // a functional component and to currentApp so it can be called within `app.runWithContext()` + const instance = currentInstance || currentRenderingInstance || currentApp + if (instance) { // #2400 // to support `app.use` plugins, // fallback to appContext's `provides` if the instance is at root const provides = - instance.parent == null + 'mount' in instance // checks if instance is an App + ? instance._context.provides + : instance.parent == null ? instance.vnode.appContext && instance.vnode.appContext.provides : instance.parent.provides @@ -60,7 +64,7 @@ export function inject( return provides[key as string] } else if (arguments.length > 1) { return treatDefaultAsFactory && isFunction(defaultValue) - ? defaultValue.call(instance.proxy) + ? defaultValue.call((instance as App & { proxy: undefined }).proxy) : defaultValue } else if (__DEV__) { warn(`injection "${String(key)}" not found.`) From 2b3e6c676c433e63d10f903e8c64e309de50cd71 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Thu, 9 Mar 2023 10:58:25 +0100 Subject: [PATCH 2/3] refactor: better types --- packages/runtime-core/src/apiInject.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/runtime-core/src/apiInject.ts b/packages/runtime-core/src/apiInject.ts index 65722739135..cb9b89e76a7 100644 --- a/packages/runtime-core/src/apiInject.ts +++ b/packages/runtime-core/src/apiInject.ts @@ -1,8 +1,9 @@ import { isFunction } from '@vue/shared' import { currentInstance } from './component' import { currentRenderingInstance } from './componentRenderContext' -import { App, currentApp } from './apiCreateApp' +import { currentApp } from './apiCreateApp' import { warn } from './warning' +import { ComponentPublicInstance } from './componentPublicInstance' export interface InjectionKey extends Symbol {} @@ -64,7 +65,9 @@ export function inject( return provides[key as string] } else if (arguments.length > 1) { return treatDefaultAsFactory && isFunction(defaultValue) - ? defaultValue.call((instance as App & { proxy: undefined }).proxy) + ? defaultValue.call( + (instance as { proxy?: ComponentPublicInstance | null }).proxy + ) : defaultValue } else if (__DEV__) { warn(`injection "${String(key)}" not found.`) From afed3c34e6b6a465cc9b555bc9bb192d1d27b822 Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 5 Apr 2023 15:13:28 +0800 Subject: [PATCH 3/3] chore: refactor --- packages/runtime-core/src/apiCreateApp.ts | 5 +++-- packages/runtime-core/src/apiInject.ts | 19 ++++++++----------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index 7a0e33492b4..6b698ba26c2 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -381,7 +381,7 @@ export function createAppAPI( }, runWithContext(fn) { - currentApp = this + currentApp = app try { return fn() } finally { @@ -399,6 +399,7 @@ export function createAppAPI( } /** - * @internal Used to identify the current app when using `inject()` within `app.runWithContext()`. + * @internal Used to identify the current app when using `inject()` within + * `app.runWithContext()`. */ export let currentApp: App | null = null diff --git a/packages/runtime-core/src/apiInject.ts b/packages/runtime-core/src/apiInject.ts index cb9b89e76a7..6eedee88c09 100644 --- a/packages/runtime-core/src/apiInject.ts +++ b/packages/runtime-core/src/apiInject.ts @@ -3,7 +3,6 @@ import { currentInstance } from './component' import { currentRenderingInstance } from './componentRenderContext' import { currentApp } from './apiCreateApp' import { warn } from './warning' -import { ComponentPublicInstance } from './componentPublicInstance' export interface InjectionKey extends Symbol {} @@ -46,28 +45,26 @@ export function inject( treatDefaultAsFactory = false ) { // fallback to `currentRenderingInstance` so that this can be called in - // a functional component and to currentApp so it can be called within `app.runWithContext()` - const instance = currentInstance || currentRenderingInstance || currentApp + // a functional component + const instance = currentInstance || currentRenderingInstance - if (instance) { + // also support looking up from app-level provides w/ `app.runWithContext()` + if (instance || currentApp) { // #2400 // to support `app.use` plugins, // fallback to appContext's `provides` if the instance is at root - const provides = - 'mount' in instance // checks if instance is an App - ? instance._context.provides - : instance.parent == null + const provides = instance + ? instance.parent == null ? instance.vnode.appContext && instance.vnode.appContext.provides : instance.parent.provides + : currentApp!._context.provides if (provides && (key as string | symbol) in provides) { // TS doesn't allow symbol as index type return provides[key as string] } else if (arguments.length > 1) { return treatDefaultAsFactory && isFunction(defaultValue) - ? defaultValue.call( - (instance as { proxy?: ComponentPublicInstance | null }).proxy - ) + ? defaultValue.call(instance && instance.proxy) : defaultValue } else if (__DEV__) { warn(`injection "${String(key)}" not found.`)