diff --git a/packages/angular_devkit/build_angular/src/browser/specs/output-hashing_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/output-hashing_spec.ts deleted file mode 100644 index e13ed05fba..0000000000 --- a/packages/angular_devkit/build_angular/src/browser/specs/output-hashing_spec.ts +++ /dev/null @@ -1,195 +0,0 @@ -/** - * @license - * Copyright Google Inc. 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 { normalize } from '@angular-devkit/core'; -import { - browserBuild, - createArchitect, - host, - lazyModuleFiles, - lazyModuleFnImport, -} from '../../test-utils'; - -describe('Browser Builder output hashing', () => { - const target = { 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('updates hash as content changes', async () => { - const OUTPUT_RE = /(main|styles|lazy\.module)\.([a-z0-9]+)\.(chunk|bundle)\.(js|css)$/; - - function generateFileHashMap(): Map { - const hashes = new Map(); - - host - .scopedSync() - .list(normalize('./dist')) - .forEach(name => { - const matches = name.match(OUTPUT_RE); - if (matches) { - const [, module, hash] = matches; - hashes.set(module, hash); - } - }); - - return hashes; - } - - function validateHashes( - oldHashes: Map, - newHashes: Map, - shouldChange: Array, - ): void { - newHashes.forEach((hash, module) => { - if (hash == oldHashes.get(module)) { - if (shouldChange.includes(module)) { - throw new Error( - `Module "${module}" did not change hash (${hash}), but was expected to.`, - ); - } - } else if (!shouldChange.includes(module)) { - throw new Error(`Module "${module}" changed hash (${hash}), but was not expected to.`); - } - }); - } - - let oldHashes: Map; - let newHashes: Map; - - host.writeMultipleFiles(lazyModuleFiles); - host.writeMultipleFiles(lazyModuleFnImport); - - const overrides = { outputHashing: 'all', extractCss: true }; - - // We must do several builds instead of a single one in watch mode, so that the output - // path is deleted on each run and only contains the most recent files. - await browserBuild(architect, host, target, overrides); - - // Save the current hashes. - oldHashes = generateFileHashMap(); - host.writeMultipleFiles(lazyModuleFiles); - host.writeMultipleFiles(lazyModuleFnImport); - - await browserBuild(architect, host, target, overrides); - newHashes = generateFileHashMap(); - validateHashes(oldHashes, newHashes, []); - oldHashes = newHashes; - host.writeMultipleFiles({ 'src/styles.css': 'body { background: blue; }' }); - - // Style hash should change. - await browserBuild(architect, host, target, overrides); - newHashes = generateFileHashMap(); - validateHashes(oldHashes, newHashes, ['styles']); - oldHashes = newHashes; - host.writeMultipleFiles({ 'src/app/app.component.css': 'h1 { margin: 10px; }' }); - - // Main hash should change, since inline styles go in the main bundle. - await browserBuild(architect, host, target, overrides); - newHashes = generateFileHashMap(); - validateHashes(oldHashes, newHashes, ['main']); - oldHashes = newHashes; - host.appendToFile('src/app/lazy/lazy.module.ts', `console.log(1);`); - - // Lazy loaded bundle should change, and so should inline. - await browserBuild(architect, host, target, overrides); - newHashes = generateFileHashMap(); - validateHashes(oldHashes, newHashes, ['lazy.module']); - oldHashes = newHashes; - host.appendToFile('src/main.ts', ''); - - // Nothing should have changed. - await browserBuild(architect, host, target, overrides); - newHashes = generateFileHashMap(); - validateHashes(oldHashes, newHashes, []); - }); - - it('supports options', async () => { - host.writeMultipleFiles({ 'src/styles.css': `h1 { background: url('./spectrum.png')}` }); - host.writeMultipleFiles(lazyModuleFiles); - host.writeMultipleFiles(lazyModuleFnImport); - - // We must do several builds instead of a single one in watch mode, so that the output - // path is deleted on each run and only contains the most recent files. - // 'all' should hash everything. - await browserBuild(architect, host, target, { outputHashing: 'all', extractCss: true }); - - expect(host.fileMatchExists('dist', /runtime\.[0-9a-f]{20}\.js/)).toBeTruthy(); - expect(host.fileMatchExists('dist', /main\.[0-9a-f]{20}\.js/)).toBeTruthy(); - expect(host.fileMatchExists('dist', /polyfills\.[0-9a-f]{20}\.js/)).toBeTruthy(); - expect(host.fileMatchExists('dist', /vendor\.[0-9a-f]{20}\.js/)).toBeTruthy(); - expect(host.fileMatchExists('dist', /styles\.[0-9a-f]{20}\.css/)).toBeTruthy(); - expect(host.fileMatchExists('dist', /spectrum\.[0-9a-f]{20}\.png/)).toBeTruthy(); - - // 'none' should hash nothing. - await browserBuild(architect, host, target, { outputHashing: 'none', extractCss: true }); - - expect(host.fileMatchExists('dist', /runtime\.[0-9a-f]{20}\.js/)).toBeFalsy(); - expect(host.fileMatchExists('dist', /main\.[0-9a-f]{20}\.js/)).toBeFalsy(); - expect(host.fileMatchExists('dist', /polyfills\.[0-9a-f]{20}\.js/)).toBeFalsy(); - expect(host.fileMatchExists('dist', /vendor\.[0-9a-f]{20}\.js/)).toBeFalsy(); - expect(host.fileMatchExists('dist', /styles\.[0-9a-f]{20}\.css/)).toBeFalsy(); - expect(host.fileMatchExists('dist', /spectrum\.[0-9a-f]{20}\.png/)).toBeFalsy(); - - // 'media' should hash css resources only. - await browserBuild(architect, host, target, { outputHashing: 'media', extractCss: true }); - - expect(host.fileMatchExists('dist', /runtime\.[0-9a-f]{20}\.js/)).toBeFalsy(); - expect(host.fileMatchExists('dist', /main\.[0-9a-f]{20}\.js/)).toBeFalsy(); - expect(host.fileMatchExists('dist', /polyfills\.[0-9a-f]{20}\.js/)).toBeFalsy(); - expect(host.fileMatchExists('dist', /vendor\.[0-9a-f]{20}\.js/)).toBeFalsy(); - expect(host.fileMatchExists('dist', /styles\.[0-9a-f]{20}\.css/)).toBeFalsy(); - expect(host.fileMatchExists('dist', /spectrum\.[0-9a-f]{20}\.png/)).toBeTruthy(); - - // 'bundles' should hash bundles only. - await browserBuild(architect, host, target, { outputHashing: 'bundles', extractCss: true }); - expect(host.fileMatchExists('dist', /runtime\.[0-9a-f]{20}\.js/)).toBeTruthy(); - expect(host.fileMatchExists('dist', /main\.[0-9a-f]{20}\.js/)).toBeTruthy(); - expect(host.fileMatchExists('dist', /polyfills\.[0-9a-f]{20}\.js/)).toBeTruthy(); - expect(host.fileMatchExists('dist', /vendor\.[0-9a-f]{20}\.js/)).toBeTruthy(); - expect(host.fileMatchExists('dist', /styles\.[0-9a-f]{20}\.css/)).toBeTruthy(); - expect(host.fileMatchExists('dist', /spectrum\.[0-9a-f]{20}\.png/)).toBeFalsy(); - }); - - it('does not hash non injected styles', async () => { - const overrides = { - outputHashing: 'all', - extractCss: true, - styles: [{ input: 'src/styles.css', inject: false }], - }; - - await browserBuild(architect, host, target, overrides); - - expect(host.fileMatchExists('dist', /styles\.[0-9a-f]{20}\.js/)).toBeFalsy(); - expect(host.fileMatchExists('dist', /styles\.[0-9a-f]{20}\.js.map/)).toBeFalsy(); - expect(host.scopedSync().exists(normalize('dist/styles.css'))).toBe(true); - expect(host.scopedSync().exists(normalize('dist/styles.css.map'))).toBe(true); - }); - - it('does not hash non injected styles when optimization is enabled', async () => { - host.writeMultipleFiles({ 'src/styles.css': 'body { background: blue; }' }); - - const overrides = { - outputHashing: 'all', - extractCss: true, - optimization: true, - styles: [{ input: 'src/styles.css', inject: false }], - }; - - await browserBuild(architect, host, target, overrides); - expect(host.fileMatchExists('dist', /styles\.[0-9a-f]{20}\.js/)).toBeFalsy(); - expect(host.fileMatchExists('dist', /styles\.[0-9a-f]{20}\.js.map/)).toBeFalsy(); - expect(host.scopedSync().exists(normalize('dist/styles.css'))).toBe(true); - expect(host.scopedSync().exists(normalize('dist/styles.css.map'))).toBe(true); - }); -}); diff --git a/packages/angular_devkit/build_angular/src/browser/tests/options/output-hashing_spec.ts b/packages/angular_devkit/build_angular/src/browser/tests/options/output-hashing_spec.ts new file mode 100644 index 0000000000..6f046fcede --- /dev/null +++ b/packages/angular_devkit/build_angular/src/browser/tests/options/output-hashing_spec.ts @@ -0,0 +1,176 @@ +/** + * @license + * Copyright Google Inc. 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 { buildWebpackBrowser } from '../../index'; +import { OutputHashing } from '../../schema'; +import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; + +describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { + describe('Option: "outputHashing"', () => { + beforeEach(async () => { + // Application code is not needed for asset tests + await harness.writeFile('src/main.ts', ''); + }); + + it('hashes all filenames when set to "all"', async () => { + await harness.writeFile( + 'src/styles.css', + `h1 { background: url('./spectrum.png')}`, + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: ['src/styles.css'], + outputHashing: OutputHashing.All, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + + expect(harness.hasFileMatch('dist', /runtime\.[0-9a-f]{20}\.js$/)).toBeTrue(); + expect(harness.hasFileMatch('dist', /main\.[0-9a-f]{20}\.js$/)).toBeTrue(); + expect(harness.hasFileMatch('dist', /polyfills\.[0-9a-f]{20}\.js$/)).toBeTrue(); + expect(harness.hasFileMatch('dist', /styles\.[0-9a-f]{20}\.css$/)).toBeTrue(); + expect(harness.hasFileMatch('dist', /spectrum\.[0-9a-f]{20}\.png$/)).toBeTrue(); + }); + + it(`doesn't hash any filenames when not set`, async () => { + await harness.writeFile( + 'src/styles.css', + `h1 { background: url('./spectrum.png')}`, + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: ['src/styles.css'], + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + + expect(harness.hasFileMatch('dist', /runtime\.[0-9a-f]{20}\.js$/)).toBeFalse(); + expect(harness.hasFileMatch('dist', /main\.[0-9a-f]{20}\.js$/)).toBeFalse(); + expect(harness.hasFileMatch('dist', /polyfills\.[0-9a-f]{20}\.js$/)).toBeFalse(); + expect(harness.hasFileMatch('dist', /styles\.[0-9a-f]{20}\.css$/)).toBeFalse(); + expect(harness.hasFileMatch('dist', /spectrum\.[0-9a-f]{20}\.png$/)).toBeFalse(); + }); + + it(`doesn't hash any filenames when set to "none"`, async () => { + await harness.writeFile( + 'src/styles.css', + `h1 { background: url('./spectrum.png')}`, + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: ['src/styles.css'], + outputHashing: OutputHashing.None, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + + expect(harness.hasFileMatch('dist', /runtime\.[0-9a-f]{20}\.js$/)).toBeFalse(); + expect(harness.hasFileMatch('dist', /main\.[0-9a-f]{20}\.js$/)).toBeFalse(); + expect(harness.hasFileMatch('dist', /polyfills\.[0-9a-f]{20}\.js$/)).toBeFalse(); + expect(harness.hasFileMatch('dist', /styles\.[0-9a-f]{20}\.css$/)).toBeFalse(); + expect(harness.hasFileMatch('dist', /spectrum\.[0-9a-f]{20}\.png$/)).toBeFalse(); + }); + + it(`hashes CSS resources filenames only when set to "media"`, async () => { + await harness.writeFile( + 'src/styles.css', + `h1 { background: url('./spectrum.png')}`, + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: ['src/styles.css'], + outputHashing: OutputHashing.Media, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + + expect(harness.hasFileMatch('dist', /runtime\.[0-9a-f]{20}\.js$/)).toBeFalse(); + expect(harness.hasFileMatch('dist', /main\.[0-9a-f]{20}\.js$/)).toBeFalse(); + expect(harness.hasFileMatch('dist', /polyfills\.[0-9a-f]{20}\.js$/)).toBeFalse(); + expect(harness.hasFileMatch('dist', /styles\.[0-9a-f]{20}\.css$/)).toBeFalse(); + expect(harness.hasFileMatch('dist', /spectrum\.[0-9a-f]{20}\.png$/)).toBeTrue(); + }); + + it(`hashes bundles filenames only when set to "bundles"`, async () => { + await harness.writeFile( + 'src/styles.css', + `h1 { background: url('./spectrum.png')}`, + ); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: ['src/styles.css'], + outputHashing: OutputHashing.Bundles, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + + expect(harness.hasFileMatch('dist', /runtime\.[0-9a-f]{20}\.js$/)).toBeTrue(); + expect(harness.hasFileMatch('dist', /main\.[0-9a-f]{20}\.js$/)).toBeTrue(); + expect(harness.hasFileMatch('dist', /polyfills\.[0-9a-f]{20}\.js$/)).toBeTrue(); + expect(harness.hasFileMatch('dist', /styles\.[0-9a-f]{20}\.css$/)).toBeTrue(); + expect(harness.hasFileMatch('dist', /spectrum\.[0-9a-f]{20}\.png$/)).toBeFalse(); + }); + + it('does not hash non injected styles', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + outputHashing: OutputHashing.All, + styles: [{ + input: 'src/styles.css', + inject: false, + }], + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + + expect(harness.hasFileMatch('dist', /styles\.[0-9a-f]{20}\.js$/)).toBeFalse(); + expect(harness.hasFileMatch('dist', /styles\.[0-9a-f]{20}\.js.map$/)).toBeFalse(); + harness.expectFile('dist/styles.css').toExist(); + harness.expectFile('dist/styles.css.map').toExist(); + }); + + it('does not override different files which has the same filenames when hashing is "none"', async () => { + await harness.writeFiles({ + 'src/styles.css': ` + h1 { background: url('./test.svg')} + h2 { background: url('./small/test.svg')} + `, + './src/test.svg': ` + Hello World + `, + './src/small/test.svg': ` + Hello World + `, + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: ['src/styles.css'], + outputHashing: OutputHashing.None, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBe(true); + + harness.expectFile('dist/test.svg').toExist(); + harness.expectFile('dist/small-test.svg').toExist(); + }); + }); +}); diff --git a/packages/angular_devkit/build_angular/src/testing/builder-harness.ts b/packages/angular_devkit/build_angular/src/testing/builder-harness.ts index ec841b3680..577ba868ad 100644 --- a/packages/angular_devkit/build_angular/src/testing/builder-harness.ts +++ b/packages/angular_devkit/build_angular/src/testing/builder-harness.ts @@ -312,6 +312,12 @@ export class BuilderHarness { return this.host.scopedSync().exists(normalize(path)); } + hasFileMatch(directory: string, pattern: RegExp): boolean { + return this.host.scopedSync() + .list(normalize(directory)) + .some(name => pattern.test(name)); + } + readFile(path: string): string { const content = this.host.scopedSync().read(normalize(path));