From ee1323f96580514902508c96536ae989a1ffc853 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 4 Aug 2026 04:42:40 +0000 Subject: [PATCH 1/2] refactor: use node:module SourceMap instead of source-map-js --- packages/nuxt-cli/package.json | 1 - packages/nuxt-cli/src/dev/error.ts | 20 ++++----- packages/nuxt-cli/test/unit/errors.spec.ts | 48 +++++++++++++++++++++- packages/nuxt-cli/tsdown.config.ts | 2 +- pnpm-lock.yaml | 3 -- 5 files changed, 57 insertions(+), 17 deletions(-) diff --git a/packages/nuxt-cli/package.json b/packages/nuxt-cli/package.json index ecd87e622..348f51d84 100644 --- a/packages/nuxt-cli/package.json +++ b/packages/nuxt-cli/package.json @@ -77,7 +77,6 @@ "pkg-types": "^2.3.1", "rc9": "^3.0.1", "scule": "^1.3.0", - "source-map-js": "^1.2.1", "srvx": "^0.12.5", "std-env": "^4.2.0", "tinyclip": "^1.0.1", diff --git a/packages/nuxt-cli/src/dev/error.ts b/packages/nuxt-cli/src/dev/error.ts index 538e5a996..1e955b9b7 100644 --- a/packages/nuxt-cli/src/dev/error.ts +++ b/packages/nuxt-cli/src/dev/error.ts @@ -2,10 +2,10 @@ import type { IncomingMessage, ServerResponse } from 'node:http' import type { SourceLoader, StackFrame } from 'youch-core/types' import { readFile } from 'node:fs/promises' +import { SourceMap } from 'node:module' import process from 'node:process' import { dirname, normalize, resolve } from 'pathe' -import { SourceMapConsumer } from 'source-map-js' import { Youch } from 'youch' import { ErrorParser } from 'youch-core' @@ -87,20 +87,18 @@ const sourceLoader: SourceLoader = async (frame) => { * Rewrite a frame to its original position. Isolated per frame so a malformed * `.map` costs only that frame's mapping rather than the whole stack. */ -async function applySourceMap(frame: StackFrame): Promise { +export async function applySourceMap(frame: StackFrame): Promise { const rawSourceMap = await readFile(`${frame.fileName}.map`, 'utf8').catch(() => undefined) if (!rawSourceMap) { return } - const consumer = new SourceMapConsumer(JSON.parse(rawSourceMap)) - const originalPosition = consumer.originalPositionFor({ - line: frame.lineNumber!, - column: frame.columnNumber!, - }) - if (originalPosition.source && originalPosition.line) { - frame.fileName = resolve(dirname(frame.fileName!), originalPosition.source) - frame.lineNumber = originalPosition.line - frame.columnNumber = originalPosition.column || 0 + const payload = JSON.parse(rawSourceMap) + const entry = new SourceMap(payload).findEntry(frame.lineNumber! - 1, frame.columnNumber!) + if ('originalSource' in entry && entry.originalSource !== undefined && entry.originalLine !== undefined) { + const source = payload.sourceRoot ? `${payload.sourceRoot.replace(/\/?$/, '/')}${entry.originalSource}` : entry.originalSource + frame.fileName = resolve(dirname(frame.fileName!), source) + frame.lineNumber = entry.originalLine + 1 + frame.columnNumber = entry.originalColumn || 0 } } diff --git a/packages/nuxt-cli/test/unit/errors.spec.ts b/packages/nuxt-cli/test/unit/errors.spec.ts index 233cc1e1b..c8973fc40 100644 --- a/packages/nuxt-cli/test/unit/errors.spec.ts +++ b/packages/nuxt-cli/test/unit/errors.spec.ts @@ -1,6 +1,11 @@ +import type { StackFrame } from 'youch-core/types' + +import { mkdtemp, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' import { describe, expect, it } from 'vitest' -import { stripCwd } from '../../src/dev/error' +import { applySourceMap, stripCwd } from '../../src/dev/error' import { isRemotePeerError } from '../../src/utils/errors' describe('isRemotePeerError', () => { @@ -34,3 +39,44 @@ describe('stripCwd', () => { expect(stripCwd('at /elsewhere/app/index.vue:1:1', '/home/me/app')).toBe('at /elsewhere/app/index.vue:1:1') }) }) + +describe('applySourceMap', () => { + const mappings = 'AAAA,SAAS,IAAI;EACX,OAAO,CAAC;AACV' + + async function withMap(map: Record, frame: Partial) { + const dir = await mkdtemp(join(tmpdir(), 'nuxi-sourcemap-')) + const file = join(dir, 'out.mjs') + await writeFile(file, 'export const noop = () => {}\n') + await writeFile(`${file}.map`, JSON.stringify(map)) + const resolved = { fileName: file, ...frame } as StackFrame + await applySourceMap(resolved) + return resolved + } + + it('should rewrite a frame to its original position', async () => { + const frame = await withMap( + { version: 3, sources: ['src/foo.ts'], names: [], mappings }, + { lineNumber: 2, columnNumber: 2 }, + ) + expect(frame.fileName?.endsWith(join('src', 'foo.ts'))).toBe(true) + expect(frame.lineNumber).toBe(2) + expect(frame.columnNumber).toBe(2) + }) + + it('should resolve sources against `sourceRoot`', async () => { + const frame = await withMap( + { version: 3, sourceRoot: '../src', sources: ['foo.ts'], names: [], mappings }, + { lineNumber: 2, columnNumber: 2 }, + ) + expect(frame.fileName?.endsWith(join('src', 'foo.ts'))).toBe(true) + }) + + it('should leave a frame with no mapping untouched', async () => { + const frame = await withMap( + { version: 3, sources: ['src/foo.ts'], names: [], mappings: '' }, + { lineNumber: 4, columnNumber: 0 }, + ) + expect(frame.lineNumber).toBe(4) + expect(frame.fileName?.endsWith('out.mjs')).toBe(true) + }) +}) diff --git a/packages/nuxt-cli/tsdown.config.ts b/packages/nuxt-cli/tsdown.config.ts index 47e132ddb..9d438579c 100644 --- a/packages/nuxt-cli/tsdown.config.ts +++ b/packages/nuxt-cli/tsdown.config.ts @@ -6,7 +6,7 @@ export const packaging: PackagingContract = { external: PARSER_SPECIFIERS, lazy: { 'dist/index.mjs': ['rc9'], - 'dist/dev/index.mjs': ['youch', 'youch-core', 'source-map-js', 'rc9'], + 'dist/dev/index.mjs': ['youch', 'youch-core', 'rc9'], }, } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 89940e700..4433a85e4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -235,9 +235,6 @@ importers: serve: specifier: ^14.2.6 version: 14.2.6(supports-color@10.2.2) - source-map-js: - specifier: ^1.2.1 - version: 1.2.1 srvx: specifier: ^0.12.5 version: 0.12.5 From aba4fb71edddd49a0aa3f4d78704d121d0dacef9 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 4 Aug 2026 12:27:46 +0200 Subject: [PATCH 2/2] test: adapt test for windows --- packages/nuxt-cli/test/unit/errors.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nuxt-cli/test/unit/errors.spec.ts b/packages/nuxt-cli/test/unit/errors.spec.ts index c8973fc40..5730f228a 100644 --- a/packages/nuxt-cli/test/unit/errors.spec.ts +++ b/packages/nuxt-cli/test/unit/errors.spec.ts @@ -58,7 +58,7 @@ describe('applySourceMap', () => { { version: 3, sources: ['src/foo.ts'], names: [], mappings }, { lineNumber: 2, columnNumber: 2 }, ) - expect(frame.fileName?.endsWith(join('src', 'foo.ts'))).toBe(true) + expect(frame.fileName?.endsWith('src/foo.ts')).toBe(true) expect(frame.lineNumber).toBe(2) expect(frame.columnNumber).toBe(2) }) @@ -68,7 +68,7 @@ describe('applySourceMap', () => { { version: 3, sourceRoot: '../src', sources: ['foo.ts'], names: [], mappings }, { lineNumber: 2, columnNumber: 2 }, ) - expect(frame.fileName?.endsWith(join('src', 'foo.ts'))).toBe(true) + expect(frame.fileName?.endsWith('src/foo.ts')).toBe(true) }) it('should leave a frame with no mapping untouched', async () => {