Skip to content

Commit

Permalink
fix(@ngtools/webpack): show a compilation error on invalid TypeScript…
Browse files Browse the repository at this point in the history
… 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)
  • Loading branch information
clydin committed Jun 28, 2022
1 parent ed302ea commit 6796998
Showing 1 changed file with 164 additions and 141 deletions.
305 changes: 164 additions & 141 deletions packages/ngtools/webpack/src/ivy/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -175,171 +184,185 @@ 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) => {
// Register plugin to ensure deterministic emit order in multi-plugin usage
const emitRegistration = this.registerWithCompilation(compilation);
this.watchMode = compiler.watchMode;

// Initialize webpack cache
if (!this.webpackCache && compilation.options.cache) {
this.webpackCache = compilation.getCache(PLUGIN_NAME);
}

// Initialize the resource loader if not already setup
if (!resourceLoader) {
resourceLoader = new WebpackResourceLoader(this.watchMode);
try {
this.setupCompilation(compilation, compilationState);
} catch (error) {
compilation.errors.push(
new WebpackError(
`Failed to initialize Angular compilation - ${
error instanceof Error ? error.message : error
}`,
),
);
}
});
}

// Initialize and process eager ngcc if not already setup
if (!ngccProcessor) {
const { processor, errors, warnings } = initializeNgccProcessor(
compiler,
this.pluginOptions.tsconfig,
this.compilerNgccModule,
);
private setupCompilation(compilation: Compilation, state: AngularCompilationState): void {
const compiler = compilation.compiler;

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

ngccProcessor = processor;
}
// Initialize webpack cache
if (!this.webpackCache && compilation.options.cache) {
this.webpackCache = compilation.getCache(PLUGIN_NAME);
}

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

// Create diagnostics reporter and report configuration file errors
const diagnosticsReporter = createDiagnosticsReporter(compilation, (diagnostic) =>
this.compilerCli.formatDiagnostics([diagnostic]),
// Initialize and process eager ngcc if not already setup
if (!state.ngccProcessor) {
const { processor, errors, warnings } = initializeNgccProcessor(
compiler,
this.pluginOptions.tsconfig,
this.compilerNgccModule,
);
diagnosticsReporter(errors);

// Update TypeScript path mapping plugin with new configuration
pathsPlugin.update(compilerOptions);
processor.process();
warnings.forEach((warning) => addWarning(compilation, warning));
errors.forEach((error) => addError(compilation, error));

// Create a Webpack-based TypeScript compiler host
const system = createWebpackSystem(
// Webpack lacks an InputFileSytem type definition with sync functions
compiler.inputFileSystem as InputFileSystemSync,
normalizePath(compiler.context),
);
const host = ts.createIncrementalCompilerHost(compilerOptions, system);

// Setup source file caching and reuse cache from previous compilation if present
let cache = this.sourceFileCache;
let changedFiles;
if (cache) {
changedFiles = new Set<string>();
for (const changedFile of [...compiler.modifiedFiles, ...compiler.removedFiles]) {
const normalizedChangedFile = normalizePath(changedFile);
// Invalidate file dependencies
this.fileDependencies.delete(normalizedChangedFile);
// Invalidate existing cache
cache.invalidate(normalizedChangedFile);

changedFiles.add(normalizedChangedFile);
}
} else {
// Initialize a new cache
cache = new SourceFileCache();
// Only store cache if in watch mode
if (this.watchMode) {
this.sourceFileCache = cache;
}
state.ngccProcessor = processor;
}

// Setup and read TypeScript and Angular compiler configuration
const { compilerOptions, rootNames, errors } = this.loadConfiguration();

// Create diagnostics reporter and report configuration file errors
const diagnosticsReporter = createDiagnosticsReporter(compilation, (diagnostic) =>
this.compilerCli.formatDiagnostics([diagnostic]),
);
diagnosticsReporter(errors);

// Update TypeScript path mapping plugin with new configuration
state.pathsPlugin.update(compilerOptions);

// Create a Webpack-based TypeScript compiler host
const system = createWebpackSystem(
// Webpack lacks an InputFileSytem type definition with sync functions
compiler.inputFileSystem as InputFileSystemSync,
normalizePath(compiler.context),
);
const host = ts.createIncrementalCompilerHost(compilerOptions, system);

// Setup source file caching and reuse cache from previous compilation if present
let cache = this.sourceFileCache;
let changedFiles;
if (cache) {
changedFiles = new Set<string>();
for (const changedFile of [...compiler.modifiedFiles, ...compiler.removedFiles]) {
const normalizedChangedFile = normalizePath(changedFile);
// Invalidate file dependencies
this.fileDependencies.delete(normalizedChangedFile);
// Invalidate existing cache
cache.invalidate(normalizedChangedFile);

changedFiles.add(normalizedChangedFile);
}
augmentHostWithCaching(host, cache);
} else {
// Initialize a new cache
cache = new SourceFileCache();
// Only store cache if in watch mode
if (this.watchMode) {
this.sourceFileCache = cache;
}
}
augmentHostWithCaching(host, cache);

const moduleResolutionCache = ts.createModuleResolutionCache(
host.getCurrentDirectory(),
host.getCanonicalFileName.bind(host),
compilerOptions,
);
const moduleResolutionCache = ts.createModuleResolutionCache(
host.getCurrentDirectory(),
host.getCanonicalFileName.bind(host),
compilerOptions,
);

// Setup source file dependency collection
augmentHostWithDependencyCollection(host, this.fileDependencies, moduleResolutionCache);

// Setup on demand ngcc
augmentHostWithNgcc(host, ngccProcessor, moduleResolutionCache);

// Setup resource loading
resourceLoader.update(compilation, changedFiles);
augmentHostWithResources(host, resourceLoader, {
directTemplateLoading: this.pluginOptions.directTemplateLoading,
inlineStyleFileExtension: this.pluginOptions.inlineStyleFileExtension,
});

// Setup source file adjustment options
augmentHostWithReplacements(host, this.pluginOptions.fileReplacements, moduleResolutionCache);
augmentHostWithSubstitutions(host, this.pluginOptions.substitutions);

// Create the file emitter used by the webpack loader
const { fileEmitter, builder, internalFiles } = this.pluginOptions.jitMode
? this.updateJitProgram(compilerOptions, rootNames, host, diagnosticsReporter)
: this.updateAotProgram(
compilerOptions,
rootNames,
host,
diagnosticsReporter,
resourceLoader,
);
// Setup source file dependency collection
augmentHostWithDependencyCollection(host, this.fileDependencies, moduleResolutionCache);

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

for (const sourceFile of builder.getSourceFiles()) {
if (internalFiles?.has(sourceFile)) {
continue;
}
// Setup resource loading
state.resourceLoader.update(compilation, changedFiles);
augmentHostWithResources(host, state.resourceLoader, {
directTemplateLoading: this.pluginOptions.directTemplateLoading,
inlineStyleFileExtension: this.pluginOptions.inlineStyleFileExtension,
});

// Ensure all program files are considered part of the compilation and will be watched.
// Webpack does not normalize paths. Therefore, we need to normalize the path with FS seperators.
compilation.fileDependencies.add(externalizePath(sourceFile.fileName));
// Setup source file adjustment options
augmentHostWithReplacements(host, this.pluginOptions.fileReplacements, moduleResolutionCache);
augmentHostWithSubstitutions(host, this.pluginOptions.substitutions);

// Create the file emitter used by the webpack loader
const { fileEmitter, builder, internalFiles } = this.pluginOptions.jitMode
? this.updateJitProgram(compilerOptions, rootNames, host, diagnosticsReporter)
: this.updateAotProgram(
compilerOptions,
rootNames,
host,
diagnosticsReporter,
state.resourceLoader,
);

// Add all non-declaration files to the initial set of unused files. The set will be
// analyzed and pruned after all Webpack modules are finished building.
if (!sourceFile.isDeclarationFile) {
currentUnused.add(normalizePath(sourceFile.fileName));
}
// Set of files used during the unused TypeScript file analysis
const currentUnused = new Set<string>();

for (const sourceFile of builder.getSourceFiles()) {
if (internalFiles?.has(sourceFile)) {
continue;
}

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

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

// Analyze program for unused files
if (compilation.errors.length > 0) {
return;
}
compilation.hooks.finishModules.tapPromise(PLUGIN_NAME, async (modules) => {
// Rebuild any remaining AOT required modules
await this.rebuildRequiredFiles(modules, compilation, fileEmitter);

for (const webpackModule of modules) {
const resource = (webpackModule as NormalModule).resource;
if (resource) {
this.markResourceUsed(normalizePath(resource), currentUnused);
}
}
// Clear out the Webpack compilation to avoid an extra retaining reference
state.resourceLoader?.clearParentCompilation();

for (const unused of currentUnused) {
if (previousUnused && previousUnused.has(unused)) {
continue;
}
addWarning(
compilation,
`${unused} is part of the TypeScript compilation but it's unused.\n` +
`Add only entry points to the 'files' or 'include' properties in your tsconfig.`,
);
// Analyze program for unused files
if (compilation.errors.length > 0) {
return;
}

for (const webpackModule of modules) {
const resource = (webpackModule as NormalModule).resource;
if (resource) {
this.markResourceUsed(normalizePath(resource), currentUnused);
}
previousUnused = currentUnused;
});
}

// Store file emitter for loader usage
emitRegistration.update(fileEmitter);
for (const unused of currentUnused) {
if (state.previousUnused?.has(unused)) {
continue;
}
addWarning(
compilation,
`${unused} is part of the TypeScript compilation but it's unused.\n` +
`Add only entry points to the 'files' or 'include' properties in your tsconfig.`,
);
}
state.previousUnused = currentUnused;
});

// Store file emitter for loader usage
emitRegistration.update(fileEmitter);
}

private registerWithCompilation(compilation: Compilation) {
Expand Down

0 comments on commit 6796998

Please sign in to comment.