feat(nuxt): support multiple nuxtApps at runtime (#27068)

This commit is contained in:
Nicolas Payot 2024-05-08 14:32:45 +02:00 committed by GitHub
parent 68f4b193be
commit 177517951c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 96 additions and 9 deletions

View File

@ -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. 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.
<!--
note
By default, the shared runtime context of Nuxt is namespaced under the [`buildId`](/docs/api/nuxt-config#buildid) option. It allows the support of multiple runtime contexts.
## Params
- `appName`: an optional application name. If you do not provide it, the Nuxt `buildId` option is used. Otherwise, it must match with an existing `buildId`. -->
## Methods ## Methods
### `provide (name, value)` ### `provide (name, value)`
@ -278,3 +286,7 @@ export function useStandType() {
} }
} }
``` ```
<!-- ### Params
- `appName`: an optional application name. If you do not provide it, the Nuxt `buildId` option is used. Otherwise, it must match with an existing `buildId`. -->

View File

@ -22,9 +22,14 @@ import type { ViewTransition } from './plugins/view-transitions.client'
import type { NuxtAppLiterals } from '#app' import type { NuxtAppLiterals } from '#app'
const nuxtAppCtx = /* @__PURE__ */ getContext<NuxtApp>('nuxt-app', { // @ts-expect-error virtual import
import { buildId } from '#build/nuxt.config.mjs'
function getNuxtAppCtx (appName?: string) {
return getContext<NuxtApp>(appName || buildId || 'nuxt-app', {
asyncContext: !!__NUXT_ASYNC_CONTEXT__ && import.meta.server, asyncContext: !!__NUXT_ASYNC_CONTEXT__ && import.meta.server,
}) })
}
type HookResult = Promise<void> | void type HookResult = Promise<void> | void
@ -93,6 +98,8 @@ export interface NuxtPayload {
} }
interface _NuxtApp { interface _NuxtApp {
/** @internal */
_name: string
vueApp: App<Element> vueApp: App<Element>
globalName: string globalName: string
versions: Record<string, string> versions: Record<string, string>
@ -237,6 +244,7 @@ export interface CreateOptions {
export function createNuxtApp (options: CreateOptions) { export function createNuxtApp (options: CreateOptions) {
let hydratingCount = 0 let hydratingCount = 0
const nuxtApp: NuxtApp = { const nuxtApp: NuxtApp = {
name: buildId,
_scope: effectScope(), _scope: effectScope(),
provide: undefined, provide: undefined,
globalName: 'nuxt', globalName: 'nuxt',
@ -447,6 +455,7 @@ export function isNuxtPlugin (plugin: unknown) {
*/ */
export function callWithNuxt<T extends (...args: any[]) => any> (nuxt: NuxtApp | _NuxtApp, setup: T, args?: Parameters<T>) { export function callWithNuxt<T extends (...args: any[]) => any> (nuxt: NuxtApp | _NuxtApp, setup: T, args?: Parameters<T>) {
const fn: () => ReturnType<T> = () => args ? setup(...args as Parameters<T>) : setup() const fn: () => ReturnType<T> = () => args ? setup(...args as Parameters<T>) : setup()
const nuxtAppCtx = getNuxtAppCtx(nuxt._name)
if (import.meta.server) { if (import.meta.server) {
return nuxt.vueApp.runWithContext(() => nuxtAppCtx.callAsync(nuxt as NuxtApp, fn)) return nuxt.vueApp.runWithContext(() => nuxtAppCtx.callAsync(nuxt as NuxtApp, fn))
} else { } else {
@ -463,13 +472,14 @@ export function callWithNuxt<T extends (...args: any[]) => any> (nuxt: NuxtApp |
* Returns `null` if Nuxt instance is unavailable. * Returns `null` if Nuxt instance is unavailable.
* @since 3.10.0 * @since 3.10.0
*/ */
export function tryUseNuxtApp (): NuxtApp | null { export function tryUseNuxtApp (): NuxtApp | null
export function tryUseNuxtApp (appName?: string): NuxtApp | null {
let nuxtAppInstance let nuxtAppInstance
if (hasInjectionContext()) { if (hasInjectionContext()) {
nuxtAppInstance = getCurrentInstance()?.appContext.app.$nuxt nuxtAppInstance = getCurrentInstance()?.appContext.app.$nuxt
} }
nuxtAppInstance = nuxtAppInstance || nuxtAppCtx.tryUse() nuxtAppInstance = nuxtAppInstance || getNuxtAppCtx(appName).tryUse()
return nuxtAppInstance || null return nuxtAppInstance || null
} }
@ -481,8 +491,10 @@ export function tryUseNuxtApp (): NuxtApp | null {
* Throws an error if Nuxt instance is unavailable. * Throws an error if Nuxt instance is unavailable.
* @since 3.0.0 * @since 3.0.0
*/ */
export function useNuxtApp (): NuxtApp { export function useNuxtApp (): NuxtApp
const nuxtAppInstance = tryUseNuxtApp() export function useNuxtApp (appName?: string): NuxtApp {
// @ts-expect-error internal usage of appName
const nuxtAppInstance = tryUseNuxtApp(appName)
if (!nuxtAppInstance) { if (!nuxtAppInstance) {
if (import.meta.dev) { if (import.meta.dev) {

View File

@ -3,7 +3,6 @@ import { existsSync, promises as fsp, readFileSync } from 'node:fs'
import { cpus } from 'node:os' import { cpus } from 'node:os'
import { join, relative, resolve } from 'pathe' import { join, relative, resolve } from 'pathe'
import { createRouter as createRadixRouter, exportMatcher, toRouteMatcher } from 'radix3' import { createRouter as createRadixRouter, exportMatcher, toRouteMatcher } from 'radix3'
import { randomUUID } from 'uncrypto'
import { joinURL, withTrailingSlash } from 'ufo' import { joinURL, withTrailingSlash } from 'ufo'
import { build, copyPublicAssets, createDevServer, createNitro, prepare, prerender, scanHandlers, writeTypes } from 'nitropack' import { build, copyPublicAssets, createDevServer, createNitro, prepare, prerender, scanHandlers, writeTypes } from 'nitropack'
import type { Nitro, NitroConfig, NitroOptions } 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) { if (nuxt.options.experimental.appManifest) {
// @ts-expect-error untyped nuxt property // @ts-expect-error untyped nuxt property
const buildId = nuxt.options.appConfig.nuxt!.buildId ||= 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 buildTimestamp = Date.now()
const manifestPrefix = joinURL(nuxt.options.app.buildAssetsDir, 'builds') const manifestPrefix = joinURL(nuxt.options.app.buildAssetsDir, 'builds')

View File

@ -397,6 +397,7 @@ export const nuxtConfigTemplate: NuxtTemplate = {
`export const fetchDefaults = ${JSON.stringify(fetchDefaults)}`, `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 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 viewTransition = ${ctx.nuxt.options.experimental.viewTransition}`,
`export const buildId = ${JSON.stringify(ctx.nuxt.options.buildId)}`,
].join('\n\n') ].join('\n\n')
}, },
} }

View File

@ -72,6 +72,7 @@
"std-env": "^3.7.0", "std-env": "^3.7.0",
"ufo": "^1.5.3", "ufo": "^1.5.3",
"unimport": "^3.7.1", "unimport": "^3.7.1",
"uncrypto": "^0.1.3",
"untyped": "^1.4.2" "untyped": "^1.4.2"
}, },
"engines": { "engines": {

View File

@ -4,6 +4,7 @@ import { join, relative, resolve } from 'pathe'
import { isDebug, isDevelopment, isTest } from 'std-env' import { isDebug, isDevelopment, isTest } from 'std-env'
import { defu } from 'defu' import { defu } from 'defu'
import { findWorkspaceDir } from 'pkg-types' import { findWorkspaceDir } from 'pkg-types'
import { randomUUID } from 'uncrypto'
import type { RuntimeConfig } from '../types/config' import type { RuntimeConfig } from '../types/config'
export default defineUntypedSchema({ export default defineUntypedSchema({
@ -153,6 +154,13 @@ export default defineUntypedSchema({
$resolve: async (val: string | undefined, get): Promise<string> => resolve(await get('rootDir') as string, val || '.nuxt'), $resolve: async (val: string | undefined, get): Promise<string> => 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 * Used to set the modules directories for path resolving (for example, webpack's
* `resolveLoading`, `nodeExternals` and `postcss`). * `resolveLoading`, `nodeExternals` and `postcss`).

View File

@ -463,6 +463,9 @@ importers:
ufo: ufo:
specifier: ^1.5.3 specifier: ^1.5.3
version: 1.5.3 version: 1.5.3
uncrypto:
specifier: ^0.1.3
version: 0.1.3
unimport: unimport:
specifier: ^3.7.1 specifier: ^3.7.1
version: 3.7.1(rollup@4.17.2) version: 3.7.1(rollup@4.17.2)

View File

@ -2642,3 +2642,42 @@ describe('defineNuxtComponent watch duplicate', () => {
expect(await page.getByTestId('define-nuxt-component-state').first().innerText()).toBe('2') 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()
})
})

View File

@ -32,6 +32,7 @@ export default defineNuxtConfig({
}, },
buildDir: process.env.NITRO_BUILD_DIR, buildDir: process.env.NITRO_BUILD_DIR,
builder: process.env.TEST_BUILDER as 'webpack' | 'vite' ?? 'vite', builder: process.env.TEST_BUILDER as 'webpack' | 'vite' ?? 'vite',
buildId: 'nuxt-app-basic',
build: { build: {
transpile: [ transpile: [
(ctx) => { (ctx) => {

View File

@ -0,0 +1,10 @@
<script setup lang="ts">
// Defaulting to buildId
useNuxtApp()
// Using correct configured buildId
useNuxtApp('nuxt-app-basic')
</script>
<template>
<div>Nuxt instance available</div>
</template>

View File

@ -13,6 +13,7 @@ export default defineVitestConfig({
environmentOptions: { environmentOptions: {
nuxt: { nuxt: {
overrides: { overrides: {
buildId: 'nuxt-app',
experimental: { experimental: {
appManifest: process.env.TEST_MANIFEST !== 'manifest-off', appManifest: process.env.TEST_MANIFEST !== 'manifest-off',
}, },