@sufalctl/honeycomb
Version:
**Honeycomb** is a clean and minimal web framework inspired by Express.js — built entirely from scratch using **raw TypeScript** with **no external libraries**.
204 lines (203 loc) • 7.97 kB
JavaScript
;
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());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Req = exports.Res = exports.honeycomb = void 0;
const node_http_1 = require("node:http");
const reqRes_1 = require("./reqRes");
Object.defineProperty(exports, "Req", { enumerable: true, get: function () { return reqRes_1.Req; } });
Object.defineProperty(exports, "Res", { enumerable: true, get: function () { return reqRes_1.Res; } });
const node_path_1 = __importDefault(require("node:path"));
const fs_1 = __importDefault(require("fs"));
class App {
constructor() {
this.routes = {};
this.middleware = [];
this.settings = {};
}
use(middleware) {
this.middleware.push(middleware);
}
set(key, value) {
this.settings[key] = value;
}
rendeViews(view, data) {
return __awaiter(this, void 0, void 0, function* () {
let ejs;
try {
ejs = require("ejs");
}
catch (err) {
throw new Error("EJS is not installed. Please run: npm install ejs");
}
const viewDir = this.settings["views"] || "./views";
const filePath = node_path_1.default.join(viewDir, `${view}.ejs`);
if (!fs_1.default.existsSync(filePath)) {
throw new Error(`view "${view}.ejs" not found`);
}
return yield ejs.renderFile(filePath, data || {}, { async: true });
});
}
serveStaticFile(req, res) {
const staticDir = this.settings["static"] || "./public";
const url = req.url || "/";
const filePath = node_path_1.default.join(staticDir, decodeURIComponent(url));
if (fs_1.default.existsSync(filePath) && fs_1.default.statSync(filePath).isFile()) {
const types = {
".css": "text/css",
".js": "application/javascript",
".png": "image/png",
".jpg": "image/jpg",
".svg": "image/svg+xml"
};
const ext = node_path_1.default.extname(filePath).toLowerCase();
const contentType = types[ext] || "application/octet-stream";
res.writeHead(200, { "content-type": contentType });
fs_1.default.createReadStream(filePath).pipe(res);
return true;
}
return false;
}
appRoute(method, path, ...handles) {
if (!this.routes[method]) {
this.routes[method] = [];
}
const middlewares = handles.slice(0, -1);
const finalHandler = handles[handles.length - 1];
const { regex, paramNames, queryNames } = this.pathToRegex(path);
this.routes[method].push({
path,
regex,
paramNames,
queryNames,
handler: (req, res) => {
let i = 0;
const next = () => {
const middleware = middlewares[i++];
if (middleware) {
middleware(req, res, next);
}
else {
finalHandler(req, res);
}
};
next();
}
});
}
get(path, ...handles) {
this.appRoute('GET', path, ...handles);
}
post(path, ...handles) {
this.appRoute('POST', path, ...handles);
}
put(path, ...handles) {
this.appRoute('PUT', path, ...handles);
}
delete(path, ...handles) {
this.appRoute('DELETE', path, ...handles);
}
listen(port, callback) {
const server = (0, node_http_1.createServer)((req, res) => {
const newReq = Object.setPrototypeOf(req, reqRes_1.Req.prototype);
const newRes = Object.setPrototypeOf(res, reqRes_1.Res.prototype);
let i = 0;
const next = () => {
const middleware = this.middleware[i++];
if (middleware) {
middleware(newReq, newRes, next);
}
else {
this.handleRequest(newReq, newRes);
}
};
next();
});
server.listen(port, callback);
}
handleRequest(req, res) {
return __awaiter(this, void 0, void 0, function* () {
const method = req.method || "GET";
const routers = this.routes[method];
if (!routers) {
res.status(404).json({ success: false, message: "Not Found" });
}
if (this.serveStaticFile(req, res)) {
return;
}
res.render = (view, data) => __awaiter(this, void 0, void 0, function* () {
res.writeHead(200, { "content-type": "text/html" });
const html = yield this.rendeViews(view, data || {});
res.end(html);
});
const url = req.url || "/";
const [pathName, queryString = ""] = url.split("?");
const params = new URLSearchParams(queryString);
const query = {};
for (const [key, value] of params.entries()) {
query[key] = value;
}
req.query = Object.assign(query);
yield this.bodyParsed(req);
for (const route of routers) {
if (pathName == route.path) {
return route.handler(req, res);
}
const match = url.match(route.regex);
if (match) {
const params = {};
route.paramNames.forEach((name, index) => {
params[name] = match[index + 1];
});
req.params = params;
return route.handler(req, res);
}
}
res.status(404).json({ success: false, message: "Not Found" });
});
}
bodyParsed(req) {
return __awaiter(this, void 0, void 0, function* () {
if (req.method === "POST" || req.method == "PUT") {
let body = "";
req.body = yield new Promise((resolve, reject) => {
req.on("data", (chunk) => body += chunk);
req.on("end", () => {
try {
resolve(body ? JSON.parse(body) : {});
}
catch (err) {
reject(new Error("Invalid JSON in request body"));
}
});
req.on("error", reject);
});
}
else {
req.body = {};
}
});
}
pathToRegex(path) {
const paramNames = [];
const queryNames = [];
const regexStr = path.replace(/:([^\/]+)/g, (_, key) => {
paramNames.push(key);
return '([^\\/]+)';
});
const regex = new RegExp(`^${regexStr}$`);
return { regex, paramNames, queryNames };
}
}
const honeycomb = () => new App();
exports.honeycomb = honeycomb;