Skip to content

Commit 4fc722c

Browse files
committed
fix(@angular/build): inline external sourcemaps for workspace library files
When no Babel plugins are required, the JavaScript transformer returns library files as-is, preserving the comment but never reading the referenced map file from disk. esbuild does not follow external sourcemap links in input files, so the chain from bundled output back to the original TypeScript source is never formed. Read the external map file and return an inline base64 sourcemap instead. esbuild processes inline sourcemaps from input files correctly, allowing it to compose the full sourcemap chain through to the original TypeScript source.
1 parent 98450e1 commit 4fc722c

File tree

1 file changed

+37
-1
lines changed

1 file changed

+37
-1
lines changed

packages/angular/build/src/tools/esbuild/javascript-transformer-worker.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,43 @@ async function transformWithBabel(
9595
// If no additional transformations are needed, return the data directly
9696
if (plugins.length === 0) {
9797
// Strip sourcemaps if they should not be used
98-
return useInputSourcemap ? data : data.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, '');
98+
if (!useInputSourcemap) {
99+
return data.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, '');
100+
}
101+
102+
// Inline any external sourceMappingURL so esbuild can chain through to the original source.
103+
// When no Babel plugins run, external map references are preserved in the returned data but
104+
// esbuild does not follow them. Converting to an inline base64 map allows esbuild to compose
105+
// the full sourcemap chain from bundle output back to the original TypeScript source.
106+
const externalMapMatch = /^\/\/# sourceMappingURL=(?!data:)([^\r\n]+)/m.exec(data);
107+
if (externalMapMatch) {
108+
const mapRef = externalMapMatch[1];
109+
const fileDir = path.dirname(filename);
110+
const mapPath = path.resolve(fileDir, mapRef);
111+
// Reject path traversal — the resolved map file must remain within the source
112+
// file's directory tree and must be a .map file. This prevents a crafted
113+
// sourceMappingURL from reading arbitrary files from disk.
114+
const fileDirPrefix = fileDir.endsWith(path.sep) ? fileDir : fileDir + path.sep;
115+
if (!mapPath.startsWith(fileDirPrefix) || !mapPath.endsWith('.map')) {
116+
return data;
117+
}
118+
try {
119+
const mapContent = await fs.promises.readFile(mapPath, 'utf-8');
120+
const inlineMap = Buffer.from(mapContent).toString('base64');
121+
return data.replace(
122+
/^\/\/# sourceMappingURL=[^\r\n]*/m,
123+
`//# sourceMappingURL=data:application/json;charset=utf-8;base64,${inlineMap}`,
124+
);
125+
} catch (error) {
126+
// Map file not readable; return data with the original external reference
127+
// eslint-disable-next-line no-console
128+
console.warn(
129+
`Unable to inline sourcemap for '${filename}': ${error instanceof Error ? error.message : error}`,
130+
);
131+
}
132+
}
133+
134+
return data;
99135
}
100136

101137
const result = await transformAsync(data, {

0 commit comments

Comments
 (0)