From fa18319a6a2462a3d0d9fc7a159db5ecf913d42a Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Tue, 13 Apr 2021 19:50:28 -0400 Subject: [PATCH] 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. --- .../build_angular/src/utils/service-worker.ts | 160 +++++++++--------- 1 file changed, 82 insertions(+), 78 deletions(-) diff --git a/packages/angular_devkit/build_angular/src/utils/service-worker.ts b/packages/angular_devkit/build_angular/src/utils/service-worker.ts index 5edeb6abf539..10eb38973855 100644 --- a/packages/angular_devkit/build_angular/src/utils/service-worker.ts +++ b/packages/angular_devkit/build_angular/src/utils/service-worker.ts @@ -5,67 +5,58 @@ * 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 { - Path, - dirname, - getSystemPath, - join, - normalize, - relative, - tags, - virtualFs, -} from '@angular-devkit/core'; -import { NodeJsSyncHost } from '@angular-devkit/core/node'; -import { - Filesystem, - Generator, -} from '@angular/service-worker/config'; +import { Path, getSystemPath, normalize } from '@angular-devkit/core'; +import { Config, Filesystem, Generator } from '@angular/service-worker/config'; import * as crypto from 'crypto'; +import { constants as fsConstants, createReadStream, promises as fs } from 'fs'; +import * as path from 'path'; +import { pipeline } from 'stream'; class CliFilesystem implements Filesystem { - constructor(private _host: virtualFs.Host, private base: string) { } + constructor(private base: string) {} - list(path: string): Promise { - return this._recursiveList(this._resolve(path), []).catch(() => []); + list(dir: string): Promise { + return this._recursiveList(this._resolve(dir), []); } - async read(path: string): Promise { - return virtualFs.fileBufferToString(await this._readIntoBuffer(path)); + read(file: string): Promise { + return fs.readFile(this._resolve(file), 'utf-8'); } - async hash(path: string): Promise { - const sha1 = crypto.createHash('sha1'); - sha1.update(Buffer.from(await this._readIntoBuffer(path))); - - return sha1.digest('hex'); - } - - write(path: string, content: string): Promise { - return this._host.write(this._resolve(path), virtualFs.stringToFileBuffer(content)) - .toPromise(); + hash(file: string): Promise { + return new Promise((resolve, reject) => { + const hash = crypto.createHash('sha1').setEncoding('hex'); + pipeline( + createReadStream(this._resolve(file)), + hash, + (error) => error ? reject(error) : resolve(hash.read()), + ); + }); } - private _readIntoBuffer(path: string): Promise { - return this._host.read(this._resolve(path)).toPromise(); + write(file: string, content: string): Promise { + return fs.writeFile(this._resolve(file), content); } - private _resolve(path: string): Path { - return join(normalize(this.base), normalize(path)); + private _resolve(file: string): string { + return path.join(this.base, file); } - private async _recursiveList(path: Path, items: string[]): Promise { - const fragments = await this._host.list(path).toPromise(); - - for (const fragment of fragments) { - const item = join(path, fragment); - - if (await this._host.isDirectory(item).toPromise()) { - await this._recursiveList(item, items); - } else { - items.push('/' + relative(normalize(this.base), item)); + private async _recursiveList(dir: string, items: string[]): Promise { + const subdirectories = []; + for await (const entry of await fs.opendir(dir)) { + if (entry.isFile()) { + // Uses posix paths since the service worker expects URLs + items.push('/' + path.posix.relative(this.base, path.posix.join(dir, entry.name))); + } else if (entry.isDirectory()) { + subdirectories.push(path.join(dir, entry.name)); } } + for (const subdirectory of subdirectories) { + await this._recursiveList(subdirectory, items); + } + return items; } } @@ -77,61 +68,74 @@ export async function augmentAppWithServiceWorker( baseHref: string, ngswConfigPath?: string, ): Promise { - const host = new NodeJsSyncHost(); - const distPath = normalize(outputPath); + const distPath = getSystemPath(normalize(outputPath)); const systemProjectRoot = getSystemPath(projectRoot); // Find the service worker package - const workerPath = normalize( - require.resolve('@angular/service-worker/ngsw-worker.js', { paths: [systemProjectRoot] }), - ); - const swConfigPath = require.resolve( - '@angular/service-worker/config', - { paths: [systemProjectRoot] }, - ); + const workerPath = require.resolve('@angular/service-worker/ngsw-worker.js', { + paths: [systemProjectRoot], + }); + const swConfigPath = require.resolve('@angular/service-worker/config', { + paths: [systemProjectRoot], + }); // Determine the configuration file path let configPath; if (ngswConfigPath) { - configPath = normalize(ngswConfigPath); + configPath = getSystemPath(normalize(ngswConfigPath)); } else { - configPath = join(appRoot, 'ngsw-config.json'); - } - - // Ensure the configuration file exists - const configExists = await host.exists(configPath).toPromise(); - if (!configExists) { - throw new Error(tags.oneLine` - Error: Expected to find an ngsw-config.json configuration - file in the ${getSystemPath(appRoot)} folder. Either provide one or disable Service Worker - in your angular.json configuration file. - `); + configPath = path.join(getSystemPath(appRoot), 'ngsw-config.json'); } // Read the configuration file - const config = JSON.parse(virtualFs.fileBufferToString(await host.read(configPath).toPromise())); + let config: Config | undefined; + try { + const configurationData = await fs.readFile(configPath, 'utf-8'); + config = JSON.parse(configurationData) as Config; + } catch (error) { + if (error.code === 'ENOENT') { + throw new Error( + 'Error: Expected to find an ngsw-config.json configuration file' + + ` in the ${getSystemPath(appRoot)} folder. Either provide one or` + + ' disable Service Worker in the angular.json configuration file.', + ); + } else { + throw error; + } + } // Generate the manifest const GeneratorConstructor = require(swConfigPath).Generator as typeof Generator; - const generator = new GeneratorConstructor(new CliFilesystem(host, outputPath), baseHref); + const generator = new GeneratorConstructor(new CliFilesystem(distPath), baseHref); const output = await generator.process(config); // Write the manifest const manifest = JSON.stringify(output, null, 2); - await host.write(join(distPath, 'ngsw.json'), virtualFs.stringToFileBuffer(manifest)).toPromise(); + await fs.writeFile(path.join(distPath, 'ngsw.json'), manifest); // Write the worker code - // NOTE: This is inefficient (kernel -> userspace -> kernel). - // `fs.copyFile` would be a better option but breaks the host abstraction - const workerCode = await host.read(workerPath).toPromise(); - await host.write(join(distPath, 'ngsw-worker.js'), workerCode).toPromise(); + await fs.copyFile( + workerPath, + path.join(distPath, 'ngsw-worker.js'), + fsConstants.COPYFILE_FICLONE, + ); // If present, write the safety worker code - const safetyPath = join(dirname(workerPath), 'safety-worker.js'); - if (await host.exists(safetyPath).toPromise()) { - const safetyCode = await host.read(safetyPath).toPromise(); - - await host.write(join(distPath, 'worker-basic.min.js'), safetyCode).toPromise(); - await host.write(join(distPath, 'safety-worker.js'), safetyCode).toPromise(); + const safetyPath = path.join(path.dirname(workerPath), 'safety-worker.js'); + try { + await fs.copyFile( + safetyPath, + path.join(distPath, 'worker-basic.min.js'), + fsConstants.COPYFILE_FICLONE, + ); + await fs.copyFile( + safetyPath, + path.join(distPath, 'safety-worker.js'), + fsConstants.COPYFILE_FICLONE, + ); + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; + } } }