Skip to content

Commit 7ecf106

Browse files
committed
perf(@angular/build): avoid encoding intermediate source maps before remapping
Use `generateDecodedMap()` instead of `generateMap()` when intermediate source maps created with `MagicString` are immediately remapped via `@ampproject/remapping`. This applies to: - `oxc-transform`: when an input source map is present - `oxc-linker`: when an input source map is present - Sass URL rebasing: intermediate stylesheet rebase maps stored for Sass source map merging Bypassing VLQ base64 encoding and immediate decoding reduces CPU time and temporary memory allocations during source map generation.
1 parent 194be20 commit 7ecf106

6 files changed

Lines changed: 117 additions & 17 deletions

File tree

packages/angular/build/src/tools/angular/linker/oxc-linker.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import type { EncodedSourceMap } from '@ampproject/remapping';
9+
import type { DecodedSourceMap } from '@ampproject/remapping';
1010
import remapping from '@ampproject/remapping';
1111
import { ConsoleLogger, LogLevel } from '@angular/compiler-cli';
1212
import type { DeclarationScope } from '@angular/compiler-cli/linker';
@@ -170,12 +170,15 @@ export function linkWithOxc(filename: string, code: string, options: OxcLinkerOp
170170

171171
let map: string | undefined;
172172
if (options.sourcemap) {
173-
const rawMap = s.generateMap({ hires: true, source: filename });
174173
const inputMap = loadInputSourceMap(filename, code);
175174
if (inputMap) {
176-
map = remapping([rawMap as EncodedSourceMap, inputMap], () => null).toString();
175+
const rawMap = s.generateDecodedMap({ hires: true, source: filename });
176+
map = remapping(
177+
[{ ...rawMap, version: 3 } satisfies DecodedSourceMap, inputMap],
178+
() => null,
179+
).toString();
177180
} else {
178-
map = rawMap.toString();
181+
map = s.generateMap({ hires: true, source: filename }).toString();
179182
}
180183
}
181184

packages/angular/build/src/tools/angular/linker/oxc-linker_spec.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,53 @@ describe('linkWithOxc', () => {
5353
expect(result.code).toContain('i0.ɵɵdefineComponent');
5454
expect(result.code).not.toContain('i0.ɵɵngDeclareComponent');
5555
});
56+
57+
it('should generate a sourcemap when sourcemap option is enabled', () => {
58+
const input = `
59+
import * as i0 from "@angular/core";
60+
export class MyDirective {}
61+
MyDirective.ɵdir = i0.ɵɵngDeclareDirective({
62+
minVersion: "12.0.0",
63+
version: "14.0.0",
64+
ngImport: i0,
65+
type: MyDirective,
66+
selector: "[my-dir]"
67+
});
68+
`;
69+
70+
const result = linkWithOxc('test.js', input, { sourcemap: true });
71+
expect(result.map).toBeDefined();
72+
const parsedMap = JSON.parse(result.map as string);
73+
expect(parsedMap.version).toBe(3);
74+
expect(parsedMap.sources).toContain('test.js');
75+
});
76+
77+
it('should remap with input sourcemap when sourcemap option is enabled and inputMap is present', () => {
78+
const inputMap = {
79+
version: 3,
80+
sources: ['original.ts'],
81+
sourcesContent: ['// original content'],
82+
mappings: 'AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA',
83+
names: [],
84+
};
85+
const base64Map = Buffer.from(JSON.stringify(inputMap)).toString('base64');
86+
const input = `
87+
import * as i0 from "@angular/core";
88+
export class MyDirective {}
89+
MyDirective.ɵdir = i0.ɵɵngDeclareDirective({
90+
minVersion: "12.0.0",
91+
version: "14.0.0",
92+
ngImport: i0,
93+
type: MyDirective,
94+
selector: "[my-dir]"
95+
});
96+
//# sourceMappingURL=data:application/json;base64,${base64Map}
97+
`;
98+
99+
const result = linkWithOxc('test.js', input, { sourcemap: true });
100+
expect(result.map).toBeDefined();
101+
const parsedMap = JSON.parse(result.map as string);
102+
expect(parsedMap.version).toBe(3);
103+
expect(parsedMap.sources).toContain('original.ts');
104+
});
56105
});

packages/angular/build/src/tools/oxc/oxc-transform.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import remapping, { type EncodedSourceMap } from '@ampproject/remapping';
9+
import remapping, { type DecodedSourceMap } from '@ampproject/remapping';
1010
import type { BindingIdentifier, Class, Node } from '@oxc-project/types';
1111
import { MagicString } from 'magic-string';
1212
import { Visitor, parseSync } from 'oxc-parser';
@@ -749,13 +749,16 @@ export function transform(filename: string, code: string, options: OxcTransformO
749749

750750
let map: string | undefined;
751751
if (options.sourcemap) {
752-
const rawMap = s.generateMap({ hires: true, source: filename });
753752
const inputMap = loadInputSourceMap(filename, code);
754753

755754
if (inputMap) {
756-
map = remapping([rawMap as EncodedSourceMap, inputMap], () => null).toString();
755+
const rawMap = s.generateDecodedMap({ hires: true, source: filename });
756+
map = remapping(
757+
[{ ...rawMap, version: 3 } satisfies DecodedSourceMap, inputMap],
758+
() => null,
759+
).toString();
757760
} else {
758-
map = rawMap.toString();
761+
map = s.generateMap({ hires: true, source: filename }).toString();
759762
}
760763
}
761764

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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 { transform } from './oxc-transform';
10+
11+
describe('oxc-transform sourcemaps', () => {
12+
it('should generate a sourcemap when sourcemap option is enabled without inputMap', () => {
13+
const input = 'var result = new SomeClass();';
14+
const result = transform('test.js', input, { sourcemap: true });
15+
16+
expect(result.map).toBeDefined();
17+
const parsedMap = JSON.parse(result.map as string);
18+
expect(parsedMap.version).toBe(3);
19+
expect(parsedMap.sources).toContain('test.js');
20+
expect(parsedMap.mappings.length).toBeGreaterThan(0);
21+
});
22+
23+
it('should remap with input sourcemap when sourcemap option is enabled and inputMap is present', () => {
24+
const inputMap = {
25+
version: 3,
26+
sources: ['original.ts'],
27+
sourcesContent: ['const result = new SomeClass();'],
28+
mappings: 'AAAA',
29+
names: [],
30+
};
31+
const base64Map = Buffer.from(JSON.stringify(inputMap)).toString('base64');
32+
const input = `var result = new SomeClass();\n//# sourceMappingURL=data:application/json;base64,${base64Map}`;
33+
34+
const result = transform('test.js', input, { sourcemap: true });
35+
36+
expect(result.map).toBeDefined();
37+
const parsedMap = JSON.parse(result.map as string);
38+
expect(parsedMap.version).toBe(3);
39+
expect(parsedMap.sources).toContain('original.ts');
40+
expect(parsedMap.mappings.length).toBeGreaterThan(0);
41+
});
42+
});

packages/angular/build/src/tools/sass/rebasing-importer.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import { RawSourceMap } from '@ampproject/remapping';
9+
import type { DecodedSourceMap } from '@ampproject/remapping';
1010
import { MagicString } from 'magic-string';
1111
import { readFileSync, readdirSync, statSync } from 'node:fs';
1212
import { basename, dirname, extname, join, relative } from 'node:path';
@@ -44,7 +44,7 @@ abstract class UrlRebasingImporter implements Importer<'sync'> {
4444
*/
4545
constructor(
4646
private entryDirectory: string,
47-
private rebaseSourceMaps?: Map<string, RawSourceMap>,
47+
private rebaseSourceMaps?: Map<string, DecodedSourceMap>,
4848
) {}
4949

5050
abstract canonicalize(url: string, options: { fromImport: boolean }): URL | null;
@@ -95,12 +95,15 @@ abstract class UrlRebasingImporter implements Importer<'sync'> {
9595
contents = updatedContents.toString();
9696
if (this.rebaseSourceMaps) {
9797
// Generate an intermediate source map for the rebasing changes
98-
const map = updatedContents.generateMap({
98+
const map = updatedContents.generateDecodedMap({
9999
hires: 'boundary',
100100
includeContent: true,
101101
source: canonicalUrl.href,
102102
});
103-
this.rebaseSourceMaps.set(canonicalUrl.href, map as RawSourceMap);
103+
this.rebaseSourceMaps.set(canonicalUrl.href, {
104+
...map,
105+
version: 3,
106+
} satisfies DecodedSourceMap);
104107
}
105108
}
106109

@@ -134,7 +137,7 @@ export class RelativeUrlRebasingImporter extends UrlRebasingImporter {
134137
constructor(
135138
entryDirectory: string,
136139
private directoryCache = new Map<string, DirectoryEntry>(),
137-
rebaseSourceMaps?: Map<string, RawSourceMap>,
140+
rebaseSourceMaps?: Map<string, DecodedSourceMap>,
138141
) {
139142
super(entryDirectory, rebaseSourceMaps);
140143
}
@@ -322,7 +325,7 @@ export class ModuleUrlRebasingImporter extends RelativeUrlRebasingImporter {
322325
constructor(
323326
entryDirectory: string,
324327
directoryCache: Map<string, DirectoryEntry>,
325-
rebaseSourceMaps: Map<string, RawSourceMap> | undefined,
328+
rebaseSourceMaps: Map<string, DecodedSourceMap> | undefined,
326329
private finder: (specifier: string, options: CanonicalizeContext) => URL | null,
327330
) {
328331
super(entryDirectory, directoryCache, rebaseSourceMaps);
@@ -349,7 +352,7 @@ export class LoadPathsUrlRebasingImporter extends RelativeUrlRebasingImporter {
349352
constructor(
350353
entryDirectory: string,
351354
directoryCache: Map<string, DirectoryEntry>,
352-
rebaseSourceMaps: Map<string, RawSourceMap> | undefined,
355+
rebaseSourceMaps: Map<string, DecodedSourceMap> | undefined,
353356
private loadPaths: Iterable<string>,
354357
) {
355358
super(entryDirectory, directoryCache, rebaseSourceMaps);

packages/angular/build/src/tools/sass/worker.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import mergeSourceMaps, { RawSourceMap } from '@ampproject/remapping';
9+
import mergeSourceMaps, { type DecodedSourceMap, type RawSourceMap } from '@ampproject/remapping';
1010
import { dirname } from 'node:path';
1111
import { fileURLToPath, pathToFileURL } from 'node:url';
1212
import { MessagePort, receiveMessageOnPort } from 'node:worker_threads';
@@ -89,7 +89,7 @@ export default async function renderSassStylesheet(
8989
let warnings: SerializableWarningMessage[] | undefined;
9090
try {
9191
const directoryCache = new Map<string, DirectoryEntry>();
92-
const rebaseSourceMaps = options.sourceMap ? new Map<string, RawSourceMap>() : undefined;
92+
const rebaseSourceMaps = options.sourceMap ? new Map<string, DecodedSourceMap>() : undefined;
9393
if (importerChannel) {
9494
// When a custom importer function is present, the importer request must be proxied
9595
// back to the main thread where it can be executed.

0 commit comments

Comments
 (0)