Get list of webpack output files and use in another module for PWA

Viewed 593

I'm trying to build a progressive web app, with support for offline usage.

According to MDN, the way to make PWAs work offline is to add the required resources to a cache in the service worker. This requires that the service worker code knows each of the output files. Ideally, this shouldn't be harcoded, and should be generated by webpack, since it knows what files it generates.

I'm struggling to actually generate this list. From my search, there are two plugins that can generate a json file containing a list of the files - webpack-assets-manifest and webpack-manifest-plugin. I can use these in combination with separate targets to generate a manifest with the page files. But I can't import the manifest, since webpack doesn't actually write the manifest until everything is done.

How can I import a list of files that one entry point generates and use them in another entry point/module?

webpack.config.js:

const path = require('path');
const WebpackAssetsManifest = require('webpack-assets-manifest');


const frontend = {
    mode: "development",
    entry: {
        page:"./src/page/page.tsx",
    },
    devtool: 'inline-source-map',
    output: {
        path: path.resolve(__dirname, 'dist'),
    },
    module: {
        rules: [
            {
                test: /\.html?$|\.png$/,
                type: "asset/resource",
                generator: {
                    filename: "[name][ext]",
                },
            },
            {
                test: /\.tsx?$/,
                loader: "ts-loader",
                exclude: /node_modules/,
            },
            {
                test: /\.json$/,
                type: "asset/resource",
                exclude: /node_modules/,
            }
        ],
    },
    resolve: {
        extensions: [".html", ".tsx", ".ts", ".js"],
    },
    plugins: [
        new WebpackAssetsManifest({
            output: "page-files.json",
            writeToDisk: true,
        }),
    ]
};

const serviceworker = {
    mode: "development",
    entry: {
        serviceworker: "./src/serviceworker/serviceworker.ts",
    },
    devtool: 'inline-source-map',
    output: {
        path: path.resolve(__dirname, 'dist'),
    },
    module: {
        rules: [
            {
                test: /\.html?$|\.png$/,
                type: "asset/resource",
                generator: {
                    filename: "[name][ext]",
                },
            },
            {
                test: /\.tsx?$/,
                loader: "ts-loader",
                exclude: /node_modules/,
            },
            {
                test: /\.json$/,
                resourceQuery: /link/,
                type: "asset/resource",
                exclude: /node_modules/,
            },
            {
                test: /\.json$/,
                resourceQuery: /str/,
                type: "asset/source",
                exclude: /node_modules/,
            }
        ],
    },
    resolve: {
        extensions: [".html", ".tsx", ".ts", ".js"],
    },
};

module.exports = [frontend, serviceworker];

serviceworker.ts:

import files from "../../dist/page-files.json?str";
console.log(files);

Error is:

Module not found: Error: Can't resolve '../../dist/page-files.json?str' in '<REDACTED>/src/serviceworker'

(When I build it again, it will find the file from the previous build)

2 Answers

Rather than relying on pre-existing webpack plugins to generate assets, I think you're going to need to write your own plugin for this use case. And if you want that plugin to write the manifest to an entry that itself needs to be bundled/compiled, creating a child compilation in that plugin would be the way to do it.

This is unfortunately not a straightforward task, but you can refer to the source code for the workbox-webpack-plugin's InjectManifest plugin, which more or does what you describe, as inspiration.

Alternatively... you can just use InjectManifest directly, if that meets your use case. While it's part of the Workbox family of libraries, InjectManifest will only actually do two things: process the entry file you pass in as swSrc via a child compilation, and replace the symbol self.__WB_MANIFEST anywhere in that swSrc file with an array of {url: '...', revision: '...'} entries generated based on the assets in the main configuration, filtered by any include/exclude parameters.

So if you don't plan on using Workbox, you can just make use of that self.__WB_MANIFEST value from your own code.

// service-worker.ts

const manifest = self.__WB_MANIFEST || [];

self.addEventListener('install', (event) => {
  // Your code to cache the contents of manifest goes here.
});
// webpack.config.js

const {InjectManifest} = require('workbox-webpack-plugin');

module.exports = {
  // ...other webpack config...
  plugins: [
    new InjectManifest({
      swSrc: 'src/service-worker.ts',
      swDest: 'service-worker.js',
      // ...exclude/include config here...
    }),
  ],
};

The reason behind this behavior is that webpack runs these 2 configurations parallelly. By forcing webpack to run sequentially we can fix the problem.

To do serial processing, add module.exports.parallelism = 1; at end of your webpack config.

module.exports = [frontend, serviceworker];
module.exports.parallelism = 1;

Here is the documentation from webpack, https://webpack.js.org/configuration/configuration-types/#parallelism

Related