From c897df27adf1868d6732be4a4134ecab0c11c737 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 10 Oct 2025 00:15:43 +0900 Subject: [PATCH 01/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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) { From 3dd1d0c6df1457254402c784211acb86727a28dc Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 09:34:43 +0900 Subject: [PATCH 18/60] chore: license.md --- packages/vite/LICENSE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vite/LICENSE.md b/packages/vite/LICENSE.md index 1be96023c64617..513652148ad6fb 100644 --- a/packages/vite/LICENSE.md +++ b/packages/vite/LICENSE.md @@ -134,11 +134,11 @@ Repository: https://github.com/rollup/plugins ## @vitest/utils License: MIT -Repository: git+https://github.com/vitest-dev/vitest.git +Repository: https://github.com/vitest-dev/vitest > MIT License > -> Copyright (c) 2021-Present Vitest Team +> Copyright (c) 2021-Present VoidZero Inc. and Vitest contributors > > Permission is hereby granted, free of charge, to any person obtaining a copy > of this software and associated documentation files (the "Software"), to deal From ba3dad01f97442d43576cd22a7ecd850b15dc139 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 09:39:26 +0900 Subject: [PATCH 19/60] fix: fix types --- packages/vite/src/node/plugins/runtimeLog.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vite/src/node/plugins/runtimeLog.ts b/packages/vite/src/node/plugins/runtimeLog.ts index c116cde6ca967a..d5e322bdd8bca3 100644 --- a/packages/vite/src/node/plugins/runtimeLog.ts +++ b/packages/vite/src/node/plugins/runtimeLog.ts @@ -2,7 +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 { RuntimeLogPayload } from '#types/customEvent' import type { DevEnvironment, Plugin } from '..' import { normalizePath } from '..' import { generateCodeFrame } from '../utils' From 7e91b456b8c77f7d7aa338f9105e073c512ca6e0 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 10:24:20 +0900 Subject: [PATCH 20/60] feat: rename runtime log feature to forward console --- docs/config/server-options.md | 2 +- packages/vite/src/client/client.ts | 8 ++++---- .../vite/src/node/plugins/clientInjections.ts | 9 +++------ .../plugins/{runtimeLog.ts => forwardConsole.ts} | 10 +++++----- packages/vite/src/node/plugins/index.ts | 6 +++--- packages/vite/src/node/server/index.ts | 4 ++-- .../shared/{runtimeLog.ts => forwardConsole.ts} | 8 ++++---- packages/vite/types/customEvent.d.ts | 4 ++-- .../__test__/forward-console.spec.ts} | 0 .../{runtime-log => forward-console}/index.html | 0 .../package.json | 2 +- playground/forward-console/public/favicon.ico | Bin 0 -> 6797 bytes .../{runtime-log => forward-console}/src/main.ts | 0 .../vite.config.ts | 2 +- playground/runtime-log/public/favicon.ico | Bin 4286 -> 0 bytes pnpm-lock.yaml | 2 +- 16 files changed, 27 insertions(+), 30 deletions(-) rename packages/vite/src/node/plugins/{runtimeLog.ts => forwardConsole.ts} (91%) rename packages/vite/src/shared/{runtimeLog.ts => forwardConsole.ts} (80%) rename playground/{runtime-log/__test__/runtime-log.spec.ts => forward-console/__test__/forward-console.spec.ts} (100%) rename playground/{runtime-log => forward-console}/index.html (100%) rename playground/{runtime-log => forward-console}/package.json (86%) create mode 100644 playground/forward-console/public/favicon.ico rename playground/{runtime-log => forward-console}/src/main.ts (100%) rename playground/{runtime-log => forward-console}/vite.config.ts (74%) delete mode 100644 playground/runtime-log/public/favicon.ico diff --git a/docs/config/server-options.md b/docs/config/server-options.md index 8f92ad35a0bee8..5a419baae44058 100644 --- a/docs/config/server-options.md +++ b/docs/config/server-options.md @@ -224,7 +224,7 @@ The error that appears in the Browser when the fallback happens can be ignored. ::: -## server.forwardRuntimeLogs +## server.forwardConsole - **Type:** `boolean` - **Default:** `false` diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index 60cd2b4d830c46..0159b2631c9bd6 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -10,7 +10,7 @@ import { normalizeModuleRunnerTransport, } from '../shared/moduleRunnerTransport' import { createHMRHandler } from '../shared/hmrHandler' -import { setupRuntimeLogHandler } from '../shared/runtimeLog' +import { setupForwardConsoleHandler } from '../shared/forwardConsole' import { ErrorOverlay, cspNonce, overlayId } from './overlay' import '@vite/env' @@ -25,7 +25,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 +declare const __SERVER_FORWARD_CONSOLE__: boolean declare const __BUNDLED_DEV__: boolean console.debug('[vite] connecting...') @@ -198,8 +198,8 @@ const hmrClient = new HMRClient( ) transport.connect!(createHMRHandler(handleMessage)) -if (__SERVER_FORWARD_RUNTIME_LOGS__) { - setupRuntimeLogHandler(transport) +if (__SERVER_FORWARD_CONSOLE__) { + setupForwardConsoleHandler(transport) } async function handleMessage(payload: HotPayload) { diff --git a/packages/vite/src/node/plugins/clientInjections.ts b/packages/vite/src/node/plugins/clientInjections.ts index d644e03a378876..5dec713acfb44c 100644 --- a/packages/vite/src/node/plugins/clientInjections.ts +++ b/packages/vite/src/node/plugins/clientInjections.ts @@ -112,8 +112,8 @@ async function createClientConfigValueReplacer( const hmrEnableOverlayReplacement = escapeReplacement(overlay) const hmrConfigNameReplacement = escapeReplacement(hmrConfigName) const wsTokenReplacement = escapeReplacement(config.webSocketToken) - const serverForwardRuntimeLogsReplacement = escapeReplacement( - config.server.forwardRuntimeLogs ?? false, + const serverForwardConsoleReplacement = escapeReplacement( + config.server.forwardConsole ?? false, ) const bundleDevReplacement = escapeReplacement( config.experimental.bundledDev || false, @@ -133,10 +133,7 @@ async function createClientConfigValueReplacer( .replace(`__HMR_ENABLE_OVERLAY__`, hmrEnableOverlayReplacement) .replace(`__HMR_CONFIG_NAME__`, hmrConfigNameReplacement) .replace(`__WS_TOKEN__`, wsTokenReplacement) - .replace( - `__SERVER_FORWARD_RUNTIME_LOGS__`, - serverForwardRuntimeLogsReplacement, - ) + .replace(`__SERVER_FORWARD_CONSOLE__`, serverForwardConsoleReplacement) .replaceAll(`__BUNDLED_DEV__`, bundleDevReplacement) } diff --git a/packages/vite/src/node/plugins/runtimeLog.ts b/packages/vite/src/node/plugins/forwardConsole.ts similarity index 91% rename from packages/vite/src/node/plugins/runtimeLog.ts rename to packages/vite/src/node/plugins/forwardConsole.ts index d5e322bdd8bca3..ea476ff6dd0952 100644 --- a/packages/vite/src/node/plugins/runtimeLog.ts +++ b/packages/vite/src/node/plugins/forwardConsole.ts @@ -2,21 +2,21 @@ 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 { ForwardConsolePayload } from '#types/customEvent' import type { DevEnvironment, Plugin } from '..' import { normalizePath } from '..' import { generateCodeFrame } from '../utils' -export function runtimeLogPlugin(pluginOpts: { +export function forwardConsolePlugin(pluginOpts: { environments: string[] }): Plugin { return { - name: 'vite:runtime-log', + name: 'vite:forward-console', apply: 'serve', configureServer(server) { for (const name of pluginOpts.environments) { const environment = server.environments[name] - environment.hot.on('vite:runtime-log', (payload) => { + environment.hot.on('vite:forward-console', (payload) => { if (payload.error) { const output = formatError(payload.error, environment) environment.config.logger.error(output, { @@ -30,7 +30,7 @@ export function runtimeLogPlugin(pluginOpts: { } function formatError( - error: NonNullable, + error: NonNullable, environment: DevEnvironment, ) { // https://github.com/vitest-dev/vitest/blob/4783137cd8d766cf998bdf2d638890eaa51e08d9/packages/browser/src/node/projectParent.ts#L58 diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index ad5fc3538f3188..7e7132421b3cd4 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -34,7 +34,7 @@ import { createFilterForTransform, createIdFilter, } from './pluginFilter' -import { runtimeLogPlugin } from './runtimeLog' +import { forwardConsolePlugin } from './forwardConsole' import { oxcPlugin } from './oxc' import { esbuildBannerFooterCompatPlugin } from './esbuildBannerFooterCompatPlugin' @@ -102,8 +102,8 @@ export async function resolvePlugins( wasmHelperPlugin(), webWorkerPlugin(config), assetPlugin(config), - config.server.forwardRuntimeLogs && - runtimeLogPlugin({ environments: ['client'] }), + config.server.forwardConsole && + forwardConsolePlugin({ environments: ['client'] }), ...normalPlugins, diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index d237f76a78e095..68b0c71fe96c0e 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -198,7 +198,7 @@ export interface ServerOptions extends CommonServerOptions { hmr: (environment: DevEnvironment) => Promise, ) => Promise - forwardRuntimeLogs?: boolean + forwardConsole?: boolean } export interface ResolvedServerOptions extends Omit< @@ -1140,7 +1140,7 @@ const _serverConfigDefaults = Object.freeze({ perEnvironmentStartEndDuringDev: false, perEnvironmentWatchChangeDuringDev: false, // hotUpdateEnvironments - forwardRuntimeLogs: false, + forwardConsole: false, } satisfies ServerOptions) export const serverConfigDefaults: Readonly> = _serverConfigDefaults diff --git a/packages/vite/src/shared/runtimeLog.ts b/packages/vite/src/shared/forwardConsole.ts similarity index 80% rename from packages/vite/src/shared/runtimeLog.ts rename to packages/vite/src/shared/forwardConsole.ts index 48f933381f0804..c772e5e8ab3869 100644 --- a/packages/vite/src/shared/runtimeLog.ts +++ b/packages/vite/src/shared/forwardConsole.ts @@ -1,21 +1,21 @@ -import type { RuntimeLogPayload } from 'types/customEvent' +import type { ForwardConsolePayload } from '#types/customEvent' import type { NormalizedModuleRunnerTransport } from './moduleRunnerTransport' -export function setupRuntimeLogHandler( +export function setupForwardConsoleHandler( transport: NormalizedModuleRunnerTransport, ): void { function sendError(error: any) { // TODO: serialize extra properties, recursive cause, etc. transport.send({ type: 'custom', - event: 'vite:runtime-log', + event: 'vite:forward-console', data: { error: { name: error?.name || 'Error', message: error?.message || String(error), stack: error?.stack, }, - } satisfies RuntimeLogPayload, + } satisfies ForwardConsolePayload, }) } diff --git a/packages/vite/types/customEvent.d.ts b/packages/vite/types/customEvent.d.ts index 1aca50abd25509..58ec23b6113d0f 100644 --- a/packages/vite/types/customEvent.d.ts +++ b/packages/vite/types/customEvent.d.ts @@ -13,7 +13,7 @@ export interface CustomEventMap { 'vite:beforeFullReload': FullReloadPayload 'vite:error': ErrorPayload 'vite:invalidate': InvalidatePayload - 'vite:runtime-log': RuntimeLogPayload + 'vite:forward-console': ForwardConsolePayload 'vite:ws:connect': WebSocketConnectionPayload 'vite:ws:disconnect': WebSocketConnectionPayload /** @internal */ @@ -41,7 +41,7 @@ export interface InvalidatePayload { firstInvalidatedBy: string } -export interface RuntimeLogPayload { +export interface ForwardConsolePayload { // make it optional for future extension? error?: { name: string diff --git a/playground/runtime-log/__test__/runtime-log.spec.ts b/playground/forward-console/__test__/forward-console.spec.ts similarity index 100% rename from playground/runtime-log/__test__/runtime-log.spec.ts rename to playground/forward-console/__test__/forward-console.spec.ts diff --git a/playground/runtime-log/index.html b/playground/forward-console/index.html similarity index 100% rename from playground/runtime-log/index.html rename to playground/forward-console/index.html diff --git a/playground/runtime-log/package.json b/playground/forward-console/package.json similarity index 86% rename from playground/runtime-log/package.json rename to playground/forward-console/package.json index 1a1262012bd799..c4568c1fa014fb 100644 --- a/playground/runtime-log/package.json +++ b/playground/forward-console/package.json @@ -1,5 +1,5 @@ { - "name": "@vitejs/test-runtime-log", + "name": "@vitejs/test-forward-console", "private": true, "version": "0.0.0", "type": "module", diff --git a/playground/forward-console/public/favicon.ico b/playground/forward-console/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..25ecc438f22ee380c65cd8a21be4e513ef36a4fb GIT binary patch literal 6797 zcmds6OHUL*5N;&IS3nY@2R*=%lOB{sP-HhT5uZUNAu;k;R1~rrApsweaKIns#h7>? znt1Z)#dz^#;$QHquKH$X=jF^Uy9bx4s_yEps_*Nb?%f3Edhj_odiA3qE_chWgG z0)f+2Au{4W82ev7V2YV28t7Ws;#gOp8IE8*tbgBZ zh$?JxEQskeP{YD{r>y^F7mdieg$Q+o^EwJIhK(5Cm_Njn-{O46cLZ|=)-&e+k-45} z*mX8zJBB-kxzGA(h}!Wn@4l{k3_I6ox9KxMBu(?)qBjMh)xj3{g;|NG zC5~2~+-S$l|7-Vs8Ek8faRjQi3~HbY=aHJMPduf$S#`!AZW#>%UqU7!i#JCDO&=V1 z3Ukxx%@M>_ilfyfKia$IZ?2%u3||%gmHGwfg=yiZT0Q9Eks11w;aMXVVk^wgYBBDF z^*@!pJ2nt_39@j_^GaxrTYfdrg;^>h;w!|@YLOf5dU*`n2$7rQ;hY>6+q)=7znePB zx#g=^1o08?Ke<{Q=Z{$bd)YmFwl#s!6m|)wI@9W?fiAq4ii|jO`B@Ee>utFv0$)Qe zLzZSL&_#IaOvWJyPhpliy*YyTQ#o6Ha;2@B|9A242MJkSmG6i7tLf)4c}{hvMrlPW zj80+Bf{}QgW~n8((r>oktQzRT)i8GxG6`9{QIuztxkfPu(%>mmJrO}JO`L63upZW* zPeiK&It#-N#$ByV~}5m1bZ*Eu2*~XOtWQZX|9o^q;o=*Rprg zi=B%IjqX{pjo%8-MUo>?k7ml@qC?QB>!8TV!_%}}ob+( zBHD{Gw6@=0&BVOr5m^MekXziIwRt~_-%YZ*jdi)3@^4`5=nQ5!5#LU}$&vS2Ga4E- z(5X9_HY)hZg&YsIoDG4mAd`@#+kQtguUT&lZ_O4& z_Ah@gFuM7)K7L^N-WV@FCvEY`Sjy@2Yol3xjXhtn2}1;rA_NY7Y6E8ZJ(Fo08x#Ex zuAJ`&f+oRw_sNNhf5hjZqpgC$FC$oHN;@~b}H8VY=2>sa8k zJ3K2R*(9eT_>TH;?Aj_@@>^azAc5EQ!NFYQ!R;|6D^7HPo|)+UYiGR@ErLI Dg(WYR literal 0 HcmV?d00001 diff --git a/playground/runtime-log/src/main.ts b/playground/forward-console/src/main.ts similarity index 100% rename from playground/runtime-log/src/main.ts rename to playground/forward-console/src/main.ts diff --git a/playground/runtime-log/vite.config.ts b/playground/forward-console/vite.config.ts similarity index 74% rename from playground/runtime-log/vite.config.ts rename to playground/forward-console/vite.config.ts index 380bc046e26b33..1cf2aa0b0c4049 100644 --- a/playground/runtime-log/vite.config.ts +++ b/playground/forward-console/vite.config.ts @@ -2,6 +2,6 @@ import { defineConfig } from 'vite' export default defineConfig({ server: { - forwardRuntimeLogs: true, + forwardConsole: true, }, }) diff --git a/playground/runtime-log/public/favicon.ico b/playground/runtime-log/public/favicon.ico deleted file mode 100644 index df36fcfb72584e00488330b560ebcf34a41c64c2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 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 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fac1c7413ad44d..328e9cf9a624b9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1441,7 +1441,7 @@ importers: playground/resolve/utf8-bom-package: {} - playground/runtime-log: {} + playground/forward-console: {} playground/self-referencing: {} From d3ee05b02f7c8d1457081933b92d71f2049373ed Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 10:46:51 +0900 Subject: [PATCH 21/60] feat: make server.forwardConsole configurable --- docs/config/server-options.md | 21 +++++- packages/vite/src/client/client.ts | 12 +++- .../vite/src/node/plugins/clientInjections.ts | 8 ++- .../vite/src/node/plugins/forwardConsole.ts | 17 +++++ packages/vite/src/node/plugins/index.ts | 3 +- packages/vite/src/node/server/index.ts | 9 ++- packages/vite/src/shared/forwardConsole.ts | 67 ++++++++++++++++++- .../vite/src/shared/forwardConsoleOptions.ts | 43 ++++++++++++ packages/vite/types/customEvent.d.ts | 5 ++ 9 files changed, 175 insertions(+), 10 deletions(-) create mode 100644 packages/vite/src/shared/forwardConsoleOptions.ts diff --git a/docs/config/server-options.md b/docs/config/server-options.md index 5a419baae44058..63566ed68135f9 100644 --- a/docs/config/server-options.md +++ b/docs/config/server-options.md @@ -226,10 +226,16 @@ The error that appears in the Browser when the fallback happens can be ignored. ## server.forwardConsole -- **Type:** `boolean` +- **Type:** `boolean | { unhandledErrors?: boolean, logLevels?: ('error' | 'warn' | 'info' | 'log' | 'debug')[] }` - **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: +Forward browser runtime events to the Vite server console during development. + +- `true` enables forwarding unhandled errors and `console.error` / `console.warn` logs. +- `unhandledErrors` controls forwarding uncaught exceptions and unhandled promise rejections. +- `logLevels` controls which `console.*` calls are forwarded. + +When unhandled errors are forwarded, they are logged in the server terminal with enhanced formatting, for example: ```log 1:18:38 AM [vite] (client) [Unhandled error] Error: this is test error @@ -245,6 +251,17 @@ Forward unhandled runtime errors from the browser to the Vite server console dur This feature is useful when working with AI coding assistants that can only see terminal output for context. +```js +export default defineConfig({ + server: { + forwardConsole: { + unhandledErrors: true, + logLevels: ['warn', 'error'], + }, + }, +}) +``` + ## server.warmup - **Type:** `{ clientFiles?: string[], ssrFiles?: string[] }` diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index 0159b2631c9bd6..e438cc431e9858 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -25,7 +25,12 @@ 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_CONSOLE__: boolean +// TODO: avoid re-typing? +declare const __SERVER_FORWARD_CONSOLE__: { + enabled: boolean + unhandledErrors: boolean + logLevels: Array<'error' | 'warn' | 'info' | 'log' | 'debug'> +} declare const __BUNDLED_DEV__: boolean console.debug('[vite] connecting...') @@ -45,6 +50,7 @@ const base = __BASE__ || '/' const hmrTimeout = __HMR_TIMEOUT__ const wsToken = __WS_TOKEN__ const isBundleMode = __BUNDLED_DEV__ +const forwardConsole = __SERVER_FORWARD_CONSOLE__ const transport = normalizeModuleRunnerTransport( (() => { @@ -198,8 +204,8 @@ const hmrClient = new HMRClient( ) transport.connect!(createHMRHandler(handleMessage)) -if (__SERVER_FORWARD_CONSOLE__) { - setupForwardConsoleHandler(transport) +if (forwardConsole.enabled) { + setupForwardConsoleHandler(transport, forwardConsole) } async function handleMessage(payload: HotPayload) { diff --git a/packages/vite/src/node/plugins/clientInjections.ts b/packages/vite/src/node/plugins/clientInjections.ts index 5dec713acfb44c..65a45e632e50a9 100644 --- a/packages/vite/src/node/plugins/clientInjections.ts +++ b/packages/vite/src/node/plugins/clientInjections.ts @@ -5,6 +5,7 @@ import type { ResolvedConfig } from '../config' import { CLIENT_ENTRY, ENV_ENTRY } from '../constants' import { isObject, normalizePath, resolveHostname } from '../utils' import { cleanUrl } from '../../shared/utils' +import { resolveForwardConsoleOptions } from '../../shared/forwardConsoleOptions' import { perEnvironmentState } from '../environment' import { replaceDefine, serializeDefine } from './define' @@ -112,9 +113,12 @@ async function createClientConfigValueReplacer( const hmrEnableOverlayReplacement = escapeReplacement(overlay) const hmrConfigNameReplacement = escapeReplacement(hmrConfigName) const wsTokenReplacement = escapeReplacement(config.webSocketToken) - const serverForwardConsoleReplacement = escapeReplacement( - config.server.forwardConsole ?? false, + // TODO: should be resolved early + const serverForwardConsole = resolveForwardConsoleOptions( + config.server.forwardConsole, ) + const serverForwardConsoleReplacement = () => + JSON.stringify(serverForwardConsole) const bundleDevReplacement = escapeReplacement( config.experimental.bundledDev || false, ) diff --git a/packages/vite/src/node/plugins/forwardConsole.ts b/packages/vite/src/node/plugins/forwardConsole.ts index ea476ff6dd0952..d2f227b511b708 100644 --- a/packages/vite/src/node/plugins/forwardConsole.ts +++ b/packages/vite/src/node/plugins/forwardConsole.ts @@ -23,6 +23,23 @@ export function forwardConsolePlugin(pluginOpts: { timestamp: true, }) } + if (payload.log) { + const output = + c.dim(`[Console ${payload.log.level}] `) + payload.log.message + if (payload.log.level === 'error') { + environment.config.logger.error(output, { + timestamp: true, + }) + } else if (payload.log.level === 'warn') { + environment.config.logger.warn(output, { + timestamp: true, + }) + } else { + environment.config.logger.info(output, { + timestamp: true, + }) + } + } }) } }, diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index 7e7132421b3cd4..1017b659baf6c6 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -12,6 +12,7 @@ import { type PluginWithRequiredHook, } from '../plugin' import { watchPackageDataPlugin } from '../packages' +import { resolveForwardConsoleOptions } from '../../shared/forwardConsoleOptions' import { oxcResolvePlugin } from './resolve' import { optimizedDepsPlugin } from './optimizedDeps' import { importAnalysisPlugin } from './importAnalysis' @@ -102,7 +103,7 @@ export async function resolvePlugins( wasmHelperPlugin(), webWorkerPlugin(config), assetPlugin(config), - config.server.forwardConsole && + resolveForwardConsoleOptions(config.server.forwardConsole).enabled && forwardConsolePlugin({ environments: ['client'] }), ...normalPlugins, diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 68b0c71fe96c0e..9b8e6c1784cd7c 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -198,7 +198,7 @@ export interface ServerOptions extends CommonServerOptions { hmr: (environment: DevEnvironment) => Promise, ) => Promise - forwardConsole?: boolean + forwardConsole?: boolean | ForwardConsoleOptions } export interface ResolvedServerOptions extends Omit< @@ -259,6 +259,13 @@ export type ServerHook = ( export type HttpServer = http.Server | Http2SecureServer +export type ForwardConsoleLogLevel = 'error' | 'warn' | 'info' | 'log' | 'debug' + +export interface ForwardConsoleOptions { + unhandledErrors?: boolean + logLevels?: ForwardConsoleLogLevel[] +} + export interface ViteDevServer { /** * The resolved vite config object diff --git a/packages/vite/src/shared/forwardConsole.ts b/packages/vite/src/shared/forwardConsole.ts index c772e5e8ab3869..27cdb7c9dad744 100644 --- a/packages/vite/src/shared/forwardConsole.ts +++ b/packages/vite/src/shared/forwardConsole.ts @@ -1,8 +1,13 @@ import type { ForwardConsolePayload } from '#types/customEvent' import type { NormalizedModuleRunnerTransport } from './moduleRunnerTransport' +import type { + ForwardConsoleLogLevel, + ResolvedForwardConsoleOptions, +} from './forwardConsoleOptions' export function setupForwardConsoleHandler( transport: NormalizedModuleRunnerTransport, + options: ResolvedForwardConsoleOptions, ): void { function sendError(error: any) { // TODO: serialize extra properties, recursive cause, etc. @@ -19,7 +24,28 @@ export function setupForwardConsoleHandler( }) } - if (typeof window !== 'undefined') { + function sendLog(level: ForwardConsoleLogLevel, args: unknown[]) { + transport.send({ + type: 'custom', + event: 'vite:forward-console', + data: { + log: { + level, + message: args.map((arg) => stringifyConsoleArg(arg)).join(' '), + }, + } satisfies ForwardConsolePayload, + }) + } + + for (const level of options.logLevels) { + const original = console[level].bind(console) + console[level] = (...args: unknown[]) => { + original(...args) + sendLog(level, args) + } + } + + if (options.unhandledErrors && typeof window !== 'undefined') { window.addEventListener('error', (event) => { sendError(event.error) }) @@ -31,3 +57,42 @@ export function setupForwardConsoleHandler( // TODO: server runtime? // if (typeof process !== 'undefined') {} } + +// TODO: adopt vitest utils for stringify? +function stringifyConsoleArg(value: unknown): string { + if (typeof value === 'string') { + return value + } + if (value instanceof Error) { + return value.stack || `${value.name}: ${value.message}` + } + if (typeof value === 'bigint') { + return `${value}n` + } + + const seen = new WeakSet() + try { + const serialized = JSON.stringify(value, (_, nested) => { + if (typeof nested === 'bigint') { + return `${nested}n` + } + if (nested instanceof Error) { + return { + name: nested.name, + message: nested.message, + stack: nested.stack, + } + } + if (nested && typeof nested === 'object') { + if (seen.has(nested)) { + return '[Circular]' + } + seen.add(nested) + } + return nested + }) + return serialized ?? String(value) + } catch { + return String(value) + } +} diff --git a/packages/vite/src/shared/forwardConsoleOptions.ts b/packages/vite/src/shared/forwardConsoleOptions.ts new file mode 100644 index 00000000000000..9d1ef904e36a12 --- /dev/null +++ b/packages/vite/src/shared/forwardConsoleOptions.ts @@ -0,0 +1,43 @@ +// TODO: should be in config.ts? + +export type ForwardConsoleLogLevel = 'error' | 'warn' | 'info' | 'log' | 'debug' + +export interface ForwardConsoleOptions { + unhandledErrors?: boolean + logLevels?: ForwardConsoleLogLevel[] +} + +export interface ResolvedForwardConsoleOptions { + enabled: boolean + unhandledErrors: boolean + logLevels: ForwardConsoleLogLevel[] +} + +export function resolveForwardConsoleOptions( + value: boolean | ForwardConsoleOptions | undefined, +): ResolvedForwardConsoleOptions { + if (!value) { + return { + enabled: false, + unhandledErrors: false, + logLevels: [], + } + } + + if (value === true) { + return { + enabled: true, + unhandledErrors: true, + logLevels: ['error', 'warn'], + } + } + + const unhandledErrors = value.unhandledErrors ?? true + const logLevels = value.logLevels ?? [] + + return { + enabled: unhandledErrors || logLevels.length > 0, + unhandledErrors, + logLevels, + } +} diff --git a/packages/vite/types/customEvent.d.ts b/packages/vite/types/customEvent.d.ts index 58ec23b6113d0f..4eb4a0b14239fc 100644 --- a/packages/vite/types/customEvent.d.ts +++ b/packages/vite/types/customEvent.d.ts @@ -41,6 +41,7 @@ export interface InvalidatePayload { firstInvalidatedBy: string } +// TODO: adjust payload structure (type-tag union?) export interface ForwardConsolePayload { // make it optional for future extension? error?: { @@ -48,6 +49,10 @@ export interface ForwardConsolePayload { message: string stack?: string } + log?: { + level: 'error' | 'warn' | 'info' | 'log' | 'debug' + message: string + } } /** From f9633bf1ffc9ddcb6532a75a9fffaa4b64ff274b Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 10:53:19 +0900 Subject: [PATCH 22/60] test: add console.error forwarding case --- .../__test__/forward-console.spec.ts | 40 +++++++++++++------ playground/forward-console/index.html | 1 + playground/forward-console/src/main.ts | 8 ++++ 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/playground/forward-console/__test__/forward-console.spec.ts b/playground/forward-console/__test__/forward-console.spec.ts index 5953dcc9a7c627..703ef83a7b7701 100644 --- a/playground/forward-console/__test__/forward-console.spec.ts +++ b/playground/forward-console/__test__/forward-console.spec.ts @@ -7,13 +7,13 @@ test.runIf(isServe)('unhandled error', async () => { 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 | } + > testError src/main.ts:24:8 22 | + 23 | function testError() { + 24 | throw new Error('this is test error') + | ^ + 25 | } + 26 | > HTMLButtonElement. src/main.ts:6:2 `) }) @@ -23,13 +23,29 @@ test.runIf(isServe)('unhandled rejection', async () => { 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 | } + > testUnhandledRejection src/main.ts:28:8 26 | + 27 | async function testUnhandledRejection() { + 28 | throw new Error('this is test unhandledrejection') + | ^ + 29 | } + 30 | > HTMLButtonElement. src/main.ts:12:4 `) }) + +test.runIf(isServe)('console.error', async () => { + const logIndex = serverLogs.length + await page.click('#test-console-error') + await expect + .poll(() => + serverLogs + .slice(logIndex) + .some((log) => + stripVTControlCharacters(log).includes( + '[Console error] this is test console error', + ), + ), + ) + .toBe(true) +}) diff --git a/playground/forward-console/index.html b/playground/forward-console/index.html index dd0f5ecab45f54..8440185cc8217c 100644 --- a/playground/forward-console/index.html +++ b/playground/forward-console/index.html @@ -1,3 +1,4 @@ + diff --git a/playground/forward-console/src/main.ts b/playground/forward-console/src/main.ts index 0f064890c35c06..cd9d342659c394 100644 --- a/playground/forward-console/src/main.ts +++ b/playground/forward-console/src/main.ts @@ -12,6 +12,10 @@ document testUnhandledRejection() }) +document.getElementById('test-console-error').addEventListener('click', () => { + testConsoleError() +}) + export type AnotherPadding = { there: boolean } @@ -23,3 +27,7 @@ function testError() { async function testUnhandledRejection() { throw new Error('this is test unhandledrejection') } + +function testConsoleError() { + console.error('this is test console error') +} From 87cde9fbb28fbe64bdfea2300766d4cb582e8486 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 11:21:34 +0900 Subject: [PATCH 23/60] fix: internal types --- packages/vite/types/customEvent.d.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/vite/types/customEvent.d.ts b/packages/vite/types/customEvent.d.ts index 4eb4a0b14239fc..98fe1aa3e3c416 100644 --- a/packages/vite/types/customEvent.d.ts +++ b/packages/vite/types/customEvent.d.ts @@ -13,10 +13,11 @@ export interface CustomEventMap { 'vite:beforeFullReload': FullReloadPayload 'vite:error': ErrorPayload 'vite:invalidate': InvalidatePayload - 'vite:forward-console': ForwardConsolePayload 'vite:ws:connect': WebSocketConnectionPayload 'vite:ws:disconnect': WebSocketConnectionPayload /** @internal */ + 'vite:forward-console': ForwardConsolePayload + /** @internal */ 'vite:module-loaded': { modules: string[] } // server events From 592545cacee272595cb97e08f8aa6851bbb8942f Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 11:29:36 +0900 Subject: [PATCH 24/60] feat: distinguish unhandled rejection in forwarded console events --- .../vite/src/node/plugins/forwardConsole.ts | 28 ++++++++---- packages/vite/src/shared/forwardConsole.ts | 43 ++++++++++++++++--- packages/vite/types/customEvent.d.ts | 37 ++++++++++------ .../__test__/forward-console.spec.ts | 24 +++++------ playground/forward-console/src/main.ts | 4 +- 5 files changed, 95 insertions(+), 41 deletions(-) diff --git a/packages/vite/src/node/plugins/forwardConsole.ts b/packages/vite/src/node/plugins/forwardConsole.ts index d2f227b511b708..ce46924a650185 100644 --- a/packages/vite/src/node/plugins/forwardConsole.ts +++ b/packages/vite/src/node/plugins/forwardConsole.ts @@ -17,20 +17,22 @@ export function forwardConsolePlugin(pluginOpts: { for (const name of pluginOpts.environments) { const environment = server.environments[name] environment.hot.on('vite:forward-console', (payload) => { - if (payload.error) { - const output = formatError(payload.error, environment) + if ( + payload.type === 'error' || + payload.type === 'unhandled-rejection' + ) { + const output = formatError(payload, environment) environment.config.logger.error(output, { timestamp: true, }) - } - if (payload.log) { + } else { const output = - c.dim(`[Console ${payload.log.level}] `) + payload.log.message - if (payload.log.level === 'error') { + c.dim(`[Console ${payload.data.level}] `) + payload.data.message + if (payload.data.level === 'error') { environment.config.logger.error(output, { timestamp: true, }) - } else if (payload.log.level === 'warn') { + } else if (payload.data.level === 'warn') { environment.config.logger.warn(output, { timestamp: true, }) @@ -47,9 +49,13 @@ export function forwardConsolePlugin(pluginOpts: { } function formatError( - error: NonNullable, + payload: Extract< + ForwardConsolePayload, + { type: 'error' | 'unhandled-rejection' } + >, environment: DevEnvironment, ) { + const error = payload.data // https://github.com/vitest-dev/vitest/blob/4783137cd8d766cf998bdf2d638890eaa51e08d9/packages/browser/src/node/projectParent.ts#L58 const stacks = parseErrorStacktrace(error, { getUrlId(id) { @@ -94,7 +100,11 @@ function formatError( }) let output = '' - output += c.red(`[Unhandled error] ${c.bold(error.name)}: ${error.message}\n`) + const title = + payload.type === 'unhandled-rejection' + ? '[Unhandled rejection]' + : '[Unhandled error]' + output += c.red(`${title} ${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/src/shared/forwardConsole.ts b/packages/vite/src/shared/forwardConsole.ts index 27cdb7c9dad744..4eaf9024094a19 100644 --- a/packages/vite/src/shared/forwardConsole.ts +++ b/packages/vite/src/shared/forwardConsole.ts @@ -9,13 +9,14 @@ export function setupForwardConsoleHandler( transport: NormalizedModuleRunnerTransport, options: ResolvedForwardConsoleOptions, ): void { - function sendError(error: any) { + function sendError(type: 'error' | 'unhandled-rejection', error: any) { // TODO: serialize extra properties, recursive cause, etc. transport.send({ type: 'custom', event: 'vite:forward-console', data: { - error: { + type, + data: { name: error?.name || 'Error', message: error?.message || String(error), stack: error?.stack, @@ -29,7 +30,8 @@ export function setupForwardConsoleHandler( type: 'custom', event: 'vite:forward-console', data: { - log: { + type: 'log', + data: { level, message: args.map((arg) => stringifyConsoleArg(arg)).join(' '), }, @@ -46,11 +48,42 @@ export function setupForwardConsoleHandler( } if (options.unhandledErrors && typeof window !== 'undefined') { + const recentUnhandledRejections = new WeakSet() + const recentUnhandledRejectionMessages = new Set() + + const rememberUnhandledRejection = (reason: unknown) => { + if (reason && typeof reason === 'object') { + recentUnhandledRejections.add(reason) + } else { + const key = String(reason) + recentUnhandledRejectionMessages.add(key) + queueMicrotask(() => { + recentUnhandledRejectionMessages.delete(key) + }) + } + } + window.addEventListener('error', (event) => { - sendError(event.error) + if ( + event.error && + typeof event.error === 'object' && + recentUnhandledRejections.has(event.error) + ) { + return + } + if ( + (!event.error || typeof event.error !== 'object') && + recentUnhandledRejectionMessages.has( + String(event.error ?? event.message), + ) + ) { + return + } + sendError('error', event.error) }) window.addEventListener('unhandledrejection', (event) => { - sendError(event.reason) + rememberUnhandledRejection(event.reason) + sendError('unhandled-rejection', event.reason) }) } diff --git a/packages/vite/types/customEvent.d.ts b/packages/vite/types/customEvent.d.ts index 98fe1aa3e3c416..27a6e635e38d93 100644 --- a/packages/vite/types/customEvent.d.ts +++ b/packages/vite/types/customEvent.d.ts @@ -42,19 +42,30 @@ export interface InvalidatePayload { firstInvalidatedBy: string } -// TODO: adjust payload structure (type-tag union?) -export interface ForwardConsolePayload { - // make it optional for future extension? - error?: { - name: string - message: string - stack?: string - } - log?: { - level: 'error' | 'warn' | 'info' | 'log' | 'debug' - message: string - } -} +export type ForwardConsolePayload = + | { + type: 'error' + data: { + name: string + message: string + stack?: string + } + } + | { + type: 'unhandled-rejection' + data: { + name: string + message: string + stack?: string + } + } + | { + type: 'log' + data: { + level: 'error' | 'warn' | 'info' | 'log' | 'debug' + message: string + } + } /** * provides types for payloads of built-in Vite events diff --git a/playground/forward-console/__test__/forward-console.spec.ts b/playground/forward-console/__test__/forward-console.spec.ts index 703ef83a7b7701..2af83adafaab2a 100644 --- a/playground/forward-console/__test__/forward-console.spec.ts +++ b/playground/forward-console/__test__/forward-console.spec.ts @@ -19,19 +19,19 @@ test.runIf(isServe)('unhandled error', async () => { }) test.runIf(isServe)('unhandled rejection', async () => { + const logIndex = serverLogs.length await page.click('#test-unhandledrejection') - await expect.poll(() => stripVTControlCharacters(serverLogs.at(-1))) - .toEqual(`\ -[Unhandled error] Error: this is test unhandledrejection - > testUnhandledRejection src/main.ts:28:8 - 26 | - 27 | async function testUnhandledRejection() { - 28 | throw new Error('this is test unhandledrejection') - | ^ - 29 | } - 30 | - > HTMLButtonElement. src/main.ts:12:4 -`) + await expect + .poll(() => + serverLogs + .slice(logIndex) + .some((log) => + stripVTControlCharacters(log).includes( + '[Unhandled rejection] Error: this is test unhandledrejection', + ), + ), + ) + .toBe(true) }) test.runIf(isServe)('console.error', async () => { diff --git a/playground/forward-console/src/main.ts b/playground/forward-console/src/main.ts index cd9d342659c394..1ec914b274871b 100644 --- a/playground/forward-console/src/main.ts +++ b/playground/forward-console/src/main.ts @@ -24,8 +24,8 @@ function testError() { throw new Error('this is test error') } -async function testUnhandledRejection() { - throw new Error('this is test unhandledrejection') +function testUnhandledRejection() { + Promise.reject(new Error('this is test unhandledrejection')) } function testConsoleError() { From 2c9cce4a170fc24fbc0279dc0f8c3ab89e0ad286 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 12:00:27 +0900 Subject: [PATCH 25/60] refactor: simplify forwarded unhandled error capture --- packages/vite/src/shared/forwardConsole.ts | 46 +++++----------------- 1 file changed, 9 insertions(+), 37 deletions(-) diff --git a/packages/vite/src/shared/forwardConsole.ts b/packages/vite/src/shared/forwardConsole.ts index 4eaf9024094a19..5dca101f892606 100644 --- a/packages/vite/src/shared/forwardConsole.ts +++ b/packages/vite/src/shared/forwardConsole.ts @@ -10,14 +10,13 @@ export function setupForwardConsoleHandler( options: ResolvedForwardConsoleOptions, ): void { function sendError(type: 'error' | 'unhandled-rejection', error: any) { - // TODO: serialize extra properties, recursive cause, etc. transport.send({ type: 'custom', event: 'vite:forward-console', data: { type, data: { - name: error?.name || 'Error', + name: error?.name || 'Unknown Error', message: error?.message || String(error), stack: error?.stack, }, @@ -48,47 +47,20 @@ export function setupForwardConsoleHandler( } if (options.unhandledErrors && typeof window !== 'undefined') { - const recentUnhandledRejections = new WeakSet() - const recentUnhandledRejectionMessages = new Set() - - const rememberUnhandledRejection = (reason: unknown) => { - if (reason && typeof reason === 'object') { - recentUnhandledRejections.add(reason) - } else { - const key = String(reason) - recentUnhandledRejectionMessages.add(key) - queueMicrotask(() => { - recentUnhandledRejectionMessages.delete(key) - }) - } - } - window.addEventListener('error', (event) => { - if ( - event.error && - typeof event.error === 'object' && - recentUnhandledRejections.has(event.error) - ) { - return - } - if ( - (!event.error || typeof event.error !== 'object') && - recentUnhandledRejectionMessages.has( - String(event.error ?? event.message), - ) - ) { - return - } - sendError('error', event.error) + // `ErrorEvent` doesn't necessarily have `ErrorEvent.error`. + // Use `ErrorEvent.message` as fallback e.g. for ResizeObserver error. + // https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent/error + // https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver#observation_errors + const error = + event.error ?? (event.message ? new Error(event.message) : event) + sendError('error', error) }) + window.addEventListener('unhandledrejection', (event) => { - rememberUnhandledRejection(event.reason) sendError('unhandled-rejection', event.reason) }) } - - // TODO: server runtime? - // if (typeof process !== 'undefined') {} } // TODO: adopt vitest utils for stringify? From 8b81eb54fc4dd2e910be64f221715385561e3338 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 12:01:55 +0900 Subject: [PATCH 26/60] chore: no highlight --- packages/vite/src/node/plugins/forwardConsole.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/vite/src/node/plugins/forwardConsole.ts b/packages/vite/src/node/plugins/forwardConsole.ts index ce46924a650185..480ede0cdaacda 100644 --- a/packages/vite/src/node/plugins/forwardConsole.ts +++ b/packages/vite/src/node/plugins/forwardConsole.ts @@ -114,7 +114,6 @@ function formatError( .join(' ')}\n` if (stack === nearest) { const code = fs.readFileSync(stack.file, 'utf-8') - // TODO: highlight? output += generateCodeFrame(code, stack).replace(/^/gm, ' ') output += '\n' } From 9557e2d22f94bf7ee30cfff893ece87e75f7b389 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 12:04:16 +0900 Subject: [PATCH 27/60] style: use console.level log prefix --- packages/vite/src/node/plugins/forwardConsole.ts | 2 +- playground/forward-console/__test__/forward-console.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vite/src/node/plugins/forwardConsole.ts b/packages/vite/src/node/plugins/forwardConsole.ts index 480ede0cdaacda..73e0c6d110e395 100644 --- a/packages/vite/src/node/plugins/forwardConsole.ts +++ b/packages/vite/src/node/plugins/forwardConsole.ts @@ -27,7 +27,7 @@ export function forwardConsolePlugin(pluginOpts: { }) } else { const output = - c.dim(`[Console ${payload.data.level}] `) + payload.data.message + c.dim(`[console.${payload.data.level}] `) + payload.data.message if (payload.data.level === 'error') { environment.config.logger.error(output, { timestamp: true, diff --git a/playground/forward-console/__test__/forward-console.spec.ts b/playground/forward-console/__test__/forward-console.spec.ts index 2af83adafaab2a..f611e05d8e5ae0 100644 --- a/playground/forward-console/__test__/forward-console.spec.ts +++ b/playground/forward-console/__test__/forward-console.spec.ts @@ -43,7 +43,7 @@ test.runIf(isServe)('console.error', async () => { .slice(logIndex) .some((log) => stripVTControlCharacters(log).includes( - '[Console error] this is test console error', + '[console.error] this is test console error', ), ), ) From 0605f14befcf19c360f747c9105f9ae020154645 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 12:13:37 +0900 Subject: [PATCH 28/60] refactor: resolve forwardConsole options in server config --- packages/vite/src/node/plugins/clientInjections.ts | 7 +------ packages/vite/src/node/plugins/index.ts | 3 +-- packages/vite/src/node/server/index.ts | 8 +++++++- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/vite/src/node/plugins/clientInjections.ts b/packages/vite/src/node/plugins/clientInjections.ts index 65a45e632e50a9..92b641a9120f41 100644 --- a/packages/vite/src/node/plugins/clientInjections.ts +++ b/packages/vite/src/node/plugins/clientInjections.ts @@ -5,7 +5,6 @@ import type { ResolvedConfig } from '../config' import { CLIENT_ENTRY, ENV_ENTRY } from '../constants' import { isObject, normalizePath, resolveHostname } from '../utils' import { cleanUrl } from '../../shared/utils' -import { resolveForwardConsoleOptions } from '../../shared/forwardConsoleOptions' import { perEnvironmentState } from '../environment' import { replaceDefine, serializeDefine } from './define' @@ -113,12 +112,8 @@ async function createClientConfigValueReplacer( const hmrEnableOverlayReplacement = escapeReplacement(overlay) const hmrConfigNameReplacement = escapeReplacement(hmrConfigName) const wsTokenReplacement = escapeReplacement(config.webSocketToken) - // TODO: should be resolved early - const serverForwardConsole = resolveForwardConsoleOptions( - config.server.forwardConsole, - ) const serverForwardConsoleReplacement = () => - JSON.stringify(serverForwardConsole) + JSON.stringify(config.server.forwardConsole) const bundleDevReplacement = escapeReplacement( config.experimental.bundledDev || false, ) diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index 1017b659baf6c6..546fe532ae50c6 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -12,7 +12,6 @@ import { type PluginWithRequiredHook, } from '../plugin' import { watchPackageDataPlugin } from '../packages' -import { resolveForwardConsoleOptions } from '../../shared/forwardConsoleOptions' import { oxcResolvePlugin } from './resolve' import { optimizedDepsPlugin } from './optimizedDeps' import { importAnalysisPlugin } from './importAnalysis' @@ -103,7 +102,7 @@ export async function resolvePlugins( wasmHelperPlugin(), webWorkerPlugin(config), assetPlugin(config), - resolveForwardConsoleOptions(config.server.forwardConsole).enabled && + config.server.forwardConsole.enabled && forwardConsolePlugin({ environments: ['client'] }), ...normalPlugins, diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 9b8e6c1784cd7c..9c23efc245f347 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -16,6 +16,10 @@ import type { ModuleRunner } from 'vite/module-runner' import type { FSWatcher, WatchOptions } from '#dep-types/chokidar' import type { Connect } from '#dep-types/connect' import type { CommonServerOptions } from '../http' +import { + type ResolvedForwardConsoleOptions, + resolveForwardConsoleOptions, +} from '../../shared/forwardConsoleOptions' import { httpServerStart, resolveHttpServer, @@ -213,7 +217,7 @@ export interface ResolvedServerOptions extends Omit< | 'origin' | 'hotUpdateEnvironments' >, - 'fs' | 'middlewareMode' | 'sourcemapIgnoreList' + 'fs' | 'middlewareMode' | 'sourcemapIgnoreList' | 'forwardConsole' > { fs: Required middlewareMode: NonNullable @@ -221,6 +225,7 @@ export interface ResolvedServerOptions extends Omit< ServerOptions['sourcemapIgnoreList'], false | undefined > + forwardConsole: ResolvedForwardConsoleOptions } export interface FileSystemServeOptions { @@ -1177,6 +1182,7 @@ export function resolveServerOptions( _server.sourcemapIgnoreList === false ? () => false : _server.sourcemapIgnoreList, + forwardConsole: resolveForwardConsoleOptions(_server.forwardConsole), } let allowDirs = server.fs.allow From cd80b848fd6d7624acd2687689ac150d401b604a Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 12:16:50 +0900 Subject: [PATCH 29/60] refactor: colocate forwardConsole option resolver in server config --- packages/vite/src/node/server/index.ts | 39 +++++++++++++++-- packages/vite/src/shared/forwardConsole.ts | 15 +++++-- .../vite/src/shared/forwardConsoleOptions.ts | 43 ------------------- 3 files changed, 46 insertions(+), 51 deletions(-) delete mode 100644 packages/vite/src/shared/forwardConsoleOptions.ts diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 9c23efc245f347..a3bc18e201e0f7 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -16,10 +16,6 @@ import type { ModuleRunner } from 'vite/module-runner' import type { FSWatcher, WatchOptions } from '#dep-types/chokidar' import type { Connect } from '#dep-types/connect' import type { CommonServerOptions } from '../http' -import { - type ResolvedForwardConsoleOptions, - resolveForwardConsoleOptions, -} from '../../shared/forwardConsoleOptions' import { httpServerStart, resolveHttpServer, @@ -271,6 +267,41 @@ export interface ForwardConsoleOptions { logLevels?: ForwardConsoleLogLevel[] } +export interface ResolvedForwardConsoleOptions { + enabled: boolean + unhandledErrors: boolean + logLevels: ForwardConsoleLogLevel[] +} + +export function resolveForwardConsoleOptions( + value: boolean | ForwardConsoleOptions | undefined, +): ResolvedForwardConsoleOptions { + if (!value) { + return { + enabled: false, + unhandledErrors: false, + logLevels: [], + } + } + + if (value === true) { + return { + enabled: true, + unhandledErrors: true, + logLevels: ['error', 'warn'], + } + } + + const unhandledErrors = value.unhandledErrors ?? true + const logLevels = value.logLevels ?? [] + + return { + enabled: unhandledErrors || logLevels.length > 0, + unhandledErrors, + logLevels, + } +} + export interface ViteDevServer { /** * The resolved vite config object diff --git a/packages/vite/src/shared/forwardConsole.ts b/packages/vite/src/shared/forwardConsole.ts index 5dca101f892606..cb39da1d5ccdb6 100644 --- a/packages/vite/src/shared/forwardConsole.ts +++ b/packages/vite/src/shared/forwardConsole.ts @@ -1,9 +1,16 @@ import type { ForwardConsolePayload } from '#types/customEvent' import type { NormalizedModuleRunnerTransport } from './moduleRunnerTransport' -import type { - ForwardConsoleLogLevel, - ResolvedForwardConsoleOptions, -} from './forwardConsoleOptions' + +type ForwardConsoleLogLevel = Extract< + ForwardConsolePayload, + { type: 'log' } +>['data']['level'] + +interface ResolvedForwardConsoleOptions { + enabled: boolean + unhandledErrors: boolean + logLevels: ForwardConsoleLogLevel[] +} export function setupForwardConsoleHandler( transport: NormalizedModuleRunnerTransport, diff --git a/packages/vite/src/shared/forwardConsoleOptions.ts b/packages/vite/src/shared/forwardConsoleOptions.ts deleted file mode 100644 index 9d1ef904e36a12..00000000000000 --- a/packages/vite/src/shared/forwardConsoleOptions.ts +++ /dev/null @@ -1,43 +0,0 @@ -// TODO: should be in config.ts? - -export type ForwardConsoleLogLevel = 'error' | 'warn' | 'info' | 'log' | 'debug' - -export interface ForwardConsoleOptions { - unhandledErrors?: boolean - logLevels?: ForwardConsoleLogLevel[] -} - -export interface ResolvedForwardConsoleOptions { - enabled: boolean - unhandledErrors: boolean - logLevels: ForwardConsoleLogLevel[] -} - -export function resolveForwardConsoleOptions( - value: boolean | ForwardConsoleOptions | undefined, -): ResolvedForwardConsoleOptions { - if (!value) { - return { - enabled: false, - unhandledErrors: false, - logLevels: [], - } - } - - if (value === true) { - return { - enabled: true, - unhandledErrors: true, - logLevels: ['error', 'warn'], - } - } - - const unhandledErrors = value.unhandledErrors ?? true - const logLevels = value.logLevels ?? [] - - return { - enabled: unhandledErrors || logLevels.length > 0, - unhandledErrors, - logLevels, - } -} From 3bb54b09e93adb24ed7d4546971441b78b0bbaa4 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 12:17:21 +0900 Subject: [PATCH 30/60] chore: cleanup --- playground/forward-console/public/favicon.ico | Bin 6797 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 playground/forward-console/public/favicon.ico diff --git a/playground/forward-console/public/favicon.ico b/playground/forward-console/public/favicon.ico deleted file mode 100644 index 25ecc438f22ee380c65cd8a21be4e513ef36a4fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6797 zcmds6OHUL*5N;&IS3nY@2R*=%lOB{sP-HhT5uZUNAu;k;R1~rrApsweaKIns#h7>? znt1Z)#dz^#;$QHquKH$X=jF^Uy9bx4s_yEps_*Nb?%f3Edhj_odiA3qE_chWgG z0)f+2Au{4W82ev7V2YV28t7Ws;#gOp8IE8*tbgBZ zh$?JxEQskeP{YD{r>y^F7mdieg$Q+o^EwJIhK(5Cm_Njn-{O46cLZ|=)-&e+k-45} z*mX8zJBB-kxzGA(h}!Wn@4l{k3_I6ox9KxMBu(?)qBjMh)xj3{g;|NG zC5~2~+-S$l|7-Vs8Ek8faRjQi3~HbY=aHJMPduf$S#`!AZW#>%UqU7!i#JCDO&=V1 z3Ukxx%@M>_ilfyfKia$IZ?2%u3||%gmHGwfg=yiZT0Q9Eks11w;aMXVVk^wgYBBDF z^*@!pJ2nt_39@j_^GaxrTYfdrg;^>h;w!|@YLOf5dU*`n2$7rQ;hY>6+q)=7znePB zx#g=^1o08?Ke<{Q=Z{$bd)YmFwl#s!6m|)wI@9W?fiAq4ii|jO`B@Ee>utFv0$)Qe zLzZSL&_#IaOvWJyPhpliy*YyTQ#o6Ha;2@B|9A242MJkSmG6i7tLf)4c}{hvMrlPW zj80+Bf{}QgW~n8((r>oktQzRT)i8GxG6`9{QIuztxkfPu(%>mmJrO}JO`L63upZW* zPeiK&It#-N#$ByV~}5m1bZ*Eu2*~XOtWQZX|9o^q;o=*Rprg zi=B%IjqX{pjo%8-MUo>?k7ml@qC?QB>!8TV!_%}}ob+( zBHD{Gw6@=0&BVOr5m^MekXziIwRt~_-%YZ*jdi)3@^4`5=nQ5!5#LU}$&vS2Ga4E- z(5X9_HY)hZg&YsIoDG4mAd`@#+kQtguUT&lZ_O4& z_Ah@gFuM7)K7L^N-WV@FCvEY`Sjy@2Yol3xjXhtn2}1;rA_NY7Y6E8ZJ(Fo08x#Ex zuAJ`&f+oRw_sNNhf5hjZqpgC$FC$oHN;@~b}H8VY=2>sa8k zJ3K2R*(9eT_>TH;?Aj_@@>^azAc5EQ!NFYQ!R;|6D^7HPo|)+UYiGR@ErLI Dg(WYR From 59578ad962e3610123c2f6a8325bc10a7471b677 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 12:22:08 +0900 Subject: [PATCH 31/60] refactor: simplify client.ts types --- packages/vite/src/client/client.ts | 11 ++--------- packages/vite/src/shared/forwardConsole.ts | 4 ++++ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index e438cc431e9858..841e82e8fbb771 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -25,12 +25,7 @@ declare const __HMR_BASE__: string declare const __HMR_TIMEOUT__: number declare const __HMR_ENABLE_OVERLAY__: boolean declare const __WS_TOKEN__: string -// TODO: avoid re-typing? -declare const __SERVER_FORWARD_CONSOLE__: { - enabled: boolean - unhandledErrors: boolean - logLevels: Array<'error' | 'warn' | 'info' | 'log' | 'debug'> -} +declare const __SERVER_FORWARD_CONSOLE__: any declare const __BUNDLED_DEV__: boolean console.debug('[vite] connecting...') @@ -204,9 +199,7 @@ const hmrClient = new HMRClient( ) transport.connect!(createHMRHandler(handleMessage)) -if (forwardConsole.enabled) { - setupForwardConsoleHandler(transport, forwardConsole) -} +setupForwardConsoleHandler(transport, forwardConsole) async function handleMessage(payload: HotPayload) { switch (payload.type) { diff --git a/packages/vite/src/shared/forwardConsole.ts b/packages/vite/src/shared/forwardConsole.ts index cb39da1d5ccdb6..0df5041d17597d 100644 --- a/packages/vite/src/shared/forwardConsole.ts +++ b/packages/vite/src/shared/forwardConsole.ts @@ -16,6 +16,10 @@ export function setupForwardConsoleHandler( transport: NormalizedModuleRunnerTransport, options: ResolvedForwardConsoleOptions, ): void { + if (!options.enabled) { + return + } + function sendError(type: 'error' | 'unhandled-rejection', error: any) { transport.send({ type: 'custom', From c25c6974d0a039a232aaf4b100840e5db85686eb Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 12:35:26 +0900 Subject: [PATCH 32/60] refactor: share forwardConsole option types --- packages/vite/src/node/server/index.ts | 17 ++++------------- packages/vite/src/shared/forwardConsole.ts | 12 +++++++----- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index a3bc18e201e0f7..8089dae2b40f21 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -16,6 +16,10 @@ import type { ModuleRunner } from 'vite/module-runner' import type { FSWatcher, WatchOptions } from '#dep-types/chokidar' import type { Connect } from '#dep-types/connect' import type { CommonServerOptions } from '../http' +import type { + ForwardConsoleOptions, + ResolvedForwardConsoleOptions, +} from '../../shared/forwardConsole' import { httpServerStart, resolveHttpServer, @@ -260,19 +264,6 @@ export type ServerHook = ( export type HttpServer = http.Server | Http2SecureServer -export type ForwardConsoleLogLevel = 'error' | 'warn' | 'info' | 'log' | 'debug' - -export interface ForwardConsoleOptions { - unhandledErrors?: boolean - logLevels?: ForwardConsoleLogLevel[] -} - -export interface ResolvedForwardConsoleOptions { - enabled: boolean - unhandledErrors: boolean - logLevels: ForwardConsoleLogLevel[] -} - export function resolveForwardConsoleOptions( value: boolean | ForwardConsoleOptions | undefined, ): ResolvedForwardConsoleOptions { diff --git a/packages/vite/src/shared/forwardConsole.ts b/packages/vite/src/shared/forwardConsole.ts index 0df5041d17597d..442043416ab115 100644 --- a/packages/vite/src/shared/forwardConsole.ts +++ b/packages/vite/src/shared/forwardConsole.ts @@ -1,12 +1,14 @@ import type { ForwardConsolePayload } from '#types/customEvent' import type { NormalizedModuleRunnerTransport } from './moduleRunnerTransport' -type ForwardConsoleLogLevel = Extract< - ForwardConsolePayload, - { type: 'log' } ->['data']['level'] +export type ForwardConsoleLogLevel = 'error' | 'warn' | 'info' | 'log' | 'debug' -interface ResolvedForwardConsoleOptions { +export interface ForwardConsoleOptions { + unhandledErrors?: boolean + logLevels?: ForwardConsoleLogLevel[] +} + +export interface ResolvedForwardConsoleOptions { enabled: boolean unhandledErrors: boolean logLevels: ForwardConsoleLogLevel[] From df657032d67d7b88b7244ed4306e18f8cd6768e2 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 12:50:46 +0900 Subject: [PATCH 33/60] test: add dependency stack scenario for forward console --- .../__test__/forward-console.spec.ts | 31 ++++++++++++++----- .../fixtures/throw-dep/index.js | 3 ++ .../fixtures/throw-dep/package.json | 9 ++++++ playground/forward-console/index.html | 1 + playground/forward-console/package.json | 4 ++- playground/forward-console/src/main.ts | 10 ++++++ pnpm-lock.yaml | 15 +++++++-- 7 files changed, 63 insertions(+), 10 deletions(-) create mode 100644 playground/forward-console/fixtures/throw-dep/index.js create mode 100644 playground/forward-console/fixtures/throw-dep/package.json diff --git a/playground/forward-console/__test__/forward-console.spec.ts b/playground/forward-console/__test__/forward-console.spec.ts index f611e05d8e5ae0..2fdb7f18e441b9 100644 --- a/playground/forward-console/__test__/forward-console.spec.ts +++ b/playground/forward-console/__test__/forward-console.spec.ts @@ -7,14 +7,14 @@ test.runIf(isServe)('unhandled error', async () => { await expect.poll(() => stripVTControlCharacters(serverLogs.at(-1))) .toEqual(`\ [Unhandled error] Error: this is test error - > testError src/main.ts:24:8 - 22 | - 23 | function testError() { - 24 | throw new Error('this is test error') + > testError src/main.ts:30:8 + 28 | + 29 | function testError() { + 30 | throw new Error('this is test error') | ^ - 25 | } - 26 | - > HTMLButtonElement. src/main.ts:6:2 + 31 | } + 32 | + > HTMLButtonElement. src/main.ts:8:2 `) }) @@ -49,3 +49,20 @@ test.runIf(isServe)('console.error', async () => { ) .toBe(true) }) + +test.runIf(isServe)('dependency stack uses source map path', async () => { + const logIndex = serverLogs.length + await page.click('#test-dep-error') + await expect + .poll(() => + serverLogs.slice(logIndex).find((log) => { + const cleanLog = stripVTControlCharacters(log) + return ( + cleanLog.includes( + '[Unhandled error] Error: this is test dependency error', + ) && cleanLog.includes('throw-dep/index.js') + ) + }), + ) + .toBeTruthy() +}) diff --git a/playground/forward-console/fixtures/throw-dep/index.js b/playground/forward-console/fixtures/throw-dep/index.js new file mode 100644 index 00000000000000..a4df650578f2a2 --- /dev/null +++ b/playground/forward-console/fixtures/throw-dep/index.js @@ -0,0 +1,3 @@ +export function throwDepError() { + throw new Error('this is test dependency error') +} diff --git a/playground/forward-console/fixtures/throw-dep/package.json b/playground/forward-console/fixtures/throw-dep/package.json new file mode 100644 index 00000000000000..041121d38accc5 --- /dev/null +++ b/playground/forward-console/fixtures/throw-dep/package.json @@ -0,0 +1,9 @@ +{ + "name": "@vitejs/test-forward-console-throw-dep", + "private": true, + "version": "0.0.0", + "type": "module", + "exports": { + ".": "./index.js" + } +} diff --git a/playground/forward-console/index.html b/playground/forward-console/index.html index 8440185cc8217c..c33611b94ea83e 100644 --- a/playground/forward-console/index.html +++ b/playground/forward-console/index.html @@ -1,4 +1,5 @@ + diff --git a/playground/forward-console/package.json b/playground/forward-console/package.json index c4568c1fa014fb..7920da380b4d3c 100644 --- a/playground/forward-console/package.json +++ b/playground/forward-console/package.json @@ -9,6 +9,8 @@ "debug": "node --inspect-brk ../../packages/vite/bin/vite", "preview": "vite preview" }, - "dependencies": {}, + "dependencies": { + "@vitejs/test-forward-console-throw-dep": "file:./fixtures/throw-dep" + }, "devDependencies": {} } diff --git a/playground/forward-console/src/main.ts b/playground/forward-console/src/main.ts index 1ec914b274871b..aa75a390ba356e 100644 --- a/playground/forward-console/src/main.ts +++ b/playground/forward-console/src/main.ts @@ -1,3 +1,5 @@ +import { throwDepError } from '@vitejs/test-forward-console-throw-dep' + export type SomePadding = { here: boolean } @@ -16,6 +18,10 @@ document.getElementById('test-console-error').addEventListener('click', () => { testConsoleError() }) +document.getElementById('test-dep-error').addEventListener('click', () => { + testDepError() +}) + export type AnotherPadding = { there: boolean } @@ -31,3 +37,7 @@ function testUnhandledRejection() { function testConsoleError() { console.error('this is test console error') } + +function testDepError() { + throwDepError() +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 328e9cf9a624b9..32005f71ea300c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -837,6 +837,14 @@ importers: specifier: ^3.5.28 version: 3.5.28(typescript@5.9.3) + playground/forward-console: + dependencies: + '@vitejs/test-forward-console-throw-dep': + specifier: file:./fixtures/throw-dep + version: file:playground/forward-console/fixtures/throw-dep + + playground/forward-console/fixtures/throw-dep: {} + playground/fs-serve: devDependencies: ws: @@ -1441,8 +1449,6 @@ importers: playground/resolve/utf8-bom-package: {} - playground/forward-console: {} - playground/self-referencing: {} playground/ssr: @@ -4204,6 +4210,9 @@ packages: '@vitejs/test-external-using-external-entry@file:playground/ssr-deps/external-using-external-entry': resolution: {directory: playground/ssr-deps/external-using-external-entry, type: directory} + '@vitejs/test-forward-console-throw-dep@file:playground/forward-console/fixtures/throw-dep': + resolution: {directory: playground/forward-console/fixtures/throw-dep, type: directory} + '@vitejs/test-forwarded-export@file:playground/ssr-deps/forwarded-export': resolution: {directory: playground/ssr-deps/forwarded-export, type: directory} @@ -10192,6 +10201,8 @@ snapshots: dependencies: external-entry: '@vitejs/test-external-entry@file:playground/ssr-deps/external-entry' + '@vitejs/test-forward-console-throw-dep@file:playground/forward-console/fixtures/throw-dep': {} + '@vitejs/test-forwarded-export@file:playground/ssr-deps/forwarded-export': dependencies: object-assigned-exports: '@vitejs/test-object-assigned-exports@file:playground/ssr-deps/object-assigned-exports' From 8e7d09db0d2ef694e179982fa7fe7a154b710eb6 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 12:58:04 +0900 Subject: [PATCH 34/60] test: simplify --- .../__test__/forward-console.spec.ts | 66 ++++++++----------- 1 file changed, 29 insertions(+), 37 deletions(-) diff --git a/playground/forward-console/__test__/forward-console.spec.ts b/playground/forward-console/__test__/forward-console.spec.ts index 2fdb7f18e441b9..bf9ede534669c9 100644 --- a/playground/forward-console/__test__/forward-console.spec.ts +++ b/playground/forward-console/__test__/forward-console.spec.ts @@ -2,18 +2,24 @@ import { stripVTControlCharacters } from 'node:util' import { expect, test } from 'vitest' import { isServe, page, serverLogs } from '~utils' +function normalizeLogs(logs: string[]) { + return logs + .map((log) => stripVTControlCharacters(log)) + .join('\n') + .replaceAll(/ +\n/g, '\n') // strip trailing spaces +} + test.runIf(isServe)('unhandled error', async () => { await page.click('#test-error') - await expect.poll(() => stripVTControlCharacters(serverLogs.at(-1))) - .toEqual(`\ + await expect.poll(() => normalizeLogs(serverLogs)).toContain(`\ [Unhandled error] Error: this is test error > testError src/main.ts:30:8 - 28 | + 28 | 29 | function testError() { 30 | throw new Error('this is test error') | ^ 31 | } - 32 | + 32 | > HTMLButtonElement. src/main.ts:8:2 `) }) @@ -21,48 +27,34 @@ test.runIf(isServe)('unhandled error', async () => { test.runIf(isServe)('unhandled rejection', async () => { const logIndex = serverLogs.length await page.click('#test-unhandledrejection') - await expect - .poll(() => - serverLogs - .slice(logIndex) - .some((log) => - stripVTControlCharacters(log).includes( - '[Unhandled rejection] Error: this is test unhandledrejection', - ), - ), - ) - .toBe(true) + await expect.poll(() => normalizeLogs(serverLogs.slice(logIndex))) + .toContain(`\ +[Unhandled rejection] Error: this is test unhandledrejection + > testUnhandledRejection src/main.ts:34:17 + 32 | + 33 | function testUnhandledRejection() { + 34 | Promise.reject(new Error('this is test unhandledrejection')) + | ^ + 35 | } + 36 | + > HTMLButtonElement. src/main.ts:14:4 +`) }) test.runIf(isServe)('console.error', async () => { const logIndex = serverLogs.length await page.click('#test-console-error') await expect - .poll(() => - serverLogs - .slice(logIndex) - .some((log) => - stripVTControlCharacters(log).includes( - '[console.error] this is test console error', - ), - ), - ) - .toBe(true) + .poll(() => normalizeLogs(serverLogs.slice(logIndex))) + .toContain(`[console.error] this is test console error`) }) test.runIf(isServe)('dependency stack uses source map path', async () => { const logIndex = serverLogs.length await page.click('#test-dep-error') - await expect - .poll(() => - serverLogs.slice(logIndex).find((log) => { - const cleanLog = stripVTControlCharacters(log) - return ( - cleanLog.includes( - '[Unhandled error] Error: this is test dependency error', - ) && cleanLog.includes('throw-dep/index.js') - ) - }), - ) - .toBeTruthy() + // TODO: test stack + await expect.poll(() => normalizeLogs(serverLogs.slice(logIndex))) + .toContain(`\ +[Unhandled error] Error: this is test dependency error +`) }) From aed804ede0f3496157d0cedccbd64d33baac3082 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 13:06:40 +0900 Subject: [PATCH 35/60] test: test error from dependency --- .../vite/src/node/plugins/forwardConsole.ts | 41 +++++++++++++++++-- .../__test__/forward-console.spec.ts | 12 +++++- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/packages/vite/src/node/plugins/forwardConsole.ts b/packages/vite/src/node/plugins/forwardConsole.ts index 73e0c6d110e395..65be08de59946a 100644 --- a/packages/vite/src/node/plugins/forwardConsole.ts +++ b/packages/vite/src/node/plugins/forwardConsole.ts @@ -10,6 +10,8 @@ import { generateCodeFrame } from '../utils' export function forwardConsolePlugin(pluginOpts: { environments: string[] }): Plugin { + const sourceMapCache = new Map() + return { name: 'vite:forward-console', apply: 'serve', @@ -21,7 +23,7 @@ export function forwardConsolePlugin(pluginOpts: { payload.type === 'error' || payload.type === 'unhandled-rejection' ) { - const output = formatError(payload, environment) + const output = formatError(payload, environment, sourceMapCache) environment.config.logger.error(output, { timestamp: true, }) @@ -54,6 +56,7 @@ function formatError( { type: 'error' | 'unhandled-rejection' } >, environment: DevEnvironment, + sourceMapCache: Map, ) { const error = payload.data // https://github.com/vitest-dev/vitest/blob/4783137cd8d766cf998bdf2d638890eaa51e08d9/packages/browser/src/node/projectParent.ts#L58 @@ -80,10 +83,26 @@ function formatError( return id }, getSourceMap(id) { - // stack is already rewritten on server - if (environment.name === 'client') { - return environment.moduleGraph.getModuleById(id)?.transformResult?.map + if (sourceMapCache.has(id)) { + return sourceMapCache.get(id) + } + + const result = environment.moduleGraph.getModuleById(id)?.transformResult + // this can happen for bundled dependencies in node_modules/.vite + if (result && !result.map) { + const sourceMapUrl = retrieveSourceMapURL(result.code) + if (!sourceMapUrl) { + return null + } + const filePath = id.split('?')[0] + const sourceMapPath = path.resolve(path.dirname(filePath), sourceMapUrl) + const map = JSON.parse(fs.readFileSync(sourceMapPath, 'utf-8')) + sourceMapCache.set(id, map) + return map } + + sourceMapCache.set(id, result?.map) + return result?.map }, // Vitest uses this option to skip internal files // https://github.com/vitejs/vitest/blob/4783137cd8d766cf998bdf2d638890eaa51e08d9/packages/utils/src/source-map.ts#L17 @@ -120,3 +139,17 @@ function formatError( } return output } + +function retrieveSourceMapURL(source: string): string | null { + const re = + /\/\/[@#]\s*sourceMappingURL=([^\s'"]+)\s*$|\/\*[@#]\s*sourceMappingURL=[^\s*'"]+\s*\*\/\s*$/gm + let lastMatch, match + + while ((match = re.exec(source))) { + lastMatch = match + } + if (!lastMatch) { + return null + } + return lastMatch[1] +} diff --git a/playground/forward-console/__test__/forward-console.spec.ts b/playground/forward-console/__test__/forward-console.spec.ts index bf9ede534669c9..ad34583f9e2554 100644 --- a/playground/forward-console/__test__/forward-console.spec.ts +++ b/playground/forward-console/__test__/forward-console.spec.ts @@ -7,6 +7,7 @@ function normalizeLogs(logs: string[]) { .map((log) => stripVTControlCharacters(log)) .join('\n') .replaceAll(/ +\n/g, '\n') // strip trailing spaces + .replaceAll(/\?v=[a-z\d]+/g, '') } test.runIf(isServe)('unhandled error', async () => { @@ -52,9 +53,18 @@ test.runIf(isServe)('console.error', async () => { test.runIf(isServe)('dependency stack uses source map path', async () => { const logIndex = serverLogs.length await page.click('#test-dep-error') - // TODO: test stack + // TODO: not working yet await expect.poll(() => normalizeLogs(serverLogs.slice(logIndex))) .toContain(`\ [Unhandled error] Error: this is test dependency error + > throwDepError node_modules/.vite/deps/@vitejs_test-forward-console-throw-dep.js:2:8 + > testDepError src/main.ts:42:2 + 40 | + 41 | function testDepError() { + 42 | throwDepError() + | ^ + 43 | } + 44 | + > HTMLButtonElement. src/main.ts:22:2 `) }) From feb3f19d63eac89fee43c1bd4d2914a1ecb33cab Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 13:34:09 +0900 Subject: [PATCH 36/60] fix: apply external dependency source maps in forwarded stacks --- packages/vite/package.json | 2 +- .../vite/src/node/plugins/forwardConsole.ts | 2 +- .../__test__/forward-console.spec.ts | 3 +-- pnpm-lock.yaml | 19 +++++++++++++++++-- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/packages/vite/package.json b/packages/vite/package.json index 2f0e09e4e2bab5..410f0af717fd37 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -94,7 +94,7 @@ "@rollup/pluginutils": "^5.3.0", "@types/escape-html": "^1.0.4", "@types/pnpapi": "^0.0.5", - "@vitest/utils": "^4.0.18", + "@vitest/utils": "4.1.0-beta.4", "@vitejs/devtools": "^0.0.0-alpha.31", "artichokie": "^0.4.2", "baseline-browser-mapping": "^2.9.19", diff --git a/packages/vite/src/node/plugins/forwardConsole.ts b/packages/vite/src/node/plugins/forwardConsole.ts index 65be08de59946a..663a470fcf856a 100644 --- a/packages/vite/src/node/plugins/forwardConsole.ts +++ b/packages/vite/src/node/plugins/forwardConsole.ts @@ -144,7 +144,7 @@ function retrieveSourceMapURL(source: string): string | null { const re = /\/\/[@#]\s*sourceMappingURL=([^\s'"]+)\s*$|\/\*[@#]\s*sourceMappingURL=[^\s*'"]+\s*\*\/\s*$/gm let lastMatch, match - + while ((match = re.exec(source))) { lastMatch = match } diff --git a/playground/forward-console/__test__/forward-console.spec.ts b/playground/forward-console/__test__/forward-console.spec.ts index ad34583f9e2554..732fbad13c1106 100644 --- a/playground/forward-console/__test__/forward-console.spec.ts +++ b/playground/forward-console/__test__/forward-console.spec.ts @@ -53,11 +53,10 @@ test.runIf(isServe)('console.error', async () => { test.runIf(isServe)('dependency stack uses source map path', async () => { const logIndex = serverLogs.length await page.click('#test-dep-error') - // TODO: not working yet await expect.poll(() => normalizeLogs(serverLogs.slice(logIndex))) .toContain(`\ [Unhandled error] Error: this is test dependency error - > throwDepError node_modules/.vite/deps/@vitejs_test-forward-console-throw-dep.js:2:8 + > throwDepError ../../node_modules/.pnpm/@vitejs+test-forward-console-throw-dep@file+playground+forward-console+fixtures+throw-dep/node_modules/@vitejs/test-forward-console-throw-dep/index.js:2:8 > testDepError src/main.ts:42:2 40 | 41 | function testDepError() { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32005f71ea300c..e46e986ab82283 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -287,8 +287,8 @@ importers: specifier: ^0.0.0-alpha.31 version: 0.0.0-alpha.31(typescript@5.9.3)(vite@packages+vite)(vue@3.5.28(typescript@5.9.3)) '@vitest/utils': - specifier: ^4.0.18 - version: 4.0.18 + specifier: 4.1.0-beta.4 + version: 4.1.0-beta.4 artichokie: specifier: ^0.4.2 version: 0.4.2 @@ -4341,6 +4341,9 @@ packages: '@vitest/pretty-format@4.0.18': resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} + '@vitest/pretty-format@4.1.0-beta.4': + resolution: {integrity: sha512-rAJOtUSRzgobQtuW98WV3bSkomdILArhgSc4JQ5G6Et0eaD6DTeMpr+k7B//F/xYG7oVeuabOTx3EWu06ILCgA==} + '@vitest/runner@4.0.18': resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} @@ -4353,6 +4356,9 @@ packages: '@vitest/utils@4.0.18': resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + '@vitest/utils@4.1.0-beta.4': + resolution: {integrity: sha512-c6oj0FpdLwmOisNpeVwhXqwe9Harehj+n9Pfz4/Iv45dSR32VHDzK2uosqT2ocCZhgzXwX4xpL8thl2Wr/wyrw==} + '@voidzero-dev/vitepress-theme@4.5.0': resolution: {integrity: sha512-jY6VprjFeIjBAPWIh0FJUSwU3GwKrHmf8u91HkLxHvzilXbyuu9sJCfF7FnjG2hYH07nF86xnnm+KGZxvFxthg==} peerDependencies: @@ -10312,6 +10318,10 @@ snapshots: dependencies: tinyrainbow: 3.0.3 + '@vitest/pretty-format@4.1.0-beta.4': + dependencies: + tinyrainbow: 3.0.3 + '@vitest/runner@4.0.18': dependencies: '@vitest/utils': 4.0.18 @@ -10330,6 +10340,11 @@ snapshots: '@vitest/pretty-format': 4.0.18 tinyrainbow: 3.0.3 + '@vitest/utils@4.1.0-beta.4': + dependencies: + '@vitest/pretty-format': 4.1.0-beta.4 + tinyrainbow: 3.0.3 + '@voidzero-dev/vitepress-theme@4.5.0(focus-trap@7.8.0)(vite@packages+vite)(vitepress@2.0.0-alpha.16(oxc-minify@0.114.0)(postcss@8.5.6)(typescript@5.9.3))(vue@3.5.28(typescript@5.9.3))': dependencies: '@docsearch/css': 4.5.4 From e0f980956f8eec418293b580a9f5d0695b973b36 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 13:49:15 +0900 Subject: [PATCH 37/60] refactor: reuse sourcemap extraction for forwarded stacks --- .../vite/src/node/plugins/forwardConsole.ts | 31 ++++++------------- packages/vite/src/node/server/sourcemap.ts | 11 ++++--- .../vite/src/node/server/transformRequest.ts | 2 +- 3 files changed, 16 insertions(+), 28 deletions(-) diff --git a/packages/vite/src/node/plugins/forwardConsole.ts b/packages/vite/src/node/plugins/forwardConsole.ts index 663a470fcf856a..ca2ea252ed88fa 100644 --- a/packages/vite/src/node/plugins/forwardConsole.ts +++ b/packages/vite/src/node/plugins/forwardConsole.ts @@ -5,6 +5,7 @@ import c from 'picocolors' import type { ForwardConsolePayload } from '#types/customEvent' import type { DevEnvironment, Plugin } from '..' import { normalizePath } from '..' +import { extractSourcemapFromFile } from '../server/sourcemap' import { generateCodeFrame } from '../utils' export function forwardConsolePlugin(pluginOpts: { @@ -88,17 +89,17 @@ function formatError( } const result = environment.moduleGraph.getModuleById(id)?.transformResult - // this can happen for bundled dependencies in node_modules/.vite + // handle non-inline source map such as pre-bundled deps in node_modules/.vite if (result && !result.map) { - const sourceMapUrl = retrieveSourceMapURL(result.code) - if (!sourceMapUrl) { + try { + const filePath = id.split('?')[0] + const extracted = extractSourcemapFromFile(result.code, filePath) + sourceMapCache.set(id, extracted?.map) + return extracted?.map + } catch { + sourceMapCache.set(id, null) return null } - const filePath = id.split('?')[0] - const sourceMapPath = path.resolve(path.dirname(filePath), sourceMapUrl) - const map = JSON.parse(fs.readFileSync(sourceMapPath, 'utf-8')) - sourceMapCache.set(id, map) - return map } sourceMapCache.set(id, result?.map) @@ -139,17 +140,3 @@ function formatError( } return output } - -function retrieveSourceMapURL(source: string): string | null { - const re = - /\/\/[@#]\s*sourceMappingURL=([^\s'"]+)\s*$|\/\*[@#]\s*sourceMappingURL=[^\s*'"]+\s*\*\/\s*$/gm - let lastMatch, match - - while ((match = re.exec(source))) { - lastMatch = match - } - if (!lastMatch) { - return null - } - return lastMatch[1] -} diff --git a/packages/vite/src/node/server/sourcemap.ts b/packages/vite/src/node/server/sourcemap.ts index 4473b533d40a9c..9f0caac861286f 100644 --- a/packages/vite/src/node/server/sourcemap.ts +++ b/packages/vite/src/node/server/sourcemap.ts @@ -1,4 +1,5 @@ import path from 'node:path' +import fs from 'node:fs' import fsp from 'node:fs/promises' import convertSourceMap from 'convert-source-map' import type { ExistingRawSourceMap, SourceMap } from 'rolldown' @@ -149,16 +150,16 @@ export function applySourcemapIgnoreList( } } -export async function extractSourcemapFromFile( +export function extractSourcemapFromFile( code: string, filePath: string, -): Promise<{ code: string; map: SourceMap } | undefined> { +): { code: string; map: SourceMap } | undefined { const map = ( convertSourceMap.fromSource(code) || - (await convertSourceMap.fromMapFileSource( + convertSourceMap.fromMapFileSource( code, createConvertSourceMapReadMap(filePath), - )) + ) )?.toObject() if (map) { @@ -171,7 +172,7 @@ export async function extractSourcemapFromFile( function createConvertSourceMapReadMap(originalFileName: string) { return (filename: string) => { - return fsp.readFile( + return fs.readFileSync( path.resolve(path.dirname(originalFileName), filename), 'utf-8', ) diff --git a/packages/vite/src/node/server/transformRequest.ts b/packages/vite/src/node/server/transformRequest.ts index 53a04a506e7bdb..397b63fd6e049c 100644 --- a/packages/vite/src/node/server/transformRequest.ts +++ b/packages/vite/src/node/server/transformRequest.ts @@ -293,7 +293,7 @@ async function loadAndTransform( } if (code) { try { - const extracted = await extractSourcemapFromFile(code, file) + const extracted = extractSourcemapFromFile(code, file) if (extracted) { code = extracted.code map = extracted.map From e6e6d6d3fad17f4f0921196372b4e416606a1152 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 13:51:49 +0900 Subject: [PATCH 38/60] chore: comment --- packages/vite/src/node/plugins/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index 546fe532ae50c6..f65bda5b7da93e 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -102,6 +102,7 @@ export async function resolvePlugins( wasmHelperPlugin(), webWorkerPlugin(config), assetPlugin(config), + // for now client only config.server.forwardConsole.enabled && forwardConsolePlugin({ environments: ['client'] }), From 016bb9e3841f5157520fd9ae0b79e06fa2c60960 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 14:02:51 +0900 Subject: [PATCH 39/60] feat: auto-enable forwardConsole for AI agents --- docs/config/server-options.md | 2 +- packages/vite/LICENSE.md | 211 ++++++++++++++++++++++++- packages/vite/package.json | 1 + packages/vite/src/node/config.ts | 2 +- packages/vite/src/node/server/index.ts | 18 ++- pnpm-lock.yaml | 3 + 6 files changed, 227 insertions(+), 10 deletions(-) diff --git a/docs/config/server-options.md b/docs/config/server-options.md index 63566ed68135f9..450e23e0e9243b 100644 --- a/docs/config/server-options.md +++ b/docs/config/server-options.md @@ -227,7 +227,7 @@ The error that appears in the Browser when the fallback happens can be ignored. ## server.forwardConsole - **Type:** `boolean | { unhandledErrors?: boolean, logLevels?: ('error' | 'warn' | 'info' | 'log' | 'debug')[] }` -- **Default:** `false` +- **Default:** auto (`true` when an AI coding agent is detected based on [`@vercel/detect-agent`](https://www.npmjs.com/package/@vercel/detect-agent), otherwise `false`) Forward browser runtime events to the Vite server console during development. diff --git a/packages/vite/LICENSE.md b/packages/vite/LICENSE.md index 513652148ad6fb..41281bb09aed2d 100644 --- a/packages/vite/LICENSE.md +++ b/packages/vite/LICENSE.md @@ -25,7 +25,7 @@ SOFTWARE. # Licenses of bundled dependencies The published Vite artifact additionally contains code with the following licenses: -BSD-2-Clause, CC0-1.0, ISC, MIT +Apache-2.0, BSD-2-Clause, CC0-1.0, ISC, MIT # Bundled dependencies: ## @jridgewell/gen-mapping, @jridgewell/remapping, @jridgewell/sourcemap-codec, @jridgewell/trace-mapping @@ -132,6 +132,215 @@ Repository: https://github.com/rollup/plugins --------------------------------------- +## @vercel/detect-agent +License: Apache-2.0 +By: Vercel +Repository: https://github.com/vercel/vercel + +> Apache License +> Version 2.0, January 2004 +> http://www.apache.org/licenses/ +> +> TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +> +> 1. Definitions. +> +> "License" shall mean the terms and conditions for use, reproduction, +> and distribution as defined by Sections 1 through 9 of this document. +> +> "Licensor" shall mean the copyright owner or entity authorized by +> the copyright owner that is granting the License. +> +> "Legal Entity" shall mean the union of the acting entity and all +> other entities that control, are controlled by, or are under common +> control with that entity. For the purposes of this definition, +> "control" means (i) the power, direct or indirect, to cause the +> direction or management of such entity, whether by contract or +> otherwise, or (ii) ownership of fifty percent (50%) or more of the +> outstanding shares, or (iii) beneficial ownership of such entity. +> +> "You" (or "Your") shall mean an individual or Legal Entity +> exercising permissions granted by this License. +> +> "Source" form shall mean the preferred form for making modifications, +> including but not limited to software source code, documentation +> source, and configuration files. +> +> "Object" form shall mean any form resulting from mechanical +> transformation or translation of a Source form, including but +> not limited to compiled object code, generated documentation, +> and conversions to other media types. +> +> "Work" shall mean the work of authorship, whether in Source or +> Object form, made available under the License, as indicated by a +> copyright notice that is included in or attached to the work +> (an example is provided in the Appendix below). +> +> "Derivative Works" shall mean any work, whether in Source or Object +> form, that is based on (or derived from) the Work and for which the +> editorial revisions, annotations, elaborations, or other modifications +> represent, as a whole, an original work of authorship. For the purposes +> of this License, Derivative Works shall not include works that remain +> separable from, or merely link (or bind by name) to the interfaces of, +> the Work and Derivative Works thereof. +> +> "Contribution" shall mean any work of authorship, including +> the original version of the Work and any modifications or additions +> to that Work or Derivative Works thereof, that is intentionally +> submitted to Licensor for inclusion in the Work by the copyright owner +> or by an individual or Legal Entity authorized to submit on behalf of +> the copyright owner. For the purposes of this definition, "submitted" +> means any form of electronic, verbal, or written communication sent +> to the Licensor or its representatives, including but not limited to +> communication on electronic mailing lists, source code control systems, +> and issue tracking systems that are managed by, or on behalf of, the +> Licensor for the purpose of discussing and improving the Work, but +> excluding communication that is conspicuously marked or otherwise +> designated in writing by the copyright owner as "Not a Contribution." +> +> "Contributor" shall mean Licensor and any individual or Legal Entity +> on behalf of whom a Contribution has been received by Licensor and +> subsequently incorporated within the Work. +> +> 2. Grant of Copyright License. Subject to the terms and conditions of +> this License, each Contributor hereby grants to You a perpetual, +> worldwide, non-exclusive, no-charge, royalty-free, irrevocable +> copyright license to reproduce, prepare Derivative Works of, +> publicly display, publicly perform, sublicense, and distribute the +> Work and such Derivative Works in Source or Object form. +> +> 3. Grant of Patent License. Subject to the terms and conditions of +> this License, each Contributor hereby grants to You a perpetual, +> worldwide, non-exclusive, no-charge, royalty-free, irrevocable +> (except as stated in this section) patent license to make, have made, +> use, offer to sell, sell, import, and otherwise transfer the Work, +> where such license applies only to those patent claims licensable +> by such Contributor that are necessarily infringed by their +> Contribution(s) alone or by combination of their Contribution(s) +> with the Work to which such Contribution(s) was submitted. If You +> institute patent litigation against any entity (including a +> cross-claim or counterclaim in a lawsuit) alleging that the Work +> or a Contribution incorporated within the Work constitutes direct +> or contributory patent infringement, then any patent licenses +> granted to You under this License for that Work shall terminate +> as of the date such litigation is filed. +> +> 4. Redistribution. You may reproduce and distribute copies of the +> Work or Derivative Works thereof in any medium, with or without +> modifications, and in Source or Object form, provided that You +> meet the following conditions: +> +> (a) You must give any other recipients of the Work or +> Derivative Works a copy of this License; and +> +> (b) You must cause any modified files to carry prominent notices +> stating that You changed the files; and +> +> (c) You must retain, in the Source form of any Derivative Works +> that You distribute, all copyright, patent, trademark, and +> attribution notices from the Source form of the Work, +> excluding those notices that do not pertain to any part of +> the Derivative Works; and +> +> (d) If the Work includes a "NOTICE" text file as part of its +> distribution, then any Derivative Works that You distribute must +> include a readable copy of the attribution notices contained +> within such NOTICE file, excluding those notices that do not +> pertain to any part of the Derivative Works, in at least one +> of the following places: within a NOTICE text file distributed +> as part of the Derivative Works; within the Source form or +> documentation, if provided along with the Derivative Works; or, +> within a display generated by the Derivative Works, if and +> wherever such third-party notices normally appear. The contents +> of the NOTICE file are for informational purposes only and +> do not modify the License. You may add Your own attribution +> notices within Derivative Works that You distribute, alongside +> or as an addendum to the NOTICE text from the Work, provided +> that such additional attribution notices cannot be construed +> as modifying the License. +> +> You may add Your own copyright statement to Your modifications and +> may provide additional or different license terms and conditions +> for use, reproduction, or distribution of Your modifications, or +> for any such Derivative Works as a whole, provided Your use, +> reproduction, and distribution of the Work otherwise complies with +> the conditions stated in this License. +> +> 5. Submission of Contributions. Unless You explicitly state otherwise, +> any Contribution intentionally submitted for inclusion in the Work +> by You to the Licensor shall be under the terms and conditions of +> this License, without any additional terms or conditions. +> Notwithstanding the above, nothing herein shall supersede or modify +> the terms of any separate license agreement you may have executed +> with Licensor regarding such Contributions. +> +> 6. Trademarks. This License does not grant permission to use the trade +> names, trademarks, service marks, or product names of the Licensor, +> except as required for reasonable and customary use in describing the +> origin of the Work and reproducing the content of the NOTICE file. +> +> 7. Disclaimer of Warranty. Unless required by applicable law or +> agreed to in writing, Licensor provides the Work (and each +> Contributor provides its Contributions) on an "AS IS" BASIS, +> WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +> implied, including, without limitation, any warranties or conditions +> of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +> PARTICULAR PURPOSE. You are solely responsible for determining the +> appropriateness of using or redistributing the Work and assume any +> risks associated with Your exercise of permissions under this License. +> +> 8. Limitation of Liability. In no event and under no legal theory, +> whether in tort (including negligence), contract, or otherwise, +> unless required by applicable law (such as deliberate and grossly +> negligent acts) or agreed to in writing, shall any Contributor be +> liable to You for damages, including any direct, indirect, special, +> incidental, or consequential damages of any character arising as a +> result of this License or out of the use or inability to use the +> Work (including but not limited to damages for loss of goodwill, +> work stoppage, computer failure or malfunction, or any and all +> other commercial damages or losses), even if such Contributor +> has been advised of the possibility of such damages. +> +> 9. Accepting Warranty or Additional Liability. While redistributing +> the Work or Derivative Works thereof, You may choose to offer, +> and charge a fee for, acceptance of support, warranty, indemnity, +> or other liability obligations and/or rights consistent with this +> License. However, in accepting such obligations, You may act only +> on Your own behalf and on Your sole responsibility, not on behalf +> of any other Contributor, and only if You agree to indemnify, +> defend, and hold each Contributor harmless for any liability +> incurred by, or claims asserted against, such Contributor by reason +> of your accepting any such warranty or additional liability. +> +> END OF TERMS AND CONDITIONS +> +> APPENDIX: How to apply the Apache License to your work. +> +> To apply the Apache License to your work, attach the following +> boilerplate notice, with the fields enclosed by brackets "[]" +> replaced with your own identifying information. (Don't include +> the brackets!) The text should be enclosed in the appropriate +> comment syntax for the file format. We also recommend that a +> file or class name and description of purpose be included on the +> same "printed page" as the copyright notice for easier +> identification within third-party archives. +> +> Copyright 2017 Vercel, Inc. +> +> Licensed under the Apache License, Version 2.0 (the "License"); +> you may not use this file except in compliance with the License. +> You may obtain a copy of the License at +> +> http://www.apache.org/licenses/LICENSE-2.0 +> +> Unless required by applicable law or agreed to in writing, software +> distributed under the License is distributed on an "AS IS" BASIS, +> WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +> See the License for the specific language governing permissions and +> limitations under the License. + +--------------------------------------- + ## @vitest/utils License: MIT Repository: https://github.com/vitest-dev/vitest diff --git a/packages/vite/package.json b/packages/vite/package.json index 410f0af717fd37..3c9c595c3cab7c 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -92,6 +92,7 @@ "@rollup/plugin-alias": "^6.0.0", "@rollup/plugin-dynamic-import-vars": "2.1.4", "@rollup/pluginutils": "^5.3.0", + "@vercel/detect-agent": "^1.1.0", "@types/escape-html": "^1.0.4", "@types/pnpapi": "^0.0.5", "@vitest/utils": "4.1.0-beta.4", diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index d45c6372f7d77b..dd84eb0e076eda 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -1712,7 +1712,7 @@ export async function resolveConfig( ) : '' - const server = resolveServerOptions(resolvedRoot, config.server, logger) + const server = await resolveServerOptions(resolvedRoot, config.server, logger) const builder = resolveBuilderOptions(config.builder) diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 8089dae2b40f21..c3293654ba78a0 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -11,6 +11,7 @@ import corsMiddleware from 'cors' import colors from 'picocolors' import chokidar from 'chokidar' import launchEditorMiddleware from 'launch-editor-middleware' +import { determineAgent } from '@vercel/detect-agent' import type { SourceMap } from 'rolldown' import type { ModuleRunner } from 'vite/module-runner' import type { FSWatcher, WatchOptions } from '#dep-types/chokidar' @@ -264,10 +265,13 @@ export type ServerHook = ( export type HttpServer = http.Server | Http2SecureServer -export function resolveForwardConsoleOptions( +export async function resolveForwardConsoleOptions( value: boolean | ForwardConsoleOptions | undefined, -): ResolvedForwardConsoleOptions { - if (!value) { +): Promise { + const { isAgent } = await determineAgent() + value ??= isAgent + + if (value === false) { return { enabled: false, unhandledErrors: false, @@ -1174,16 +1178,16 @@ const _serverConfigDefaults = Object.freeze({ perEnvironmentStartEndDuringDev: false, perEnvironmentWatchChangeDuringDev: false, // hotUpdateEnvironments - forwardConsole: false, + forwardConsole: undefined, } satisfies ServerOptions) export const serverConfigDefaults: Readonly> = _serverConfigDefaults -export function resolveServerOptions( +export async function resolveServerOptions( root: string, raw: ServerOptions | undefined, logger: Logger, -): ResolvedServerOptions { +): Promise { const _server = mergeWithDefaults( { ..._serverConfigDefaults, @@ -1204,7 +1208,7 @@ export function resolveServerOptions( _server.sourcemapIgnoreList === false ? () => false : _server.sourcemapIgnoreList, - forwardConsole: resolveForwardConsoleOptions(_server.forwardConsole), + forwardConsole: await resolveForwardConsoleOptions(_server.forwardConsole), } let allowDirs = server.fs.allow diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e46e986ab82283..abcfe49bbeda8d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -283,6 +283,9 @@ importers: '@types/pnpapi': specifier: ^0.0.5 version: 0.0.5 + '@vercel/detect-agent': + specifier: ^1.1.0 + version: 1.1.0 '@vitejs/devtools': specifier: ^0.0.0-alpha.31 version: 0.0.0-alpha.31(typescript@5.9.3)(vite@packages+vite)(vue@3.5.28(typescript@5.9.3)) From 55b758724f779763ae47c2f64122f3a271759937 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 14:07:32 +0900 Subject: [PATCH 40/60] test: fix windows --- .../__test__/forward-console.spec.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/playground/forward-console/__test__/forward-console.spec.ts b/playground/forward-console/__test__/forward-console.spec.ts index 732fbad13c1106..6f497c9a3482b6 100644 --- a/playground/forward-console/__test__/forward-console.spec.ts +++ b/playground/forward-console/__test__/forward-console.spec.ts @@ -3,11 +3,18 @@ import { expect, test } from 'vitest' import { isServe, page, serverLogs } from '~utils' function normalizeLogs(logs: string[]) { - return logs - .map((log) => stripVTControlCharacters(log)) - .join('\n') - .replaceAll(/ +\n/g, '\n') // strip trailing spaces - .replaceAll(/\?v=[a-z\d]+/g, '') + return ( + logs + .map((log) => stripVTControlCharacters(log)) + .join('\n') + // normalize .pnpm path + .replaceAll( + /node_modules\/\.pnpm\/[^/\n]+\/node_modules\//g, + 'node_modules/.pnpm//node_modules/', + ) + // strip trailing spaces of code frame + .replaceAll(/ +\n/g, '\n') + ) } test.runIf(isServe)('unhandled error', async () => { @@ -56,7 +63,7 @@ test.runIf(isServe)('dependency stack uses source map path', async () => { await expect.poll(() => normalizeLogs(serverLogs.slice(logIndex))) .toContain(`\ [Unhandled error] Error: this is test dependency error - > throwDepError ../../node_modules/.pnpm/@vitejs+test-forward-console-throw-dep@file+playground+forward-console+fixtures+throw-dep/node_modules/@vitejs/test-forward-console-throw-dep/index.js:2:8 + > throwDepError ../../node_modules/.pnpm//node_modules/@vitejs/test-forward-console-throw-dep/index.js:2:8 > testDepError src/main.ts:42:2 40 | 41 | function testDepError() { From 502c6dfba3e8b7c66dce70042bb2e16bb0277a0b Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 14:16:55 +0900 Subject: [PATCH 41/60] chore: comment --- packages/vite/src/node/plugins/forwardConsole.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/vite/src/node/plugins/forwardConsole.ts b/packages/vite/src/node/plugins/forwardConsole.ts index ca2ea252ed88fa..1c34ea3124e5ce 100644 --- a/packages/vite/src/node/plugins/forwardConsole.ts +++ b/packages/vite/src/node/plugins/forwardConsole.ts @@ -60,7 +60,6 @@ function formatError( sourceMapCache: Map, ) { const error = payload.data - // 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 @@ -105,12 +104,11 @@ function formatError( sourceMapCache.set(id, result?.map) return result?.map }, - // Vitest uses this option to skip internal files - // https://github.com/vitejs/vitest/blob/4783137cd8d766cf998bdf2d638890eaa51e08d9/packages/utils/src/source-map.ts#L17 + // override it to empty since vitest uses this option to skip internal files by default. + // https://github.com/vitest-dev/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 const nearest = stacks.find((stack) => { const modules = environment.moduleGraph.getModulesByFile(stack.file) return ( From 62cc26d0a942209c84174183083a8b8fc290459a Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 15:03:44 +0900 Subject: [PATCH 42/60] refactor: align forwarded console formatting behavior --- packages/vite/src/shared/forwardConsole.ts | 87 ++++++++++++++++++- .../__test__/forward-console.spec.ts | 16 ++-- playground/forward-console/src/main.ts | 19 +++- 3 files changed, 109 insertions(+), 13 deletions(-) diff --git a/packages/vite/src/shared/forwardConsole.ts b/packages/vite/src/shared/forwardConsole.ts index 442043416ab115..3265a15a132d01 100644 --- a/packages/vite/src/shared/forwardConsole.ts +++ b/packages/vite/src/shared/forwardConsole.ts @@ -45,7 +45,7 @@ export function setupForwardConsoleHandler( type: 'log', data: { level, - message: args.map((arg) => stringifyConsoleArg(arg)).join(' '), + message: formatConsoleArgs(args), }, } satisfies ForwardConsolePayload, }) @@ -76,11 +76,94 @@ export function setupForwardConsoleHandler( } } -// TODO: adopt vitest utils for stringify? +function formatConsoleArgs(args: unknown[]): string { + if (args.length === 0) { + return '' + } + + if (typeof args[0] !== 'string') { + return args.map((arg) => stringifyConsoleArg(arg)).join(' ') + } + + const len = args.length + let i = 1 + let message = args[0].replace(/%[sdjifoOc%]/g, (specifier) => { + if (specifier === '%%') { + return '%' + } + if (i >= len) { + return specifier + } + + const arg = args[i++] + switch (specifier) { + case '%s': + if (typeof arg === 'bigint') { + return `${arg.toString()}n` + } + return typeof arg === 'object' && arg !== null + ? stringifyConsoleArg(arg) + : String(arg) + case '%d': + if (typeof arg === 'bigint') { + return `${arg.toString()}n` + } + if (typeof arg === 'symbol') { + return 'NaN' + } + return Number(arg).toString() + case '%i': + if (typeof arg === 'bigint') { + return `${arg.toString()}n` + } + return Number.parseInt(String(arg), 10).toString() + case '%f': + return Number.parseFloat(String(arg)).toString() + case '%o': + case '%O': + return stringifyConsoleArg(arg) + case '%j': + try { + const serialized = JSON.stringify(arg) + return serialized ?? 'undefined' + } catch { + return '[Circular]' + } + case '%c': + return '' + default: + return specifier + } + }) + + for (let arg = args[i]; i < len; arg = args[++i]) { + if (arg === null || typeof arg !== 'object') { + message += ` ${typeof arg === 'symbol' ? arg.toString() : String(arg)}` + } else { + message += ` ${stringifyConsoleArg(arg)}` + } + } + + return message +} + function stringifyConsoleArg(value: unknown): string { if (typeof value === 'string') { return value } + if ( + typeof value === 'number' || + typeof value === 'boolean' || + typeof value === 'undefined' + ) { + return String(value) + } + if (typeof value === 'symbol') { + return value.toString() + } + if (typeof value === 'function') { + return value.name ? `[Function: ${value.name}]` : '[Function]' + } if (value instanceof Error) { return value.stack || `${value.name}: ${value.message}` } diff --git a/playground/forward-console/__test__/forward-console.spec.ts b/playground/forward-console/__test__/forward-console.spec.ts index 6f497c9a3482b6..832b7e371ca495 100644 --- a/playground/forward-console/__test__/forward-console.spec.ts +++ b/playground/forward-console/__test__/forward-console.spec.ts @@ -54,7 +54,9 @@ test.runIf(isServe)('console.error', async () => { await page.click('#test-console-error') await expect .poll(() => normalizeLogs(serverLogs.slice(logIndex))) - .toContain(`[console.error] this is test console error`) + .toContain( + `[console.error] format: string=hello number=12.9 int=42 float=3.5 json={"id":1} object={"enabled":true} object2={"nested":{"deep":1}} style= literal=% trailing done`, + ) }) test.runIf(isServe)('dependency stack uses source map path', async () => { @@ -64,13 +66,13 @@ test.runIf(isServe)('dependency stack uses source map path', async () => { .toContain(`\ [Unhandled error] Error: this is test dependency error > throwDepError ../../node_modules/.pnpm//node_modules/@vitejs/test-forward-console-throw-dep/index.js:2:8 - > testDepError src/main.ts:42:2 - 40 | - 41 | function testDepError() { - 42 | throwDepError() + > testDepError src/main.ts:38:2 + 36 | + 37 | function testDepError() { + 38 | throwDepError() | ^ - 43 | } - 44 | + 39 | } + 40 | > HTMLButtonElement. src/main.ts:22:2 `) }) diff --git a/playground/forward-console/src/main.ts b/playground/forward-console/src/main.ts index aa75a390ba356e..071e8d82ce8db0 100644 --- a/playground/forward-console/src/main.ts +++ b/playground/forward-console/src/main.ts @@ -34,10 +34,21 @@ function testUnhandledRejection() { Promise.reject(new Error('this is test unhandledrejection')) } -function testConsoleError() { - console.error('this is test console error') -} - function testDepError() { throwDepError() } + +function testConsoleError() { + console.error( + 'format: string=%s number=%d int=%i float=%f json=%j object=%o object2=%O style=%c literal=%% trailing', + 'hello', + 12.9, + '42px', + '3.5', + { id: 1 }, + { enabled: true }, + { nested: { deep: 1 } }, + 'color:red', + 'done', + ) +} From 2554c9e8965d55ba679b3550c783784aa27f88b5 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 15:08:26 +0900 Subject: [PATCH 43/60] test: cover formatConsoleArgs with unit cases --- .../src/node/__tests__/forwardConsole.spec.ts | 41 +++++++++++++++++++ packages/vite/src/shared/forwardConsole.ts | 2 +- 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 packages/vite/src/node/__tests__/forwardConsole.spec.ts diff --git a/packages/vite/src/node/__tests__/forwardConsole.spec.ts b/packages/vite/src/node/__tests__/forwardConsole.spec.ts new file mode 100644 index 00000000000000..eb7a6617022686 --- /dev/null +++ b/packages/vite/src/node/__tests__/forwardConsole.spec.ts @@ -0,0 +1,41 @@ +import { describe, expect, test } from 'vitest' +import { formatConsoleArgs } from '../../shared/forwardConsole' + +describe('formatConsoleArgs', () => { + test('formats placeholders similar to browser console', () => { + const message = formatConsoleArgs([ + 'format: string=%s number=%d int=%i float=%f json=%j object=%o object2=%O style=%c literal=%% trailing', + 'hello', + 12.9, + '42px', + '3.5', + { id: 1 }, + { enabled: true }, + { nested: { deep: 1 } }, + 'color:red', + 'done', + ]) + + expect(message).toMatchInlineSnapshot( + '"format: string=hello number=12.9 int=42 float=3.5 json={"id":1} object={"enabled":true} object2={"nested":{"deep":1}} style= literal=% trailing done"', + ) + }) + + test('formats non-template arguments', () => { + function sampleFn() { + return undefined + } + expect( + formatConsoleArgs([{ ok: true }, Symbol.for('x'), sampleFn]), + ).toMatchInlineSnapshot('"{"ok":true} Symbol(x) [Function: sampleFn]"') + }) + + test('formats circular values for %j', () => { + const circular: any = {} + circular.self = circular + + expect(formatConsoleArgs(['circular=%j', circular])).toMatchInlineSnapshot( + '"circular=[Circular]"', + ) + }) +}) diff --git a/packages/vite/src/shared/forwardConsole.ts b/packages/vite/src/shared/forwardConsole.ts index 3265a15a132d01..b63775b96ee4d7 100644 --- a/packages/vite/src/shared/forwardConsole.ts +++ b/packages/vite/src/shared/forwardConsole.ts @@ -76,7 +76,7 @@ export function setupForwardConsoleHandler( } } -function formatConsoleArgs(args: unknown[]): string { +export function formatConsoleArgs(args: unknown[]): string { if (args.length === 0) { return '' } From 91f947fa0717fc637f0c7bc11dc19e078fab46a8 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 15:13:22 +0900 Subject: [PATCH 44/60] chore: comment --- packages/vite/src/shared/forwardConsole.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/vite/src/shared/forwardConsole.ts b/packages/vite/src/shared/forwardConsole.ts index b63775b96ee4d7..cfc1523166db9e 100644 --- a/packages/vite/src/shared/forwardConsole.ts +++ b/packages/vite/src/shared/forwardConsole.ts @@ -76,6 +76,8 @@ export function setupForwardConsoleHandler( } } +// Zero dep version of Vitest's console formatter +// https://github.com/vitest-dev/vitest/blob/a2d650e00dbd8220397c5c25aef05c850100e446/packages/utils/src/display.ts#L129 export function formatConsoleArgs(args: unknown[]): string { if (args.length === 0) { return '' From c5cb816bba2961bdcbb6db951b498d58992399d9 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 15:47:50 +0900 Subject: [PATCH 45/60] refactor: support custom forwardConsole log levels --- packages/vite/src/shared/forwardConsole.ts | 15 ++++++++++++--- packages/vite/types/customEvent.d.ts | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/vite/src/shared/forwardConsole.ts b/packages/vite/src/shared/forwardConsole.ts index cfc1523166db9e..4401ab17cff2dc 100644 --- a/packages/vite/src/shared/forwardConsole.ts +++ b/packages/vite/src/shared/forwardConsole.ts @@ -1,7 +1,13 @@ import type { ForwardConsolePayload } from '#types/customEvent' import type { NormalizedModuleRunnerTransport } from './moduleRunnerTransport' -export type ForwardConsoleLogLevel = 'error' | 'warn' | 'info' | 'log' | 'debug' +export type ForwardConsoleLogLevel = + | 'error' + | 'warn' + | 'info' + | 'log' + | 'debug' + | (string & {}) export interface ForwardConsoleOptions { unhandledErrors?: boolean @@ -52,8 +58,11 @@ export function setupForwardConsoleHandler( } for (const level of options.logLevels) { - const original = console[level].bind(console) - console[level] = (...args: unknown[]) => { + const original = (console as any)[level] + if (typeof original !== 'function') { + continue + } + ;(console as any)[level] = (...args: unknown[]) => { original(...args) sendLog(level, args) } diff --git a/packages/vite/types/customEvent.d.ts b/packages/vite/types/customEvent.d.ts index 27a6e635e38d93..c971a9609bdcd0 100644 --- a/packages/vite/types/customEvent.d.ts +++ b/packages/vite/types/customEvent.d.ts @@ -62,7 +62,7 @@ export type ForwardConsolePayload = | { type: 'log' data: { - level: 'error' | 'warn' | 'info' | 'log' | 'debug' + level: string message: string } } From f019f9cbcbadecbc4e9bcac2395a0c866afeec83 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 16:12:25 +0900 Subject: [PATCH 46/60] chore: fix forwardConsole formatter lint warnings --- packages/vite/src/shared/forwardConsole.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vite/src/shared/forwardConsole.ts b/packages/vite/src/shared/forwardConsole.ts index 4401ab17cff2dc..d675cae366006e 100644 --- a/packages/vite/src/shared/forwardConsole.ts +++ b/packages/vite/src/shared/forwardConsole.ts @@ -112,7 +112,7 @@ export function formatConsoleArgs(args: unknown[]): string { if (typeof arg === 'bigint') { return `${arg.toString()}n` } - return typeof arg === 'object' && arg !== null + return typeof arg === 'object' && arg != null ? stringifyConsoleArg(arg) : String(arg) case '%d': @@ -148,7 +148,7 @@ export function formatConsoleArgs(args: unknown[]): string { }) for (let arg = args[i]; i < len; arg = args[++i]) { - if (arg === null || typeof arg !== 'object') { + if (arg == null || typeof arg !== 'object') { message += ` ${typeof arg === 'symbol' ? arg.toString() : String(arg)}` } else { message += ` ${stringifyConsoleArg(arg)}` From 519f2724351f69c127c4d2f699f28f67460b560b Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 16:14:26 +0900 Subject: [PATCH 47/60] docs: tweak --- docs/config/server-options.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/config/server-options.md b/docs/config/server-options.md index 450e23e0e9243b..4c018dae7e3a2d 100644 --- a/docs/config/server-options.md +++ b/docs/config/server-options.md @@ -235,6 +235,19 @@ Forward browser runtime events to the Vite server console during development. - `unhandledErrors` controls forwarding uncaught exceptions and unhandled promise rejections. - `logLevels` controls which `console.*` calls are forwarded. +For example: + +```js +export default defineConfig({ + server: { + forwardConsole: { + unhandledErrors: true, + logLevels: ['warn', 'error'], + }, + }, +}) +``` + When unhandled errors are forwarded, they are logged in the server terminal with enhanced formatting, for example: ```log @@ -249,19 +262,6 @@ When unhandled errors are forwarded, they are logged in the server terminal with > HTMLButtonElement. src/main.ts:6:2 ``` -This feature is useful when working with AI coding assistants that can only see terminal output for context. - -```js -export default defineConfig({ - server: { - forwardConsole: { - unhandledErrors: true, - logLevels: ['warn', 'error'], - }, - }, -}) -``` - ## server.warmup - **Type:** `{ clientFiles?: string[], ssrFiles?: string[] }` From 4262b514d442bc371d5f63ce3e039521e8c0236c Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 16:23:31 +0900 Subject: [PATCH 48/60] test: more unit --- .../src/node/__tests__/forwardConsole.spec.ts | 74 ++++++++++++------- 1 file changed, 47 insertions(+), 27 deletions(-) diff --git a/packages/vite/src/node/__tests__/forwardConsole.spec.ts b/packages/vite/src/node/__tests__/forwardConsole.spec.ts index eb7a6617022686..bb50ba7242917f 100644 --- a/packages/vite/src/node/__tests__/forwardConsole.spec.ts +++ b/packages/vite/src/node/__tests__/forwardConsole.spec.ts @@ -2,40 +2,60 @@ import { describe, expect, test } from 'vitest' import { formatConsoleArgs } from '../../shared/forwardConsole' describe('formatConsoleArgs', () => { - test('formats placeholders similar to browser console', () => { - const message = formatConsoleArgs([ - 'format: string=%s number=%d int=%i float=%f json=%j object=%o object2=%O style=%c literal=%% trailing', - 'hello', - 12.9, - '42px', - '3.5', - { id: 1 }, - { enabled: true }, - { nested: { deep: 1 } }, - 'color:red', - 'done', - ]) - - expect(message).toMatchInlineSnapshot( - '"format: string=hello number=12.9 int=42 float=3.5 json={"id":1} object={"enabled":true} object2={"nested":{"deep":1}} style= literal=% trailing done"', + test('formats placeholders', () => { + expect( + formatConsoleArgs([ + 'format: string=%s number=%d int=%i float=%f json=%j object=%o object2=%O sym=%d style=%c literal=%% trailing', + 'hello', + 12.9, + '42px', + '3.5', + { id: 1 }, + { enabled: true }, + { nested: { deep: 1 } }, + Symbol.for('x'), + 'color:red', + 'done', + ]), + ).toMatchInlineSnapshot( + `"format: string=hello number=12.9 int=42 float=3.5 json={"id":1} object={"enabled":true} object2={"nested":{"deep":1}} sym=NaN style= literal=% trailing done"`, ) - }) - test('formats non-template arguments', () => { - function sampleFn() { - return undefined - } expect( - formatConsoleArgs([{ ok: true }, Symbol.for('x'), sampleFn]), - ).toMatchInlineSnapshot('"{"ok":true} Symbol(x) [Function: sampleFn]"') + formatConsoleArgs(['num=%d int=%i pct=%% miss=%s sym=%d', 3.14, '42px']), + ).toMatchInlineSnapshot(`"num=3.14 int=42 pct=% miss=%s sym=%d"`) }) - test('formats circular values for %j', () => { - const circular: any = {} + test('stringifies diverse non-template arguments', () => { + const topError = new Error('boom') + topError.stack = undefined + + const nestedError = new Error('nested') + nestedError.stack = undefined + + const circular: any = { + ok: true, + big: 2n, + err: nestedError, + } circular.self = circular - expect(formatConsoleArgs(['circular=%j', circular])).toMatchInlineSnapshot( - '"circular=[Circular]"', + function sampleFn() { + return undefined + } + + expect( + formatConsoleArgs([ + 1n, + undefined, + true, + Symbol.for('s'), + sampleFn, + topError, + circular, + ]), + ).toMatchInlineSnapshot( + `"1n undefined true Symbol(s) [Function: sampleFn] Error: boom {"ok":true,"big":"2n","err":{"name":"Error","message":"nested"},"self":"[Circular]"}"`, ) }) }) From 2d3b96ca53aa926e2107e9f4d44b15ffe52272a1 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 18:19:31 +0900 Subject: [PATCH 49/60] experiment: enable by default --- packages/vite/src/node/server/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index c3293654ba78a0..c1100259a285ef 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -269,6 +269,7 @@ export async function resolveForwardConsoleOptions( value: boolean | ForwardConsoleOptions | undefined, ): Promise { const { isAgent } = await determineAgent() + value ??= true value ??= isAgent if (value === false) { From 98874d5dd10760631c1eab5d525dc8b6548bcda9 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 18:37:28 +0900 Subject: [PATCH 50/60] test: update hmr.spec.ts so it passes with forwardConsole --- playground/hmr/__tests__/hmr.spec.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/playground/hmr/__tests__/hmr.spec.ts b/playground/hmr/__tests__/hmr.spec.ts index 3841942f24a657..c5a14b7624db1e 100644 --- a/playground/hmr/__tests__/hmr.spec.ts +++ b/playground/hmr/__tests__/hmr.spec.ts @@ -1053,16 +1053,19 @@ if (!isBuild) { await page.goto(viteTestUrl + '/self-accept-within-circular/index.html') const el = await page.$('.self-accept-within-circular') expect(await el.textContent()).toBe('c') + const lastServerLogIndex = serverLogs.length editFile('self-accept-within-circular/c.js', (code) => code.replace(`export const c = 'c'`, `export const c = 'cc'`), ) await expect .poll(() => page.textContent('.self-accept-within-circular')) .toBe('cc') - expect(serverLogs.length).greaterThanOrEqual(1) + const serverLogsAfter = serverLogs.slice(lastServerLogIndex) // Should still keep hmr update, but it'll error on the browser-side and will refresh itself. // Match on full log not possible because of color markers - expect(serverLogs.at(-1)!).toContain('hmr update') + expect(serverLogsAfter).toContain( + 'hmr update /self-accept-within-circular/c.js', + ) }) test('hmr should not reload if no accepted within circular imported files', async () => { From 3eab10cf7953da79008ddba1bb16867679c95c95 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 18:52:50 +0900 Subject: [PATCH 51/60] test: debug --- playground/hmr/__tests__/hmr.spec.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/playground/hmr/__tests__/hmr.spec.ts b/playground/hmr/__tests__/hmr.spec.ts index c5a14b7624db1e..dcc6d8f5c78c5f 100644 --- a/playground/hmr/__tests__/hmr.spec.ts +++ b/playground/hmr/__tests__/hmr.spec.ts @@ -1049,7 +1049,9 @@ if (!isBuild) { await expect.poll(() => el.textContent()).toMatch('2') }) - test('hmr works for self-accepted module within circular imported files', async () => { + test('hmr works for self-accepted module within circular imported files', async ({ + onTestFailed, + }) => { await page.goto(viteTestUrl + '/self-accept-within-circular/index.html') const el = await page.$('.self-accept-within-circular') expect(await el.textContent()).toBe('c') @@ -1060,12 +1062,14 @@ if (!isBuild) { await expect .poll(() => page.textContent('.self-accept-within-circular')) .toBe('cc') - const serverLogsAfter = serverLogs.slice(lastServerLogIndex) + onTestFailed(() => { + console.log('debug:', serverLogs.slice(lastServerLogIndex)) + }) // Should still keep hmr update, but it'll error on the browser-side and will refresh itself. // Match on full log not possible because of color markers - expect(serverLogsAfter).toContain( - 'hmr update /self-accept-within-circular/c.js', - ) + await expect + .poll(() => serverLogs.slice(lastServerLogIndex)) + .toContain('hmr update /self-accept-within-circular/c.js') }) test('hmr should not reload if no accepted within circular imported files', async () => { From 3d298274032053e6d44789b9925ad3e4adbe8e92 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 18:55:57 +0900 Subject: [PATCH 52/60] test: stripVTControlCharacters --- playground/hmr/__tests__/hmr.spec.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/playground/hmr/__tests__/hmr.spec.ts b/playground/hmr/__tests__/hmr.spec.ts index dcc6d8f5c78c5f..1ea2ca1d2bff98 100644 --- a/playground/hmr/__tests__/hmr.spec.ts +++ b/playground/hmr/__tests__/hmr.spec.ts @@ -1,3 +1,4 @@ +import { stripVTControlCharacters } from 'node:util' import { beforeAll, describe, expect, it, test } from 'vitest' import type { Page } from 'playwright-chromium' import { @@ -1066,9 +1067,10 @@ if (!isBuild) { console.log('debug:', serverLogs.slice(lastServerLogIndex)) }) // Should still keep hmr update, but it'll error on the browser-side and will refresh itself. - // Match on full log not possible because of color markers await expect - .poll(() => serverLogs.slice(lastServerLogIndex)) + .poll(() => + serverLogs.slice(lastServerLogIndex).map(stripVTControlCharacters), + ) .toContain('hmr update /self-accept-within-circular/c.js') }) From 9eac5d57842f10f71228186b211d817c1918fbf2 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 19:04:25 +0900 Subject: [PATCH 53/60] test: cleanup --- playground/hmr/__tests__/hmr.spec.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/playground/hmr/__tests__/hmr.spec.ts b/playground/hmr/__tests__/hmr.spec.ts index 1ea2ca1d2bff98..4097670d8400d1 100644 --- a/playground/hmr/__tests__/hmr.spec.ts +++ b/playground/hmr/__tests__/hmr.spec.ts @@ -1050,9 +1050,7 @@ if (!isBuild) { await expect.poll(() => el.textContent()).toMatch('2') }) - test('hmr works for self-accepted module within circular imported files', async ({ - onTestFailed, - }) => { + test('hmr works for self-accepted module within circular imported files', async () => { await page.goto(viteTestUrl + '/self-accept-within-circular/index.html') const el = await page.$('.self-accept-within-circular') expect(await el.textContent()).toBe('c') @@ -1063,15 +1061,10 @@ if (!isBuild) { await expect .poll(() => page.textContent('.self-accept-within-circular')) .toBe('cc') - onTestFailed(() => { - console.log('debug:', serverLogs.slice(lastServerLogIndex)) - }) // Should still keep hmr update, but it'll error on the browser-side and will refresh itself. - await expect - .poll(() => - serverLogs.slice(lastServerLogIndex).map(stripVTControlCharacters), - ) - .toContain('hmr update /self-accept-within-circular/c.js') + expect( + serverLogs.slice(lastServerLogIndex).map(stripVTControlCharacters), + ).toContain('hmr update /self-accept-within-circular/c.js') }) test('hmr should not reload if no accepted within circular imported files', async () => { From df818e7dff8571d50e0ce398869e34c8e36e97df Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 23 Feb 2026 19:09:27 +0900 Subject: [PATCH 54/60] Revert "experiment: enable by default" This reverts commit 2d3b96ca53aa926e2107e9f4d44b15ffe52272a1. --- packages/vite/src/node/server/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index c1100259a285ef..c3293654ba78a0 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -269,7 +269,6 @@ export async function resolveForwardConsoleOptions( value: boolean | ForwardConsoleOptions | undefined, ): Promise { const { isAgent } = await determineAgent() - value ??= true value ??= isAgent if (value === false) { From 7089143fd73a17ea97098c2307f3d6697e609de9 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Fri, 27 Feb 2026 18:22:00 +0900 Subject: [PATCH 55/60] chore: update @vitest/utils --- packages/vite/package.json | 2 +- pnpm-lock.yaml | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/vite/package.json b/packages/vite/package.json index 4638f5182e5414..670f02925bb61e 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -94,7 +94,7 @@ "@vercel/detect-agent": "^1.1.0", "@types/escape-html": "^1.0.4", "@types/pnpapi": "^0.0.5", - "@vitest/utils": "4.1.0-beta.4", + "@vitest/utils": "4.1.0-beta.5", "@vitejs/devtools": "^0.0.0-alpha.32", "artichokie": "^0.4.2", "baseline-browser-mapping": "^2.10.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ce4e11017c6932..7742d511d027e3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -284,8 +284,8 @@ importers: specifier: ^0.0.0-alpha.32 version: 0.0.0-alpha.32(typescript@5.9.3)(vite@packages+vite)(vue@3.5.28(typescript@5.9.3)) '@vitest/utils': - specifier: 4.1.0-beta.4 - version: 4.1.0-beta.4 + specifier: 4.1.0-beta.5 + version: 4.1.0-beta.5 artichokie: specifier: ^0.4.2 version: 0.4.2 @@ -4333,8 +4333,8 @@ packages: '@vitest/pretty-format@4.0.18': resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} - '@vitest/pretty-format@4.1.0-beta.4': - resolution: {integrity: sha512-rAJOtUSRzgobQtuW98WV3bSkomdILArhgSc4JQ5G6Et0eaD6DTeMpr+k7B//F/xYG7oVeuabOTx3EWu06ILCgA==} + '@vitest/pretty-format@4.1.0-beta.5': + resolution: {integrity: sha512-QH/FGecnl2uwLveL/n1awB/nm/dJL9M0vMKVwmW0tvLAqTOp5GQQOypRuVvpXNFGhIl2bfpUSjruuDQlCBeFjw==} '@vitest/runner@4.0.18': resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} @@ -4348,8 +4348,8 @@ packages: '@vitest/utils@4.0.18': resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} - '@vitest/utils@4.1.0-beta.4': - resolution: {integrity: sha512-c6oj0FpdLwmOisNpeVwhXqwe9Harehj+n9Pfz4/Iv45dSR32VHDzK2uosqT2ocCZhgzXwX4xpL8thl2Wr/wyrw==} + '@vitest/utils@4.1.0-beta.5': + resolution: {integrity: sha512-yDobPgmVL/4YhVXsbBcmeUb5CIdZiJkoonPnuJXKOxmnj0XZyu7OgIX3KLOcRStbia3nJZ9VIIBWoSv+HS+wVA==} '@voidzero-dev/vitepress-theme@4.5.0': resolution: {integrity: sha512-jY6VprjFeIjBAPWIh0FJUSwU3GwKrHmf8u91HkLxHvzilXbyuu9sJCfF7FnjG2hYH07nF86xnnm+KGZxvFxthg==} @@ -10251,7 +10251,7 @@ snapshots: dependencies: tinyrainbow: 3.0.3 - '@vitest/pretty-format@4.1.0-beta.4': + '@vitest/pretty-format@4.1.0-beta.5': dependencies: tinyrainbow: 3.0.3 @@ -10273,9 +10273,10 @@ snapshots: '@vitest/pretty-format': 4.0.18 tinyrainbow: 3.0.3 - '@vitest/utils@4.1.0-beta.4': + '@vitest/utils@4.1.0-beta.5': dependencies: - '@vitest/pretty-format': 4.1.0-beta.4 + '@vitest/pretty-format': 4.1.0-beta.5 + convert-source-map: 2.0.0 tinyrainbow: 3.0.3 '@voidzero-dev/vitepress-theme@4.5.0(focus-trap@7.8.0)(vite@packages+vite)(vitepress@2.0.0-alpha.16(oxc-minify@0.115.0)(postcss@8.5.6)(typescript@5.9.3))(vue@3.5.28(typescript@5.9.3))': From 78dbc4dd4340ef7da69fec31553ed25547ef41b2 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Fri, 27 Feb 2026 18:36:12 +0900 Subject: [PATCH 56/60] refactor: move forwardConsole test path --- .../vite/src/{node => shared}/__tests__/forwardConsole.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename packages/vite/src/{node => shared}/__tests__/forwardConsole.spec.ts (96%) diff --git a/packages/vite/src/node/__tests__/forwardConsole.spec.ts b/packages/vite/src/shared/__tests__/forwardConsole.spec.ts similarity index 96% rename from packages/vite/src/node/__tests__/forwardConsole.spec.ts rename to packages/vite/src/shared/__tests__/forwardConsole.spec.ts index bb50ba7242917f..44c6b1361f2783 100644 --- a/packages/vite/src/node/__tests__/forwardConsole.spec.ts +++ b/packages/vite/src/shared/__tests__/forwardConsole.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from 'vitest' -import { formatConsoleArgs } from '../../shared/forwardConsole' +import { formatConsoleArgs } from '../forwardConsole' describe('formatConsoleArgs', () => { test('formats placeholders', () => { From 98a0144669d958d9e1248c2e9b14f49e3e121128 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Fri, 27 Feb 2026 18:53:34 +0900 Subject: [PATCH 57/60] chore: fix types --- packages/vite/src/shared/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/vite/src/shared/tsconfig.json b/packages/vite/src/shared/tsconfig.json index 18d41981260d48..448d4f977d643c 100644 --- a/packages/vite/src/shared/tsconfig.json +++ b/packages/vite/src/shared/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.base.json", "include": ["./", "../types"], + "exclude": ["__tests__"], "compilerOptions": { "lib": ["ESNext", "DOM"], "stripInternal": true From 33a8810233a3d1f5c901af9ff4b258ee255ecc1c Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 4 Mar 2026 16:58:17 +0900 Subject: [PATCH 58/60] refactor: use escapeReplacement --- packages/vite/src/node/plugins/clientInjections.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vite/src/node/plugins/clientInjections.ts b/packages/vite/src/node/plugins/clientInjections.ts index 92b641a9120f41..e568a2551c5a83 100644 --- a/packages/vite/src/node/plugins/clientInjections.ts +++ b/packages/vite/src/node/plugins/clientInjections.ts @@ -113,7 +113,7 @@ async function createClientConfigValueReplacer( const hmrConfigNameReplacement = escapeReplacement(hmrConfigName) const wsTokenReplacement = escapeReplacement(config.webSocketToken) const serverForwardConsoleReplacement = () => - JSON.stringify(config.server.forwardConsole) + escapeReplacement(config.server.forwardConsole as any) const bundleDevReplacement = escapeReplacement( config.experimental.bundledDev || false, ) From 56907ea0b58299421b6edb7d19d01be49b736b42 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 4 Mar 2026 17:08:33 +0900 Subject: [PATCH 59/60] fix: determineAgent only if null --- packages/vite/src/node/server/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index c3293654ba78a0..d9764bd69c53db 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -268,8 +268,7 @@ export type HttpServer = http.Server | Http2SecureServer export async function resolveForwardConsoleOptions( value: boolean | ForwardConsoleOptions | undefined, ): Promise { - const { isAgent } = await determineAgent() - value ??= isAgent + value ??= (await determineAgent()).isAgent if (value === false) { return { From 7b9a8195c4e521c840dce1a030190cd9fa5e7c7a Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 4 Mar 2026 17:56:43 +0900 Subject: [PATCH 60/60] fix: typo --- packages/vite/src/node/plugins/clientInjections.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/vite/src/node/plugins/clientInjections.ts b/packages/vite/src/node/plugins/clientInjections.ts index e568a2551c5a83..21c6c6106d3fcd 100644 --- a/packages/vite/src/node/plugins/clientInjections.ts +++ b/packages/vite/src/node/plugins/clientInjections.ts @@ -112,8 +112,9 @@ async function createClientConfigValueReplacer( const hmrEnableOverlayReplacement = escapeReplacement(overlay) const hmrConfigNameReplacement = escapeReplacement(hmrConfigName) const wsTokenReplacement = escapeReplacement(config.webSocketToken) - const serverForwardConsoleReplacement = () => - escapeReplacement(config.server.forwardConsole as any) + const serverForwardConsoleReplacement = escapeReplacement( + config.server.forwardConsole as any, + ) const bundleDevReplacement = escapeReplacement( config.experimental.bundledDev || false, )