Build frontend loader system

A custom server-side renderer is used to deliver pages to the client
with values defined on load. This makes templating easier.
This commit is contained in:
Wolvan 2021-12-28 23:49:18 +01:00
parent 1031a4c36f
commit 8c3001042b
5 changed files with 140 additions and 2 deletions

13
frontend/errors/404.html Normal file
View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ TITLE }}</title>
<link rel="stylesheet" href="/static/main.css">
</head>
<body>
<h1>Page not found</h1>
<h2>Server Error 404</h2>
</body>
</html>

15
frontend/errors/500.html Normal file
View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ TITLE }}</title>
<link rel="stylesheet" href="/static/css/main.css">
</head>
<body>
<h1>Oops! Something went wrong</h1>
<h2>Server Error {{ HTTP_ERROR_CODE }}</h2>
<p>An error occured while processing your request. Please send the following code to the {{ DEVELOPER_CONTACT_INFO }}:</p>
<code>{{ JS_ERROR_STACK }}</code>
</body>
</html>

12
frontend/html/index.html Normal file
View file

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ TITLE }}</title>
<link rel="stylesheet" href="/static/css/main.css">
</head>
<body>
<h1>Welcome to poll.horse!</h1>
</body>
</html>

View file

@ -0,0 +1,3 @@
h1 {
color: red;
}

View file

@ -1,6 +1,101 @@
"use strict";
import { Router } from "express";
import fs from "fs-extra";
import { resolve } from "path";
import { Transform as TransformStream, Stream } from "stream";
import { Router, Request, Response } from "express";
const RenderBuffer = new WeakMap();
const RenderReplacements = new WeakMap();
interface NodeError extends Error {
code?: string;
}
// TODO: Implement conditional transform
class RenderTransform extends TransformStream {
constructor(replacements = {}) {
super();
RenderReplacements.set(this, replacements);
RenderBuffer.set(this, "");
}
_transform(chunk: any, encoding: string, callback: () => void) {
const r = RenderReplacements.get(this);
let c = RenderBuffer.get(this) + chunk.toString();
Object.entries(r).forEach(([key, value]) => c = c.replace(new RegExp("{{ ?" + key + " ?}}", "ig"), value));
if (c.match(/\{\{.*?(?<!\}\})$/)) RenderBuffer.set(this, c);
else {
RenderBuffer.set(this, "");
this.push(c.replace(/\\\{/g, "{").replace(/\\\}/g, "}"));
}
callback();
}
_flush(callback: () => void) {
const c = RenderBuffer.get(this);
if (c) this.push(c.replace(/\\\{/g, "{").replace(/\\\}/g, "}"));
callback();
}
}
class MinificationTransform extends RenderTransform {
constructor(replacements = {}) {
super(replacements);
}
_transform(chunk: any, encoding: string, callback: () => void) {
super._transform(chunk.toString().replace(/\s{2,}/g, ""), encoding, callback);
}
}
const defaultReplacements = {
"TITLE": "Poll Horse",
"DEVELOPER_CONTACT_INFO": "developer@poll.horse"
};
class Defaults2RenderTransform extends MinificationTransform {
constructor(replacements = {}) {
super(Object.assign({}, defaultReplacements, replacements));
}
}
async function displayPage(req: Request, res: Response, htmlFilename: string, replacements = {}, statusCode = 200) {
const promisifyStream = (fn: Stream) => new Promise(pRes => fn.on("finish", pRes));
try {
await fs.stat(resolve(__dirname, "../frontend/html", htmlFilename));
await promisifyStream(
fs.createReadStream(resolve(__dirname, "../frontend/html", htmlFilename))
.pipe(new Defaults2RenderTransform(replacements))
.pipe(res.status(statusCode))
);
} catch (error) {
if (error instanceof Error)
if ((error as NodeError).code === "ENOENT") {
await promisifyStream(
fs.createReadStream(resolve(__dirname, "../frontend/errors/404.html"))
.pipe(new Defaults2RenderTransform())
.pipe(res.status(404))
);
} else {
await promisifyStream(
fs.createReadStream(resolve(__dirname, "../frontend/errors/500.html")).pipe(new Defaults2RenderTransform({
"JS_ERROR_STACK": (error as NodeError).stack,
"HTTP_ERROR_CODE": 500
}))
.pipe(res.status(500))
);
}
}
}
export default function init(router: Router): void {
router.get("/:id/r", async (req, res) => {
const id = req.params.id;
res.redirect(`/`);
});
router.get("/:id", async (req, res) => {
const id = req.params.id;
res.redirect(`/`);
});
router.get("/", (req, res) => displayPage(req, res, "index.html"));
}