mirror of
https://github.com/nuxt/nuxt.git
synced 2025-02-12 19:58:10 +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'
|
||||
|
||||
/** 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')
|
||||
|
||||
/** 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
|
||||
|
||||
/**
|
||||
@ -16,7 +29,7 @@ export const nuxtCtx = getContext<Nuxt>('nuxt')
|
||||
* ```
|
||||
*/
|
||||
export function useNuxt (): Nuxt {
|
||||
const instance = nuxtCtx.tryUse()
|
||||
const instance = asyncNuxtStorage.tryUse() || nuxtCtx.tryUse()
|
||||
if (!instance) {
|
||||
throw new Error('Nuxt instance is unavailable!')
|
||||
}
|
||||
@ -36,5 +49,9 @@ export function useNuxt (): Nuxt {
|
||||
* ```
|
||||
*/
|
||||
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 { addComponent, addComponentsDir } 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 { addLayout } from './layout'
|
||||
export { addRouteMiddleware, extendPages, extendRouteRules } from './pages'
|
||||
|
@ -2,6 +2,7 @@ import { pathToFileURL } from 'node:url'
|
||||
import { readPackageJSON, resolvePackageJSON } from 'pkg-types'
|
||||
import type { Nuxt } from '@nuxt/schema'
|
||||
import { importModule, tryImportModule } from '../internal/esm'
|
||||
import { runWithNuxtContext } from '../context'
|
||||
import type { LoadNuxtConfigOptions } from './config'
|
||||
|
||||
export interface LoadNuxtOptions extends LoadNuxtConfigOptions {
|
||||
@ -76,10 +77,10 @@ export async function buildNuxt (nuxt: Nuxt): Promise<any> {
|
||||
// Nuxt 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 })
|
||||
return build(nuxt)
|
||||
return runWithNuxtContext(nuxt, () => build(nuxt))
|
||||
}
|
||||
|
||||
// Nuxt 2
|
||||
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 { rm } from 'node:fs/promises'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { AsyncLocalStorage } from 'node:async_hooks'
|
||||
import { join, normalize, relative, resolve } from 'pathe'
|
||||
import { createDebugger, createHooks } from 'hookable'
|
||||
import ignore from 'ignore'
|
||||
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 { PackageJson } from 'pkg-types'
|
||||
import { readPackageJSON } from 'pkg-types'
|
||||
@ -52,17 +53,24 @@ import { VirtualFSPlugin } from './plugins/virtual'
|
||||
export function createNuxt (options: NuxtOptions): Nuxt {
|
||||
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 = {
|
||||
__name: randomUUID(),
|
||||
_version: version,
|
||||
_asyncLocalStorageModule: options.experimental.debugModuleMutation ? new AsyncLocalStorage() : undefined,
|
||||
hooks,
|
||||
callHook: hooks.callHook,
|
||||
addHooks: hooks.addHooks,
|
||||
hook: hooks.hook,
|
||||
ready: () => initNuxt(nuxt),
|
||||
ready: () => runWithNuxtContext(nuxt, () => initNuxt(nuxt)),
|
||||
close: () => hooks.callHook('close', nuxt),
|
||||
vfs: {},
|
||||
apps: {},
|
||||
runWithContext: fn => runWithNuxtContext(nuxt, fn),
|
||||
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() })
|
||||
|
||||
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 || []
|
||||
|
||||
// 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)
|
||||
|
||||
if (nuxt.options.dev && !nuxt.options.test) {
|
||||
nuxt.hooks.hookOnce('build:done', () => {
|
||||
for (const dep of keyDependencies) {
|
||||
checkDependencyVersion(dep, nuxt._version)
|
||||
.catch(e => logger.warn(`Problem checking \`${dep}\` version.`, e))
|
||||
}
|
||||
})
|
||||
}
|
||||
nuxt.runWithContext(() => {
|
||||
if (nuxt.options.dev && !nuxt.options.test) {
|
||||
nuxt.hooks.hookOnce('build:done', () => {
|
||||
for (const dep of keyDependencies) {
|
||||
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
|
||||
if (opts.overrides?.hooks) {
|
||||
nuxt.hooks.addHooks(opts.overrides.hooks)
|
||||
}
|
||||
// We register hooks layer-by-layer so any overrides need to be registered separately
|
||||
if (opts.overrides?.hooks) {
|
||||
nuxt.hooks.addHooks(opts.overrides.hooks)
|
||||
}
|
||||
|
||||
if (
|
||||
nuxt.options.debug
|
||||
&& nuxt.options.debug.hooks
|
||||
&& (nuxt.options.debug.hooks === true || nuxt.options.debug.hooks.server)
|
||||
) {
|
||||
createDebugger(nuxt.hooks, { tag: 'nuxt' })
|
||||
}
|
||||
if (
|
||||
nuxt.options.debug
|
||||
&& nuxt.options.debug.hooks
|
||||
&& (nuxt.options.debug.hooks === true || nuxt.options.debug.hooks.server)
|
||||
) {
|
||||
createDebugger(nuxt.hooks, { tag: 'nuxt' })
|
||||
}
|
||||
})
|
||||
|
||||
if (opts.ready !== false) {
|
||||
await nuxt.ready()
|
||||
|
@ -1,7 +1,8 @@
|
||||
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 { withoutTrailingSlash } from 'ufo'
|
||||
import { logger, tryUseNuxt, useNuxt } from '@nuxt/kit'
|
||||
import { loadNuxt } from '../src'
|
||||
|
||||
const repoRoot = withoutTrailingSlash(normalize(fileURLToPath(new URL('../../../', import.meta.url))))
|
||||
@ -12,6 +13,7 @@ vi.stubGlobal('console', {
|
||||
warn: vi.fn(console.warn),
|
||||
})
|
||||
|
||||
const loggerWarn = vi.spyOn(logger, 'warn')
|
||||
vi.mock('pkg-types', async (og) => {
|
||||
const originalPkgTypes = (await og<typeof import('pkg-types')>())
|
||||
return {
|
||||
@ -20,6 +22,9 @@ vi.mock('pkg-types', async (og) => {
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
loggerWarn.mockClear()
|
||||
})
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
@ -41,4 +46,41 @@ describe('loadNuxt', () => {
|
||||
await nuxt.close()
|
||||
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 {
|
||||
// Private fields.
|
||||
__name: string
|
||||
_version: string
|
||||
_ignore?: Ignore
|
||||
_dependencies?: Set<string>
|
||||
@ -96,6 +97,7 @@ export interface Nuxt {
|
||||
hook: Nuxt['hooks']['hook']
|
||||
callHook: Nuxt['hooks']['callHook']
|
||||
addHooks: Nuxt['hooks']['addHooks']
|
||||
runWithContext: <T extends (...args: any[]) => any>(fn: T) => ReturnType<T>
|
||||
|
||||
ready: () => Promise<void>
|
||||
close: () => Promise<void>
|
||||
|
Loading…
Reference in New Issue
Block a user