From 10d4ede2de42dfc302dcb4c5790274290170568d Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Thu, 2 Dec 2021 13:56:10 +0100 Subject: [PATCH] fix(@ngtools/webpack): handle promise rejection during Angular program analyzes Currently, when `analyzeAsync` promise is rejected, the build fails with an `UnhandledPromiseRejectionWarning`. Example: ``` (node:69086) UnhandledPromiseRejectionWarning: Error: Cannot use a JavaScript or TypeScript file (/Users/error-testing/src/app/app.component.ts) in a component's styleUrls or templateUrl. at WebpackResourceLoader._compile (/Users/error-testing/node_modules/@ngtools/webpack/src/resource_loader.js:107:19) at WebpackResourceLoader.get (/Users/error-testing/node_modules/@ngtools/webpack/src/resource_loader.js:266:44) at Object.resourceHost.readResource (/Users/error-testing/node_modules/@ngtools/webpack/src/ivy/host.js:48:35) at AdapterResourceLoader.preload (file:///Users/error-testing/node_modules/@angular/compiler-cli/bundles/index.js:12177:31) at resolveStyleUrl (file:///Users/error-testing/node_modules/@angular/compiler-cli/bundles/index.js:9593:36) at file:///Users/error-testing/node_modules/@angular/compiler-cli/bundles/index.js:9621:47 at Array.map () at ComponentDecoratorHandler.preanalyze (file:///Users/error-testing/node_modules/@angular/compiler-cli/bundles/index.js:9621:29) at TraitCompiler.analyzeClass (file:///Users/error-testing/node_modules/@angular/compiler-cli/bundles/index.js:6647:39) at visit2 (file:///Users/error-testing/node_modules/@angular/compiler-cli/bundles/index.js:6494:14) ``` With this change we handle such error and also hide stacktraces ``` ./src/app/app.module.ts - Error: Module build failed (from ./node_modules/@ngtools/webpack/src/ivy/index.js): Error: Cannot use a JavaScript or TypeScript file (/Users/error-testing/src/app/app.component.ts) in a component's styleUrls or templateUrl. at /Users/error-testing/node_modules/@ngtools/webpack/src/ivy/loader.js:75:34 at processTicksAndRejections (internal/process/task_queues.js:95:5) ``` --- packages/ngtools/webpack/src/ivy/loader.ts | 4 +- packages/ngtools/webpack/src/ivy/plugin.ts | 121 +++++++++++---------- 2 files changed, 65 insertions(+), 60 deletions(-) diff --git a/packages/ngtools/webpack/src/ivy/loader.ts b/packages/ngtools/webpack/src/ivy/loader.ts index e35c8244bd41..583ee02fa83e 100644 --- a/packages/ngtools/webpack/src/ivy/loader.ts +++ b/packages/ngtools/webpack/src/ivy/loader.ts @@ -67,7 +67,9 @@ export function angularWebpackLoader(this: LoaderContext, content: stri callback(undefined, resultContent, resultMap); }) .catch((err) => { - callback(err); + // The below is needed to hide stacktraces from users. + const message = err instanceof Error ? err.message : err; + callback(new Error(message)); }); } diff --git a/packages/ngtools/webpack/src/ivy/plugin.ts b/packages/ngtools/webpack/src/ivy/plugin.ts index ad3ab58722be..17eaed1ab8d7 100644 --- a/packages/ngtools/webpack/src/ivy/plugin.ts +++ b/packages/ngtools/webpack/src/ivy/plugin.ts @@ -553,74 +553,77 @@ export class AngularWebpackPlugin { // Required to support asynchronous resource loading // Must be done before creating transformers or getting template diagnostics - const pendingAnalysis = angularCompiler.analyzeAsync().then(() => { - this.requiredFilesToEmit.clear(); + const pendingAnalysis = angularCompiler + .analyzeAsync() + .then(() => { + this.requiredFilesToEmit.clear(); - for (const sourceFile of builder.getSourceFiles()) { - if (sourceFile.isDeclarationFile) { - continue; - } + for (const sourceFile of builder.getSourceFiles()) { + if (sourceFile.isDeclarationFile) { + continue; + } - // Collect sources that are required to be emitted - if ( - !ignoreForEmit.has(sourceFile) && - !angularCompiler.incrementalDriver.safeToSkipEmit(sourceFile) - ) { - this.requiredFilesToEmit.add(normalizePath(sourceFile.fileName)); + // Collect sources that are required to be emitted + if ( + !ignoreForEmit.has(sourceFile) && + !angularCompiler.incrementalDriver.safeToSkipEmit(sourceFile) + ) { + this.requiredFilesToEmit.add(normalizePath(sourceFile.fileName)); - // If required to emit, diagnostics may have also changed - if (!ignoreForDiagnostics.has(sourceFile)) { - affectedFiles.add(sourceFile); - } - } else if ( - this.sourceFileCache && - !affectedFiles.has(sourceFile) && - !ignoreForDiagnostics.has(sourceFile) - ) { - // Use cached Angular diagnostics for unchanged and unaffected files - const angularDiagnostics = this.sourceFileCache.getAngularDiagnostics(sourceFile); - if (angularDiagnostics) { - diagnosticsReporter(angularDiagnostics); + // If required to emit, diagnostics may have also changed + if (!ignoreForDiagnostics.has(sourceFile)) { + affectedFiles.add(sourceFile); + } + } else if ( + this.sourceFileCache && + !affectedFiles.has(sourceFile) && + !ignoreForDiagnostics.has(sourceFile) + ) { + // Use cached Angular diagnostics for unchanged and unaffected files + const angularDiagnostics = this.sourceFileCache.getAngularDiagnostics(sourceFile); + if (angularDiagnostics) { + diagnosticsReporter(angularDiagnostics); + } } } - } - // Temporary workaround during transition to ESM-only @angular/compiler-cli - // TODO_ESM: This workaround should be removed prior to the final release of v13 - // and replaced with only `this.compilerCli.OptimizeFor`. - const OptimizeFor = - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (this.compilerCli as any).OptimizeFor ?? - require('@angular/compiler-cli/src/ngtsc/typecheck/api').OptimizeFor; - - // Collect new Angular diagnostics for files affected by changes - const optimizeDiagnosticsFor = - affectedFiles.size <= DIAGNOSTICS_AFFECTED_THRESHOLD - ? OptimizeFor.SingleFile - : OptimizeFor.WholeProgram; - for (const affectedFile of affectedFiles) { - const angularDiagnostics = angularCompiler.getDiagnosticsForFile( - affectedFile, - optimizeDiagnosticsFor, - ); - diagnosticsReporter(angularDiagnostics); - this.sourceFileCache?.updateAngularDiagnostics(affectedFile, angularDiagnostics); - } + // Collect new Angular diagnostics for files affected by changes + const OptimizeFor = this.compilerCli.OptimizeFor; + const optimizeDiagnosticsFor = + affectedFiles.size <= DIAGNOSTICS_AFFECTED_THRESHOLD + ? OptimizeFor.SingleFile + : OptimizeFor.WholeProgram; + for (const affectedFile of affectedFiles) { + const angularDiagnostics = angularCompiler.getDiagnosticsForFile( + affectedFile, + optimizeDiagnosticsFor, + ); + diagnosticsReporter(angularDiagnostics); + this.sourceFileCache?.updateAngularDiagnostics(affectedFile, angularDiagnostics); + } + + return { + emitter: this.createFileEmitter( + builder, + mergeTransformers(angularCompiler.prepareEmit().transformers, transformers), + getDependencies, + (sourceFile) => { + this.requiredFilesToEmit.delete(normalizePath(sourceFile.fileName)); + angularCompiler.incrementalDriver.recordSuccessfulEmit(sourceFile); + }, + ), + }; + }) + .catch((err) => ({ errorMessage: err instanceof Error ? err.message : `${err}` })); - return this.createFileEmitter( - builder, - mergeTransformers(angularCompiler.prepareEmit().transformers, transformers), - getDependencies, - (sourceFile) => { - this.requiredFilesToEmit.delete(normalizePath(sourceFile.fileName)); - angularCompiler.incrementalDriver.recordSuccessfulEmit(sourceFile); - }, - ); - }); const analyzingFileEmitter: FileEmitter = async (file) => { - const innerFileEmitter = await pendingAnalysis; + const analysis = await pendingAnalysis; + + if ('errorMessage' in analysis) { + throw new Error(analysis.errorMessage); + } - return innerFileEmitter(file); + return analysis.emitter(file); }; return {