Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(nuxt): pass server logs to client (#25936)
Co-authored-by: Sébastien Chopin <[email protected]>
- Loading branch information
Showing
11 changed files
with
186 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import { consola, createConsola } from 'consola' | ||
import type { LogObject } from 'consola' | ||
|
||
import { defineNuxtPlugin } from '../nuxt' | ||
|
||
// @ts-expect-error virtual file | ||
import { devLogs, devRootDir } from '#build/nuxt.config.mjs' | ||
|
||
export default defineNuxtPlugin((nuxtApp) => { | ||
// Show things in console | ||
if (devLogs !== 'silent') { | ||
const logger = createConsola({ | ||
formatOptions: { | ||
colors: true, | ||
date: true | ||
} | ||
}) | ||
const hydrationLogs = new Set<string>() | ||
consola.wrapConsole() | ||
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', () => consola.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__) | ||
} | ||
}) | ||
}) | ||
|
||
function normalizeFilenames (stack?: string) { | ||
stack = stack?.split('\n')[0] || '' | ||
stack = stack.replace(`${devRootDir}/`, '') | ||
stack = stack.replace(/:\d+:\d+\)?$/, '') | ||
return stack | ||
} | ||
|
||
function normalizeServerLog (log: LogObject) { | ||
log.additional = normalizeFilenames(log.stack as string) | ||
log.tag = 'ssr' | ||
delete log.stack | ||
return log | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import { AsyncLocalStorage } from 'node:async_hooks' | ||
import type { LogObject } from 'consola' | ||
import { consola } from 'consola' | ||
import devalue from '@nuxt/devalue' | ||
import type { H3Event } 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<NuxtDevAsyncContext>('nuxt-dev', { asyncContext: true, AsyncLocalStorage }) | ||
|
||
export default (nitroApp: NitroApp) => { | ||
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() | ||
if (stack.includes('runtime/vite-node.mjs')) { return } | ||
|
||
const log = { | ||
..._log, | ||
// 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 | ||
stack: normalizeFilenames(stack) | ||
} | ||
|
||
// retain log to be include in the next render | ||
ctx.logs.push(log) | ||
}) | ||
|
||
nitroApp.hooks.hook('afterResponse', () => { | ||
const ctx = asyncContext.use() | ||
return nitroApp.hooks.callHook('dev:ssr-logs', { logs: ctx.logs, path: ctx.event.path }) | ||
}) | ||
|
||
// Pass any logs to the client | ||
nitroApp.hooks.hook('render:html', (htmlContext) => { | ||
htmlContext.bodyAppend.unshift(`<script>window.__NUXT_LOGS__ = ${devalue(asyncContext.use().logs)}</script>`) | ||
}) | ||
} | ||
|
||
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 | ||
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) { | ||
consola.addReporter({ | ||
log (logObj) { | ||
callback(logObj) | ||
} | ||
}) | ||
consola.wrapConsole() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters