mirror of
https://github.com/nuxt/nuxt.git
synced 2025-02-13 04:08:11 +00:00
feat(kit,nuxt): allow multiple nuxts to run in one process (#30510)
This commit is contained in:
parent
d47b830d3f
commit
2fa17462da
@ -1,9 +1,22 @@
|
|||||||
import { getContext } from 'unctx'
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
||||||
|
import { createContext, getContext } from 'unctx'
|
||||||
import type { Nuxt } from '@nuxt/schema'
|
import type { Nuxt } from '@nuxt/schema'
|
||||||
|
|
||||||
/** Direct access to the Nuxt context - see https://github.com/unjs/unctx. */
|
/**
|
||||||
|
* Direct access to the Nuxt global context - see https://github.com/unjs/unctx.
|
||||||
|
* @deprecated Use `getNuxtCtx` instead
|
||||||
|
*/
|
||||||
export const nuxtCtx = getContext<Nuxt>('nuxt')
|
export const nuxtCtx = getContext<Nuxt>('nuxt')
|
||||||
|
|
||||||
|
/** async local storage for the name of the current nuxt instance */
|
||||||
|
const asyncNuxtStorage = createContext<Nuxt>({
|
||||||
|
asyncContext: true,
|
||||||
|
AsyncLocalStorage,
|
||||||
|
})
|
||||||
|
|
||||||
|
/** Direct access to the Nuxt context with asyncLocalStorage - see https://github.com/unjs/unctx. */
|
||||||
|
export const getNuxtCtx = () => asyncNuxtStorage.tryUse()
|
||||||
|
|
||||||
// TODO: Use use/tryUse from unctx. https://github.com/unjs/unctx/issues/6
|
// TODO: Use use/tryUse from unctx. https://github.com/unjs/unctx/issues/6
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -16,7 +29,7 @@ export const nuxtCtx = getContext<Nuxt>('nuxt')
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function useNuxt (): Nuxt {
|
export function useNuxt (): Nuxt {
|
||||||
const instance = nuxtCtx.tryUse()
|
const instance = asyncNuxtStorage.tryUse() || nuxtCtx.tryUse()
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
throw new Error('Nuxt instance is unavailable!')
|
throw new Error('Nuxt instance is unavailable!')
|
||||||
}
|
}
|
||||||
@ -36,5 +49,9 @@ export function useNuxt (): Nuxt {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function tryUseNuxt (): Nuxt | null {
|
export function tryUseNuxt (): Nuxt | null {
|
||||||
return nuxtCtx.tryUse()
|
return asyncNuxtStorage.tryUse() || nuxtCtx.tryUse()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runWithNuxtContext<T extends (...args: any[]) => any> (nuxt: Nuxt, fn: T) {
|
||||||
|
return asyncNuxtStorage.call(nuxt, fn) as ReturnType<T>
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ export type { ExtendConfigOptions, ExtendViteConfigOptions, ExtendWebpackConfigO
|
|||||||
export { assertNuxtCompatibility, checkNuxtCompatibility, getNuxtVersion, hasNuxtCompatibility, isNuxtMajorVersion, normalizeSemanticVersion, isNuxt2, isNuxt3 } from './compatibility'
|
export { assertNuxtCompatibility, checkNuxtCompatibility, getNuxtVersion, hasNuxtCompatibility, isNuxtMajorVersion, normalizeSemanticVersion, isNuxt2, isNuxt3 } from './compatibility'
|
||||||
export { addComponent, addComponentsDir } from './components'
|
export { addComponent, addComponentsDir } from './components'
|
||||||
export type { AddComponentOptions } from './components'
|
export type { AddComponentOptions } from './components'
|
||||||
export { nuxtCtx, tryUseNuxt, useNuxt } from './context'
|
export { getNuxtCtx, runWithNuxtContext, tryUseNuxt, useNuxt, nuxtCtx } from './context'
|
||||||
export { createIsIgnored, isIgnored, resolveIgnorePatterns } from './ignore'
|
export { createIsIgnored, isIgnored, resolveIgnorePatterns } from './ignore'
|
||||||
export { addLayout } from './layout'
|
export { addLayout } from './layout'
|
||||||
export { addRouteMiddleware, extendPages, extendRouteRules } from './pages'
|
export { addRouteMiddleware, extendPages, extendRouteRules } from './pages'
|
||||||
|
@ -2,6 +2,7 @@ import { pathToFileURL } from 'node:url'
|
|||||||
import { readPackageJSON, resolvePackageJSON } from 'pkg-types'
|
import { readPackageJSON, resolvePackageJSON } from 'pkg-types'
|
||||||
import type { Nuxt } from '@nuxt/schema'
|
import type { Nuxt } from '@nuxt/schema'
|
||||||
import { importModule, tryImportModule } from '../internal/esm'
|
import { importModule, tryImportModule } from '../internal/esm'
|
||||||
|
import { runWithNuxtContext } from '../context'
|
||||||
import type { LoadNuxtConfigOptions } from './config'
|
import type { LoadNuxtConfigOptions } from './config'
|
||||||
|
|
||||||
export interface LoadNuxtOptions extends LoadNuxtConfigOptions {
|
export interface LoadNuxtOptions extends LoadNuxtConfigOptions {
|
||||||
@ -76,10 +77,10 @@ export async function buildNuxt (nuxt: Nuxt): Promise<any> {
|
|||||||
// Nuxt 3
|
// Nuxt 3
|
||||||
if (nuxt.options._majorVersion === 3) {
|
if (nuxt.options._majorVersion === 3) {
|
||||||
const { build } = await tryImportModule<typeof import('nuxt')>('nuxt-nightly', { paths: rootDir }) || await tryImportModule<typeof import('nuxt')>('nuxt3', { paths: rootDir }) || await importModule<typeof import('nuxt')>('nuxt', { paths: rootDir })
|
const { build } = await tryImportModule<typeof import('nuxt')>('nuxt-nightly', { paths: rootDir }) || await tryImportModule<typeof import('nuxt')>('nuxt3', { paths: rootDir }) || await importModule<typeof import('nuxt')>('nuxt', { paths: rootDir })
|
||||||
return build(nuxt)
|
return runWithNuxtContext(nuxt, () => build(nuxt))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nuxt 2
|
// Nuxt 2
|
||||||
const { build } = await tryImportModule<{ build: any }>('nuxt-edge', { paths: rootDir }) || await importModule<{ build: any }>('nuxt', { paths: rootDir })
|
const { build } = await tryImportModule<{ build: any }>('nuxt-edge', { paths: rootDir }) || await importModule<{ build: any }>('nuxt', { paths: rootDir })
|
||||||
return build(nuxt)
|
return runWithNuxtContext(nuxt, () => build(nuxt))
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { existsSync } from 'node:fs'
|
import { existsSync } from 'node:fs'
|
||||||
import { rm } from 'node:fs/promises'
|
import { rm } from 'node:fs/promises'
|
||||||
|
import { randomUUID } from 'node:crypto'
|
||||||
import { AsyncLocalStorage } from 'node:async_hooks'
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
||||||
import { join, normalize, relative, resolve } from 'pathe'
|
import { join, normalize, relative, resolve } from 'pathe'
|
||||||
import { createDebugger, createHooks } from 'hookable'
|
import { createDebugger, createHooks } from 'hookable'
|
||||||
import ignore from 'ignore'
|
import ignore from 'ignore'
|
||||||
import type { LoadNuxtOptions } from '@nuxt/kit'
|
import type { LoadNuxtOptions } from '@nuxt/kit'
|
||||||
import { addBuildPlugin, addComponent, addPlugin, addPluginTemplate, addRouteMiddleware, addServerPlugin, addTypeTemplate, addVitePlugin, addWebpackPlugin, installModule, loadNuxtConfig, nuxtCtx, resolveAlias, resolveFiles, resolveIgnorePatterns, resolvePath, tryResolveModule, useNitro } from '@nuxt/kit'
|
import { addBuildPlugin, addComponent, addPlugin, addPluginTemplate, addRouteMiddleware, addServerPlugin, addTypeTemplate, addVitePlugin, addWebpackPlugin, installModule, loadNuxtConfig, nuxtCtx, resolveAlias, resolveFiles, resolveIgnorePatterns, resolvePath, runWithNuxtContext, tryResolveModule, useNitro } from '@nuxt/kit'
|
||||||
import type { Nuxt, NuxtHooks, NuxtModule, NuxtOptions } from 'nuxt/schema'
|
import type { Nuxt, NuxtHooks, NuxtModule, NuxtOptions } from 'nuxt/schema'
|
||||||
import type { PackageJson } from 'pkg-types'
|
import type { PackageJson } from 'pkg-types'
|
||||||
import { readPackageJSON } from 'pkg-types'
|
import { readPackageJSON } from 'pkg-types'
|
||||||
@ -52,17 +53,24 @@ import { VirtualFSPlugin } from './plugins/virtual'
|
|||||||
export function createNuxt (options: NuxtOptions): Nuxt {
|
export function createNuxt (options: NuxtOptions): Nuxt {
|
||||||
const hooks = createHooks<NuxtHooks>()
|
const hooks = createHooks<NuxtHooks>()
|
||||||
|
|
||||||
|
const { callHook, callHookParallel, callHookWith } = hooks
|
||||||
|
hooks.callHook = (...args) => runWithNuxtContext(nuxt, () => callHook(...args))
|
||||||
|
hooks.callHookParallel = (...args) => runWithNuxtContext(nuxt, () => callHookParallel(...args))
|
||||||
|
hooks.callHookWith = (...args) => runWithNuxtContext(nuxt, () => callHookWith(...args))
|
||||||
|
|
||||||
const nuxt: Nuxt = {
|
const nuxt: Nuxt = {
|
||||||
|
__name: randomUUID(),
|
||||||
_version: version,
|
_version: version,
|
||||||
_asyncLocalStorageModule: options.experimental.debugModuleMutation ? new AsyncLocalStorage() : undefined,
|
_asyncLocalStorageModule: options.experimental.debugModuleMutation ? new AsyncLocalStorage() : undefined,
|
||||||
hooks,
|
hooks,
|
||||||
callHook: hooks.callHook,
|
callHook: hooks.callHook,
|
||||||
addHooks: hooks.addHooks,
|
addHooks: hooks.addHooks,
|
||||||
hook: hooks.hook,
|
hook: hooks.hook,
|
||||||
ready: () => initNuxt(nuxt),
|
ready: () => runWithNuxtContext(nuxt, () => initNuxt(nuxt)),
|
||||||
close: () => hooks.callHook('close', nuxt),
|
close: () => hooks.callHook('close', nuxt),
|
||||||
vfs: {},
|
vfs: {},
|
||||||
apps: {},
|
apps: {},
|
||||||
|
runWithContext: fn => runWithNuxtContext(nuxt, fn),
|
||||||
options,
|
options,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,6 +122,14 @@ export function createNuxt (options: NuxtOptions): Nuxt {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!nuxtCtx.tryUse()) {
|
||||||
|
// backward compatibility with 3.x
|
||||||
|
nuxtCtx.set(nuxt)
|
||||||
|
nuxt.hook('close', () => {
|
||||||
|
nuxtCtx.unset()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
hooks.hookOnce('close', () => { hooks.removeAllHooks() })
|
hooks.hookOnce('close', () => { hooks.removeAllHooks() })
|
||||||
|
|
||||||
return nuxt
|
return nuxt
|
||||||
@ -221,11 +237,6 @@ async function initNuxt (nuxt: Nuxt) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Set nuxt instance for useNuxt
|
|
||||||
nuxtCtx.set(nuxt)
|
|
||||||
nuxt.hook('close', () => nuxtCtx.unset())
|
|
||||||
|
|
||||||
const coreTypePackages = nuxt.options.typescript.hoist || []
|
const coreTypePackages = nuxt.options.typescript.hoist || []
|
||||||
|
|
||||||
// Disable environment types entirely if `typescript.builder` is false
|
// Disable environment types entirely if `typescript.builder` is false
|
||||||
@ -860,27 +871,29 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise<Nuxt> {
|
|||||||
|
|
||||||
const nuxt = createNuxt(options)
|
const nuxt = createNuxt(options)
|
||||||
|
|
||||||
if (nuxt.options.dev && !nuxt.options.test) {
|
nuxt.runWithContext(() => {
|
||||||
nuxt.hooks.hookOnce('build:done', () => {
|
if (nuxt.options.dev && !nuxt.options.test) {
|
||||||
for (const dep of keyDependencies) {
|
nuxt.hooks.hookOnce('build:done', () => {
|
||||||
checkDependencyVersion(dep, nuxt._version)
|
for (const dep of keyDependencies) {
|
||||||
.catch(e => logger.warn(`Problem checking \`${dep}\` version.`, e))
|
checkDependencyVersion(dep, nuxt._version)
|
||||||
}
|
.catch(e => logger.warn(`Problem checking \`${dep}\` version.`, e))
|
||||||
})
|
}
|
||||||
}
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// We register hooks layer-by-layer so any overrides need to be registered separately
|
// We register hooks layer-by-layer so any overrides need to be registered separately
|
||||||
if (opts.overrides?.hooks) {
|
if (opts.overrides?.hooks) {
|
||||||
nuxt.hooks.addHooks(opts.overrides.hooks)
|
nuxt.hooks.addHooks(opts.overrides.hooks)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
nuxt.options.debug
|
nuxt.options.debug
|
||||||
&& nuxt.options.debug.hooks
|
&& nuxt.options.debug.hooks
|
||||||
&& (nuxt.options.debug.hooks === true || nuxt.options.debug.hooks.server)
|
&& (nuxt.options.debug.hooks === true || nuxt.options.debug.hooks.server)
|
||||||
) {
|
) {
|
||||||
createDebugger(nuxt.hooks, { tag: 'nuxt' })
|
createDebugger(nuxt.hooks, { tag: 'nuxt' })
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
if (opts.ready !== false) {
|
if (opts.ready !== false) {
|
||||||
await nuxt.ready()
|
await nuxt.ready()
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { normalize } from 'pathe'
|
import { normalize } from 'pathe'
|
||||||
import { withoutTrailingSlash } from 'ufo'
|
import { withoutTrailingSlash } from 'ufo'
|
||||||
|
import { logger, tryUseNuxt, useNuxt } from '@nuxt/kit'
|
||||||
import { loadNuxt } from '../src'
|
import { loadNuxt } from '../src'
|
||||||
|
|
||||||
const repoRoot = withoutTrailingSlash(normalize(fileURLToPath(new URL('../../../', import.meta.url))))
|
const repoRoot = withoutTrailingSlash(normalize(fileURLToPath(new URL('../../../', import.meta.url))))
|
||||||
@ -12,6 +13,7 @@ vi.stubGlobal('console', {
|
|||||||
warn: vi.fn(console.warn),
|
warn: vi.fn(console.warn),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const loggerWarn = vi.spyOn(logger, 'warn')
|
||||||
vi.mock('pkg-types', async (og) => {
|
vi.mock('pkg-types', async (og) => {
|
||||||
const originalPkgTypes = (await og<typeof import('pkg-types')>())
|
const originalPkgTypes = (await og<typeof import('pkg-types')>())
|
||||||
return {
|
return {
|
||||||
@ -20,6 +22,9 @@ vi.mock('pkg-types', async (og) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
loggerWarn.mockClear()
|
||||||
|
})
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
})
|
})
|
||||||
@ -41,4 +46,41 @@ describe('loadNuxt', () => {
|
|||||||
await nuxt.close()
|
await nuxt.close()
|
||||||
expect(hookRan).toBe(true)
|
expect(hookRan).toBe(true)
|
||||||
})
|
})
|
||||||
|
it('load multiple nuxt', async () => {
|
||||||
|
await Promise.all([
|
||||||
|
loadNuxt({
|
||||||
|
cwd: repoRoot,
|
||||||
|
}),
|
||||||
|
loadNuxt({
|
||||||
|
cwd: repoRoot,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
expect(loggerWarn).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('expect hooks to get the correct context outside of initNuxt', async () => {
|
||||||
|
const nuxt = await loadNuxt({
|
||||||
|
cwd: repoRoot,
|
||||||
|
})
|
||||||
|
|
||||||
|
// @ts-expect-error - random hook
|
||||||
|
nuxt.hook('test', () => {
|
||||||
|
expect(useNuxt().__name).toBe(nuxt.__name)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(tryUseNuxt()?.__name).not.toBe(nuxt.__name)
|
||||||
|
|
||||||
|
// second nuxt context
|
||||||
|
const second = await loadNuxt({
|
||||||
|
cwd: repoRoot,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(second.__name).not.toBe(nuxt.__name)
|
||||||
|
expect(tryUseNuxt()?.__name).not.toBe(nuxt.__name)
|
||||||
|
|
||||||
|
// @ts-expect-error - random hook
|
||||||
|
await nuxt.callHook('test')
|
||||||
|
|
||||||
|
expect(loggerWarn).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -83,6 +83,7 @@ export interface NuxtApp {
|
|||||||
|
|
||||||
export interface Nuxt {
|
export interface Nuxt {
|
||||||
// Private fields.
|
// Private fields.
|
||||||
|
__name: string
|
||||||
_version: string
|
_version: string
|
||||||
_ignore?: Ignore
|
_ignore?: Ignore
|
||||||
_dependencies?: Set<string>
|
_dependencies?: Set<string>
|
||||||
@ -96,6 +97,7 @@ export interface Nuxt {
|
|||||||
hook: Nuxt['hooks']['hook']
|
hook: Nuxt['hooks']['hook']
|
||||||
callHook: Nuxt['hooks']['callHook']
|
callHook: Nuxt['hooks']['callHook']
|
||||||
addHooks: Nuxt['hooks']['addHooks']
|
addHooks: Nuxt['hooks']['addHooks']
|
||||||
|
runWithContext: <T extends (...args: any[]) => any>(fn: T) => ReturnType<T>
|
||||||
|
|
||||||
ready: () => Promise<void>
|
ready: () => Promise<void>
|
||||||
close: () => Promise<void>
|
close: () => Promise<void>
|
||||||
|
Loading…
Reference in New Issue
Block a user