Skip to content

Commit ec26831

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 3302352 commit ec26831

File tree

1 file changed

+81
-78
lines changed

1 file changed

+81
-78
lines changed

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

Lines changed: 81 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -5,67 +5,57 @@
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+
items.push('/' + path.relative(this.base, path.join(dir, entry.name)));
50+
} else if (entry.isDirectory()) {
51+
subdirectories.push(path.join(dir, entry.name));
6652
}
6753
}
6854

55+
for (const subdirectory of subdirectories) {
56+
await this._recursiveList(subdirectory, items);
57+
}
58+
6959
return items;
7060
}
7161
}
@@ -77,61 +67,74 @@ export async function augmentAppWithServiceWorker(
7767
baseHref: string,
7868
ngswConfigPath?: string,
7969
): Promise<void> {
80-
const host = new NodeJsSyncHost();
81-
const distPath = normalize(outputPath);
70+
const distPath = getSystemPath(normalize(outputPath));
8271
const systemProjectRoot = getSystemPath(projectRoot);
8372

8473
// 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-
);
74+
const workerPath = require.resolve('@angular/service-worker/ngsw-worker.js', {
75+
paths: [systemProjectRoot],
76+
});
77+
const swConfigPath = require.resolve('@angular/service-worker/config', {
78+
paths: [systemProjectRoot],
79+
});
9280

9381
// Determine the configuration file path
9482
let configPath;
9583
if (ngswConfigPath) {
96-
configPath = normalize(ngswConfigPath);
84+
configPath = getSystemPath(normalize(ngswConfigPath));
9785
} 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-
`);
86+
configPath = path.join(getSystemPath(appRoot), 'ngsw-config.json');
10987
}
11088

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

114106
// Generate the manifest
115107
const GeneratorConstructor = require(swConfigPath).Generator as typeof Generator;
116-
const generator = new GeneratorConstructor(new CliFilesystem(host, outputPath), baseHref);
108+
const generator = new GeneratorConstructor(new CliFilesystem(distPath), baseHref);
117109
const output = await generator.process(config);
118110

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

123115
// 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();
116+
await fs.copyFile(
117+
workerPath,
118+
path.join(distPath, 'ngsw-worker.js'),
119+
fsConstants.COPYFILE_FICLONE,
120+
);
128121

129122
// 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();
123+
const safetyPath = path.join(path.dirname(workerPath), 'safety-worker.js');
124+
try {
125+
await fs.copyFile(
126+
safetyPath,
127+
path.join(distPath, 'worker-basic.min.js'),
128+
fsConstants.COPYFILE_FICLONE,
129+
);
130+
await fs.copyFile(
131+
safetyPath,
132+
path.join(distPath, 'safety-worker.js'),
133+
fsConstants.COPYFILE_FICLONE,
134+
);
135+
} catch (error) {
136+
if (error.code !== 'ENOENT') {
137+
throw error;
138+
}
136139
}
137140
}

0 commit comments

Comments
 (0)