mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-25 07:05:11 +00:00
feat(nuxt): pass server logs to client (#25936)
Co-authored-by: Sébastien Chopin <seb@nuxt.com>
This commit is contained in:
parent
5be9253cfe
commit
e272b2f2e1
@ -29,6 +29,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).
|
||||
`page:view-transition:start` | `transition` | Client | Called after `document.startViewTransition` is called when [experimental viewTransition support is enabled](https://nuxt.com/docs/getting-started/transitions#view-transitions-api-experimental).
|
||||
|
||||
## Nuxt Hooks (build time)
|
||||
@ -92,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)
|
||||
|
@ -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'
|
||||
|
||||
import type { NuxtIslandContext } from '../app/types'
|
||||
@ -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<void>
|
||||
'link:prefetch': (link: string) => HookResult
|
||||
'page:start': (Component?: VNode) => HookResult
|
||||
'page:finish': (Component?: VNode) => HookResult
|
||||
|
62
packages/nuxt/src/app/plugins/dev-server-logs.client.ts
Normal file
62
packages/nuxt/src/app/plugins/dev-server-logs.client.ts
Normal file
@ -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
|
||||
}
|
2
packages/nuxt/src/app/types/augments.d.ts
vendored
2
packages/nuxt/src/app/types/augments.d.ts
vendored
@ -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<string, any>
|
||||
useNuxtApp?: typeof useNuxtApp
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { dirname, 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 { resolvePath as _resolvePath } from 'mlly'
|
||||
import type { Nuxt, NuxtHooks, NuxtOptions } from 'nuxt/schema'
|
||||
import { resolvePackageJSON } from 'pkg-types'
|
||||
@ -175,6 +175,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 `<script setup>` to preserve context
|
||||
if (nuxt.options.experimental.asyncContext) {
|
||||
addBuildPlugin(AsyncContextInjectionPlugin(nuxt))
|
||||
|
81
packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts
Normal file
81
packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts
Normal file
@ -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()
|
||||
}
|
@ -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<void>
|
||||
'render:html': (htmlContext: NuxtRenderHTMLContext, context: { event: H3Event }) => void | Promise<void>
|
||||
'render:island': (islandResponse: NuxtIslandResponse, context: { event: H3Event, islandContext: NuxtIslandContext }) => void | Promise<void>
|
||||
}
|
||||
@ -388,6 +390,7 @@ export const nuxtConfigTemplate: NuxtTemplate = {
|
||||
`export const selectiveClient = ${typeof ctx.nuxt.options.experimental.componentIslands === 'object' && Boolean(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)}`,
|
||||
|
@ -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<void>
|
||||
'render:html': (htmlContext: NuxtRenderHTMLContext, context: { event: H3Event }) => void | Promise<void>
|
||||
'render:island': (islandResponse: NuxtIslandResponse, context: { event: H3Event, islandContext: NuxtIslandContext }) => void | Promise<void>
|
||||
}
|
||||
|
2
packages/nuxt/types.d.ts
vendored
2
packages/nuxt/types.d.ts
vendored
@ -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<void>
|
||||
'render:html': (htmlContext: NuxtRenderHTMLContext, context: { event: H3Event }) => void | Promise<void>
|
||||
'render:island': (islandResponse: NuxtIslandResponse, context: { event: H3Event, islandContext: NuxtIslandContext }) => void | Promise<void>
|
||||
}
|
||||
|
@ -52,6 +52,21 @@ export default defineUntypedSchema({
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Stream server logs to the client as you are developing. These logs can
|
||||
* be handled in the `dev:ssr-logs` hook.
|
||||
*
|
||||
* If set to `silent`, the logs will not be printed to the browser console.
|
||||
* @type {boolean | 'silent'}
|
||||
*/
|
||||
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.
|
||||
* You can also disable scripts more granularly within `routeRules`.
|
||||
|
@ -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)
|
||||
}
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user