Daniel Imfeld
Posted on November 1, 2021
I recently converted my website from Sapper to SvelteKit. SvelteKit uses the Vite build tool, and one of Vite’s great features is Hot Module Reloading, or HMR, which can reload changed parts of a site without reloading the entire browser page.
This is great for speeding up developer productivity, and so I wanted to see if I could support HMR when changing the Markdown and HTML files that make up the content of this site as well. Between Vite’s HMR API and some SvelteKit features, it was easier than I expected.
Sometimes you may be able to just use Vite’s
import.meta.glob
to accomplish this. If this doesn’t fit your needs, read on!
Vite HMR Plugin
The first step is to add a build plugin into the Vite configuration. With SvelteKit, this configuration goes inside your svelte.config.js
.
Vite plugins have a similar API to Rollup plugins, but with some extra methods. The plugins provide one or more hooks into the build process, and then Vite calls those hooks at the appropriate time.
First, our plugin provides a configureServer
hook which tells Vite to watch the content directories for changes. In Rollup this would be done in the buildStart
hook and call this.watch
to add the paths, but that doesn’t work for Vite dev mode. Instead, you call server.watcher.add(path)
. In the snippet below, you can see I add three directories to the watcher.
Adding these directories tells Vite to monitor them for changes and have them participate in the HMR process. When a change occurs, Vite will call the handleHotUpdate
hook with one argument of type HmrContext
.
interface HmrContext {
file: string;
timestamp: number;
modules: Array<ModuleNode>;
read: () => string | Promise<string>;
server: ViteDevServer;
}
The full documentation explains it in more detail, but you can then return modules
, filter down the list of modules to reload fewer modules, or send a custom event to the client side using server.ws.send
. In our case, we look for a path matching one of the content directories, and send a custom client event. The full plugin is below.
/** @type {import('@sveltejs/kit').Config} */
export default {
kit: {
vite: () => {
plugins: [
{
name: 'watch-content',
configureServer(server) {
server.watcher.add(path.join(dirname, 'posts'));
server.watcher.add(path.join(dirname, 'notes'));
server.watcher.add(path.join(dirname, 'roam-pages'));
},
handleHotUpdate(ctx) {
let m = /(notes|posts|roam-pages)\/(.*)\.(md|html)$/.exec(ctx.file);
if (m) {
let contentType = m[1];
let id = m[2];
// This is just a conversion from the directory
// names to the URLs used in the site.
if (contentType === 'roam-pages') {
contentType = 'notes';
} else if (contentType === 'posts') {
contentType = 'writing';
}
ctx.server.ws.send({
type: 'custom',
event: 'content-update',
data: {
type: contentType,
id,
},
});
// Return an empty module list since we
// handled it manually.
return [];
}
// Not an event we care about, so just do
// the default behavior.
return ctx.modules;
},
},
];
},
},
};
The Client Side
Once we can send events to the client, we need to handle them and actually perform the reload. Happily, SvelteKit makes this easy to do. Each article page uses a SvelteKit load
function to fetch the content, and SvelteKit also lets you force these load functions to rerun using the invalidate
function.
Vite’s HMR API is fairly complex, but for our case of just listening to a custom event, it’s straightforward. We check for the presence of import.meta.hot
to see if Vite is running, and if so it’s just a matter of adding the event listener for the custom content-update
event.
<script>
import { invalidate } from '$app/navigation';
if (import.meta.hot) {
import.meta.hot.on('content-update', (data) => {
if (data.type === 'notes') {
invalidate('/notes/list.json');
invalidate('/notes/tags.json');
invalidate(`/notes/note/${data.id}.json`);
} else if (data.type === 'writing') {
invalidate('/writing/list.json');
invalidate(`/writing/${data.id}.json`);
}
});
}
</script>
Here I just placed a single event listener at the root __layout.svelte
file of the site, which examines the event and invalidates all the relevant endpoints. Larger sites would probably want to break this out to have each component manage its own HMR event handling, but the concept is the same.
SvelteKit internally tracks the URLs fetched by each load
function, and to invalidate
then compares its argument to the tracked URLs, and reruns any load
functions on active pages or layout components that match. And that’s it!
Posted on November 1, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.