mirror of
https://github.com/Wolvan/poll.horse.git
synced 2025-02-16 09:44:22 +01:00
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:
parent
1031a4c36f
commit
8c3001042b
5 changed files with 140 additions and 2 deletions
13
frontend/errors/404.html
Normal file
13
frontend/errors/404.html
Normal 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
15
frontend/errors/500.html
Normal 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
12
frontend/html/index.html
Normal 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>
|
3
frontend/static/css/main.css
Normal file
3
frontend/static/css/main.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
h1 {
|
||||
color: red;
|
||||
}
|
|
@ -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"));
|
||||
}
|
Loading…
Reference in a new issue