refactored...

This commit is contained in:
Flummi 2021-06-06 15:57:56 +02:00
parent 187cc32c66
commit a595aea282
6 changed files with 344 additions and 168 deletions

View File

@ -1,42 +1,42 @@
# flummpress # flummpress
## Usage Example ## Usage Example
```javascript ```javascript
import path from "path"; import path from "path";
import flummpress, { router, views } from "flummpress"; import flummpress, { router, views } from "flummpress";
(async () => { (async () => {
const port = 8080; const port = 8080;
(await new flummpress()) (await new flummpress())
.listen(port) .listen(port)
.on("listening", () => { .on("listening", () => {
console.log(`flummpress is listening on port ${port}`); console.log(`flummpress is listening on port ${port}`);
// new route GET // new route GET
router.get(/^\/$/, (req, res) => { router.get(/^\/$/, (req, res) => {
res.reply({ res.reply({
body: "hello world!" body: "hello world!"
}); });
}); });
// new route POST // new route POST
router.post(/^\/$/, async (req, res) => { router.post(/^\/$/, async (req, res) => {
const postdata = await req.post; const postdata = await req.post;
console.log(postdata); console.log(postdata);
res.reply({ res.reply({
body: "hello post!" body: "hello post!"
}); });
}); });
// public folder // public folder
router.static({ router.static({
dir: path.resolve() + "/public", dir: path.resolve() + "/public",
route: /^\/public/ route: /^\/public/
}); });
}); });
})(); })();
``` ```
## documentation ## documentation
coming soon coming soon

View File

@ -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": {

View File

@ -1,46 +1,178 @@
import http from "http"; import path from "path";
import url from "url"; import fs from "fs";
import querystring from "querystring"; import http from "http";
import { URL } from "url";
import router from "./router.mjs"; import querystring from "querystring";
import views from "./views.mjs";
import Router from "./router.mjs";
export default class flummpress { import Tpl from "./template.mjs";
constructor() {
this.router = new router(); export { Router, Tpl };
this.views = new views();
} export default class flummpress {
#mimes;
use(item) { #server;
switch(item.type) { constructor() {
case "route": this.router = new Router();
item.data.forEach((v, k) => this.router.routes.set(k, v)); this.tpl = new Tpl();
break;
case "view": return this;
item.data.forEach((v, k) => this.views.set(k, v)); };
break;
} use(obj) {
} if(obj instanceof Router) {
this.router.routes = new Map([ ...this.router.routes, ...obj.routes ]);
listen(...args) { this.router.sortRoutes();
this.router.routes.forEach((v, k) => console.log("route set", v.method, k)); }
return http.createServer(async (req, res, r) => { else if(obj instanceof Tpl) {
req.url = url.parse(req.url.replace(/(?!^.)(\/+)?%/, '')); this.tpl = obj;
req.url.qs = querystring.parse(req.url.query); }
return this;
req.post = new Promise((resolve, _, data = "") => req };
.on("data", d => void (data += d))
.on("end", () => void resolve(Object.fromEntries(Object.entries(querystring.parse(data)).map(([k, v]) => [k, decodeURIComponent(v)]))))); static({ dir = path.resolve() + "/public", route = /^\/public/ }) {
if(!this.#mimes) {
res.reply = ({ this.#mimes = new Map();
code = 200, (fs.readFileSync("./mime.types", "utf-8"))
type = "text/html", .split("\n")
body .filter(e => !e.startsWith("#") && e)
}) => res.writeHead(code, { "Content-Type": `${type}; charset=utf-8` }).end(body); .map(e => e.split(/\s{2,}/))
.filter(e => e.length > 1)
!(r = this.router.routes.getRegex(req.url.pathname, req.method)) ? res.writeHead(404).end(`404 - ${req.url.pathname}`) : await r(req, res); .forEach(m => m[1].split(" ").forEach(ext => this.#mimes.set(ext, m[0])));
console.log(`[${(new Date()).toLocaleTimeString()}] ${res.statusCode} ${req.method}\t${req.url.pathname}`); }
}).listen(...args);
} this.router.get(route, (req, res) => {
}; try {
export { router, views }; const filename = req.url.pathname.replace(route, "") || "index.html";
const mime = this.#mimes.get(filename.split(".").pop());
const file = path.join(dir, filename);
let stat;
try {
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) {
this.#server = http.createServer(async (req, res) => {
const t_start = process.hrtime();
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("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 = ({
code = 200,
type = "text/html",
body
}) => res.writeHead(code, { "Content-Type": `${type}; charset=utf-8` }).end(body);
res.json = msg => res.reply({ type: "application/json", body: msg });
res.html = msg => res.reply({ type: "text/html", body: msg });
return res;
};
processMiddleware(middleware, req, res) {
if(!middleware)
return new Promise(resolve => resolve(true));
return new Promise(resolve => middleware(req, res, () => resolve(true)));
};
};

View File

@ -1,58 +1,68 @@
import { promises as fs } from "fs"; export default class Router {
import path from "path"; constructor() {
this.routes = new Map();
export default class Router { return this;
#mimes; };
constructor() {
this.routes = new Map(); group(path, cb) {
} const methods = {
async loadRoutes(path) { get: this.get.bind(this),
await Promise.all( post: this.post.bind(this),
(await fs.readdir(path)) };
.filter(f => f.endsWith(".mjs")) const target = {
.map(async route => (await import(`${path}/${route}`)).default(this)) path: new RegExp(path),
); };
} const handler = {
route(method, args) { get: (opt, method) => (p, ...args) => methods[method](
//console.log("route set", method, args[0]); new RegExp([ opt.path, new RegExp(p === "/" ? "$": p) ]
return { .map(regex => regex.source)
type: "route", .join("")
data: this.routes.set(args[0], { method: method, f: args[1] }) .replace(/(\\\/){1,}/g, "/")),
}; ...args,
} )
get() { };
return this.route("GET", arguments); cb(new Proxy(target, handler));
} return this;
post() { };
return this.route("POST", arguments);
} get(path, ...args) {
async static({ dir = path.resolve() + "/public", route = /^\/public/ }) { if(args.length === 1)
if(!this.#mimes) { this.registerRoute(path, args[0], "get");
this.#mimes = new Map(); else
(await fs.readFile("/etc/mime.types", "utf-8")) this.registerRoute(path, args[1], "get", args[0]);
.split("\n") return this;
.filter(e => !e.startsWith("#") && e) };
.map(e => e.split(/\s{2,}/))
.filter(e => e.length > 1) post(path, ...args) {
.forEach(m => m[1].split(" ").forEach(ext => this.#mimes.set(ext, m[0]))); if(args.length === 1)
} this.registerRoute(path, args[0], "post");
else
this.get(route, async (req, res) => { this.registerRoute(path, args[1], "post", args[0]);
try { return this;
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") registerRoute(path, cb, method, middleware) {
}); if(!this.routes.has(path))
} catch { this.routes.set(path, {});
return res.reply({ this.routes.set(path, {
code: 404, ...this.routes.get(path),
body: "404 - file not found" [method]: cb,
}); [method + "mw"]: middleware,
} });
}); console.log("route set:", method.toUpperCase(), path);
} this.sortRoutes();
}; return this;
};
Map.prototype.getRegex = function(path, method) {
return [...this.entries()].filter(r => r[0].exec(path) && r[1].method.includes(method))[0]?.[1].f; 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
View 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, '&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 a iterable object`);
};
};

View File

@ -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}`))));
}
};