From 7ecf1068618e1e039009b2114cc1c919937569d7 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 5 Aug 2026 18:18:11 -0400 Subject: [PATCH] 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. --- .../src/tools/angular/linker/oxc-linker.ts | 11 +++-- .../tools/angular/linker/oxc-linker_spec.ts | 49 +++++++++++++++++++ .../build/src/tools/oxc/oxc-transform.ts | 11 +++-- .../build/src/tools/oxc/oxc-transform_spec.ts | 42 ++++++++++++++++ .../build/src/tools/sass/rebasing-importer.ts | 17 ++++--- .../angular/build/src/tools/sass/worker.ts | 4 +- 6 files changed, 117 insertions(+), 17 deletions(-) create mode 100644 packages/angular/build/src/tools/oxc/oxc-transform_spec.ts diff --git a/packages/angular/build/src/tools/angular/linker/oxc-linker.ts b/packages/angular/build/src/tools/angular/linker/oxc-linker.ts index 467a1332cd97..6743fb720c21 100644 --- a/packages/angular/build/src/tools/angular/linker/oxc-linker.ts +++ b/packages/angular/build/src/tools/angular/linker/oxc-linker.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import type { EncodedSourceMap } from '@ampproject/remapping'; +import type { DecodedSourceMap } from '@ampproject/remapping'; import remapping from '@ampproject/remapping'; import { ConsoleLogger, LogLevel } from '@angular/compiler-cli'; import type { DeclarationScope } from '@angular/compiler-cli/linker'; @@ -170,12 +170,15 @@ export function linkWithOxc(filename: string, code: string, options: OxcLinkerOp let map: string | undefined; if (options.sourcemap) { - const rawMap = s.generateMap({ hires: true, source: filename }); const inputMap = loadInputSourceMap(filename, code); if (inputMap) { - map = remapping([rawMap as EncodedSourceMap, inputMap], () => null).toString(); + const rawMap = s.generateDecodedMap({ hires: true, source: filename }); + map = remapping( + [{ ...rawMap, version: 3 } satisfies DecodedSourceMap, inputMap], + () => null, + ).toString(); } else { - map = rawMap.toString(); + map = s.generateMap({ hires: true, source: filename }).toString(); } } diff --git a/packages/angular/build/src/tools/angular/linker/oxc-linker_spec.ts b/packages/angular/build/src/tools/angular/linker/oxc-linker_spec.ts index 148c67f9dce3..6b1ae742581a 100644 --- a/packages/angular/build/src/tools/angular/linker/oxc-linker_spec.ts +++ b/packages/angular/build/src/tools/angular/linker/oxc-linker_spec.ts @@ -53,4 +53,53 @@ describe('linkWithOxc', () => { expect(result.code).toContain('i0.ɵɵdefineComponent'); expect(result.code).not.toContain('i0.ɵɵngDeclareComponent'); }); + + it('should generate a sourcemap when sourcemap option is enabled', () => { + const input = ` + import * as i0 from "@angular/core"; + export class MyDirective {} + MyDirective.ɵdir = i0.ɵɵngDeclareDirective({ + minVersion: "12.0.0", + version: "14.0.0", + ngImport: i0, + type: MyDirective, + selector: "[my-dir]" + }); + `; + + const result = linkWithOxc('test.js', input, { sourcemap: true }); + expect(result.map).toBeDefined(); + const parsedMap = JSON.parse(result.map as string); + expect(parsedMap.version).toBe(3); + expect(parsedMap.sources).toContain('test.js'); + }); + + it('should remap with input sourcemap when sourcemap option is enabled and inputMap is present', () => { + const inputMap = { + version: 3, + sources: ['original.ts'], + sourcesContent: ['// original content'], + mappings: 'AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA', + names: [], + }; + const base64Map = Buffer.from(JSON.stringify(inputMap)).toString('base64'); + const input = ` + import * as i0 from "@angular/core"; + export class MyDirective {} + MyDirective.ɵdir = i0.ɵɵngDeclareDirective({ + minVersion: "12.0.0", + version: "14.0.0", + ngImport: i0, + type: MyDirective, + selector: "[my-dir]" + }); + //# sourceMappingURL=data:application/json;base64,${base64Map} + `; + + const result = linkWithOxc('test.js', input, { sourcemap: true }); + expect(result.map).toBeDefined(); + const parsedMap = JSON.parse(result.map as string); + expect(parsedMap.version).toBe(3); + expect(parsedMap.sources).toContain('original.ts'); + }); }); diff --git a/packages/angular/build/src/tools/oxc/oxc-transform.ts b/packages/angular/build/src/tools/oxc/oxc-transform.ts index 64a5aa74062e..7f373ff09f08 100644 --- a/packages/angular/build/src/tools/oxc/oxc-transform.ts +++ b/packages/angular/build/src/tools/oxc/oxc-transform.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import remapping, { type EncodedSourceMap } from '@ampproject/remapping'; +import remapping, { type DecodedSourceMap } from '@ampproject/remapping'; import type { BindingIdentifier, Class, Node } from '@oxc-project/types'; import { MagicString } from 'magic-string'; import { Visitor, parseSync } from 'oxc-parser'; @@ -749,13 +749,16 @@ export function transform(filename: string, code: string, options: OxcTransformO let map: string | undefined; if (options.sourcemap) { - const rawMap = s.generateMap({ hires: true, source: filename }); const inputMap = loadInputSourceMap(filename, code); if (inputMap) { - map = remapping([rawMap as EncodedSourceMap, inputMap], () => null).toString(); + const rawMap = s.generateDecodedMap({ hires: true, source: filename }); + map = remapping( + [{ ...rawMap, version: 3 } satisfies DecodedSourceMap, inputMap], + () => null, + ).toString(); } else { - map = rawMap.toString(); + map = s.generateMap({ hires: true, source: filename }).toString(); } } diff --git a/packages/angular/build/src/tools/oxc/oxc-transform_spec.ts b/packages/angular/build/src/tools/oxc/oxc-transform_spec.ts new file mode 100644 index 000000000000..ee37c7fec1fb --- /dev/null +++ b/packages/angular/build/src/tools/oxc/oxc-transform_spec.ts @@ -0,0 +1,42 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { transform } from './oxc-transform'; + +describe('oxc-transform sourcemaps', () => { + it('should generate a sourcemap when sourcemap option is enabled without inputMap', () => { + const input = 'var result = new SomeClass();'; + const result = transform('test.js', input, { sourcemap: true }); + + expect(result.map).toBeDefined(); + const parsedMap = JSON.parse(result.map as string); + expect(parsedMap.version).toBe(3); + expect(parsedMap.sources).toContain('test.js'); + expect(parsedMap.mappings.length).toBeGreaterThan(0); + }); + + it('should remap with input sourcemap when sourcemap option is enabled and inputMap is present', () => { + const inputMap = { + version: 3, + sources: ['original.ts'], + sourcesContent: ['const result = new SomeClass();'], + mappings: 'AAAA', + names: [], + }; + const base64Map = Buffer.from(JSON.stringify(inputMap)).toString('base64'); + const input = `var result = new SomeClass();\n//# sourceMappingURL=data:application/json;base64,${base64Map}`; + + const result = transform('test.js', input, { sourcemap: true }); + + expect(result.map).toBeDefined(); + const parsedMap = JSON.parse(result.map as string); + expect(parsedMap.version).toBe(3); + expect(parsedMap.sources).toContain('original.ts'); + expect(parsedMap.mappings.length).toBeGreaterThan(0); + }); +}); diff --git a/packages/angular/build/src/tools/sass/rebasing-importer.ts b/packages/angular/build/src/tools/sass/rebasing-importer.ts index ccc90014616d..5d2a4ddc2267 100644 --- a/packages/angular/build/src/tools/sass/rebasing-importer.ts +++ b/packages/angular/build/src/tools/sass/rebasing-importer.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import { RawSourceMap } from '@ampproject/remapping'; +import type { DecodedSourceMap } from '@ampproject/remapping'; import { MagicString } from 'magic-string'; import { readFileSync, readdirSync, statSync } from 'node:fs'; import { basename, dirname, extname, join, relative } from 'node:path'; @@ -44,7 +44,7 @@ abstract class UrlRebasingImporter implements Importer<'sync'> { */ constructor( private entryDirectory: string, - private rebaseSourceMaps?: Map, + private rebaseSourceMaps?: Map, ) {} abstract canonicalize(url: string, options: { fromImport: boolean }): URL | null; @@ -95,12 +95,15 @@ abstract class UrlRebasingImporter implements Importer<'sync'> { contents = updatedContents.toString(); if (this.rebaseSourceMaps) { // Generate an intermediate source map for the rebasing changes - const map = updatedContents.generateMap({ + const map = updatedContents.generateDecodedMap({ hires: 'boundary', includeContent: true, source: canonicalUrl.href, }); - this.rebaseSourceMaps.set(canonicalUrl.href, map as RawSourceMap); + this.rebaseSourceMaps.set(canonicalUrl.href, { + ...map, + version: 3, + } satisfies DecodedSourceMap); } } @@ -134,7 +137,7 @@ export class RelativeUrlRebasingImporter extends UrlRebasingImporter { constructor( entryDirectory: string, private directoryCache = new Map(), - rebaseSourceMaps?: Map, + rebaseSourceMaps?: Map, ) { super(entryDirectory, rebaseSourceMaps); } @@ -322,7 +325,7 @@ export class ModuleUrlRebasingImporter extends RelativeUrlRebasingImporter { constructor( entryDirectory: string, directoryCache: Map, - rebaseSourceMaps: Map | undefined, + rebaseSourceMaps: Map | undefined, private finder: (specifier: string, options: CanonicalizeContext) => URL | null, ) { super(entryDirectory, directoryCache, rebaseSourceMaps); @@ -349,7 +352,7 @@ export class LoadPathsUrlRebasingImporter extends RelativeUrlRebasingImporter { constructor( entryDirectory: string, directoryCache: Map, - rebaseSourceMaps: Map | undefined, + rebaseSourceMaps: Map | undefined, private loadPaths: Iterable, ) { super(entryDirectory, directoryCache, rebaseSourceMaps); diff --git a/packages/angular/build/src/tools/sass/worker.ts b/packages/angular/build/src/tools/sass/worker.ts index 1a2e1184892f..e4167a3d1c69 100644 --- a/packages/angular/build/src/tools/sass/worker.ts +++ b/packages/angular/build/src/tools/sass/worker.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import mergeSourceMaps, { RawSourceMap } from '@ampproject/remapping'; +import mergeSourceMaps, { type DecodedSourceMap, type RawSourceMap } from '@ampproject/remapping'; import { dirname } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { MessagePort, receiveMessageOnPort } from 'node:worker_threads'; @@ -89,7 +89,7 @@ export default async function renderSassStylesheet( let warnings: SerializableWarningMessage[] | undefined; try { const directoryCache = new Map(); - const rebaseSourceMaps = options.sourceMap ? new Map() : undefined; + const rebaseSourceMaps = options.sourceMap ? new Map() : undefined; if (importerChannel) { // When a custom importer function is present, the importer request must be proxied // back to the main thread where it can be executed.