From 442797ecfa68a26bd7b7e2d3ee30f7a2230f40b5 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Fri, 26 Nov 2021 15:07:46 +0100 Subject: [PATCH 1/2] test(@angular-devkit/build-angular): refactor bundle budgets option test to use new test harness With this change we replace the bundle budgets specs to use the new test harness. --- .../browser/specs/bundle-budgets_spec.ts | 196 ------------------ .../tests/options/bundle-budgets_spec.ts | 185 +++++++++++++++++ 2 files changed, 185 insertions(+), 196 deletions(-) delete mode 100644 packages/angular_devkit/build_angular/src/builders/browser/specs/bundle-budgets_spec.ts create mode 100644 packages/angular_devkit/build_angular/src/builders/browser/tests/options/bundle-budgets_spec.ts diff --git a/packages/angular_devkit/build_angular/src/builders/browser/specs/bundle-budgets_spec.ts b/packages/angular_devkit/build_angular/src/builders/browser/specs/bundle-budgets_spec.ts deleted file mode 100644 index 03026c7fe09d..000000000000 --- a/packages/angular_devkit/build_angular/src/builders/browser/specs/bundle-budgets_spec.ts +++ /dev/null @@ -1,196 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import { Architect } from '@angular-devkit/architect'; -import { logging } from '@angular-devkit/core'; -import { createArchitect, host } from '../../../testing/test-utils'; - -describe('Browser Builder bundle budgets', () => { - const cssExtensions = ['css', 'scss', 'less', 'styl']; - const targetSpec = { project: 'app', target: 'build' }; - let architect: Architect; - - beforeEach(async () => { - await host.initialize().toPromise(); - architect = (await createArchitect(host.root())).architect; - }); - afterEach(async () => host.restore().toPromise()); - - it('accepts valid bundles', async () => { - const overrides = { - optimization: true, - budgets: [{ type: 'allScript', maximumError: '100mb' }], - }; - const logger = new logging.Logger(''); - const logs: string[] = []; - logger.subscribe((e) => logs.push(e.message)); - - const run = await architect.scheduleTarget(targetSpec, overrides, { logger }); - const output = await run.result; - expect(output.success).toBe(true); - expect(logs.join()).not.toContain('Warning'); - await run.stop(); - }); - - it('shows errors', async () => { - const overrides = { - optimization: true, - budgets: [{ type: 'all', maximumError: '100b' }], - }; - - const run = await architect.scheduleTarget(targetSpec, overrides); - const output = await run.result; - expect(output.success).toBe(false); - await run.stop(); - }); - - it('shows warnings', async () => { - const overrides = { - optimization: true, - budgets: [{ type: 'all', minimumWarning: '100mb' }], - }; - const logger = new logging.Logger(''); - const logs: string[] = []; - logger.subscribe((e) => logs.push(e.message)); - - const run = await architect.scheduleTarget(targetSpec, overrides, { logger }); - const output = await run.result; - expect(output.success).toBe(true); - expect(logs.join()).toContain('Warning'); - await run.stop(); - }); - - cssExtensions.forEach((ext) => { - it(`shows warnings for large component ${ext} when using 'anyComponentStyle' when AOT`, async () => { - const overrides = { - aot: true, - optimization: true, - budgets: [{ type: 'anyComponentStyle', maximumWarning: '1b' }], - styles: [`src/styles.${ext}`], - }; - - const cssContent = ` - .foo { color: white; padding: 1px; } - .buz { color: white; padding: 2px; } - .bar { color: white; padding: 3px; } - `; - - host.writeMultipleFiles({ - [`src/app/app.component.${ext}`]: cssContent, - [`src/assets/foo.${ext}`]: cssContent, - [`src/styles.${ext}`]: cssContent, - }); - - host.replaceInFile( - 'src/app/app.component.ts', - './app.component.css', - `./app.component.${ext}`, - ); - - const logger = new logging.Logger(''); - const logs: string[] = []; - logger.subscribe((e) => logs.push(e.message)); - - const run = await architect.scheduleTarget(targetSpec, overrides, { logger }); - const output = await run.result; - expect(output.success).toBe(true); - expect(logs.join()).toMatch(`Warning.+app\\.component\\.${ext}`); - await run.stop(); - }); - }); - - cssExtensions.forEach((ext) => { - it(`shows error for large component ${ext} when using 'anyComponentStyle' when AOT`, async () => { - const overrides = { - aot: true, - optimization: true, - budgets: [{ type: 'anyComponentStyle', maximumError: '1b' }], - styles: [`src/styles.${ext}`], - }; - - const cssContent = ` - .foo { color: white; padding: 1px; } - .buz { color: white; padding: 2px; } - .bar { color: white; padding: 3px; } - `; - - host.writeMultipleFiles({ - [`src/app/app.component.${ext}`]: cssContent, - [`src/assets/foo.${ext}`]: cssContent, - [`src/styles.${ext}`]: cssContent, - }); - - host.replaceInFile( - 'src/app/app.component.ts', - './app.component.css', - `./app.component.${ext}`, - ); - - const logger = new logging.Logger(''); - const logs: string[] = []; - logger.subscribe((e) => logs.push(e.message)); - - const run = await architect.scheduleTarget(targetSpec, overrides, { logger }); - const output = await run.result; - expect(output.success).toBe(false); - expect(logs.join()).toMatch(`Error.+app\\.component\\.${ext}`); - await run.stop(); - }); - }); - - describe(`should ignore '.map' files`, () => { - it(`when 'bundle' budget`, async () => { - const overrides = { - optimization: true, - extractLicenses: true, - budgets: [{ type: 'bundle', name: 'main', maximumError: '3Kb' }], - }; - - const run = await architect.scheduleTarget(targetSpec, overrides); - const output = await run.result; - expect(output.success).toBe(true); - await run.stop(); - }); - - it(`when 'intial' budget`, async () => { - const overrides = { - optimization: true, - budgets: [{ type: 'initial', maximumError: '1mb' }], - }; - - const run = await architect.scheduleTarget(targetSpec, overrides); - const output = await run.result; - expect(output.success).toBe(true); - await run.stop(); - }); - - it(`when 'all' budget`, async () => { - const overrides = { - optimization: true, - budgets: [{ type: 'all', maximumError: '1mb' }], - }; - - const run = await architect.scheduleTarget(targetSpec, overrides); - const output = await run.result; - expect(output.success).toBe(true); - await run.stop(); - }); - - it(`when 'any' budget`, async () => { - const overrides = { - optimization: true, - budgets: [{ type: 'any', maximumError: '1mb' }], - }; - - const run = await architect.scheduleTarget(targetSpec, overrides); - const output = await run.result; - expect(output.success).toBe(true); - await run.stop(); - }); - }); -}); diff --git a/packages/angular_devkit/build_angular/src/builders/browser/tests/options/bundle-budgets_spec.ts b/packages/angular_devkit/build_angular/src/builders/browser/tests/options/bundle-budgets_spec.ts new file mode 100644 index 000000000000..3d1a266ecce1 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/builders/browser/tests/options/bundle-budgets_spec.ts @@ -0,0 +1,185 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { logging } from '@angular-devkit/core'; +import { buildWebpackBrowser } from '../../index'; +import { Type } from '../../schema'; +import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; + +describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { + const CSS_EXTENSIONS = ['css', 'scss', 'less', 'styl']; + const BUDGET_NOT_MET_REGEXP = /Budget .+ was not met by/; + + describe('Option: "bundleBudgets"', () => { + it(`should not warn when size is below threshold`, async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + optimization: true, + budgets: [{ type: Type.All, maximumWarning: '100mb' }], + }); + + const { result, logs } = await harness.executeOnce(); + expect(result?.success).toBe(true); + expect(logs).not.toContain( + jasmine.objectContaining({ + level: 'warn', + message: jasmine.stringMatching(BUDGET_NOT_MET_REGEXP), + }), + ); + }); + + it(`should error when size is above 'maximumError' threshold`, async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + optimization: true, + budgets: [{ type: Type.All, maximumError: '100b' }], + }); + + const { result, logs } = await harness.executeOnce(); + expect(result?.success).toBe(false); + expect(logs).toContain( + jasmine.objectContaining({ + level: 'error', + message: jasmine.stringMatching(BUDGET_NOT_MET_REGEXP), + }), + ); + }); + + it(`should warn when size is above 'maximumWarning' threshold`, async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + optimization: true, + budgets: [{ type: Type.All, maximumWarning: '100b' }], + }); + + const { result, logs } = await harness.executeOnce(); + expect(result?.success).toBe(true); + expect(logs).toContain( + jasmine.objectContaining({ + level: 'warn', + message: jasmine.stringMatching(BUDGET_NOT_MET_REGEXP), + }), + ); + }); + + CSS_EXTENSIONS.forEach((ext) => { + it(`shows warnings for large component ${ext} when using 'anyComponentStyle' when AOT`, async () => { + const cssContent = ` + .foo { color: white; padding: 1px; } + .buz { color: white; padding: 2px; } + .bar { color: white; padding: 3px; } + `; + + await harness.writeFiles({ + [`src/app/app.component.${ext}`]: cssContent, + [`src/assets/foo.${ext}`]: cssContent, + [`src/styles.${ext}`]: cssContent, + }); + + await harness.modifyFile('src/app/app.component.ts', (content) => + content.replace('app.component.css', `app.component.${ext}`), + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + optimization: true, + aot: true, + styles: [`src/styles.${ext}`], + budgets: [{ type: Type.AnyComponentStyle, maximumWarning: '1b' }], + }); + + const { result, logs } = await harness.executeOnce(); + expect(result?.success).toBe(true); + expect(logs).toContain( + jasmine.objectContaining({ + level: 'warn', + message: jasmine.stringMatching(new RegExp(`Warning.+app.component.${ext}`)), + }), + ); + }); + }); + + describe(`should ignore '.map' files`, () => { + it(`when 'bundle' budget`, async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + sourceMap: true, + optimization: true, + extractLicenses: true, + budgets: [{ type: Type.Bundle, name: 'main', maximumError: '1mb' }], + }); + + const { result, logs } = await harness.executeOnce(); + expect(result?.success).toBe(true); + expect(logs).not.toContain( + jasmine.objectContaining({ + level: 'error', + message: jasmine.stringMatching(BUDGET_NOT_MET_REGEXP), + }), + ); + }); + + it(`when 'intial' budget`, async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + sourceMap: true, + optimization: true, + extractLicenses: true, + budgets: [{ type: Type.Initial, name: 'main', maximumError: '1mb' }], + }); + + const { result, logs } = await harness.executeOnce(); + expect(result?.success).toBe(true); + expect(logs).not.toContain( + jasmine.objectContaining({ + level: 'error', + message: jasmine.stringMatching(BUDGET_NOT_MET_REGEXP), + }), + ); + }); + + it(`when 'all' budget`, async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + sourceMap: true, + optimization: true, + extractLicenses: true, + budgets: [{ type: Type.All, maximumError: '1mb' }], + }); + + const { result, logs } = await harness.executeOnce(); + expect(result?.success).toBe(true); + expect(logs).not.toContain( + jasmine.objectContaining({ + level: 'error', + message: jasmine.stringMatching(BUDGET_NOT_MET_REGEXP), + }), + ); + }); + + it(`when 'any' budget`, async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + sourceMap: true, + optimization: true, + extractLicenses: true, + budgets: [{ type: Type.Any, maximumError: '1mb' }], + }); + + const { result, logs } = await harness.executeOnce(); + expect(result?.success).toBe(true); + expect(logs).not.toContain( + jasmine.objectContaining({ + level: 'error', + message: jasmine.stringMatching(BUDGET_NOT_MET_REGEXP), + }), + ); + }); + }); + }); +}); From b81af2029287eba0cbf4de0084a7a17629e32e01 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Fri, 26 Nov 2021 20:27:28 +0100 Subject: [PATCH 2/2] fix(@angular-devkit/build-angular): lazy modules bundle budgets Since the introduction of Webpack 5 in version 12 bundle budgets for lazy chunks have been broken due to the removal of the `NamedLazyChunksPlugin`. https://github.com/angular/angular-cli/blob/21a49e6492dda1c4a325c0339518c3c110880d02/packages/angular_devkit/build_angular/src/webpack/plugins/named-chunks-plugin.ts#L8 With this change we re-introduce a similar plugin to allow setting bundle budgets on lazy chunks. This issue has also been reported on Slack by a GDE https://angular-team.slack.com/archives/C08M4JKNH/p1637115196222300 Closes: #11019 --- .../tests/options/bundle-budgets_spec.ts | 21 ++++++++ .../src/webpack/configs/common.ts | 3 +- .../webpack/plugins/named-chunks-plugin.ts | 53 +++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 packages/angular_devkit/build_angular/src/webpack/plugins/named-chunks-plugin.ts diff --git a/packages/angular_devkit/build_angular/src/builders/browser/tests/options/bundle-budgets_spec.ts b/packages/angular_devkit/build_angular/src/builders/browser/tests/options/bundle-budgets_spec.ts index 3d1a266ecce1..9fa3a66852b6 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser/tests/options/bundle-budgets_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser/tests/options/bundle-budgets_spec.ts @@ -7,6 +7,7 @@ */ import { logging } from '@angular-devkit/core'; +import { lazyModuleFiles, lazyModuleFnImport } from '../../../../testing/test-utils'; import { buildWebpackBrowser } from '../../index'; import { Type } from '../../schema'; import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; @@ -67,6 +68,26 @@ describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { ); }); + it(`should warn when lazy bundle is above 'maximumWarning' threshold`, async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + optimization: true, + budgets: [{ type: Type.Bundle, name: 'lazy-lazy-module', maximumWarning: '100b' }], + }); + + await harness.writeFiles(lazyModuleFiles); + await harness.writeFiles(lazyModuleFnImport); + + const { result, logs } = await harness.executeOnce(); + expect(result?.success).toBe(true); + expect(logs).toContain( + jasmine.objectContaining({ + level: 'warn', + message: jasmine.stringMatching('lazy-lazy-module exceeded maximum budget'), + }), + ); + }); + CSS_EXTENSIONS.forEach((ext) => { it(`shows warnings for large component ${ext} when using 'anyComponentStyle' when AOT`, async () => { const cssContent = ` diff --git a/packages/angular_devkit/build_angular/src/webpack/configs/common.ts b/packages/angular_devkit/build_angular/src/webpack/configs/common.ts index 1ec02db43899..a72944f69053 100644 --- a/packages/angular_devkit/build_angular/src/webpack/configs/common.ts +++ b/packages/angular_devkit/build_angular/src/webpack/configs/common.ts @@ -30,6 +30,7 @@ import { JsonStatsPlugin, ScriptsWebpackPlugin, } from '../plugins'; +import { NamedChunksPlugin } from '../plugins/named-chunks-plugin'; import { ProgressPlugin } from '../plugins/progress-plugin'; import { TransferSizePlugin } from '../plugins/transfer-size-plugin'; import { createIvyPlugin } from '../plugins/typescript'; @@ -448,7 +449,7 @@ export async function getCommonConfig(wco: WebpackConfigOptions): Promise { + compilation.hooks.chunkAsset.tap(PLUGIN_NAME, (chunk) => { + if (chunk.name) { + return; + } + + const name = this.generateName(chunk); + if (name) { + chunk.name = name; + } + }); + }); + } + + private generateName(chunk: Chunk): string | undefined { + for (const group of chunk.groupsIterable) { + const [block] = group.getBlocks(); + if (!(block instanceof AsyncDependenciesBlock)) { + continue; + } + + for (const dependency of block.dependencies) { + if (dependency instanceof ImportDependency) { + return Template.toPath(dependency.request); + } + } + } + + return undefined; + } +}