feat(nuxt): support tracking changes to nuxt options by modules (#30555)

This commit is contained in:
Anthony Fu 2025-02-07 05:56:58 +08:00 committed by GitHub
parent 5e7d4938cd
commit 090bc6d7da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 100 additions and 4 deletions

View File

@ -28,7 +28,9 @@ export async function installModule<
} }
// Call module // Call module
const res = await nuxtModule(inlineOptions || {}, nuxt) ?? {} const res = nuxt.options.experimental?.debugModuleMutation && nuxt._asyncLocalStorageModule
? await nuxt._asyncLocalStorageModule.run(nuxtModule, () => nuxtModule(inlineOptions || {}, nuxt)) ?? {}
: await nuxtModule(inlineOptions || {}, nuxt) ?? {}
if (res === false /* setup aborted */) { if (res === false /* setup aborted */) {
return return
} }
@ -53,6 +55,7 @@ export async function installModule<
nuxt.options._installedModules.push({ nuxt.options._installedModules.push({
meta: defu(await nuxtModule.getMeta?.(), buildTimeModuleMeta), meta: defu(await nuxtModule.getMeta?.(), buildTimeModuleMeta),
module: nuxtModule,
timings: res.timings, timings: res.timings,
entryPath, entryPath,
}) })

View File

@ -104,6 +104,7 @@
"nypm": "^0.5.2", "nypm": "^0.5.2",
"ofetch": "^1.4.1", "ofetch": "^1.4.1",
"ohash": "^1.1.4", "ohash": "^1.1.4",
"on-change": "^5.0.1",
"pathe": "^2.0.2", "pathe": "^2.0.2",
"perfect-debounce": "^1.0.0", "perfect-debounce": "^1.0.0",
"pkg-types": "^1.3.1", "pkg-types": "^1.3.1",

View File

@ -1,5 +1,6 @@
import { existsSync } from 'node:fs' import { existsSync } from 'node:fs'
import { rm } from 'node:fs/promises' import { rm } from 'node:fs/promises'
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'
@ -10,6 +11,7 @@ import type { PackageJson } from 'pkg-types'
import { readPackageJSON } from 'pkg-types' import { readPackageJSON } from 'pkg-types'
import { hash } from 'ohash' import { hash } from 'ohash'
import consola from 'consola' import consola from 'consola'
import onChange from 'on-change'
import { colorize } from 'consola/utils' import { colorize } from 'consola/utils'
import { updateConfig } from 'c12/update' import { updateConfig } from 'c12/update'
import { formatDate, resolveCompatibilityDatesFromEnv } from 'compatx' import { formatDate, resolveCompatibilityDatesFromEnv } from 'compatx'
@ -53,7 +55,7 @@ export function createNuxt (options: NuxtOptions): Nuxt {
const nuxt: Nuxt = { const nuxt: Nuxt = {
_version: version, _version: version,
options, _asyncLocalStorageModule: options.experimental.debugModuleMutation ? new AsyncLocalStorage() : undefined,
hooks, hooks,
callHook: hooks.callHook, callHook: hooks.callHook,
addHooks: hooks.addHooks, addHooks: hooks.addHooks,
@ -62,6 +64,55 @@ export function createNuxt (options: NuxtOptions): Nuxt {
close: () => hooks.callHook('close', nuxt), close: () => hooks.callHook('close', nuxt),
vfs: {}, vfs: {},
apps: {}, apps: {},
options,
}
if (options.experimental.debugModuleMutation) {
const proxiedOptions = new WeakMap<NuxtModule, NuxtOptions>()
Object.defineProperty(nuxt, 'options', {
get () {
const currentModule = nuxt._asyncLocalStorageModule!.getStore()
if (!currentModule) {
return options
}
if (proxiedOptions.has(currentModule)) {
return proxiedOptions.get(currentModule)!
}
nuxt._debug ||= {}
nuxt._debug.moduleMutationRecords ||= []
const proxied = onChange(options, (keys, newValue, previousValue, applyData) => {
if (newValue === previousValue && !applyData) {
return
}
let value = applyData?.args ?? newValue
// Make a shallow copy of the value
if (Array.isArray(value)) {
value = [...value]
} else if (typeof value === 'object') {
value = { ...(value as any) }
}
nuxt._debug!.moduleMutationRecords!.push({
module: currentModule,
keys,
target: 'nuxt.options',
value,
timestamp: Date.now(),
method: applyData?.name,
})
}, {
ignoreUnderscores: true,
ignoreSymbols: true,
pathAsArray: true,
})
proxiedOptions.set(currentModule, proxied)
return proxied
},
})
} }
hooks.hookOnce('close', () => { hooks.removeAllHooks() }) hooks.hookOnce('close', () => { hooks.removeAllHooks() })

View File

@ -437,5 +437,14 @@ export default defineUntypedSchema({
browserDevtoolsTiming: { browserDevtoolsTiming: {
$resolve: async (val, get) => val ?? await get('dev'), $resolve: async (val, get) => val ?? await get('dev'),
}, },
/**
* Record mutations to `nuxt.options` in module context
*/
debugModuleMutation: {
$resolve: async (val, get) => {
return val ?? Boolean(await get('debug'))
},
},
}, },
}) })

View File

@ -25,7 +25,7 @@ export default defineUntypedSchema({
appDir: '', appDir: '',
/** /**
* @private * @private
* @type {Array<{ meta: ModuleMeta; timings?: Record<string, number | undefined>; entryPath?: string }>} * @type {Array<{ meta: ModuleMeta; module: NuxtModule, timings?: Record<string, number | undefined>; entryPath?: string }>}
*/ */
_installedModules: [], _installedModules: [],
/** @private */ /** @private */

View File

@ -8,7 +8,7 @@ export type { AppHeadMetaObject, MetaObject, MetaObjectRaw, HeadAugmentations }
export type { ModuleDefinition, ModuleMeta, ModuleOptions, ModuleSetupInstallResult, ModuleSetupReturn, NuxtModule, ResolvedModuleOptions } from './types/module' export type { ModuleDefinition, ModuleMeta, ModuleOptions, ModuleSetupInstallResult, ModuleSetupReturn, NuxtModule, ResolvedModuleOptions } from './types/module'
export type { Nuxt, NuxtApp, NuxtPlugin, NuxtPluginTemplate, NuxtTemplate, NuxtTypeTemplate, NuxtServerTemplate, ResolvedNuxtTemplate } from './types/nuxt' export type { Nuxt, NuxtApp, NuxtPlugin, NuxtPluginTemplate, NuxtTemplate, NuxtTypeTemplate, NuxtServerTemplate, ResolvedNuxtTemplate } from './types/nuxt'
export type { RouterConfig, RouterConfigSerializable, RouterOptions } from './types/router' export type { RouterConfig, RouterConfigSerializable, RouterOptions } from './types/router'
export type { NuxtDebugOptions } from './types/debug' export type { NuxtDebugContext, NuxtDebugModuleMutationRecord } from './types/debug'
// Schema // Schema
export { default as NuxtConfigSchema } from './config/index' export { default as NuxtConfigSchema } from './config/index'

View File

@ -1,4 +1,21 @@
import type { NitroOptions } from 'nitro/types' import type { NitroOptions } from 'nitro/types'
import type { NuxtModule } from './module'
export interface NuxtDebugContext {
/**
* Module mutation records to the `nuxt` instance.
*/
moduleMutationRecords?: NuxtDebugModuleMutationRecord[]
}
export interface NuxtDebugModuleMutationRecord {
module: NuxtModule
keys: (string | symbol)[]
target: 'nuxt.options'
value: any
method?: string
timestamp: number
}
export interface NuxtDebugOptions { export interface NuxtDebugOptions {
/** Debug for Nuxt templates */ /** Debug for Nuxt templates */

View File

@ -1,8 +1,11 @@
import type { AsyncLocalStorage } from 'node:async_hooks'
import type { Hookable } from 'hookable' import type { Hookable } from 'hookable'
import type { Ignore } from 'ignore' import type { Ignore } from 'ignore'
import type { NuxtModule } from './module'
import type { NuxtHooks, NuxtLayout, NuxtMiddleware, NuxtPage } from './hooks' import type { NuxtHooks, NuxtLayout, NuxtMiddleware, NuxtPage } from './hooks'
import type { Component } from './components' import type { Component } from './components'
import type { NuxtOptions } from './config' import type { NuxtOptions } from './config'
import type { NuxtDebugContext } from './debug'
export interface NuxtPlugin { export interface NuxtPlugin {
/** @deprecated use mode */ /** @deprecated use mode */
@ -83,6 +86,9 @@ export interface Nuxt {
_version: string _version: string
_ignore?: Ignore _ignore?: Ignore
_dependencies?: Set<string> _dependencies?: Set<string>
_debug?: NuxtDebugContext
/** Async local storage for current running Nuxt module instance. */
_asyncLocalStorageModule?: AsyncLocalStorage<NuxtModule>
/** The resolved Nuxt configuration. */ /** The resolved Nuxt configuration. */
options: NuxtOptions options: NuxtOptions

View File

@ -444,6 +444,9 @@ importers:
ohash: ohash:
specifier: 1.1.4 specifier: 1.1.4
version: 1.1.4 version: 1.1.4
on-change:
specifier: ^5.0.1
version: 5.0.1
pathe: pathe:
specifier: ^2.0.2 specifier: ^2.0.2
version: 2.0.2 version: 2.0.2
@ -6064,6 +6067,10 @@ packages:
ohash@1.1.4: ohash@1.1.4:
resolution: {integrity: sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==} resolution: {integrity: sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==}
on-change@5.0.1:
resolution: {integrity: sha512-n7THCP7RkyReRSLkJb8kUWoNsxUIBxTkIp3JKno+sEz6o/9AJ3w3P9fzQkITEkMwyTKJjZciF3v/pVoouxZZMg==}
engines: {node: '>=18'}
on-finished@2.4.1: on-finished@2.4.1:
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@ -14067,6 +14074,8 @@ snapshots:
ohash@1.1.4: {} ohash@1.1.4: {}
on-change@5.0.1: {}
on-finished@2.4.1: on-finished@2.4.1:
dependencies: dependencies:
ee-first: 1.1.1 ee-first: 1.1.1