Skip to content

Commit e10f49e

Browse files
clydinalan-agius4
authored andcommitted
fix(@angular-devkit/build-angular): convert AOT compiler exceptions into diagnostics
When using the `application` builder, exceptions thrown by the AOT compiler will now be converted into error diagnostics. This allows for more actionable information to be displayed in the build output. It also prevents the typically incorrect "missing file" error from occurring in these cases which previously occurred due to TypeScript files not being emitted as a result of the compiler failure. (cherry picked from commit f76e1a0)
1 parent 1b38430 commit e10f49e

File tree

1 file changed

+117
-65
lines changed

1 file changed

+117
-65
lines changed

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

Lines changed: 117 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ export function createCompilerPlugin(
9090
const compilation: AngularCompilation = pluginOptions.noopTypeScriptCompilation
9191
? new NoopCompilation()
9292
: await createAngularCompilation(!!pluginOptions.jit);
93+
// Compilation is initially assumed to have errors until emitted
94+
let hasCompilationErrors = true;
9395

9496
// Determines if TypeScript should process JavaScript files based on tsconfig `allowJs` option
9597
let shouldTsIgnoreJs = true;
@@ -233,66 +235,32 @@ export function createCompilerPlugin(
233235

234236
// Initialize the Angular compilation for the current build.
235237
// In watch mode, previous build state will be reused.
236-
const {
237-
compilerOptions: { allowJs },
238-
referencedFiles,
239-
} = await compilation.initialize(tsconfigPath, hostOptions, (compilerOptions) => {
240-
// target of 9 is ES2022 (using the number avoids an expensive import of typescript just for an enum)
241-
if (compilerOptions.target === undefined || compilerOptions.target < 9) {
242-
// If 'useDefineForClassFields' is already defined in the users project leave the value as is.
243-
// Otherwise fallback to false due to https://github.com/microsoft/TypeScript/issues/45995
244-
// which breaks the deprecated `@Effects` NGRX decorator and potentially other existing code as well.
245-
compilerOptions.target = 9;
246-
compilerOptions.useDefineForClassFields ??= false;
247-
248-
// Only add the warning on the initial build
249-
setupWarnings?.push({
250-
text:
251-
'TypeScript compiler options "target" and "useDefineForClassFields" are set to "ES2022" and ' +
252-
'"false" respectively by the Angular CLI.',
253-
location: { file: pluginOptions.tsconfig },
254-
notes: [
255-
{
256-
text:
257-
'To control ECMA version and features use the Browerslist configuration. ' +
258-
'For more information, see https://angular.io/guide/build#configuring-browser-compatibility',
259-
},
260-
],
261-
});
262-
}
263-
264-
if (compilerOptions.compilationMode === 'partial') {
265-
setupWarnings?.push({
266-
text: 'Angular partial compilation mode is not supported when building applications.',
267-
location: null,
268-
notes: [{ text: 'Full compilation mode will be used instead.' }],
269-
});
270-
compilerOptions.compilationMode = 'full';
271-
}
238+
let referencedFiles;
239+
try {
240+
const initializationResult = await compilation.initialize(
241+
tsconfigPath,
242+
hostOptions,
243+
createCompilerOptionsTransformer(setupWarnings, pluginOptions, preserveSymlinks),
244+
);
245+
shouldTsIgnoreJs = !initializationResult.compilerOptions.allowJs;
246+
referencedFiles = initializationResult.referencedFiles;
247+
} catch (error) {
248+
(result.errors ??= []).push({
249+
text: 'Angular compilation initialization failed.',
250+
location: null,
251+
notes: [
252+
{
253+
text: error instanceof Error ? error.stack ?? error.message : `${error}`,
254+
location: null,
255+
},
256+
],
257+
});
272258

273-
// Enable incremental compilation by default if caching is enabled
274-
if (pluginOptions.sourceFileCache?.persistentCachePath) {
275-
compilerOptions.incremental ??= true;
276-
// Set the build info file location to the configured cache directory
277-
compilerOptions.tsBuildInfoFile = path.join(
278-
pluginOptions.sourceFileCache?.persistentCachePath,
279-
'.tsbuildinfo',
280-
);
281-
} else {
282-
compilerOptions.incremental = false;
283-
}
259+
// Initialization failure prevents further compilation steps
260+
hasCompilationErrors = true;
284261

285-
return {
286-
...compilerOptions,
287-
noEmitOnError: false,
288-
inlineSources: pluginOptions.sourcemap,
289-
inlineSourceMap: pluginOptions.sourcemap,
290-
mapRoot: undefined,
291-
sourceRoot: undefined,
292-
preserveSymlinks,
293-
};
294-
});
295-
shouldTsIgnoreJs = !allowJs;
262+
return result;
263+
}
296264

297265
if (compilation instanceof NoopCompilation) {
298266
await sharedTSCompilationState.waitUntilReady;
@@ -301,19 +269,32 @@ export function createCompilerPlugin(
301269
}
302270

303271
const diagnostics = await compilation.diagnoseFiles();
304-
if (diagnostics.errors) {
272+
if (diagnostics.errors?.length) {
305273
(result.errors ??= []).push(...diagnostics.errors);
306274
}
307-
if (diagnostics.warnings) {
275+
if (diagnostics.warnings?.length) {
308276
(result.warnings ??= []).push(...diagnostics.warnings);
309277
}
310278

311279
// Update TypeScript file output cache for all affected files
312-
await profileAsync('NG_EMIT_TS', async () => {
313-
for (const { filename, contents } of await compilation.emitAffectedFiles()) {
314-
typeScriptFileCache.set(pathToFileURL(filename).href, contents);
315-
}
316-
});
280+
try {
281+
await profileAsync('NG_EMIT_TS', async () => {
282+
for (const { filename, contents } of await compilation.emitAffectedFiles()) {
283+
typeScriptFileCache.set(pathToFileURL(filename).href, contents);
284+
}
285+
});
286+
} catch (error) {
287+
(result.errors ??= []).push({
288+
text: 'Angular compilation emit failed.',
289+
location: null,
290+
notes: [
291+
{
292+
text: error instanceof Error ? error.stack ?? error.message : `${error}`,
293+
location: null,
294+
},
295+
],
296+
});
297+
}
317298

318299
// Add errors from failed additional results.
319300
// This must be done after emit to capture latest web worker results.
@@ -331,6 +312,8 @@ export function createCompilerPlugin(
331312
];
332313
}
333314

315+
hasCompilationErrors = !!result.errors?.length;
316+
334317
// Reset the setup warnings so that they are only shown during the first build.
335318
setupWarnings = undefined;
336319

@@ -354,6 +337,12 @@ export function createCompilerPlugin(
354337
let contents = typeScriptFileCache.get(pathToFileURL(request).href);
355338

356339
if (contents === undefined) {
340+
// If the Angular compilation had errors the file may not have been emitted.
341+
// To avoid additional errors about missing files, return empty contents.
342+
if (hasCompilationErrors) {
343+
return { contents: '', loader: 'js' };
344+
}
345+
357346
// No TS result indicates the file is not part of the TypeScript program.
358347
// If allowJs is enabled and the file is JS then defer to the next load hook.
359348
if (!shouldTsIgnoreJs && /\.[cm]?js$/.test(request)) {
@@ -446,6 +435,69 @@ export function createCompilerPlugin(
446435
};
447436
}
448437

438+
function createCompilerOptionsTransformer(
439+
setupWarnings: PartialMessage[] | undefined,
440+
pluginOptions: CompilerPluginOptions,
441+
preserveSymlinks: boolean | undefined,
442+
): Parameters<AngularCompilation['initialize']>[2] {
443+
return (compilerOptions) => {
444+
// target of 9 is ES2022 (using the number avoids an expensive import of typescript just for an enum)
445+
if (compilerOptions.target === undefined || compilerOptions.target < 9) {
446+
// If 'useDefineForClassFields' is already defined in the users project leave the value as is.
447+
// Otherwise fallback to false due to https://github.com/microsoft/TypeScript/issues/45995
448+
// which breaks the deprecated `@Effects` NGRX decorator and potentially other existing code as well.
449+
compilerOptions.target = 9;
450+
compilerOptions.useDefineForClassFields ??= false;
451+
452+
// Only add the warning on the initial build
453+
setupWarnings?.push({
454+
text:
455+
'TypeScript compiler options "target" and "useDefineForClassFields" are set to "ES2022" and ' +
456+
'"false" respectively by the Angular CLI.',
457+
location: { file: pluginOptions.tsconfig },
458+
notes: [
459+
{
460+
text:
461+
'To control ECMA version and features use the Browerslist configuration. ' +
462+
'For more information, see https://angular.io/guide/build#configuring-browser-compatibility',
463+
},
464+
],
465+
});
466+
}
467+
468+
if (compilerOptions.compilationMode === 'partial') {
469+
setupWarnings?.push({
470+
text: 'Angular partial compilation mode is not supported when building applications.',
471+
location: null,
472+
notes: [{ text: 'Full compilation mode will be used instead.' }],
473+
});
474+
compilerOptions.compilationMode = 'full';
475+
}
476+
477+
// Enable incremental compilation by default if caching is enabled
478+
if (pluginOptions.sourceFileCache?.persistentCachePath) {
479+
compilerOptions.incremental ??= true;
480+
// Set the build info file location to the configured cache directory
481+
compilerOptions.tsBuildInfoFile = path.join(
482+
pluginOptions.sourceFileCache?.persistentCachePath,
483+
'.tsbuildinfo',
484+
);
485+
} else {
486+
compilerOptions.incremental = false;
487+
}
488+
489+
return {
490+
...compilerOptions,
491+
noEmitOnError: false,
492+
inlineSources: pluginOptions.sourcemap,
493+
inlineSourceMap: pluginOptions.sourcemap,
494+
mapRoot: undefined,
495+
sourceRoot: undefined,
496+
preserveSymlinks,
497+
};
498+
};
499+
}
500+
449501
function bundleWebWorker(
450502
build: PluginBuild,
451503
pluginOptions: CompilerPluginOptions,

0 commit comments

Comments
 (0)