Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 09d0a11

Browse filesBrowse files
committed
perf(@angular/build): enable fast-path AST printing with sourcemaps in AotCompilation
Previously, when sourcemaps were enabled, AotCompilation was forced to run the full, slow TypeScript compilation/emit path (`useTypeScriptTranspilation = true`) because the standard `ts.createPrinter().printFile` method does not support generating sourcemaps. This commit leverages TypeScript's internal `createTextWriter`, `createSourceMapGenerator`, and `printer.writeFile` APIs to generate sourcemaps directly during AST printing. This allows AotCompilation to use the fast AST transformation and printing path even when sourcemaps are enabled (as long as `isolatedModules` is true), drastically improving rebuild performance for dev server builds.
1 parent 41bd100 commit 09d0a11
Copy full SHA for 09d0a11

4 files changed

+205-14Lines changed: 205 additions & 14 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎packages/angular/build/src/tools/angular/compilation/aot-compilation.ts‎

Copy file name to clipboardExpand all lines: packages/angular/build/src/tools/angular/compilation/aot-compilation.ts
+35-9Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { lazyRoutesTransformer } from '../transformers/lazy-routes-transformer';
2121
import { createWorkerTransformer } from '../transformers/web-worker-transformer';
2222
import { AngularCompilation, DiagnosticModes, EmitFileResult } from './angular-compilation';
2323
import { collectHmrCandidates } from './hmr-candidates';
24+
import { printSourceFileWithMap } from './typescript-printer';
2425

2526
/**
2627
* The modified files count limit for performing component HMR analysis.
@@ -37,6 +38,7 @@ class AngularCompilationState {
3738
public readonly affectedFiles: ReadonlySet<ts.SourceFile>,
3839
public readonly templateDiagnosticsOptimization: ng.OptimizeFor,
3940
public readonly webWorkerTransform: ts.TransformerFactory<ts.SourceFile>,
41+
public readonly useTypeScriptTranspilation: boolean,
4042
public readonly diagnosticCache = new WeakMap<ts.SourceFile, ts.Diagnostic[]>(),
4143
) {}
4244

@@ -76,6 +78,10 @@ export class AotCompilation extends AngularCompilation {
7678
const compilerOptions =
7779
compilerOptionsTransformer?.(originalCompilerOptions) ?? originalCompilerOptions;
7880

81+
const useTypeScriptTranspilation =
82+
(compilerOptions['_useTypeScriptTranspilation'] as boolean | undefined) ??
83+
!compilerOptions.isolatedModules;
84+
7985
if (compilerOptions.externalRuntimeStyles) {
8086
hostOptions.externalStylesheets ??= new Map();
8187
}
@@ -209,6 +215,7 @@ export class AotCompilation extends AngularCompilation {
209215
affectedFiles,
210216
affectedFiles.size === 1 ? OptimizeFor.SingleFile : OptimizeFor.WholeProgram,
211217
createWorkerTransformer(hostOptions.processWebWorker.bind(hostOptions)),
218+
useTypeScriptTranspilation,
212219
this.#state?.diagnosticCache,
213220
);
214221

@@ -297,14 +304,16 @@ export class AotCompilation extends AngularCompilation {
297304

298305
emitAffectedFiles(): Iterable<EmitFileResult> {
299306
assert(this.#state, 'Angular compilation must be initialized prior to emitting files.');
300-
const { affectedFiles, angularCompiler, compilerHost, typeScriptProgram, webWorkerTransform } =
301-
this.#state;
307+
const {
308+
affectedFiles,
309+
angularCompiler,
310+
compilerHost,
311+
typeScriptProgram,
312+
webWorkerTransform,
313+
useTypeScriptTranspilation,
314+
} = this.#state;
302315
const compilerOptions = typeScriptProgram.getCompilerOptions();
303316
const buildInfoFilename = compilerOptions.tsBuildInfoFile ?? '.tsbuildinfo';
304-
const useTypeScriptTranspilation =
305-
!compilerOptions.isolatedModules ||
306-
!!compilerOptions.sourceMap ||
307-
!!compilerOptions.inlineSourceMap;
308317

309318
const emittedFiles = new Map<ts.SourceFile, EmitFileResult>();
310319
const writeFileCallback: ts.WriteFileCallback = (filename, contents, _a, _b, sourceFiles) => {
@@ -401,14 +410,31 @@ export class AotCompilation extends AngularCompilation {
401410
'TypeScript transforms should not produce multiple outputs for ' + sourceFile.fileName,
402411
);
403412

404-
let contents;
413+
let contents: string;
405414
if (sourceFile === transformResult.transformed[0]) {
406415
// Use original content if no changes were made
407416
contents = sourceFile.text;
408417
} else {
409-
// Otherwise, print the transformed source file
418+
// Otherwise, print the transformed source file with map if needed
410419
const printer = ts.createPrinter(compilerOptions, transformResult);
411-
contents = printer.printFile(transformResult.transformed[0]);
420+
const printResult = printSourceFileWithMap(
421+
transformResult.transformed[0],
422+
printer,
423+
compilerHost,
424+
compilerOptions,
425+
);
426+
427+
contents = printResult.code;
428+
429+
if (printResult.map) {
430+
if (compilerOptions.inlineSourceMap) {
431+
const base64Map = Buffer.from(printResult.map).toString('base64');
432+
contents += `\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,${base64Map}`;
433+
} else if (compilerOptions.sourceMap) {
434+
const mapFilename = sourceFile.fileName + '.map';
435+
emittedFiles.set(sourceFile, { filename: mapFilename, contents: printResult.map });
436+
}
437+
}
412438
}
413439

414440
angularCompiler.incrementalCompilation.recordSuccessfulEmit(sourceFile);
Collapse file

‎packages/angular/build/src/tools/angular/compilation/parallel-worker.ts‎

Copy file name to clipboardExpand all lines: packages/angular/build/src/tools/angular/compilation/parallel-worker.ts
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ export async function initialize(request: InitRequest) {
114114
isolatedModules: compilerOptions.isolatedModules,
115115
sourceMap: compilerOptions.sourceMap,
116116
inlineSourceMap: compilerOptions.inlineSourceMap,
117+
_useTypeScriptTranspilation: compilerOptions['_useTypeScriptTranspilation'] as
118+
boolean | undefined,
117119
},
118120
componentResourcesDependencies,
119121
};
Collapse file
+161Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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+
/**
10+
* @fileoverview Helper functions and typings for utilizing internal TypeScript printer
11+
* and sourcemap generation APIs.
12+
*/
13+
14+
import assert from 'node:assert';
15+
import { dirname } from 'node:path';
16+
import ts from 'typescript';
17+
18+
/**
19+
* Partial interface representing the internal TypeScript `EmitTextWriter` API.
20+
* Used to collect printed text output from the printer.
21+
*/
22+
export interface EmitTextWriter {
23+
write(s: string): void;
24+
rawWrite(s: string): void;
25+
writeLine(force?: boolean): void;
26+
getText(): string;
27+
clear(): void;
28+
isAtStartOfLine(): boolean;
29+
getTextPos(): number;
30+
writeComment(s: string): void;
31+
}
32+
33+
/**
34+
* Partial interface representing the internal TypeScript `SourceMapGenerator` API.
35+
* Used to construct mappings and serialize a sourcemap.
36+
*/
37+
export interface SourceMapGenerator {
38+
getSources(): string[];
39+
addSource(fileName: string): number;
40+
setSourceContent(sourceIndex: number, content: string | null): void;
41+
addMapping(
42+
generatedLine: number,
43+
generatedCharacter: number,
44+
sourceIndex: number,
45+
sourceLine: number,
46+
sourceCharacter: number,
47+
nameIndex?: number,
48+
): void;
49+
toJSON(): object;
50+
toString(): string;
51+
}
52+
53+
/**
54+
* Extended `ts.Printer` interface containing the internal `writeFile` method
55+
* which supports sourcemap generation.
56+
*/
57+
export interface ExtendedPrinter extends ts.Printer {
58+
writeFile(
59+
sourceFile: ts.SourceFile,
60+
writer: EmitTextWriter,
61+
sourceMapGenerator?: SourceMapGenerator,
62+
): void;
63+
}
64+
65+
/**
66+
* Typing structure for internal TypeScript module exports that are not exposed
67+
* in the public `@types/typescript` package.
68+
*/
69+
export interface TypeScriptInternals {
70+
createTextWriter(newLine: string): EmitTextWriter;
71+
createSourceMapGenerator(
72+
host: {
73+
getCurrentDirectory(): string;
74+
getCanonicalFileName(fileName: string): string;
75+
},
76+
file: string,
77+
sourceRoot: string | undefined,
78+
sourcesDirectoryPath: string,
79+
generatorOptions: ts.CompilerOptions,
80+
): SourceMapGenerator;
81+
}
82+
83+
const tsInternals = ts as unknown as TypeScriptInternals;
84+
85+
/**
86+
* Asserts that the required internal TypeScript APIs are present in the currently
87+
* loaded TypeScript module.
88+
*
89+
* @throws {AssertionError} If any required internal API is missing.
90+
*/
91+
export function assertTypeScriptPrinterInternals(): void {
92+
assert(
93+
typeof tsInternals.createTextWriter === 'function',
94+
'TypeScript internal "createTextWriter" is missing.',
95+
);
96+
assert(
97+
typeof tsInternals.createSourceMapGenerator === 'function',
98+
'TypeScript internal "createSourceMapGenerator" is missing.',
99+
);
100+
}
101+
102+
/**
103+
* Result object returned from printing a source file.
104+
*/
105+
export interface PrintResult {
106+
/** The printed source code text. */
107+
code: string;
108+
109+
/** The generated sourcemap JSON string, if sourcemap generation was requested. */
110+
map?: string;
111+
}
112+
113+
/**
114+
* Prints a TypeScript source file AST to a string, optionally generating a sourcemap
115+
* using internal TypeScript APIs.
116+
*
117+
* @param sourceFile The TypeScript AST node representing the file to print.
118+
* @param printer The printer instance to print the file with.
119+
* @param compilerHost The TypeScript compiler host, used for path canonicalization and context.
120+
* @param compilerOptions The compiler options configured for the build target.
121+
* @returns A result containing the printed code and optional sourcemap text.
122+
*/
123+
export function printSourceFileWithMap(
124+
sourceFile: ts.SourceFile,
125+
printer: ts.Printer,
126+
compilerHost: ts.CompilerHost,
127+
compilerOptions: ts.CompilerOptions,
128+
): PrintResult {
129+
const shouldGenerateMap = compilerOptions.sourceMap || compilerOptions.inlineSourceMap;
130+
if (!shouldGenerateMap) {
131+
return { code: printer.printFile(sourceFile) };
132+
}
133+
134+
assertTypeScriptPrinterInternals();
135+
136+
const extendedPrinter = printer as ExtendedPrinter;
137+
assert(
138+
typeof extendedPrinter.writeFile === 'function',
139+
'TypeScript Printer is missing internal "writeFile" method.',
140+
);
141+
142+
const writer = tsInternals.createTextWriter(compilerHost.getNewLine());
143+
144+
const sourceMapGenerator = tsInternals.createSourceMapGenerator(
145+
{
146+
getCurrentDirectory: () => compilerHost.getCurrentDirectory(),
147+
getCanonicalFileName: (fileName) => compilerHost.getCanonicalFileName(fileName),
148+
},
149+
sourceFile.fileName,
150+
compilerOptions.sourceRoot,
151+
dirname(sourceFile.fileName),
152+
compilerOptions,
153+
);
154+
155+
extendedPrinter.writeFile(sourceFile, writer, sourceMapGenerator);
156+
157+
const code = writer.getText();
158+
const map = sourceMapGenerator.toString();
159+
160+
return { code, map };
161+
}
Collapse file

‎packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts‎

Copy file name to clipboardExpand all lines: packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts
+7-5Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -318,12 +318,8 @@ export function createCompilerPlugin(
318318
),
319319
);
320320
shouldTsIgnoreJs = !initializationResult.compilerOptions.allowJs;
321-
// Isolated modules option ensures safe non-TypeScript transpilation.
322-
// Typescript printing support for sourcemaps is not yet integrated.
323321
useTypeScriptTranspilation =
324-
!initializationResult.compilerOptions.isolatedModules ||
325-
!!initializationResult.compilerOptions.sourceMap ||
326-
!!initializationResult.compilerOptions.inlineSourceMap;
322+
!!initializationResult.compilerOptions['_useTypeScriptTranspilation'];
327323
referencedFiles = initializationResult.referencedFiles;
328324
externalStylesheets = initializationResult.externalStylesheets;
329325
if (initializationResult.templateUpdates) {
@@ -753,6 +749,12 @@ function createCompilerOptionsTransformer(
753749
preserveSymlinks,
754750
externalRuntimeStyles: pluginOptions.externalRuntimeStyles,
755751
_enableHmr: !!pluginOptions.templateUpdates,
752+
// TypeScript transpilation is forced if:
753+
// - isolatedModules is disabled (TS needs full module types to emit JS).
754+
// - Karma code coverage is active (the coverage instrumentation transformer is Babel-based
755+
// and cannot parse raw TypeScript code; Vitest handles coverage instrumentation downstream).
756+
_useTypeScriptTranspilation:
757+
!compilerOptions.isolatedModules || !!pluginOptions.instrumentForCoverage,
756758
supportTestBed: !!pluginOptions.includeTestMetadata,
757759
supportJitMode: !!pluginOptions.includeTestMetadata,
758760
};

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.