mirror of
https://github.com/Wolvan/poll.horse.git
synced 2025-02-19 19:14: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";
|
"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 {
|
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