I'm using Angular 17 and NX. I want to build my workbox service worker in the same build process as angular and not in a separate build step, because that would mean that I could not test my service worker using nx serve myApp
. I would have to nx build myApp
, then build my service-worker and then run the thing using another http-server alltogether.
What I got so far:
- Replaced all occurrences of
@angular-devkit/build-angular
with@nx/angular
in myproject.json
- Added the following under
targets.build.options
:"plugins": ["apps/myApp/wbInject.plugin.ts"]
- Created
apps/myApp/wbInject.plugin.ts
:
import * as esbuild from 'esbuild';
import { readFileSync } from 'fs';
import * as path from 'path';
const MATCH_FILES = /^.+\.(js|css|svg|img|html|txt|webmanifest)$/i;
const manifest: { url: string; revision: string }[] = [];
const projectRoot = 'apps/myApp/src';
const wbInject: esbuild.Plugin = {
name: 'wbInject',
setup(build: esbuild.PluginBuild) {
// Append ngsw-worker to entry points
// This is done so that it is discoverable in the onEnd hook
const options = build.initialOptions;
Object.assign(options.entryPoints || {}, {
'ngsw-worker': path.resolve(projectRoot, 'ngsw-worker.ts'),
});
let workerCode = '';
// Pre-build the worker, so that we get a single module for this
// as angular would chunk it otherwise
build.onStart(async () => {
console.log('Building service-worker');
const result = await esbuild.build({
entryPoints: [projectRoot + '/ngsw-worker.ts'],
bundle: true,
write: false,
platform: 'browser',
target: 'es2017',
});
workerCode = result.outputFiles[0].text;
});
// Inject manifest into worker
build.onEnd(async result => {
if (result.errors.length !== 0) return;
// Add build output to manifest
const files = result.outputFiles?.filter(f => f.path.match(MATCH_FILES) && !f.path.match(/ngsw-worker/)) || [];
for (const file of files) await addToManifest(file.path, file.contents);
const workerFile = result.outputFiles?.find(file => file.path.match(/ngsw-worker.*\.js$/));
const workerSourceMap = result.outputFiles?.find(file => file.path.match(/ngsw-worker.*\.js.map$/));
if (!workerFile) return;
// Remove cache busting from worker file
[workerFile, ...(workerSourceMap != null ? [workerSourceMap] : [])].forEach(
file => (file.path = file.path.replace(/\-[A-Z0-9]{8}\./, '.')),
);
// Inject manifest into worker
const updatedWorkerCode = workerCode.replace('self.__WB_MANIFEST', JSON.stringify(manifest));
// Update the worker file in the output
workerFile.contents = new TextEncoder().encode(updatedWorkerCode);
});
},
};
async function addToManifest(filePath: string, contents?: Uint8Array) {
const relPath = path.relative(process.cwd(), filePath);
if (manifest.findIndex(i => i.url === relPath) !== -1) return;
const content = contents || (await readFileSync(path.resolve(projectRoot, relPath)));
const hash = await createHash(content);
manifest.push({ url: relPath, revision: hash });
}
async function createHash(content: BufferSource) {
const hash: ArrayBuffer = await crypto.subtle.digest('SHA-256', content);
return Array.from(new Uint8Array(hash))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
export default wbInject;
So this does two things:
- hook into the build process, add a new entrypoint for my serviceworker file
ngsw-worker.ts
*, build the file separately and inject the result into the final build output. - Analyze the build output and create a manifest of entries (minus the service-worker itself) and inject that into my service-worker.
*: (the name is because we replaced angular service-worker with workbox and did not want to confuse browsers where this app is already installed)
Now you are asking; can't you just use workbox own injectManifest function for this? Well, first off it relies on disk content. And when building using nx serve myApp
, esbuild does not write to disk. Secondly InjectManifest is based on webpack which fits poorly into an esbuild context.
The problem
The esbuild result.outputFiles
only seem to contain the built javascript chunks, and not css, assets and html files. These files should also be a part of the workbox pre-cache. How can I retreive these files from the build process?