88
99import type { Config , Filesystem } from '@angular/service-worker/config' ;
1010import * as crypto from 'crypto' ;
11- import { constants as fsConstants , promises as fsPromises } from 'fs' ;
11+ import type { OutputFile } from 'esbuild' ;
12+ import { existsSync , constants as fsConstants , promises as fsPromises } from 'node:fs' ;
1213import * as path from 'path' ;
1314import { assertIsError } from './error' ;
1415import { loadEsmModule } from './load-esm' ;
@@ -61,6 +62,49 @@ class CliFilesystem implements Filesystem {
6162 }
6263}
6364
65+ class ResultFilesystem implements Filesystem {
66+ private readonly fileReaders = new Map < string , ( ) => Promise < string > > ( ) ;
67+
68+ constructor ( outputFiles : OutputFile [ ] , assetFiles : { source : string ; destination : string } [ ] ) {
69+ for ( const file of outputFiles ) {
70+ this . fileReaders . set ( '/' + file . path . replace ( / \\ / g, '/' ) , async ( ) => file . text ) ;
71+ }
72+ for ( const file of assetFiles ) {
73+ this . fileReaders . set ( '/' + file . destination . replace ( / \\ / g, '/' ) , ( ) =>
74+ fsPromises . readFile ( file . source , 'utf-8' ) ,
75+ ) ;
76+ }
77+ }
78+
79+ async list ( dir : string ) : Promise < string [ ] > {
80+ if ( dir !== '/' ) {
81+ throw new Error ( 'Serviceworker manifest generator should only list files from root.' ) ;
82+ }
83+
84+ return [ ...this . fileReaders . keys ( ) ] ;
85+ }
86+
87+ read ( file : string ) : Promise < string > {
88+ const reader = this . fileReaders . get ( file ) ;
89+ if ( reader === undefined ) {
90+ throw new Error ( 'File does not exist.' ) ;
91+ }
92+
93+ return reader ( ) ;
94+ }
95+
96+ async hash ( file : string ) : Promise < string > {
97+ return crypto
98+ . createHash ( 'sha1' )
99+ . update ( await this . read ( file ) )
100+ . digest ( 'hex' ) ;
101+ }
102+
103+ write ( ) : never {
104+ throw new Error ( 'Serviceworker manifest generator should not attempted to write.' ) ;
105+ }
106+ }
107+
64108export async function augmentAppWithServiceWorker (
65109 appRoot : string ,
66110 workspaceRoot : string ,
@@ -93,22 +137,37 @@ export async function augmentAppWithServiceWorker(
93137 }
94138 }
95139
96- return augmentAppWithServiceWorkerCore (
140+ const result = await augmentAppWithServiceWorkerCore (
97141 config ,
98- outputPath ,
142+ new CliFilesystem ( outputFileSystem , outputPath ) ,
99143 baseHref ,
100- inputputFileSystem ,
101- outputFileSystem ,
102144 ) ;
145+
146+ const copy = async ( src : string , dest : string ) : Promise < void > => {
147+ const resolvedDest = path . join ( outputPath , dest ) ;
148+
149+ return inputputFileSystem === outputFileSystem
150+ ? // Native FS (Builder).
151+ inputputFileSystem . copyFile ( src , resolvedDest , fsConstants . COPYFILE_FICLONE )
152+ : // memfs (Webpack): Read the file from the input FS (disk) and write it to the output FS (memory).
153+ outputFileSystem . writeFile ( resolvedDest , await inputputFileSystem . readFile ( src ) ) ;
154+ } ;
155+
156+ await outputFileSystem . writeFile ( path . join ( outputPath , 'ngsw.json' ) , result . manifest ) ;
157+
158+ for ( const { source, destination } of result . assetFiles ) {
159+ await copy ( source , destination ) ;
160+ }
103161}
104162
105163// This is currently used by the esbuild-based builder
106164export async function augmentAppWithServiceWorkerEsbuild (
107165 workspaceRoot : string ,
108166 configPath : string ,
109- outputPath : string ,
110167 baseHref : string ,
111- ) : Promise < void > {
168+ outputFiles : OutputFile [ ] ,
169+ assetFiles : { source : string ; destination : string } [ ] ,
170+ ) : Promise < { manifest : string ; assetFiles : { source : string ; destination : string } [ ] } > {
112171 // Read the configuration file
113172 let config : Config | undefined ;
114173 try {
@@ -128,17 +187,18 @@ export async function augmentAppWithServiceWorkerEsbuild(
128187 }
129188 }
130189
131- // TODO: Return the output files and any errors/warnings
132- return augmentAppWithServiceWorkerCore ( config , outputPath , baseHref ) ;
190+ return augmentAppWithServiceWorkerCore (
191+ config ,
192+ new ResultFilesystem ( outputFiles , assetFiles ) ,
193+ baseHref ,
194+ ) ;
133195}
134196
135197export async function augmentAppWithServiceWorkerCore (
136198 config : Config ,
137- outputPath : string ,
199+ serviceWorkerFilesystem : Filesystem ,
138200 baseHref : string ,
139- inputputFileSystem = fsPromises ,
140- outputFileSystem = fsPromises ,
141- ) : Promise < void > {
201+ ) : Promise < { manifest : string ; assetFiles : { source : string ; destination : string } [ ] } > {
142202 // Load ESM `@angular/service-worker/config` using the TypeScript dynamic import workaround.
143203 // Once TypeScript provides support for keeping the dynamic import this workaround can be
144204 // changed to a direct dynamic import.
@@ -149,41 +209,27 @@ export async function augmentAppWithServiceWorkerCore(
149209 ) . Generator ;
150210
151211 // Generate the manifest
152- const generator = new GeneratorConstructor (
153- new CliFilesystem ( outputFileSystem , outputPath ) ,
154- baseHref ,
155- ) ;
212+ const generator = new GeneratorConstructor ( serviceWorkerFilesystem , baseHref ) ;
156213 const output = await generator . process ( config ) ;
157214
158215 // Write the manifest
159216 const manifest = JSON . stringify ( output , null , 2 ) ;
160- await outputFileSystem . writeFile ( path . join ( outputPath , 'ngsw.json' ) , manifest ) ;
161217
162218 // Find the service worker package
163219 const workerPath = require . resolve ( '@angular/service-worker/ngsw-worker.js' ) ;
164220
165- const copy = async ( src : string , dest : string ) : Promise < void > => {
166- const resolvedDest = path . join ( outputPath , dest ) ;
167-
168- return inputputFileSystem === outputFileSystem
169- ? // Native FS (Builder).
170- inputputFileSystem . copyFile ( src , resolvedDest , fsConstants . COPYFILE_FICLONE )
171- : // memfs (Webpack): Read the file from the input FS (disk) and write it to the output FS (memory).
172- outputFileSystem . writeFile ( resolvedDest , await inputputFileSystem . readFile ( src ) ) ;
221+ const result = {
222+ manifest,
223+ // Main worker code
224+ assetFiles : [ { source : workerPath , destination : 'ngsw-worker.js' } ] ,
173225 } ;
174226
175- // Write the worker code
176- await copy ( workerPath , 'ngsw-worker.js' ) ;
177-
178227 // If present, write the safety worker code
179- try {
180- const safetyPath = path . join ( path . dirname ( workerPath ) , 'safety-worker.js' ) ;
181- await copy ( safetyPath , 'worker-basic.min.js' ) ;
182- await copy ( safetyPath , 'safety-worker.js' ) ;
183- } catch ( error ) {
184- assertIsError ( error ) ;
185- if ( error . code !== 'ENOENT' ) {
186- throw error ;
187- }
228+ const safetyPath = path . join ( path . dirname ( workerPath ) , 'safety-worker.js' ) ;
229+ if ( existsSync ( safetyPath ) ) {
230+ result . assetFiles . push ( { source : safetyPath , destination : 'worker-basic.min.js' } ) ;
231+ result . assetFiles . push ( { source : safetyPath , destination : 'safety-worker.js' } ) ;
188232 }
233+
234+ return result ;
189235}
0 commit comments