mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-22 06:41:45 +08:00
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:
parent
a69000407c
commit
34ecf669dd
@ -53,6 +53,16 @@ export interface AngularWebpackPluginOptions {
|
|||||||
inlineStyleFileExtension?: string;
|
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(
|
function initializeNgccProcessor(
|
||||||
compiler: Compiler,
|
compiler: Compiler,
|
||||||
tsconfig: string,
|
tsconfig: string,
|
||||||
@ -138,9 +148,8 @@ export class AngularWebpackPlugin {
|
|||||||
return this.pluginOptions;
|
return this.pluginOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line max-lines-per-function
|
|
||||||
apply(compiler: Compiler): void {
|
apply(compiler: Compiler): void {
|
||||||
const { NormalModuleReplacementPlugin, util } = compiler.webpack;
|
const { NormalModuleReplacementPlugin, WebpackError, util } = compiler.webpack;
|
||||||
this.webpackCreateHash = util.createHash;
|
this.webpackCreateHash = util.createHash;
|
||||||
|
|
||||||
// Setup file replacements with webpack
|
// Setup file replacements with webpack
|
||||||
@ -175,171 +184,185 @@ export class AngularWebpackPlugin {
|
|||||||
// Load the compiler-cli if not already available
|
// Load the compiler-cli if not already available
|
||||||
compiler.hooks.beforeCompile.tapPromise(PLUGIN_NAME, () => this.initializeCompilerCli());
|
compiler.hooks.beforeCompile.tapPromise(PLUGIN_NAME, () => this.initializeCompilerCli());
|
||||||
|
|
||||||
let ngccProcessor: NgccProcessor | undefined;
|
const compilationState: AngularCompilationState = { pathsPlugin };
|
||||||
let resourceLoader: WebpackResourceLoader | undefined;
|
|
||||||
let previousUnused: Set<string> | undefined;
|
|
||||||
compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
|
compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
|
||||||
// Register plugin to ensure deterministic emit order in multi-plugin usage
|
try {
|
||||||
const emitRegistration = this.registerWithCompilation(compilation);
|
this.setupCompilation(compilation, compilationState);
|
||||||
this.watchMode = compiler.watchMode;
|
} catch (error) {
|
||||||
|
compilation.errors.push(
|
||||||
// Initialize webpack cache
|
new WebpackError(
|
||||||
if (!this.webpackCache && compilation.options.cache) {
|
`Failed to initialize Angular compilation - ${
|
||||||
this.webpackCache = compilation.getCache(PLUGIN_NAME);
|
error instanceof Error ? error.message : error
|
||||||
|
}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize the resource loader if not already setup
|
private setupCompilation(compilation: Compilation, state: AngularCompilationState): void {
|
||||||
if (!resourceLoader) {
|
const compiler = compilation.compiler;
|
||||||
resourceLoader = new WebpackResourceLoader(this.watchMode);
|
|
||||||
|
// 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 (!state.resourceLoader) {
|
||||||
|
state.resourceLoader = new WebpackResourceLoader(this.watchMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize and process eager ngcc if not already setup
|
||||||
|
if (!state.ngccProcessor) {
|
||||||
|
const { processor, errors, warnings } = initializeNgccProcessor(
|
||||||
|
compiler,
|
||||||
|
this.pluginOptions.tsconfig,
|
||||||
|
this.compilerNgccModule,
|
||||||
|
);
|
||||||
|
|
||||||
|
processor.process();
|
||||||
|
warnings.forEach((warning) => addWarning(compilation, warning));
|
||||||
|
errors.forEach((error) => addError(compilation, error));
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Initialize a new cache
|
||||||
|
cache = new SourceFileCache();
|
||||||
|
// Only store cache if in watch mode
|
||||||
|
if (this.watchMode) {
|
||||||
|
this.sourceFileCache = cache;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
augmentHostWithCaching(host, cache);
|
||||||
|
|
||||||
// Initialize and process eager ngcc if not already setup
|
const moduleResolutionCache = ts.createModuleResolutionCache(
|
||||||
if (!ngccProcessor) {
|
host.getCurrentDirectory(),
|
||||||
const { processor, errors, warnings } = initializeNgccProcessor(
|
host.getCanonicalFileName.bind(host),
|
||||||
compiler,
|
compilerOptions,
|
||||||
this.pluginOptions.tsconfig,
|
);
|
||||||
this.compilerNgccModule,
|
|
||||||
|
// Setup source file dependency collection
|
||||||
|
augmentHostWithDependencyCollection(host, this.fileDependencies, moduleResolutionCache);
|
||||||
|
|
||||||
|
// Setup on demand ngcc
|
||||||
|
augmentHostWithNgcc(host, state.ngccProcessor, moduleResolutionCache);
|
||||||
|
|
||||||
|
// Setup resource loading
|
||||||
|
state.resourceLoader.update(compilation, changedFiles);
|
||||||
|
augmentHostWithResources(host, state.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,
|
||||||
|
state.resourceLoader,
|
||||||
);
|
);
|
||||||
|
|
||||||
processor.process();
|
// Set of files used during the unused TypeScript file analysis
|
||||||
warnings.forEach((warning) => addWarning(compilation, warning));
|
const currentUnused = new Set<string>();
|
||||||
errors.forEach((error) => addError(compilation, error));
|
|
||||||
|
|
||||||
ngccProcessor = processor;
|
for (const sourceFile of builder.getSourceFiles()) {
|
||||||
|
if (internalFiles?.has(sourceFile)) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup and read TypeScript and Angular compiler configuration
|
// Ensure all program files are considered part of the compilation and will be watched.
|
||||||
const { compilerOptions, rootNames, errors } = this.loadConfiguration();
|
// Webpack does not normalize paths. Therefore, we need to normalize the path with FS seperators.
|
||||||
|
compilation.fileDependencies.add(externalizePath(sourceFile.fileName));
|
||||||
|
|
||||||
// Create diagnostics reporter and report configuration file errors
|
// Add all non-declaration files to the initial set of unused files. The set will be
|
||||||
const diagnosticsReporter = createDiagnosticsReporter(compilation, (diagnostic) =>
|
// analyzed and pruned after all Webpack modules are finished building.
|
||||||
this.compilerCli.formatDiagnostics([diagnostic]),
|
if (!sourceFile.isDeclarationFile) {
|
||||||
);
|
currentUnused.add(normalizePath(sourceFile.fileName));
|
||||||
diagnosticsReporter(errors);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update TypeScript path mapping plugin with new configuration
|
compilation.hooks.finishModules.tapPromise(PLUGIN_NAME, async (modules) => {
|
||||||
pathsPlugin.update(compilerOptions);
|
// Rebuild any remaining AOT required modules
|
||||||
|
await this.rebuildRequiredFiles(modules, compilation, fileEmitter);
|
||||||
|
|
||||||
// Create a Webpack-based TypeScript compiler host
|
// Clear out the Webpack compilation to avoid an extra retaining reference
|
||||||
const system = createWebpackSystem(
|
state.resourceLoader?.clearParentCompilation();
|
||||||
// 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
|
// Analyze program for unused files
|
||||||
let cache = this.sourceFileCache;
|
if (compilation.errors.length > 0) {
|
||||||
let changedFiles;
|
return;
|
||||||
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);
|
for (const webpackModule of modules) {
|
||||||
}
|
const resource = (webpackModule as NormalModule).resource;
|
||||||
} else {
|
if (resource) {
|
||||||
// Initialize a new cache
|
this.markResourceUsed(normalizePath(resource), currentUnused);
|
||||||
cache = new SourceFileCache();
|
|
||||||
// Only store cache if in watch mode
|
|
||||||
if (this.watchMode) {
|
|
||||||
this.sourceFileCache = cache;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
augmentHostWithCaching(host, cache);
|
|
||||||
|
|
||||||
const moduleResolutionCache = ts.createModuleResolutionCache(
|
for (const unused of currentUnused) {
|
||||||
host.getCurrentDirectory(),
|
if (state.previousUnused?.has(unused)) {
|
||||||
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,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 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;
|
continue;
|
||||||
}
|
}
|
||||||
|
addWarning(
|
||||||
// Ensure all program files are considered part of the compilation and will be watched.
|
compilation,
|
||||||
// Webpack does not normalize paths. Therefore, we need to normalize the path with FS seperators.
|
`${unused} is part of the TypeScript compilation but it's unused.\n` +
|
||||||
compilation.fileDependencies.add(externalizePath(sourceFile.fileName));
|
`Add only entry points to the 'files' or 'include' properties in your tsconfig.`,
|
||||||
|
);
|
||||||
// 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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
state.previousUnused = currentUnused;
|
||||||
compilation.hooks.finishModules.tapPromise(PLUGIN_NAME, async (modules) => {
|
|
||||||
// Rebuild any remaining AOT required modules
|
|
||||||
await this.rebuildRequiredFiles(modules, compilation, fileEmitter);
|
|
||||||
|
|
||||||
// Clear out the Webpack compilation to avoid an extra retaining reference
|
|
||||||
resourceLoader?.clearParentCompilation();
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
previousUnused = currentUnused;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Store file emitter for loader usage
|
|
||||||
emitRegistration.update(fileEmitter);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Store file emitter for loader usage
|
||||||
|
emitRegistration.update(fileEmitter);
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerWithCompilation(compilation: Compilation) {
|
private registerWithCompilation(compilation: Compilation) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user