Skip to content

Commit 1c00edc

Browse files
committed
perf(@angular/build): optimize sourcemap stripping and loading with buffer fast path
Optimize sourcemap detection, loading, and stripping during JavaScript transformations by inspecting incoming raw Uint8Array/Buffer data directly before decoding into strings. Files without sourcemap comments are identified via fast Buffer.indexOf() and skip comment removal entirely. For single trailing comments, the sourcemap is parsed directly from the trailing URL slice and the raw code buffer is sliced using a zero-copy subarray view to avoid large string allocations and trailing state-machine scans. Line-start boundaries and end-of-file trailing whitespace are validated to prevent false positives with template strings, safely falling back to full string state-machine parsing when necessary.
1 parent c536ae3 commit 1c00edc

3 files changed

Lines changed: 280 additions & 54 deletions

File tree

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

Lines changed: 87 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ import { type PluginItem, transformAsync } from '@babel/core';
1111
import { createRequire } from 'node:module';
1212
import Piscina from 'piscina';
1313
import { useBabelLinker } from '../../utils/environment-options.js';
14-
import { loadInputSourceMap, removeSourceMappingURL } from '../../utils/source-map';
14+
import {
15+
isTrailingSourceMapComment,
16+
loadInputSourceMap,
17+
loadInputSourceMapFromUrl,
18+
removeSourceMappingURL,
19+
} from '../../utils/source-map';
1520

1621
interface JavaScriptTransformRequest {
1722
filename: string;
@@ -25,8 +30,14 @@ interface JavaScriptTransformRequest {
2530
instrumentForCoverage?: boolean;
2631
}
2732

33+
interface TransformOptions extends Omit<JavaScriptTransformRequest, 'filename' | 'data'> {
34+
inputSourceMap?: EncodedSourceMap;
35+
isAlreadyStripped?: boolean;
36+
}
37+
2838
const textDecoder = new TextDecoder();
2939
const textEncoder = new TextEncoder();
40+
const SOURCEMAP_COMMENT_BYTES = Buffer.from('//# sourceMappingURL=');
3041

3142
/**
3243
* The function name prefix for all Angular partial compilation functions.
@@ -84,11 +95,77 @@ export default async function transformJavaScript(
8495
request: JavaScriptTransformRequest,
8596
): Promise<unknown> {
8697
const { filename, data, ...options } = request;
87-
const textData = typeof data === 'string' ? data : textDecoder.decode(data);
8898

89-
const transformedData = await transformJavaScriptImpl(filename, textData, options);
99+
const useInputSourcemap =
100+
options.sourcemap &&
101+
(!!options.thirdPartySourcemaps || !/[\\/]node_modules[\\/]/.test(filename));
90102

91-
// Transfer the data via `move` instead of cloning
103+
let textData: string;
104+
let inputSourceMap: EncodedSourceMap | undefined;
105+
let isAlreadyStripped = false;
106+
107+
if (typeof data !== 'string') {
108+
const dataBuffer = Buffer.isBuffer(data)
109+
? data
110+
: Buffer.from(data.buffer, data.byteOffset, data.byteLength);
111+
112+
const firstIndex = dataBuffer.indexOf(SOURCEMAP_COMMENT_BYTES);
113+
if (firstIndex === -1) {
114+
// 0 comments: fast path, no sourcemap to load or strip
115+
textData = textDecoder.decode(data);
116+
isAlreadyStripped = true;
117+
} else {
118+
const lastIndex = dataBuffer.lastIndexOf(SOURCEMAP_COMMENT_BYTES);
119+
// Skip any preceding horizontal whitespace (spaces/tabs) to find the start of the line.
120+
let prevIdx = lastIndex - 1;
121+
while (prevIdx >= 0 && (dataBuffer[prevIdx] === 32 || dataBuffer[prevIdx] === 9)) {
122+
prevIdx--;
123+
}
124+
// Ensure the comment starts at the beginning of a line or the start of the file,
125+
// preventing false positives for occurrences inside inline string literals or code.
126+
const isLineStart = prevIdx < 0 || dataBuffer[prevIdx] === 10 || dataBuffer[prevIdx] === 13;
127+
128+
if (firstIndex === lastIndex && isLineStart) {
129+
const urlLine = dataBuffer
130+
.subarray(lastIndex + SOURCEMAP_COMMENT_BYTES.length)
131+
.toString('utf-8');
132+
133+
if (useInputSourcemap) {
134+
inputSourceMap = loadInputSourceMapFromUrl(filename, urlLine);
135+
if (inputSourceMap !== undefined) {
136+
// Valid trailing sourcemap comment confirmed: safe to slice code buffer for transformation passes.
137+
// Note: If no passes modify the code, the untouched original `data` buffer is returned below.
138+
textData = textDecoder.decode(dataBuffer.subarray(0, prevIdx < 0 ? 0 : prevIdx + 1));
139+
isAlreadyStripped = true;
140+
} else {
141+
// Not a valid trailing sourcemap (e.g. inside template literal): fallback to full decode
142+
textData = textDecoder.decode(data);
143+
}
144+
} else if (isTrailingSourceMapComment(urlLine)) {
145+
// Valid trailing sourcemap comment confirmed: safe to slice code buffer
146+
textData = textDecoder.decode(dataBuffer.subarray(0, prevIdx < 0 ? 0 : prevIdx + 1));
147+
isAlreadyStripped = true;
148+
} else {
149+
// Fallback to full decode and state-machine stripping
150+
textData = textDecoder.decode(data);
151+
}
152+
} else {
153+
// Multiple comments or comment not at line start: fall back to full decode and string parser
154+
textData = textDecoder.decode(data);
155+
}
156+
}
157+
} else {
158+
textData = data;
159+
}
160+
161+
const transformedData = await transformJavaScriptImpl(filename, textData, {
162+
...options,
163+
inputSourceMap,
164+
isAlreadyStripped,
165+
});
166+
167+
// If no transformations modified the code, return the original untouched data buffer via `move`.
168+
// This preserves any original trailing sourcemap comment and avoids re-encoding.
92169
if (transformedData === textData && typeof data !== 'string') {
93170
return Piscina.move(data);
94171
}
@@ -109,7 +186,7 @@ let oxcTransformModule: typeof import('../oxc/oxc-transform.js') | undefined;
109186
async function transformJavaScriptImpl(
110187
filename: string,
111188
data: string,
112-
options: Omit<JavaScriptTransformRequest, 'filename' | 'data'>,
189+
options: TransformOptions,
113190
): Promise<string> {
114191
const shouldLink = !options.skipLinker && requiresLinking(filename, data);
115192
const useInputSourcemap =
@@ -194,9 +271,11 @@ async function transformJavaScriptImpl(
194271
}
195272

196273
if (useInputSourcemap) {
197-
const baseMap = coverageMap ?? loadInputSourceMap(filename, data);
274+
const baseMap = coverageMap ?? options.inputSourceMap ?? loadInputSourceMap(filename, data);
198275
if (maps.length > 0 || coverageMap) {
199-
code = removeSourceMappingURL(code);
276+
if (!options.isAlreadyStripped) {
277+
code = removeSourceMappingURL(code);
278+
}
200279
const remappingChain: (DecodedSourceMap | EncodedSourceMap)[] = maps.reverse();
201280
if (baseMap) {
202281
remappingChain.push(baseMap);
@@ -213,7 +292,7 @@ async function transformJavaScriptImpl(
213292
}
214293

215294
// Strip sourcemaps if they should not be used
216-
return removeSourceMappingURL(code);
295+
return options.isAlreadyStripped ? code : removeSourceMappingURL(code);
217296
}
218297

219298
function requiresLinking(path: string, source: string): boolean {

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

Lines changed: 103 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -184,71 +184,101 @@ export function removeSourceMappingURL(code: string): string {
184184
}
185185

186186
/**
187-
* Finds, resolves, and loads the input sourcemap referenced in the code's trailing
188-
* sourceMappingURL comment, if present. Supports inline base64 data URIs, local absolute
189-
* file URLs, and relative/absolute filesystem paths.
187+
* Extracts the base64 payload from an inline sourcemap data URI line and verifies
188+
* that only trailing whitespace follows the payload.
189+
*
190+
* @returns The base64 payload string if valid and trailing, or `undefined` otherwise.
190191
*/
191-
export function loadInputSourceMap(filename: string, code: string): EncodedSourceMap | undefined {
192-
// Locate the last sourceMappingURL comment using lastIndexOf to avoid scanning
193-
// the entire file with a regular expression (significant for large files).
194-
const lastSourceMapIndex = code.lastIndexOf('//# sourceMappingURL=');
195-
if (lastSourceMapIndex === -1) {
192+
function extractTrailingBase64Payload(urlLine: string): string | undefined {
193+
if (!urlLine.startsWith('data:application/json;')) {
196194
return undefined;
197195
}
198196

199-
const urlLine = code.slice(lastSourceMapIndex + 21);
200-
201-
// Inline base64-encoded sourcemaps can be extremely large (up to megabytes).
202-
// Parse them without regular expressions to avoid heavy backtracking and allocations.
203-
if (urlLine.startsWith('data:application/json;')) {
204-
const base64StartIndex = urlLine.indexOf('base64,');
205-
if (base64StartIndex === -1) {
206-
return undefined;
207-
}
197+
const base64StartIndex = urlLine.indexOf('base64,');
198+
if (base64StartIndex === -1) {
199+
return undefined;
200+
}
208201

209-
const payloadStart = base64StartIndex + 7;
210-
let payloadEnd = urlLine.length;
211-
// Find the first trailing whitespace character that marks the end of the base64 payload.
212-
for (let i = payloadStart; i < urlLine.length; i++) {
213-
const char = urlLine[i];
214-
if (char === ' ' || char === '\r' || char === '\n' || char === '\t') {
215-
payloadEnd = i;
216-
break;
217-
}
202+
const payloadStart = base64StartIndex + 7;
203+
let payloadEnd = urlLine.length;
204+
// Find the first trailing whitespace character that marks the end of the base64 payload.
205+
for (let i = payloadStart; i < urlLine.length; i++) {
206+
const char = urlLine[i];
207+
if (char === ' ' || char === '\r' || char === '\n' || char === '\t') {
208+
payloadEnd = i;
209+
break;
218210
}
211+
}
219212

220-
// Verify that everything after the base64 payload is trailing whitespace
221-
// to ensure this is a valid trailing sourceMappingURL comment at the end of the file.
222-
for (let i = payloadEnd; i < urlLine.length; i++) {
223-
const char = urlLine[i];
224-
if (char !== ' ' && char !== '\r' && char !== '\n' && char !== '\t') {
225-
return undefined;
226-
}
213+
// Verify that everything after the base64 payload is trailing whitespace
214+
// to ensure this is a valid trailing sourceMappingURL comment at the end of the file.
215+
for (let i = payloadEnd; i < urlLine.length; i++) {
216+
const char = urlLine[i];
217+
if (char !== ' ' && char !== '\r' && char !== '\n' && char !== '\t') {
218+
return undefined;
227219
}
220+
}
228221

229-
try {
230-
// Extract the base64 payload and decode it directly into binary memory.
231-
const base64Content = urlLine.slice(payloadStart, payloadEnd);
222+
return urlLine.slice(payloadStart, payloadEnd);
223+
}
232224

233-
return JSON.parse(Buffer.from(base64Content, 'base64').toString('utf-8')) as EncodedSourceMap;
234-
} catch {
235-
return undefined;
236-
}
225+
/**
226+
* Extracts the URL from an external sourcemap comment line and verifies
227+
* that only trailing whitespace follows the URL.
228+
*
229+
* @returns The URL string if valid and trailing, or `undefined` otherwise.
230+
*/
231+
function extractTrailingUrl(urlLine: string): string | undefined {
232+
if (urlLine.startsWith('data:')) {
233+
return undefined;
237234
}
238235

239-
// Non-inline sourcemap comments (always small, typically < 200 characters).
240-
const urlMatch = /^([^\r\n\s]+)/.exec(urlLine);
236+
const urlMatch = /^([^\r\n\s'"`]+)/.exec(urlLine);
241237
if (!urlMatch) {
242238
return undefined;
243239
}
244240

245-
const url = urlMatch[1];
246-
const remaining = urlLine.slice(url.length);
247-
// Verify there is only whitespace after the URL to the end of the file.
241+
const remaining = urlLine.slice(urlMatch[1].length);
248242
if (!/^\s*$/.test(remaining)) {
249243
return undefined;
250244
}
251245

246+
return urlMatch[1];
247+
}
248+
249+
/**
250+
* Checks whether a `//# sourceMappingURL=` URL line snippet represents a valid trailing comment at the end of the file.
251+
*/
252+
export function isTrailingSourceMapComment(urlLine: string): boolean {
253+
return (
254+
extractTrailingBase64Payload(urlLine) !== undefined || extractTrailingUrl(urlLine) !== undefined
255+
);
256+
}
257+
258+
/**
259+
* Resolves and loads the input sourcemap referenced in a `//# sourceMappingURL=` URL line snippet.
260+
* Supports inline base64 data URIs, local absolute file URLs, and relative/absolute filesystem paths.
261+
*/
262+
export function loadInputSourceMapFromUrl(
263+
filename: string,
264+
urlLine: string,
265+
): EncodedSourceMap | undefined {
266+
// Inline base64-encoded sourcemaps can be extremely large (up to megabytes).
267+
// Parse them without regular expressions to avoid heavy backtracking and allocations.
268+
const base64Payload = extractTrailingBase64Payload(urlLine);
269+
if (base64Payload !== undefined) {
270+
try {
271+
return JSON.parse(Buffer.from(base64Payload, 'base64').toString('utf-8')) as EncodedSourceMap;
272+
} catch {
273+
return undefined;
274+
}
275+
}
276+
277+
const url = extractTrailingUrl(urlLine);
278+
if (!url) {
279+
return undefined;
280+
}
281+
252282
if (url.startsWith('file://')) {
253283
// Local absolute file URL scheme.
254284
try {
@@ -269,3 +299,31 @@ export function loadInputSourceMap(filename: string, code: string): EncodedSourc
269299

270300
return undefined;
271301
}
302+
303+
/**
304+
* Finds, resolves, and loads the input sourcemap referenced in the code's trailing
305+
* sourceMappingURL comment, if present. Supports inline base64 data URIs, local absolute
306+
* file URLs, and relative/absolute filesystem paths.
307+
*/
308+
export function loadInputSourceMap(filename: string, code: string): EncodedSourceMap | undefined {
309+
// Locate the last sourceMappingURL comment using lastIndexOf to avoid scanning
310+
// the entire file with a regular expression (significant for large files).
311+
const lastSourceMapIndex = code.lastIndexOf('//# sourceMappingURL=');
312+
if (lastSourceMapIndex === -1) {
313+
return undefined;
314+
}
315+
316+
if (lastSourceMapIndex > 0) {
317+
// Skip any preceding horizontal whitespace (spaces/tabs) to find the start of the line.
318+
let prevIdx = lastSourceMapIndex - 1;
319+
while (prevIdx >= 0 && (code[prevIdx] === ' ' || code[prevIdx] === '\t')) {
320+
prevIdx--;
321+
}
322+
// Ensure the comment starts at the beginning of a line, preventing false positives within code or strings.
323+
if (prevIdx >= 0 && code[prevIdx] !== '\n' && code[prevIdx] !== '\r') {
324+
return undefined;
325+
}
326+
}
327+
328+
return loadInputSourceMapFromUrl(filename, code.slice(lastSourceMapIndex + 21));
329+
}

0 commit comments

Comments
 (0)