Skip to content

Commit d5fed32

Browse files
clydinangular-robot[bot]
authored andcommitted
refactor(@angular-devkit/build-angular): move TS/NG configuration reading into compiler classes
The TypeScript and Angular configuration reading (`tsconfig.json`) has now been moved into the Angular compilation classes (AOT/JIT) directly. This removes compilation specific functionality from the esbuild plugin and allows the plugin to focus primarily on integration with esbuild.
1 parent 6023adf commit d5fed32

File tree

4 files changed

+157
-137
lines changed

4 files changed

+157
-137
lines changed

packages/angular_devkit/build_angular/src/builders/browser-esbuild/angular/angular-compilation.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import type ng from '@angular/compiler-cli';
1010
import type ts from 'typescript';
1111
import { loadEsmModule } from '../../../utils/load-esm';
12+
import { profileSync } from '../profiling';
1213
import type { AngularHostOptions } from './angular-host';
1314

1415
export interface EmitFileResult {
@@ -32,12 +33,29 @@ export abstract class AngularCompilation {
3233
return AngularCompilation.#angularCompilerCliModule;
3334
}
3435

36+
protected async loadConfiguration(tsconfig: string): Promise<ng.CompilerOptions> {
37+
const { readConfiguration } = await AngularCompilation.loadCompilerCli();
38+
39+
return profileSync('NG_READ_CONFIG', () =>
40+
readConfiguration(tsconfig, {
41+
// Angular specific configuration defaults and overrides to ensure a functioning compilation.
42+
suppressOutputPathCheck: true,
43+
outDir: undefined,
44+
sourceMap: false,
45+
declaration: false,
46+
declarationMap: false,
47+
allowEmptyCodegenFiles: false,
48+
annotationsAs: 'decorators',
49+
enableResourceInlining: false,
50+
}),
51+
);
52+
}
53+
3554
abstract initialize(
36-
rootNames: string[],
37-
compilerOptions: ts.CompilerOptions,
55+
tsconfig: string,
3856
hostOptions: AngularHostOptions,
39-
configurationDiagnostics?: ts.Diagnostic[],
40-
): Promise<{ affectedFiles: ReadonlySet<ts.SourceFile> }>;
57+
compilerOptionsTransformer?: (compilerOptions: ng.CompilerOptions) => ng.CompilerOptions,
58+
): Promise<{ affectedFiles: ReadonlySet<ts.SourceFile>; compilerOptions: ng.CompilerOptions }>;
4159

4260
abstract collectDiagnostics(): Iterable<ts.Diagnostic>;
4361

packages/angular_devkit/build_angular/src/builders/browser-esbuild/angular/aot-compilation.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,22 @@ export class AotCompilation extends AngularCompilation {
3939
#state?: AngularCompilationState;
4040

4141
async initialize(
42-
rootNames: string[],
43-
compilerOptions: ng.CompilerOptions,
42+
tsconfig: string,
4443
hostOptions: AngularHostOptions,
45-
configurationDiagnostics?: ts.Diagnostic[],
46-
): Promise<{ affectedFiles: ReadonlySet<ts.SourceFile> }> {
44+
compilerOptionsTransformer?: (compilerOptions: ng.CompilerOptions) => ng.CompilerOptions,
45+
): Promise<{ affectedFiles: ReadonlySet<ts.SourceFile>; compilerOptions: ng.CompilerOptions }> {
4746
// Dynamically load the Angular compiler CLI package
4847
const { NgtscProgram, OptimizeFor } = await AngularCompilation.loadCompilerCli();
4948

49+
// Load the compiler configuration and transform as needed
50+
const {
51+
options: originalCompilerOptions,
52+
rootNames,
53+
errors: configurationDiagnostics,
54+
} = await this.loadConfiguration(tsconfig);
55+
const compilerOptions =
56+
compilerOptionsTransformer?.(originalCompilerOptions) ?? originalCompilerOptions;
57+
5058
// Create Angular compiler host
5159
const host = createAngularCompilerHost(compilerOptions, hostOptions);
5260

@@ -79,7 +87,7 @@ export class AotCompilation extends AngularCompilation {
7987
this.#state?.diagnosticCache,
8088
);
8189

82-
return { affectedFiles };
90+
return { affectedFiles, compilerOptions };
8391
}
8492

8593
*collectDiagnostics(): Iterable<ts.Diagnostic> {

packages/angular_devkit/build_angular/src/builders/browser-esbuild/angular/compiler-plugin.ts

Lines changed: 108 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -165,15 +165,13 @@ export function createCompilerPlugin(
165165
name: 'angular-compiler',
166166
// eslint-disable-next-line max-lines-per-function
167167
async setup(build: PluginBuild): Promise<void> {
168-
let setupWarnings: PartialMessage[] | undefined;
168+
let setupWarnings: PartialMessage[] | undefined = [];
169169

170170
// Initialize a worker pool for JavaScript transformations
171171
const javascriptTransformer = new JavaScriptTransformer(pluginOptions, maxWorkers);
172172

173-
const { GLOBAL_DEFS_FOR_TERSER_WITH_AOT, readConfiguration } =
174-
await AngularCompilation.loadCompilerCli();
175-
176173
// Setup defines based on the values provided by the Angular compiler-cli
174+
const { GLOBAL_DEFS_FOR_TERSER_WITH_AOT } = await AngularCompilation.loadCompilerCli();
177175
build.initialOptions.define ??= {};
178176
for (const [key, value] of Object.entries(GLOBAL_DEFS_FOR_TERSER_WITH_AOT)) {
179177
if (key in build.initialOptions.define) {
@@ -189,71 +187,26 @@ export function createCompilerPlugin(
189187
build.initialOptions.define[key] = value.toString();
190188
}
191189

192-
// The tsconfig is loaded in setup instead of in start to allow the esbuild target build option to be modified.
193-
// esbuild build options can only be modified in setup prior to starting the build.
194-
const {
195-
options: compilerOptions,
196-
rootNames,
197-
errors: configurationDiagnostics,
198-
} = profileSync('NG_READ_CONFIG', () =>
199-
readConfiguration(pluginOptions.tsconfig, {
200-
noEmitOnError: false,
201-
suppressOutputPathCheck: true,
202-
outDir: undefined,
203-
inlineSources: pluginOptions.sourcemap,
204-
inlineSourceMap: pluginOptions.sourcemap,
205-
sourceMap: false,
206-
mapRoot: undefined,
207-
sourceRoot: undefined,
208-
declaration: false,
209-
declarationMap: false,
210-
allowEmptyCodegenFiles: false,
211-
annotationsAs: 'decorators',
212-
enableResourceInlining: false,
213-
}),
214-
);
215-
216-
if (compilerOptions.target === undefined || compilerOptions.target < ts.ScriptTarget.ES2022) {
217-
// If 'useDefineForClassFields' is already defined in the users project leave the value as is.
218-
// Otherwise fallback to false due to https://github.com/microsoft/TypeScript/issues/45995
219-
// which breaks the deprecated `@Effects` NGRX decorator and potentially other existing code as well.
220-
compilerOptions.target = ts.ScriptTarget.ES2022;
221-
compilerOptions.useDefineForClassFields ??= false;
222-
223-
(setupWarnings ??= []).push({
224-
text:
225-
'TypeScript compiler options "target" and "useDefineForClassFields" are set to "ES2022" and ' +
226-
'"false" respectively by the Angular CLI.\n' +
227-
`NOTE: You can set the "target" to "ES2022" in the project's tsconfig to remove this warning.`,
228-
location: { file: pluginOptions.tsconfig },
229-
notes: [
230-
{
231-
text:
232-
'To control ECMA version and features use the Browerslist configuration. ' +
233-
'For more information, see https://angular.io/guide/build#configuring-browser-compatibility',
234-
},
235-
],
236-
});
237-
}
238-
239190
// The file emitter created during `onStart` that will be used during the build in `onLoad` callbacks for TS files
240191
let fileEmitter: FileEmitter | undefined;
241192

242193
// The stylesheet resources from component stylesheets that will be added to the build results output files
243194
let stylesheetResourceFiles: OutputFile[] = [];
244-
245195
let stylesheetMetafiles: Metafile[];
246196

247-
let compilation: AngularCompilation | undefined;
197+
// Create new reusable compilation for the appropriate mode based on the `jit` plugin option
198+
const compilation: AngularCompilation = pluginOptions.jit
199+
? new JitCompilation()
200+
: new AotCompilation();
201+
202+
// Determines if TypeScript should process JavaScript files based on tsconfig `allowJs` option
203+
let shouldTsIgnoreJs = true;
248204

249205
build.onStart(async () => {
250206
const result: OnStartResult = {
251207
warnings: setupWarnings,
252208
};
253209

254-
// Reset the setup warnings so that they are only shown during the first build.
255-
setupWarnings = undefined;
256-
257210
// Reset debug performance tracking
258211
resetCumulativeDurations();
259212

@@ -293,21 +246,48 @@ export function createCompilerPlugin(
293246
},
294247
};
295248

296-
// Create new compilation if first build; otherwise, use existing for rebuilds
297-
if (pluginOptions.jit) {
298-
compilation ??= new JitCompilation();
299-
} else {
300-
compilation ??= new AotCompilation();
301-
}
302-
303249
// Initialize the Angular compilation for the current build.
304250
// In watch mode, previous build state will be reused.
305-
const { affectedFiles } = await compilation.initialize(
306-
rootNames,
307-
compilerOptions,
308-
hostOptions,
309-
configurationDiagnostics,
310-
);
251+
const {
252+
affectedFiles,
253+
compilerOptions: { allowJs },
254+
} = await compilation.initialize(pluginOptions.tsconfig, hostOptions, (compilerOptions) => {
255+
if (
256+
compilerOptions.target === undefined ||
257+
compilerOptions.target < ts.ScriptTarget.ES2022
258+
) {
259+
// If 'useDefineForClassFields' is already defined in the users project leave the value as is.
260+
// Otherwise fallback to false due to https://github.com/microsoft/TypeScript/issues/45995
261+
// which breaks the deprecated `@Effects` NGRX decorator and potentially other existing code as well.
262+
compilerOptions.target = ts.ScriptTarget.ES2022;
263+
compilerOptions.useDefineForClassFields ??= false;
264+
265+
// Only add the warning on the initial build
266+
setupWarnings?.push({
267+
text:
268+
'TypeScript compiler options "target" and "useDefineForClassFields" are set to "ES2022" and ' +
269+
'"false" respectively by the Angular CLI.',
270+
location: { file: pluginOptions.tsconfig },
271+
notes: [
272+
{
273+
text:
274+
'To control ECMA version and features use the Browerslist configuration. ' +
275+
'For more information, see https://angular.io/guide/build#configuring-browser-compatibility',
276+
},
277+
],
278+
});
279+
}
280+
281+
return {
282+
...compilerOptions,
283+
noEmitOnError: false,
284+
inlineSources: pluginOptions.sourcemap,
285+
inlineSourceMap: pluginOptions.sourcemap,
286+
mapRoot: undefined,
287+
sourceRoot: undefined,
288+
};
289+
});
290+
shouldTsIgnoreJs = !allowJs;
311291

312292
// Clear affected files from the cache (if present)
313293
if (pluginOptions.sourceFileCache) {
@@ -319,8 +299,7 @@ export function createCompilerPlugin(
319299
}
320300

321301
profileSync('NG_DIAGNOSTICS_TOTAL', () => {
322-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
323-
for (const diagnostic of compilation!.collectDiagnostics()) {
302+
for (const diagnostic of compilation.collectDiagnostics()) {
324303
const message = convertTypeScriptDiagnostic(diagnostic);
325304
if (diagnostic.category === ts.DiagnosticCategory.Error) {
326305
(result.errors ??= []).push(message);
@@ -332,67 +311,73 @@ export function createCompilerPlugin(
332311

333312
fileEmitter = compilation.createFileEmitter();
334313

314+
// Reset the setup warnings so that they are only shown during the first build.
315+
setupWarnings = undefined;
316+
335317
return result;
336318
});
337319

338-
build.onLoad(
339-
{ filter: compilerOptions.allowJs ? /\.[cm]?[jt]sx?$/ : /\.[cm]?tsx?$/ },
340-
(args) =>
341-
profileAsync(
342-
'NG_EMIT_TS*',
343-
async () => {
344-
assert.ok(fileEmitter, 'Invalid plugin execution order');
345-
346-
const request = pluginOptions.fileReplacements?.[args.path] ?? args.path;
347-
348-
// The filename is currently used as a cache key. Since the cache is memory only,
349-
// the options cannot change and do not need to be represented in the key. If the
350-
// cache is later stored to disk, then the options that affect transform output
351-
// would need to be added to the key as well as a check for any change of content.
352-
let contents = pluginOptions.sourceFileCache?.typeScriptFileCache.get(
353-
pathToFileURL(request).href,
354-
);
320+
build.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, (args) =>
321+
profileAsync(
322+
'NG_EMIT_TS*',
323+
async () => {
324+
assert.ok(fileEmitter, 'Invalid plugin execution order');
355325

356-
if (contents === undefined) {
357-
const typescriptResult = await fileEmitter(request);
358-
if (!typescriptResult?.content) {
359-
// No TS result indicates the file is not part of the TypeScript program.
360-
// If allowJs is enabled and the file is JS then defer to the next load hook.
361-
if (compilerOptions.allowJs && /\.[cm]?js$/.test(request)) {
362-
return undefined;
363-
}
364-
365-
// Otherwise return an error
366-
return {
367-
errors: [
368-
createMissingFileError(
369-
request,
370-
args.path,
371-
build.initialOptions.absWorkingDir ?? '',
372-
),
373-
],
374-
};
375-
}
326+
const request = pluginOptions.fileReplacements?.[args.path] ?? args.path;
376327

377-
contents = await javascriptTransformer.transformData(
378-
request,
379-
typescriptResult.content,
380-
true /* skipLinker */,
381-
);
328+
// Skip TS load attempt if JS TypeScript compilation not enabled and file is JS
329+
if (shouldTsIgnoreJs && /\.[cm]?js$/.test(request)) {
330+
return undefined;
331+
}
382332

383-
pluginOptions.sourceFileCache?.typeScriptFileCache.set(
384-
pathToFileURL(request).href,
385-
contents,
386-
);
333+
// The filename is currently used as a cache key. Since the cache is memory only,
334+
// the options cannot change and do not need to be represented in the key. If the
335+
// cache is later stored to disk, then the options that affect transform output
336+
// would need to be added to the key as well as a check for any change of content.
337+
let contents = pluginOptions.sourceFileCache?.typeScriptFileCache.get(
338+
pathToFileURL(request).href,
339+
);
340+
341+
if (contents === undefined) {
342+
const typescriptResult = await fileEmitter(request);
343+
if (!typescriptResult?.content) {
344+
// No TS result indicates the file is not part of the TypeScript program.
345+
// If allowJs is enabled and the file is JS then defer to the next load hook.
346+
if (!shouldTsIgnoreJs && /\.[cm]?js$/.test(request)) {
347+
return undefined;
348+
}
349+
350+
// Otherwise return an error
351+
return {
352+
errors: [
353+
createMissingFileError(
354+
request,
355+
args.path,
356+
build.initialOptions.absWorkingDir ?? '',
357+
),
358+
],
359+
};
387360
}
388361

389-
return {
362+
contents = await javascriptTransformer.transformData(
363+
request,
364+
typescriptResult.content,
365+
true /* skipLinker */,
366+
);
367+
368+
pluginOptions.sourceFileCache?.typeScriptFileCache.set(
369+
pathToFileURL(request).href,
390370
contents,
391-
loader: 'js',
392-
};
393-
},
394-
true,
395-
),
371+
);
372+
}
373+
374+
return {
375+
contents,
376+
loader: 'js',
377+
};
378+
},
379+
true,
380+
),
396381
);
397382

398383
build.onLoad({ filter: /\.[cm]?js$/ }, (args) =>

0 commit comments

Comments
 (0)