diff --git a/packages/angular_devkit/build_angular/src/builders/browser/tests/options/verbose_spec.ts b/packages/angular_devkit/build_angular/src/builders/browser/tests/options/verbose_spec.ts index b9e50f007d..598a2b44a2 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser/tests/options/verbose_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser/tests/options/verbose_spec.ts @@ -102,5 +102,59 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { ) .toPromise(); }); + + it('should not include error stacktraces when false', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + verbose: false, + styles: ['./src/styles.scss'], + }); + + // Create a compilatation error. + await harness.writeFile('./src/styles.scss', `@import 'invalid-module';`); + + const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); + expect(result?.success).toBeFalse(); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching(`SassError: Can't find stylesheet to import.`), + }), + ); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('styles.scss.webpack'), + }), + ); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('at Object.loader'), + }), + ); + }); + + it('should include error stacktraces when true', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + verbose: true, + styles: ['./src/styles.scss'], + }); + + // Create a compilatation error. + await harness.writeFile('./src/styles.scss', `@import 'invalid-module';`); + + const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); + expect(result?.success).toBeFalse(); + + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('styles.scss.webpack'), + }), + ); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('at Object.loader'), + }), + ); + }); }); }); diff --git a/packages/angular_devkit/build_angular/src/webpack/utils/helpers.ts b/packages/angular_devkit/build_angular/src/webpack/utils/helpers.ts index e965339348..16c0978bef 100644 --- a/packages/angular_devkit/build_angular/src/webpack/utils/helpers.ts +++ b/packages/angular_devkit/build_angular/src/webpack/utils/helpers.ts @@ -29,6 +29,8 @@ export interface HashFormat { script: string; } +export type WebpackStatsOptions = Exclude; + export function getOutputHashFormat(outputHashing = OutputHashing.None, length = 20): HashFormat { const hashTemplate = `.[contenthash:${length}]`; @@ -276,7 +278,6 @@ export function externalizePackages( } } -type WebpackStatsOptions = Exclude; export function getStatsOptions(verbose = false): WebpackStatsOptions { const webpackOutputOptions: WebpackStatsOptions = { all: false, // Fallback value for stats options when an option is not defined. It has precedence over local webpack defaults. @@ -306,6 +307,7 @@ export function getStatsOptions(verbose = false): WebpackStatsOptions { version: true, chunkModules: true, errorDetails: true, + errorStack: true, moduleTrace: true, logging: 'verbose', modulesSpace: Infinity, diff --git a/packages/angular_devkit/build_angular/src/webpack/utils/stats.ts b/packages/angular_devkit/build_angular/src/webpack/utils/stats.ts index b294cc5ee0..d39dfcc2f6 100644 --- a/packages/angular_devkit/build_angular/src/webpack/utils/stats.ts +++ b/packages/angular_devkit/build_angular/src/webpack/utils/stats.ts @@ -15,7 +15,7 @@ import { Schema as BrowserBuilderOptions } from '../../builders/browser/schema'; import { BudgetCalculatorResult } from '../../utils/bundle-calculator'; import { colors as ansiColors, removeColor } from '../../utils/color'; import { markAsyncChunksNonInitial } from './async-chunks'; -import { getStatsOptions, normalizeExtraEntryPoints } from './helpers'; +import { WebpackStatsOptions, getStatsOptions, normalizeExtraEntryPoints } from './helpers'; export function formatSize(size: number): string { if (size <= 0) { @@ -317,8 +317,10 @@ function statsToString( } } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function statsWarningsToString(json: StatsCompilation, statsConfig: any): string { +export function statsWarningsToString( + json: StatsCompilation, + statsConfig: WebpackStatsOptions, +): string { const colors = statsConfig.colors; const c = (x: string) => (colors ? ansiColors.reset.cyan(x) : x); const y = (x: string) => (colors ? ansiColors.reset.yellow(x) : x); @@ -352,8 +354,10 @@ export function statsWarningsToString(json: StatsCompilation, statsConfig: any): return output ? '\n' + output : output; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function statsErrorsToString(json: StatsCompilation, statsConfig: any): string { +export function statsErrorsToString( + json: StatsCompilation, + statsConfig: WebpackStatsOptions, +): string { const colors = statsConfig.colors; const c = (x: string) => (colors ? ansiColors.reset.cyan(x) : x); const yb = (x: string) => (colors ? ansiColors.reset.yellowBright(x) : x); @@ -369,7 +373,17 @@ export function statsErrorsToString(json: StatsCompilation, statsConfig: any): s if (typeof error === 'string') { output += r(`Error: ${error}\n\n`); } else { - const file = error.file || error.moduleName; + let file = error.file || error.moduleName; + // Clean up error paths + // Ex: ./src/app/styles.scss.webpack[javascript/auto]!=!./node_modules/css-loader/dist/cjs.js.... + // to ./src/app/styles.scss.webpack + if (file && !statsConfig.errorDetails) { + const webpackPathIndex = file.indexOf('.webpack['); + if (webpackPathIndex !== -1) { + file = file.substring(0, webpackPathIndex); + } + } + if (file) { output += c(file); if (error.loc) { @@ -377,10 +391,18 @@ export function statsErrorsToString(json: StatsCompilation, statsConfig: any): s } output += ' - '; } - if (!/^error/i.test(error.message)) { + + // In most cases webpack will add stack traces to error messages. + // This below cleans up the error from stacks. + // See: https://github.com/webpack/webpack/issues/15980 + const message = statsConfig.errorStack + ? error.message + : /[\s\S]+?(?=[\n\s]+at)/.exec(error.message)?.[0] ?? error.message; + + if (!/^error/i.test(message)) { output += r('Error: '); } - output += `${error.message}\n\n`; + output += `${message}\n\n`; } } @@ -428,9 +450,14 @@ export function webpackStatsLogger( ): void { logger.info(statsToString(json, config.stats, budgetFailures)); + if (typeof config.stats !== 'object') { + throw new Error('Invalid Webpack stats configuration.'); + } + if (statsHasWarnings(json)) { logger.warn(statsWarningsToString(json, config.stats)); } + if (statsHasErrors(json)) { logger.error(statsErrorsToString(json, config.stats)); }