From 177517951c9c034758f4600d70856fc4649f37f6 Mon Sep 17 00:00:00 2001 From: Nicolas Payot Date: Wed, 8 May 2024 14:32:45 +0200 Subject: [PATCH] feat(nuxt): support multiple nuxtApps at runtime (#27068) --- docs/3.api/2.composables/use-nuxt-app.md | 12 ++++++ packages/nuxt/src/app/nuxt.ts | 26 +++++++++---- packages/nuxt/src/core/nitro.ts | 3 +- packages/nuxt/src/core/templates.ts | 1 + packages/schema/package.json | 1 + packages/schema/src/config/common.ts | 8 ++++ pnpm-lock.yaml | 3 ++ test/basic.test.ts | 39 +++++++++++++++++++ test/fixtures/basic/nuxt.config.ts | 1 + .../basic/pages/namespace-nuxt-app.vue | 10 +++++ vitest.nuxt.config.ts | 1 + 11 files changed, 96 insertions(+), 9 deletions(-) create mode 100644 test/fixtures/basic/pages/namespace-nuxt-app.vue diff --git a/docs/3.api/2.composables/use-nuxt-app.md b/docs/3.api/2.composables/use-nuxt-app.md index 29a920cdb1..87c7be7418 100644 --- a/docs/3.api/2.composables/use-nuxt-app.md +++ b/docs/3.api/2.composables/use-nuxt-app.md @@ -18,6 +18,14 @@ const nuxtApp = useNuxtApp() If runtime context is unavailable in your scope, `useNuxtApp` will throw an exception when called. You can use [`tryUseNuxtApp`](#tryusenuxtapp) instead for composables that do not require `nuxtApp`, or to simply check if context is available or not without an exception. + + ## Methods ### `provide (name, value)` @@ -278,3 +286,7 @@ export function useStandType() { } } ``` + + diff --git a/packages/nuxt/src/app/nuxt.ts b/packages/nuxt/src/app/nuxt.ts index ee602fa58b..75821e7b55 100644 --- a/packages/nuxt/src/app/nuxt.ts +++ b/packages/nuxt/src/app/nuxt.ts @@ -22,9 +22,14 @@ import type { ViewTransition } from './plugins/view-transitions.client' import type { NuxtAppLiterals } from '#app' -const nuxtAppCtx = /* @__PURE__ */ getContext('nuxt-app', { - asyncContext: !!__NUXT_ASYNC_CONTEXT__ && import.meta.server, -}) +// @ts-expect-error virtual import +import { buildId } from '#build/nuxt.config.mjs' + +function getNuxtAppCtx (appName?: string) { + return getContext(appName || buildId || 'nuxt-app', { + asyncContext: !!__NUXT_ASYNC_CONTEXT__ && import.meta.server, + }) +} type HookResult = Promise | void @@ -93,6 +98,8 @@ export interface NuxtPayload { } interface _NuxtApp { + /** @internal */ + _name: string vueApp: App globalName: string versions: Record @@ -237,6 +244,7 @@ export interface CreateOptions { export function createNuxtApp (options: CreateOptions) { let hydratingCount = 0 const nuxtApp: NuxtApp = { + name: buildId, _scope: effectScope(), provide: undefined, globalName: 'nuxt', @@ -447,6 +455,7 @@ export function isNuxtPlugin (plugin: unknown) { */ export function callWithNuxt any> (nuxt: NuxtApp | _NuxtApp, setup: T, args?: Parameters) { const fn: () => ReturnType = () => args ? setup(...args as Parameters) : setup() + const nuxtAppCtx = getNuxtAppCtx(nuxt._name) if (import.meta.server) { return nuxt.vueApp.runWithContext(() => nuxtAppCtx.callAsync(nuxt as NuxtApp, fn)) } else { @@ -463,13 +472,14 @@ export function callWithNuxt any> (nuxt: NuxtApp | * Returns `null` if Nuxt instance is unavailable. * @since 3.10.0 */ -export function tryUseNuxtApp (): NuxtApp | null { +export function tryUseNuxtApp (): NuxtApp | null +export function tryUseNuxtApp (appName?: string): NuxtApp | null { let nuxtAppInstance if (hasInjectionContext()) { nuxtAppInstance = getCurrentInstance()?.appContext.app.$nuxt } - nuxtAppInstance = nuxtAppInstance || nuxtAppCtx.tryUse() + nuxtAppInstance = nuxtAppInstance || getNuxtAppCtx(appName).tryUse() return nuxtAppInstance || null } @@ -481,8 +491,10 @@ export function tryUseNuxtApp (): NuxtApp | null { * Throws an error if Nuxt instance is unavailable. * @since 3.0.0 */ -export function useNuxtApp (): NuxtApp { - const nuxtAppInstance = tryUseNuxtApp() +export function useNuxtApp (): NuxtApp +export function useNuxtApp (appName?: string): NuxtApp { + // @ts-expect-error internal usage of appName + const nuxtAppInstance = tryUseNuxtApp(appName) if (!nuxtAppInstance) { if (import.meta.dev) { diff --git a/packages/nuxt/src/core/nitro.ts b/packages/nuxt/src/core/nitro.ts index 01ffd58ad5..0bbc09aa6d 100644 --- a/packages/nuxt/src/core/nitro.ts +++ b/packages/nuxt/src/core/nitro.ts @@ -3,7 +3,6 @@ import { existsSync, promises as fsp, readFileSync } from 'node:fs' import { cpus } from 'node:os' import { join, relative, resolve } from 'pathe' import { createRouter as createRadixRouter, exportMatcher, toRouteMatcher } from 'radix3' -import { randomUUID } from 'uncrypto' import { joinURL, withTrailingSlash } from 'ufo' import { build, copyPublicAssets, createDevServer, createNitro, prepare, prerender, scanHandlers, writeTypes } from 'nitropack' import type { Nitro, NitroConfig, NitroOptions } from 'nitropack' @@ -237,7 +236,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) { if (nuxt.options.experimental.appManifest) { // @ts-expect-error untyped nuxt property const buildId = nuxt.options.appConfig.nuxt!.buildId ||= - (nuxt.options.dev ? 'dev' : nuxt.options.test ? 'test' : randomUUID()) + (nuxt.options.dev ? 'dev' : nuxt.options.test ? 'test' : nuxt.options.buildId) const buildTimestamp = Date.now() const manifestPrefix = joinURL(nuxt.options.app.buildAssetsDir, 'builds') diff --git a/packages/nuxt/src/core/templates.ts b/packages/nuxt/src/core/templates.ts index 2234e60dca..9cccd80902 100644 --- a/packages/nuxt/src/core/templates.ts +++ b/packages/nuxt/src/core/templates.ts @@ -397,6 +397,7 @@ export const nuxtConfigTemplate: NuxtTemplate = { `export const fetchDefaults = ${JSON.stringify(fetchDefaults)}`, `export const vueAppRootContainer = ${ctx.nuxt.options.app.rootId ? `'#${ctx.nuxt.options.app.rootId}'` : `'body > ${ctx.nuxt.options.app.rootTag}'`}`, `export const viewTransition = ${ctx.nuxt.options.experimental.viewTransition}`, + `export const buildId = ${JSON.stringify(ctx.nuxt.options.buildId)}`, ].join('\n\n') }, } diff --git a/packages/schema/package.json b/packages/schema/package.json index 1ef6a46a05..95d7e59131 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -72,6 +72,7 @@ "std-env": "^3.7.0", "ufo": "^1.5.3", "unimport": "^3.7.1", + "uncrypto": "^0.1.3", "untyped": "^1.4.2" }, "engines": { diff --git a/packages/schema/src/config/common.ts b/packages/schema/src/config/common.ts index a5bea18ae6..42191feace 100644 --- a/packages/schema/src/config/common.ts +++ b/packages/schema/src/config/common.ts @@ -4,6 +4,7 @@ import { join, relative, resolve } from 'pathe' import { isDebug, isDevelopment, isTest } from 'std-env' import { defu } from 'defu' import { findWorkspaceDir } from 'pkg-types' +import { randomUUID } from 'uncrypto' import type { RuntimeConfig } from '../types/config' export default defineUntypedSchema({ @@ -153,6 +154,13 @@ export default defineUntypedSchema({ $resolve: async (val: string | undefined, get): Promise => resolve(await get('rootDir') as string, val || '.nuxt'), }, + /** + * A unique identifier matching the build. This may contain the hash of the current state of the project. + */ + buildId: { + $resolve: (val: string) => val ?? randomUUID(), + }, + /** * Used to set the modules directories for path resolving (for example, webpack's * `resolveLoading`, `nodeExternals` and `postcss`). diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 68396ac133..acb51ab74c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -463,6 +463,9 @@ importers: ufo: specifier: ^1.5.3 version: 1.5.3 + uncrypto: + specifier: ^0.1.3 + version: 0.1.3 unimport: specifier: ^3.7.1 version: 3.7.1(rollup@4.17.2) diff --git a/test/basic.test.ts b/test/basic.test.ts index 81f8ff916b..aa64efe29c 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -2642,3 +2642,42 @@ describe('defineNuxtComponent watch duplicate', () => { expect(await page.getByTestId('define-nuxt-component-state').first().innerText()).toBe('2') }) }) + +describe('namespace access to useNuxtApp', () => { + it('should return the nuxt instance when used with correct buildId', async () => { + const { page, pageErrors } = await renderPage('/namespace-nuxt-app') + + expect(pageErrors).toEqual([]) + + await page.waitForFunction(() => window.useNuxtApp?.() && !window.useNuxtApp?.().isHydrating) + + // Defaulting to buildId + await page.evaluate(() => window.useNuxtApp?.()) + // Using correct configured buildId + // @ts-expect-error not public API yet + await page.evaluate(() => window.useNuxtApp?.('nuxt-app-basic')) + + await page.close() + }) + + it('should throw an error when used with wrong buildId', async () => { + const { page, pageErrors } = await renderPage('/namespace-nuxt-app') + + expect(pageErrors).toEqual([]) + + await page.waitForFunction(() => window.useNuxtApp?.() && !window.useNuxtApp?.().isHydrating) + + let error: unknown + try { + // Using wrong/unknown buildId + // @ts-expect-error not public API yet + await page.evaluate(() => window.useNuxtApp?.('nuxt-app-unknown')) + } catch (err) { + error = err + } + + expect(error).toBeTruthy() + + await page.close() + }) +}) diff --git a/test/fixtures/basic/nuxt.config.ts b/test/fixtures/basic/nuxt.config.ts index 130d0f7bbd..7064f0eb85 100644 --- a/test/fixtures/basic/nuxt.config.ts +++ b/test/fixtures/basic/nuxt.config.ts @@ -32,6 +32,7 @@ export default defineNuxtConfig({ }, buildDir: process.env.NITRO_BUILD_DIR, builder: process.env.TEST_BUILDER as 'webpack' | 'vite' ?? 'vite', + buildId: 'nuxt-app-basic', build: { transpile: [ (ctx) => { diff --git a/test/fixtures/basic/pages/namespace-nuxt-app.vue b/test/fixtures/basic/pages/namespace-nuxt-app.vue new file mode 100644 index 0000000000..66a5a0d05f --- /dev/null +++ b/test/fixtures/basic/pages/namespace-nuxt-app.vue @@ -0,0 +1,10 @@ + + + diff --git a/vitest.nuxt.config.ts b/vitest.nuxt.config.ts index fec80025ca..55e081bfb7 100644 --- a/vitest.nuxt.config.ts +++ b/vitest.nuxt.config.ts @@ -13,6 +13,7 @@ export default defineVitestConfig({ environmentOptions: { nuxt: { overrides: { + buildId: 'nuxt-app', experimental: { appManifest: process.env.TEST_MANIFEST !== 'manifest-off', },