-
-
Notifications
You must be signed in to change notification settings - Fork 110
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.