How to make a svelte preprocessor
The other day I needed to do a bit of a weird thing in Svelte on this website, and I ended up making a small preprocessor plugin. It’s a fun little project, and I thought it’s worth sharing the details.
Before I start, an acknowledgement. I didn’t invent any of the code in here, I’m just trying to walk you through how this thing works. Matter of fact, I have lifted the o/g idea from Github comments somewhere.
What’s the problem?
I have all my posts in isolated markdown files, and I want them to know nothing of the application that renders them. It works fine by the most part, the mdsvex plugin imports and preprocesses the markdown files just fine. It’s actually pretty darn great at it.
The thing is though, it doesn’t really preprocess the front-matter metadata. And that’s a problem for me because I want to specify thumbnail images for my articles, and I want those images to be processed by the svelte/vite pipelines like everything else:
---
title: My article
thumbnail: ./local-image.jpg
...
---
I could in theory just use mdsvex for what it was designed for and just import the image in an embedded script like so:
---
title: My article
...
---
<script type="module">
import thumb from "./local-image.jpg"
metadata.thumbnail = thumb;
</script>
the post text
Call me pedantic, but, I don’t like the idea because it breaks the idea of keeping my markdown files agnostic from the application. Adn it’s likely to become a problem down the road when I decide to publish those articles somewhere else.
So, I wanted some magic thing that would pre-process those markdown files for me and make them compatible with the svelte/vite ecosystem without me touching the files. And so enter the svelte plugins ecosystem.
Svelte pre-processing plugins
When you look inside of your svelte.config.js
file you will see the
preprocessors
section:
const config = {
preprocessors: [
sveltePreprocess({ ... }),
vitePreprocess(),
mdsvex({ ... }),
// ....
]
}
What those plugins do is they, well, preprocess the source code files that are being imported into the application before they are finally imported and initialized.
Think of this as basically meta programming, it’s a way to modify the source
code and possibly generate more code before a module is fully initialised.
That’s what mdsvex
does in a nut shell, they turn the original markdown file
into basically a .svelte
file which then can be imported by the svelte/vite
ecosystem as a regular module.
How does it work?
It works quite simple on the inside, a preprocessor plugin is basically an async function that receives a file name and content, and returns modified version of the code and a source map of the changes.
const myPlugin = {
async markup({ content, filename }) {
// do your thing here
return {
code: newContentString,
map: theSourceMapOfChanges, // optional
};
},
};
Once you have that, you can just plug it into the preprocessors
list in the
svelte.conf.js
file and it will dutifully shovel everything that’s being
imported into the application through your function.
To mess with the original content, svelte also has some handy functions to parse the content into an AST and traverse the tree. Svelte is pretty dope like that.
import { parse, walk } from "svelte/compiler";
const myPlugin = {
async markup({ content, filename }) {
const ast = parse(content, { filename });
walk(ast.module, {
enter(node) {
// do your thing here
},
});
return {
code: newContentString,
map: theSourceMapOfChanges, // optional
};
},
};
You probably don’t want to edit the original content manually, though. And instead use something like the magic-string package to safely edit your codebase, and generate the source-map for your changes. Like so:
import { parse, walk } from "svelte/compiler";
import MagicString from "magic-string";
const myPlugin = {
async markup({ content, filename }) {
const ast = parse(content, { filename });
const s = new MagicString(content);
walk(ast.module, {
enter(node) {
// do your thing here
},
});
return {
code: s.toString(),
map: s.generateMap(),
};
},
};
An example
I have my little thumbnail preprocessing function plugged in after the mdsvex
does its thing. Meaning it sees an already processed markdown file that was
turned into a .svelte
file. And at the header of it will have a module
declaration string that looks like so:
<script type="module">
export const metadata = {
title: "My article",
description: "Blah blah blah",
thumbnail: "./local-image.jpg",
// ...
}
</script>
the rest of the content
What I want my plugin to do is to find that metadata
const declaration and
replace it with a svelte native image import; preferably with the imagetools
optimisation parameters.
To that end, I’m using the svelte’s built in AST parser and walker; and then I
use the magic-string
to safely patch the declaration.
walk(ast.module, {
enter(node) {
// finding the `metadata` const declaration
if (node.type === "VariableDeclarator" && node.id.name === "metadata") {
for (const property of node.init.properties) {
// finding the `thumbnail` property
if (property.key.value === "thumbnail") {
const importThumb =
`import THUMBNAIL from "${property.value.value}?w=1200&h=600&metadata";`;
// adding the native thumb import to the source code
s.appendLeft(ast.module.content.start, importThumb);
// replacing the o/g path with the new imported variable name
s.overwrite(property.value.start, property.value.end, "THUMBNAIL");
}
}
}
},
});
And that’s all there is to it. In the end this plugin will produce a .svelte
file that looks somewhat like this:
<script type="module">
import THUMBNAIL from "./local-image.jpg?w=1200&h=600&metadata";
export const metadata = {
title: "My article",
description: "Blah blah blah",
thumbnail: THUMBNAIL,
// ...
}
</script>
the rest of the content
And so, when the file is finally imported by svelte it will have the thumbnail
property on the metadata
export. And that thumbnail
will be a regular Svelte
image import. Which, you can then use normally anywhere in the app, for example
add it to the open graph meta tags like so:
<script type="ts">
export const post; // <- the imported .md post
$: thumb = post.thumbnail;
</script>
<svelte:head>
{#if thumb}
<meta property="og:image" content={thumb.src} />
<meta property="og:image:width" content={Math.round(thumb.width)} />
<meta property="og:image:height" content={Math.round(thumb.height)} />
{/if}
</svelte:head>
And that’s pretty much the whole story. Enjoy!