fix(@ngtools/webpack): show a compilation error on invalid TypeScript version

A TypeScript version mismatch with the Angular compiler will no longer cause an
exception to propagate up through the Webpack system. In Node.js v14, this resulted
in an unhandled promise rejection warning and the build command never completing.
This can also be reproduced in newer versions of Node.js by using the Node.js
option `--unhandled-rejections=warn`. To correct this issue, the version mismatch
is now treated as a compilation error and added to the list of errors that are
displayed at the end of the build. This also has the benefit of avoiding the stack
trace of the exception from being shown which previously drew attention away from
the actual error message.
This commit is contained in:
Charles Lyding 2022-06-27 19:28:27 -04:00 committed by Charles
parent a69000407c
commit 34ecf669dd

View File

@ -53,6 +53,16 @@ export interface AngularWebpackPluginOptions {
inlineStyleFileExtension?: string;
}
/**
* The Angular compilation state that is maintained across each Webpack compilation.
*/
interface AngularCompilationState {
ngccProcessor?: NgccProcessor;
resourceLoader?: WebpackResourceLoader;
previousUnused?: Set<string>;
pathsPlugin: TypeScriptPathsPlugin;
}
function initializeNgccProcessor(
compiler: Compiler,
tsconfig: string,
@ -138,9 +148,8 @@ export class AngularWebpackPlugin {
return this.pluginOptions;
}
// eslint-disable-next-line max-lines-per-function
apply(compiler: Compiler): void {
const { NormalModuleReplacementPlugin, util } = compiler.webpack;
const { NormalModuleReplacementPlugin, WebpackError, util } = compiler.webpack;
this.webpackCreateHash = util.createHash;
// Setup file replacements with webpack
@ -175,10 +184,25 @@ export class AngularWebpackPlugin {
// Load the compiler-cli if not already available
compiler.hooks.beforeCompile.tapPromise(PLUGIN_NAME, () => this.initializeCompilerCli());
let ngccProcessor: NgccProcessor | undefined;
let resourceLoader: WebpackResourceLoader | undefined;
let previousUnused: Set<string> | undefined;
const compilationState: AngularCompilationState = { pathsPlugin };
compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
try {
this.setupCompilation(compilation, compilationState);
} catch (error) {
compilation.errors.push(
new WebpackError(
`Failed to initialize Angular compilation - ${
error instanceof Error ? error.message : error
}`,
),
);
}
});
}
private setupCompilation(compilation: Compilation, state: AngularCompilationState): void {
const compiler = compilation.compiler;
// Register plugin to ensure deterministic emit order in multi-plugin usage
const emitRegistration = this.registerWithCompilation(compilation);
this.watchMode = compiler.watchMode;
@ -189,12 +213,12 @@ export class AngularWebpackPlugin {
}
// Initialize the resource loader if not already setup
if (!resourceLoader) {
resourceLoader = new WebpackResourceLoader(this.watchMode);
if (!state.resourceLoader) {
state.resourceLoader = new WebpackResourceLoader(this.watchMode);
}
// Initialize and process eager ngcc if not already setup
if (!ngccProcessor) {
if (!state.ngccProcessor) {
const { processor, errors, warnings } = initializeNgccProcessor(
compiler,
this.pluginOptions.tsconfig,
@ -205,7 +229,7 @@ export class AngularWebpackPlugin {
warnings.forEach((warning) => addWarning(compilation, warning));
errors.forEach((error) => addError(compilation, error));
ngccProcessor = processor;
state.ngccProcessor = processor;
}
// Setup and read TypeScript and Angular compiler configuration
@ -218,7 +242,7 @@ export class AngularWebpackPlugin {
diagnosticsReporter(errors);
// Update TypeScript path mapping plugin with new configuration
pathsPlugin.update(compilerOptions);
state.pathsPlugin.update(compilerOptions);
// Create a Webpack-based TypeScript compiler host
const system = createWebpackSystem(
@ -262,11 +286,11 @@ export class AngularWebpackPlugin {
augmentHostWithDependencyCollection(host, this.fileDependencies, moduleResolutionCache);
// Setup on demand ngcc
augmentHostWithNgcc(host, ngccProcessor, moduleResolutionCache);
augmentHostWithNgcc(host, state.ngccProcessor, moduleResolutionCache);
// Setup resource loading
resourceLoader.update(compilation, changedFiles);
augmentHostWithResources(host, resourceLoader, {
state.resourceLoader.update(compilation, changedFiles);
augmentHostWithResources(host, state.resourceLoader, {
directTemplateLoading: this.pluginOptions.directTemplateLoading,
inlineStyleFileExtension: this.pluginOptions.inlineStyleFileExtension,
});
@ -283,7 +307,7 @@ export class AngularWebpackPlugin {
rootNames,
host,
diagnosticsReporter,
resourceLoader,
state.resourceLoader,
);
// Set of files used during the unused TypeScript file analysis
@ -310,7 +334,7 @@ export class AngularWebpackPlugin {
await this.rebuildRequiredFiles(modules, compilation, fileEmitter);
// Clear out the Webpack compilation to avoid an extra retaining reference
resourceLoader?.clearParentCompilation();
state.resourceLoader?.clearParentCompilation();
// Analyze program for unused files
if (compilation.errors.length > 0) {
@ -325,7 +349,7 @@ export class AngularWebpackPlugin {
}
for (const unused of currentUnused) {
if (previousUnused && previousUnused.has(unused)) {
if (state.previousUnused?.has(unused)) {
continue;
}
addWarning(
@ -334,12 +358,11 @@ export class AngularWebpackPlugin {
`Add only entry points to the 'files' or 'include' properties in your tsconfig.`,
);
}
previousUnused = currentUnused;
state.previousUnused = currentUnused;
});
// Store file emitter for loader usage
emitRegistration.update(fileEmitter);
});
}
private registerWithCompilation(compilation: Compilation) {