From 8c3001042b7ee916387784faf8236281cadafd59 Mon Sep 17 00:00:00 2001 From: Wolvan Date: Tue, 28 Dec 2021 23:49:18 +0100 Subject: [PATCH] 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. --- frontend/errors/404.html | 13 +++++ frontend/errors/500.html | 15 ++++++ frontend/html/index.html | 12 +++++ frontend/static/css/main.css | 3 ++ src/frontend.ts | 99 +++++++++++++++++++++++++++++++++++- 5 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 frontend/errors/404.html create mode 100644 frontend/errors/500.html create mode 100644 frontend/html/index.html create mode 100644 frontend/static/css/main.css diff --git a/frontend/errors/404.html b/frontend/errors/404.html new file mode 100644 index 0000000..a2a4065 --- /dev/null +++ b/frontend/errors/404.html @@ -0,0 +1,13 @@ + + + + + + {{ TITLE }} + + + +

Page not found

+

Server Error 404

+ + diff --git a/frontend/errors/500.html b/frontend/errors/500.html new file mode 100644 index 0000000..7cbf0ca --- /dev/null +++ b/frontend/errors/500.html @@ -0,0 +1,15 @@ + + + + + + {{ TITLE }} + + + +

Oops! Something went wrong

+

Server Error {{ HTTP_ERROR_CODE }}

+

An error occured while processing your request. Please send the following code to the {{ DEVELOPER_CONTACT_INFO }}:

+ {{ JS_ERROR_STACK }} + + diff --git a/frontend/html/index.html b/frontend/html/index.html new file mode 100644 index 0000000..418708b --- /dev/null +++ b/frontend/html/index.html @@ -0,0 +1,12 @@ + + + + + + {{ TITLE }} + + + +

Welcome to poll.horse!

+ + \ No newline at end of file diff --git a/frontend/static/css/main.css b/frontend/static/css/main.css new file mode 100644 index 0000000..4717ad4 --- /dev/null +++ b/frontend/static/css/main.css @@ -0,0 +1,3 @@ +h1 { + color: red; +} \ No newline at end of file diff --git a/src/frontend.ts b/src/frontend.ts index 50f3209..33a32c6 100644 --- a/src/frontend.ts +++ b/src/frontend.ts @@ -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(/\{\{.*?(? 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")); } \ No newline at end of file