Skip to content

Configuration Recipes

Jovi De Croock edited this page Feb 19, 2021 · 15 revisions

A collection of configuration "recipes" that might come in handy when building with wmr.

Recipes:


Minifying HTML

To minify HTML we can use rollup-plugin-html-minifier:

npm i rollup-plugin-html-minifier

After installing, add it to the config file (wmr.config.js).

import htmlMinifier from 'rollup-plugin-html-minifier';

export function build({ plugins }) {
  plugins.push(
    htmlMinifier({
      // any options here
    })
  );
}

Importing directories of files

The first step to import multiple files from a directory is to list that directory's contents.

To do this in WMR, we can create a simple Rollup plugin that implements an ls: import prefix scheme:

import { promises as fs } from 'fs';
import path from 'path';

/**
 *  ls(1) plugin for Rollup / WMR
 *  import pages from 'ls:./pages';
 *  console.log(pages); // ['a.md', 'b.md']
 */
export default function lsPlugin({ cwd } = {}) {
  return {
    name: 'ls',
    async resolveId(id, importer) {
      if (!id.startsWith('ls:')) return;

      // pass through other plugins to resolve (the \0 avoids them trying to read the dir as a file)
      const r = await this.resolve(id.slice(3) + '\0', importer, { skipSelf: true });

      // during development, this will generate URLs like "/@ls/pages":
      if (r) return '\0ls:' + r.id.replace(/\0$/, '');
    },
    async load(id) {
      if (!id.startsWith('\0ls:')) return;

      // remove the "\0ls:" prefix and convert to an absolute path:
      id = path.resolve(cwd || '.', id.slice(4));

      // watch the directory for changes:
      this.addWatchFile(id);

      // generate a module that exports the directory contents as an Array:
      const files = (await fs.readdir(id)).filter(d => d[0] != '.');
      return `export default ${JSON.stringify(files)}`;
    }
  };
}

Using the plugin

Then enable the plugin by importing (or pasting!) it into your wmr.config.js:

// import the plugin function, or paste it (omitting the `export`):
import lsPlugin from './ls-plugin.js';

export default function (config) {
  // inject the `ls:` plugin into WMR:
  config.plugins.push(lsPlugin(config));
}

Let's assume a folder structure that looks like this:

index.js
pages/
  home.js
  about.js

Now we can "import" the list of files contained in our pages directory from index.js:

import files from 'ls:./pages';
console.log(files);  // ['home.js', 'about.js']

Filesystem-based routing / page component loading

Filesystem-based routing can be convenient. WMR doesn't provide this out-of-the-box, but it's relatively easy to implement.

πŸ‘‰ Try this filesystem-based routing example app on Glitch.

The first step is to set up the ls: import prefix plugin from our previous recipe.

With the plugin set up, we can "import" the list of the modules in a pages/ directory. From there, we can generate a URL for each module using its filename:

import files from 'ls:./pages';

files.map(name => {
  // the module can be dynamically imported like this:
  import(`./pages/${name}`).then(m => { ... });

  // the URL is the module's filename without an extension:
  const url = '/' + name.replace(/\.\w+$/, '');
  // note: we could also remove `index` from the name to produce `/` from `index.js`
});

This gives us a list of "page" modules, each with a URL and a way to import it.

The final step is to use the lazy() function from preact-iso to generate route components for each module, which automatically import the render component modules the first time they're used:

import { hydrate, lazy, ErrorBoundary, Router } from 'preact-iso';
import files from 'ls:./pages';

// Generate a Route component and URL for each "page" module:
const routes = files.map(name => ({
  Route: lazy(() => import(`./pages/${name}`)),
  url: '/' + name.replace(/(index)?\.\w+$/, '')  // strip file extension and "index"
}));

// Our simple example application:
const App = () => (
  <ErrorBoundary>
    <div id="app">
      <Router>
        {routes.map(({ Route, url }) =>
          <Route path={url} />
        )}
      </Router>
    </div>
  </ErrorBoundary>
);

hydrate(<App />);

That's it! This technique even works with prerendering and hydration - just export a prerender function from preact-iso at the bottom of the file:

export const prerender = async data => (await import('preact-iso/prerender')).default(<App {...data} />);

Service Worker

We'd like to make this simpler, or potentially handle Service Workers by default. For now though, here's how to add a Service Worker to your project that works in both development and production.

πŸ‘‰ Try This demo on Glitch.

First, add a simple Workbox-based Service Worker to your app:

// public/sw.js
import { pageCache, staticResourceCache } from 'workbox-recipes';
pageCache();
staticResourceCache();

Then, use a special sw: import to get the URL for that service worker:

// public/index.js
import swURL from 'sw:./sw.js';
navigator.serviceWorker.register(swURL);

Finally, add this swPlugin plugin to your wmr.config.js:

// wmr.config.js
import swPlugin from './sw-plugin.js';

export default function (options) {
  swPlugin(options);
}

Copy the plugin code below to a file sw-plugin.js in your repository root (next to the wmr.config.js file):

// sw-plugin.js
import path from 'path';
import { request } from 'http';

/**
 * Service Worker plugin for WMR.
 * @param {import('wmr').Options} options
 */
export default function swPlugin(options) {
  // In development, inject a middleware just to obtain the local address of WMR's HTTP server:
  let loopback;
  if (!options.prod) {
    options.middleware.push((req, res, next) => {
      if (!loopback) loopback = req.connection.address();
      next();
    });
  }

  const wmrProxyPlugin = {
    resolveId(id) {
      if (id.startsWith('/@npm/')) return id;
      if (!/^\.*\//.test(id)) return '/@npm/' + id;
    },
    load(id) {
      if (id.startsWith('/@npm/')) return new Promise((y, n) => {
        request({ ...loopback, path: id }, res => {
          let data = '';
          res.setEncoding('utf-8');
          res.on('data', c => { data += c });
          res.on('end', () => { y(data) });
        }).on('error', n).end();
      });
    }
  };

  options.plugins.push({
    name: 'sw',
    async resolveId(id, importer) {
      if (!id.startsWith('sw:')) return;
      const resolved = await this.resolve(id.slice(3), importer);
      if (resolved) return `\0sw:${resolved.id}`;
    },
    async load(id) {
      if (!id.startsWith('\0sw:')) return;

      // In production, we just emit a chunk:
      if (options.prod) {
        const fileId = this.emitFile({
          type: 'chunk',
          id: id.slice(4),
          fileName: 'sw.js'
        });
        return `export default import.meta.ROLLUP_FILE_URL_${fileId}`;
      }

      // In development, we bundle to IIFE via Rollup, but use WMR's HTTP server to transpile dependencies:
      id = path.resolve(options.cwd, id.slice(4));

      try {
        var { rollup } = await import('rollup');
      } catch (e) {
        console.error(e = 'Error: Service Worker compilation requires that you install Rollup:\n  npm i rollup');
        return `export default null; throw ${JSON.stringify(e)};`;
      }
      const bundle = await rollup({
        input: id,
        plugins: [wmrProxyPlugin]
      });
      const { output } = await bundle.generate({ format: 'iife', compact: true });
      const fileId = this.emitFile({
        type: 'asset',
        name: '_' + id,
        fileName: '_sw.js',
        source: output[0].code
      });
      return `export default import.meta.ROLLUP_FILE_URL_${fileId}`;
    }
  });
}

Restart the development or production server and you should see your Service Worker functioning once the page loads. For a full functioning PWA, make sure you add a web manifest file (manifest.json) and link to it.

Web worker

wmr supports web-workers out of the box, to make this happen you have to add the following code:

import url from 'bundle:./path/to/worker.js';

const worker = new Worker(url, { type: 'module' });

In production this will rely on Module Workers, which aren't perfectly supported everywhere. To fix that you could use a module workers polyfill (YMMV) or the rollup-plugin-off-main-thread plugin.