Skip to content

Commit d51b258

Browse files
committed
refactor(@angular/build): centralize sourcemap buffer slicing and removal
Extract and centralize trailing sourcemap comment inspection, extraction, and buffer slicing into the shared source-map utility module. Previously, buffer scanning and removal logic was duplicated between the main-thread transformer fast path and the worker script. The `removeSourceMappingURL` function is overloaded to accept both `string` and `Uint8Array` / `Buffer` inputs natively. When given raw byte buffers, it uses a zero-copy fast path to strip single trailing sourcemap comments directly from the buffer without decoding into a JavaScript string, falling back to the state-machine parser only when multiple or non-trailing comments are present.
1 parent 7112b75 commit d51b258

4 files changed

Lines changed: 143 additions & 66 deletions

File tree

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

Lines changed: 19 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { createRequire } from 'node:module';
1212
import Piscina from 'piscina';
1313
import { useBabelLinker } from '../../utils/environment-options.js';
1414
import {
15+
findTrailingSourceMapComment,
1516
isTrailingSourceMapComment,
1617
loadInputSourceMap,
1718
loadInputSourceMapFromUrl,
@@ -37,7 +38,6 @@ interface TransformOptions extends Omit<JavaScriptTransformRequest, 'filename' |
3738

3839
const textDecoder = new TextDecoder();
3940
const textEncoder = new TextEncoder();
40-
const SOURCEMAP_COMMENT_BYTES = Buffer.from('//# sourceMappingURL=');
4141

4242
async function instrumentCoverage(
4343
filename: string,
@@ -97,54 +97,34 @@ export default async function transformJavaScript(
9797
let isAlreadyStripped = false;
9898

9999
if (typeof data !== 'string') {
100-
const dataBuffer = Buffer.isBuffer(data)
101-
? data
102-
: Buffer.from(data.buffer, data.byteOffset, data.byteLength);
103-
104-
const firstIndex = dataBuffer.indexOf(SOURCEMAP_COMMENT_BYTES);
105-
if (firstIndex === -1) {
100+
const trailing = findTrailingSourceMapComment(data);
101+
if (trailing === null) {
106102
// 0 comments: fast path, no sourcemap to load or strip
107103
textData = textDecoder.decode(data);
108104
isAlreadyStripped = true;
109-
} else {
110-
const lastIndex = dataBuffer.lastIndexOf(SOURCEMAP_COMMENT_BYTES);
111-
// Skip any preceding horizontal whitespace (spaces/tabs) to find the start of the line.
112-
let prevIdx = lastIndex - 1;
113-
while (prevIdx >= 0 && (dataBuffer[prevIdx] === 32 || dataBuffer[prevIdx] === 9)) {
114-
prevIdx--;
115-
}
116-
// Ensure the comment starts at the beginning of a line or the start of the file,
117-
// preventing false positives for occurrences inside inline string literals or code.
118-
const isLineStart = prevIdx < 0 || dataBuffer[prevIdx] === 10 || dataBuffer[prevIdx] === 13;
119-
120-
if (firstIndex === lastIndex && isLineStart) {
121-
const urlLine = dataBuffer
122-
.subarray(lastIndex + SOURCEMAP_COMMENT_BYTES.length)
123-
.toString('utf-8');
124-
125-
if (useInputSourcemap) {
126-
inputSourceMap = loadInputSourceMapFromUrl(filename, urlLine);
127-
if (inputSourceMap !== undefined) {
128-
// Valid trailing sourcemap comment confirmed: safe to slice code buffer for transformation passes.
129-
// Note: If no passes modify the code, the untouched original `data` buffer is returned below.
130-
textData = textDecoder.decode(dataBuffer.subarray(0, prevIdx < 0 ? 0 : prevIdx + 1));
131-
isAlreadyStripped = true;
132-
} else {
133-
// Not a valid trailing sourcemap (e.g. inside template literal): fallback to full decode
134-
textData = textDecoder.decode(data);
135-
}
136-
} else if (isTrailingSourceMapComment(urlLine)) {
137-
// Valid trailing sourcemap comment confirmed: safe to slice code buffer
138-
textData = textDecoder.decode(dataBuffer.subarray(0, prevIdx < 0 ? 0 : prevIdx + 1));
105+
} else if (trailing !== undefined) {
106+
if (useInputSourcemap) {
107+
inputSourceMap = loadInputSourceMapFromUrl(filename, trailing.urlLine);
108+
if (inputSourceMap !== undefined) {
109+
// Valid trailing sourcemap comment confirmed: safe to slice code buffer for transformation passes.
110+
// Note: If no passes modify the code, the untouched original `data` buffer is returned below.
111+
textData = textDecoder.decode(trailing.code);
139112
isAlreadyStripped = true;
140113
} else {
141-
// Fallback to full decode and state-machine stripping
114+
// Not a valid trailing sourcemap (e.g. inside template literal): fallback to full decode
142115
textData = textDecoder.decode(data);
143116
}
117+
} else if (isTrailingSourceMapComment(trailing.urlLine)) {
118+
// Valid trailing sourcemap comment confirmed: safe to slice code buffer
119+
textData = textDecoder.decode(trailing.code);
120+
isAlreadyStripped = true;
144121
} else {
145-
// Multiple comments or comment not at line start: fall back to full decode and string parser
122+
// Fallback to full decode and state-machine stripping
146123
textData = textDecoder.decode(data);
147124
}
125+
} else {
126+
// Multiple comments or comment not at line start: fall back to full decode and string parser
127+
textData = textDecoder.decode(data);
148128
}
149129
} else {
150130
textData = data;

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

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { removeSourceMappingURL } from '../../utils/source-map';
1313
import { WorkerPool, WorkerPoolOptions } from '../../utils/worker-pool';
1414
import { Cache } from './cache';
1515

16-
const SOURCEMAP_COMMENT_BYTES = Buffer.from('sourceMappingURL=');
1716
const LINKER_DECLARATION_PREFIX = 'ɵɵngDeclare';
1817
const LINKER_DECLARATION_PREFIX_BYTES = Buffer.from(LINKER_DECLARATION_PREFIX, 'utf-8');
1918

@@ -226,27 +225,13 @@ export class JavaScriptTransformer {
226225
this.#commonOptions.sourcemap &&
227226
(!!this.#commonOptions.thirdPartySourcemaps || !/[\\/]node_modules[\\/]/.test(filename));
228227

229-
if (typeof data === 'string') {
230-
return Buffer.from(keepSourcemap ? data : removeSourceMappingURL(data), 'utf-8');
231-
}
232-
233228
if (keepSourcemap) {
234-
return data;
235-
}
236-
237-
const dataBuffer = Buffer.isBuffer(data)
238-
? data
239-
: Buffer.from(data.buffer, data.byteOffset, data.byteLength);
240-
241-
// Fast check on raw ASCII bytes to avoid UTF-8 string decoding if no comment exists.
242-
if (dataBuffer.indexOf(SOURCEMAP_COMMENT_BYTES) === -1) {
243-
return data;
229+
return typeof data === 'string' ? Buffer.from(data, 'utf-8') : data;
244230
}
245231

246-
const text = dataBuffer.toString('utf-8');
247-
const stripped = removeSourceMappingURL(text);
248-
249-
return stripped === text ? data : Buffer.from(stripped, 'utf-8');
232+
return typeof data === 'string'
233+
? Buffer.from(removeSourceMappingURL(data), 'utf-8')
234+
: removeSourceMappingURL(data);
250235
}
251236

252237
// Only standalone (non-pooled) ArrayBuffers can be transferred across worker threads.

packages/angular/build/src/utils/source-map.ts

Lines changed: 84 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,90 @@ import { existsSync, readFileSync } from 'node:fs';
1111
import { dirname, resolve } from 'node:path';
1212
import { fileURLToPath } from 'node:url';
1313

14+
export const SOURCEMAP_COMMENT_PREFIX = '//# sourceMappingURL=';
15+
export const SOURCEMAP_COMMENT_BYTES = Buffer.from(SOURCEMAP_COMMENT_PREFIX);
16+
17+
/**
18+
* Checks for a single trailing `//# sourceMappingURL=` comment on a raw buffer.
19+
*
20+
* @param data The raw byte buffer to inspect.
21+
* @returns An object containing the sliced code buffer and URL snippet if a single trailing comment exists,
22+
* `null` if no sourcemap comment exists in the buffer, or `undefined` if multiple/non-trailing comments exist.
23+
*/
24+
export function findTrailingSourceMapComment(
25+
data: Uint8Array,
26+
): { code: Uint8Array; urlLine: string } | null | undefined {
27+
const dataBuffer = Buffer.isBuffer(data)
28+
? data
29+
: Buffer.from(data.buffer, data.byteOffset, data.byteLength);
30+
31+
const firstIndex = dataBuffer.indexOf(SOURCEMAP_COMMENT_BYTES);
32+
if (firstIndex === -1) {
33+
return null;
34+
}
35+
36+
const lastIndex = dataBuffer.lastIndexOf(SOURCEMAP_COMMENT_BYTES);
37+
// Skip any preceding horizontal whitespace (spaces/tabs) to find the start of the line.
38+
let prevIdx = lastIndex - 1;
39+
while (prevIdx >= 0 && (dataBuffer[prevIdx] === 32 || dataBuffer[prevIdx] === 9)) {
40+
prevIdx--;
41+
}
42+
// Ensure the comment starts at the beginning of a line or the start of the file,
43+
// preventing false positives for occurrences inside inline string literals or code.
44+
const isLineStart = prevIdx < 0 || dataBuffer[prevIdx] === 10 || dataBuffer[prevIdx] === 13;
45+
46+
if (firstIndex === lastIndex && isLineStart) {
47+
const urlLine = dataBuffer
48+
.subarray(lastIndex + SOURCEMAP_COMMENT_BYTES.length)
49+
.toString('utf-8');
50+
51+
return {
52+
code: dataBuffer.subarray(0, prevIdx < 0 ? 0 : prevIdx + 1),
53+
urlLine,
54+
};
55+
}
56+
57+
return undefined;
58+
}
59+
1460
/**
1561
* Removes `//# sourceMappingURL=` comments safely from the given JavaScript code,
1662
* ignoring any occurrences that are inside string literals, template literals, or block comments.
1763
*
18-
* It uses a lightweight state-machine parser to accurately handle nested template literals.
64+
* For raw Uint8Array / Buffer inputs, it optimizes performance by inspecting trailing byte sequences
65+
* to slice the buffer directly without full string decoding.
1966
*
20-
* @param code The JavaScript source code.
67+
* @param code The JavaScript source code as a string or Uint8Array.
2168
* @returns The code with top-level sourcemap comments removed.
2269
*/
23-
export function removeSourceMappingURL(code: string): string {
24-
if (!code.includes('//# sourceMappingURL=')) {
70+
export function removeSourceMappingURL(code: string): string;
71+
export function removeSourceMappingURL(code: Uint8Array): Uint8Array;
72+
export function removeSourceMappingURL(code: string | Uint8Array): string | Uint8Array {
73+
if (typeof code === 'string') {
74+
return removeSourceMappingURLFromString(code);
75+
}
76+
77+
const trailing = findTrailingSourceMapComment(code);
78+
if (trailing === null) {
79+
return code;
80+
}
81+
82+
if (trailing && isTrailingSourceMapComment(trailing.urlLine)) {
83+
return trailing.code;
84+
}
85+
86+
// Fallback to full decode and state-machine stripping for multiple comments or non-trailing comments
87+
const dataBuffer = Buffer.isBuffer(code)
88+
? code
89+
: Buffer.from(code.buffer, code.byteOffset, code.byteLength);
90+
const text = dataBuffer.toString('utf-8');
91+
const stripped = removeSourceMappingURLFromString(text);
92+
93+
return stripped === text ? code : Buffer.from(stripped, 'utf-8');
94+
}
95+
96+
function removeSourceMappingURLFromString(code: string): string {
97+
if (!code.includes(SOURCEMAP_COMMENT_PREFIX)) {
2598
return code;
2699
}
27100

@@ -64,12 +137,12 @@ export function removeSourceMappingURL(code: string): string {
64137
}
65138
}
66139

67-
if (!isEscaped && code.startsWith('//# sourceMappingURL=', i)) {
140+
if (!isEscaped && code.startsWith(SOURCEMAP_COMMENT_PREFIX, i)) {
68141
if (i > lastCopiedIndex) {
69142
result.push(code.slice(lastCopiedIndex, i));
70143
}
71144
// Skip the rest of the comment line up to the newline
72-
i += 21;
145+
i += SOURCEMAP_COMMENT_PREFIX.length;
73146
while (i < len && code[i] !== '\n' && code[i] !== '\r') {
74147
i++;
75148
}
@@ -308,7 +381,7 @@ export function loadInputSourceMapFromUrl(
308381
export function loadInputSourceMap(filename: string, code: string): EncodedSourceMap | undefined {
309382
// Locate the last sourceMappingURL comment using lastIndexOf to avoid scanning
310383
// the entire file with a regular expression (significant for large files).
311-
const lastSourceMapIndex = code.lastIndexOf('//# sourceMappingURL=');
384+
const lastSourceMapIndex = code.lastIndexOf(SOURCEMAP_COMMENT_PREFIX);
312385
if (lastSourceMapIndex === -1) {
313386
return undefined;
314387
}
@@ -325,5 +398,8 @@ export function loadInputSourceMap(filename: string, code: string): EncodedSourc
325398
}
326399
}
327400

328-
return loadInputSourceMapFromUrl(filename, code.slice(lastSourceMapIndex + 21));
401+
return loadInputSourceMapFromUrl(
402+
filename,
403+
code.slice(lastSourceMapIndex + SOURCEMAP_COMMENT_PREFIX.length),
404+
);
329405
}

packages/angular/build/src/utils/source-map_spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,42 @@ describe('removeSourceMappingURL', () => {
102102
const code = 'console.log("hello");\r\n//# sourceMappingURL=main.js.map\r\nconst next = 2;';
103103
expect(removeSourceMappingURL(code)).toBe('console.log("hello");\r\n\r\nconst next = 2;');
104104
});
105+
106+
describe('with Uint8Array / Buffer inputs', () => {
107+
it('should strip trailing sourcemap comment from Uint8Array buffer', () => {
108+
const buffer = Buffer.from(
109+
'console.log("hello");\n//# sourceMappingURL=main.js.map',
110+
'utf-8',
111+
);
112+
const result = removeSourceMappingURL(buffer);
113+
114+
expect(Buffer.from(result).toString('utf-8')).toBe('console.log("hello");\n');
115+
});
116+
117+
it('should return exact input buffer when no sourcemap comment is present', () => {
118+
const buffer = Buffer.from('console.log("hello");\nconst x = 1;', 'utf-8');
119+
const result = removeSourceMappingURL(buffer);
120+
121+
expect(result).toBe(buffer);
122+
});
123+
124+
it('should handle multiple sourcemap comments in a buffer via fallback', () => {
125+
const buffer = Buffer.from(
126+
'//# sourceMappingURL=first.js.map\nconsole.log("mid");\n//# sourceMappingURL=second.js.map',
127+
'utf-8',
128+
);
129+
const result = removeSourceMappingURL(buffer);
130+
131+
expect(Buffer.from(result).toString('utf-8')).toBe('\nconsole.log("mid");\n');
132+
});
133+
134+
it('should not strip sourcemap comments inside template strings in a buffer', () => {
135+
const buffer = Buffer.from('const str = `\n//# sourceMappingURL=inline.js.map\n`;', 'utf-8');
136+
const result = removeSourceMappingURL(buffer);
137+
138+
expect(Buffer.from(result).toString('utf-8')).toBe(buffer.toString('utf-8'));
139+
});
140+
});
105141
});
106142

107143
describe('loadInputSourceMapFromUrl', () => {

0 commit comments

Comments
 (0)