From 1a5b71bb11859adb44caf8b740ef5d864c052b7b Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Mon, 16 Apr 2018 15:00:15 +0100 Subject: [PATCH] fix(@angular-devkit/build-angular): fix service worker assets paths Fix https://github.com/angular/angular-cli/issues/10297 --- .../utilities/service-worker/index.ts | 23 +++++- .../build_angular/src/server/index.ts | 3 + .../build_angular/src/server/schema.d.ts | 4 +- .../build_angular/src/server/schema.json | 46 ++++++++++-- .../test/browser/service-worker_spec_large.ts | 73 ++++++++++++++++++- 5 files changed, 135 insertions(+), 14 deletions(-) diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/service-worker/index.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/service-worker/index.ts index f0427f0d21..e77a8ed4bd 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/service-worker/index.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/service-worker/index.ts @@ -1,14 +1,14 @@ // tslint:disable // TODO: cleanup this file, it's copied as is from Angular CLI. -import { Path, join, normalize, virtualFs, dirname, getSystemPath, tags } from '@angular-devkit/core'; +import { Path, join, normalize, virtualFs, dirname, getSystemPath, tags, fragment } from '@angular-devkit/core'; import { Filesystem } from '@angular/service-worker/config'; import * as crypto from 'crypto'; import * as fs from 'fs'; import * as semver from 'semver'; import { resolveProjectModule } from '../require-project-module'; -import { map, reduce, switchMap } from "rxjs/operators"; -import { Observable, merge, of } from "rxjs"; +import { map, reduce, switchMap, concatMap, mergeMap, toArray, tap } from "rxjs/operators"; +import { Observable, merge, of, from } from "rxjs"; export const NEW_SW_VERSION = '5.0.0-rc.0'; @@ -18,7 +18,22 @@ class CliFilesystem implements Filesystem { constructor(private _host: virtualFs.Host, private base: string) { } list(path: string): Promise { - return this._host.list(this._resolve(path)).toPromise().then(x => x, _err => []); + const recursiveList = (path: Path): Observable => this._host.list(path).pipe( + // Emit each fragment individually. + concatMap(fragments => from(fragments)), + // Join the path with fragment. + map(fragment => join(path, fragment)), + // Emit directory content paths instead of the directory path. + mergeMap(path => this._host.isDirectory(path).pipe( + concatMap(isDir => isDir ? recursiveList(path) : of(path)) + ) + ), + ); + + return recursiveList(this._resolve(path)).pipe( + map(path => path.replace(this.base, '')), + toArray(), + ).toPromise().then(x => x, _err => []); } read(path: string): Promise { diff --git a/packages/angular_devkit/build_angular/src/server/index.ts b/packages/angular_devkit/build_angular/src/server/index.ts index c147bb74d6..4e42558909 100644 --- a/packages/angular_devkit/build_angular/src/server/index.ts +++ b/packages/angular_devkit/build_angular/src/server/index.ts @@ -37,6 +37,7 @@ import { import { BuildWebpackServerSchema } from './schema'; const webpackMerge = require('webpack-merge'); +import { addFileReplacements } from '../utils'; export class ServerBuilder implements Builder { @@ -46,12 +47,14 @@ export class ServerBuilder implements Builder { const options = builderConfig.options; const root = this.context.workspace.root; const projectRoot = resolve(root, builderConfig.root); + const host = new virtualFs.AliasHost(this.context.host as virtualFs.Host); // TODO: verify using of(null) to kickstart things is a pattern. return of(null).pipe( concatMap(() => options.deleteOutputPath ? this._deleteOutputDir(root, normalize(options.outputPath)) : of(null)), + concatMap(() => addFileReplacements(root, host, options.fileReplacements)), concatMap(() => new Observable(obs => { // Ensure Build Optimizer is only used with AOT. let webpackConfig; diff --git a/packages/angular_devkit/build_angular/src/server/schema.d.ts b/packages/angular_devkit/build_angular/src/server/schema.d.ts index d4daa6557d..12ec739897 100644 --- a/packages/angular_devkit/build_angular/src/server/schema.d.ts +++ b/packages/angular_devkit/build_angular/src/server/schema.d.ts @@ -70,9 +70,9 @@ export interface BuildWebpackServerSchema { */ lazyModules?: string[]; /** - * Defines the build environment. + * Replace files with other files in the build. */ - environment?: string; + fileReplacements: FileReplacements[]; /** * Define the output filename cache-busting hashing mode. */ diff --git a/packages/angular_devkit/build_angular/src/server/schema.json b/packages/angular_devkit/build_angular/src/server/schema.json index 42b582a164..f16c2e18ab 100644 --- a/packages/angular_devkit/build_angular/src/server/schema.json +++ b/packages/angular_devkit/build_angular/src/server/schema.json @@ -32,9 +32,13 @@ "description": "Defines the optimization level of the build.", "default": false }, - "environment": { - "type": "string", - "description": "Defines the build environment." + "fileReplacements": { + "description": "Replace files with other files in the build.", + "type": "array", + "items": { + "$ref": "#/definitions/fileReplacement" + }, + "default": [] }, "outputPath": { "type": "string", @@ -155,5 +159,37 @@ "outputPath", "main", "tsConfig" - ] -} \ No newline at end of file + ], + "definitions": { + "fileReplacement": { + "oneOf": [ + { + "type": "object", + "properties": { + "src": { + "type": "string" + }, + "replaceWith": { + "type": "string" + } + }, + "additionalProperties": false, + "required": ["src", "replaceWith"] + }, + { + "type": "object", + "properties": { + "replace": { + "type": "string" + }, + "with": { + "type": "string" + } + }, + "additionalProperties": false, + "required": ["replace", "with"] + } + ] + } + } +} diff --git a/packages/angular_devkit/build_angular/test/browser/service-worker_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/service-worker_spec_large.ts index c65292c6b5..ca23df7c2b 100644 --- a/packages/angular_devkit/build_angular/test/browser/service-worker_spec_large.ts +++ b/packages/angular_devkit/build_angular/test/browser/service-worker_spec_large.ts @@ -53,6 +53,7 @@ describe('Browser Builder', () => { it('works with service worker', (done) => { host.writeMultipleFiles({ 'src/ngsw-config.json': JSON.stringify(manifest), + 'src/assets/folder-asset.txt': 'folder-asset.txt', }); const overrides = { serviceWorker: true }; @@ -60,6 +61,40 @@ describe('Browser Builder', () => { tap(buildEvent => { expect(buildEvent.success).toBe(true); expect(host.scopedSync().exists(normalize('dist/ngsw.json'))); + const ngswJson = JSON.parse(virtualFs.fileBufferToString( + host.scopedSync().read(normalize('dist/ngsw.json')))); + // Verify index and assets are there. + expect(ngswJson).toEqual({ + configVersion: 1, + index: '/index.html', + assetGroups: [ + { + name: 'app', + installMode: 'prefetch', + updateMode: 'prefetch', + urls: [ + '/favicon.ico', + '/index.html', + ], + patterns: [], + }, + { + name: 'assets', + installMode: 'lazy', + updateMode: 'prefetch', + urls: [ + '/assets/folder-asset.txt', + ], + patterns: [], + }, + ], + dataGroups: [], + hashTable: { + '/favicon.ico': '460fcbd48b20fcc32b184388606af1238c890dba', + '/assets/folder-asset.txt': '617f202968a6a81050aa617c2e28e1dca11ce8d4', + '/index.html': '3e659d6e536916b7d178d02a2e6e5492f868bf68', + }, + }); }), ).subscribe(undefined, done.fail, done); }, Timeout.Basic); @@ -67,6 +102,7 @@ describe('Browser Builder', () => { it('works with service worker and baseHref', (done) => { host.writeMultipleFiles({ 'src/ngsw-config.json': JSON.stringify(manifest), + 'src/assets/folder-asset.txt': 'folder-asset.txt', }); const overrides = { serviceWorker: true, baseHref: '/foo/bar' }; @@ -74,9 +110,40 @@ describe('Browser Builder', () => { tap(buildEvent => { expect(buildEvent.success).toBe(true); expect(host.scopedSync().exists(normalize('dist/ngsw.json'))); - expect(virtualFs.fileBufferToString( - host.scopedSync().read(normalize('dist/ngsw.json')), - )).toMatch(/"\/foo\/bar\/index.html"/); + const ngswJson = JSON.parse(virtualFs.fileBufferToString( + host.scopedSync().read(normalize('dist/ngsw.json')))); + // Verify index and assets include the base href. + expect(ngswJson).toEqual({ + configVersion: 1, + index: '/foo/bar/index.html', + assetGroups: [ + { + name: 'app', + installMode: 'prefetch', + updateMode: 'prefetch', + urls: [ + '/foo/bar/favicon.ico', + '/foo/bar/index.html', + ], + patterns: [], + }, + { + name: 'assets', + installMode: 'lazy', + updateMode: 'prefetch', + urls: [ + '/foo/bar/assets/folder-asset.txt', + ], + patterns: [], + }, + ], + dataGroups: [], + hashTable: { + '/foo/bar/favicon.ico': '460fcbd48b20fcc32b184388606af1238c890dba', + '/foo/bar/assets/folder-asset.txt': '617f202968a6a81050aa617c2e28e1dca11ce8d4', + '/foo/bar/index.html': '5b53fa9e07e4111b8ef84613fb989a56fee502b0', + }, + }); }), ).subscribe(undefined, done.fail, done); }, Timeout.Basic);