From 8dce356e2dfa5bb80cfabe98490c77d9dc5f4255 Mon Sep 17 00:00:00 2001 From: Jonas Galvez Date: Sun, 27 Jul 2025 20:22:02 -0300 Subject: [PATCH 1/4] initial pieces --- packages/vite/src/client/client.ts | 14 ++++ packages/vite/src/node/constants.ts | 1 + packages/vite/src/node/server/index.ts | 4 + .../node/server/middlewares/errorIngest.ts | 74 +++++++++++++++++++ packages/vite/src/node/utils.ts | 2 + playground/client-errors/index.html | 5 ++ playground/client-errors/index.js | 8 ++ playground/client-errors/package.json | 15 ++++ playground/client-errors/vite.config.ts | 5 ++ pnpm-lock.yaml | 6 ++ 10 files changed, 134 insertions(+) create mode 100644 packages/vite/src/node/server/middlewares/errorIngest.ts create mode 100644 playground/client-errors/index.html create mode 100644 playground/client-errors/index.js create mode 100644 playground/client-errors/package.json create mode 100644 playground/client-errors/vite.config.ts diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index 1db4a49aa2556a..2b5898156b7928 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -108,6 +108,20 @@ if (typeof window !== 'undefined') { window.addEventListener('beforeunload', () => { willUnload = true }) + window.addEventListener('error', (error) => { + console.log(error); + fetch('/@vite/errors', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + message: error.message, + colno: error.colno, + lineno: error.lineno, + }) + }) + }) } function cleanUrl(pathname: string): string { diff --git a/packages/vite/src/node/constants.ts b/packages/vite/src/node/constants.ts index ce204b1c04e30b..ecfc47b4e9f095 100644 --- a/packages/vite/src/node/constants.ts +++ b/packages/vite/src/node/constants.ts @@ -102,6 +102,7 @@ export const SPECIAL_QUERY_RE = /[?&](?:worker|sharedworker|raw|url)\b/ export const FS_PREFIX = `/@fs/` export const CLIENT_PUBLIC_PATH = `/@vite/client` +export const CLIENT_ERROR_RELAY_PATH = '/@vite/errors' export const ENV_PUBLIC_PATH = `/@vite/env` export const VITE_PACKAGE_DIR = resolve( fileURLToPath(import.meta.url), diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 9cc9956dc046b5..72675904e09d1f 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -93,6 +93,7 @@ import { ModuleGraph } from './mixedModuleGraph' import type { ModuleNode } from './mixedModuleGraph' import { notFoundMiddleware } from './middlewares/notFound' import { buildErrorMessage, errorMiddleware } from './middlewares/error' +import { errorIngestMiddleware } from './middlewares/errorIngest' import type { HmrOptions, NormalizedHotChannel } from './hmr' import { handleHMRUpdate, updateModules } from './hmr' import { openBrowser as _openBrowser } from './openBrowser' @@ -946,6 +947,9 @@ export async function _createServer( middlewares.use(notFoundMiddleware()) } + // error handler + middlewares.use(errorIngestMiddleware(server)) + // error handler middlewares.use(errorMiddleware(server, !!middlewareMode)) diff --git a/packages/vite/src/node/server/middlewares/errorIngest.ts b/packages/vite/src/node/server/middlewares/errorIngest.ts new file mode 100644 index 00000000000000..738790d02c4dc8 --- /dev/null +++ b/packages/vite/src/node/server/middlewares/errorIngest.ts @@ -0,0 +1,74 @@ +import path from 'node:path' +import { stripVTControlCharacters as strip } from 'node:util' +import colors from 'picocolors' +import type { RollupError } from 'rollup' +import type { Connect } from 'dep-types/connect' +import type { ErrorPayload } from 'types/hmrPayload' +import { pad } from '../../utils' +import type { ViteDevServer } from '../..' +import { CLIENT_ERROR_RELAY_PATH } from '../../constants' + +export function prepareError(err: Error | RollupError): ErrorPayload['err'] { + // only copy the information we need and avoid serializing unnecessary + // properties, since some errors may attach full objects (e.g. PostCSS) + return { + message: strip(err.message), + stack: strip(cleanStack(err.stack || '')), + id: (err as RollupError).id, + frame: strip((err as RollupError).frame || ''), + plugin: (err as RollupError).plugin, + pluginCode: (err as RollupError).pluginCode?.toString(), + loc: (err as RollupError).loc, + } +} + +export function buildErrorMessage( + err: RollupError, + args: string[] = [], + includeStack = true, +): string { + if (err.plugin) args.push(` Plugin: ${colors.magenta(err.plugin)}`) + const loc = err.loc ? `:${err.loc.line}:${err.loc.column}` : '' + if (err.id) args.push(` File: ${colors.cyan(err.id)}${loc}`) + if (err.frame) args.push(colors.yellow(pad(err.frame))) + if (includeStack && err.stack) args.push(pad(cleanStack(err.stack))) + return args.join('\n') +} + +function cleanStack(stack: string) { + return stack + .split(/\n/) + .filter((l) => /^\s*at/.test(l)) + .join('\n') +} + +export function logError(server: ViteDevServer, err: any): void { + const msg = buildErrorMessage(err, [ + colors.red(`Client error: ${err.message}`), + ]) + + server.config.logger.error(msg, { + clear: true, + timestamp: true, + error: err, + }) + + server.environments.client.hot.send({ + type: 'error', + err: prepareError(err), + }) +} + +export function errorIngestMiddleware( + server: ViteDevServer, +): Connect.ErrorHandleFunction { + return function viteErrorIngestMiddleware(req, res, next) { + if (req.url === CLIENT_ERROR_RELAY_PATH) { + console.log('req', req) + req.statusCode = 201 + req.end('') + } else { + next() + } + } +} diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index 083010b9455a30..4c35892c82946f 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -33,6 +33,7 @@ import { VALID_ID_PREFIX } from '../shared/constants' import { CLIENT_ENTRY, CLIENT_PUBLIC_PATH, + CLIENT_ERROR_RELAY_PATH, CSS_LANGS_RE, ENV_PUBLIC_PATH, FS_PREFIX, @@ -326,6 +327,7 @@ const internalPrefixes = [ FS_PREFIX, VALID_ID_PREFIX, CLIENT_PUBLIC_PATH, + CLIENT_ERROR_RELAY_PATH, ENV_PUBLIC_PATH, ] const InternalPrefixRE = new RegExp(`^(?:${internalPrefixes.join('|')})`) diff --git a/playground/client-errors/index.html b/playground/client-errors/index.html new file mode 100644 index 00000000000000..bffbf1b8898578 --- /dev/null +++ b/playground/client-errors/index.html @@ -0,0 +1,5 @@ + +

Test Client Reload

+ + + diff --git a/playground/client-errors/index.js b/playground/client-errors/index.js new file mode 100644 index 00000000000000..da16ac2820870a --- /dev/null +++ b/playground/client-errors/index.js @@ -0,0 +1,8 @@ + +const input = document.querySelector('input') + +input.addEventListener('click', handleClick) + +function handleClick () { + throw new Error('Something went wrong') +} diff --git a/playground/client-errors/package.json b/playground/client-errors/package.json new file mode 100644 index 00000000000000..73df251135ccfa --- /dev/null +++ b/playground/client-errors/package.json @@ -0,0 +1,15 @@ +{ + "name": "@vitejs/test-client-errors", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "debug": "node --inspect-brk ../../packages/vite/bin/vite", + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "vite": "workspace:^" + } +} diff --git a/playground/client-errors/vite.config.ts b/playground/client-errors/vite.config.ts new file mode 100644 index 00000000000000..4c9c4be6ba0c82 --- /dev/null +++ b/playground/client-errors/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 bfba69208f1d38..25f6efaf3ee5be 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -564,6 +564,12 @@ importers: specifier: ^0.11.4 version: 0.11.4 + playground/client-errors: + devDependencies: + vite: + specifier: workspace:* + version: link:../../packages/vite + playground/client-reload: {} playground/csp: {} From 20807413e273e451c7f137b275a48f8ea507140e Mon Sep 17 00:00:00 2001 From: Jonas Galvez Date: Sun, 3 Aug 2025 16:35:57 -0300 Subject: [PATCH 2/4] rm @shikijs/cli dependency --- packages/vite/package.json | 1 - packages/vite/src/node/server/middlewares/errorIngest.ts | 5 +---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/vite/package.json b/packages/vite/package.json index ff9d0b218afde9..2e07b23e6458fa 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -81,7 +81,6 @@ }, "//": "READ CONTRIBUTING.md to understand what to put under deps vs. devDeps!", "dependencies": { - "@shikijs/cli": "^3.8.1", "body-parser": "^2.2.0", "esbuild": "^0.25.0", "fdir": "^6.4.6", diff --git a/packages/vite/src/node/server/middlewares/errorIngest.ts b/packages/vite/src/node/server/middlewares/errorIngest.ts index 8f819f8819aae7..191b9b3105d045 100644 --- a/packages/vite/src/node/server/middlewares/errorIngest.ts +++ b/packages/vite/src/node/server/middlewares/errorIngest.ts @@ -1,7 +1,6 @@ import path from 'node:path' import repl from 'node:repl' import { readFileSync } from 'fs'; -import { codeToANSI } from '@shikijs/cli'; import { stripVTControlCharacters as strip } from 'node:util' import colors from 'picocolors' @@ -55,9 +54,7 @@ async function getErrorFragment (info) { for (const [lineno, line] of filtered) { fragment += `${ lineno === (info.lineno - 1) ? colors.red('>') : ' ' - }${String(lineno).padStart(padding)} | ${ - await codeToANSI(line, 'typescript', 'slack-dark') - }` + }${String(lineno).padStart(padding)} | ${line}` if (lineno === (info.lineno - 1)) { const leftPadding = Math.max(0, (info.colno - 1) + padding + 5) fragment += `${new Array(leftPadding).fill('').join(' ')}${colors.red('^')}\n` From a130b77b82b6b6b5a4394a28a211be5dee5ad855 Mon Sep 17 00:00:00 2001 From: Jonas Galvez Date: Sun, 3 Aug 2025 19:22:25 -0300 Subject: [PATCH 3/4] use hot channel, fix stacktrace when sourcemap available --- packages/vite/src/client/client.ts | 19 ++--- .../vite/src/node/server/errorIngestion.ts | 53 ++++++++++++ packages/vite/src/node/server/index.ts | 20 +++-- .../node/server/middlewares/errorIngest.ts | 64 --------------- packages/vite/src/node/server/stacktrace.ts | 80 +++++++++++++++++++ packages/vite/src/node/server/ws.ts | 10 ++- packages/vite/src/node/stacktrace.ts | 0 7 files changed, 158 insertions(+), 88 deletions(-) create mode 100644 packages/vite/src/node/server/errorIngestion.ts delete mode 100644 packages/vite/src/node/server/middlewares/errorIngest.ts create mode 100644 packages/vite/src/node/server/stacktrace.ts create mode 100644 packages/vite/src/node/stacktrace.ts diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index 8c9d3705868071..fb7030fa10f850 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -110,18 +110,13 @@ if (typeof window !== 'undefined') { willUnload = true }) window.addEventListener('error', (error) => { - fetch('/@vite/errors', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - stack: error.error.stack, - filename: error.filename, - message: error.message, - colno: error.colno, - lineno: error.lineno, - }) + transport.send({ + type: 'browser-error', + stack: error.error.stack, + filename: error.filename, + message: error.message, + colno: error.colno, + lineno: error.lineno, }) }) } diff --git a/packages/vite/src/node/server/errorIngestion.ts b/packages/vite/src/node/server/errorIngestion.ts new file mode 100644 index 00000000000000..6646ae2046ff48 --- /dev/null +++ b/packages/vite/src/node/server/errorIngestion.ts @@ -0,0 +1,53 @@ +import path from 'node:path' +import repl from 'node:repl' +import { readFileSync } from 'fs'; + +import { stripVTControlCharacters as strip } from 'node:util' +import colors from 'picocolors' +import type { RollupError } from 'rollup' +import bodyParser from 'body-parser' +import type { Connect } from 'dep-types/connect' +import type { ErrorPayload } from 'types/hmrPayload' +import { pad } from '../../utils' +import type { ResolvedConfig } from '../..' +import { CLIENT_ERROR_RELAY_PATH } from '../../constants' + +export interface BrowserErrorInfo { + type: 'browser-error' + stack: string + lineno: number + colno: number + message: string +} + +export async function logBrowserError( + source: string, + error: BrowserErrorInfo, + stack: string, + config: ResolvedConfig +) { + const fragment = await getErrorFragment(error, source) + const msg = `${colors.magenta('[browser]')} ${colors.red(stack)}` + config.logger.error(`${msg}\n\n${fragment}`, { + clear: true, + timestamp: true, + }) +} + +async function getErrorFragment (error, source) { + const filtered = source.split(/\r?\n/g) + .map((line, i) => [i, line]) + .slice(Math.max(1, error.lineno - 2), error.lineno + 3) + let fragment = "" + let padding = String(error.lineno).length + for (const [lineno, line] of filtered) { + fragment += `${ + lineno === (error.lineno - 1) ? colors.red('>') : ' ' + }${String(lineno).padStart(padding)} | ${line}\n` + if (lineno === (error.lineno - 1)) { + const leftPadding = Math.max(0, (error.colno - 1) + padding + 5) + fragment += `${new Array(leftPadding).fill('').join(' ')}${colors.red('^')}\n` + } + } + return fragment +} diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 7cb00a1325ed66..aee567960716ec 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -93,7 +93,6 @@ import { ModuleGraph } from './mixedModuleGraph' import type { ModuleNode } from './mixedModuleGraph' import { notFoundMiddleware } from './middlewares/notFound' import { errorMiddleware } from './middlewares/error' -import { errorIngestMiddleware } from './middlewares/errorIngest' import type { HmrOptions, NormalizedHotChannel } from './hmr' import { handleHMRUpdate, updateModules } from './hmr' import { openBrowser as _openBrowser } from './openBrowser' @@ -483,7 +482,14 @@ export async function _createServer( ? null : await resolveHttpServer(serverConfig, middlewares, httpsOptions) - const ws = createWebSocketServer(httpServer, config, httpsOptions) + const environments: Record = {} + + let moduleGraph = new ModuleGraph({ + client: () => environments.client.moduleGraph, + ssr: () => environments.ssr.moduleGraph, + }) + + const ws = createWebSocketServer(httpServer, config, httpsOptions, moduleGraph) const publicFiles = await initPublicFilesPromise const { publicDir } = config @@ -510,7 +516,6 @@ export async function _createServer( ) as FSWatcher) : createNoopWatcher(resolvedWatchOptions) - const environments: Record = {} for (const [name, environmentOptions] of Object.entries( config.environments, @@ -531,10 +536,6 @@ export async function _createServer( // Backward compatibility - let moduleGraph = new ModuleGraph({ - client: () => environments.client.moduleGraph, - ssr: () => environments.ssr.moduleGraph, - }) let pluginContainer = createPluginContainer(environments) const closeHttpServer = createServerCloseFn(httpServer) @@ -911,9 +912,6 @@ export async function _createServer( // open in editor support middlewares.use('/__open-in-editor', launchEditorMiddleware()) - // error ingest handler - middlewares.use(errorIngestMiddleware(server)) - // ping request handler // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...` middlewares.use(function viteHMRPingMiddleware(req, res, next) { @@ -958,7 +956,7 @@ export async function _createServer( } // error handler - middlewares.use(errorIngestMiddleware(server)) + middlewares.use(errorMiddleware(server, !!middlewareMode)) // httpServer.listen can be called multiple times // when port when using next port number diff --git a/packages/vite/src/node/server/middlewares/errorIngest.ts b/packages/vite/src/node/server/middlewares/errorIngest.ts deleted file mode 100644 index 191b9b3105d045..00000000000000 --- a/packages/vite/src/node/server/middlewares/errorIngest.ts +++ /dev/null @@ -1,64 +0,0 @@ -import path from 'node:path' -import repl from 'node:repl' -import { readFileSync } from 'fs'; - -import { stripVTControlCharacters as strip } from 'node:util' -import colors from 'picocolors' -import type { RollupError } from 'rollup' -import bodyParser from 'body-parser' -import type { Connect } from 'dep-types/connect' -import type { ErrorPayload } from 'types/hmrPayload' -import { pad } from '../../utils' -import type { ViteDevServer } from '../..' -import { CLIENT_ERROR_RELAY_PATH } from '../../constants' - -export function logBrowserError( - server: ViteDevServer, - stack: string, - fragment: string -): void { - const msg = `${colors.magenta('[browser]')} ${colors.red(stack)}` - server.config.logger.error(`${msg}\n\n${fragment}`, { - clear: true, - timestamp: true, - }) -} - -export function errorIngestMiddleware( - server: ViteDevServer, -): Connect.ErrorHandleFunction { - const jsonParser = bodyParser.json() - return function viteErrorIngestMiddleware(req, res, next) { - if (req.url === CLIENT_ERROR_RELAY_PATH) { - jsonParser(req, res, async () => { - const fragment = await getErrorFragment(req.body) - logBrowserError(server, req.body.stack, fragment) - res.statusCode = 201 - res.end('') - }) - } else { - next() - } - } -} - -async function getErrorFragment (info) { - const res = await fetch(info.filename) - const source = await res.text() - const filtered = source.split(/\r?\n/g) - .map((line, i) => [i, line]) - .slice(Math.max(1, info.lineno - 2), info.lineno + 3) - .filter(([_,line]) => !line.startsWith('//# sourceMappingURL')) - let fragment = "" - let padding = String(info.lineno).length - for (const [lineno, line] of filtered) { - fragment += `${ - lineno === (info.lineno - 1) ? colors.red('>') : ' ' - }${String(lineno).padStart(padding)} | ${line}` - if (lineno === (info.lineno - 1)) { - const leftPadding = Math.max(0, (info.colno - 1) + padding + 5) - fragment += `${new Array(leftPadding).fill('').join(' ')}${colors.red('^')}\n` - } - } - return fragment -} diff --git a/packages/vite/src/node/server/stacktrace.ts b/packages/vite/src/node/server/stacktrace.ts new file mode 100644 index 00000000000000..29b2512247a1a0 --- /dev/null +++ b/packages/vite/src/node/server/stacktrace.ts @@ -0,0 +1,80 @@ +import path from 'node:path' +import { TraceMap, originalPositionFor } from '@jridgewell/trace-mapping' +import type { EnvironmentModuleGraph } from './server' + +let offset: number + +function calculateOffsetOnce() { + if (offset !== undefined) { + return + } + + try { + new Function('throw new Error(1)')() + } catch (e) { + // in Node 12, stack traces account for the function wrapper. + // in Node 13 and later, the function wrapper adds two lines, + // which must be subtracted to generate a valid mapping + const match = /:(\d+):\d+\)$/.exec(e.stack.split('\n')[1]) + offset = match ? +match[1] - 1 : 0 + } +} + + +export function browserRewriteStacktrace( + stack: string, + moduleGraph: EnvironmentModuleGraph, +): [string, string] { + let firstSource + const processedStack = stack + .split('\n') + .map((line) => { + return line.replace( + /^ {4}at (?:(\S.*?)\s\()?(.*?)\)$/, + (input, varName, url) => { + const { pathname } = new URL(url) + const [,id, line, column] = pathname.match(/^(.*?):(\d+):(\d+)$/) + if (!id) return input + + const mod = moduleGraph.urlToModuleMap.get(id) + const rawSource = mod?._clientModule.transformResult.code + if (!firstSource) { + firstSource = rawSource + } + const rawSourceMap = mod?._clientModule.transformResult?.map + const trimmedVarName = varName?.trim() + + if (!rawSourceMap) { + if (!trimmedVarName || trimmedVarName === 'eval') { + return ` at (${pathname})` + } else { + return ` at ${trimmedVarName} (${pathname})` + } + } + + const traced = new TraceMap(rawSourceMap as any) + + const pos = originalPositionFor(traced, { + line: Number(line) - offset, + // stacktrace's column is 1-indexed, but sourcemap's one is 0-indexed + column: Number(column) - 1, + }) + + if (!pos.source) { + return input + } + + const sourceFile = path.resolve(path.dirname(id), pos.source) + // stacktrace's column is 1-indexed, but sourcemap's one is 0-indexed + const source = `${sourceFile}:${pos.line}:${pos.column + 1}` + if (!trimmedVarName || trimmedVarName === 'eval') { + return ` at ${source}` + } else { + return ` at ${trimmedVarName} (${source})` + } + }, + ) + }) + .join('\n') + return [firstSource, processedStack] +} diff --git a/packages/vite/src/node/server/ws.ts b/packages/vite/src/node/server/ws.ts index 0a3f1d15c8b689..307416d73d205e 100644 --- a/packages/vite/src/node/server/ws.ts +++ b/packages/vite/src/node/server/ws.ts @@ -18,6 +18,9 @@ import { isObject } from '../utils' import type { NormalizedHotChannel, NormalizedHotChannelClient } from './hmr' import { normalizeHotChannel } from './hmr' import type { HttpServer } from '.' +import type { EnvironmentModuleGraph } from '..' +import { browserRewriteStacktrace } from './stacktrace' +import { logBrowserError } from './errorIngestion' /* In Bun, the `ws` module is overridden to hook into the native code. Using the bundled `js` version * of `ws` will not work as Bun's req.socket does not allow reading/writing to the underlying socket. @@ -116,6 +119,7 @@ export function createWebSocketServer( server: HttpServer | null, config: ResolvedConfig, httpsOptions?: HttpsServerOptions, + moduleGraph: EnvironmentModuleGraph, ): WebSocketServer { if (config.server.ws === false) { return { @@ -283,11 +287,15 @@ export function createWebSocketServer( wss.on('connection', (socket) => { socket.on('message', (raw) => { - if (!customListeners.size) return let parsed: any try { parsed = JSON.parse(String(raw)) } catch {} + if (parsed.type === 'browser-error') { + const [source, stack] = browserRewriteStacktrace(parsed.stack, moduleGraph) + logBrowserError(source, parsed, stack, config) + } + if (!customListeners.size) return if (!parsed || parsed.type !== 'custom' || !parsed.event) return const listeners = customListeners.get(parsed.event) if (!listeners?.size) return diff --git a/packages/vite/src/node/stacktrace.ts b/packages/vite/src/node/stacktrace.ts new file mode 100644 index 00000000000000..e69de29bb2d1d6 From 097c9f7826954184d744c9c543ca2a6a594fa5d0 Mon Sep 17 00:00:00 2001 From: Jonas Galvez Date: Sun, 3 Aug 2025 19:23:20 -0300 Subject: [PATCH 4/4] rm uneeded dependency --- packages/vite/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/vite/package.json b/packages/vite/package.json index 2e07b23e6458fa..7f1eeb5a73a9d4 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -81,7 +81,6 @@ }, "//": "READ CONTRIBUTING.md to understand what to put under deps vs. devDeps!", "dependencies": { - "body-parser": "^2.2.0", "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.3",