refactored...
This commit is contained in:
parent
187cc32c66
commit
a595aea282
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "flummpress",
|
"name": "flummpress",
|
||||||
"version": "1.0.2",
|
"version": "2.0.0",
|
||||||
"description": "Express für arme",
|
"description": "Express für arme",
|
||||||
"main": "src/index.mjs",
|
"main": "src/index.mjs",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|
182
src/index.mjs
182
src/index.mjs
|
@ -1,46 +1,178 @@
|
||||||
|
import path from "path";
|
||||||
|
import fs from "fs";
|
||||||
import http from "http";
|
import http from "http";
|
||||||
import url from "url";
|
import { URL } from "url";
|
||||||
import querystring from "querystring";
|
import querystring from "querystring";
|
||||||
|
|
||||||
import router from "./router.mjs";
|
import Router from "./router.mjs";
|
||||||
import views from "./views.mjs";
|
import Tpl from "./template.mjs";
|
||||||
|
|
||||||
|
export { Router, Tpl };
|
||||||
|
|
||||||
export default class flummpress {
|
export default class flummpress {
|
||||||
|
#mimes;
|
||||||
|
#server;
|
||||||
constructor() {
|
constructor() {
|
||||||
this.router = new router();
|
this.router = new Router();
|
||||||
this.views = new views();
|
this.tpl = new Tpl();
|
||||||
|
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
use(obj) {
|
||||||
|
if(obj instanceof Router) {
|
||||||
|
this.router.routes = new Map([ ...this.router.routes, ...obj.routes ]);
|
||||||
|
this.router.sortRoutes();
|
||||||
|
}
|
||||||
|
else if(obj instanceof Tpl) {
|
||||||
|
this.tpl = obj;
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
static({ dir = path.resolve() + "/public", route = /^\/public/ }) {
|
||||||
|
if(!this.#mimes) {
|
||||||
|
this.#mimes = new Map();
|
||||||
|
(fs.readFileSync("./mime.types", "utf-8"))
|
||||||
|
.split("\n")
|
||||||
|
.filter(e => !e.startsWith("#") && e)
|
||||||
|
.map(e => e.split(/\s{2,}/))
|
||||||
|
.filter(e => e.length > 1)
|
||||||
|
.forEach(m => m[1].split(" ").forEach(ext => this.#mimes.set(ext, m[0])));
|
||||||
}
|
}
|
||||||
|
|
||||||
use(item) {
|
this.router.get(route, (req, res) => {
|
||||||
switch(item.type) {
|
try {
|
||||||
case "route":
|
const filename = req.url.pathname.replace(route, "") || "index.html";
|
||||||
item.data.forEach((v, k) => this.router.routes.set(k, v));
|
const mime = this.#mimes.get(filename.split(".").pop());
|
||||||
break;
|
const file = path.join(dir, filename);
|
||||||
case "view":
|
let stat;
|
||||||
item.data.forEach((v, k) => this.views.set(k, v));
|
try {
|
||||||
break;
|
stat = fs.statSync(file);
|
||||||
|
} catch {
|
||||||
|
res.reply({
|
||||||
|
code: 404,
|
||||||
|
body: "404 - file not found."
|
||||||
|
});
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!mime.startsWith("video") && !mime.startsWith("audio")) {
|
||||||
|
res.reply({
|
||||||
|
type: this.#mimes.get(filename.split(".").pop()).toLowerCase(),
|
||||||
|
body: fs.readFileSync(path.join(dir, filename))
|
||||||
|
});
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(req.headers.range) {
|
||||||
|
const parts = req.headers.range.replace(/bytes=/, "").split("-");
|
||||||
|
const start = parseInt(parts[0], 10);
|
||||||
|
const end = parts[1] ? parseInt(parts[1], 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,
|
||||||
|
});
|
||||||
|
const stream = fs.createReadStream(file, { start: start, end: end })
|
||||||
|
.on("open", () => stream.pipe(res))
|
||||||
|
.on("error", err => res.end(err));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
res.writeHead(200, {
|
||||||
|
"Content-Length": stat.size,
|
||||||
|
"Content-Type": mime,
|
||||||
|
});
|
||||||
|
fs.createReadStream(file).pipe(res);
|
||||||
|
}
|
||||||
|
} catch(err) {
|
||||||
|
console.error(err);
|
||||||
|
res.reply({
|
||||||
|
code: 500,
|
||||||
|
body: "500 - f0ck hat keinen b0ck"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
listen(...args) {
|
listen(...args) {
|
||||||
this.router.routes.forEach((v, k) => console.log("route set", v.method, k));
|
this.#server = http.createServer(async (req, res) => {
|
||||||
return http.createServer(async (req, res, r) => {
|
const t_start = process.hrtime();
|
||||||
req.url = url.parse(req.url.replace(/(?!^.)(\/+)?%/, ''));
|
|
||||||
req.url.qs = querystring.parse(req.url.query);
|
|
||||||
|
|
||||||
req.post = new Promise((resolve, _, data = "") => req
|
const _url = new URL(req.url.replace(/(?!^.)(\/+)?$/, ''), "relative:///");
|
||||||
|
req.url = {
|
||||||
|
pathname: _url.pathname,
|
||||||
|
split: _url.pathname.split("/").slice(1),
|
||||||
|
searchParams: _url.searchParams,
|
||||||
|
qs: {...querystring.parse(_url.search.substring(1))} // legacy
|
||||||
|
};
|
||||||
|
|
||||||
|
const method = req.method.toLowerCase();
|
||||||
|
const route = this.router.getRoute(req.url.pathname, req.method);
|
||||||
|
|
||||||
|
if(route) { // 200
|
||||||
|
const cb = route[1][method];
|
||||||
|
const middleware = route[1][`${method}mw`];
|
||||||
|
req.params = req.url.pathname.match(new RegExp(route[0]))?.groups;
|
||||||
|
req.post = await this.readBody(req);
|
||||||
|
|
||||||
|
const result = await this.processMiddleware(middleware, req, this.createResponse(res));
|
||||||
|
if(result)
|
||||||
|
cb(req, res);
|
||||||
|
}
|
||||||
|
else { // 404
|
||||||
|
res
|
||||||
|
.writeHead(404)
|
||||||
|
.end(`404 - ${req.method} ${req.url.pathname}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log([
|
||||||
|
`[${(new Date()).toLocaleTimeString()}]`,
|
||||||
|
`${(process.hrtime(t_start)[1] / 1e6).toFixed(2)}ms`,
|
||||||
|
`${req.method} ${res.statusCode}`,
|
||||||
|
req.url.pathname
|
||||||
|
].map(e => e.toString().padEnd(15)).join(""));
|
||||||
|
|
||||||
|
}).listen(...args);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
readBody(req) {
|
||||||
|
return new Promise((resolve, _, data = "") => req
|
||||||
.on("data", d => void (data += d))
|
.on("data", d => void (data += d))
|
||||||
.on("end", () => void resolve(Object.fromEntries(Object.entries(querystring.parse(data)).map(([k, v]) => [k, decodeURIComponent(v)])))));
|
.on("end", () => {
|
||||||
|
if(req.headers['content-type'] === "application/json") {
|
||||||
|
try {
|
||||||
|
return void resolve(JSON.parse(data));
|
||||||
|
} catch(err) {}
|
||||||
|
}
|
||||||
|
void resolve(Object.fromEntries(Object.entries(querystring.parse(data)).map(([k, v]) => {
|
||||||
|
try {
|
||||||
|
return [k, decodeURIComponent(v)];
|
||||||
|
} catch(err) {
|
||||||
|
return [k, v];
|
||||||
|
}
|
||||||
|
})));
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
createResponse(res) {
|
||||||
res.reply = ({
|
res.reply = ({
|
||||||
code = 200,
|
code = 200,
|
||||||
type = "text/html",
|
type = "text/html",
|
||||||
body
|
body
|
||||||
}) => res.writeHead(code, { "Content-Type": `${type}; charset=utf-8` }).end(body);
|
}) => res.writeHead(code, { "Content-Type": `${type}; charset=utf-8` }).end(body);
|
||||||
|
res.json = msg => res.reply({ type: "application/json", body: msg });
|
||||||
!(r = this.router.routes.getRegex(req.url.pathname, req.method)) ? res.writeHead(404).end(`404 - ${req.url.pathname}`) : await r(req, res);
|
res.html = msg => res.reply({ type: "text/html", body: msg });
|
||||||
console.log(`[${(new Date()).toLocaleTimeString()}] ${res.statusCode} ${req.method}\t${req.url.pathname}`);
|
return res;
|
||||||
}).listen(...args);
|
};
|
||||||
}
|
|
||||||
|
processMiddleware(middleware, req, res) {
|
||||||
|
if(!middleware)
|
||||||
|
return new Promise(resolve => resolve(true));
|
||||||
|
return new Promise(resolve => middleware(req, res, () => resolve(true)));
|
||||||
|
};
|
||||||
|
|
||||||
};
|
};
|
||||||
export { router, views };
|
|
||||||
|
|
114
src/router.mjs
114
src/router.mjs
|
@ -1,58 +1,68 @@
|
||||||
import { promises as fs } from "fs";
|
|
||||||
import path from "path";
|
|
||||||
|
|
||||||
export default class Router {
|
export default class Router {
|
||||||
#mimes;
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.routes = new Map();
|
this.routes = new Map();
|
||||||
}
|
return this;
|
||||||
async loadRoutes(path) {
|
|
||||||
await Promise.all(
|
|
||||||
(await fs.readdir(path))
|
|
||||||
.filter(f => f.endsWith(".mjs"))
|
|
||||||
.map(async route => (await import(`${path}/${route}`)).default(this))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
route(method, args) {
|
|
||||||
//console.log("route set", method, args[0]);
|
|
||||||
return {
|
|
||||||
type: "route",
|
|
||||||
data: this.routes.set(args[0], { method: method, f: args[1] })
|
|
||||||
};
|
|
||||||
}
|
|
||||||
get() {
|
|
||||||
return this.route("GET", arguments);
|
|
||||||
}
|
|
||||||
post() {
|
|
||||||
return this.route("POST", arguments);
|
|
||||||
}
|
|
||||||
async static({ dir = path.resolve() + "/public", route = /^\/public/ }) {
|
|
||||||
if(!this.#mimes) {
|
|
||||||
this.#mimes = new Map();
|
|
||||||
(await fs.readFile("/etc/mime.types", "utf-8"))
|
|
||||||
.split("\n")
|
|
||||||
.filter(e => !e.startsWith("#") && e)
|
|
||||||
.map(e => e.split(/\s{2,}/))
|
|
||||||
.filter(e => e.length > 1)
|
|
||||||
.forEach(m => m[1].split(" ").forEach(ext => this.#mimes.set(ext, m[0])));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.get(route, async (req, res) => {
|
|
||||||
try {
|
|
||||||
return res.reply({
|
|
||||||
type: this.#mimes.get(req.url.path.split(".").pop()).toLowerCase(),
|
|
||||||
body: await fs.readFile(path.join(dir, req.url.path.replace(route, "")), "utf-8")
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
return res.reply({
|
|
||||||
code: 404,
|
|
||||||
body: "404 - file not found"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Map.prototype.getRegex = function(path, method) {
|
group(path, cb) {
|
||||||
return [...this.entries()].filter(r => r[0].exec(path) && r[1].method.includes(method))[0]?.[1].f;
|
const methods = {
|
||||||
|
get: this.get.bind(this),
|
||||||
|
post: this.post.bind(this),
|
||||||
|
};
|
||||||
|
const target = {
|
||||||
|
path: new RegExp(path),
|
||||||
|
};
|
||||||
|
const handler = {
|
||||||
|
get: (opt, method) => (p, ...args) => methods[method](
|
||||||
|
new RegExp([ opt.path, new RegExp(p === "/" ? "$": p) ]
|
||||||
|
.map(regex => regex.source)
|
||||||
|
.join("")
|
||||||
|
.replace(/(\\\/){1,}/g, "/")),
|
||||||
|
...args,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
cb(new Proxy(target, handler));
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
get(path, ...args) {
|
||||||
|
if(args.length === 1)
|
||||||
|
this.registerRoute(path, args[0], "get");
|
||||||
|
else
|
||||||
|
this.registerRoute(path, args[1], "get", args[0]);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
post(path, ...args) {
|
||||||
|
if(args.length === 1)
|
||||||
|
this.registerRoute(path, args[0], "post");
|
||||||
|
else
|
||||||
|
this.registerRoute(path, args[1], "post", args[0]);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
registerRoute(path, cb, method, middleware) {
|
||||||
|
if(!this.routes.has(path))
|
||||||
|
this.routes.set(path, {});
|
||||||
|
this.routes.set(path, {
|
||||||
|
...this.routes.get(path),
|
||||||
|
[method]: cb,
|
||||||
|
[method + "mw"]: middleware,
|
||||||
|
});
|
||||||
|
console.log("route set:", method.toUpperCase(), path);
|
||||||
|
this.sortRoutes();
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
getRoute(path, method) {
|
||||||
|
method = method.toLowerCase();
|
||||||
|
return [...this.routes.entries()].filter(r => {
|
||||||
|
return (r[0] === path || r[0].exec?.(path)) && r[1].hasOwnProperty(method);
|
||||||
|
})[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
sortRoutes() {
|
||||||
|
this.routes = new Map([...this.routes.entries()].sort().reverse());
|
||||||
|
return this;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
56
src/template.mjs
Normal file
56
src/template.mjs
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
export default class {
|
||||||
|
constructor() {
|
||||||
|
this.views = path.resolve() + "/views";
|
||||||
|
};
|
||||||
|
getTemplate(tpl) {
|
||||||
|
return fs.readFileSync(`${this.views}/${tpl}.html`, "utf-8");
|
||||||
|
}
|
||||||
|
render(tpl, data = {}) {
|
||||||
|
try {
|
||||||
|
const code = 'with(_data){const __html = [];' +
|
||||||
|
'__html.push(\`' +
|
||||||
|
tpl.replace(/[\t]/g, ' ')
|
||||||
|
.replace(/'(?=[^\{]*}})/, '\t')
|
||||||
|
.split('\`').join('\\\`')
|
||||||
|
.split('\t').join('\`')
|
||||||
|
.replace(/{{=(.+?)}}/g, '\`,$1,\`')
|
||||||
|
.replace(/{{-(.+?)}}/g, '\`,this.escape($1),\`')
|
||||||
|
.replace(/{{include (.*?)}}/g, (_, inc) => this.render(this.getTemplate(inc), data))
|
||||||
|
.replace(/{{each (?<key>.*) as (?<val>.*)}}/g, (_, key, val) => `\`);this.forEach(${key},(${val},key)=>{__html.push(\``)
|
||||||
|
.replace(/{{\/each}}/g, "\`);});__html.push(`")
|
||||||
|
.split('{{').join('\`);')
|
||||||
|
.split('}}').join('__html.push(\`')
|
||||||
|
+ '\`);return __html.join(\'\').replace(/\\n\\s*\\n/g, "\\n");}';
|
||||||
|
|
||||||
|
return (new Function("_data", code)).bind({
|
||||||
|
escape: this.escape,
|
||||||
|
forEach: this.forEach
|
||||||
|
})(data);
|
||||||
|
} catch(err) {
|
||||||
|
console.log(err);
|
||||||
|
return err.message;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
escape(str) {
|
||||||
|
return (str + '')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
.replace(/{/g, '{')
|
||||||
|
.replace(/}/g, '}')
|
||||||
|
;
|
||||||
|
};
|
||||||
|
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 a iterable object`);
|
||||||
|
};
|
||||||
|
};
|
|
@ -1,22 +0,0 @@
|
||||||
import { promises as fs } from "fs";
|
|
||||||
|
|
||||||
export default class ViewController {
|
|
||||||
#views;
|
|
||||||
constructor() {
|
|
||||||
this.#views = new Map();
|
|
||||||
}
|
|
||||||
get(name) {
|
|
||||||
return this.#views.get(name) || false;
|
|
||||||
}
|
|
||||||
set(name, view) {
|
|
||||||
return {
|
|
||||||
type: "view",
|
|
||||||
data: this.#views.set(name, view)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
async loadViews(path) {
|
|
||||||
return await Promise.all((await fs.readdir(path))
|
|
||||||
.filter(view => view.endsWith(".html"))
|
|
||||||
.map(async view => this.set(view.slice(0, -5), await fs.readFile(`${path}/${view}`))));
|
|
||||||
}
|
|
||||||
};
|
|
Loading…
Reference in New Issue
Block a user