Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/config/server-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,27 @@ The error that appears in the Browser when the fallback happens can be ignored.

:::

## server.forwardRuntimeLogs

- **Type:** `boolean`
- **Default:** `false`

Forward unhandled runtime errors from the browser to the Vite server console during development. When enabled, errors like unhandled promise rejections and uncaught exceptions that occur in the browser will be logged in the server terminal with enhanced formatting, for example:

```log
1:18:38 AM [vite] (client) [Unhandled error] Error: this is test error
> testError src/main.ts:20:8
18|
19| function testError() {
20| throw new Error('this is test error')
| ^
21| }
22|
> HTMLButtonElement.<anonymous> src/main.ts:6:2
```

This feature is useful when working with AI coding assistants that can only see terminal output for context.

## server.warmup

- **Type:** `{ clientFiles?: string[], ssrFiles?: string[] }`
Expand Down
28 changes: 28 additions & 0 deletions packages/vite/LICENSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,34 @@ Repository: rollup/plugins

---------------------------------------

## @vitest/utils
License: MIT
Repository: git+https://github.com/vitest-dev/vitest.git

> MIT License
>
> Copyright (c) 2021-Present Vitest Team
>
> Permission is hereby granted, free of charge, to any person obtaining a copy
> of this software and associated documentation files (the "Software"), to deal
> in the Software without restriction, including without limitation the rights
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
> copies of the Software, and to permit persons to whom the Software is
> furnished to do so, subject to the following conditions:
>
> The above copyright notice and this permission notice shall be included in all
> copies or substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
> SOFTWARE.

---------------------------------------

## anymatch
License: ISC
By: Elan Shanker
Expand Down
1 change: 1 addition & 0 deletions packages/vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
"@rollup/pluginutils": "^5.3.0",
"@types/escape-html": "^1.0.4",
"@types/pnpapi": "^0.0.5",
"@vitest/utils": "^3.2.4",
"artichokie": "^0.4.2",
"baseline-browser-mapping": "^2.8.12",
"cac": "^6.7.14",
Expand Down
2 changes: 2 additions & 0 deletions packages/vite/src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
normalizeModuleRunnerTransport,
} from '../shared/moduleRunnerTransport'
import { createHMRHandler } from '../shared/hmrHandler'
import { setupRuntimeLogHandler } from '../shared/runtimeLog'
import { ErrorOverlay, cspNonce, overlayId } from './overlay'
import '@vite/env'

Expand Down Expand Up @@ -168,6 +169,7 @@ const hmrClient = new HMRClient(
},
)
transport.connect!(createHMRHandler(handleMessage))
setupRuntimeLogHandler(transport)

async function handleMessage(payload: HotPayload) {
switch (payload.type) {
Expand Down
3 changes: 3 additions & 0 deletions packages/vite/src/node/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
createFilterForTransform,
createIdFilter,
} from './pluginFilter'
import { runtimeLogPlugin } from './runtimeLog'

export async function resolvePlugins(
config: ResolvedConfig,
Expand Down Expand Up @@ -73,6 +74,8 @@ export async function resolvePlugins(
wasmHelperPlugin(),
webWorkerPlugin(config),
assetPlugin(config),
config.server.forwardRuntimeLogs &&
runtimeLogPlugin({ environments: ['client'] }),

...normalPlugins,

Expand Down
96 changes: 96 additions & 0 deletions packages/vite/src/node/plugins/runtimeLog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import path from 'node:path'
import fs from 'node:fs'
import { parseErrorStacktrace } from '@vitest/utils/source-map'
import c from 'picocolors'
import type { DevEnvironment, Plugin } from '..'
import { normalizePath } from '..'
import { generateCodeFrame } from '../utils'

export function runtimeLogPlugin(pluginOpts: {
environments: string[]
}): Plugin {
return {
name: 'vite:runtime-log',
apply: 'serve',
configureServer(server) {
for (const name of pluginOpts.environments) {
const environment = server.environments[name]
environment.hot.on('vite:runtime-log', (payload: RuntimeLogPayload) => {
const output = formatError(payload.error, environment)
environment.config.logger.error(output, {
timestamp: true,
})
})
}
},
}
}

type RuntimeLogPayload = {
error: {
name: string
message: string
stack?: string
}
}

function formatError(error: any, environment: DevEnvironment) {
// https://github.com/vitest-dev/vitest/blob/4783137cd8d766cf998bdf2d638890eaa51e08d9/packages/browser/src/node/projectParent.ts#L58
const stacks = parseErrorStacktrace(error, {
getUrlId(id) {
const moduleGraph = environment.moduleGraph
const mod = moduleGraph.getModuleById(id)
if (mod) {
return id
}
const resolvedPath = normalizePath(
path.resolve(environment.config.root, id.slice(1)),
)
const modUrl = moduleGraph.getModuleById(resolvedPath)
if (modUrl) {
return resolvedPath
}
// some browsers (looking at you, safari) don't report queries in stack traces
// the next best thing is to try the first id that this file resolves to
const files = moduleGraph.getModulesByFile(resolvedPath)
if (files && files.size) {
return files.values().next().value!.id!
}
return id
},
getSourceMap(id) {
// stack is already rewritten on server
if (environment.name === 'client') {
return environment.moduleGraph.getModuleById(id)?.transformResult?.map
}
},
})

// https://github.com/vitest-dev/vitest/blob/4783137cd8d766cf998bdf2d638890eaa51e08d9/packages/vitest/src/node/printError.ts#L64
const nearest = stacks.find((stack) => {
const modules = environment.moduleGraph.getModulesByFile(stack.file)
return (
[...(modules || [])].some((m) => m.transformResult) &&
fs.existsSync(stack.file)
)
})

let output = ''
const errorName = error.name || 'Unknown Error'
output += c.red(`[Unhandled error] ${c.bold(errorName)}: ${error.message}\n`)
for (const stack of stacks) {
const file = normalizePath(
path.relative(environment.config.root, stack.file),
)
output += ` > ${[stack.method, `${file}:${stack.line}:${stack.column}`]
.filter(Boolean)
.join(' ')}\n`
if (stack === nearest) {
const code = fs.readFileSync(stack.file, 'utf-8')
// TODO: highlight?
output += generateCodeFrame(code, stack).replace(/^/gm, ' ')
output += '\n'
}
}
return output
}
3 changes: 3 additions & 0 deletions packages/vite/src/node/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,8 @@ export interface ServerOptions extends CommonServerOptions {
server: ViteDevServer,
hmr: (environment: DevEnvironment) => Promise<void>,
) => Promise<void>

forwardRuntimeLogs?: boolean
}

export interface ResolvedServerOptions
Expand Down Expand Up @@ -1103,6 +1105,7 @@ export const serverConfigDefaults = Object.freeze({
// sourcemapIgnoreList
perEnvironmentStartEndDuringDev: false,
// hotUpdateEnvironments
forwardRuntimeLogs: false,
} satisfies ServerOptions)

export function resolveServerOptions(
Expand Down
40 changes: 40 additions & 0 deletions packages/vite/src/shared/runtimeLog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { NormalizedModuleRunnerTransport } from './moduleRunnerTransport'

export type RuntimeLogPayload = {
error: {
name: string
message: string
stack?: string
}
}

export function setupRuntimeLogHandler(
transport: NormalizedModuleRunnerTransport,
): void {
function sendError(error: any) {
// TODO: serialize extra properties, recursive cause, etc.
transport.send({
type: 'custom',
event: 'vite:runtime-log',
data: {
error: {
name: error.name,
message: error.message,
stack: error.stack,
},
} satisfies RuntimeLogPayload,
})
}

if (typeof window !== 'undefined') {
window.addEventListener('error', (event) => {
sendError(event.error)
})
window.addEventListener('unhandledrejection', (event) => {
sendError(event.reason)
})
}

// TODO: server runtime?
// if (typeof process !== 'undefined') {}
}
35 changes: 35 additions & 0 deletions playground/runtime-log/__test__/runtime-log.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { stripVTControlCharacters } from 'node:util'
import { expect, test } from 'vitest'
import { isServe, page, serverLogs } from '~utils'

test.runIf(isServe)('unhandled error', async () => {
await page.click('#test-error')
await expect.poll(() => stripVTControlCharacters(serverLogs.at(-1)))
.toEqual(`\
[Unhandled error] Error: this is test error
> testError src/main.ts:20:8
18 |
19 | function testError() {
20 | throw new Error('this is test error')
| ^
21 | }
22 |
> HTMLButtonElement.<anonymous> src/main.ts:6:2
`)
})

test.runIf(isServe)('unhandled rejection', async () => {
await page.click('#test-unhandledrejection')
await expect.poll(() => stripVTControlCharacters(serverLogs.at(-1)))
.toEqual(`\
[Unhandled error] Error: this is test unhandledrejection
> testUnhandledRejection src/main.ts:24:8
22 |
23 | async function testUnhandledRejection() {
24 | throw new Error('this is test unhandledrejection')
| ^
25 | }
26 |
> HTMLButtonElement.<anonymous> src/main.ts:12:4
`)
})
3 changes: 3 additions & 0 deletions playground/runtime-log/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<button id="test-error">Test error</button>
<button id="test-unhandledrejection">Test unhandledrejection</button>
<script type="module" src="/src/main.ts"></script>
14 changes: 14 additions & 0 deletions playground/runtime-log/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "@vitejs/test-runtime-log",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"debug": "node --inspect-brk ../../packages/vite/bin/vite",
"preview": "vite preview"
},
"dependencies": {},
"devDependencies": {}
}
Binary file added playground/runtime-log/public/favicon.ico
Binary file not shown.
25 changes: 25 additions & 0 deletions playground/runtime-log/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export type SomePadding = {
here: boolean
}

document.getElementById('test-error').addEventListener('click', () => {
testError()
})

document
.getElementById('test-unhandledrejection')
.addEventListener('click', () => {
testUnhandledRejection()
})

export type AnotherPadding = {
there: boolean
}

function testError() {
throw new Error('this is test error')
}

async function testUnhandledRejection() {
throw new Error('this is test unhandledrejection')
}
7 changes: 7 additions & 0 deletions playground/runtime-log/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'

export default defineConfig({
server: {
forwardRuntimeLogs: true,
},
})
Loading
Loading