Building workbox into angular using custom esbuilder

76 views Asked by At

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:

  1. Replaced all occurrences of @angular-devkit/build-angular with @nx/angular in my project.json
  2. Added the following under targets.build.options: "plugins": ["apps/myApp/wbInject.plugin.ts"]
  3. 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:

  1. 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.
  2. 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?

0

There are 0 answers