Skip to main content
Module

x/fastro/core/server.ts

Fast and simple web application framework for deno
Go to Latest
File
import { serve, ServerRequest, Server, decode,} from "../deps.ts";import { version } from "../mod.ts";
export class Fastro { constructor(options?: ServerOptions) { if (options && options.payload) this.payloadEnabled = true; if (options && options.static) this.staticEnabled = true; if (options && options.logger) this.loggerEnabled = true; }
private server!: Server; private middlewareList: Middleware[] = []; private routerList: Router[] = []; private staticEnabled!: boolean; private loggerEnabled!: boolean; private payloadEnabled!: boolean; private staticFileList: Static[] = []; private pluginList: Instance[] = [];
public route(router: Router) { const [exist] = this.routerList.filter((r) => { return (r.url === router.url) && (r.method === router.method); }); if (!exist) this.routerList.push(router); return this; }
public get(url: string, handler: Handler) { return this.route({ method: "GET", url, handler }); }
public post(url: string, handler: Handler) { return this.route({ method: "POST", url, handler }); }
public head(url: string, handler: Handler) { return this.route({ method: "HEAD", url, handler }); }
public patch(url: string, handler: Handler) { return this.route({ method: "PATCH", url, handler }); }
public put(url: string, handler: Handler) { return this.route({ method: "PUT", url, handler }); }
public options(url: string, handler: Handler) { return this.route({ method: "OPTIONS", url, handler }); }
public delete(url: string, handler: Handler) { return this.route({ method: "DELETE", url, handler }); }
public async close(): Promise<void> { if (this.server) { this.server.close(); } }
public static(url: string, folder: string) { this.staticFileList.push({ url, folder }); return this; }
private loadPlugin(fastro: Fastro) { let afterLoadRouter: Router[]; let afterLoadPlugin: Instance[]; const loop = (plugins: Instance[]) => { plugins.forEach((p) => { afterLoadRouter = [...this.routerList]; afterLoadPlugin = [...this.pluginList]; p.plugin(fastro, () => { const list = this.pluginList.filter((plugin) => !afterLoadPlugin.includes(plugin) ); if (list.length > 0) loop(list); if (p.prefix) { this.routerList .filter((r) => !afterLoadRouter.includes(r)) .forEach((x) => { const url = x.url === "/" ? "" : x.url; x.url = `/${p.prefix}${url}`; }); } }); }); }; loop(this.pluginList); this.routerList = [...new Set(this.routerList)]; return this; }
register(plugin: Plugin): Fastro; register(prefix: string, plugin: Plugin): Fastro; register(prefixOrPlugin: string | Plugin, plugin?: Plugin) { if (typeof prefixOrPlugin !== "string") { this.pluginList.push({ plugin: prefixOrPlugin }); } if (typeof prefixOrPlugin === "string" && plugin) { this.pluginList.push({ prefix: prefixOrPlugin, plugin }); } return this; }
public use(handler: Handler): Fastro; public use(url: string, handler: Handler): Fastro; public use(handlerOrUrl: string | Handler, handler?: Handler) { if (typeof handlerOrUrl !== "string") { this.middlewareList.push({ url: "/", handler: handlerOrUrl }); } if (handler && (typeof handlerOrUrl === "string")) { this.middlewareList.push({ url: handlerOrUrl, handler }); } return this; }
private send<T>( payload: string | T, status: number | undefined = 200, headers: Headers | undefined = new Headers(), req: ServerRequest, ) { try { // deno-lint-ignore no-explicit-any let body: any; if ( typeof payload === "string" || payload instanceof Uint8Array ) { body = payload; } else if (typeof payload === "undefined") body = "undefined"; else if (Array.isArray(payload)) body = JSON.stringify(payload); else if ( typeof payload === "number" || typeof payload === "boolean" || typeof payload === "bigint" || typeof payload === "function" || typeof payload === "function" || typeof payload === "symbol" ) { body = payload.toString(); } else body = JSON.stringify(payload);
headers.set("x-powered-by", `Fastro/${version.fastro}`); req.respond({ body, status, headers }); } catch (e) { if (this.loggerEnabled) { throw fastroError("SEND_ERROR", e); } } }
private async getPayload(req: ServerRequest) { return decode(await Deno.readAll(req.body)); }
private getParameter(incoming: string) { try { let incomingSplit = incoming.substr(1, incoming.length).split("/"); const params: string[] = []; incomingSplit .map((path, idx) => { return { path, idx }; }) .forEach((value) => params.push(incomingSplit[value.idx]));
return params; } catch (error) { if (this.loggerEnabled) throw fastroError("GET_PARAMETER_ERROR", error); } }
private checkUrl(incoming: string, registered: string) { if (incoming.length > 1 && (registered === "/")) return false; return incoming.includes(registered); }
private async routeHandler(req: Request) { try { const [router] = this.routerList.filter((router) => { return this.checkUrl(req.url, router.url) && (req.method === router.method); }); if (!router) return req.respond({ body: "not found", status: 404 }); router.handler(req, () => {}); } catch (error) { if (this.loggerEnabled) throw fastroError("ROUTE_HANDLER_ERROR", error); } }
private checkExtension(url: string) { const split = url.split("/"); const extension = split[split.length - 1].includes("."); return extension; }
private formatDirText(contentList: any[]) { let formattedText: string = ""; contentList.forEach((e) => { const text: any = e.name; formattedText = formattedText.concat(text + "\n"); }); return formattedText; }
private async defaultStaticRoot(req: Request, folder: string) { const [defaultFolder] = this.staticFileList.filter((staticFile) => { return (req.url === staticFile.url) && !this.checkExtension(req.url); }); if (!defaultFolder) return req.send("file not found", 404); const contentList = await readDir(folder); const contentText = this.formatDirText(contentList); req.send(contentText); }
private fileCache: StaticFileCache[] = [];
private async readStaticFileHandler( req: Request, f: Static, ) { const filePath = req.url.replace(f.url, "").replace("/", ""); const finalPath = `${f.folder}/${filePath}`; try { const [exist] = this.fileCache.filter((cache) => { return cache.path === finalPath; }); let file; if (!exist) { file = await Deno.readFile(finalPath); this.fileCache.push({ path: finalPath, content: file }); } else { file = exist.content; } req.send(file); } catch (e) { // console.log(e) this.defaultStaticRoot(req, f.folder); } }
// deno-lint-ignore no-explicit-any private staticContainer = new Map<string, any>();
private addStaticRoute(url: string, staticFile: Static) { this.staticContainer.set(url, staticFile); return this.get(url, (req) => { const [router] = this.routerList.filter((r) => r.url === req.url); const staticFile = this.staticContainer.get(router.url); this.readStaticFileHandler(req, staticFile); }); }
private async staticHandler(request: Request) { const [rootStaticFile] = this.staticFileList.filter((staticFile) => { return request.url === staticFile.url; });
if (rootStaticFile) return this.addStaticRoute(request.url, rootStaticFile);
const [staticFile] = this.staticFileList.filter((staticFile) => { if (staticFile.url === "/") return false; const staticLength = staticFile.url.split("/").length; const requestLength = request.url.split("/").length; const diff = requestLength - staticLength; return request.url.includes(staticFile.url) && diff < 2; }); if (staticFile) return this.addStaticRoute(request.url, staticFile); const [idx] = this.staticFileList.filter((f) => f.url === "/"); this.addStaticRoute(request.url, idx); }
private async middlewareHandler(req: Request) { try { const midddlewares = this.middlewareList.filter((m) => { return m.url && this.checkUrl(req.url, m.url); });
if (midddlewares.length < 1) return this.routeHandler(req);
midddlewares.forEach((m) => { m.handler(req, () => { this.routeHandler(req); }); }); } catch (error) { if (this.loggerEnabled) { throw fastroError("MIDDLEWARE_HANDLER_ERROR", error); } } }
private async requestHandler(request: ServerRequest) { try { const req = request as Request; if (this.payloadEnabled) req.payload = await this.getPayload(request); if (this.loggerEnabled) { const msg = [ new Date(), req.proto, req.method, req.payload, `${req.headers.get("host")}${req.url}`, `${req.headers.get("user-agent")}`, ]; console.log(JSON.stringify(msg)); } req.parameter = this.getParameter(req.url) ?? []; req.send = (payload, status, headers) => { return this.send(payload, status, headers, req); }; if (this.staticEnabled && this.staticFileList.length > 0) { this.staticHandler(req); } if (this.middlewareList.length > 0) return this.middlewareHandler(req); this.routeHandler(req); } catch (error) { if (this.loggerEnabled) throw fastroError("REQUEST_HANDLER_ERROR", error); } }
public async listen( options?: ListenOptions, callback?: (error: Error | undefined, address: string | undefined) => void, ) { try { let opt = options ? options : { port: 3000 }; this.server = serve(opt); this.loadPlugin(this); if (!callback) console.info("Server listen on port", opt.port); // deno-lint-ignore no-explicit-any else callback(undefined, opt as any); for await (const req of this.server) { await this.requestHandler(req); } } catch (error) { throw fastroError("LISTEN_ERROR", error); } }}
export class Request extends ServerRequest { parameter!: string[]; payload!: string | undefined; send!: { <T>(payload: string | T, status?: number, headers?: Headers): void; }; // deno-lint-ignore no-explicit-any [key: string]: any}
export function fastroError(name: string, error: Error) { const msg = [new Date(), name, error.message, error.stack]; console.error(JSON.stringify(msg));}
export interface ServerOptions { payload?: boolean; static?: boolean; logger?: boolean;}
export interface ListenOptions { port: number; hostname?: string;}
interface Router { method: string; url: string; handler(req: Request, callback: Function): void; // deno-lint-ignore no-explicit-any [key: string]: any;}
interface Middleware { url?: string; handler(req: Request, callback: Function): void;}
interface Handler { (req: Request, callback: Function): void;}
interface Static { url: string; folder: string;}
interface StaticFileCache { path: string; // deno-lint-ignore no-explicit-any content: any;}
interface Plugin { (fastro: Fastro, callback: Function): void;}interface Instance { plugin: Plugin; prefix?: string;}
async function readDir(target: string) { type entry = { name: string; isDirectory: boolean; isFile: boolean; isSymlink: boolean; }; let files: entry[] = []; const results = Deno.readDir(target); for await (const dirEntry of results) { let file = target + "/" + dirEntry.name; files.push(dirEntry); } return files;}