diff --git a/packages/kit/src/module/install.ts b/packages/kit/src/module/install.ts index ba16c59496..44fead197f 100644 --- a/packages/kit/src/module/install.ts +++ b/packages/kit/src/module/install.ts @@ -28,7 +28,9 @@ export async function installModule< } // 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 */) { return } @@ -53,6 +55,7 @@ export async function installModule< nuxt.options._installedModules.push({ meta: defu(await nuxtModule.getMeta?.(), buildTimeModuleMeta), + module: nuxtModule, timings: res.timings, entryPath, }) diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index 4cdc299c88..35ca8ccb68 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -104,6 +104,7 @@ "nypm": "^0.5.2", "ofetch": "^1.4.1", "ohash": "^1.1.4", + "on-change": "^5.0.1", "pathe": "^2.0.2", "perfect-debounce": "^1.0.0", "pkg-types": "^1.3.1", diff --git a/packages/nuxt/src/core/nuxt.ts b/packages/nuxt/src/core/nuxt.ts index a5f8b06ef7..7014ba7d61 100644 --- a/packages/nuxt/src/core/nuxt.ts +++ b/packages/nuxt/src/core/nuxt.ts @@ -1,5 +1,6 @@ import { existsSync } from 'node:fs' import { rm } from 'node:fs/promises' +import { AsyncLocalStorage } from 'node:async_hooks' import { join, normalize, relative, resolve } from 'pathe' import { createDebugger, createHooks } from 'hookable' import ignore from 'ignore' @@ -10,6 +11,7 @@ import type { PackageJson } from 'pkg-types' import { readPackageJSON } from 'pkg-types' import { hash } from 'ohash' import consola from 'consola' +import onChange from 'on-change' import { colorize } from 'consola/utils' import { updateConfig } from 'c12/update' import { formatDate, resolveCompatibilityDatesFromEnv } from 'compatx' @@ -53,7 +55,7 @@ export function createNuxt (options: NuxtOptions): Nuxt { const nuxt: Nuxt = { _version: version, - options, + _asyncLocalStorageModule: options.experimental.debugModuleMutation ? new AsyncLocalStorage() : undefined, hooks, callHook: hooks.callHook, addHooks: hooks.addHooks, @@ -62,6 +64,55 @@ export function createNuxt (options: NuxtOptions): Nuxt { close: () => hooks.callHook('close', nuxt), vfs: {}, apps: {}, + options, + } + + if (options.experimental.debugModuleMutation) { + const proxiedOptions = new WeakMap() + + 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() }) diff --git a/packages/schema/src/config/experimental.ts b/packages/schema/src/config/experimental.ts index 90c0bc4add..bd627972a0 100644 --- a/packages/schema/src/config/experimental.ts +++ b/packages/schema/src/config/experimental.ts @@ -437,5 +437,14 @@ export default defineUntypedSchema({ browserDevtoolsTiming: { $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')) + }, + }, }, }) diff --git a/packages/schema/src/config/internal.ts b/packages/schema/src/config/internal.ts index 77c79bd5bc..0207964f64 100644 --- a/packages/schema/src/config/internal.ts +++ b/packages/schema/src/config/internal.ts @@ -25,7 +25,7 @@ export default defineUntypedSchema({ appDir: '', /** * @private - * @type {Array<{ meta: ModuleMeta; timings?: Record; entryPath?: string }>} + * @type {Array<{ meta: ModuleMeta; module: NuxtModule, timings?: Record; entryPath?: string }>} */ _installedModules: [], /** @private */ diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts index 8ecabf766a..40294bcdce 100644 --- a/packages/schema/src/index.ts +++ b/packages/schema/src/index.ts @@ -8,7 +8,7 @@ export type { AppHeadMetaObject, MetaObject, MetaObjectRaw, HeadAugmentations } 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 { RouterConfig, RouterConfigSerializable, RouterOptions } from './types/router' -export type { NuxtDebugOptions } from './types/debug' +export type { NuxtDebugContext, NuxtDebugModuleMutationRecord } from './types/debug' // Schema export { default as NuxtConfigSchema } from './config/index' diff --git a/packages/schema/src/types/debug.ts b/packages/schema/src/types/debug.ts index 9ea76b7fc0..d08b4d1530 100644 --- a/packages/schema/src/types/debug.ts +++ b/packages/schema/src/types/debug.ts @@ -1,4 +1,21 @@ 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 { /** Debug for Nuxt templates */ diff --git a/packages/schema/src/types/nuxt.ts b/packages/schema/src/types/nuxt.ts index ab9d1a2eeb..5e50e7adaf 100644 --- a/packages/schema/src/types/nuxt.ts +++ b/packages/schema/src/types/nuxt.ts @@ -1,8 +1,11 @@ +import type { AsyncLocalStorage } from 'node:async_hooks' import type { Hookable } from 'hookable' import type { Ignore } from 'ignore' +import type { NuxtModule } from './module' import type { NuxtHooks, NuxtLayout, NuxtMiddleware, NuxtPage } from './hooks' import type { Component } from './components' import type { NuxtOptions } from './config' +import type { NuxtDebugContext } from './debug' export interface NuxtPlugin { /** @deprecated use mode */ @@ -83,6 +86,9 @@ export interface Nuxt { _version: string _ignore?: Ignore _dependencies?: Set + _debug?: NuxtDebugContext + /** Async local storage for current running Nuxt module instance. */ + _asyncLocalStorageModule?: AsyncLocalStorage /** The resolved Nuxt configuration. */ options: NuxtOptions diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 927f0cd79f..77f561f5d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -444,6 +444,9 @@ importers: ohash: specifier: 1.1.4 version: 1.1.4 + on-change: + specifier: ^5.0.1 + version: 5.0.1 pathe: specifier: ^2.0.2 version: 2.0.2 @@ -6064,6 +6067,10 @@ packages: ohash@1.1.4: 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: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -14067,6 +14074,8 @@ snapshots: ohash@1.1.4: {} + on-change@5.0.1: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1