Skip to content

Commit 6796998

Browse files
committed
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. (cherry picked from commit 34ecf66)
1 parent ed302ea commit 6796998

File tree

1 file changed

+164
-141
lines changed

1 file changed

+164
-141
lines changed

packages/ngtools/webpack/src/ivy/plugin.ts

+164-141
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,16 @@ export interface AngularWebpackPluginOptions {
5353
inlineStyleFileExtension?: string;
5454
}
5555

56+
/**
57+
* The Angular compilation state that is maintained across each Webpack compilation.
58+
*/
59+
interface AngularCompilationState {
60+
ngccProcessor?: NgccProcessor;
61+
resourceLoader?: WebpackResourceLoader;
62+
previousUnused?: Set<string>;
63+
pathsPlugin: TypeScriptPathsPlugin;
64+
}
65+
5666
function initializeNgccProcessor(
5767
compiler: Compiler,
5868
tsconfig: string,
@@ -138,9 +148,8 @@ export class AngularWebpackPlugin {
138148
return this.pluginOptions;
139149
}
140150

141-
// eslint-disable-next-line max-lines-per-function
142151
apply(compiler: Compiler): void {
143-
const { NormalModuleReplacementPlugin, util } = compiler.webpack;
152+
const { NormalModuleReplacementPlugin, WebpackError, util } = compiler.webpack;
144153
this.webpackCreateHash = util.createHash;
145154

146155
// Setup file replacements with webpack
@@ -175,171 +184,185 @@ export class AngularWebpackPlugin {
175184
// Load the compiler-cli if not already available
176185
compiler.hooks.beforeCompile.tapPromise(PLUGIN_NAME, () => this.initializeCompilerCli());
177186

178-
let ngccProcessor: NgccProcessor | undefined;
179-
let resourceLoader: WebpackResourceLoader | undefined;
180-
let previousUnused: Set<string> | undefined;
187+
const compilationState: AngularCompilationState = { pathsPlugin };
181188
compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
182-
// Register plugin to ensure deterministic emit order in multi-plugin usage
183-
const emitRegistration = this.registerWithCompilation(compilation);
184-
this.watchMode = compiler.watchMode;
185-
186-
// Initialize webpack cache
187-
if (!this.webpackCache && compilation.options.cache) {
188-
this.webpackCache = compilation.getCache(PLUGIN_NAME);
189-
}
190-
191-
// Initialize the resource loader if not already setup
192-
if (!resourceLoader) {
193-
resourceLoader = new WebpackResourceLoader(this.watchMode);
189+
try {
190+
this.setupCompilation(compilation, compilationState);
191+
} catch (error) {
192+
compilation.errors.push(
193+
new WebpackError(
194+
`Failed to initialize Angular compilation - ${
195+
error instanceof Error ? error.message : error
196+
}`,
197+
),
198+
);
194199
}
200+
});
201+
}
195202

196-
// Initialize and process eager ngcc if not already setup
197-
if (!ngccProcessor) {
198-
const { processor, errors, warnings } = initializeNgccProcessor(
199-
compiler,
200-
this.pluginOptions.tsconfig,
201-
this.compilerNgccModule,
202-
);
203+
private setupCompilation(compilation: Compilation, state: AngularCompilationState): void {
204+
const compiler = compilation.compiler;
203205

204-
processor.process();
205-
warnings.forEach((warning) => addWarning(compilation, warning));
206-
errors.forEach((error) => addError(compilation, error));
206+
// Register plugin to ensure deterministic emit order in multi-plugin usage
207+
const emitRegistration = this.registerWithCompilation(compilation);
208+
this.watchMode = compiler.watchMode;
207209

208-
ngccProcessor = processor;
209-
}
210+
// Initialize webpack cache
211+
if (!this.webpackCache && compilation.options.cache) {
212+
this.webpackCache = compilation.getCache(PLUGIN_NAME);
213+
}
210214

211-
// Setup and read TypeScript and Angular compiler configuration
212-
const { compilerOptions, rootNames, errors } = this.loadConfiguration();
215+
// Initialize the resource loader if not already setup
216+
if (!state.resourceLoader) {
217+
state.resourceLoader = new WebpackResourceLoader(this.watchMode);
218+
}
213219

214-
// Create diagnostics reporter and report configuration file errors
215-
const diagnosticsReporter = createDiagnosticsReporter(compilation, (diagnostic) =>
216-
this.compilerCli.formatDiagnostics([diagnostic]),
220+
// Initialize and process eager ngcc if not already setup
221+
if (!state.ngccProcessor) {
222+
const { processor, errors, warnings } = initializeNgccProcessor(
223+
compiler,
224+
this.pluginOptions.tsconfig,
225+
this.compilerNgccModule,
217226
);
218-
diagnosticsReporter(errors);
219227

220-
// Update TypeScript path mapping plugin with new configuration
221-
pathsPlugin.update(compilerOptions);
228+
processor.process();
229+
warnings.forEach((warning) => addWarning(compilation, warning));
230+
errors.forEach((error) => addError(compilation, error));
222231

223-
// Create a Webpack-based TypeScript compiler host
224-
const system = createWebpackSystem(
225-
// Webpack lacks an InputFileSytem type definition with sync functions
226-
compiler.inputFileSystem as InputFileSystemSync,
227-
normalizePath(compiler.context),
228-
);
229-
const host = ts.createIncrementalCompilerHost(compilerOptions, system);
230-
231-
// Setup source file caching and reuse cache from previous compilation if present
232-
let cache = this.sourceFileCache;
233-
let changedFiles;
234-
if (cache) {
235-
changedFiles = new Set<string>();
236-
for (const changedFile of [...compiler.modifiedFiles, ...compiler.removedFiles]) {
237-
const normalizedChangedFile = normalizePath(changedFile);
238-
// Invalidate file dependencies
239-
this.fileDependencies.delete(normalizedChangedFile);
240-
// Invalidate existing cache
241-
cache.invalidate(normalizedChangedFile);
242-
243-
changedFiles.add(normalizedChangedFile);
244-
}
245-
} else {
246-
// Initialize a new cache
247-
cache = new SourceFileCache();
248-
// Only store cache if in watch mode
249-
if (this.watchMode) {
250-
this.sourceFileCache = cache;
251-
}
232+
state.ngccProcessor = processor;
233+
}
234+
235+
// Setup and read TypeScript and Angular compiler configuration
236+
const { compilerOptions, rootNames, errors } = this.loadConfiguration();
237+
238+
// Create diagnostics reporter and report configuration file errors
239+
const diagnosticsReporter = createDiagnosticsReporter(compilation, (diagnostic) =>
240+
this.compilerCli.formatDiagnostics([diagnostic]),
241+
);
242+
diagnosticsReporter(errors);
243+
244+
// Update TypeScript path mapping plugin with new configuration
245+
state.pathsPlugin.update(compilerOptions);
246+
247+
// Create a Webpack-based TypeScript compiler host
248+
const system = createWebpackSystem(
249+
// Webpack lacks an InputFileSytem type definition with sync functions
250+
compiler.inputFileSystem as InputFileSystemSync,
251+
normalizePath(compiler.context),
252+
);
253+
const host = ts.createIncrementalCompilerHost(compilerOptions, system);
254+
255+
// Setup source file caching and reuse cache from previous compilation if present
256+
let cache = this.sourceFileCache;
257+
let changedFiles;
258+
if (cache) {
259+
changedFiles = new Set<string>();
260+
for (const changedFile of [...compiler.modifiedFiles, ...compiler.removedFiles]) {
261+
const normalizedChangedFile = normalizePath(changedFile);
262+
// Invalidate file dependencies
263+
this.fileDependencies.delete(normalizedChangedFile);
264+
// Invalidate existing cache
265+
cache.invalidate(normalizedChangedFile);
266+
267+
changedFiles.add(normalizedChangedFile);
252268
}
253-
augmentHostWithCaching(host, cache);
269+
} else {
270+
// Initialize a new cache
271+
cache = new SourceFileCache();
272+
// Only store cache if in watch mode
273+
if (this.watchMode) {
274+
this.sourceFileCache = cache;
275+
}
276+
}
277+
augmentHostWithCaching(host, cache);
254278

255-
const moduleResolutionCache = ts.createModuleResolutionCache(
256-
host.getCurrentDirectory(),
257-
host.getCanonicalFileName.bind(host),
258-
compilerOptions,
259-
);
279+
const moduleResolutionCache = ts.createModuleResolutionCache(
280+
host.getCurrentDirectory(),
281+
host.getCanonicalFileName.bind(host),
282+
compilerOptions,
283+
);
260284

261-
// Setup source file dependency collection
262-
augmentHostWithDependencyCollection(host, this.fileDependencies, moduleResolutionCache);
263-
264-
// Setup on demand ngcc
265-
augmentHostWithNgcc(host, ngccProcessor, moduleResolutionCache);
266-
267-
// Setup resource loading
268-
resourceLoader.update(compilation, changedFiles);
269-
augmentHostWithResources(host, resourceLoader, {
270-
directTemplateLoading: this.pluginOptions.directTemplateLoading,
271-
inlineStyleFileExtension: this.pluginOptions.inlineStyleFileExtension,
272-
});
273-
274-
// Setup source file adjustment options
275-
augmentHostWithReplacements(host, this.pluginOptions.fileReplacements, moduleResolutionCache);
276-
augmentHostWithSubstitutions(host, this.pluginOptions.substitutions);
277-
278-
// Create the file emitter used by the webpack loader
279-
const { fileEmitter, builder, internalFiles } = this.pluginOptions.jitMode
280-
? this.updateJitProgram(compilerOptions, rootNames, host, diagnosticsReporter)
281-
: this.updateAotProgram(
282-
compilerOptions,
283-
rootNames,
284-
host,
285-
diagnosticsReporter,
286-
resourceLoader,
287-
);
285+
// Setup source file dependency collection
286+
augmentHostWithDependencyCollection(host, this.fileDependencies, moduleResolutionCache);
288287

289-
// Set of files used during the unused TypeScript file analysis
290-
const currentUnused = new Set<string>();
288+
// Setup on demand ngcc
289+
augmentHostWithNgcc(host, state.ngccProcessor, moduleResolutionCache);
291290

292-
for (const sourceFile of builder.getSourceFiles()) {
293-
if (internalFiles?.has(sourceFile)) {
294-
continue;
295-
}
291+
// Setup resource loading
292+
state.resourceLoader.update(compilation, changedFiles);
293+
augmentHostWithResources(host, state.resourceLoader, {
294+
directTemplateLoading: this.pluginOptions.directTemplateLoading,
295+
inlineStyleFileExtension: this.pluginOptions.inlineStyleFileExtension,
296+
});
296297

297-
// Ensure all program files are considered part of the compilation and will be watched.
298-
// Webpack does not normalize paths. Therefore, we need to normalize the path with FS seperators.
299-
compilation.fileDependencies.add(externalizePath(sourceFile.fileName));
298+
// Setup source file adjustment options
299+
augmentHostWithReplacements(host, this.pluginOptions.fileReplacements, moduleResolutionCache);
300+
augmentHostWithSubstitutions(host, this.pluginOptions.substitutions);
301+
302+
// Create the file emitter used by the webpack loader
303+
const { fileEmitter, builder, internalFiles } = this.pluginOptions.jitMode
304+
? this.updateJitProgram(compilerOptions, rootNames, host, diagnosticsReporter)
305+
: this.updateAotProgram(
306+
compilerOptions,
307+
rootNames,
308+
host,
309+
diagnosticsReporter,
310+
state.resourceLoader,
311+
);
300312

301-
// Add all non-declaration files to the initial set of unused files. The set will be
302-
// analyzed and pruned after all Webpack modules are finished building.
303-
if (!sourceFile.isDeclarationFile) {
304-
currentUnused.add(normalizePath(sourceFile.fileName));
305-
}
313+
// Set of files used during the unused TypeScript file analysis
314+
const currentUnused = new Set<string>();
315+
316+
for (const sourceFile of builder.getSourceFiles()) {
317+
if (internalFiles?.has(sourceFile)) {
318+
continue;
306319
}
307320

308-
compilation.hooks.finishModules.tapPromise(PLUGIN_NAME, async (modules) => {
309-
// Rebuild any remaining AOT required modules
310-
await this.rebuildRequiredFiles(modules, compilation, fileEmitter);
321+
// Ensure all program files are considered part of the compilation and will be watched.
322+
// Webpack does not normalize paths. Therefore, we need to normalize the path with FS seperators.
323+
compilation.fileDependencies.add(externalizePath(sourceFile.fileName));
311324

312-
// Clear out the Webpack compilation to avoid an extra retaining reference
313-
resourceLoader?.clearParentCompilation();
325+
// Add all non-declaration files to the initial set of unused files. The set will be
326+
// analyzed and pruned after all Webpack modules are finished building.
327+
if (!sourceFile.isDeclarationFile) {
328+
currentUnused.add(normalizePath(sourceFile.fileName));
329+
}
330+
}
314331

315-
// Analyze program for unused files
316-
if (compilation.errors.length > 0) {
317-
return;
318-
}
332+
compilation.hooks.finishModules.tapPromise(PLUGIN_NAME, async (modules) => {
333+
// Rebuild any remaining AOT required modules
334+
await this.rebuildRequiredFiles(modules, compilation, fileEmitter);
319335

320-
for (const webpackModule of modules) {
321-
const resource = (webpackModule as NormalModule).resource;
322-
if (resource) {
323-
this.markResourceUsed(normalizePath(resource), currentUnused);
324-
}
325-
}
336+
// Clear out the Webpack compilation to avoid an extra retaining reference
337+
state.resourceLoader?.clearParentCompilation();
326338

327-
for (const unused of currentUnused) {
328-
if (previousUnused && previousUnused.has(unused)) {
329-
continue;
330-
}
331-
addWarning(
332-
compilation,
333-
`${unused} is part of the TypeScript compilation but it's unused.\n` +
334-
`Add only entry points to the 'files' or 'include' properties in your tsconfig.`,
335-
);
339+
// Analyze program for unused files
340+
if (compilation.errors.length > 0) {
341+
return;
342+
}
343+
344+
for (const webpackModule of modules) {
345+
const resource = (webpackModule as NormalModule).resource;
346+
if (resource) {
347+
this.markResourceUsed(normalizePath(resource), currentUnused);
336348
}
337-
previousUnused = currentUnused;
338-
});
349+
}
339350

340-
// Store file emitter for loader usage
341-
emitRegistration.update(fileEmitter);
351+
for (const unused of currentUnused) {
352+
if (state.previousUnused?.has(unused)) {
353+
continue;
354+
}
355+
addWarning(
356+
compilation,
357+
`${unused} is part of the TypeScript compilation but it's unused.\n` +
358+
`Add only entry points to the 'files' or 'include' properties in your tsconfig.`,
359+
);
360+
}
361+
state.previousUnused = currentUnused;
342362
});
363+
364+
// Store file emitter for loader usage
365+
emitRegistration.update(fileEmitter);
343366
}
344367

345368
private registerWithCompilation(compilation: Compilation) {

0 commit comments

Comments
 (0)