Search code examples
angularwebpackworkboxnrwl-nxworkbox-webpack-plugin

Adding WorkBox to an angular webpack app in an NX workspace


I'm trying to switch ServiceWorker in my application, from @angular/service-worker to workbox, but I'm finding documentation lacking good examples. I know this is a framework agnostic tool, but the build seems a bit cumbersome. Is there a more streamlined way of doing this? Or am I following best-practice here?

I have the following structure:

  apps
  ⨽ myApp
    ⊦ sw.ts                  ← My Workbox SW template
    ⊦ tsconfig.workbox.json  ← Only needed because of my extra webpack build step
    ⊦ webpack.config.js      ← Config of the extra build step
    ⊦ workbox-build.js       ← Config for the workbox-cli when injecting
    ⊦ project.json           ← NX version of the angular cli config
  dist
  ⨽ apps
    ⨽ myApp

sw.ts

import { clientsClaim, skipWaiting } from 'workbox-core';
import { cleanupOutdatedCaches, precacheAndRoute } from 'workbox-precaching';

declare const self: ServiceWorkerGlobalScope;

skipWaiting();
clientsClaim();
cleanupOutdatedCaches();

precacheAndRoute(self.__WB_MANIFEST); // Will be filled in build time

tsconfig.workbox.json:

{
  "compilerOptions": {
    "typeRoots": ["./typings"],
    "module": "esnext",
    "moduleResolution": "node",
    "importHelpers": true,
    "target": "es2015",
    "lib": ["esnext", "webworker"]
  },
  "files": ["./sw.ts"]
}

webpack.config.json:

const path = require('path');

module.exports = {
  mode: 'production',
  output: {
    path: path.resolve(__dirname, '../../dist/apps/myApp'),
    filename: 'sw.js',
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        loader: 'ts-loader',
        options: { configFile: 'tsconfig.workbox.json' },
      },
    ],
  },
  resolve: { extensions: ['.js', '.ts'] },
};

workbox-build.js:

const { injectManifest } = require('workbox-build');

const workboxConfig = {
  globDirectory: 'dist/apps/myApp',
  globPatterns: ['**/*.{css,eot,html,ico,jpg,js,json,png,svg,ttf,txt,webmanifest,woff,woff2,webm,xml}'],
  globFollow: true, // follow symlinks
  globStrict: true, // fail on error
  globIgnores: [
    `**/*-es5.js*`,
    'sw.js',
  ],
  // Allows to avoid using cache busting for Angular files because Angular already takes care of that!
  dontCacheBustURLsMatching: new RegExp('.+.[a-f0-9]{20}..+'),
  maximumFileSizeToCacheInBytes: 10 * 1024 * 1024, // 10MB
  swSrc: 'dist/apps/myApp/sw.js',
  swDest: 'dist/apps/myApp/sw.js',
};

// Calling the method and output the result
injectManifest(workboxConfig).then(({ count, size }) => {
  console.log(`Generated ${workboxConfig.swDest},
  which will precache ${count} files, ${size} bytes.`);
});

And a script in my package.json:

"build:wb": "webpack ./apps/myApp/sw.ts --config ./apps/myApp/webpack.config.js && node ./apps/myApp/workbox-build.js"

So this runs two tasks; 1) compile sw.ts and 2) inject static resources as precache into the compiled sw.js. But I think it's a messy solution.

Angular already builds using webpack, can't I integrate this build process into my project.json using a custom webpack config which runs automatically after angular webpack has done it's thing? Does it really have to be an extra build step which is manually kicked off AFTER angular is done building?

I guess what I thought could be possible here, is have a reference to a custom webpack config in my project.json file, which was executed as the last step in the angular build pipeline. The webpack config would first compile my sw.ts, then collect the static files from the angular build, inject a precache of those files to the compiled sw.ts and output the result in the dist folder along side the rest of the angular compillation.

This would elliminate the need for an extra tsconfig file and a separate build script and make the whole build a lot more comprehensible.


Solution

  • Actually solved it using a custom webpack:

    projects.json:

    {
      ...
      "targets": {
        "build": {
          "executor": "@angular-builders/custom-webpack:browser",
          "options": {
            "customWebpackConfig": {
              "path": "apps/myApp/webpack.config.js",
              "replaceDuplicatePlugins": true
            },
            ...
          },
          ...
        },
        "serve": {
          "executor": "@angular-builders/custom-webpack:dev-server",
          ...
        }
      }
    }
    

    webpack.config.js:

    const path = require('path');
    const { InjectManifest } = require('workbox-webpack-plugin');
    const CopyPlugin = require('copy-webpack-plugin');
    const AngularWebpackPlugin = require('@ngtools/webpack').AngularWebpackPlugin;
    
    module.exports = {
      plugins: [
        // Required to get InjectManifest to precache assets, since this webpack config
        // apparently runs before angular copies assets to dist.
        new CopyPlugin({
          patterns: [
            {
              context: 'apps/myApp/src/assets',
              from: '**/*',
              to: './assets',
            }
          ],
        }),
        new InjectManifest({
          swSrc: path.resolve(__dirname, './src/sw.ts'),
          swDest: path.resolve(__dirname, '../../dist/apps/myApp/sw.js'),
          // Allows the plugin to compile the sw.ts file
          compileSrc: true,
          // Without this, Webpack yields an error.
          webpackCompilationPlugins: [
            // The only reason for having a separate tsconfig here, is to add
            // a different `files` array. If I include the sw.ts file in the main
            // tsconfig, angular complains a lot.
            new AngularWebpackPlugin({ tsconfig: 'apps/myApp/tsconfig.wb.json' }),
          ],
          exclude: [/\-es5.js$/, /sw.js/],
          maximumFileSizeToCacheInBytes: 10 * 1024 * 1024, // 10MB
        }),
      ],
    };
    

    Now I can run nx build myApp and it builds out everything, including the service-worker!

    I will happily accept other answers if anybody can improve on this.