Skip to content

Commit 5194b41

Browse files
committed
fix(@angular/build): prevent stripping nested sourceMappingURL comments
When compiling files, the dev server and transformers strip original `//# sourceMappingURL=` comments. However, using a naive RegExp like `/^\/\/# sourceMappingURL=[^\r\n]*/gm` matches the comment even when it is embedded inside multiline template literals or string literals (such as inline web worker code), which causes template literals to become unterminated and fail compilation. This introduces a robust, lexer-style utility `removeSourceMappingURL` that ignores occurrences of the comment when they appear inside single/double quotes, template literals, or block comments, preventing syntax errors in compiled chunks.
1 parent 035c72a commit 5194b41

6 files changed

Lines changed: 121 additions & 11 deletions

File tree

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

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import fs from 'node:fs';
1111
import { createRequire } from 'node:module';
1212
import path from 'node:path';
1313
import Piscina from 'piscina';
14+
import { removeSourceMappingURL } from '../../utils/source-map';
1415

1516
interface JavaScriptTransformRequest {
1617
filename: string;
@@ -51,8 +52,7 @@ export default async function transformJavaScript(
5152
* Cached instance of the compiler-cli linker's createEs2015LinkerPlugin function.
5253
*/
5354
let linkerPluginCreator:
54-
| typeof import('@angular/compiler-cli/linker/babel').createEs2015LinkerPlugin
55-
| undefined;
55+
typeof import('@angular/compiler-cli/linker/babel').createEs2015LinkerPlugin | undefined;
5656

5757
async function transformWithBabel(
5858
filename: string,
@@ -119,7 +119,7 @@ async function transformWithBabel(
119119
// If no additional transformations are needed, return the data directly
120120
if (plugins.length === 0) {
121121
// Strip sourcemaps if they should not be used
122-
return useInputSourcemap ? data : data.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, '');
122+
return useInputSourcemap ? data : removeSourceMappingURL(data);
123123
}
124124

125125
const result = await transformAsync(data, {
@@ -137,9 +137,7 @@ async function transformWithBabel(
137137

138138
// Strip sourcemaps if they should not be used.
139139
// Babel will keep the original comments even if sourcemaps are disabled.
140-
return useInputSourcemap
141-
? outputCode
142-
: outputCode.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, '');
140+
return useInputSourcemap ? outputCode : removeSourceMappingURL(outputCode);
143141
}
144142

145143
async function requiresLinking(path: string, source: string): Promise<boolean> {

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import { createHash } from 'node:crypto';
1010
import { readFile } from 'node:fs/promises';
1111
import { IMPORT_EXEC_ARGV } from '../../utils/server-rendering/esm-in-memory-loader/utils';
12+
import { removeSourceMappingURL } from '../../utils/source-map';
1213
import { WorkerPool, WorkerPoolOptions } from '../../utils/worker-pool';
1314
import { Cache } from './cache';
1415

@@ -205,10 +206,7 @@ export class JavaScriptTransformer {
205206
this.#commonOptions.sourcemap &&
206207
(!!this.#commonOptions.thirdPartySourcemaps || !/[\\/]node_modules[\\/]/.test(filename));
207208

208-
return Buffer.from(
209-
keepSourcemap ? data : data.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, ''),
210-
'utf-8',
211-
);
209+
return Buffer.from(keepSourcemap ? data : removeSourceMappingURL(data), 'utf-8');
212210
}
213211

214212
return this.#runWithThrottle(() =>

packages/angular/build/src/tools/vite/plugins/angular-memory-plugin.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { fileURLToPath } from 'node:url';
1313
import type * as Vite from 'vite' with {
1414
'resolution-mode': 'import',
1515
};
16+
import { removeSourceMappingURL } from '../../../utils/source-map';
1617
import { AngularMemoryOutputFiles } from '../utils';
1718

1819
interface AngularMemoryPluginOptions {
@@ -104,7 +105,7 @@ export async function createAngularMemoryPlugin(
104105
return {
105106
// Remove source map URL comments from the code if a sourcemap is present.
106107
// Vite will inline and add an additional sourcemap URL for the sourcemap.
107-
code: mapContents ? code.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, '') : code,
108+
code: mapContents ? removeSourceMappingURL(code) : code,
108109
map: mapContents && Buffer.from(mapContents).toString('utf-8'),
109110
};
110111
},

packages/angular/build/src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ export * from './normalize-asset-patterns';
1010
export * from './normalize-optimization';
1111
export * from './normalize-source-maps';
1212
export * from './load-proxy-config';
13+
export * from './source-map';
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
// A regular expression that matches string literals, template literals, block comments,
10+
// and normal comments, to skip them, and matches/captures the target sourcemap comments
11+
// to safely remove only the top-level ones.
12+
const SOURCEMAP_REMOVAL_REGEX =
13+
// eslint-disable-next-line max-len
14+
/(?:"(?:[^"\\]|\\.)*")|(?:'(?:[^'\\]|\\.)*')|(?:`(?:[^`\\]|\\.)*`)|(?:\/\*[\s\S]*?\*\/)|((?<!\\)\/\/# sourceMappingURL=[^\r\n]*)|(?:\/\/[^\r\n]*)/g;
15+
16+
/**
17+
* Removes `//# sourceMappingURL=` comments safely from the given JavaScript code,
18+
* ignoring any occurrences that are inside string literals, template literals, or block comments.
19+
*
20+
* @param code The JavaScript source code.
21+
* @returns The code with top-level sourcemap comments removed.
22+
*/
23+
export function removeSourceMappingURL(code: string): string {
24+
return code.replace(SOURCEMAP_REMOVAL_REGEX, (match, sourcemap) => {
25+
return sourcemap ? '' : match;
26+
});
27+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { removeSourceMappingURL } from './source-map';
10+
11+
describe('removeSourceMappingURL', () => {
12+
it('should remove top-level sourcemap comments', () => {
13+
const code = 'console.log("hello");\n//# sourceMappingURL=main.js.map';
14+
expect(removeSourceMappingURL(code)).toBe('console.log("hello");\n');
15+
});
16+
17+
it('should not remove sourcemap comments inside double-quoted strings', () => {
18+
const code = 'const str = "//# sourceMappingURL=inline.js.map";';
19+
expect(removeSourceMappingURL(code)).toBe(code);
20+
});
21+
22+
it('should not remove sourcemap comments inside single-quoted strings', () => {
23+
const code = "const str = '//# sourceMappingURL=inline.js.map';";
24+
expect(removeSourceMappingURL(code)).toBe(code);
25+
});
26+
27+
it('should not remove sourcemap comments inside template literals', () => {
28+
const code = 'const str = `\n//# sourceMappingURL=inline.js.map\n`;';
29+
expect(removeSourceMappingURL(code)).toBe(code);
30+
});
31+
32+
it('should not remove sourcemap comments inside block comments', () => {
33+
const code = '/*\n//# sourceMappingURL=inline.js.map\n*/';
34+
expect(removeSourceMappingURL(code)).toBe(code);
35+
});
36+
37+
it('should not remove sourcemap comments inside normal single-line comments', () => {
38+
const code = '// Some description of //# sourceMappingURL=inline.js.map';
39+
expect(removeSourceMappingURL(code)).toBe(code);
40+
});
41+
42+
it('should remove multiple top-level sourcemap comments', () => {
43+
const code =
44+
'//# sourceMappingURL=first.js.map\nconsole.log("mid");\n//# sourceMappingURL=second.js.map';
45+
expect(removeSourceMappingURL(code)).toBe('\nconsole.log("mid");\n');
46+
});
47+
48+
it('should not remove sourcemap comments inside strings containing escaped quotes', () => {
49+
const codeDouble = 'const str = "escaped \\" //# sourceMappingURL=inline.js.map";';
50+
expect(removeSourceMappingURL(codeDouble)).toBe(codeDouble);
51+
52+
const codeSingle = "const str = 'escaped \\' //# sourceMappingURL=inline.js.map';";
53+
expect(removeSourceMappingURL(codeSingle)).toBe(codeSingle);
54+
});
55+
56+
it('should handle strings containing escaped backslashes correctly', () => {
57+
const code = 'const str = "backslash \\\\";\n//# sourceMappingURL=main.js.map';
58+
expect(removeSourceMappingURL(code)).toBe('const str = "backslash \\\\";\n');
59+
});
60+
61+
it('should not remove sourcemap comments inside template literal interpolations', () => {
62+
const code = 'const str = `hello ${"//# sourceMappingURL=inline.js.map"} world`;';
63+
expect(removeSourceMappingURL(code)).toBe(code);
64+
});
65+
66+
it('should not remove sourcemap comments inside nested template literals', () => {
67+
const code = 'const str = `nested ${`inner ${"//# sourceMappingURL=inline.js.map"}`}`;';
68+
expect(removeSourceMappingURL(code)).toBe(code);
69+
});
70+
71+
it('should not remove sourcemap comments inside regex literals', () => {
72+
const code = 'const regex = /\\/\\/# sourceMappingURL=inline.js.map/;';
73+
expect(removeSourceMappingURL(code)).toBe(code);
74+
});
75+
76+
it('should not affect normal division operators', () => {
77+
const code = 'const ratio = 10 / 2;\n//# sourceMappingURL=main.js.map';
78+
expect(removeSourceMappingURL(code)).toBe('const ratio = 10 / 2;\n');
79+
});
80+
81+
it('should only remove exact sourceMappingURL prefix comments', () => {
82+
const code = '// # sourceMappingURL=main.js.map\n//# sourceMappingURL=main.js.map';
83+
expect(removeSourceMappingURL(code)).toBe('// # sourceMappingURL=main.js.map\n');
84+
});
85+
});

0 commit comments

Comments
 (0)