mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-25 15:15:19 +00:00
feat(nuxt): support multiple nuxtApps at runtime (#27068)
This commit is contained in:
parent
68f4b193be
commit
177517951c
@ -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`. -->
|
||||||
|
@ -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) {
|
||||||
|
@ -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')
|
||||||
|
@ -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')
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -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": {
|
||||||
|
@ -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`).
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
1
test/fixtures/basic/nuxt.config.ts
vendored
1
test/fixtures/basic/nuxt.config.ts
vendored
@ -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) => {
|
||||||
|
10
test/fixtures/basic/pages/namespace-nuxt-app.vue
vendored
Normal file
10
test/fixtures/basic/pages/namespace-nuxt-app.vue
vendored
Normal 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>
|
@ -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',
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user