diff --git a/src/index.mjs b/src/index.mjs index 06fbd27..b3bfda7 100644 --- a/src/index.mjs +++ b/src/index.mjs @@ -1,5 +1,3 @@ -import path from "path"; -import fs from "fs"; import http from "http"; import { URL } from "url"; import querystring from "querystring"; @@ -10,90 +8,26 @@ import Tpl from "./template.mjs"; export { Router, Tpl }; export default class flummpress { - #mimes; #server; + constructor() { this.router = new Router(); this.tpl = new Tpl(); - + this.middleware = new Set(); return this; }; use(obj) { if(obj instanceof Router) { - this.router.routes = new Map([ ...this.router.routes, ...obj.routes ]); - this.router.sortRoutes(); + this.router.use(obj); } 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]))); + else { + if(!this.middleware.has(obj)) + this.middleware.add(obj); } - - this.router.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); - 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; }; @@ -109,6 +43,16 @@ export default class flummpress { qs: {...querystring.parse(_url.search.substring(1))} // legacy }; + req.cookies = {}; + if(req.headers.cookie) { + req.headers.cookie.split("; ").forEach(c => { + const parts = c.split('='); + req.cookies[parts.shift().trim()] = decodeURI(parts.join('=')); + }); + } + + await Promise.all([...this.middleware].map(m => m(req, res))); + const method = req.method.toLowerCase(); const route = this.router.getRoute(req.url.pathname, req.method); @@ -166,6 +110,10 @@ export default class flummpress { }) => 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 }); + res.redirect = (target, code = 307) => res.writeHead(code, { + "Cache-Control": "no-cache, public", + "Location": target + }).end(); return res; }; diff --git a/src/router.mjs b/src/router.mjs index ef0f41a..f25e483 100644 --- a/src/router.mjs +++ b/src/router.mjs @@ -1,9 +1,27 @@ +import fs from "fs"; +import path from "path"; +import Tpl from "./template.mjs"; + export default class Router { + #mimes; + constructor() { this.routes = new Map(); return this; }; + async importRoutesFromPath(p, tpl = false) { + await Promise.all( + fs.readdirSync(path.resolve() + "/" + p) + .filter(r => r.endsWith(".mjs")) + .map(async r => { + const tmp = (await import(`${path.resolve()}/${p}/${r}`)).default(this, tpl); + this.use(tmp); + }) + ); + return this; + }; + group(path, cb) { const methods = { get: this.get.bind(this), @@ -25,6 +43,16 @@ export default class Router { return this; }; + 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, ...args) { if(args.length === 1) this.registerRoute(path, args[0], "get"); @@ -53,7 +81,7 @@ export default class Router { this.sortRoutes(); return this; }; - + getRoute(path, method) { method = method.toLowerCase(); return [...this.routes.entries()].filter(r => { @@ -65,4 +93,75 @@ export default class Router { this.routes = new Map([...this.routes.entries()].sort().reverse()); return this; }; + + readMimes(file = "/etc/mime.types") { + this.#mimes = new Map(); + (fs.readFileSync(file, "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]))); + }; + + static({ dir = path.resolve() + "/public", route = /^\/public/ }) { + if(!this.#mimes) + 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); + let stat; + try { + stat = fs.statSync(file); + } catch(err) { + console.log(err); + 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 - internal server error" + }); + } + }); + return this; + }; }; diff --git a/src/template.mjs b/src/template.mjs index 9fd2fb3..10a8549 100644 --- a/src/template.mjs +++ b/src/template.mjs @@ -2,38 +2,108 @@ import fs from "fs"; import path from "path"; export default class { + #views; + #globals; + #cache; + #templates; + #debug; + constructor() { - this.views = path.resolve() + "/views"; + this.#views = "./views"; + this.#globals = {}; + this.#debug = false; + this.#cache = true; + this.#templates = new Map(); }; - getTemplate(tpl) { - return fs.readFileSync(`${this.views}/${tpl}.html`, "utf-8"); + + set debug(d) { + this.#debug = !!d; + }; + set views(v) { + this.#views = v; + this.readdir(v); + }; + set globals(g) { + this.#globals = g; + }; + set cache(c) { + this.#cache = !!c; + }; + + readdir(dir, root = dir) { + for(const d of fs.readdirSync(`${path.resolve()}/${dir}`)) { + if(d.endsWith(".html")) { // template + 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 { // directory + this.readdir(`${dir}/${d}`, root); + } + } } - render(tpl, data = {}) { + + getTemplate(tpl) { + let template = {}; + let cache = false; + if(this.#cache && this.#templates.has(tpl)) { + template = this.#templates.get(tpl); + cache = template.cached; + } + else { + template = { + code: fs.readFileSync(`${this.#views}/${tpl}.html`, "utf-8"), + cached: new Date() + }; + this.#templates.set(tpl, template); + } + return [ + template.code, + (this.#debug ? `" : "") + ].join(""); + }; + + render(file, data = {}, locals) { + data = { ...data, ...this.#globals, ...locals }; try { const code = 'with(_data){const __html = [];' + '__html.push(\`' + - tpl.replace(/[\t]/g, ' ') - .replace(/'(?=[^\{]*}})/, '\t') + this.getTemplate(file) + .replace(/[\t]/g, ' ') .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 (?.*) as (?.*)}}/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");}'; + + .replace(/{{--(.*?)--}}/g, "") // comments + .replace(/{{(.+?)}}/g, '\`,$1,\`') // yield variable + .replace(/{!!(.+?)!!}/g, '\`,this.escape($1),\`') // yield escaped variable + + .replace(/@js/g, "`);") // inject bare javascript + .replace(/@endjs/g, ";__html.push(`") + + .replace(/@include\((.*?)\)/g, (_, inc) => this.render(inc, data)) // include template + + .replace(/@for\((.*?)\)$/gm, `\`);for($1){__html.push(\``) + .replace(/@endfor/g, `\`);}__html.push(\``) + + .replace(/@each\((.*?) as (.*?)\)/g, `\`);this.forEach($1,($2,key)=>{__html.push(\``) // foreach for the poor + .replace(/@endeach/g, "\`);});__html.push(`") + + .replace(/@elseif\((.*?)\)(\)?)/g, `\`);}else if($1$2){__html.push(\``) // if lol + .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 })(data); } catch(err) { - console.log(err); - return err.message; + console.log(file, err.message); + return (this.#debug ? `${err.message} in ${file}` : ''); } }; + escape(str) { return (str + '') .replace(/&/g, '&') @@ -45,6 +115,7 @@ export default class { .replace(/}/g, '}') ; }; + forEach(o, f) { if(Array.isArray(o)) o.forEach(f);