To Build a Website

TL;DR: I ended up creating my own implementation of EJS. The surprisingly short code can be found at the bottom of the article.

When I started building this iteration of this website, it would have been very easy to use all of the existing tools that people have made over the years to make building a website easier. But clearly I am not that sane type of person, and instead decided that I would micromanage all parts of the website.

I started with trying to decide on a templating engine, as even I'm not insane enough to write raw HTML. Strike that, writing HTML is actually really simple, maintaining and updating it is just a nightmare. For example, imagine having 100 or so blog posts deciding to update the footer. And having to update that footer. On. Every. Single. Page. ... Well that's just insanity.

Sorry, you don't need to hear this pointless rant.

So I settled on EJS. It is nice in that if I feel like getting fancy, I could use custom javascript to add whatever I wanted without having to fight a more "full featured" template system that uses safe filters and things like that. I prefer to live on the wild side, where I can happily shoot myself in the foot and not realize it for days.

My goal was to use EJS to include all of the used files, including the javascript and css, to inline and minify everything to make the site load as fast as possible. Basically like a poor man's HTTP2 with server push.

Unfortunately, EJS does not support asynchronous templates. This was a huge sticking point for me. Honestly I couldn't figure out why it couldn't.

I mean, await can (rightly or wrongly) be safely slapped in front of any expression, and it will resolve it if it is a promise, or just return the expression again. So making it support a sort of poor-man's async isn't all too hard. Of course using it well is still another trick, since kicking off the async stuff in parallel can give some super easy performance wins, but hey.

I'm most definitely not saying that it's anything to be proud of. And it's missing a lot of super useful things like proper error messages and stack traces when there's a problem. But the base functionality is actually impressively simple. It's only about 40 lines of Typescript. See for yourself:

// Note that `AsyncFunction` is not a global object.
// It could be obtained by evaluating the following code.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncFunction
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;

function escape(s: string): string {
    return s
        .replace(/\\/g, "\\\\")
        .replace(/\$/g, "\\$")
        .replace(/`/g, "\\`");
}

export function createTemplate<T>(tmpl: string): (data?: T) => Promise<string> {
    const body = ["let $=[];with(data){"];

    tmpl.split("<%")
        .forEach((s, i) => {
            if (i === 0) {
                body.push("$.push(`", escape(s), "`)");
                return;
            }

            const [h, tail] = s.split("%>", 2);
            const head = h.trim();

            if (head.startsWith("=")) {
                body.push(";$.push(await ", head.slice(1), ")");
            } else if (head.startsWith(")")) {
                body.push(head);
            } else {
                body.push(";", head);
            }

            if (!head.endsWith("(")) {
                body.push(";$.push");
            }
            body.push("(`", escape(tail), "`)");
        });

    body.push(";return $.join(``);}");
    return new AsyncFunction("data", body.join(""));
}