Skip to content

Commit fa18319

Browse files
committed
refactor(@angular-devkit/build-angular): use Node.js promise fs API in service-worker augmentation
With the minimum version of Node.js now set to v12, the promise fs API can now be leveraged within the tooling. This change also uses `copyFile` (with copy-on-write where available) to setup the the service worker files as well a streaming APIs to generate service worker hashes. Both of which improves performance and reduces memory usage.
1 parent 8dbc5e7 commit fa18319

File tree

1 file changed

+82
-78
lines changed

1 file changed

+82
-78
lines changed

packages/angular_devkit/build_angular/src/utils/service-worker.ts

Lines changed: 82 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -5,67 +5,58 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.io/license
77
*/
8-
import {
9-
Path,
10-
dirname,
11-
getSystemPath,
12-
join,
13-
normalize,
14-
relative,
15-
tags,
16-
virtualFs,
17-
} from '@angular-devkit/core';
18-
import { NodeJsSyncHost } from '@angular-devkit/core/node';
19-
import {
20-
Filesystem,
21-
Generator,
22-
} from '@angular/service-worker/config';
8+
import { Path, getSystemPath, normalize } from '@angular-devkit/core';
9+
import { Config, Filesystem, Generator } from '@angular/service-worker/config';
2310
import * as crypto from 'crypto';
11+
import { constants as fsConstants, createReadStream, promises as fs } from 'fs';
12+
import * as path from 'path';
13+
import { pipeline } from 'stream';
2414

2515
class CliFilesystem implements Filesystem {
26-
constructor(private _host: virtualFs.Host, private base: string) { }
16+
constructor(private base: string) {}
2717

28-
list(path: string): Promise<string[]> {
29-
return this._recursiveList(this._resolve(path), []).catch(() => []);
18+
list(dir: string): Promise<string[]> {
19+
return this._recursiveList(this._resolve(dir), []);
3020
}
3121

32-
async read(path: string): Promise<string> {
33-
return virtualFs.fileBufferToString(await this._readIntoBuffer(path));
22+
read(file: string): Promise<string> {
23+
return fs.readFile(this._resolve(file), 'utf-8');
3424
}
3525

36-
async hash(path: string): Promise<string> {
37-
const sha1 = crypto.createHash('sha1');
38-
sha1.update(Buffer.from(await this._readIntoBuffer(path)));
39-
40-
return sha1.digest('hex');
41-
}
42-
43-
write(path: string, content: string): Promise<void> {
44-
return this._host.write(this._resolve(path), virtualFs.stringToFileBuffer(content))
45-
.toPromise();
26+
hash(file: string): Promise<string> {
27+
return new Promise((resolve, reject) => {
28+
const hash = crypto.createHash('sha1').setEncoding('hex');
29+
pipeline(
30+
createReadStream(this._resolve(file)),
31+
hash,
32+
(error) => error ? reject(error) : resolve(hash.read()),
33+
);
34+
});
4635
}
4736

48-
private _readIntoBuffer(path: string): Promise<ArrayBuffer> {
49-
return this._host.read(this._resolve(path)).toPromise();
37+
write(file: string, content: string): Promise<void> {
38+
return fs.writeFile(this._resolve(file), content);
5039
}
5140

52-
private _resolve(path: string): Path {
53-
return join(normalize(this.base), normalize(path));
41+
private _resolve(file: string): string {
42+
return path.join(this.base, file);
5443
}
5544

56-
private async _recursiveList(path: Path, items: string[]): Promise<string[]> {
57-
const fragments = await this._host.list(path).toPromise();
58-
59-
for (const fragment of fragments) {
60-
const item = join(path, fragment);
61-
62-
if (await this._host.isDirectory(item).toPromise()) {
63-
await this._recursiveList(item, items);
64-
} else {
65-
items.push('/' + relative(normalize(this.base), item));
45+
private async _recursiveList(dir: string, items: string[]): Promise<string[]> {
46+
const subdirectories = [];
47+
for await (const entry of await fs.opendir(dir)) {
48+
if (entry.isFile()) {
49+
// Uses posix paths since the service worker expects URLs
50+
items.push('/' + path.posix.relative(this.base, path.posix.join(dir, entry.name)));
51+
} else if (entry.isDirectory()) {
52+
subdirectories.push(path.join(dir, entry.name));
6653
}
6754
}
6855

56+
for (const subdirectory of subdirectories) {
57+
await this._recursiveList(subdirectory, items);
58+
}
59+
6960
return items;
7061
}
7162
}
@@ -77,61 +68,74 @@ export async function augmentAppWithServiceWorker(
7768
baseHref: string,
7869
ngswConfigPath?: string,
7970
): Promise<void> {
80-
const host = new NodeJsSyncHost();
81-
const distPath = normalize(outputPath);
71+
const distPath = getSystemPath(normalize(outputPath));
8272
const systemProjectRoot = getSystemPath(projectRoot);
8373

8474
// Find the service worker package
85-
const workerPath = normalize(
86-
require.resolve('@angular/service-worker/ngsw-worker.js', { paths: [systemProjectRoot] }),
87-
);
88-
const swConfigPath = require.resolve(
89-
'@angular/service-worker/config',
90-
{ paths: [systemProjectRoot] },
91-
);
75+
const workerPath = require.resolve('@angular/service-worker/ngsw-worker.js', {
76+
paths: [systemProjectRoot],
77+
});
78+
const swConfigPath = require.resolve('@angular/service-worker/config', {
79+
paths: [systemProjectRoot],
80+
});
9281

9382
// Determine the configuration file path
9483
let configPath;
9584
if (ngswConfigPath) {
96-
configPath = normalize(ngswConfigPath);
85+
configPath = getSystemPath(normalize(ngswConfigPath));
9786
} else {
98-
configPath = join(appRoot, 'ngsw-config.json');
99-
}
100-
101-
// Ensure the configuration file exists
102-
const configExists = await host.exists(configPath).toPromise();
103-
if (!configExists) {
104-
throw new Error(tags.oneLine`
105-
Error: Expected to find an ngsw-config.json configuration
106-
file in the ${getSystemPath(appRoot)} folder. Either provide one or disable Service Worker
107-
in your angular.json configuration file.
108-
`);
87+
configPath = path.join(getSystemPath(appRoot), 'ngsw-config.json');
10988
}
11089

11190
// Read the configuration file
112-
const config = JSON.parse(virtualFs.fileBufferToString(await host.read(configPath).toPromise()));
91+
let config: Config | undefined;
92+
try {
93+
const configurationData = await fs.readFile(configPath, 'utf-8');
94+
config = JSON.parse(configurationData) as Config;
95+
} catch (error) {
96+
if (error.code === 'ENOENT') {
97+
throw new Error(
98+
'Error: Expected to find an ngsw-config.json configuration file' +
99+
` in the ${getSystemPath(appRoot)} folder. Either provide one or` +
100+
' disable Service Worker in the angular.json configuration file.',
101+
);
102+
} else {
103+
throw error;
104+
}
105+
}
113106

114107
// Generate the manifest
115108
const GeneratorConstructor = require(swConfigPath).Generator as typeof Generator;
116-
const generator = new GeneratorConstructor(new CliFilesystem(host, outputPath), baseHref);
109+
const generator = new GeneratorConstructor(new CliFilesystem(distPath), baseHref);
117110
const output = await generator.process(config);
118111

119112
// Write the manifest
120113
const manifest = JSON.stringify(output, null, 2);
121-
await host.write(join(distPath, 'ngsw.json'), virtualFs.stringToFileBuffer(manifest)).toPromise();
114+
await fs.writeFile(path.join(distPath, 'ngsw.json'), manifest);
122115

123116
// Write the worker code
124-
// NOTE: This is inefficient (kernel -> userspace -> kernel).
125-
// `fs.copyFile` would be a better option but breaks the host abstraction
126-
const workerCode = await host.read(workerPath).toPromise();
127-
await host.write(join(distPath, 'ngsw-worker.js'), workerCode).toPromise();
117+
await fs.copyFile(
118+
workerPath,
119+
path.join(distPath, 'ngsw-worker.js'),
120+
fsConstants.COPYFILE_FICLONE,
121+
);
128122

129123
// If present, write the safety worker code
130-
const safetyPath = join(dirname(workerPath), 'safety-worker.js');
131-
if (await host.exists(safetyPath).toPromise()) {
132-
const safetyCode = await host.read(safetyPath).toPromise();
133-
134-
await host.write(join(distPath, 'worker-basic.min.js'), safetyCode).toPromise();
135-
await host.write(join(distPath, 'safety-worker.js'), safetyCode).toPromise();
124+
const safetyPath = path.join(path.dirname(workerPath), 'safety-worker.js');
125+
try {
126+
await fs.copyFile(
127+
safetyPath,
128+
path.join(distPath, 'worker-basic.min.js'),
129+
fsConstants.COPYFILE_FICLONE,
130+
);
131+
await fs.copyFile(
132+
safetyPath,
133+
path.join(distPath, 'safety-worker.js'),
134+
fsConstants.COPYFILE_FICLONE,
135+
);
136+
} catch (error) {
137+
if (error.code !== 'ENOENT') {
138+
throw error;
139+
}
136140
}
137141
}

0 commit comments

Comments
 (0)