Writing Your Own Preprocessors
This article references
svelte.preprocess
throughout but you may be more familiar with thepreprocess
option ofsvelte-loader
orrollup-plugin-svelte
. Thispreprocess
option callssvelte.preprocess
internally. The bundler plugin gives you easy access to it, so you don’t need to transform your components before compilation manually.
The Svelte compiler expects all components it receives to be valid Svelte syntax. To use compile-to-js or compile-to-css languages, you need to make sure that any non-standard syntax is transformed before Svelte tries to parse it. To enable this Svelte provides a preprocess
method allowing you to transform different parts of the component before it reaches the compiler.
With svelte.preprocess
you have a great deal of flexibility in how you write your components while ensuring that the Svelte compiler receives a plain component.
svelte.preprocess
Svelte’s preprocess
method expects an object or an array of objects with one or more of markup
, script
, and style
properties, each being a function receiving the source code as an argument. The preprocessors run in this order.
const preprocess = {
markup,
script,
style
};
In general, preprocessors receive the component source code and must return the transformed source code, either as a string or as an object containing a code
and map
property. The code
property must contain the transformed source code, while the map
property can optionally contain a sourcemap. The sourcemap is currently unused by Svelte.
How to make a pre-processor that makes it possible to use Pug/Jade
Pug is an alternative templating language that compiles to html, using whitespace instead of angle brackets:
const pug = require('pug');
const string = 'p Hello World!'; // pug template language
const html = pug.render(file);
console.log(html); // <p>Hello World!</p>
Our goal is to write Pug in our Svelte files instead of HTML where we can do all the scoped styling and JS goodness. Let’s create an app.svelte
file with this:
p Hello World!
<style>
p {
color: red;
}
</style>
We need to run it through Svelte to generate JavaScript, not HTML:
const svelte = require('svelte/compiler');
const fs = require('fs');
const file = fs.readFileSync('app.svelte', 'utf8');
const result = svelte.compile(file);
console.log(result.js.code); // svelte.compile returns both js and sourcemap
But this logs out the wrong thing!
/* generated by Svelte v3.23.0 */
import { SvelteComponent, detach, init, insert, noop, safe_not_equal, text } from 'svelte/internal';
function create_fragment(ctx) {
let t;
return {
c() {
t = text('p Hello World!'); // WRONG
},
m(target, anchor) {
insert(target, t, anchor);
},
p: noop,
i: noop,
o: noop,
d(detaching) {
if (detaching) detach(t);
}
};
}
class Component extends SvelteComponent {
constructor(options) {
super();
init(this, options, null, create_fragment, safe_not_equal, {});
}
}
export default Component;
To do this properly, we need to preprocess the template file. Let’s use svelte.preprocess
instead of svelte.compile
:
const result = svelte.preprocess(file, {
markup: ({ content }) => console.log(content)
});
Notice also that the markup
includes the style
tag as well. Some preprocessors might need that, but we don’t, so we can strip it out. Finally, we can run the stripped code through pug
:
const pug = require('pug');
const svelte = require('svelte/compiler');
const fs = require('fs');
const file = fs.readFileSync('app.svelte', 'utf8');
// https://github.com/sveltejs/svelte/blob/8fc85f0ef6b53ed85e54c129d79270fe577626dc/src/compiler/preprocess/index.ts#L97
const styleRegex = /<!--[^]*?-->|<style(s[^]*?)?>([^]*?)</style>/gi;
const scriptRegex = /<!--[^]*?-->|<script(s[^]*?)?>([^]*?)</script>/gi;
(async function () {
const result = await svelte.preprocess(file, {
// options
markup: ({ content }) => {
let code = content.replace(styleRegex, '').replace(scriptRegex, '');
code = pug.render(code);
// TODO: if still want CSS/JS, still have to append the style/script tags BACK onto `code` for svelte to pick them up
return { code };
}
});
console.log(result); // <p>Hello World!</p>
})();
This looks right. Our final task is to run this through svelte.compile
:
const res = svelte.compile(result.code);
console.log(res.js.code);
And this generates the correct result.
/* generated by Svelte v3.23.0 */
import {
SvelteComponent,
detach,
element,
init,
insert,
noop,
safe_not_equal
} from 'svelte/internal';
function create_fragment(ctx) {
let p;
return {
c() {
p = element('p');
p.textContent = 'Hello World!'; // correct!
},
m(target, anchor) {
insert(target, p, anchor);
},
p: noop,
i: noop,
o: noop,
d(detaching) {
if (detaching) detach(p);
}
};
}
class Component extends SvelteComponent {
constructor(options) {
super();
init(this, options, null, create_fragment, safe_not_equal, {});
}
}
export default Component;
The svelte.preprocess
API also offers a dependencies
array you can use to optimize for recompiling when files are changed.
Most of the time, you will be using a bundler plugin rather than using svelte.compile
and svelte.preprocess
directly. These plugins have a slightly different API, but you can still slot in your handwritten preprocessor code inline:
// example rollup.config.js
import * as fs from 'fs';
import svelte from 'rollup-plugin-svelte';
const pug = require('pug');
const styleRegex = /<!--[^]*?-->|<style(s[^]*?)?>([^]*?)</style>/gi;
const scriptRegex = /<!--[^]*?-->|<script(s[^]*?)?>([^]*?)</script>/gi;
export default {
input: 'src/main.js',
output: {
file: 'public/bundle.js',
format: 'iife'
},
plugins: [
svelte({
// etc
preprocess: {
markup: ({ content }) => {
let code = content.replace(styleRegex, '').replace(scriptRegex, '');
code = pug.render(code);
return { code };
}
}
})
]
};
If you want to use Pug and HTML markup interchangeably, you may wish to adopt the non-standard but community norm of adding a <template>
tag for non-HTML markup:
<template lang="pug">
p Hello World!
</template>
<style>
p {
color: red;
}
</style>
This guarantees no ambiguity when you have a Svelte codebase that is a mix of Pug and HTML templates. In order to process this you will have to add some checking and preprocessing:
const pug = require('pug');
const svelte = require('svelte/compiler');
const fs = require('fs');
const file = fs.readFileSync('app.svelte', 'utf8');
// https://github.com/sveltejs/svelte/blob/8fc85f0ef6b53ed85e54c129d79270fe577626dc/src/compiler/preprocess/index.ts#L97
const styleRegex = /<!--[^]*?-->|<style(s[^]*?)?>([^]*?)</style>/gi;
const scriptRegex = /<!--[^]*?-->|<script(s[^]*?)?>([^]*?)</script>/gi;
const markupPattern = new RegExp(`<template([\s\S]*?)(?:>([\s\S]*)<\/template>|/>)`);
(async function () {
const result = await svelte.preprocess(file, {
// options
markup: ({ content }) => {
// lifted from https://github.com/sveltejs/svelte-preprocess/blob/38b32b110b7e81c995da14dc813f002346b9e0af/src/autoProcess.ts#L179
const templateMatch = content.match(markupPattern);
if (!templateMatch) return { code: content };
const [fullMatch, attributesStr, templateCode] = templateMatch;
/** Transform an attribute string into a key-value object */
const attributes = attributesStr
.split(/s+/)
.filter(Boolean)
.reduce((acc, attr) => {
const [name, value] = attr.split('=');
// istanbul ignore next
acc[name] = value ? value.replace(/['"]/g, '') : true;
return acc;
}, {});
/** Transform the found template code */
let code = content.replace(styleRegex, '').replace(scriptRegex, '');
if (attributes.lang === 'pug') {
code = templateCode;
code = pug.render(code);
}
code =
content.slice(0, templateMatch.index) +
code +
content.slice(templateMatch.index + fullMatch.length);
return { code };
}
});
const res = svelte.compile(result.code);
console.log(res.js.code);
})();
This makes the preprocessor only work if <template lang="pug">
is used.