- 
          
 - 
                Notifications
    
You must be signed in to change notification settings  - Fork 107
 
Configuration Recipes
A collection of configuration "recipes" that might come in handy when building with wmr.
Recipes:
- Minifying HTML
 - Importing directories of files
 - Filesystem-based routing / page component loading
 - Service Worker
 - Web Worker
 
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
    })
  );
}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)}`;
    }
  };
}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 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} />);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.
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.