Skip to content

Commit ecd6ec4

Browse files
clydinangular-robot[bot]
authored andcommitted
refactor(@angular-devkit/build-angular): encapsulate Angular compilation within esbuild compiler plugin
The creation of the esbuild Angular plugin's Angular compilation has now been consolidated in a separate class. This refactor reduces the amount of code within the plugin's main start function as well as centralizing initialization, analysis, and source file emitting for the Angular build process.
1 parent b5d42f4 commit ecd6ec4

File tree

2 files changed

+272
-198
lines changed

2 files changed

+272
-198
lines changed
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
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.io/license
7+
*/
8+
9+
import type ng from '@angular/compiler-cli';
10+
import assert from 'node:assert';
11+
import ts from 'typescript';
12+
import { loadEsmModule } from '../../utils/load-esm';
13+
import {
14+
AngularHostOptions,
15+
createAngularCompilerHost,
16+
ensureSourceFileVersions,
17+
} from './angular-host';
18+
import { profileAsync, profileSync } from './profiling';
19+
20+
// Temporary deep import for transformer support
21+
// TODO: Move these to a private exports location or move the implementation into this package.
22+
const { mergeTransformers, replaceBootstrap } = require('@ngtools/webpack/src/ivy/transformation');
23+
24+
class AngularCompilationState {
25+
constructor(
26+
public readonly angularProgram: ng.NgtscProgram,
27+
public readonly typeScriptProgram: ts.EmitAndSemanticDiagnosticsBuilderProgram,
28+
public readonly affectedFiles: ReadonlySet<ts.SourceFile>,
29+
public readonly templateDiagnosticsOptimization: ng.OptimizeFor,
30+
public readonly diagnosticCache = new WeakMap<ts.SourceFile, ts.Diagnostic[]>(),
31+
) {}
32+
33+
get angularCompiler() {
34+
return this.angularProgram.compiler;
35+
}
36+
}
37+
38+
export interface EmitFileResult {
39+
content?: string;
40+
map?: string;
41+
dependencies: readonly string[];
42+
}
43+
export type FileEmitter = (file: string) => Promise<EmitFileResult | undefined>;
44+
45+
export class AngularCompilation {
46+
static #angularCompilerCliModule?: typeof ng;
47+
48+
#state?: AngularCompilationState;
49+
50+
static async loadCompilerCli(): Promise<typeof ng> {
51+
// This uses a wrapped dynamic import to load `@angular/compiler-cli` which is ESM.
52+
// Once TypeScript provides support for retaining dynamic imports this workaround can be dropped.
53+
this.#angularCompilerCliModule ??= await loadEsmModule<typeof ng>('@angular/compiler-cli');
54+
55+
return this.#angularCompilerCliModule;
56+
}
57+
58+
constructor() {}
59+
60+
async initialize(
61+
rootNames: string[],
62+
compilerOptions: ng.CompilerOptions,
63+
hostOptions: AngularHostOptions,
64+
configurationDiagnostics?: ts.Diagnostic[],
65+
): Promise<{ affectedFiles: ReadonlySet<ts.SourceFile> }> {
66+
// Dynamically load the Angular compiler CLI package
67+
const { NgtscProgram, OptimizeFor } = await AngularCompilation.loadCompilerCli();
68+
69+
// Create Angular compiler host
70+
const host = createAngularCompilerHost(compilerOptions, hostOptions);
71+
72+
// Create the Angular specific program that contains the Angular compiler
73+
const angularProgram = profileSync(
74+
'NG_CREATE_PROGRAM',
75+
() => new NgtscProgram(rootNames, compilerOptions, host, this.#state?.angularProgram),
76+
);
77+
const angularCompiler = angularProgram.compiler;
78+
const angularTypeScriptProgram = angularProgram.getTsProgram();
79+
ensureSourceFileVersions(angularTypeScriptProgram);
80+
81+
const typeScriptProgram = ts.createEmitAndSemanticDiagnosticsBuilderProgram(
82+
angularTypeScriptProgram,
83+
host,
84+
this.#state?.typeScriptProgram,
85+
configurationDiagnostics,
86+
);
87+
88+
await profileAsync('NG_ANALYZE_PROGRAM', () => angularCompiler.analyzeAsync());
89+
const affectedFiles = profileSync('NG_FIND_AFFECTED', () =>
90+
findAffectedFiles(typeScriptProgram, angularCompiler),
91+
);
92+
93+
this.#state = new AngularCompilationState(
94+
angularProgram,
95+
typeScriptProgram,
96+
affectedFiles,
97+
affectedFiles.size === 1 ? OptimizeFor.SingleFile : OptimizeFor.WholeProgram,
98+
this.#state?.diagnosticCache,
99+
);
100+
101+
return { affectedFiles };
102+
}
103+
104+
*collectDiagnostics(): Iterable<ts.Diagnostic> {
105+
assert(this.#state, 'Angular compilation must be initialized prior to collecting diagnostics.');
106+
const {
107+
affectedFiles,
108+
angularCompiler,
109+
diagnosticCache,
110+
templateDiagnosticsOptimization,
111+
typeScriptProgram,
112+
} = this.#state;
113+
114+
// Collect program level diagnostics
115+
yield* typeScriptProgram.getConfigFileParsingDiagnostics();
116+
yield* angularCompiler.getOptionDiagnostics();
117+
yield* typeScriptProgram.getOptionsDiagnostics();
118+
yield* typeScriptProgram.getGlobalDiagnostics();
119+
120+
// Collect source file specific diagnostics
121+
for (const sourceFile of typeScriptProgram.getSourceFiles()) {
122+
if (angularCompiler.ignoreForDiagnostics.has(sourceFile)) {
123+
continue;
124+
}
125+
126+
// TypeScript will use cached diagnostics for files that have not been
127+
// changed or affected for this build when using incremental building.
128+
yield* profileSync(
129+
'NG_DIAGNOSTICS_SYNTACTIC',
130+
() => typeScriptProgram.getSyntacticDiagnostics(sourceFile),
131+
true,
132+
);
133+
yield* profileSync(
134+
'NG_DIAGNOSTICS_SEMANTIC',
135+
() => typeScriptProgram.getSemanticDiagnostics(sourceFile),
136+
true,
137+
);
138+
139+
// Declaration files cannot have template diagnostics
140+
if (sourceFile.isDeclarationFile) {
141+
continue;
142+
}
143+
144+
// Only request Angular template diagnostics for affected files to avoid
145+
// overhead of template diagnostics for unchanged files.
146+
if (affectedFiles.has(sourceFile)) {
147+
const angularDiagnostics = profileSync(
148+
'NG_DIAGNOSTICS_TEMPLATE',
149+
() => angularCompiler.getDiagnosticsForFile(sourceFile, templateDiagnosticsOptimization),
150+
true,
151+
);
152+
diagnosticCache.set(sourceFile, angularDiagnostics);
153+
yield* angularDiagnostics;
154+
} else {
155+
const angularDiagnostics = diagnosticCache.get(sourceFile);
156+
if (angularDiagnostics) {
157+
yield* angularDiagnostics;
158+
}
159+
}
160+
}
161+
}
162+
163+
createFileEmitter(onAfterEmit?: (sourceFile: ts.SourceFile) => void): FileEmitter {
164+
assert(this.#state, 'Angular compilation must be initialized prior to emitting files.');
165+
const { angularCompiler, typeScriptProgram } = this.#state;
166+
167+
const transformers = mergeTransformers(angularCompiler.prepareEmit().transformers, {
168+
before: [replaceBootstrap(() => typeScriptProgram.getProgram().getTypeChecker())],
169+
});
170+
171+
return async (file: string) => {
172+
const sourceFile = typeScriptProgram.getSourceFile(file);
173+
if (!sourceFile) {
174+
return undefined;
175+
}
176+
177+
let content: string | undefined;
178+
typeScriptProgram.emit(
179+
sourceFile,
180+
(filename, data) => {
181+
if (/\.[cm]?js$/.test(filename)) {
182+
content = data;
183+
}
184+
},
185+
undefined /* cancellationToken */,
186+
undefined /* emitOnlyDtsFiles */,
187+
transformers,
188+
);
189+
190+
angularCompiler.incrementalCompilation.recordSuccessfulEmit(sourceFile);
191+
onAfterEmit?.(sourceFile);
192+
193+
return { content, dependencies: [] };
194+
};
195+
}
196+
}
197+
198+
function findAffectedFiles(
199+
builder: ts.EmitAndSemanticDiagnosticsBuilderProgram,
200+
{ ignoreForDiagnostics, ignoreForEmit, incrementalCompilation }: ng.NgtscProgram['compiler'],
201+
): Set<ts.SourceFile> {
202+
const affectedFiles = new Set<ts.SourceFile>();
203+
204+
// eslint-disable-next-line no-constant-condition
205+
while (true) {
206+
const result = builder.getSemanticDiagnosticsOfNextAffectedFile(undefined, (sourceFile) => {
207+
// If the affected file is a TTC shim, add the shim's original source file.
208+
// This ensures that changes that affect TTC are typechecked even when the changes
209+
// are otherwise unrelated from a TS perspective and do not result in Ivy codegen changes.
210+
// For example, changing @Input property types of a directive used in another component's
211+
// template.
212+
// A TTC shim is a file that has been ignored for diagnostics and has a filename ending in `.ngtypecheck.ts`.
213+
if (ignoreForDiagnostics.has(sourceFile) && sourceFile.fileName.endsWith('.ngtypecheck.ts')) {
214+
// This file name conversion relies on internal compiler logic and should be converted
215+
// to an official method when available. 15 is length of `.ngtypecheck.ts`
216+
const originalFilename = sourceFile.fileName.slice(0, -15) + '.ts';
217+
const originalSourceFile = builder.getSourceFile(originalFilename);
218+
if (originalSourceFile) {
219+
affectedFiles.add(originalSourceFile);
220+
}
221+
222+
return true;
223+
}
224+
225+
return false;
226+
});
227+
228+
if (!result) {
229+
break;
230+
}
231+
232+
affectedFiles.add(result.affected as ts.SourceFile);
233+
}
234+
235+
// A file is also affected if the Angular compiler requires it to be emitted
236+
for (const sourceFile of builder.getSourceFiles()) {
237+
if (ignoreForEmit.has(sourceFile) || incrementalCompilation.safeToSkipEmit(sourceFile)) {
238+
continue;
239+
}
240+
241+
affectedFiles.add(sourceFile);
242+
}
243+
244+
return affectedFiles;
245+
}

0 commit comments

Comments
 (0)