From bb54b77df89723e96e065fade30471dcf911f83f Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Thu, 14 May 2026 14:15:10 +1000 Subject: [PATCH 1/2] feat(scripts): build-time `debug` flag with script-lifecycle tracing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adds `scripts.debug` module option that injects a `__NUXT_SCRIPTS_DEBUG__` build-time constant via Vite `define` / Nitro `replace`. Debug branches DCE in production when `debug: false` (the default). - New `runtime/debug.ts` exports `debugEnabled` (reads the constant) for guard checks in runtime code. - `runtime/logger.ts` opts the consola instance up to `level: 4` when `debugEnabled`, so `logger.debug` calls actually fire. - `useScript` now traces every script's lifecycle through consola when debug is enabled: `registered` (with src + trigger + caller), every `script:updated` status change (`awaitingLoad → loading → loaded`, `error`, `removed`, …), and explicit `load() / remove() / reload()` invocations. Each log carries a `{ id, registryKey, src, loadedFrom, elapsedMs, loadMs }` payload, tagged with the registryKey when present. All wired off the existing unhead `script:updated` hook (no new hook surface). --- packages/script/src/module.ts | 8 +++ .../src/runtime/composables/useScript.ts | 53 +++++++++++++++++++ packages/script/src/runtime/debug.ts | 4 ++ packages/script/src/runtime/logger.ts | 4 ++ 4 files changed, 69 insertions(+) create mode 100644 packages/script/src/runtime/debug.ts diff --git a/packages/script/src/module.ts b/packages/script/src/module.ts index 1d5652099..e32da9c96 100644 --- a/packages/script/src/module.ts +++ b/packages/script/src/module.ts @@ -563,6 +563,14 @@ export default defineNuxtModule({ : undefined, } as any + // Build-time constant: `__NUXT_SCRIPTS_DEBUG__` is replaced inline by the + // bundler, so debug branches DCE away in production when `debug: false`. + const debugConst = JSON.stringify(!!config.debug) + nuxt.options.vite ||= {} + nuxt.options.vite.define = { ...nuxt.options.vite.define, __NUXT_SCRIPTS_DEBUG__: debugConst } + nuxt.options.nitro ||= {} + nuxt.options.nitro.replace = { ...nuxt.options.nitro.replace, __NUXT_SCRIPTS_DEBUG__: debugConst } + // Register proxy handler unconditionally. The handler rejects unknown domains // at runtime, so it's safe to register even when no scripts use proxy. const scriptsBase = config.prefix || '/_scripts' diff --git a/packages/script/src/runtime/composables/useScript.ts b/packages/script/src/runtime/composables/useScript.ts index d67511381..2f5e5cb2a 100644 --- a/packages/script/src/runtime/composables/useScript.ts +++ b/packages/script/src/runtime/composables/useScript.ts @@ -6,6 +6,7 @@ import { injectHead, onNuxtReady, useHead, useNuxtApp, useRuntimeConfig } from ' import { markRaw, ref } from 'vue' // @ts-expect-error virtual template import { resolveTrigger } from '#build/nuxt-scripts-trigger-resolver' +import { debugEnabled } from '../debug' import { logger } from '../logger' type NuxtScriptsApp = ReturnType & { @@ -293,6 +294,58 @@ export function useScript = Record { + if (entry.script.id !== instance.id) + return + const status = entry.script.status + const elapsed = Math.round(performance.now() - t0) + if (status === 'loading') + tLoadStart = performance.now() + const payload: Record = { ...ctx, status, elapsedMs: elapsed } + if (status === 'loaded' && tLoadStart) + payload.loadMs = Math.round(performance.now() - tLoadStart) + const fn = status === 'error' ? log.warn : log.debug + fn(`status: ${status}`, payload) + }) + const _origLoad = instance.load + instance.load = () => { + log.debug('load() called', ctx) + return _origLoad() + } + const _origRemove = instance.remove + instance.remove = () => { + log.debug('remove() called', ctx) + return _origRemove() + } + const _origReload = instance.reload + instance.reload = async () => { + log.debug('reload() called', ctx) + return _origReload() + } + } + // used for devtools integration if (import.meta.dev && import.meta.client) { if (exists) { diff --git a/packages/script/src/runtime/debug.ts b/packages/script/src/runtime/debug.ts new file mode 100644 index 000000000..6c74abf8c --- /dev/null +++ b/packages/script/src/runtime/debug.ts @@ -0,0 +1,4 @@ +declare const __NUXT_SCRIPTS_DEBUG__: boolean + +export const debugEnabled: boolean + = typeof __NUXT_SCRIPTS_DEBUG__ !== 'undefined' && __NUXT_SCRIPTS_DEBUG__ diff --git a/packages/script/src/runtime/logger.ts b/packages/script/src/runtime/logger.ts index e7e56c5ae..413357ed0 100644 --- a/packages/script/src/runtime/logger.ts +++ b/packages/script/src/runtime/logger.ts @@ -1,6 +1,10 @@ import { createConsola } from 'consola' +import { debugEnabled } from './debug' export const logger = createConsola({ + // 4 = debug, 3 = info (consola defaults). Lift the threshold so `logger.debug` + // fires when debug is opted-in at build time or in dev. + level: debugEnabled ? 4 : 3, defaults: { tag: 'nuxt-scripts', }, From 51de226a3269c88a54da6b70687e20309a3ddf1b Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Thu, 14 May 2026 14:20:31 +1000 Subject: [PATCH 2/2] fix(types): declare `devtools.loadedFrom` on NuxtUseScriptOptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `useScript` reads `options?.devtools?.loadedFrom` for debug-log context and `useRegistryScript`/`devtools-standalone-bridge.client` write to it, but the field wasn't declared on the `devtools` shape — caused a typecheck failure flagged by CodeRabbit on #760. --- packages/script/src/runtime/types.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/script/src/runtime/types.ts b/packages/script/src/runtime/types.ts index bbbaf1af6..8419fc27b 100644 --- a/packages/script/src/runtime/types.ts +++ b/packages/script/src/runtime/types.ts @@ -168,6 +168,13 @@ export type NuxtUseScriptOptions = {}> = * @internal */ registryMeta?: Record + /** + * Source location (file:line:col) the script was registered from, captured + * via dev-only stack-trace parsing in `useRegistryScript`. Surfaced in + * debug logs and Nuxt DevTools. + * @internal + */ + loadedFrom?: string /** * Known third-party domains this script communicates with. * @internal