From 0415507c486fe4442434d2e81eec74ab0819bebd Mon Sep 17 00:00:00 2001
From: Flummi <git@srv.fail>
Date: Sun, 16 Mar 2025 18:44:02 +0100
Subject: [PATCH] blah

---
 .gitignore       |   1 -
 dist/index.js    | 133 +++++++++++++++++++++++++++++++++++++
 dist/router.js   | 169 +++++++++++++++++++++++++++++++++++++++++++++++
 dist/template.js | 122 ++++++++++++++++++++++++++++++++++
 package.json     |   3 +-
 src/test.mjs     |  36 ++++++++++
 6 files changed, 461 insertions(+), 3 deletions(-)
 create mode 100644 dist/index.js
 create mode 100644 dist/router.js
 create mode 100644 dist/template.js
 create mode 100644 src/test.mjs

diff --git a/.gitignore b/.gitignore
index 16acd49..d5f19d8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,2 @@
 node_modules
-dist
 package-lock.json
diff --git a/dist/index.js b/dist/index.js
new file mode 100644
index 0000000..af3e77a
--- /dev/null
+++ b/dist/index.js
@@ -0,0 +1,133 @@
+var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
+    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
+    return new (P || (P = Promise))(function (resolve, reject) {
+        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
+        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
+        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
+        step((generator = generator.apply(thisArg, _arguments || [])).next());
+    });
+};
+import http from "node:http";
+import { URL } from "node:url";
+import querystring from "node:querystring";
+import Router from "./router.js";
+import Tpl from "./template.js";
+export { Router, Tpl };
+export default class Flummpress {
+    constructor() {
+        this.router = new Router();
+        this.tpl = new Tpl();
+        this.middleware = [];
+    }
+    use(plugin) {
+        if (plugin instanceof Router)
+            this.router.use(plugin);
+        else if (plugin instanceof Tpl)
+            this.tpl = plugin;
+        else if (typeof plugin === "function")
+            this.middleware.push(plugin);
+        return this;
+    }
+    processPipeline(handlers, req, res) {
+        return __awaiter(this, void 0, void 0, function* () {
+            for (const handler of handlers) {
+                if (typeof handler !== "function")
+                    throw new TypeError(`Handler is not a function: ${handler}`);
+                let nextCalled = false;
+                yield handler(req, res, () => nextCalled = true);
+                if (!nextCalled || res.writableEnded)
+                    return;
+            }
+        });
+    }
+    listen(...args) {
+        this.server = http.createServer((request, response) => __awaiter(this, void 0, void 0, function* () {
+            var _a, _b;
+            const req = this.parseRequest(request);
+            const res = this.createResponse(response);
+            const start = process.hrtime();
+            try {
+                yield this.processPipeline(this.middleware, req, res);
+                const route = this.router.getRoute(req.url.pathname, req.method);
+                if (route) {
+                    const [pathPattern, methods] = route;
+                    const handler = methods[(_a = req.method) === null || _a === void 0 ? void 0 : _a.toLowerCase()];
+                    req.params = ((_b = req.url.pathname.match(new RegExp(pathPattern))) === null || _b === void 0 ? void 0 : _b.groups) || {};
+                    req.post = yield this.readBody(req);
+                    yield this.processPipeline(handler, req, res);
+                }
+                else {
+                    res.writeHead(404).end("404 - Not Found");
+                }
+            }
+            catch (err) {
+                console.error(err);
+                res.writeHead(500).end("500 - Internal Server Error");
+            }
+            console.log([
+                `[${new Date().toISOString()}]`,
+                `${(process.hrtime(start)[1] / 1e6).toFixed(2)}ms`,
+                `${req.method} ${res.statusCode}`,
+                req.url.pathname,
+            ].join(" | "));
+        })).listen(...args);
+        return this;
+    }
+    parseRequest(request) {
+        const url = new URL(request.url.replace(/(?!^.)(\/+)?$/, ""), "http://localhost");
+        const req = request;
+        req.url = {
+            pathname: url.pathname,
+            split: url.pathname.split("/").slice(1),
+            searchParams: url.searchParams,
+            qs: Object.fromEntries(url.searchParams.entries()),
+        };
+        req.cookies = {};
+        if (req.headers.cookie) {
+            req.headers.cookie.split("; ").forEach(cookie => {
+                const [key, value] = cookie.split("=");
+                req.cookies[key] = decodeURIComponent(value);
+            });
+        }
+        return req;
+    }
+    readBody(req) {
+        return __awaiter(this, void 0, void 0, function* () {
+            return new Promise((resolve, reject) => {
+                let body = "";
+                req.on("data", (chunk) => {
+                    body += chunk;
+                });
+                req.on("end", () => {
+                    try {
+                        resolve(req.headers["content-type"] === "application/json"
+                            ? JSON.parse(body)
+                            : querystring.parse(body));
+                    }
+                    catch (err) {
+                        reject(err);
+                    }
+                });
+                req.on("error", reject);
+            });
+        });
+    }
+    createResponse(response) {
+        const res = response;
+        res.reply = ({ code = 200, type = "text/html", body }) => {
+            res.writeHead(code, { "Content-Type": `${type}; charset=utf-8` });
+            res.end(body);
+        };
+        res.json = (body, code = 200) => {
+            res.reply({ code, type: "application/json", body: JSON.stringify(body) });
+        };
+        res.html = (body, code = 200) => {
+            res.reply({ code, type: "text/html", body });
+        };
+        res.redirect = (target, code = 302) => {
+            res.writeHead(code, { Location: target });
+            res.end();
+        };
+        return res;
+    }
+}
diff --git a/dist/router.js b/dist/router.js
new file mode 100644
index 0000000..d02ff58
--- /dev/null
+++ b/dist/router.js
@@ -0,0 +1,169 @@
+var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
+    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
+    return new (P || (P = Promise))(function (resolve, reject) {
+        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
+        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
+        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
+        step((generator = generator.apply(thisArg, _arguments || [])).next());
+    });
+};
+import fs from "node:fs";
+import path from "node:path";
+import Tpl from "./template.js";
+export default class Router {
+    constructor() {
+        this.routes = new Map();
+        this.mimes = new Map();
+    }
+    importRoutesFromPath(p_1) {
+        return __awaiter(this, arguments, void 0, function* (p, tpl = false) {
+            const dirEntries = yield fs.promises.readdir(path.resolve() + '/' + p, { withFileTypes: true });
+            for (const tmp of dirEntries) {
+                if (tmp.isFile() && (tmp.name.endsWith(".mjs") || tmp.name.endsWith(".js"))) {
+                    const routeModule = (yield import(`${path.resolve()}/${p}/${tmp.name}`)).default;
+                    this.use(routeModule(this, tpl));
+                }
+                else if (tmp.isDirectory()) {
+                    yield this.importRoutesFromPath(p + '/' + tmp.name);
+                }
+            }
+            return this;
+        });
+    }
+    group(basePath, callback) {
+        const self = this;
+        const methods = {
+            get(path, ...handlers) {
+                const fullPath = self.combinePaths(basePath, path);
+                return self.registerRoute(fullPath, "get", handlers);
+            },
+            post(path, ...handlers) {
+                const fullPath = self.combinePaths(basePath, path);
+                return self.registerRoute(fullPath, "post", handlers);
+            },
+            put(path, ...handlers) {
+                const fullPath = self.combinePaths(basePath, path);
+                return self.registerRoute(fullPath, "put", handlers);
+            },
+            delete(path, ...handlers) {
+                const fullPath = self.combinePaths(basePath, path);
+                return self.registerRoute(fullPath, "delete", handlers);
+            },
+            patch(path, ...handlers) {
+                const fullPath = self.combinePaths(basePath, path);
+                return self.registerRoute(fullPath, "patch", handlers);
+            }
+        };
+        callback(methods);
+        return this;
+    }
+    combinePaths(basePath, subPath) {
+        if (typeof basePath === "string" && typeof subPath === "string")
+            return `${basePath.replace(/\/$/, "")}/${subPath.replace(/^\//, "")}`;
+        if (basePath instanceof RegExp && typeof subPath === "string")
+            return new RegExp(`${basePath.source}${subPath.replace(/^\//, "")}`);
+        if (typeof basePath === "string" && subPath instanceof RegExp)
+            return new RegExp(`${basePath.replace(/\/$/, "")}${subPath.source}`);
+        if (basePath instanceof RegExp && subPath instanceof RegExp)
+            return new RegExp(`${basePath.source}${subPath.source}`);
+        throw new TypeError("Invalid path types. Both basePath and subPath must be either string or RegExp.");
+    }
+    use(obj) {
+        if (obj instanceof Router) {
+            this.routes = new Map([...this.routes, ...obj.routes]);
+            this.sortRoutes();
+        }
+        if (obj instanceof Tpl) {
+            this.tpl = obj;
+        }
+    }
+    get(path, cb) {
+        this.registerRoute(path, "get", cb);
+        return this;
+    }
+    post(path, cb) {
+        this.registerRoute(path, "post", cb);
+        return this;
+    }
+    head(path, cb) {
+        this.registerRoute(path, "head", cb);
+        return this;
+    }
+    put(path, cb) {
+        this.registerRoute(path, "put", cb);
+        return this;
+    }
+    delete(path, cb) {
+        this.registerRoute(path, "delete", cb);
+        return this;
+    }
+    patch(path, cb) {
+        this.registerRoute(path, "patch", cb);
+        return this;
+    }
+    registerRoute(path, method, handlers) {
+        if (!this.routes.has(path)) {
+            this.routes.set(path, {});
+        }
+        this.routes.get(path)[method] = handlers.flat();
+        console.log("route set:", method.toUpperCase(), path);
+        this.sortRoutes();
+        return this;
+    }
+    getRoute(path, method) {
+        method = method.toLowerCase();
+        return [...this.routes.entries()].find(r => {
+            var _a, _b;
+            return (typeof r[0] === "string" ? r[0] === path : (_b = (_a = r[0]).exec) === null || _b === void 0 ? void 0 : _b.call(_a, path)) && r[1][method];
+        });
+    }
+    sortRoutes() {
+        this.routes = new Map([...this.routes.entries()].sort().reverse());
+        return this;
+    }
+    readMimes(file = "/etc/mime.types") {
+        fs.readFileSync(file, "utf-8")
+            .split("\n")
+            .filter(line => !line.startsWith("#") && line)
+            .forEach(line => {
+            const [mimeType, extensions] = line.split(/\s+/);
+            extensions === null || extensions === void 0 ? void 0 : extensions.split(" ").forEach(ext => this.mimes.set(ext, mimeType));
+        });
+    }
+    static({ dir = path.resolve() + "/public", route = /^\/public/ }) {
+        if (!this.mimes.size)
+            this.readMimes();
+        this.get(route, [(req, res) => {
+                try {
+                    const filename = req.url.pathname.replace(route, "") || "index.html";
+                    const mime = this.mimes.get(filename.split(".").pop() || "");
+                    const file = path.join(dir, filename);
+                    const stat = fs.statSync(file);
+                    if (req.headers.range) {
+                        const [startStr, endStr] = req.headers.range.replace(/bytes=/, "").split("-");
+                        const start = parseInt(startStr, 10);
+                        const end = endStr ? parseInt(endStr, 10) : stat.size - 1;
+                        res.writeHead(206, {
+                            "Content-Range": `bytes ${start}-${end}/${stat.size}`,
+                            "Accept-Ranges": "bytes",
+                            "Content-Length": end - start + 1,
+                            "Content-Type": mime,
+                        });
+                        fs.createReadStream(file, { start, end }).pipe(res);
+                    }
+                    else {
+                        res.writeHead(200, {
+                            "Content-Length": stat.size,
+                            "Content-Type": mime,
+                        });
+                        fs.createReadStream(file).pipe(res);
+                    }
+                }
+                catch (err) {
+                    console.error(err);
+                    res.reply({ code: 404, body: "404 - File not found" });
+                }
+            }]);
+        return this;
+    }
+}
diff --git a/dist/template.js b/dist/template.js
new file mode 100644
index 0000000..0f2a632
--- /dev/null
+++ b/dist/template.js
@@ -0,0 +1,122 @@
+import fs from "node:fs";
+import path from "node:path";
+export default class Template {
+    constructor() {
+        this.views = "./views";
+        this.globals = {};
+        this.debug = false;
+        this.cache = true;
+        this.templates = new Map();
+    }
+    setDebug(debug) {
+        this.debug = debug;
+    }
+    setViews(views) {
+        this.views = views;
+        this.readdir(views);
+    }
+    setGlobals(globals) {
+        this.globals = globals;
+    }
+    setCache(cache) {
+        this.cache = cache;
+    }
+    readdir(dir, root = dir) {
+        for (const d of fs.readdirSync(`${path.resolve()}/${dir}`)) {
+            if (d.endsWith(".html")) {
+                const file = path.parse(`${dir.replace(this.views, "")}/${d}`);
+                const t_dir = file.dir.substring(1);
+                const t_file = file.name;
+                this.getTemplate(!t_dir ? t_file : `${t_dir}/${t_file}`);
+            }
+            else {
+                this.readdir(`${dir}/${d}`, root);
+            }
+        }
+    }
+    getTemplate(tpl) {
+        let template;
+        let cache = false;
+        if (this.cache && this.templates.has(tpl)) {
+            template = this.templates.get(tpl);
+            cache = true;
+        }
+        else {
+            template = {
+                code: fs.readFileSync(`${this.views}/${tpl}.html`, "utf-8"),
+                cached: new Date(),
+            };
+            this.templates.set(tpl, template);
+        }
+        return [
+            template.code,
+            this.debug ? `<!-- ${tpl}.html ${cache ? `cached ${template.cached}` : "not cached"} -->` : "",
+        ].join("");
+    }
+    render(file, data = {}, locals = {}) {
+        data = Object.assign(Object.assign(Object.assign({}, data), locals), this.globals);
+        try {
+            const code = 'with(_data){const __html = [];' +
+                '__html.push(`' +
+                this.getTemplate(file)
+                    .replace(/[\t]/g, " ")
+                    .split("`")
+                    .join("\\`")
+                    .replace(/{{--(.*?)--}}/g, "")
+                    .replace(/{{(.+?)}}/g, "`, $1, `")
+                    .replace(/{!!(.+?)!!}/g, "`, this.escape($1), `")
+                    .replace(/@js/g, "`);")
+                    .replace(/@endjs/g, ";__html.push(`")
+                    .replace(/@mtime\((.*?)\)/g, "`);__html.push(this.getMtime('$1'));__html.push(`")
+                    .replace(/@include\((.*?)\)/g, (_, inc) => this.render(inc, data))
+                    .replace(/@for\((.*?)\)$/gm, "`); for($1) { __html.push(`")
+                    .replace(/@endfor/g, "`); } __html.push(`")
+                    .replace(/@each\((.*?) as (.*?)\)/g, "`); this.forEach($1, ($2, key) => { __html.push(`")
+                    .replace(/@endeach/g, "`); }); __html.push(`")
+                    .replace(/@elseif\((.*?)\)(\)?)/g, "`); } else if ($1$2) { __html.push(`")
+                    .replace(/@if\((.*?)\)(\)?)/g, "`); if ($1$2) { __html.push(`")
+                    .replace(/@else/g, "`); } else { __html.push(`")
+                    .replace(/@endif/g, "`); } __html.push(`") +
+                "`); return __html.join('').replace(/\\n\\s*\\n/g, '\\n'); }";
+            return new Function("_data", code).bind({
+                escape: this.escape,
+                forEach: this.forEach,
+                getMtime: this.getMtime,
+            })(data);
+        }
+        catch (err) {
+            console.log(file, err.message);
+            return this.debug ? `${err.message} in ${file}` : "";
+        }
+    }
+    escape(str) {
+        return (str + "")
+            .replace(/&/g, "&amp;")
+            .replace(/</g, "&lt;")
+            .replace(/>/g, "&gt;")
+            .replace(/"/g, "&quot;")
+            .replace(/'/g, "&#39;")
+            .replace(/{/g, "&#123;")
+            .replace(/}/g, "&#125;");
+    }
+    forEach(o, f) {
+        if (Array.isArray(o)) {
+            o.forEach(f);
+        }
+        else if (typeof o === "object") {
+            Object.keys(o).forEach((k) => f.call(null, o[k], k));
+        }
+        else {
+            throw new Error(`${o} is not an iterable object`);
+        }
+    }
+    getMtime(file) {
+        try {
+            return +`${fs.statSync(path.normalize(process.cwd() + file)).mtimeMs}`.split(".")[0];
+        }
+        catch (err) {
+            console.log(err);
+            return 0;
+        }
+    }
+}
diff --git a/package.json b/package.json
index 7d23b2d..5b9f8cd 100644
--- a/package.json
+++ b/package.json
@@ -2,10 +2,9 @@
   "name": "flummpress",
   "version": "3.0.0",
   "description": "Express für arme",
-  "main": "index.mjs",
+  "main": "./dist/index.js",
   "scripts": {
     "build": "tsc",
-    "start": "node ./dist/index.js",
     "doc": "typedoc --out docs src"
   },
   "keywords": [],
diff --git a/src/test.mjs b/src/test.mjs
new file mode 100644
index 0000000..390f697
--- /dev/null
+++ b/src/test.mjs
@@ -0,0 +1,36 @@
+import flummpress from '../dist/index.js';
+
+process.on('unhandledRejection', err => {
+  console.error(err);
+  throw err;
+});
+
+
+const app = new flummpress();
+
+const loggedin = async (req, res, next) => {
+  console.log("Logged in");
+  await next();
+};
+
+app.router.group(/^\/api\/v2\/admin\/(?<postid>\d+)\/tags/, group => {
+  group.get(/$/,
+    (req, res, next) => { // middleware davor
+      console.log("Logged in");
+      next();
+    },
+    (req, res, next) => { // eigentlicher request
+      res.reply({
+        body: JSON.stringify(req.params)
+      });
+      next();
+    },
+    (req, res) => { // middleware danach
+      console.log("Logged out");
+    }
+  );
+});
+
+app.listen(3000);
+
+console.log(app.router);