Writing Your Own Preprocessors

This article references svelte.preprocess throughout but you may be more familiar with the preprocess option of svelte-loader or rollup-plugin-svelte. This preprocess option calls svelte.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:

<style>
  p {
    color: red;
  }
</style>

p Hello World!

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:

<style>
  p {
    color: red;
  }
</style>

<template lang="pug">p Hello World!</template>

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.