From 96b8d40f34ee1dd5f2d6accc2c00ed9534b2b967 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 23 Feb 2024 17:35:19 +0000 Subject: [PATCH 01/16] feat(nuxt): pass + stream server logs to client --- docs/3.api/6.advanced/1.hooks.md | 1 + .../src/app/plugins/dev-server-logs.client.ts | 80 ++++++++++++++ packages/nuxt/src/app/types/augments.d.ts | 2 + packages/nuxt/src/core/nuxt.ts | 15 ++- .../src/core/runtime/nitro/dev-server-logs.ts | 101 ++++++++++++++++++ packages/nuxt/src/core/templates.ts | 1 + packages/schema/src/config/experimental.ts | 11 ++ 7 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 packages/nuxt/src/app/plugins/dev-server-logs.client.ts create mode 100644 packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts diff --git a/docs/3.api/6.advanced/1.hooks.md b/docs/3.api/6.advanced/1.hooks.md index 35520e27c314..38288f728a9b 100644 --- a/docs/3.api/6.advanced/1.hooks.md +++ b/docs/3.api/6.advanced/1.hooks.md @@ -28,6 +28,7 @@ Hook | Arguments | Environment | Description `page:loading:start` | - | Client | Called when the `setup()` of the new page is running. `page:loading:end` | - | Client | Called after `page:finish` `page:transition:finish`| `pageComponent?` | Client | After page transition [onAfterLeave](https://vuejs.org/guide/built-ins/transition.html#javascript-hooks) event. +`dev:ssr-logs` | `logs` | Client | Called with an array of server-side logs that have been passed to the client (if `features.devLogs` is enabled). ## Nuxt Hooks (build time) diff --git a/packages/nuxt/src/app/plugins/dev-server-logs.client.ts b/packages/nuxt/src/app/plugins/dev-server-logs.client.ts new file mode 100644 index 000000000000..50f18ef8a1f5 --- /dev/null +++ b/packages/nuxt/src/app/plugins/dev-server-logs.client.ts @@ -0,0 +1,80 @@ +import { consola, createConsola } from 'consola' +import type { LogObject } from 'consola' +import { isAbsolute } from 'pathe' + +import { defineNuxtPlugin } from '../nuxt' + +// @ts-expect-error virtual file +import { devLogs, devRootDir } from '#build/nuxt.config.mjs' + +declare module '#app' { + interface RuntimeNuxtHooks { + 'dev:ssr-logs': (logs: LogObject[]) => void | Promise + } +} + +export default defineNuxtPlugin(nuxtApp => { + // Show things in console + if (devLogs !== 'silent') { + const logger = createConsola({ + formatOptions: { + colors: true, + date: true, + } + }) + const hydrationLogs = new Set() + consola.wrapAll() + consola.addReporter({ + log (logObj) { + try { + hydrationLogs.add(JSON.stringify(logObj.args)) + } catch { + // silently ignore - the worst case is a user gets log twice + } + } + }) + nuxtApp.hook('dev:ssr-logs', logs => { + for (const log of logs) { + // deduplicate so we don't print out things that are logged on client + if (!hydrationLogs.size || !hydrationLogs.has(JSON.stringify(log.args))) { + logger.log(normalizeServerLog({ ...log })) + } + } + }) + + nuxtApp.hooks.hook('app:suspense:resolve', () => logger.restoreAll()) + nuxtApp.hooks.hookOnce('dev:ssr-logs', () => hydrationLogs.clear()) + } + + // pass SSR logs after hydration + nuxtApp.hooks.hook('app:suspense:resolve', async () => { + if (window && window.__NUXT_LOGS__) { + await nuxtApp.hooks.callHook('dev:ssr-logs', window.__NUXT_LOGS__) + } + }) + + // initialise long-running SSE connection + const source = new EventSource('/_nuxt_logs') + source.onmessage = (event) => { + const log = JSON.parse(event.data) as LogObject + log.date = new Date(log.date) + nuxtApp.hooks.callHook('dev:ssr-logs', [log]) + } +}) + +function normalizeFilenames (stack?: string) { + return stack?.replace(/at.*\(([^)]+)\)/g, (match, filename) => { + if (!isAbsolute(filename)) { return match } + // TODO: normalise file names for clickable links in console + return match.replace(filename, filename.replace(devRootDir, '')) + }) +} + +function normalizeServerLog (log: LogObject) { + if (log.type === 'error' || log.type === 'warn') { + log.additional = normalizeFilenames(log.stack as string) + } + log.tag = `[ssr]${log.filename ? ` ${log.filename}` : ''}${log.tag || ''}` + delete log.stack + return log +} diff --git a/packages/nuxt/src/app/types/augments.d.ts b/packages/nuxt/src/app/types/augments.d.ts index e9ea30b9274c..1f9a89e8bf7b 100644 --- a/packages/nuxt/src/app/types/augments.d.ts +++ b/packages/nuxt/src/app/types/augments.d.ts @@ -1,4 +1,5 @@ import type { UseHeadInput } from '@unhead/vue' +import type { LogObject } from 'consola' import type { NuxtApp, useNuxtApp } from '../nuxt' interface NuxtStaticBuildFlags { @@ -17,6 +18,7 @@ declare global { interface ImportMeta extends NuxtStaticBuildFlags {} interface Window { + __NUXT_LOGS__?: LogObject[] __NUXT__?: Record useNuxtApp?: typeof useNuxtApp } diff --git a/packages/nuxt/src/core/nuxt.ts b/packages/nuxt/src/core/nuxt.ts index adc2aac1fae9..f5a5dc93681f 100644 --- a/packages/nuxt/src/core/nuxt.ts +++ b/packages/nuxt/src/core/nuxt.ts @@ -1,7 +1,7 @@ import { join, normalize, relative, resolve } from 'pathe' import { createDebugger, createHooks } from 'hookable' import type { LoadNuxtOptions } from '@nuxt/kit' -import { addBuildPlugin, addComponent, addPlugin, addRouteMiddleware, addVitePlugin, addWebpackPlugin, installModule, loadNuxtConfig, logger, nuxtCtx, resolveAlias, resolveFiles, resolvePath, tryResolveModule, useNitro } from '@nuxt/kit' +import { addBuildPlugin, addComponent, addPlugin, addRouteMiddleware, addServerPlugin, addVitePlugin, addWebpackPlugin, installModule, loadNuxtConfig, logger, nuxtCtx, resolveAlias, resolveFiles, resolvePath, tryResolveModule, useNitro } from '@nuxt/kit' import type { Nuxt, NuxtHooks, NuxtOptions } from 'nuxt/schema' import escapeRE from 'escape-string-regexp' @@ -158,6 +158,19 @@ async function initNuxt (nuxt: Nuxt) { addPlugin(resolve(nuxt.options.appDir, 'plugins/check-if-layout-used')) } + if (nuxt.options.dev && nuxt.options.features.devLogs) { + addPlugin(resolve(nuxt.options.appDir, 'plugins/dev-server-logs.client')) + addServerPlugin(resolve(distDir, 'core/runtime/nitro/dev-server-logs')) + nuxt.options.nitro = defu(nuxt.options.nitro, { + externals: { + inline: [/#internal\/dev-server-logs-options/] + }, + virtual: { + ['#internal/dev-server-logs-options']: () => `export const rootDir = ${JSON.stringify(nuxt.options.rootDir)};` + } + }) + } + // Transform initial composable call within ``) + logs.length = 0 + }) +} + +const EXCLUDE_TRACE_RE = new RegExp('^.*at.*(\\/node_modules\\/(.*\\/)?(nuxt|consola|@vue)\\/.*|core\\/runtime\\/nitro.*)$\\n?', 'gm') +function getStack () { + // Pass along stack traces if needed (for error and warns) + const stack = new Error() + Error.captureStackTrace(stack) + return stack.stack?.replace(EXCLUDE_TRACE_RE, '').replace(/^Error.*\n/, '') || '' +} + +const FILENAME_RE = /at.*\(([^:)]+)[):]/ +const FILENAME_RE_GLOBAL = /at.*\(([^)]+)\)/g +function extractFilenameFromStack (stacktrace: string) { + return stacktrace.match(FILENAME_RE)?.[1].replace(withTrailingSlash(rootDir), '') +} +function normalizeFilenames (stacktrace: string) { + // remove line numbers and file: protocol - TODO: sourcemap support for line numbers + return stacktrace.replace(FILENAME_RE_GLOBAL, (match, filename) => match.replace(filename, filename.replace('file:///', '/').replace(/:.*$/, ''))) +} + +function onConsoleLog (callback: (log: LogObject) => void) { + const logger = createConsola({ + reporters: [ + { + log (logObj) { + // Don't swallow log messages in console - is there a better way to do this @pi0? + // TODO: display (clickable) filename in server log as well when we use consola for this + (originalConsole[logObj.type as 'log'] || originalConsole.log)(...logObj.args) + + callback(logObj) + }, + } + ] + }) + logger.wrapAll() +} diff --git a/packages/nuxt/src/core/templates.ts b/packages/nuxt/src/core/templates.ts index 55ee480e135f..3d6afc1d9a1b 100644 --- a/packages/nuxt/src/core/templates.ts +++ b/packages/nuxt/src/core/templates.ts @@ -384,6 +384,7 @@ export const nuxtConfigTemplate: NuxtTemplate = { `export const selectiveClient = ${typeof ctx.nuxt.options.experimental.componentIslands === 'object' && ctx.nuxt.options.experimental.componentIslands.selectiveClient}`, `export const devPagesDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.dir.pages) : 'null'}`, `export const devRootDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.rootDir) : 'null'}`, + `export const devLogs = ${JSON.stringify(ctx.nuxt.options.features.devLogs)}`, `export const nuxtLinkDefaults = ${JSON.stringify(ctx.nuxt.options.experimental.defaults.nuxtLink)}`, `export const asyncDataDefaults = ${JSON.stringify(ctx.nuxt.options.experimental.defaults.useAsyncData)}`, `export const fetchDefaults = ${JSON.stringify(fetchDefaults)}`, diff --git a/packages/schema/src/config/experimental.ts b/packages/schema/src/config/experimental.ts index 5c969b70669f..f25cfa0d8d01 100644 --- a/packages/schema/src/config/experimental.ts +++ b/packages/schema/src/config/experimental.ts @@ -52,6 +52,17 @@ export default defineUntypedSchema({ } }, + /** + * Display logs on the client console that are normally only shown in the server console. + * + * This also enables an ongoing stream of server logs in the browser console as you are developing. + * + * If set to `silent`, the logs will be streamed and you can handle them yourself with the `dev:ssr-logs` hook, + * but they will not be shown in the browser console. + * @type {boolean | 'silent'} + */ + devLogs: true, + /** * Turn off rendering of Nuxt scripts and JS resource hints. * You can also disable scripts more granularly within `routeRules`. From ea096862ed43934479bb4f8f85a8bceda3c51ed9 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 23 Feb 2024 17:46:37 +0000 Subject: [PATCH 02/16] fix: move hook type to correct place --- packages/nuxt/src/app/nuxt.ts | 2 ++ packages/nuxt/src/app/plugins/dev-server-logs.client.ts | 6 ------ 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/nuxt/src/app/nuxt.ts b/packages/nuxt/src/app/nuxt.ts index bbdeb499d7b0..3cb0dcb281a8 100644 --- a/packages/nuxt/src/app/nuxt.ts +++ b/packages/nuxt/src/app/nuxt.ts @@ -9,6 +9,7 @@ import type { SSRContext, createRenderer } from 'vue-bundle-renderer/runtime' import type { EventHandlerRequest, H3Event } from 'h3' import type { AppConfig, AppConfigInput, RuntimeConfig } from 'nuxt/schema' import type { RenderResponse } from 'nitropack' +import type { LogObject } from 'consola' import type { MergeHead, VueHeadClient } from '@unhead/vue' // eslint-disable-next-line import/no-restricted-paths @@ -40,6 +41,7 @@ export interface RuntimeNuxtHooks { 'app:chunkError': (options: { error: any }) => HookResult 'app:data:refresh': (keys?: string[]) => HookResult 'app:manifest:update': (meta?: NuxtAppManifestMeta) => HookResult + 'dev:ssr-logs': (logs: LogObject[]) => void | Promise 'link:prefetch': (link: string) => HookResult 'page:start': (Component?: VNode) => HookResult 'page:finish': (Component?: VNode) => HookResult diff --git a/packages/nuxt/src/app/plugins/dev-server-logs.client.ts b/packages/nuxt/src/app/plugins/dev-server-logs.client.ts index 50f18ef8a1f5..4dc3da85950b 100644 --- a/packages/nuxt/src/app/plugins/dev-server-logs.client.ts +++ b/packages/nuxt/src/app/plugins/dev-server-logs.client.ts @@ -7,12 +7,6 @@ import { defineNuxtPlugin } from '../nuxt' // @ts-expect-error virtual file import { devLogs, devRootDir } from '#build/nuxt.config.mjs' -declare module '#app' { - interface RuntimeNuxtHooks { - 'dev:ssr-logs': (logs: LogObject[]) => void | Promise - } -} - export default defineNuxtPlugin(nuxtApp => { // Show things in console if (devLogs !== 'silent') { From 1f69a053e8fe0194be81bd3395e67a343c0d3279 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Sun, 25 Feb 2024 10:00:17 +0000 Subject: [PATCH 03/16] fix(schema): don't enable in test mode --- packages/schema/src/config/experimental.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/schema/src/config/experimental.ts b/packages/schema/src/config/experimental.ts index f25cfa0d8d01..f0bbfd4fa9fb 100644 --- a/packages/schema/src/config/experimental.ts +++ b/packages/schema/src/config/experimental.ts @@ -53,15 +53,19 @@ export default defineUntypedSchema({ }, /** - * Display logs on the client console that are normally only shown in the server console. + * Stream server logs to the client as you are developing. These logs can + * be handled in the `dev:ssr-logs` hook. * - * This also enables an ongoing stream of server logs in the browser console as you are developing. - * - * If set to `silent`, the logs will be streamed and you can handle them yourself with the `dev:ssr-logs` hook, - * but they will not be shown in the browser console. + * If set to `silent`, the logs will not be printed to the browser console. * @type {boolean | 'silent'} */ - devLogs: true, + devLogs: { + async $resolve (val, get) { + if (val !== undefined) return val + const [isDev, isTest] = await Promise.all([get('dev'), get('test')]) + return isDev && !isTest + } + }, /** * Turn off rendering of Nuxt scripts and JS resource hints. From 0dd300b72e0373d499e753e1f0b5126670292e4e Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Sun, 25 Feb 2024 10:00:31 +0000 Subject: [PATCH 04/16] fix: add to HTML before other Nuxt scripts --- packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts b/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts index e8feb2b98717..b3258f8af14d 100644 --- a/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts +++ b/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts @@ -60,7 +60,7 @@ export default (nitroApp: NitroApp) => { // Pass any unhandled logs to the client nitroApp.hooks.hook('render:html', htmlContext => { - htmlContext.bodyAppend.push(``) + htmlContext.bodyAppend.unshift(``) logs.length = 0 }) } From e8c5188c4b9629622eb3bf4230d793d806481dc0 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 26 Feb 2024 17:37:51 +0000 Subject: [PATCH 05/16] refactor: use new `createEventStream` helper --- .../src/core/runtime/nitro/dev-server-logs.ts | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts b/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts index b3258f8af14d..19bf1bea6aae 100644 --- a/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts +++ b/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts @@ -2,7 +2,7 @@ import type { LogObject } from 'consola' import { createConsola } from 'consola' import devalue from '@nuxt/devalue' import { createHooks } from 'hookable' -import { defineEventHandler, setHeaders, setResponseStatus } from 'h3' +import { createEventStream, defineEventHandler } from 'h3' import { withTrailingSlash } from 'ufo' import type { NitroApp } from '#internal/nitro/app' @@ -39,23 +39,19 @@ export default (nitroApp: NitroApp) => { }) // Add SSE endpoint for streaming logs to the client - nitroApp.router.add('/_nuxt_logs', defineEventHandler(async (event) => { - setResponseStatus(event, 200) - setHeaders(event, { - 'cache-control': 'no-cache', - 'connection': 'keep-alive', - 'content-type': 'text/event-stream' - }) - - // Let Nitro know the connection is opened - event._handled = true + nitroApp.router.add('/_nuxt_logs', defineEventHandler((event) => { + const eventStream = createEventStream(event) - let counter = 0 + const unsubscribe = hooks.hook('log', async data => { + await eventStream.push(JSON.stringify(data)) + }) - hooks.hook('log', data => { - event.node.res.write(`id: ${++counter}\n`) - event.node.res.write(`data: ${JSON.stringify(data)}\n\n`) + eventStream.onClosed(async () => { + unsubscribe() + await eventStream.close() }) + + return eventStream.send() })) // Pass any unhandled logs to the client From b58849208b2d999f013e19041989a984e30ee19e Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 15 Mar 2024 11:49:05 -0700 Subject: [PATCH 06/16] style: lint --- .../nuxt/src/app/plugins/dev-server-logs.client.ts | 6 +++--- .../nuxt/src/core/runtime/nitro/dev-server-logs.ts | 13 +++++++------ packages/schema/src/config/experimental.ts | 2 +- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/nuxt/src/app/plugins/dev-server-logs.client.ts b/packages/nuxt/src/app/plugins/dev-server-logs.client.ts index 4dc3da85950b..ee1efd389d9b 100644 --- a/packages/nuxt/src/app/plugins/dev-server-logs.client.ts +++ b/packages/nuxt/src/app/plugins/dev-server-logs.client.ts @@ -7,13 +7,13 @@ import { defineNuxtPlugin } from '../nuxt' // @ts-expect-error virtual file import { devLogs, devRootDir } from '#build/nuxt.config.mjs' -export default defineNuxtPlugin(nuxtApp => { +export default defineNuxtPlugin((nuxtApp) => { // Show things in console if (devLogs !== 'silent') { const logger = createConsola({ formatOptions: { colors: true, - date: true, + date: true } }) const hydrationLogs = new Set() @@ -27,7 +27,7 @@ export default defineNuxtPlugin(nuxtApp => { } } }) - nuxtApp.hook('dev:ssr-logs', logs => { + nuxtApp.hook('dev:ssr-logs', (logs) => { for (const log of logs) { // deduplicate so we don't print out things that are logged on client if (!hydrationLogs.size || !hydrationLogs.has(JSON.stringify(log.args))) { diff --git a/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts b/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts index 19bf1bea6aae..5d4d5742626a 100644 --- a/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts +++ b/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts @@ -14,11 +14,11 @@ const originalConsole = { log: console.log, warn: console.warn, info: console.info, - error: console.error, + error: console.error } export default (nitroApp: NitroApp) => { - const hooks = createHooks<{ log: (data: any) => void }>() + const hooks = createHooks<{ log:(data: any) => void }>() const logs: LogObject[] = [] onConsoleLog((_log) => { @@ -42,7 +42,7 @@ export default (nitroApp: NitroApp) => { nitroApp.router.add('/_nuxt_logs', defineEventHandler((event) => { const eventStream = createEventStream(event) - const unsubscribe = hooks.hook('log', async data => { + const unsubscribe = hooks.hook('log', async (data) => { await eventStream.push(JSON.stringify(data)) }) @@ -55,15 +55,16 @@ export default (nitroApp: NitroApp) => { })) // Pass any unhandled logs to the client - nitroApp.hooks.hook('render:html', htmlContext => { + nitroApp.hooks.hook('render:html', (htmlContext) => { htmlContext.bodyAppend.unshift(``) logs.length = 0 }) } -const EXCLUDE_TRACE_RE = new RegExp('^.*at.*(\\/node_modules\\/(.*\\/)?(nuxt|consola|@vue)\\/.*|core\\/runtime\\/nitro.*)$\\n?', 'gm') +const EXCLUDE_TRACE_RE = /^.*at.*(\/node_modules\/(.*\/)?(nuxt|consola|@vue)\/.*|core\/runtime\/nitro.*)$\n?/gm function getStack () { // Pass along stack traces if needed (for error and warns) + // eslint-disable-next-line unicorn/error-message const stack = new Error() Error.captureStackTrace(stack) return stack.stack?.replace(EXCLUDE_TRACE_RE, '').replace(/^Error.*\n/, '') || '' @@ -89,7 +90,7 @@ function onConsoleLog (callback: (log: LogObject) => void) { (originalConsole[logObj.type as 'log'] || originalConsole.log)(...logObj.args) callback(logObj) - }, + } } ] }) diff --git a/packages/schema/src/config/experimental.ts b/packages/schema/src/config/experimental.ts index 7a69946ee641..6c835355e3ac 100644 --- a/packages/schema/src/config/experimental.ts +++ b/packages/schema/src/config/experimental.ts @@ -61,7 +61,7 @@ export default defineUntypedSchema({ */ devLogs: { async $resolve (val, get) { - if (val !== undefined) return val + if (val !== undefined) { return val } const [isDev, isTest] = await Promise.all([get('dev'), get('test')]) return isDev && !isTest } From 10f8d23d94a79d56bb22b9c7d9de85759003fcea Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 15 Mar 2024 11:53:47 -0700 Subject: [PATCH 07/16] fix: use original consola instance --- .../src/core/runtime/nitro/dev-server-logs.ts | 27 +++++-------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts b/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts index 5d4d5742626a..b739ee65e8f9 100644 --- a/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts +++ b/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts @@ -1,5 +1,5 @@ import type { LogObject } from 'consola' -import { createConsola } from 'consola' +import { consola } from 'consola' import devalue from '@nuxt/devalue' import { createHooks } from 'hookable' import { createEventStream, defineEventHandler } from 'h3' @@ -10,13 +10,6 @@ import type { NitroApp } from '#internal/nitro/app' // @ts-expect-error virtual file import { rootDir } from '#internal/dev-server-logs-options' -const originalConsole = { - log: console.log, - warn: console.warn, - info: console.info, - error: console.error -} - export default (nitroApp: NitroApp) => { const hooks = createHooks<{ log:(data: any) => void }>() const logs: LogObject[] = [] @@ -81,18 +74,10 @@ function normalizeFilenames (stacktrace: string) { } function onConsoleLog (callback: (log: LogObject) => void) { - const logger = createConsola({ - reporters: [ - { - log (logObj) { - // Don't swallow log messages in console - is there a better way to do this @pi0? - // TODO: display (clickable) filename in server log as well when we use consola for this - (originalConsole[logObj.type as 'log'] || originalConsole.log)(...logObj.args) - - callback(logObj) - } - } - ] + consola.addReporter({ + log (logObj) { + callback(logObj) + } }) - logger.wrapAll() + consola.wrapAll() } From fcbd195c0958a935a6412c605821543995c9d9c5 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 15 Mar 2024 13:00:09 -0700 Subject: [PATCH 08/16] feat: use asyncContext to associate logs with request --- .../src/app/plugins/dev-server-logs.client.ts | 2 +- .../src/core/runtime/nitro/dev-server-logs.ts | 26 +++++++++++++++---- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/nuxt/src/app/plugins/dev-server-logs.client.ts b/packages/nuxt/src/app/plugins/dev-server-logs.client.ts index ee1efd389d9b..dc620beeee5f 100644 --- a/packages/nuxt/src/app/plugins/dev-server-logs.client.ts +++ b/packages/nuxt/src/app/plugins/dev-server-logs.client.ts @@ -68,7 +68,7 @@ function normalizeServerLog (log: LogObject) { if (log.type === 'error' || log.type === 'warn') { log.additional = normalizeFilenames(log.stack as string) } - log.tag = `[ssr]${log.filename ? ` ${log.filename}` : ''}${log.tag || ''}` + log.tag = `[ssr: ${log.path}]${log.filename ? ` ${log.filename}` : ''}${log.tag || ''}` delete log.stack return log } diff --git a/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts b/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts index b739ee65e8f9..afae3de8208a 100644 --- a/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts +++ b/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts @@ -1,24 +1,40 @@ +import { AsyncLocalStorage } from 'node:async_hooks' import type { LogObject } from 'consola' import { consola } from 'consola' import devalue from '@nuxt/devalue' import { createHooks } from 'hookable' +import type { H3Event } from 'h3' import { createEventStream, defineEventHandler } from 'h3' import { withTrailingSlash } from 'ufo' +import { getContext } from 'unctx' import type { NitroApp } from '#internal/nitro/app' // @ts-expect-error virtual file import { rootDir } from '#internal/dev-server-logs-options' +interface NuxtDevAsyncContext { + logs: LogObject[] + event: H3Event +} + +const asyncContext = getContext('nuxt-dev', { asyncContext: true, AsyncLocalStorage }) + export default (nitroApp: NitroApp) => { - const hooks = createHooks<{ log:(data: any) => void }>() - const logs: LogObject[] = [] + const hooks = createHooks<{ log: (data: any) => void }>() + + const handler = nitroApp.h3App.handler + nitroApp.h3App.handler = (event) => { + return asyncContext.callAsync({ logs: [], event }, () => handler(event)) + } onConsoleLog((_log) => { + const ctx = asyncContext.use() const stack = getStack() const log = { ..._log, + path: ctx.event.path, // Pass along filename to allow the client to display more info about where log comes from filename: extractFilenameFromStack(stack), // Clean up file names in stack trace @@ -26,7 +42,7 @@ export default (nitroApp: NitroApp) => { } // retain log to be include in the next render - logs.push(log) + ctx.logs.push(log) // send log messages to client via SSE hooks.callHook('log', log) }) @@ -49,8 +65,8 @@ export default (nitroApp: NitroApp) => { // Pass any unhandled logs to the client nitroApp.hooks.hook('render:html', (htmlContext) => { + const logs = asyncContext.use().logs htmlContext.bodyAppend.unshift(``) - logs.length = 0 }) } @@ -79,5 +95,5 @@ function onConsoleLog (callback: (log: LogObject) => void) { callback(logObj) } }) - consola.wrapAll() + consola.wrapConsole() } From 1ae91208316fb7d85433a8ee15136eb4e8fede2c Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 15 Mar 2024 20:02:29 +0000 Subject: [PATCH 09/16] [autofix.ci] apply automated fixes --- packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts b/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts index afae3de8208a..a209dbe81dd0 100644 --- a/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts +++ b/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts @@ -21,7 +21,7 @@ interface NuxtDevAsyncContext { const asyncContext = getContext('nuxt-dev', { asyncContext: true, AsyncLocalStorage }) export default (nitroApp: NitroApp) => { - const hooks = createHooks<{ log: (data: any) => void }>() + const hooks = createHooks<{ log:(data: any) => void }>() const handler = nitroApp.h3App.handler nitroApp.h3App.handler = (event) => { From 2e3f8d3d3d54c1b5283f44268c54610f24b4eed4 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 15 Mar 2024 15:29:08 -0700 Subject: [PATCH 10/16] fix: remove sse endpoint and add nitro `dev:ssr-logs` hook --- docs/3.api/6.advanced/1.hooks.md | 1 + .../src/app/plugins/dev-server-logs.client.ts | 10 +----- .../src/core/runtime/nitro/dev-server-logs.ts | 31 ++++--------------- packages/nuxt/src/core/templates.ts | 2 ++ packages/nuxt/types.d.mts | 2 ++ packages/nuxt/types.d.ts | 2 ++ packages/schema/src/config/typescript.ts | 2 +- 7 files changed, 15 insertions(+), 35 deletions(-) diff --git a/docs/3.api/6.advanced/1.hooks.md b/docs/3.api/6.advanced/1.hooks.md index 318ea3f65c87..0980575a37c1 100644 --- a/docs/3.api/6.advanced/1.hooks.md +++ b/docs/3.api/6.advanced/1.hooks.md @@ -93,6 +93,7 @@ See [Nitro](https://nitro.unjs.io/guide/plugins#available-hooks) for all availab Hook | Arguments | Description | Types -----------------------|-----------------------|--------------------------------------|------------------ +`dev:ssr-logs` | `{ path, logs }` | Server | Called at the end of a request cycle with an array of server-side logs. `render:response` | `response, { event }` | Called before sending the response. | [response](https://github.com/nuxt/nuxt/blob/71ef8bd3ff207fd51c2ca18d5a8c7140476780c7/packages/nuxt/src/core/runtime/nitro/renderer.ts#L24), [event](https://github.com/unjs/h3/blob/f6ceb5581043dc4d8b6eab91e9be4531e0c30f8e/src/types.ts#L38) `render:html` | `html, { event }` | Called before constructing the HTML. | [html](https://github.com/nuxt/nuxt/blob/71ef8bd3ff207fd51c2ca18d5a8c7140476780c7/packages/nuxt/src/core/runtime/nitro/renderer.ts#L15), [event](https://github.com/unjs/h3/blob/f6ceb5581043dc4d8b6eab91e9be4531e0c30f8e/src/types.ts#L38) `render:island` | `islandResponse, { event, islandContext }` | Called before constructing the island HTML. | [islandResponse](https://github.com/nuxt/nuxt/blob/e50cabfed1984c341af0d0c056a325a8aec26980/packages/nuxt/src/core/runtime/nitro/renderer.ts#L28), [event](https://github.com/unjs/h3/blob/f6ceb5581043dc4d8b6eab91e9be4531e0c30f8e/src/types.ts#L38), [islandContext](https://github.com/nuxt/nuxt/blob/e50cabfed1984c341af0d0c056a325a8aec26980/packages/nuxt/src/core/runtime/nitro/renderer.ts#L38) diff --git a/packages/nuxt/src/app/plugins/dev-server-logs.client.ts b/packages/nuxt/src/app/plugins/dev-server-logs.client.ts index dc620beeee5f..e6a2e774fd58 100644 --- a/packages/nuxt/src/app/plugins/dev-server-logs.client.ts +++ b/packages/nuxt/src/app/plugins/dev-server-logs.client.ts @@ -46,14 +46,6 @@ export default defineNuxtPlugin((nuxtApp) => { await nuxtApp.hooks.callHook('dev:ssr-logs', window.__NUXT_LOGS__) } }) - - // initialise long-running SSE connection - const source = new EventSource('/_nuxt_logs') - source.onmessage = (event) => { - const log = JSON.parse(event.data) as LogObject - log.date = new Date(log.date) - nuxtApp.hooks.callHook('dev:ssr-logs', [log]) - } }) function normalizeFilenames (stack?: string) { @@ -68,7 +60,7 @@ function normalizeServerLog (log: LogObject) { if (log.type === 'error' || log.type === 'warn') { log.additional = normalizeFilenames(log.stack as string) } - log.tag = `[ssr: ${log.path}]${log.filename ? ` ${log.filename}` : ''}${log.tag || ''}` + log.tag = `[ssr]${log.filename ? ` ${log.filename}` : ''}${log.tag || ''}` delete log.stack return log } diff --git a/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts b/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts index a209dbe81dd0..7d993a80b6c0 100644 --- a/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts +++ b/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts @@ -2,9 +2,7 @@ import { AsyncLocalStorage } from 'node:async_hooks' import type { LogObject } from 'consola' import { consola } from 'consola' import devalue from '@nuxt/devalue' -import { createHooks } from 'hookable' import type { H3Event } from 'h3' -import { createEventStream, defineEventHandler } from 'h3' import { withTrailingSlash } from 'ufo' import { getContext } from 'unctx' @@ -21,8 +19,6 @@ interface NuxtDevAsyncContext { const asyncContext = getContext('nuxt-dev', { asyncContext: true, AsyncLocalStorage }) export default (nitroApp: NitroApp) => { - const hooks = createHooks<{ log:(data: any) => void }>() - const handler = nitroApp.h3App.handler nitroApp.h3App.handler = (event) => { return asyncContext.callAsync({ logs: [], event }, () => handler(event)) @@ -34,7 +30,6 @@ export default (nitroApp: NitroApp) => { const log = { ..._log, - path: ctx.event.path, // Pass along filename to allow the client to display more info about where log comes from filename: extractFilenameFromStack(stack), // Clean up file names in stack trace @@ -43,30 +38,16 @@ export default (nitroApp: NitroApp) => { // retain log to be include in the next render ctx.logs.push(log) - // send log messages to client via SSE - hooks.callHook('log', log) }) - // Add SSE endpoint for streaming logs to the client - nitroApp.router.add('/_nuxt_logs', defineEventHandler((event) => { - const eventStream = createEventStream(event) - - const unsubscribe = hooks.hook('log', async (data) => { - await eventStream.push(JSON.stringify(data)) - }) - - eventStream.onClosed(async () => { - unsubscribe() - await eventStream.close() - }) - - return eventStream.send() - })) + nitroApp.hooks.hook('afterResponse', () => { + const ctx = asyncContext.use() + return nitroApp.hooks.callHook('dev:ssr-logs', { logs: ctx.logs, path: ctx.event.path }) + }) - // Pass any unhandled logs to the client + // Pass any logs to the client nitroApp.hooks.hook('render:html', (htmlContext) => { - const logs = asyncContext.use().logs - htmlContext.bodyAppend.unshift(``) + htmlContext.bodyAppend.unshift(``) }) } diff --git a/packages/nuxt/src/core/templates.ts b/packages/nuxt/src/core/templates.ts index a40f9845b1f3..52cb7a460d7b 100644 --- a/packages/nuxt/src/core/templates.ts +++ b/packages/nuxt/src/core/templates.ts @@ -228,6 +228,7 @@ export const nitroSchemaTemplate: NuxtTemplate = { import type { RuntimeConfig } from 'nuxt/schema' import type { H3Event } from 'h3' +import type { LogObject } from 'consola' import type { NuxtIslandContext, NuxtIslandResponse, NuxtRenderHTMLContext } from 'nuxt/app' declare module 'nitropack' { @@ -245,6 +246,7 @@ declare module 'nitropack' { experimentalNoScripts?: boolean } interface NitroRuntimeHooks { + 'dev:ssr-logs': (ctx: { logs: LogObject[], path: string }) => void | Promise 'render:html': (htmlContext: NuxtRenderHTMLContext, context: { event: H3Event }) => void | Promise 'render:island': (islandResponse: NuxtIslandResponse, context: { event: H3Event, islandContext: NuxtIslandContext }) => void | Promise } diff --git a/packages/nuxt/types.d.mts b/packages/nuxt/types.d.mts index a2d0de646c62..98771c18e2ed 100644 --- a/packages/nuxt/types.d.mts +++ b/packages/nuxt/types.d.mts @@ -5,6 +5,7 @@ import type { DefineNuxtConfig } from 'nuxt/config' import type { RuntimeConfig, SchemaDefinition } from 'nuxt/schema' import type { H3Event } from 'h3' import type { NuxtIslandContext, NuxtIslandResponse, NuxtRenderHTMLContext } from './dist/app/types.js' +import type { LogObject } from 'consola' declare global { const defineNuxtConfig: DefineNuxtConfig @@ -27,6 +28,7 @@ declare module 'nitropack' { experimentalNoScripts?: boolean } interface NitroRuntimeHooks { + 'dev:ssr-logs': (ctx: { logs: LogObject[], path: string }) => void | Promise 'render:html': (htmlContext: NuxtRenderHTMLContext, context: { event: H3Event }) => void | Promise 'render:island': (islandResponse: NuxtIslandResponse, context: { event: H3Event, islandContext: NuxtIslandContext }) => void | Promise } diff --git a/packages/nuxt/types.d.ts b/packages/nuxt/types.d.ts index 7f9075ad8238..8b9c9b911887 100644 --- a/packages/nuxt/types.d.ts +++ b/packages/nuxt/types.d.ts @@ -2,6 +2,7 @@ import type { DefineNuxtConfig } from 'nuxt/config' import type { RuntimeConfig, SchemaDefinition } from 'nuxt/schema' import type { H3Event } from 'h3' +import type { LogObject } from 'consola' import type { NuxtIslandContext, NuxtIslandResponse, NuxtRenderHTMLContext } from './dist/app/types' export * from './dist/index' @@ -27,6 +28,7 @@ declare module 'nitropack' { experimentalNoScripts?: boolean } interface NitroRuntimeHooks { + 'dev:ssr-logs': (ctx: { logs: LogObject[], path: string }) => void | Promise 'render:html': (htmlContext: NuxtRenderHTMLContext, context: { event: H3Event }) => void | Promise 'render:island': (islandResponse: NuxtIslandResponse, context: { event: H3Event, islandContext: NuxtIslandContext }) => void | Promise } diff --git a/packages/schema/src/config/typescript.ts b/packages/schema/src/config/typescript.ts index 8c9eb2c2d0c9..75693392209e 100644 --- a/packages/schema/src/config/typescript.ts +++ b/packages/schema/src/config/typescript.ts @@ -32,7 +32,7 @@ export default defineUntypedSchema({ */ hoist: { $resolve: (val) => { - const defaults = ['nitropack', 'defu', 'h3', '@unhead/vue', 'vue', 'vue-router', '@nuxt/schema'] + const defaults = ['nitropack', 'defu', 'h3', '@unhead/vue', 'vue', 'vue-router', 'consola', '@nuxt/schema'] return val === false ? [] : (Array.isArray(val) ? val.concat(defaults) : defaults) } }, From 87e82ca19523f8d558653cf027a4d5636636201b Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 15 Mar 2024 15:30:06 -0700 Subject: [PATCH 11/16] fix: add nuxt edge packages to ignore list --- packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts b/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts index 7d993a80b6c0..aa9a49ab5316 100644 --- a/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts +++ b/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts @@ -51,7 +51,7 @@ export default (nitroApp: NitroApp) => { }) } -const EXCLUDE_TRACE_RE = /^.*at.*(\/node_modules\/(.*\/)?(nuxt|consola|@vue)\/.*|core\/runtime\/nitro.*)$\n?/gm +const EXCLUDE_TRACE_RE = /^.*at.*(\/node_modules\/(.*\/)?(nuxt|nuxt-nightly|nuxt-edge|nuxt3|consola|@vue)\/.*|core\/runtime\/nitro.*)$\n?/gm function getStack () { // Pass along stack traces if needed (for error and warns) // eslint-disable-next-line unicorn/error-message From 0bfa8adad32eabcfc53b2210660cdc64d0868d7e Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 15 Mar 2024 15:45:10 -0700 Subject: [PATCH 12/16] fix: call restore on main consola instance --- packages/nuxt/src/app/plugins/dev-server-logs.client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nuxt/src/app/plugins/dev-server-logs.client.ts b/packages/nuxt/src/app/plugins/dev-server-logs.client.ts index e6a2e774fd58..149d4e0f467e 100644 --- a/packages/nuxt/src/app/plugins/dev-server-logs.client.ts +++ b/packages/nuxt/src/app/plugins/dev-server-logs.client.ts @@ -17,7 +17,7 @@ export default defineNuxtPlugin((nuxtApp) => { } }) const hydrationLogs = new Set() - consola.wrapAll() + consola.wrapConsole() consola.addReporter({ log (logObj) { try { @@ -36,7 +36,7 @@ export default defineNuxtPlugin((nuxtApp) => { } }) - nuxtApp.hooks.hook('app:suspense:resolve', () => logger.restoreAll()) + nuxtApp.hooks.hook('app:suspense:resolve', () => consola.restoreAll()) nuxtApp.hooks.hookOnce('dev:ssr-logs', () => hydrationLogs.clear()) } From 3c32946992c5b39ae87147459109ffb183330d25 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 15 Mar 2024 15:57:52 -0700 Subject: [PATCH 13/16] fix: exclude vite-node logs --- packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts b/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts index aa9a49ab5316..63b955eae64c 100644 --- a/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts +++ b/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts @@ -27,6 +27,7 @@ export default (nitroApp: NitroApp) => { onConsoleLog((_log) => { const ctx = asyncContext.use() const stack = getStack() + if (stack.includes('runtime/vite-node.mjs')) { return } const log = { ..._log, From 33176c06cda81313aad80501e1544b7c951270c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Chopin?= Date: Sat, 16 Mar 2024 00:11:40 +0100 Subject: [PATCH 14/16] chore: move filename to additional and only one line --- .../src/app/plugins/dev-server-logs.client.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/nuxt/src/app/plugins/dev-server-logs.client.ts b/packages/nuxt/src/app/plugins/dev-server-logs.client.ts index 149d4e0f467e..07f465449dcb 100644 --- a/packages/nuxt/src/app/plugins/dev-server-logs.client.ts +++ b/packages/nuxt/src/app/plugins/dev-server-logs.client.ts @@ -49,18 +49,15 @@ export default defineNuxtPlugin((nuxtApp) => { }) function normalizeFilenames (stack?: string) { - return stack?.replace(/at.*\(([^)]+)\)/g, (match, filename) => { - if (!isAbsolute(filename)) { return match } - // TODO: normalise file names for clickable links in console - return match.replace(filename, filename.replace(devRootDir, '')) - }) + stack = stack?.split('\n')[0] || '' + stack = stack.replace(`${devRootDir}/`, '') + stack = stack.replace(/:\d+:\d+\)?$/, '') + return stack } function normalizeServerLog (log: LogObject) { - if (log.type === 'error' || log.type === 'warn') { - log.additional = normalizeFilenames(log.stack as string) - } - log.tag = `[ssr]${log.filename ? ` ${log.filename}` : ''}${log.tag || ''}` + log.additional = normalizeFilenames(log.stack as string) + log.tag = `ssr` delete log.stack return log } From ba70ecc68c79e06ffe6b9d2bd54a258204e960f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Chopin?= Date: Sat, 16 Mar 2024 00:16:42 +0100 Subject: [PATCH 15/16] Update dev-server-logs.client.ts --- packages/nuxt/src/app/plugins/dev-server-logs.client.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/nuxt/src/app/plugins/dev-server-logs.client.ts b/packages/nuxt/src/app/plugins/dev-server-logs.client.ts index 07f465449dcb..e3bcb6e988eb 100644 --- a/packages/nuxt/src/app/plugins/dev-server-logs.client.ts +++ b/packages/nuxt/src/app/plugins/dev-server-logs.client.ts @@ -1,6 +1,5 @@ import { consola, createConsola } from 'consola' import type { LogObject } from 'consola' -import { isAbsolute } from 'pathe' import { defineNuxtPlugin } from '../nuxt' From 896a818f5d85bf8198ce10a4097646ac7467a21d Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 15 Mar 2024 23:18:59 +0000 Subject: [PATCH 16/16] [autofix.ci] apply automated fixes --- packages/nuxt/src/app/plugins/dev-server-logs.client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nuxt/src/app/plugins/dev-server-logs.client.ts b/packages/nuxt/src/app/plugins/dev-server-logs.client.ts index e3bcb6e988eb..9cfc39e85823 100644 --- a/packages/nuxt/src/app/plugins/dev-server-logs.client.ts +++ b/packages/nuxt/src/app/plugins/dev-server-logs.client.ts @@ -56,7 +56,7 @@ function normalizeFilenames (stack?: string) { function normalizeServerLog (log: LogObject) { log.additional = normalizeFilenames(log.stack as string) - log.tag = `ssr` + log.tag = 'ssr' delete log.stack return log }