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 + stream server logs to client
- Loading branch information
Showing
7 changed files
with
210 additions
and
1 deletion.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<void> | ||
} | ||
} | ||
|
||
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.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 | ||
} |
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
101 changes: 101 additions & 0 deletions
101
packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts
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,101 @@ | ||
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 { withTrailingSlash } from 'ufo' | ||
|
||
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[] = [] | ||
|
||
onConsoleLog((_log) => { | ||
const stack = getStack() | ||
|
||
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 | ||
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(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 | ||
|
||
let counter = 0 | ||
|
||
hooks.hook('log', data => { | ||
event.node.res.write(`id: ${++counter}\n`) | ||
event.node.res.write(`data: ${JSON.stringify(data)}\n\n`) | ||
}) | ||
})) | ||
|
||
// Pass any unhandled logs to the client | ||
nitroApp.hooks.hook('render:html', htmlContext => { | ||
htmlContext.bodyAppend.push(`<script>window.__NUXT_LOGS__ = ${devalue(logs)}</script>`) | ||
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() | ||
} |
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