Skip to content

Commit c4efd82

Browse files
committed
feat(@angular-devkit/build-webpack): add support for service-worker
1 parent 4b3cf51 commit c4efd82

File tree

2 files changed

+116
-70
lines changed
  • packages/angular_devkit/build_webpack/src

2 files changed

+116
-70
lines changed
Lines changed: 92 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,51 @@
11
// tslint:disable
22
// TODO: cleanup this file, it's copied as is from Angular CLI.
3-
3+
import { Path, join, normalize, virtualFs, dirname, getSystemPath } from '@angular-devkit/core';
44
import { Filesystem } from '@angular/service-worker/config';
55
import { oneLine, stripIndent } from 'common-tags';
66
import * as crypto from 'crypto';
77
import * as fs from 'fs';
8-
import * as path from 'path';
98
import * as semver from 'semver';
109

1110
import { resolveProjectModule } from '../require-project-module';
11+
import { map, reduce, switchMap } from "rxjs/operators";
12+
import { Observable } from "rxjs";
13+
import { merge } from "rxjs/observable/merge";
14+
import { of } from "rxjs/observable/of";
15+
1216

1317
export const NEW_SW_VERSION = '5.0.0-rc.0';
1418

15-
class CliFilesystem implements Filesystem {
16-
constructor(private base: string) { }
1719

18-
list(_path: string): Promise<string[]> {
19-
return Promise.resolve(this.syncList(_path));
20-
}
21-
22-
private syncList(_path: string): string[] {
23-
const dir = this.canonical(_path);
24-
const entries = fs.readdirSync(dir).map(
25-
(entry: string) => ({ entry, stats: fs.statSync(path.posix.join(dir, entry)) }));
26-
const files = entries.filter((entry: any) => !entry.stats.isDirectory())
27-
.map((entry: any) => path.posix.join(_path, entry.entry));
20+
class CliFilesystem implements Filesystem {
21+
constructor(private _host: virtualFs.Host, private base: string) { }
2822

29-
return entries.filter((entry: any) => entry.stats.isDirectory())
30-
.map((entry: any) => path.posix.join(_path, entry.entry))
31-
.reduce((list: string[], subdir: string) => list.concat(this.syncList(subdir)), files);
23+
list(path: string): Promise<string[]> {
24+
return this._host.list(this._resolve(path)).toPromise().then(x => x, _err => []);
3225
}
3326

34-
read(_path: string): Promise<string> {
35-
const file = this.canonical(_path);
36-
return Promise.resolve(fs.readFileSync(file).toString());
27+
read(path: string): Promise<string> {
28+
return this._host.read(this._resolve(path))
29+
.toPromise()
30+
.then(content => virtualFs.fileBufferToString(content));
3731
}
3832

39-
hash(_path: string): Promise<string> {
33+
hash(path: string): Promise<string> {
4034
const sha1 = crypto.createHash('sha1');
41-
const file = this.canonical(_path);
42-
const contents: Buffer = fs.readFileSync(file);
43-
sha1.update(contents);
44-
return Promise.resolve(sha1.digest('hex'));
35+
36+
return this.read(path)
37+
.then(content => sha1.update(content))
38+
.then(() => sha1.digest('hex'));
4539
}
4640

47-
write(_path: string, contents: string): Promise<void> {
48-
const file = this.canonical(_path);
49-
fs.writeFileSync(file, contents);
50-
return Promise.resolve();
41+
write(path: string, content: string): Promise<void> {
42+
return this._host.write(this._resolve(path), virtualFs.stringToFileBuffer(content))
43+
.toPromise();
5144
}
5245

53-
private canonical(_path: string): string { return path.posix.join(this.base, _path); }
46+
private _resolve(path: string): Path {
47+
return join(normalize(this.base), path);
48+
}
5449
}
5550

5651
export function usesServiceWorker(projectRoot: string): boolean {
@@ -81,38 +76,75 @@ export function usesServiceWorker(projectRoot: string): boolean {
8176
return true;
8277
}
8378

84-
export function augmentAppWithServiceWorker(projectRoot: string, appRoot: string,
85-
outputPath: string, baseHref: string): Promise<void> {
79+
export function augmentAppWithServiceWorker(
80+
host: virtualFs.Host,
81+
projectRoot: Path,
82+
appRoot: Path,
83+
outputPath: Path,
84+
baseHref: string,
85+
): Promise<void> {
8686
// Path to the worker script itself.
87-
const workerPath = resolveProjectModule(projectRoot, '@angular/service-worker/ngsw-worker.js');
88-
const safetyPath = path.join(path.dirname(workerPath), 'safety-worker.js');
89-
const configPath = path.resolve(appRoot, 'ngsw-config.json');
90-
91-
if (!fs.existsSync(configPath)) {
92-
return Promise.reject(new Error(oneLine`
93-
Error: Expected to find an ngsw-config.json configuration
94-
file in the ${appRoot} folder. Either provide one or disable Service Worker
95-
in .angular-cli.json.`,
96-
));
97-
}
98-
const config = fs.readFileSync(configPath, 'utf8');
87+
const distPath = normalize(outputPath);
88+
const workerPath = normalize(
89+
resolveProjectModule(getSystemPath(projectRoot), '@angular/service-worker/ngsw-worker.js'),
90+
);
91+
const swConfigPath = resolveProjectModule(
92+
getSystemPath(projectRoot),
93+
'@angular/service-worker/config',
94+
);
95+
const safetyPath = join(dirname(workerPath), 'safety-worker.js');
96+
const configPath = join(appRoot, 'ngsw-config.json');
97+
98+
return host.exists(configPath).pipe(
99+
switchMap(exists => {
100+
if (!exists) {
101+
throw new Error(oneLine`
102+
Error: Expected to find an ngsw-config.json configuration
103+
file in the ${appRoot} folder. Either provide one or disable Service Worker
104+
in your angular.json configuration file.`,
105+
);
106+
}
99107

100-
const Generator = require('@angular/service-worker/config').Generator;
101-
const gen = new Generator(new CliFilesystem(outputPath), baseHref);
102-
return gen
103-
.process(JSON.parse(config))
104-
.then((output: Object) => {
108+
return host.read(configPath) as Observable<virtualFs.FileBuffer>;
109+
}),
110+
map(content => JSON.parse(virtualFs.fileBufferToString(content))),
111+
switchMap(configJson => {
112+
const Generator = require(swConfigPath).Generator;
113+
const gen = new Generator(new CliFilesystem(host, outputPath), baseHref);
114+
115+
return gen.process(configJson);
116+
}),
117+
118+
switchMap(output => {
105119
const manifest = JSON.stringify(output, null, 2);
106-
fs.writeFileSync(path.resolve(outputPath, 'ngsw.json'), manifest);
107-
// Copy worker script to dist directory.
108-
const workerCode = fs.readFileSync(workerPath);
109-
fs.writeFileSync(path.resolve(outputPath, 'ngsw-worker.js'), workerCode);
110-
111-
// If @angular/service-worker has the safety script, copy it into two locations.
112-
if (fs.existsSync(safetyPath)) {
113-
const safetyCode = fs.readFileSync(safetyPath);
114-
fs.writeFileSync(path.resolve(outputPath, 'worker-basic.min.js'), safetyCode);
115-
fs.writeFileSync(path.resolve(outputPath, 'safety-worker.js'), safetyCode);
120+
return host.read(workerPath).pipe(
121+
switchMap(workerCode => {
122+
return merge(
123+
host.write(join(distPath, 'ngsw.json'), virtualFs.stringToFileBuffer(manifest)),
124+
host.write(join(distPath, 'ngsw-worker.js'), workerCode),
125+
) as Observable<void>;
126+
}),
127+
);
128+
}),
129+
130+
switchMap(() => host.exists(safetyPath)),
131+
// If @angular/service-worker has the safety script, copy it into two locations.
132+
switchMap(exists => {
133+
if (!exists) {
134+
return of<void>(undefined);
116135
}
117-
});
136+
137+
return host.read(safetyPath).pipe(
138+
switchMap(safetyCode => {
139+
return merge(
140+
host.write(join(distPath, 'worker-basic.min.js'), safetyCode),
141+
host.write(join(distPath, 'safety-worker.js'), safetyCode),
142+
) as Observable<void>;
143+
}),
144+
);
145+
}),
146+
147+
// Remove all elements, reduce them to a single emit.
148+
reduce(() => {}),
149+
).toPromise();
118150
}

packages/angular_devkit/build_webpack/src/browser/index.ts

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
import { getWebpackStatsConfig } from '../angular-cli-files/models/webpack-configs/utils';
2929
import { readTsconfig } from '../angular-cli-files/utilities/read-tsconfig';
3030
import { requireProjectModule } from '../angular-cli-files/utilities/require-project-module';
31+
import { augmentAppWithServiceWorker } from '../angular-cli-files/utilities/service-worker';
3132
import {
3233
statsErrorsToString,
3334
statsToString,
@@ -164,20 +165,33 @@ export class BrowserBuilder implements Builder<BrowserBuilderOptions> {
164165
this.context.logger.error(statsErrorsToString(json, statsConfig));
165166
}
166167

167-
obs.next({ success: !stats.hasErrors() });
168-
169168
if (options.watch) {
169+
obs.next({ success: !stats.hasErrors() });
170+
170171
// Never complete on watch mode.
171172
return;
172173
} else {
173-
// if (!!app.serviceWorker && runTaskOptions.target === 'production' &&
174-
// usesServiceWorker(this.project.root) && runTaskOptions.serviceWorker !== false) {
175-
// const appRoot = path.resolve(this.project.root, app.root);
176-
// augmentAppWithServiceWorker(this.project.root, appRoot, path.resolve(outputPath),
177-
// runTaskOptions.baseHref || '/')
178-
// .then(() => resolve(), (err: any) => reject(err));
179-
// }
180-
obs.complete();
174+
if (builderConfig.options.serviceWorker) {
175+
augmentAppWithServiceWorker(
176+
this.context.host,
177+
root,
178+
projectRoot,
179+
resolve(root, normalize(options.outputPath)),
180+
options.baseHref || '/',
181+
).then(
182+
() => {
183+
obs.next({ success: !stats.hasErrors() });
184+
obs.complete();
185+
},
186+
(err: Error) => {
187+
// We error out here because we're not in watch mode anyway (see above).
188+
obs.error(err);
189+
},
190+
);
191+
} else {
192+
obs.next({ success: !stats.hasErrors() });
193+
obs.complete();
194+
}
181195
}
182196
};
183197

0 commit comments

Comments
 (0)