Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,46 @@ async function transformWithBabel(
// If no additional transformations are needed, return the data directly
if (plugins.length === 0) {
// Strip sourcemaps if they should not be used
return useInputSourcemap ? data : data.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, '');
if (!useInputSourcemap) {
return data.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, '');
}

// Inline any external sourceMappingURL so esbuild can chain through to the original source.
// When no Babel plugins run, external map references are preserved in the returned data but
// esbuild does not follow them. Converting to an inline base64 map allows esbuild to compose
// the full sourcemap chain from bundle output back to the original TypeScript source.
const externalMapMatch = /^\/\/# sourceMappingURL=(?!data:)([^\r\n]+)/m.exec(data);
if (externalMapMatch) {
const mapRef = externalMapMatch[1];
const fileDir = path.dirname(filename);
const mapPath = path.resolve(fileDir, mapRef);
// Reject path traversal — the resolved map file must remain within the source
// file's directory tree and must be a .map file. This prevents a crafted
// sourceMappingURL from reading arbitrary files from disk.
const fileDirPrefix = fileDir.endsWith(path.sep) ? fileDir : fileDir + path.sep;
if (!mapPath.startsWith(fileDirPrefix) || !mapPath.endsWith('.map')) {
return data;
}
try {
const mapContent = await fs.promises.readFile(mapPath, 'utf-8');
const inlineMap = Buffer.from(mapContent).toString('base64');
// Strip ALL sourceMappingURL comments before appending the composed inline map.
// When allowJs + inlineSourceMap are enabled, the TypeScript compiler preserves
// the original external reference AND appends its own data: inline sourcemap.
// esbuild uses the last comment, so leaving both would cause it to follow the
// TS-generated map (which only traces back to the compiled JS, not TypeScript).
const stripped = data.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, '');
return stripped.trimEnd() + '\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,' + inlineMap + '\n';
} catch (error) {
// Map file not readable; return data with the original external reference
// eslint-disable-next-line no-console
console.warn(
`Unable to inline sourcemap for '${filename}': ${error instanceof Error ? error.message : error}`,
);
}
}

return data;
}

const result = await transformAsync(data, {
Expand Down
49 changes: 45 additions & 4 deletions packages/angular/build/src/tools/esbuild/javascript-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import { createHash } from 'node:crypto';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { IMPORT_EXEC_ARGV } from '../../utils/server-rendering/esm-in-memory-loader/utils';
import { WorkerPool, WorkerPoolOptions } from '../../utils/worker-pool';
import { Cache } from './cache';
Expand Down Expand Up @@ -165,10 +166,50 @@ export class JavaScriptTransformer {
this.#commonOptions.sourcemap &&
(!!this.#commonOptions.thirdPartySourcemaps || !/[\\/]node_modules[\\/]/.test(filename));

return Buffer.from(
keepSourcemap ? data : data.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, ''),
'utf-8',
);
if (!keepSourcemap) {
return Buffer.from(data.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, ''), 'utf-8');
}

// Inline any external sourceMappingURL so esbuild can chain through to the original source.
// When no Babel plugins run, external map references are preserved in the returned data but
// esbuild does not follow them. Converting to an inline base64 map allows esbuild to compose
// the full sourcemap chain from bundle output back to the original TypeScript source.
const externalMapMatch = /^\/\/# sourceMappingURL=(?!data:)([^\r\n]+)/m.exec(data);
if (externalMapMatch) {
const mapRef = externalMapMatch[1];
const fileDir = path.dirname(filename);
const mapPath = path.resolve(fileDir, mapRef);
// Reject path traversal — the resolved map file must remain within the source
// file's directory tree and must be a .map file. This prevents a crafted
// sourceMappingURL from reading arbitrary files from disk.
const fileDirPrefix = fileDir.endsWith(path.sep) ? fileDir : fileDir + path.sep;
if (mapPath.startsWith(fileDirPrefix) && mapPath.endsWith('.map')) {
try {
const mapContent = await readFile(mapPath, 'utf-8');
const inlineMap = Buffer.from(mapContent).toString('base64');
// Strip ALL sourceMappingURL comments before appending the composed inline map.
// When allowJs + inlineSourceMap are enabled, the TypeScript compiler preserves
// the original external reference AND appends its own data: inline sourcemap.
// esbuild uses the last comment, so leaving both would cause it to follow the
// TS-generated map (which only traces back to the compiled JS, not TypeScript).
const stripped = data.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, '');
const result =
stripped.trimEnd() +
'\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,' +
inlineMap +
'\n';
return Buffer.from(result, 'utf-8');
} catch (error) {
// Map file not readable; return data with the original external reference
// eslint-disable-next-line no-console
console.warn(
`Unable to inline sourcemap for '${filename}': ${error instanceof Error ? error.message : error}`,
);
}
}
}

return Buffer.from(data, 'utf-8');
}

return this.#ensureWorkerPool().run({
Expand Down
Loading