From c897df27adf1868d6732be4a4134ecab0c11c737 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 10 Oct 2025 00:15:43 +0900 Subject: [PATCH 01/17] feat: log unhandled runtime error on server --- packages/vite/LICENSE.md | 28 +++ packages/vite/package.json | 1 + packages/vite/src/client/client.ts | 2 + packages/vite/src/node/plugins/index.ts | 3 + .../src/node/plugins/runtimeLog-shared.ts | 40 ++++ packages/vite/src/node/plugins/runtimeLog.ts | 205 ++++++++++++++++++ .../runtime-log/__test__/tailwind.spec.ts | 90 ++++++++ playground/runtime-log/index.html | 3 + playground/runtime-log/package.json | 14 ++ playground/runtime-log/public/favicon.ico | Bin 0 -> 4286 bytes playground/runtime-log/src/main.ts | 25 +++ playground/runtime-log/vite.config.ts | 5 + pnpm-lock.yaml | 201 ++++++++++++++++- 13 files changed, 615 insertions(+), 2 deletions(-) create mode 100644 packages/vite/src/node/plugins/runtimeLog-shared.ts create mode 100644 packages/vite/src/node/plugins/runtimeLog.ts create mode 100644 playground/runtime-log/__test__/tailwind.spec.ts create mode 100644 playground/runtime-log/index.html create mode 100644 playground/runtime-log/package.json create mode 100644 playground/runtime-log/public/favicon.ico create mode 100644 playground/runtime-log/src/main.ts create mode 100644 playground/runtime-log/vite.config.ts diff --git a/packages/vite/LICENSE.md b/packages/vite/LICENSE.md index ed5c85681d20bb..1b19839f87fd77 100644 --- a/packages/vite/LICENSE.md +++ b/packages/vite/LICENSE.md @@ -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 diff --git a/packages/vite/package.json b/packages/vite/package.json index 9d1f750f19d214..4b595bf75e9f78 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -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", diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index ef7b6be46dd9b7..ce54d4882d34a5 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -6,6 +6,7 @@ import { normalizeModuleRunnerTransport, } from '../shared/moduleRunnerTransport' import { createHMRHandler } from '../shared/hmrHandler' +import { setupRuntimeLog } from '../node/plugins/runtimeLog-shared' import { ErrorOverlay, cspNonce, overlayId } from './overlay' import '@vite/env' @@ -168,6 +169,7 @@ const hmrClient = new HMRClient( }, ) transport.connect!(createHMRHandler(handleMessage)) +setupRuntimeLog(transport) async function handleMessage(payload: HotPayload) { switch (payload.type) { diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index 82d87ab1f621b7..67a5c8400a6cf0 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -28,6 +28,7 @@ import { createFilterForTransform, createIdFilter, } from './pluginFilter' +import { runtimeLogPlugin } from './runtimeLog' export async function resolvePlugins( config: ResolvedConfig, @@ -73,6 +74,8 @@ export async function resolvePlugins( wasmHelperPlugin(), webWorkerPlugin(config), assetPlugin(config), + // TODO: opt-in + runtimeLogPlugin({ environments: ['client'] }), ...normalPlugins, diff --git a/packages/vite/src/node/plugins/runtimeLog-shared.ts b/packages/vite/src/node/plugins/runtimeLog-shared.ts new file mode 100644 index 00000000000000..97f60b5afa4d84 --- /dev/null +++ b/packages/vite/src/node/plugins/runtimeLog-shared.ts @@ -0,0 +1,40 @@ +import type { NormalizedModuleRunnerTransport } from '../../shared/moduleRunnerTransport' + +export type RuntimeLogPayload = { + error: { + name: string + message: string + stack?: string + } +} + +export function setupRuntimeLog( + 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') {} +} diff --git a/packages/vite/src/node/plugins/runtimeLog.ts b/packages/vite/src/node/plugins/runtimeLog.ts new file mode 100644 index 00000000000000..4956139868ee0a --- /dev/null +++ b/packages/vite/src/node/plugins/runtimeLog.ts @@ -0,0 +1,205 @@ +import path from 'node:path' +import fs from 'node:fs' +import { stripVTControlCharacters } from 'node:util' +import { parseErrorStacktrace } from '@vitest/utils/source-map' +import type { DevEnvironment, Plugin } from '..' +import { normalizePath } from '..' + +export function runtimeLogPlugin(pluginOpts?: { + /** @default ["client"] */ + environments?: string[] +}): Plugin { + const environmentNames = pluginOpts?.environments || ['client'] + + return { + name: 'vite:runtime-log', + apply: 'serve', + configureServer(server) { + for (const name of environmentNames) { + const environment = server.environments[name] + environment.hot.on('vite:runtime-log', (payload: RuntimeLogPayload) => { + const output = formatError(payload.error, environment) + environment.config.logger.error('[RUNTIME] ' + 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 = '' + output += `${error.name}: ${error.message}\n` + for (const stack of stacks) { + const file = 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') + output += generateCodeFrame(code, 4, stack) + output += '\n' + } + } + return output +} + +const c = { + gray: (s: string) => s, + red: (s: string) => s, +} + +function generateCodeFrame( + source: string, + indent = 0, + loc: { line: number; column: number } | number, + range = 2, +): string { + const start = + typeof loc === 'object' + ? positionToOffset(source, loc.line, loc.column) + : loc + const end = start + const lines = source.split(lineSplitRE) + const nl = /\r\n/.test(source) ? 2 : 1 + let count = 0 + let res: string[] = [] + + const columns = process.stdout?.columns || 80 + + for (let i = 0; i < lines.length; i++) { + count += lines[i].length + nl + if (count >= start) { + for (let j = i - range; j <= i + range || end > count; j++) { + if (j < 0 || j >= lines.length) { + continue + } + + const lineLength = lines[j].length + const strippedContent = stripVTControlCharacters(lines[j]) + + if (strippedContent.startsWith('//# sourceMappingURL')) { + continue + } + + // too long, maybe it's a minified file, skip for codeframe + if (strippedContent.length > 200) { + return '' + } + + res.push( + lineNo(j + 1) + + truncateString(lines[j].replace(/\t/g, ' '), columns - 5 - indent), + ) + + if (j === i) { + // push underline + const pad = start - (count - lineLength) + (nl - 1) + const length = Math.max( + 1, + end > count ? lineLength - pad : end - start, + ) + res.push(lineNo() + ' '.repeat(pad) + c.red('^'.repeat(length))) + } else if (j > i) { + if (end > count) { + const length = Math.max(1, Math.min(end - count, lineLength)) + res.push(lineNo() + c.red('^'.repeat(length))) + } + count += lineLength + 1 + } + } + break + } + } + + if (indent) { + res = res.map((line) => ' '.repeat(indent) + line) + } + + return res.join('\n') +} + +function lineNo(no: number | string = '') { + return c.gray(`${String(no).padStart(3, ' ')}| `) +} + +const lineSplitRE: RegExp = /\r?\n/ + +function positionToOffset( + source: string, + lineNumber: number, + columnNumber: number, +): number { + const lines = source.split(lineSplitRE) + const nl = /\r\n/.test(source) ? 2 : 1 + let start = 0 + + if (lineNumber > lines.length) { + return source.length + } + + for (let i = 0; i < lineNumber - 1; i++) { + start += lines[i].length + nl + } + + return start + columnNumber +} + +function truncateString(text: string, maxLength: number): string { + const plainText = stripVTControlCharacters(text) + + if (plainText.length <= maxLength) { + return text + } + + return `${plainText.slice(0, maxLength - 1)}…` +} diff --git a/playground/runtime-log/__test__/tailwind.spec.ts b/playground/runtime-log/__test__/tailwind.spec.ts new file mode 100644 index 00000000000000..a8b977ef9e1f3d --- /dev/null +++ b/playground/runtime-log/__test__/tailwind.spec.ts @@ -0,0 +1,90 @@ +import { expect, test } from 'vitest' +import { editFile, getColor, isServe, page, untilBrowserLogAfter } from '~utils' + +test('should render', async () => { + expect(await page.textContent('#pagetitle')).toBe('Page title') +}) + +test.runIf(isServe)( + 'full reload happens when the HTML is changed', + async () => { + await expect + .poll(() => getColor('.html')) + .toBe('oklch(0.623 0.214 259.815)') + + editFile('index.html', (code) => + code.replace('"html text-blue-500"', '"html text-green-500"'), + ) + await expect + .poll(() => getColor('.html')) + .toBe('oklch(0.723 0.219 149.579)') + }, +) + +test.runIf(isServe)('regenerate CSS and HMR (glob pattern)', async () => { + const el = page.locator('#view1-text') + expect(await getColor(el)).toBe('oklch(0.627 0.194 149.214)') + + await untilBrowserLogAfter( + () => + editFile('src/views/view1.js', (code) => + code.replace('|view1|', '|view1 updated|'), + ), + [ + '[vite] css hot updated: /index.css', + '[vite] hot updated: /src/views/view1.js via /src/main.js', + ], + false, + ) + await expect.poll(() => el.textContent()).toMatch('|view1 updated|') + + await untilBrowserLogAfter( + () => + editFile('src/views/view1.js', (code) => + code.replace('text-green-600', 'text-orange-600'), + ), + [ + '[vite] css hot updated: /index.css', + '[vite] hot updated: /src/views/view1.js via /src/main.js', + ], + false, + ) + await expect.poll(() => getColor(el)).toBe('oklch(0.646 0.222 41.116)') +}) + +test.runIf(isServe)( + 'same file duplicated in module graph (#4267)', + async () => { + const el = page.locator('#component1') + expect(await getColor(el)).toBe('oklch(0.577 0.245 27.325)') + + // when duplicated, page reload happens + await untilBrowserLogAfter( + () => + editFile('src/components/component1.js', (code) => + code.replace('text-red-600', 'text-blue-600'), + ), + [ + '[vite] css hot updated: /index.css', + '[vite] hot updated: /src/components/component1.js', + ], + false, + ) + await expect.poll(() => getColor(el)).toBe('oklch(0.546 0.245 262.881)') + }, +) + +test.runIf(isServe)('regenerate CSS and HMR (relative path)', async () => { + const el = page.locator('#pagetitle') + expect(await getColor(el)).toBe('oklch(0.541 0.281 293.009)') + + await untilBrowserLogAfter( + () => + editFile('src/main.js', (code) => + code.replace('text-violet-600', 'text-cyan-600'), + ), + ['[vite] css hot updated: /index.css', '[vite] hot updated: /src/main.js'], + false, + ) + await expect.poll(() => getColor(el)).toBe('oklch(0.609 0.126 221.723)') +}) diff --git a/playground/runtime-log/index.html b/playground/runtime-log/index.html new file mode 100644 index 00000000000000..dd0f5ecab45f54 --- /dev/null +++ b/playground/runtime-log/index.html @@ -0,0 +1,3 @@ + + + diff --git a/playground/runtime-log/package.json b/playground/runtime-log/package.json new file mode 100644 index 00000000000000..1a1262012bd799 --- /dev/null +++ b/playground/runtime-log/package.json @@ -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": {} +} diff --git a/playground/runtime-log/public/favicon.ico b/playground/runtime-log/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..df36fcfb72584e00488330b560ebcf34a41c64c2 GIT binary patch literal 4286 zcmds*O-Phc6o&64GDVCEQHxsW(p4>LW*W<827=Unuo8sGpRux(DN@jWP-e29Wl%wj zY84_aq9}^Am9-cWTD5GGEo#+5Fi2wX_P*bo+xO!)p*7B;iKlbFd(U~_d(U?#hLj56 zPhFkj-|A6~Qk#@g^#D^U0XT1cu=c-vu1+SElX9NR;kzAUV(q0|dl0|%h|dI$%VICy zJnu2^L*Te9JrJMGh%-P79CL0}dq92RGU6gI{v2~|)p}sG5x0U*z<8U;Ij*hB9z?ei z@g6Xq-pDoPl=MANPiR7%172VA%r)kevtV-_5H*QJKFmd;8yA$98zCxBZYXTNZ#QFk2(TX0;Y2dt&WitL#$96|gJY=3xX zpCoi|YNzgO3R`f@IiEeSmKrPSf#h#Qd<$%Ej^RIeeYfsxhPMOG`S`Pz8q``=511zm zAm)MX5AV^5xIWPyEu7u>qYs?pn$I4nL9J!=K=SGlKLXpE<5x+2cDTXq?brj?n6sp= zphe9;_JHf40^9~}9i08r{XM$7HB!`{Ys~TK0kx<}ZQng`UPvH*11|q7&l9?@FQz;8 zx!=3<4seY*%=OlbCbcae?5^V_}*K>Uo6ZWV8mTyE^B=DKy7-sdLYkR5Z?paTgK-zyIkKjIcpyO z{+uIt&YSa_$QnN_@t~L014dyK(fOOo+W*MIxbA6Ndgr=Y!f#Tokqv}n<7-9qfHkc3 z=>a|HWqcX8fzQCT=dqVbogRq!-S>H%yA{1w#2Pn;=e>JiEj7Hl;zdt-2f+j2%DeVD zsW0Ab)ZK@0cIW%W7z}H{&~yGhn~D;aiP4=;m-HCo`BEI+Kd6 z={Xwx{TKxD#iCLfl2vQGDitKtN>z|-AdCN|$jTFDg0m3O`WLD4_s#$S literal 0 HcmV?d00001 diff --git a/playground/runtime-log/src/main.ts b/playground/runtime-log/src/main.ts new file mode 100644 index 00000000000000..23b2726398d599 --- /dev/null +++ b/playground/runtime-log/src/main.ts @@ -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('testError') +} + +async function testUnhandledRejection() { + throw new Error('testUnhandledRejection') +} diff --git a/playground/runtime-log/vite.config.ts b/playground/runtime-log/vite.config.ts new file mode 100644 index 00000000000000..4c9c4be6ba0c82 --- /dev/null +++ b/playground/runtime-log/vite.config.ts @@ -0,0 +1,5 @@ +import { defineConfig } from 'vite' + +export default defineConfig({ + server: {}, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b8d778c7a39037..86cd6c04a51ac9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -287,6 +287,9 @@ importers: '@types/pnpapi': specifier: ^0.0.5 version: 0.0.5 + '@vitest/utils': + specifier: ^3.2.4 + version: 3.2.4 artichokie: specifier: ^0.4.2 version: 0.4.2 @@ -1399,6 +1402,8 @@ importers: playground/resolve/utf8-bom-package: {} + playground/runtime-log: {} + playground/self-referencing: {} playground/ssr: @@ -2762,6 +2767,9 @@ packages: '@napi-rs/wasm-runtime@1.0.5': resolution: {integrity: sha512-TBr9Cf9onSAS2LQ2+QHx6XcC6h9+RIzJgbqG3++9TUZSH204AwEy5jg3BTQ0VATsyoGj4ee49tN/y6rvaOOtcg==} + '@napi-rs/wasm-runtime@1.0.6': + resolution: {integrity: sha512-DXj75ewm11LIWUk198QSKUTxjyRjsBwk09MuMk5DGK+GDUtyPhhEHOGP/Xwwj3DjQXXkivoBirmOnKrLfc0+9g==} + '@node-rs/bcrypt-android-arm-eabi@1.10.7': resolution: {integrity: sha512-8dO6/PcbeMZXS3VXGEtct9pDYdShp2WBOWlDvSbcRwVqyB580aCBh0BEFmKYtXLzLvUK8Wf+CG3U6sCdILW1lA==} engines: {node: '>= 10'} @@ -2867,6 +2875,9 @@ packages: '@oxc-project/types@0.93.0': resolution: {integrity: sha512-yNtwmWZIBtJsMr5TEfoZFDxIWV6OdScOpza/f5YxbqUMJk+j6QX3Cf3jgZShGEFYWQJ5j9mJ6jM0tZHu2J9Yrg==} + '@oxc-project/types@0.94.0': + resolution: {integrity: sha512-+UgQT/4o59cZfH6Cp7G0hwmqEQ0wE+AdIwhikdwnhWI9Dp8CgSY081+Q3O67/wq3VJu8mgUEB93J9EHHn70fOw==} + '@parcel/watcher-android-arm64@2.5.1': resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} engines: {node: '>= 10.0.0'} @@ -2982,89 +2993,175 @@ packages: cpu: [arm64] os: [android] + '@rolldown/binding-android-arm64@1.0.0-beta.42': + resolution: {integrity: sha512-W5ZKF3TP3bOWuBfotAGp+UGjxOkGV7jRmIRbBA7NFjggx7Oi6vOmGDqpHEIX7kDCiry1cnIsWQaxNvWbMdkvzQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + '@rolldown/binding-darwin-arm64@1.0.0-beta.41': resolution: {integrity: sha512-XGCzqfjdk7550PlyZRTBKbypXrB7ATtXhw/+bjtxnklLQs0mKP/XkQVOKyn9qGKSlvH8I56JLYryVxl0PCvSNw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] + '@rolldown/binding-darwin-arm64@1.0.0-beta.42': + resolution: {integrity: sha512-abw/wtgJA8OCgaTlL+xJxnN/Z01BwV1rfzIp5Hh9x+IIO6xOBfPsQ0nzi0+rWx3TyZ9FZXyC7bbC+5NpQ9EaXQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + '@rolldown/binding-darwin-x64@1.0.0-beta.41': resolution: {integrity: sha512-Ho6lIwGJed98zub7n0xcRKuEtnZgbxevAmO4x3zn3C3N4GVXZD5xvCvTVxSMoeBJwTcIYzkVDRTIhylQNsTgLQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] + '@rolldown/binding-darwin-x64@1.0.0-beta.42': + resolution: {integrity: sha512-Y/UrZIRVr8CvXVEB88t6PeC46r1K9/QdPEo2ASE/b/KBEyXIx+QbM6kv9QfQVWU2Atly2+SVsQzxQsIvuk3lZQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + '@rolldown/binding-freebsd-x64@1.0.0-beta.41': resolution: {integrity: sha512-ijAZETywvL+gACjbT4zBnCp5ez1JhTRs6OxRN4J+D6AzDRbU2zb01Esl51RP5/8ZOlvB37xxsRQ3X4YRVyYb3g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] + '@rolldown/binding-freebsd-x64@1.0.0-beta.42': + resolution: {integrity: sha512-zRM0oOk7BZiy6DoWBvdV4hyEg+j6+WcBZIMHVirMEZRu8hd18kZdJkg+bjVMfCEhwpWeFUfBfZ1qcaZ5UdYzlQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.41': resolution: {integrity: sha512-EgIOZt7UildXKFEFvaiLNBXm+4ggQyGe3E5Z1QP9uRcJJs9omihOnm897FwOBQdCuMvI49iBgjFrkhH+wMJ2MA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.42': + resolution: {integrity: sha512-6RjFaC52QNwo7ilU8C5H7swbGlgfTkG9pudXwzr3VYyT18s0C9gLg3mvc7OMPIGqNxnQ0M5lU8j6aQCk2DTRVg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.41': resolution: {integrity: sha512-F8bUwJq8v/JAU8HSwgF4dztoqJ+FjdyjuvX4//3+Fbe2we9UktFeZ27U4lRMXF1vxWtdV4ey6oCSqI7yUrSEeg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.42': + resolution: {integrity: sha512-LMYHM5Sf6ROq+VUwHMDVX2IAuEsWTv4SnlFEedBnMGpvRuQ14lCmD4m5Q8sjyAQCgyha9oghdGoK8AEg1sXZKg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.41': resolution: {integrity: sha512-MioXcCIX/wB1pBnBoJx8q4OGucUAfC1+/X1ilKFsjDK05VwbLZGRgOVD5OJJpUQPK86DhQciNBrfOKDiatxNmg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.42': + resolution: {integrity: sha512-/bNTYb9aKNhzdbPn3O4MK2aLv55AlrkUKPE4KNfBYjkoZUfDr4jWp7gsSlvTc5A/99V1RCm9axvt616ZzeXGyA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.41': resolution: {integrity: sha512-m66M61fizvRCwt5pOEiZQMiwBL9/y0bwU/+Kc4Ce/Pef6YfoEkR28y+DzN9rMdjo8Z28NXjsDPq9nH4mXnAP0g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.42': + resolution: {integrity: sha512-n/SLa4h342oyeGykZdch7Y3GNCNliRPL4k5wkeZ/5eQZs+c6/ZG1SHCJQoy7bZcmxiMyaXs9HoFmv1PEKrZgWg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + '@rolldown/binding-linux-x64-musl@1.0.0-beta.41': resolution: {integrity: sha512-yRxlSfBvWnnfrdtJfvi9lg8xfG5mPuyoSHm0X01oiE8ArmLRvoJGHUTJydCYz+wbK2esbq5J4B4Tq9WAsOlP1Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + '@rolldown/binding-linux-x64-musl@1.0.0-beta.42': + resolution: {integrity: sha512-4PSd46sFzqpLHSGdaSViAb1mk55sCUMpJg+X8ittXaVocQsV3QLG/uydSH8RyL0ngHX5fy3D70LcCzlB15AgHw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + '@rolldown/binding-openharmony-arm64@1.0.0-beta.41': resolution: {integrity: sha512-PHVxYhBpi8UViS3/hcvQQb9RFqCtvFmFU1PvUoTRiUdBtgHA6fONNHU4x796lgzNlVSD3DO/MZNk1s5/ozSMQg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] + '@rolldown/binding-openharmony-arm64@1.0.0-beta.42': + resolution: {integrity: sha512-BmWoeJJyeZXmZBcfoxG6J9+rl2G7eO47qdTkAzEegj4n3aC6CBIHOuDcbE8BvhZaEjQR0nh0nJrtEDlt65Q7Sw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + '@rolldown/binding-wasm32-wasi@1.0.0-beta.41': resolution: {integrity: sha512-OAfcO37ME6GGWmj9qTaDT7jY4rM0T2z0/8ujdQIJQ2x2nl+ztO32EIwURfmXOK0U1tzkyuaKYvE34Pug/ucXlQ==} engines: {node: '>=14.0.0'} cpu: [wasm32] + '@rolldown/binding-wasm32-wasi@1.0.0-beta.42': + resolution: {integrity: sha512-2Ft32F7uiDTrGZUKws6CLNTlvTWHC33l4vpXrzUucf9rYtUThAdPCOt89Pmn13tNX6AulxjGEP2R0nZjTSW3eQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.41': resolution: {integrity: sha512-NIYGuCcuXaq5BC4Q3upbiMBvmZsTsEPG9k/8QKQdmrch+ocSy5Jv9tdpdmXJyighKqm182nh/zBt+tSJkYoNlg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.42': + resolution: {integrity: sha512-hC1kShXW/z221eG+WzQMN06KepvPbMBknF0iGR3VMYJLOe9gwnSTfGxFT5hf8XrPv7CEZqTWRd0GQpkSHRbGsw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.41': resolution: {integrity: sha512-kANdsDbE5FkEOb5NrCGBJBCaZ2Sabp3D7d4PRqMYJqyLljwh9mDyYyYSv5+QNvdAmifj+f3lviNEUUuUZPEFPw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.42': + resolution: {integrity: sha512-AICBYromawouGjj+GS33369E8Vwhy6UwhQEhQ5evfS8jPCsyVvoICJatbDGDGH01dwtVGLD5eDFzPicUOVpe4g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.41': resolution: {integrity: sha512-UlpxKmFdik0Y2VjZrgUCgoYArZJiZllXgIipdBRV1hw6uK45UbQabSTW6Kp6enuOu7vouYWftwhuxfpE8J2JAg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.42': + resolution: {integrity: sha512-XpZ0M+tjoEiSc9c+uZR7FCnOI0uxDRNs1elGOMjeB0pUP1QmvVbZGYNsyLbLoP4u7e3VQN8rie1OQ8/mB6rcJg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@rolldown/pluginutils@1.0.0-beta.29': resolution: {integrity: sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==} '@rolldown/pluginutils@1.0.0-beta.41': resolution: {integrity: sha512-ycMEPrS3StOIeb87BT3/+bu+blEtyvwQ4zmo2IcJQy0Rd1DAAhKksA0iUZ3MYSpJtjlPhg0Eo6mvVS6ggPhRbw==} + '@rolldown/pluginutils@1.0.0-beta.42': + resolution: {integrity: sha512-N7pQzk9CyE7q0bBN/q0J8s6Db279r5kUZc6d7/wWRe9/zXqC52HQovVyu6iXPIDY4BEzzgbVLhVFXrOuGJ22ZQ==} + '@rollup/plugin-alias@5.1.1': resolution: {integrity: sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==} engines: {node: '>=14.0.0'} @@ -6570,6 +6667,11 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + rolldown@1.0.0-beta.42: + resolution: {integrity: sha512-xaPcckj+BbJhYLsv8gOqezc8EdMcKKe/gk8v47B0KPvgABDrQ0qmNPAiT/gh9n9Foe0bUkEv2qzj42uU5q1WRg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + rollup-plugin-license@3.6.0: resolution: {integrity: sha512-1ieLxTCaigI5xokIfszVDRoy6c/Wmlot1fDEnea7Q/WXSR8AqOjYljHDLObAx7nFxHC2mbxT3QnTSPhaic2IYw==} engines: {node: '>=14.0.0'} @@ -8682,6 +8784,13 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@napi-rs/wasm-runtime@1.0.6': + dependencies: + '@emnapi/core': 1.5.0 + '@emnapi/runtime': 1.5.0 + '@tybys/wasm-util': 0.10.1 + optional: true + '@node-rs/bcrypt-android-arm-eabi@1.10.7': optional: true @@ -8759,6 +8868,8 @@ snapshots: '@oxc-project/types@0.93.0': {} + '@oxc-project/types@0.94.0': {} + '@parcel/watcher-android-arm64@2.5.1': optional: true @@ -8848,51 +8959,97 @@ snapshots: '@rolldown/binding-android-arm64@1.0.0-beta.41': optional: true + '@rolldown/binding-android-arm64@1.0.0-beta.42': + optional: true + '@rolldown/binding-darwin-arm64@1.0.0-beta.41': optional: true + '@rolldown/binding-darwin-arm64@1.0.0-beta.42': + optional: true + '@rolldown/binding-darwin-x64@1.0.0-beta.41': optional: true + '@rolldown/binding-darwin-x64@1.0.0-beta.42': + optional: true + '@rolldown/binding-freebsd-x64@1.0.0-beta.41': optional: true + '@rolldown/binding-freebsd-x64@1.0.0-beta.42': + optional: true + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.41': optional: true + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.42': + optional: true + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.41': optional: true + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.42': + optional: true + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.41': optional: true + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.42': + optional: true + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.41': optional: true + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.42': + optional: true + '@rolldown/binding-linux-x64-musl@1.0.0-beta.41': optional: true + '@rolldown/binding-linux-x64-musl@1.0.0-beta.42': + optional: true + '@rolldown/binding-openharmony-arm64@1.0.0-beta.41': optional: true + '@rolldown/binding-openharmony-arm64@1.0.0-beta.42': + optional: true + '@rolldown/binding-wasm32-wasi@1.0.0-beta.41': dependencies: '@napi-rs/wasm-runtime': 1.0.5 optional: true + '@rolldown/binding-wasm32-wasi@1.0.0-beta.42': + dependencies: + '@napi-rs/wasm-runtime': 1.0.6 + optional: true + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.41': optional: true + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.42': + optional: true + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.41': optional: true + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.42': + optional: true + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.41': optional: true + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.42': + optional: true + '@rolldown/pluginutils@1.0.0-beta.29': {} '@rolldown/pluginutils@1.0.0-beta.41': {} + '@rolldown/pluginutils@1.0.0-beta.42': {} + '@rollup/plugin-alias@5.1.1(rollup@4.43.0)': optionalDependencies: rollup: 4.43.0 @@ -12535,6 +12692,25 @@ snapshots: - oxc-resolver - supports-color + rolldown-plugin-dts@0.16.11(rolldown@1.0.0-beta.42)(typescript@5.9.2)(vue-tsc@3.1.0(typescript@5.9.2)): + dependencies: + '@babel/generator': 7.28.3 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + ast-kit: 2.1.2 + birpc: 2.6.1 + debug: 4.4.3 + dts-resolver: 2.1.2 + get-tsconfig: 4.10.1 + magic-string: 0.30.19 + rolldown: 1.0.0-beta.42 + optionalDependencies: + typescript: 5.9.2 + vue-tsc: 3.1.0(typescript@5.9.2) + transitivePeerDependencies: + - oxc-resolver + - supports-color + rolldown@1.0.0-beta.41: dependencies: '@oxc-project/types': 0.93.0 @@ -12556,6 +12732,27 @@ snapshots: '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.41 '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.41 + rolldown@1.0.0-beta.42: + dependencies: + '@oxc-project/types': 0.94.0 + '@rolldown/pluginutils': 1.0.0-beta.42 + ansis: 4.2.0 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-beta.42 + '@rolldown/binding-darwin-arm64': 1.0.0-beta.42 + '@rolldown/binding-darwin-x64': 1.0.0-beta.42 + '@rolldown/binding-freebsd-x64': 1.0.0-beta.42 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.42 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.42 + '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.42 + '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.42 + '@rolldown/binding-linux-x64-musl': 1.0.0-beta.42 + '@rolldown/binding-openharmony-arm64': 1.0.0-beta.42 + '@rolldown/binding-wasm32-wasi': 1.0.0-beta.42 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.42 + '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.42 + '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.42 + rollup-plugin-license@3.6.0(picomatch@4.0.3)(rollup@4.43.0): dependencies: commenting: 1.1.0 @@ -13179,8 +13376,8 @@ snapshots: diff: 8.0.2 empathic: 2.0.0 hookable: 5.5.3 - rolldown: 1.0.0-beta.41 - rolldown-plugin-dts: 0.16.11(rolldown@1.0.0-beta.41)(typescript@5.9.2)(vue-tsc@3.1.0(typescript@5.9.2)) + rolldown: 1.0.0-beta.42 + rolldown-plugin-dts: 0.16.11(rolldown@1.0.0-beta.42)(typescript@5.9.2)(vue-tsc@3.1.0(typescript@5.9.2)) semver: 7.7.2 tinyexec: 1.0.1 tinyglobby: 0.2.15 From c9fb02890e15939c3692f6e205d707e2c5e1d1ba Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 10 Oct 2025 00:27:49 +0900 Subject: [PATCH 02/17] tweak --- packages/vite/src/node/plugins/runtimeLog.ts | 12 +++++------- playground/runtime-log/src/main.ts | 4 ++-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/vite/src/node/plugins/runtimeLog.ts b/packages/vite/src/node/plugins/runtimeLog.ts index 4956139868ee0a..df4fc6beb00a57 100644 --- a/packages/vite/src/node/plugins/runtimeLog.ts +++ b/packages/vite/src/node/plugins/runtimeLog.ts @@ -2,6 +2,7 @@ import path from 'node:path' import fs from 'node:fs' import { stripVTControlCharacters } from 'node:util' import { parseErrorStacktrace } from '@vitest/utils/source-map' +import c from 'picocolors' import type { DevEnvironment, Plugin } from '..' import { normalizePath } from '..' @@ -19,7 +20,7 @@ export function runtimeLogPlugin(pluginOpts?: { const environment = server.environments[name] environment.hot.on('vite:runtime-log', (payload: RuntimeLogPayload) => { const output = formatError(payload.error, environment) - environment.config.logger.error('[RUNTIME] ' + output, { + environment.config.logger.error(output, { timestamp: true, }) }) @@ -78,7 +79,8 @@ function formatError(error: any, environment: DevEnvironment) { }) let output = '' - output += `${error.name}: ${error.message}\n` + const errorName = error.name || 'Unknown Error' + output += c.red(`[Unhandled error] ${c.bold(errorName)}: ${error.message}\n`) for (const stack of stacks) { const file = path.relative(environment.config.root, stack.file) output += ` > ${[stack.method, `${file}:${stack.line}:${stack.column}`] @@ -86,6 +88,7 @@ function formatError(error: any, environment: DevEnvironment) { .join(' ')}\n` if (stack === nearest) { const code = fs.readFileSync(stack.file, 'utf-8') + // TODO: highlight output += generateCodeFrame(code, 4, stack) output += '\n' } @@ -93,11 +96,6 @@ function formatError(error: any, environment: DevEnvironment) { return output } -const c = { - gray: (s: string) => s, - red: (s: string) => s, -} - function generateCodeFrame( source: string, indent = 0, diff --git a/playground/runtime-log/src/main.ts b/playground/runtime-log/src/main.ts index 23b2726398d599..0f064890c35c06 100644 --- a/playground/runtime-log/src/main.ts +++ b/playground/runtime-log/src/main.ts @@ -17,9 +17,9 @@ export type AnotherPadding = { } function testError() { - throw new Error('testError') + throw new Error('this is test error') } async function testUnhandledRejection() { - throw new Error('testUnhandledRejection') + throw new Error('this is test unhandledrejection') } From 13958cf9e73a7f6501daacef7664dd7f8362ea69 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 10 Oct 2025 00:53:12 +0900 Subject: [PATCH 03/17] test: add test --- .../runtime-log/__test__/runtime-log.spec.ts | 33 +++++++ .../runtime-log/__test__/tailwind.spec.ts | 90 ------------------- 2 files changed, 33 insertions(+), 90 deletions(-) create mode 100644 playground/runtime-log/__test__/runtime-log.spec.ts delete mode 100644 playground/runtime-log/__test__/tailwind.spec.ts diff --git a/playground/runtime-log/__test__/runtime-log.spec.ts b/playground/runtime-log/__test__/runtime-log.spec.ts new file mode 100644 index 00000000000000..e3049df5cc624f --- /dev/null +++ b/playground/runtime-log/__test__/runtime-log.spec.ts @@ -0,0 +1,33 @@ +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(() => 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. src/main.ts:6:2 +`) + console.log(serverLogs.at(-1)) +}) + +test.runIf(isServe)('unhandled rejection', async () => { + await page.click('#test-unhandledrejection') + await expect.poll(() => 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. src/main.ts:12:4 +`) +}) diff --git a/playground/runtime-log/__test__/tailwind.spec.ts b/playground/runtime-log/__test__/tailwind.spec.ts deleted file mode 100644 index a8b977ef9e1f3d..00000000000000 --- a/playground/runtime-log/__test__/tailwind.spec.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { expect, test } from 'vitest' -import { editFile, getColor, isServe, page, untilBrowserLogAfter } from '~utils' - -test('should render', async () => { - expect(await page.textContent('#pagetitle')).toBe('Page title') -}) - -test.runIf(isServe)( - 'full reload happens when the HTML is changed', - async () => { - await expect - .poll(() => getColor('.html')) - .toBe('oklch(0.623 0.214 259.815)') - - editFile('index.html', (code) => - code.replace('"html text-blue-500"', '"html text-green-500"'), - ) - await expect - .poll(() => getColor('.html')) - .toBe('oklch(0.723 0.219 149.579)') - }, -) - -test.runIf(isServe)('regenerate CSS and HMR (glob pattern)', async () => { - const el = page.locator('#view1-text') - expect(await getColor(el)).toBe('oklch(0.627 0.194 149.214)') - - await untilBrowserLogAfter( - () => - editFile('src/views/view1.js', (code) => - code.replace('|view1|', '|view1 updated|'), - ), - [ - '[vite] css hot updated: /index.css', - '[vite] hot updated: /src/views/view1.js via /src/main.js', - ], - false, - ) - await expect.poll(() => el.textContent()).toMatch('|view1 updated|') - - await untilBrowserLogAfter( - () => - editFile('src/views/view1.js', (code) => - code.replace('text-green-600', 'text-orange-600'), - ), - [ - '[vite] css hot updated: /index.css', - '[vite] hot updated: /src/views/view1.js via /src/main.js', - ], - false, - ) - await expect.poll(() => getColor(el)).toBe('oklch(0.646 0.222 41.116)') -}) - -test.runIf(isServe)( - 'same file duplicated in module graph (#4267)', - async () => { - const el = page.locator('#component1') - expect(await getColor(el)).toBe('oklch(0.577 0.245 27.325)') - - // when duplicated, page reload happens - await untilBrowserLogAfter( - () => - editFile('src/components/component1.js', (code) => - code.replace('text-red-600', 'text-blue-600'), - ), - [ - '[vite] css hot updated: /index.css', - '[vite] hot updated: /src/components/component1.js', - ], - false, - ) - await expect.poll(() => getColor(el)).toBe('oklch(0.546 0.245 262.881)') - }, -) - -test.runIf(isServe)('regenerate CSS and HMR (relative path)', async () => { - const el = page.locator('#pagetitle') - expect(await getColor(el)).toBe('oklch(0.541 0.281 293.009)') - - await untilBrowserLogAfter( - () => - editFile('src/main.js', (code) => - code.replace('text-violet-600', 'text-cyan-600'), - ), - ['[vite] css hot updated: /index.css', '[vite] hot updated: /src/main.js'], - false, - ) - await expect.poll(() => getColor(el)).toBe('oklch(0.609 0.126 221.723)') -}) From e8ea26a264bef2c79a575bd734ec1a127a85c8a3 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 10 Oct 2025 00:53:25 +0900 Subject: [PATCH 04/17] cleanup --- playground/runtime-log/__test__/runtime-log.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/playground/runtime-log/__test__/runtime-log.spec.ts b/playground/runtime-log/__test__/runtime-log.spec.ts index e3049df5cc624f..c64732bb3d535c 100644 --- a/playground/runtime-log/__test__/runtime-log.spec.ts +++ b/playground/runtime-log/__test__/runtime-log.spec.ts @@ -14,7 +14,6 @@ test.runIf(isServe)('unhandled error', async () => { 22| > HTMLButtonElement. src/main.ts:6:2 `) - console.log(serverLogs.at(-1)) }) test.runIf(isServe)('unhandled rejection', async () => { From 67c7ac29f45dd8a0048b2ce969794b81590b75ec Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 10 Oct 2025 01:01:46 +0900 Subject: [PATCH 05/17] feat: add forwardRuntimeLogs --- packages/vite/src/node/plugins/index.ts | 4 ++-- packages/vite/src/node/server/index.ts | 3 +++ playground/runtime-log/vite.config.ts | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index 67a5c8400a6cf0..fc3cc112dc7a7a 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -74,8 +74,8 @@ export async function resolvePlugins( wasmHelperPlugin(), webWorkerPlugin(config), assetPlugin(config), - // TODO: opt-in - runtimeLogPlugin({ environments: ['client'] }), + config.server.forwardRuntimeLogs && + runtimeLogPlugin({ environments: ['client'] }), ...normalPlugins, diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 7d250a68a8ff41..cc5d361a7d8c02 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -188,6 +188,8 @@ export interface ServerOptions extends CommonServerOptions { server: ViteDevServer, hmr: (environment: DevEnvironment) => Promise, ) => Promise + + forwardRuntimeLogs?: boolean } export interface ResolvedServerOptions @@ -1103,6 +1105,7 @@ export const serverConfigDefaults = Object.freeze({ // sourcemapIgnoreList perEnvironmentStartEndDuringDev: false, // hotUpdateEnvironments + forwardRuntimeLogs: false, } satisfies ServerOptions) export function resolveServerOptions( diff --git a/playground/runtime-log/vite.config.ts b/playground/runtime-log/vite.config.ts index 4c9c4be6ba0c82..380bc046e26b33 100644 --- a/playground/runtime-log/vite.config.ts +++ b/playground/runtime-log/vite.config.ts @@ -1,5 +1,7 @@ import { defineConfig } from 'vite' export default defineConfig({ - server: {}, + server: { + forwardRuntimeLogs: true, + }, }) From bfd7a893afd51446fadd8f78d431377383ec047a Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 10 Oct 2025 01:15:57 +0900 Subject: [PATCH 06/17] docs: add server.forwardRuntimeLogs documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- docs/config/server-options.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/docs/config/server-options.md b/docs/config/server-options.md index 6e646c97472e92..3b355014250b31 100644 --- a/docs/config/server-options.md +++ b/docs/config/server-options.md @@ -226,6 +226,38 @@ 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: + +- Source-mapped stack traces +- Code frames showing the error location +- Relative file paths for easier navigation + +This helps developers quickly identify and debug client-side runtime errors without having to check the browser console. + +::: tip +This feature is particularly useful when: + +- Developing applications where browser console access is limited +- Working with AI coding assistants that can only see terminal output +- Debugging in headless browser environments +- You prefer to see all errors in a centralized location + ::: + +Example configuration: + +```js +export default defineConfig({ + server: { + forwardRuntimeLogs: true, + }, +}) +``` + ## server.warmup - **Type:** `{ clientFiles?: string[], ssrFiles?: string[] }` From b6e589cfadabb2f0a733e484099cba5c0ea516f0 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 10 Oct 2025 01:19:34 +0900 Subject: [PATCH 07/17] docs --- docs/config/server-options.md | 35 ++++++++++++----------------------- 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/docs/config/server-options.md b/docs/config/server-options.md index 3b355014250b31..ecf4133f64db01 100644 --- a/docs/config/server-options.md +++ b/docs/config/server-options.md @@ -233,31 +233,20 @@ The error that appears in the Browser when the fallback happens can be ignored. 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: -- Source-mapped stack traces -- Code frames showing the error location -- Relative file paths for easier navigation - -This helps developers quickly identify and debug client-side runtime errors without having to check the browser console. - -::: tip -This feature is particularly useful when: - -- Developing applications where browser console access is limited -- Working with AI coding assistants that can only see terminal output -- Debugging in headless browser environments -- You prefer to see all errors in a centralized location - ::: - -Example configuration: - -```js -export default defineConfig({ - server: { - forwardRuntimeLogs: true, - }, -}) +```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. src/main.ts:6:2 ``` +This feature is useful when working with AI coding assistants that sees terminal output for the context. + ## server.warmup - **Type:** `{ clientFiles?: string[], ssrFiles?: string[] }` From b8db0dcbd2b3a8d3c3faef03071913500bc864dd Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 10 Oct 2025 01:21:20 +0900 Subject: [PATCH 08/17] docs: improve grammar in forwardRuntimeLogs documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- docs/config/server-options.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config/server-options.md b/docs/config/server-options.md index ecf4133f64db01..1a3ccc59a341ea 100644 --- a/docs/config/server-options.md +++ b/docs/config/server-options.md @@ -245,7 +245,7 @@ Forward unhandled runtime errors from the browser to the Vite server console dur > HTMLButtonElement. src/main.ts:6:2 ``` -This feature is useful when working with AI coding assistants that sees terminal output for the context. +This feature is useful when working with AI coding assistants that can only see terminal output for context. ## server.warmup From f2a7d2f9852e813380ff786c212719bd5fb2ed4a Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 10 Oct 2025 01:22:34 +0900 Subject: [PATCH 09/17] test: on ci --- playground/runtime-log/__test__/runtime-log.spec.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/playground/runtime-log/__test__/runtime-log.spec.ts b/playground/runtime-log/__test__/runtime-log.spec.ts index c64732bb3d535c..ae5173dd081c0b 100644 --- a/playground/runtime-log/__test__/runtime-log.spec.ts +++ b/playground/runtime-log/__test__/runtime-log.spec.ts @@ -1,9 +1,11 @@ +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(() => serverLogs.at(-1)).toEqual(`\ + await expect.poll(() => stripVTControlCharacters(serverLogs.at(-1))) + .toEqual(`\ [Unhandled error] Error: this is test error > testError src/main.ts:20:8 18| @@ -18,7 +20,8 @@ test.runIf(isServe)('unhandled error', async () => { test.runIf(isServe)('unhandled rejection', async () => { await page.click('#test-unhandledrejection') - await expect.poll(() => serverLogs.at(-1)).toEqual(`\ + await expect.poll(() => stripVTControlCharacters(serverLogs.at(-1))) + .toEqual(`\ [Unhandled error] Error: this is test unhandledrejection > testUnhandledRejection src/main.ts:24:8 22| From 807df92770703353f72f058221cbbdb20592894b Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 10 Oct 2025 01:26:29 +0900 Subject: [PATCH 10/17] chore: cleanup --- .../src/node/plugins/runtimeLog-shared.ts | 2 +- packages/vite/src/node/plugins/runtimeLog.ts | 112 +----------------- 2 files changed, 4 insertions(+), 110 deletions(-) diff --git a/packages/vite/src/node/plugins/runtimeLog-shared.ts b/packages/vite/src/node/plugins/runtimeLog-shared.ts index 97f60b5afa4d84..b586c346707b2b 100644 --- a/packages/vite/src/node/plugins/runtimeLog-shared.ts +++ b/packages/vite/src/node/plugins/runtimeLog-shared.ts @@ -8,7 +8,7 @@ export type RuntimeLogPayload = { } } -export function setupRuntimeLog( +export function setupRuntimeLogHandler( transport: NormalizedModuleRunnerTransport, ): void { function sendError(error: any) { diff --git a/packages/vite/src/node/plugins/runtimeLog.ts b/packages/vite/src/node/plugins/runtimeLog.ts index df4fc6beb00a57..a1952aa9233ffa 100644 --- a/packages/vite/src/node/plugins/runtimeLog.ts +++ b/packages/vite/src/node/plugins/runtimeLog.ts @@ -1,10 +1,10 @@ import path from 'node:path' import fs from 'node:fs' -import { stripVTControlCharacters } from 'node:util' 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?: { /** @default ["client"] */ @@ -88,116 +88,10 @@ function formatError(error: any, environment: DevEnvironment) { .join(' ')}\n` if (stack === nearest) { const code = fs.readFileSync(stack.file, 'utf-8') - // TODO: highlight - output += generateCodeFrame(code, 4, stack) + // TODO: highlight? + output += generateCodeFrame(code, stack) output += '\n' } } return output } - -function generateCodeFrame( - source: string, - indent = 0, - loc: { line: number; column: number } | number, - range = 2, -): string { - const start = - typeof loc === 'object' - ? positionToOffset(source, loc.line, loc.column) - : loc - const end = start - const lines = source.split(lineSplitRE) - const nl = /\r\n/.test(source) ? 2 : 1 - let count = 0 - let res: string[] = [] - - const columns = process.stdout?.columns || 80 - - for (let i = 0; i < lines.length; i++) { - count += lines[i].length + nl - if (count >= start) { - for (let j = i - range; j <= i + range || end > count; j++) { - if (j < 0 || j >= lines.length) { - continue - } - - const lineLength = lines[j].length - const strippedContent = stripVTControlCharacters(lines[j]) - - if (strippedContent.startsWith('//# sourceMappingURL')) { - continue - } - - // too long, maybe it's a minified file, skip for codeframe - if (strippedContent.length > 200) { - return '' - } - - res.push( - lineNo(j + 1) + - truncateString(lines[j].replace(/\t/g, ' '), columns - 5 - indent), - ) - - if (j === i) { - // push underline - const pad = start - (count - lineLength) + (nl - 1) - const length = Math.max( - 1, - end > count ? lineLength - pad : end - start, - ) - res.push(lineNo() + ' '.repeat(pad) + c.red('^'.repeat(length))) - } else if (j > i) { - if (end > count) { - const length = Math.max(1, Math.min(end - count, lineLength)) - res.push(lineNo() + c.red('^'.repeat(length))) - } - count += lineLength + 1 - } - } - break - } - } - - if (indent) { - res = res.map((line) => ' '.repeat(indent) + line) - } - - return res.join('\n') -} - -function lineNo(no: number | string = '') { - return c.gray(`${String(no).padStart(3, ' ')}| `) -} - -const lineSplitRE: RegExp = /\r?\n/ - -function positionToOffset( - source: string, - lineNumber: number, - columnNumber: number, -): number { - const lines = source.split(lineSplitRE) - const nl = /\r\n/.test(source) ? 2 : 1 - let start = 0 - - if (lineNumber > lines.length) { - return source.length - } - - for (let i = 0; i < lineNumber - 1; i++) { - start += lines[i].length + nl - } - - return start + columnNumber -} - -function truncateString(text: string, maxLength: number): string { - const plainText = stripVTControlCharacters(text) - - if (plainText.length <= maxLength) { - return text - } - - return `${plainText.slice(0, maxLength - 1)}…` -} From 5c86a9931e0dda8de1631882c68930d7f682215f Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 10 Oct 2025 01:32:37 +0900 Subject: [PATCH 11/17] tweak --- packages/vite/src/client/client.ts | 4 ++-- packages/vite/src/node/plugins/runtimeLog.ts | 2 +- .../runtimeLog.ts} | 2 +- .../runtime-log/__test__/runtime-log.spec.ts | 24 +++++++++---------- 4 files changed, 16 insertions(+), 16 deletions(-) rename packages/vite/src/{node/plugins/runtimeLog-shared.ts => shared/runtimeLog.ts} (90%) diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index ce54d4882d34a5..f6d497ed152ef6 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -6,7 +6,7 @@ import { normalizeModuleRunnerTransport, } from '../shared/moduleRunnerTransport' import { createHMRHandler } from '../shared/hmrHandler' -import { setupRuntimeLog } from '../node/plugins/runtimeLog-shared' +import { setupRuntimeLogHandler } from '../shared/runtimeLog' import { ErrorOverlay, cspNonce, overlayId } from './overlay' import '@vite/env' @@ -169,7 +169,7 @@ const hmrClient = new HMRClient( }, ) transport.connect!(createHMRHandler(handleMessage)) -setupRuntimeLog(transport) +setupRuntimeLogHandler(transport) async function handleMessage(payload: HotPayload) { switch (payload.type) { diff --git a/packages/vite/src/node/plugins/runtimeLog.ts b/packages/vite/src/node/plugins/runtimeLog.ts index a1952aa9233ffa..7b45decff574be 100644 --- a/packages/vite/src/node/plugins/runtimeLog.ts +++ b/packages/vite/src/node/plugins/runtimeLog.ts @@ -89,7 +89,7 @@ function formatError(error: any, environment: DevEnvironment) { if (stack === nearest) { const code = fs.readFileSync(stack.file, 'utf-8') // TODO: highlight? - output += generateCodeFrame(code, stack) + output += generateCodeFrame(code, stack).replace(/^/gm, ' ') output += '\n' } } diff --git a/packages/vite/src/node/plugins/runtimeLog-shared.ts b/packages/vite/src/shared/runtimeLog.ts similarity index 90% rename from packages/vite/src/node/plugins/runtimeLog-shared.ts rename to packages/vite/src/shared/runtimeLog.ts index b586c346707b2b..9f4c74361b750e 100644 --- a/packages/vite/src/node/plugins/runtimeLog-shared.ts +++ b/packages/vite/src/shared/runtimeLog.ts @@ -1,4 +1,4 @@ -import type { NormalizedModuleRunnerTransport } from '../../shared/moduleRunnerTransport' +import type { NormalizedModuleRunnerTransport } from './moduleRunnerTransport' export type RuntimeLogPayload = { error: { diff --git a/playground/runtime-log/__test__/runtime-log.spec.ts b/playground/runtime-log/__test__/runtime-log.spec.ts index ae5173dd081c0b..5953dcc9a7c627 100644 --- a/playground/runtime-log/__test__/runtime-log.spec.ts +++ b/playground/runtime-log/__test__/runtime-log.spec.ts @@ -8,12 +8,12 @@ test.runIf(isServe)('unhandled error', async () => { .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| + 18 | + 19 | function testError() { + 20 | throw new Error('this is test error') + | ^ + 21 | } + 22 | > HTMLButtonElement. src/main.ts:6:2 `) }) @@ -24,12 +24,12 @@ test.runIf(isServe)('unhandled rejection', async () => { .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| + 22 | + 23 | async function testUnhandledRejection() { + 24 | throw new Error('this is test unhandledrejection') + | ^ + 25 | } + 26 | > HTMLButtonElement. src/main.ts:12:4 `) }) From cd90e3164c751ef2e6af26d5b93a95f2dc74b225 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 10 Oct 2025 01:33:53 +0900 Subject: [PATCH 12/17] cleanup --- packages/vite/src/node/plugins/runtimeLog.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/vite/src/node/plugins/runtimeLog.ts b/packages/vite/src/node/plugins/runtimeLog.ts index 7b45decff574be..088b12d2d61070 100644 --- a/packages/vite/src/node/plugins/runtimeLog.ts +++ b/packages/vite/src/node/plugins/runtimeLog.ts @@ -6,17 +6,14 @@ import type { DevEnvironment, Plugin } from '..' import { normalizePath } from '..' import { generateCodeFrame } from '../utils' -export function runtimeLogPlugin(pluginOpts?: { - /** @default ["client"] */ - environments?: string[] +export function runtimeLogPlugin(pluginOpts: { + environments: string[] }): Plugin { - const environmentNames = pluginOpts?.environments || ['client'] - return { name: 'vite:runtime-log', apply: 'serve', configureServer(server) { - for (const name of environmentNames) { + 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) From f32e11ee454053084a5d04eb8f3d2393239c3675 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 10 Oct 2025 01:44:25 +0900 Subject: [PATCH 13/17] fix: windows --- packages/vite/src/node/plugins/runtimeLog.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/vite/src/node/plugins/runtimeLog.ts b/packages/vite/src/node/plugins/runtimeLog.ts index 088b12d2d61070..93739059db1f01 100644 --- a/packages/vite/src/node/plugins/runtimeLog.ts +++ b/packages/vite/src/node/plugins/runtimeLog.ts @@ -79,7 +79,9 @@ function formatError(error: any, environment: DevEnvironment) { const errorName = error.name || 'Unknown Error' output += c.red(`[Unhandled error] ${c.bold(errorName)}: ${error.message}\n`) for (const stack of stacks) { - const file = path.relative(environment.config.root, stack.file) + const file = normalizePath( + path.relative(environment.config.root, stack.file), + ) output += ` > ${[stack.method, `${file}:${stack.line}:${stack.column}`] .filter(Boolean) .join(' ')}\n` From 991a5d4a74ff6fd2e0ede551a309ec2d66b62f19 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 10 Oct 2025 09:29:21 +0900 Subject: [PATCH 14/17] chore: comment --- packages/vite/src/node/plugins/runtimeLog.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/vite/src/node/plugins/runtimeLog.ts b/packages/vite/src/node/plugins/runtimeLog.ts index 93739059db1f01..81f7d49278f8a9 100644 --- a/packages/vite/src/node/plugins/runtimeLog.ts +++ b/packages/vite/src/node/plugins/runtimeLog.ts @@ -64,6 +64,9 @@ function formatError(error: any, environment: DevEnvironment) { return environment.moduleGraph.getModuleById(id)?.transformResult?.map } }, + // Vitest uses to skip internal files + // https://github.com/vitejs/vitest/blob/4783137cd8d766cf998bdf2d638890eaa51e08d9/packages/utils/src/source-map.ts#L17 + ignoreStackEntries: [], }) // https://github.com/vitest-dev/vitest/blob/4783137cd8d766cf998bdf2d638890eaa51e08d9/packages/vitest/src/node/printError.ts#L64 From 42d364d28da0d283f44fcb82d65a48fb0a607024 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 10 Oct 2025 10:22:34 +0900 Subject: [PATCH 15/17] refactor: tweak types --- packages/vite/src/node/plugins/runtimeLog.ts | 12 ++---------- packages/vite/src/shared/runtimeLog.ts | 15 ++++----------- packages/vite/types/customEvent.d.ts | 9 +++++++++ 3 files changed, 15 insertions(+), 21 deletions(-) diff --git a/packages/vite/src/node/plugins/runtimeLog.ts b/packages/vite/src/node/plugins/runtimeLog.ts index 81f7d49278f8a9..4d3e7bd52199ba 100644 --- a/packages/vite/src/node/plugins/runtimeLog.ts +++ b/packages/vite/src/node/plugins/runtimeLog.ts @@ -15,7 +15,7 @@ export function runtimeLogPlugin(pluginOpts: { configureServer(server) { for (const name of pluginOpts.environments) { const environment = server.environments[name] - environment.hot.on('vite:runtime-log', (payload: RuntimeLogPayload) => { + environment.hot.on('vite:runtime-log', (payload) => { const output = formatError(payload.error, environment) environment.config.logger.error(output, { timestamp: true, @@ -26,14 +26,6 @@ export function runtimeLogPlugin(pluginOpts: { } } -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, { @@ -64,7 +56,7 @@ function formatError(error: any, environment: DevEnvironment) { return environment.moduleGraph.getModuleById(id)?.transformResult?.map } }, - // Vitest uses to skip internal files + // Vitest uses this option to skip internal files // https://github.com/vitejs/vitest/blob/4783137cd8d766cf998bdf2d638890eaa51e08d9/packages/utils/src/source-map.ts#L17 ignoreStackEntries: [], }) diff --git a/packages/vite/src/shared/runtimeLog.ts b/packages/vite/src/shared/runtimeLog.ts index 9f4c74361b750e..48f933381f0804 100644 --- a/packages/vite/src/shared/runtimeLog.ts +++ b/packages/vite/src/shared/runtimeLog.ts @@ -1,13 +1,6 @@ +import type { RuntimeLogPayload } from 'types/customEvent' import type { NormalizedModuleRunnerTransport } from './moduleRunnerTransport' -export type RuntimeLogPayload = { - error: { - name: string - message: string - stack?: string - } -} - export function setupRuntimeLogHandler( transport: NormalizedModuleRunnerTransport, ): void { @@ -18,9 +11,9 @@ export function setupRuntimeLogHandler( event: 'vite:runtime-log', data: { error: { - name: error.name, - message: error.message, - stack: error.stack, + name: error?.name || 'Error', + message: error?.message || String(error), + stack: error?.stack, }, } satisfies RuntimeLogPayload, }) diff --git a/packages/vite/types/customEvent.d.ts b/packages/vite/types/customEvent.d.ts index 96145a6fddadf4..c9aab56e184c4d 100644 --- a/packages/vite/types/customEvent.d.ts +++ b/packages/vite/types/customEvent.d.ts @@ -12,6 +12,7 @@ export interface CustomEventMap { 'vite:beforeFullReload': FullReloadPayload 'vite:error': ErrorPayload 'vite:invalidate': InvalidatePayload + 'vite:runtime-log': RuntimeLogPayload 'vite:ws:connect': WebSocketConnectionPayload 'vite:ws:disconnect': WebSocketConnectionPayload } @@ -33,6 +34,14 @@ export interface InvalidatePayload { firstInvalidatedBy: string } +export interface RuntimeLogPayload { + error: { + name: string + message: string + stack?: string + } +} + /** * provides types for payloads of built-in Vite events */ From f6749558e335f4d2532375c1818174fd395f7d23 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 10 Oct 2025 10:26:15 +0900 Subject: [PATCH 16/17] chore: more types --- packages/vite/src/node/plugins/runtimeLog.ts | 19 ++++++++++++------- packages/vite/types/customEvent.d.ts | 3 ++- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/vite/src/node/plugins/runtimeLog.ts b/packages/vite/src/node/plugins/runtimeLog.ts index 4d3e7bd52199ba..c116cde6ca967a 100644 --- a/packages/vite/src/node/plugins/runtimeLog.ts +++ b/packages/vite/src/node/plugins/runtimeLog.ts @@ -2,6 +2,7 @@ import path from 'node:path' import fs from 'node:fs' import { parseErrorStacktrace } from '@vitest/utils/source-map' import c from 'picocolors' +import type { RuntimeLogPayload } from 'types/customEvent' import type { DevEnvironment, Plugin } from '..' import { normalizePath } from '..' import { generateCodeFrame } from '../utils' @@ -16,17 +17,22 @@ export function runtimeLogPlugin(pluginOpts: { for (const name of pluginOpts.environments) { const environment = server.environments[name] environment.hot.on('vite:runtime-log', (payload) => { - const output = formatError(payload.error, environment) - environment.config.logger.error(output, { - timestamp: true, - }) + if (payload.error) { + const output = formatError(payload.error, environment) + environment.config.logger.error(output, { + timestamp: true, + }) + } }) } }, } } -function formatError(error: any, environment: DevEnvironment) { +function formatError( + error: NonNullable, + environment: DevEnvironment, +) { // https://github.com/vitest-dev/vitest/blob/4783137cd8d766cf998bdf2d638890eaa51e08d9/packages/browser/src/node/projectParent.ts#L58 const stacks = parseErrorStacktrace(error, { getUrlId(id) { @@ -71,8 +77,7 @@ function formatError(error: any, environment: DevEnvironment) { }) let output = '' - const errorName = error.name || 'Unknown Error' - output += c.red(`[Unhandled error] ${c.bold(errorName)}: ${error.message}\n`) + output += c.red(`[Unhandled error] ${c.bold(error.name)}: ${error.message}\n`) for (const stack of stacks) { const file = normalizePath( path.relative(environment.config.root, stack.file), diff --git a/packages/vite/types/customEvent.d.ts b/packages/vite/types/customEvent.d.ts index c9aab56e184c4d..fd7ce7c97506dc 100644 --- a/packages/vite/types/customEvent.d.ts +++ b/packages/vite/types/customEvent.d.ts @@ -35,7 +35,8 @@ export interface InvalidatePayload { } export interface RuntimeLogPayload { - error: { + // make it optional for future extension? + error?: { name: string message: string stack?: string From 0dc47db7fd89b3e792f2e71cf5a8c71b6537f6d7 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 10 Oct 2025 10:37:00 +0900 Subject: [PATCH 17/17] fix: setupRuntimeLogHandler only when enabled --- packages/vite/src/client/client.ts | 6 +++++- packages/vite/src/node/plugins/clientInjections.ts | 7 +++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index f6d497ed152ef6..3d66e7cf84ad65 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -21,6 +21,7 @@ declare const __HMR_BASE__: string declare const __HMR_TIMEOUT__: number declare const __HMR_ENABLE_OVERLAY__: boolean declare const __WS_TOKEN__: string +declare const __SERVER_FORWARD_RUNTIME_LOGS__: boolean console.debug('[vite] connecting...') @@ -169,7 +170,10 @@ const hmrClient = new HMRClient( }, ) transport.connect!(createHMRHandler(handleMessage)) -setupRuntimeLogHandler(transport) + +if (__SERVER_FORWARD_RUNTIME_LOGS__) { + setupRuntimeLogHandler(transport) +} async function handleMessage(payload: HotPayload) { switch (payload.type) { diff --git a/packages/vite/src/node/plugins/clientInjections.ts b/packages/vite/src/node/plugins/clientInjections.ts index 3fa745f7d2f220..575e03c935236b 100644 --- a/packages/vite/src/node/plugins/clientInjections.ts +++ b/packages/vite/src/node/plugins/clientInjections.ts @@ -77,6 +77,9 @@ export function clientInjectionsPlugin(config: ResolvedConfig): Plugin { const hmrEnableOverlayReplacement = escapeReplacement(overlay) const hmrConfigNameReplacement = escapeReplacement(hmrConfigName) const wsTokenReplacement = escapeReplacement(config.webSocketToken) + const serverForwardRuntimeLogsReplacement = escapeReplacement( + config.server.forwardRuntimeLogs, + ) injectConfigValues = (code: string) => { return code @@ -92,6 +95,10 @@ export function clientInjectionsPlugin(config: ResolvedConfig): Plugin { .replace(`__HMR_ENABLE_OVERLAY__`, hmrEnableOverlayReplacement) .replace(`__HMR_CONFIG_NAME__`, hmrConfigNameReplacement) .replace(`__WS_TOKEN__`, wsTokenReplacement) + .replace( + `__SERVER_FORWARD_RUNTIME_LOGS__`, + serverForwardRuntimeLogsReplacement, + ) } }, async transform(code, id) {