|
1 | 1 | import fs from 'fs' |
2 | 2 | import path from 'path' |
3 | | -import { pathToFileURL } from 'url' |
| 3 | +import { pathToFileURL, fileURLToPath } from 'url' |
| 4 | +import { TraceMap, originalPositionFor } from '@jridgewell/trace-mapping' |
4 | 5 |
|
5 | 6 | /** |
6 | 7 | * Load tsconfig.json if it exists |
@@ -65,26 +66,38 @@ function resolveTsPathAlias(importPath, tsConfig, configDir) { |
65 | 66 | * @returns {Promise<{tempFile: string, allTempFiles: string[], fileMapping: any}>} - Main temp file and all temp files created |
66 | 67 | */ |
67 | 68 | export async function transpileTypeScript(mainFilePath, typescript) { |
68 | | - const { transpile } = typescript |
69 | | - |
70 | 69 | /** |
71 | 70 | * Transpile a single TypeScript file to JavaScript |
72 | 71 | * Injects CommonJS shims (require, module, exports, __dirname, __filename) as needed |
73 | 72 | */ |
74 | 73 | const transpileTS = (filePath) => { |
75 | 74 | const tsContent = fs.readFileSync(filePath, 'utf8') |
76 | 75 |
|
77 | | - // Transpile TypeScript to JavaScript with ES module output |
78 | | - let jsContent = transpile(tsContent, { |
79 | | - module: 99, // ModuleKind.ESNext |
80 | | - target: 99, // ScriptTarget.ESNext |
81 | | - esModuleInterop: true, |
82 | | - allowSyntheticDefaultImports: true, |
83 | | - lib: ['lib.esnext.d.ts'], // Enable latest features including top-level await |
84 | | - suppressOutputPathCheck: true, |
85 | | - skipLibCheck: true, |
| 76 | + // Transpile TypeScript to JavaScript with ES module output. |
| 77 | + // transpileModule (unlike transpile) can emit a source map, which we inline |
| 78 | + // so stack frames can later be mapped back to the original .ts positions. |
| 79 | + const { outputText, sourceMapText } = typescript.transpileModule(tsContent, { |
| 80 | + compilerOptions: { |
| 81 | + module: 99, // ModuleKind.ESNext |
| 82 | + target: 99, // ScriptTarget.ESNext |
| 83 | + esModuleInterop: true, |
| 84 | + allowSyntheticDefaultImports: true, |
| 85 | + lib: ['lib.esnext.d.ts'], // Enable latest features including top-level await |
| 86 | + suppressOutputPathCheck: true, |
| 87 | + skipLibCheck: true, |
| 88 | + sourceMap: true, |
| 89 | + inlineSources: true, |
| 90 | + }, |
| 91 | + fileName: path.resolve(filePath), |
86 | 92 | }) |
87 | 93 |
|
| 94 | + // transpileModule appends an external sourceMappingURL comment; we inline the map instead |
| 95 | + let jsContent = outputText.replace(/\/\/# sourceMappingURL=.*(\r?\n)?$/, '') |
| 96 | + const sourceMap = JSON.parse(sourceMapText) |
| 97 | + // transpileModule records only the base file name; store the absolute path |
| 98 | + // so mapped stack frames can be resolved back to the source file |
| 99 | + sourceMap.sources = [path.resolve(filePath)] |
| 100 | + |
88 | 101 | // Check if the code uses CommonJS globals |
89 | 102 | const usesCommonJSGlobals = /__dirname|__filename/.test(jsContent) |
90 | 103 | const usesRequire = /\brequire\s*\(/.test(jsContent) |
@@ -150,12 +163,22 @@ const __dirname = __dirname_fn(__filename); |
150 | 163 |
|
151 | 164 | jsContent = esmGlobals + jsContent |
152 | 165 |
|
| 166 | + // The injected shim shifts every generated line down; reflect that in the |
| 167 | + // source map by prepending one empty mapping entry per shim line. |
| 168 | + const shimLineCount = esmGlobals.split('\n').length - 1 |
| 169 | + sourceMap.mappings = `${';'.repeat(shimLineCount)}${sourceMap.mappings}` |
| 170 | + |
153 | 171 | // If module.exports is used, we need to export it as default |
154 | 172 | if (usesModuleExports) { |
155 | 173 | jsContent += `\nexport default module.exports;\n` |
156 | 174 | } |
157 | 175 | } |
158 | 176 |
|
| 177 | + // Append the source map inline so stack frames in the temp file can be |
| 178 | + // mapped back to the original .ts positions (see mapTranspiledStackFrame) |
| 179 | + const inlineMap = Buffer.from(JSON.stringify(sourceMap), 'utf8').toString('base64') |
| 180 | + jsContent += `//# sourceMappingURL=data:application/json;charset=utf-8;base64,${inlineMap}\n` |
| 181 | + |
159 | 182 | return jsContent |
160 | 183 | } |
161 | 184 |
|
@@ -407,6 +430,50 @@ const __dirname = __dirname_fn(__filename); |
407 | 430 | return { tempFile: tempFileUrl, allTempFiles, fileMapping: transpiledFiles } |
408 | 431 | } |
409 | 432 |
|
| 433 | +const sourceMapCache = new Map() |
| 434 | + |
| 435 | +function getInlineSourceMap(tempFilePath) { |
| 436 | + if (sourceMapCache.has(tempFilePath)) return sourceMapCache.get(tempFilePath) |
| 437 | + |
| 438 | + let traceMap = null |
| 439 | + try { |
| 440 | + const content = fs.readFileSync(tempFilePath, 'utf8') |
| 441 | + const match = /\/\/# sourceMappingURL=data:application\/json;charset=utf-8;base64,([A-Za-z0-9+/=]+)\s*$/.exec(content) |
| 442 | + if (match) { |
| 443 | + traceMap = new TraceMap(JSON.parse(Buffer.from(match[1], 'base64').toString('utf8'))) |
| 444 | + } |
| 445 | + } catch { |
| 446 | + // No usable inline source map; callers fall back to path-only mapping |
| 447 | + } |
| 448 | + |
| 449 | + sourceMapCache.set(tempFilePath, traceMap) |
| 450 | + return traceMap |
| 451 | +} |
| 452 | + |
| 453 | +/** |
| 454 | + * Map a stack frame pointing into a transpiled `.temp.mjs` file back to the |
| 455 | + * original TypeScript source position, using the inline source map emitted |
| 456 | + * during transpilation. |
| 457 | + * |
| 458 | + * @param {string} frame - Single stack frame line, e.g. `at Object.open (file:///path/foo.123.temp.mjs:4:11)` |
| 459 | + * @returns {string|null} - Rewritten frame with the original source position, or null when mapping is not possible |
| 460 | + */ |
| 461 | +export function mapTranspiledStackFrame(frame) { |
| 462 | + const match = /\((file:\/\/)?([^()]*?\.temp\.mjs):(\d+):(\d+)\)/.exec(frame) |
| 463 | + if (!match) return null |
| 464 | + |
| 465 | + const [, isFileUrl, fileRef, genLine, genColumn] = match |
| 466 | + const tempFilePath = isFileUrl ? fileURLToPath(`${isFileUrl}${fileRef}`) : fileRef |
| 467 | + const traceMap = getInlineSourceMap(tempFilePath) |
| 468 | + if (!traceMap) return null |
| 469 | + |
| 470 | + // V8 reports 1-based columns, trace-mapping expects 0-based |
| 471 | + const pos = originalPositionFor(traceMap, { line: Number(genLine), column: Number(genColumn) - 1 }) |
| 472 | + if (!pos || pos.line == null || !pos.source) return null |
| 473 | + |
| 474 | + return frame.replace(match[0], `(${pos.source}:${pos.line}:${pos.column + 1})`) |
| 475 | +} |
| 476 | + |
410 | 477 | /** |
411 | 478 | * Map error stack traces from temp .mjs files back to original .ts files |
412 | 479 | * @param {Error} error - The error object to fix |
|
0 commit comments