diff --git a/packages/nuxt3/package.json b/packages/nuxt3/package.json index 55a8e0c1a4..a5988e6f38 100644 --- a/packages/nuxt3/package.json +++ b/packages/nuxt3/package.json @@ -59,6 +59,7 @@ "perfect-debounce": "^0.1.3", "scule": "^0.2.1", "ufo": "^0.8.3", + "unctx": "^1.1.4", "unimport": "^0.1.3", "unplugin": "^0.6.1", "untyped": "^0.4.4", diff --git a/packages/nuxt3/src/app/nuxt.ts b/packages/nuxt3/src/app/nuxt.ts index 56b909b53b..d2db362dc9 100644 --- a/packages/nuxt3/src/app/nuxt.ts +++ b/packages/nuxt3/src/app/nuxt.ts @@ -3,8 +3,11 @@ import { getCurrentInstance, reactive } from 'vue' import type { App, onErrorCaptured, VNode } from 'vue' import { createHooks, Hookable } from 'hookable' import type { RuntimeConfig } from '@nuxt/schema' +import { getContext } from 'unctx' import { legacyPlugin, LegacyContext } from './compat/legacy-app' +const nuxtAppCtx = getContext('nuxt-app') + type NuxtMeta = { htmlAttrs?: string headAttrs?: string @@ -172,12 +175,6 @@ export function isLegacyPlugin (plugin: unknown): plugin is LegacyPlugin { return !plugin[NuxtPluginIndicator] } -let currentNuxtAppInstance: NuxtApp | null - -export const setNuxtAppInstance = (nuxt: NuxtApp | null) => { - currentNuxtAppInstance = nuxt -} - /** * Ensures that the setup function passed in has access to the Nuxt instance via `useNuxt`. * @@ -185,13 +182,14 @@ export const setNuxtAppInstance = (nuxt: NuxtApp | null) => { * @param setup The function to call */ export function callWithNuxt any> (nuxt: NuxtApp | _NuxtApp, setup: T, args?: Parameters) { - setNuxtAppInstance(nuxt as NuxtApp) - const p: ReturnType = args ? setup(...args as Parameters) : setup() + const fn = () => args ? setup(...args as Parameters) : setup() if (process.server) { - // Unset nuxt instance to prevent context-sharing in server-side - setNuxtAppInstance(null) + return nuxtAppCtx.callAsync>(nuxt, fn) + } else { + // In client side we could assume nuxt app is singleton + nuxtAppCtx.set(nuxt) + return fn() } - return p } /** @@ -201,10 +199,11 @@ export function useNuxtApp () { const vm = getCurrentInstance() if (!vm) { - if (!currentNuxtAppInstance) { + const nuxtAppInstance = nuxtAppCtx.use() + if (!nuxtAppInstance) { throw new Error('nuxt instance unavailable') } - return currentNuxtAppInstance + return nuxtAppInstance } return vm.appContext.app.$nuxt as NuxtApp diff --git a/packages/nuxt3/src/core/nuxt.ts b/packages/nuxt3/src/core/nuxt.ts index 3730b77b9e..0f610d8a80 100644 --- a/packages/nuxt3/src/core/nuxt.ts +++ b/packages/nuxt3/src/core/nuxt.ts @@ -12,6 +12,7 @@ import autoImportsModule from '../auto-imports/module' import { distDir, pkgDir } from '../dirs' import { version } from '../../package.json' import { ImportProtectionPlugin, vueAppPatterns } from './plugins/import-protection' +import { UnctxTransformPlugin } from './plugins/unctx' import { addModuleTranspiles } from './modules' export function createNuxt (options: NuxtOptions): Nuxt { @@ -57,7 +58,6 @@ async function initNuxt (nuxt: Nuxt) { }) // Add import protection - const config = { rootDir: nuxt.options.rootDir, patterns: vueAppPatterns(nuxt) @@ -65,6 +65,10 @@ async function initNuxt (nuxt: Nuxt) { addVitePlugin(ImportProtectionPlugin.vite(config)) addWebpackPlugin(ImportProtectionPlugin.webpack(config)) + // Add unctx transform + addVitePlugin(UnctxTransformPlugin(nuxt).vite()) + addWebpackPlugin(UnctxTransformPlugin(nuxt).webpack()) + // Init user modules await nuxt.callHook('modules:before', { nuxt } as ModuleContainer) const modulesToInstall = [ diff --git a/packages/nuxt3/src/core/plugins/unctx.ts b/packages/nuxt3/src/core/plugins/unctx.ts new file mode 100644 index 0000000000..036d21c677 --- /dev/null +++ b/packages/nuxt3/src/core/plugins/unctx.ts @@ -0,0 +1,31 @@ +import { Nuxt, NuxtApp, NuxtMiddleware } from '@nuxt/schema' +import { createTransformer } from 'unctx/transform' +import { createUnplugin } from 'unplugin' + +export const UnctxTransformPlugin = (nuxt: Nuxt) => { + const transformer = createTransformer({ + asyncFunctions: ['defineNuxtPlugin', 'defineNuxtRouteMiddleware'] + }) + + let app: NuxtApp | undefined + let middleware: NuxtMiddleware[] = [] + nuxt.hook('app:resolve', (_app) => { app = _app }) + nuxt.hook('pages:middleware:extend', (_middlewares) => { middleware = _middlewares }) + + return createUnplugin(() => ({ + name: 'unctx:transfrom', + enforce: 'post', + transformInclude (id) { + return Boolean(app?.plugins.find(i => i.src === id) || middleware.find(m => m.path === id)) + }, + transform (code, id) { + const result = transformer.transform(code) + if (result) { + return { + code: result.code, + map: result.magicString.generateMap({ source: id, includeContent: true }) + } + } + } + })) +} diff --git a/test/basic.test.ts b/test/basic.test.ts index f4423084a5..62931bd3f5 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -40,8 +40,6 @@ describe('pages', () => { // composables auto import expect(html).toContain('Composable | foo: auto imported from ~/components/foo.ts') expect(html).toContain('Composable | bar: auto imported from ~/components/useBar.ts') - // plugins - expect(html).toContain('Plugin | myPlugin: Injected by my-plugin') // should import components expect(html).toContain('This is a custom component with a named export.') }) @@ -146,6 +144,18 @@ describe('middlewares', () => { }) }) +describe('plugins', () => { + it('basic plugin', async () => { + const html = await $fetch('/plugins') + expect(html).toContain('myPlugin: Injected by my-plugin') + }) + + it('async plugin', async () => { + const html = await $fetch('/plugins') + expect(html).toContain('asyncPlugin: Async plugin works! 123') + }) +}) + describe('layouts', () => { it('should apply custom layout', async () => { const html = await $fetch('/with-layout') diff --git a/test/fixtures/basic/middleware/redirect.global.ts b/test/fixtures/basic/middleware/redirect.global.ts index 9d7f4fe541..1c926d193e 100644 --- a/test/fixtures/basic/middleware/redirect.global.ts +++ b/test/fixtures/basic/middleware/redirect.global.ts @@ -1,5 +1,6 @@ -export default defineNuxtRouteMiddleware((to) => { +export default defineNuxtRouteMiddleware(async (to) => { if (to.path.startsWith('/redirect/')) { + await new Promise(resolve => setTimeout(resolve, 100)) return navigateTo(to.path.slice('/redirect/'.length - 1)) } }) diff --git a/test/fixtures/basic/pages/index.vue b/test/fixtures/basic/pages/index.vue index 6c2f183ed8..3302ee453c 100644 --- a/test/fixtures/basic/pages/index.vue +++ b/test/fixtures/basic/pages/index.vue @@ -7,7 +7,6 @@
RuntimeConfig | testConfig: {{ config.testConfig }}
Composable | foo: {{ foo }}
Composable | bar: {{ bar }}
-
Plugin | myPlugin: {{ $myPlugin() }}
diff --git a/test/fixtures/basic/pages/plugins.vue b/test/fixtures/basic/pages/plugins.vue new file mode 100644 index 0000000000..1b8a0285c1 --- /dev/null +++ b/test/fixtures/basic/pages/plugins.vue @@ -0,0 +1,6 @@ + diff --git a/test/fixtures/basic/plugins/async-plugin.ts b/test/fixtures/basic/plugins/async-plugin.ts new file mode 100644 index 0000000000..dd5d9106a5 --- /dev/null +++ b/test/fixtures/basic/plugins/async-plugin.ts @@ -0,0 +1,12 @@ +export default defineNuxtPlugin(async (/* nuxtApp */) => { + const config1 = useRuntimeConfig() + await new Promise(resolve => setTimeout(resolve, 100)) + const config2 = useRuntimeConfig() + return { + provide: { + asyncPlugin: () => config1 && config1 === config2 + ? 'Async plugin works! ' + config1.testConfig + : 'Async plugin failed!' + } + } +}) diff --git a/yarn.lock b/yarn.lock index 49a1c1166f..c8b97c3ed8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15500,6 +15500,7 @@ __metadata: scule: ^0.2.1 ufo: ^0.8.3 unbuild: latest + unctx: ^1.1.4 unimport: ^0.1.3 unplugin: ^0.6.1 untyped: ^0.4.4