/** * @license * Copyright Google LLC All Rights Reserved. * * 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, getSystemPath, normalize } from '@angular-devkit/core'; import type { Config, Filesystem } from '@angular/service-worker/config'; import * as crypto from 'crypto'; import { createReadStream, promises as fs, constants as fsConstants } from 'fs'; import * as path from 'path'; import { pipeline } from 'stream'; import { loadEsmModule } from './load-esm'; class CliFilesystem implements Filesystem { constructor(private base: string) {} list(dir: string): Promise { return this._recursiveList(this._resolve(dir), []); } read(file: string): Promise { return fs.readFile(this._resolve(file), 'utf-8'); } 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()), ); }); } write(file: string, content: string): Promise { return fs.writeFile(this._resolve(file), content); } private _resolve(file: string): string { return path.join(this.base, file); } 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.relative(this.base, path.join(dir, entry.name)).replace(/\\/g, '/')); } else if (entry.isDirectory()) { subdirectories.push(path.join(dir, entry.name)); } } for (const subdirectory of subdirectories) { await this._recursiveList(subdirectory, items); } return items; } } export async function augmentAppWithServiceWorker( appRoot: Path, outputPath: Path, baseHref: string, ngswConfigPath?: string, ): Promise { const distPath = getSystemPath(normalize(outputPath)); // Determine the configuration file path const configPath = ngswConfigPath ? getSystemPath(normalize(ngswConfigPath)) : path.join(getSystemPath(appRoot), 'ngsw-config.json'); // Read the configuration file 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; } } // Load ESM `@angular/service-worker/config` using the TypeScript dynamic import workaround. // Once TypeScript provides support for keeping the dynamic import this workaround can be // changed to a direct dynamic import. const GeneratorConstructor = ( await loadEsmModule( '@angular/service-worker/config', ) ).Generator; // Generate the manifest 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 fs.writeFile(path.join(distPath, 'ngsw.json'), manifest); // Find the service worker package const workerPath = require.resolve('@angular/service-worker/ngsw-worker.js'); // Write the worker code await fs.copyFile( workerPath, path.join(distPath, 'ngsw-worker.js'), fsConstants.COPYFILE_FICLONE, ); // If present, write the safety worker code 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; } } }