From 846fa9497a802a2dbd43c6db0d0f487b548ea48a Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 15 Jul 2026 10:13:04 -0400 Subject: [PATCH] fix(@angular/build): anchor debug ID comment matching and make injection idempotent Anchor the DEBUG_ID_COMMENT regex to the end of the file to prevent it from matching and replacing comments nested inside string literals or template literals. Introduce the stripDebugIdFromSourceMap utility to clean source map text before hashing, making injectDebugIds idempotent in-memory. --- .../builders/application/inject-debug-ids.ts | 7 +++- packages/angular/build/src/utils/debug-id.ts | 14 +++++-- .../angular/build/src/utils/debug-id_spec.ts | 41 ++++++++++++++++++- 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/packages/angular/build/src/builders/application/inject-debug-ids.ts b/packages/angular/build/src/builders/application/inject-debug-ids.ts index 61c7defb4fd0..59dc9c76f3af 100644 --- a/packages/angular/build/src/builders/application/inject-debug-ids.ts +++ b/packages/angular/build/src/builders/application/inject-debug-ids.ts @@ -11,6 +11,7 @@ import { generateDebugId, injectDebugIdIntoJs, injectDebugIdIntoSourceMap, + stripDebugIdFromSourceMap, } from '../../utils/debug-id'; /** @@ -42,8 +43,10 @@ export function injectDebugIds(outputFiles: BuildOutputFile[]): void { continue; } - const id = generateDebugId(map.contents); + const mapText = map.text; + const mapTextForHash = stripDebugIdFromSourceMap(mapText); + const id = generateDebugId(mapTextForHash); file.contents = encoder.encode(injectDebugIdIntoJs(file.text, id)); - map.contents = encoder.encode(injectDebugIdIntoSourceMap(map.text, id)); + map.contents = encoder.encode(injectDebugIdIntoSourceMap(mapText, id)); } } diff --git a/packages/angular/build/src/utils/debug-id.ts b/packages/angular/build/src/utils/debug-id.ts index 5093c293269a..a03fd571ec72 100644 --- a/packages/angular/build/src/utils/debug-id.ts +++ b/packages/angular/build/src/utils/debug-id.ts @@ -40,8 +40,8 @@ export function generateDebugId(name: string | Uint8Array): string { return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`; } -/** Pattern matching an existing `//# debugId=` comment anywhere in the file. */ -const DEBUG_ID_COMMENT = /\n\/\/# debugId=[^\r\n]*\n?/; +/** Pattern matching an existing `//# debugId=` comment at the end of the file. */ +const DEBUG_ID_COMMENT = /(\n\/\/# debugId=[^\r\n]*)(\n\/\/# sourceMappingURL=[^\r\n]*)?(\s*)$/; /** Pattern matching the `//# sourceMappingURL=` comment, used to position the debug-id line. */ const SOURCE_MAPPING_URL_COMMENT = /\n\/\/# sourceMappingURL=[^\r\n]*\s*$/; @@ -58,7 +58,7 @@ export function injectDebugIdIntoJs(text: string, id: string): string { // Replace any existing debugId comment to keep the operation idempotent. if (DEBUG_ID_COMMENT.test(text)) { - return text.replace(DEBUG_ID_COMMENT, `\n${comment}\n`); + return text.replace(DEBUG_ID_COMMENT, (_, p1, p2, p3) => `\n${comment}${p2 || ''}${p3 || ''}`); } if (SOURCE_MAPPING_URL_COMMENT.test(text)) { @@ -96,3 +96,11 @@ export function injectDebugIdIntoSourceMap(json: string, id: string): string { return JSON.stringify(parsed, null, indent); } + +/** + * Strips any existing `debugId` field from the source map JSON string to restore + * the original JSON contents for deterministic/idempotent hashing. + */ +export function stripDebugIdFromSourceMap(json: string): string { + return json.replace(/,\s*"debugId"\s*:\s*"[^"]*"|\s*"debugId"\s*:\s*"[^"]*"\s*,?/g, ''); +} diff --git a/packages/angular/build/src/utils/debug-id_spec.ts b/packages/angular/build/src/utils/debug-id_spec.ts index d5e833d6a903..f254e28f6eee 100644 --- a/packages/angular/build/src/utils/debug-id_spec.ts +++ b/packages/angular/build/src/utils/debug-id_spec.ts @@ -6,7 +6,12 @@ * found in the LICENSE file at https://angular.dev/license */ -import { generateDebugId, injectDebugIdIntoJs, injectDebugIdIntoSourceMap } from './debug-id'; +import { + generateDebugId, + injectDebugIdIntoJs, + injectDebugIdIntoSourceMap, + stripDebugIdFromSourceMap, +} from './debug-id'; const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; @@ -59,6 +64,17 @@ describe('debug-id', () => { // Re-running with the same id is a no-op. expect(injectDebugIdIntoJs(result, id)).toBe(result); }); + + it('does not replace a debugId comment that is inside a string or template literal', () => { + const text = + 'const t = `\n//# debugId=00000000-0000-5000-8000-000000000000\n`;\n//# sourceMappingURL=foo.js.map\n'; + const result = injectDebugIdIntoJs(text, id); + expect(result).toBe( + 'const t = `\n//# debugId=00000000-0000-5000-8000-000000000000\n`;\n' + + `//# debugId=${id}\n` + + '//# sourceMappingURL=foo.js.map\n', + ); + }); }); describe('injectDebugIdIntoSourceMap', () => { @@ -95,4 +111,27 @@ describe('debug-id', () => { expect(injectDebugIdIntoSourceMap(malformed, id)).toBe(malformed); }); }); + + describe('stripDebugIdFromSourceMap', () => { + it('removes the debugId field from pretty-printed source maps', () => { + const original = '{\n\t"version": 3,\n\t"mappings": ""\n}'; + const updated = + '{\n\t"version": 3,\n\t"mappings": "",\n\t"debugId": "11111111-2222-5333-9444-555555555555"\n}'; + expect(stripDebugIdFromSourceMap(updated)).toBe(original); + }); + + it('removes the debugId field from minified source maps', () => { + const original = '{"version":3,"mappings":""}'; + const updated = + '{"version":3,"mappings":"","debugId":"11111111-2222-5333-9444-555555555555"}'; + expect(stripDebugIdFromSourceMap(updated)).toBe(original); + }); + + it('removes the debugId field when it is the first property', () => { + const original = '{\n\t"version": 3,\n\t"mappings": ""\n}'; + const updated = + '{\n\t"debugId": "11111111-2222-5333-9444-555555555555",\n\t"version": 3,\n\t"mappings": ""\n}'; + expect(stripDebugIdFromSourceMap(updated)).toBe(original); + }); + }); });