Skip to main content
Deno 2 is finally here 🎉️
Learn more
Module

x/fastro/http/server.ts

Fast and simple web application framework for deno
Go to Latest
File
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093
// deno-lint-ignore-file no-explicit-anyimport { Esbuild } from "../build/esbuild.ts";import { EsbuildMod } from "../build/esbuildMod.ts";import { addSalt, exportCryptoKey, keyPromise, reverseString, SALT,} from "../crypto/key.ts";
import { contentType, extname, renderToReadableStream, Status, STATUS_TEXT, toHashString,} from "./deps.ts";
import { Render } from "./render.tsx";
type ServerHandler = Deno.ServeHandler;
type HandlerArgument = | ServerHandler | RequestHandler | MiddlewareArgument;
export interface Next { (error?: Error, data?: unknown): unknown;}
export type Hook = ( server: Fastro, request: Request, info: Info,) => Response | Promise<Response>;export class HttpRequest extends Request { record!: Record<string, any>; match?: URLPatternResult | null; params?: Record<string, string | undefined>; query?: Record<string, string>; [key: string]: any;}
export type Info = Deno.ServeHandlerInfo;
type Meta = { name?: string; content?: string; charset?: string; property?: string; itemprop?: string;};
type Script = { type?: string; src?: string; crossorigin?: "anonymous" | "use-credentials" | "" | undefined; nonce?: string; integrity?: string;};
type Link = { href?: string; as?: string; onload?: any; rel?: string; integrity?: string; media?: string; crossorigin?: "anonymous" | "use-credentials" | "" | undefined;};
type NoScriptLink = { rel?: string; href?: string;};
type ModuleFunction = (f: Fastro) => Fastro | Promise<Fastro>;
type onError = (error: any) => void;export type RenderOptions = { // A callback that fires whenever there is a server error, whether recoverable or not. By default, this only calls console.error. If you override it to log crash reports, make sure that you still call console.error. You can also use it to adjust the status code before the shell is emitted. onError?: onError; // An abort signal that lets you abort server rendering and render the rest on the client. abortController?: AbortController; build?: boolean; cache?: boolean; pageFolder?: string; status?: number; props?: any; development?: boolean; hydrate?: boolean; theme?: boolean; themeColor?: string; layout?: Layout; customRoot?: any;};
export const hydrateFolder = ".hydrate";
export class Context { server!: Fastro; info!: Info; render!: (options?: RenderOptions) => Response | Promise<Response>; send!: (data: unknown, status?: number) => Response | Promise<Response>;}
type RequestHandler = ( request: HttpRequest, ctx: Context,) => | Response | Promise<Response>;
type MiddlewareArgument = ( request: HttpRequest, ctx: Context, next: Next,) => | Promise<unknown> | unknown;
type RouteNest = { handler: HandlerArgument; method?: string; pathname?: string; params?: Record<string, string | undefined>; query?: Record<string, string>; match?: URLPatternResult; element?: Component; handlers?: Array<MiddlewareArgument>;};
type Route = { method: string; path: string; handler: HandlerArgument;};
type Middleware = { method?: string; path?: string; handler: MiddlewareArgument;};
export type PageComponent = { component: FunctionComponent | JSX.Element; folder: string;};
export type Component = | FunctionComponent | JSX.Element | PageComponent | React.ReactNode;
export function isPageComponent(c: PageComponent) { return c.component != undefined && c.folder != undefined;}
export function checkReferer(req: Request) { const referer = req.headers.get("referer"); const host = req.headers.get("host") as string; if (!referer || !referer?.includes(host)) { return new Response(STATUS_TEXT[Status.NotFound], { status: Status.NotFound, }); }}
export type FunctionComponent = (props?: any) => JSX.Element;
export type Layout = ({ children, data }: { children: React.ReactNode; data: any;}) => JSX.Element;
type Page = { path: string; element: Component; handlers: Array<MiddlewareArgument>; method?: string;};
export interface Fastro { /** * Immediately close the server listeners and associated HTTP connections. * @returns void */ close: () => void; /** * Add application level middleware * * ### Example * * ```ts * import fastro from "../mod.ts"; * * const f = new fastro(); * f.use((req: HttpRequest, _ctx: Context, next: Next) => { * console.log(`${req.method} ${req.url}`); * return next(); * }); * * await f.serve(); * ``` * @param handler */ use(...handler: Array<HandlerArgument>): Fastro; get(path: string, ...handler: Array<HandlerArgument>): Fastro; post(path: string, ...handler: Array<HandlerArgument>): Fastro; put(path: string, ...handler: Array<HandlerArgument>): Fastro; delete(path: string, ...handler: Array<HandlerArgument>): Fastro; patch(path: string, ...handler: Array<HandlerArgument>): Fastro; options(path: string, ...handler: Array<HandlerArgument>): Fastro; head(path: string, ...handler: Array<HandlerArgument>): Fastro; /** * Allow you access Server, Request, and Info after Middleware, Routes, Pages, and Static File Processing. * It can return `Response`, `Promise<Response>` or `void`. * * ### Example * * ```ts * import fastro, { Fastro, Info } from "../mod.ts"; * * const f = new fastro(); * * f.hook((_f: Fastro, _r: Request, _i: Info) => new Response("Hello World")); * * await f.serve(); * ``` * * @param hook */ hook(hook: Hook): Fastro; /** * Allow you to access static files with custom `path`, `folder`, `maxAge`, and `referer` checking. * * ### Example * * ```ts * import fastro from "../mod.ts"; * * const f = new fastro(); * * f.static("/static", { folder: "static", maxAge: 90, referer: true }); * * await f.serve(); * ``` * @param path * @param options */ static( path: string, options?: { maxAge?: number; folder?: string; referer?: boolean }, ): Fastro; /** * Allow you to define SSR page with custom `path`, `element`, and `handler` * * ### Example * * ```ts * import fastro, { Context, HttpRequest } from "../mod.ts"; * import user from "../pages/user.tsx"; * * const f = new fastro(); * * f.static("/static", { folder: "static", maxAge: 90 }); * * f.page("/", user, (_req: HttpRequest, ctx: Context) => { * const options = { * props: { data: "Guest" }, * status: 200, * html: { head: { title: "React Component" } }, * }; * return ctx.render(options); * }, * ); * await f.serve(); * ``` * @param path * @param element * @param handler */ page( path: string, element: Component, ...handler: Array<MiddlewareArgument> ): Fastro; register(mf: ModuleFunction): Promise<Fastro>; getStaticFolder(): string; getStaticPath(): string; getDevelopmentStatus(): boolean; /** * Add a handler directly * * @param method * @param path * @param handler */ push( method?: string, path?: string, ...handler: Array<HandlerArgument> ): Fastro; onListen(handler: ListenHandler): void; finished(): Promise<void> | undefined; getNest(): Nest; record: Record<string, any>; /** * Serves HTTP requests * * If the server was constructed without a specified port, 8000 is used. */ serve(): Promise<void>;}
type ListenHandler = (params: { hostname: string; port: number }) => void;
type Static = { file: string; contentType: string;};
type Nest = Record< string, any>;
export const BUILD_ID = Deno.env.get("DENO_DEPLOYMENT_ID") || toHashString( new Uint8Array( await crypto.subtle.digest( "sha-1", new TextEncoder().encode(crypto.randomUUID()), ), ), "hex",);
export default class HttpServer implements Fastro { #server: Deno.Server | undefined; #routes: Route[]; #middlewares: Middleware[]; #pages: Page[]; #patterns: Record<string, URLPattern>; #root: URLPattern; #port; #ac; #staticUrl: string; #staticReferer: boolean; #staticFolder: string; #maxAge: number; record: Record<string, any>; #development: boolean; #nest: Nest; #body: ReadableStream<any> | undefined; #listenHandler: ListenHandler | undefined; #hook: Hook | undefined; #staticPath: string | undefined;
constructor(options?: { port?: number }) { this.#port = options?.port ?? 8000; this.#routes = []; this.#middlewares = []; this.#pages = []; this.#hook = undefined; this.#patterns = {}; this.#nest = {}; this.record = {}; this.#root = new URLPattern({ pathname: "/*" }); this.#ac = new AbortController(); this.#staticUrl = ""; this.#staticReferer = false; this.#staticFolder = ""; this.#maxAge = 0; this.#development = this.#getDevelopment(); this.#handleInit(); if (this.#development) this.#handleDevelopment(); const status = this.#development ? "Development" : "Production"; console.log( `%cStatus %c${status}`, "color: blue", "color: yellow", ); }
getNest(): Nest { return this.#nest; }
#getDevelopment = () => { return Deno.env.get("ENV") === "DEVELOPMENT"; };
#handleInitialData = async () => { const key = await keyPromise; let exportedKeyString = await exportCryptoKey(key); exportedKeyString = btoa(exportedKeyString); exportedKeyString = addSalt(exportedKeyString, SALT); exportedKeyString = reverseString(exportedKeyString); exportedKeyString = "{" + exportedKeyString + "}"; return exportedKeyString; };
serve = async (options?: { port: number }) => { this.record["exportedKeyString"] = await this.#handleInitialData(); this.record["salt"] = SALT; const port = options?.port ?? this.#port; const [s] = await this.#build(); if (s) return;
this.#server = Deno.serve({ port, handler: this.#handleRequest, onListen: this.#listenHandler, onError: this.#handleError, signal: this.#ac.signal, }); };
#hydrateExist = async () => { for await (const dirEntry of Deno.readDir(`${Deno.cwd()}`)) { if (dirEntry.name === hydrateFolder) { return true; } } return false; };
#createHydrateFile = async (e: string) => { const t = `${Deno.cwd()}/${hydrateFolder}/${e.toLowerCase()}.hydrate.tsx`; const js = this.#createHydrate(e.toLowerCase()); await Deno.writeTextFile(t, js); };
async #buildComponent(elementName: string) { const es = new Esbuild(elementName); await es.build(); es.stop(); }
#createHydrate(comp: string, module?: boolean) { function wordUpperCase(word: string) { return word[0].toUpperCase() + word.slice(1); }
const dev = Deno.env.get("ENV") === "DEVELOPMENT"; const hydrateRoot = dev ? "react-dom/client?dev&no-dts" : "react-dom/client?no-dts"; const page = module ? "./" : "../pages/"; return `// deno-lint-ignore-file no-explicit-any/** == ${ new Date().toLocaleString() } == DO NOT EDIT!! AUTOMATICALLY GENERATED BY FASTRO TO HYDRATE HTML FILES SO JS FILES CAN INTERACT. **/ import React from "react";import { hydrateRoot } from "${hydrateRoot}";import ${ wordUpperCase(comp) } from "${page}${comp.toLowerCase()}.page.tsx";import { atobMe as a,clean as c,extractOriginalString as ex,importCryptoKey as i,keyType,keyUsages,reverseString as rev} from "https://deno.land/x/fastro/crypto/key.ts";import { decryptData as d } from "https://deno.land/x/fastro/crypto/decrypt.ts";declare global {interface Window {__INITIAL_DATA__: any;}} fetch("/__INITIAL_DATA__").then((r) => r.json()).then((v) => { let r = c(v.d) as any;let s = c(v.s) as any;s = rev(s);s = a(s);r = rev(r);r = ex(r, s);r = a(r);i(r,keyType,keyUsages).then((k) => {d(k, window.__INITIAL_DATA__).then((v) => {delete window.__INITIAL_DATA__;const p = v as any;const r = document.getElementById("root");if (r) {const props = JSON.parse(p);hydrateRoot(document.getElementById("root") as Element,<${ wordUpperCase(comp) } {...props} />,);}}).catch((error) => console.error(error));}).catch((error) => console.error(error));}).catch((error) => console.error(error));`; }
#build = async () => { // deno-lint-ignore no-deprecated-deno-api if (Deno.run == undefined) return []; if (!await this.#hydrateExist()) { await Deno.mkdir(`${Deno.cwd()}/${hydrateFolder}`); }
for (let index = 0; index < this.#pages.length; index++) { const page = this.#pages[index]; if (isPageComponent(page.element as PageComponent)) { const pc = page.element as PageComponent; if (this.#isJSX(pc.component as JSX.Element)) continue; const fc = pc.component as FunctionComponent; await this.#createHydratePageComponentFile(pc); await this.#buildPageComponent(pc); this.#consoleLog(fc.name.toLowerCase()); } else { if (this.#isJSX(page.element as JSX.Element)) continue; const fc = page.element as FunctionComponent; await this.#createHydrateFile(fc.name); await this.#buildComponent(fc.name); this.#consoleLog(fc.name.toLowerCase()); } }
return Deno.args.filter((v) => v === "--hydrate"); };
#consoleLog(name: string) { console.log( `%c${name}.js %cCreated!`, "color: blue", "color: green", ); }
#createHydratePageComponentFile = async (c: PageComponent) => { const fc = c.component as FunctionComponent; const t = `${Deno.cwd()}/${c.folder}/${fc.name.toLowerCase()}.hydrate.tsx`; const h = this.#createHydrate(fc.name, true); await Deno.writeTextFile(t, h); };
async #buildPageComponent(c: PageComponent) { const es = new EsbuildMod(c); await es.build(); es.stop(); }
onListen = (handler: ListenHandler) => { this.#listenHandler = handler; };
push( method?: string, path?: string, ...handler: Array<HandlerArgument> ) { if (method && path) { this.#patterns[path] = new URLPattern({ pathname: path, });
if (handler.length === 1) { return this.#pushHandler(method, path, handler[0]); } this.#pushHandler(method, path, handler[handler.length - 1]);
for (let i = 0; i < handler.length - 1; i++) { this.#pushMiddleware( <MiddlewareArgument> <unknown> handler[i], method, path, ); } } else { for (let i = 0; i < handler.length; i++) { this.#pushMiddleware( <MiddlewareArgument> <unknown> handler[i], ); } }
return this; }
#pushHandler( method: string, path: string, handler: HandlerArgument, ) { this.#routes.push({ method, path, handler }); return this; }
#pushMiddleware( handler: MiddlewareArgument, method?: string, path?: string, ) { this.#middlewares.push({ method, path, handler }); return this; }
use(...handler: HandlerArgument[]) { return this.push(undefined, undefined, ...handler); }
get(path: string, ...handler: Array<HandlerArgument>) { return this.push("GET", path, ...handler); }
post(path: string, ...handler: Array<HandlerArgument>) { return this.push("POST", path, ...handler); }
put(path: string, ...handler: Array<HandlerArgument>) { return this.push("PUT", path, ...handler); }
head(path: string, ...handler: Array<HandlerArgument>) { return this.push("HEAD", path, ...handler); }
options(path: string, ...handler: Array<HandlerArgument>) { return this.push("OPTIONS", path, ...handler); }
delete(path: string, ...handler: Array<HandlerArgument>) { return this.push("DELETE", path, ...handler); }
patch(path: string, ...handler: Array<HandlerArgument>) { return this.push("PATCH", path, ...handler); }
static( path: string, options?: { maxAge?: number; folder?: string; referer?: boolean }, ) { this.#staticUrl = path; if (options?.folder) this.#staticFolder = options?.folder; if (options?.referer) this.#staticReferer = options.referer; if (options?.maxAge) this.#maxAge = options.maxAge; return this; }
page( path: string, element: Component, ...handlers: MiddlewareArgument[] ): Fastro { this.#patterns[path] = new URLPattern({ pathname: path, }); this.#pages.push({ path, element, handlers }); return this; }
getDevelopmentStatus() { return this.#development; }
#handleDevelopment = () => { const refreshPath = `/___refresh___`; this.#patterns[refreshPath] = new URLPattern({ pathname: refreshPath, }); const refreshStream = (_req: Request) => { let timerId: number | undefined = undefined; this.#body = new ReadableStream({ start(controller) { controller.enqueue(`data: ${BUILD_ID}\nretry: 100\n\n`); timerId = setInterval(() => { controller.enqueue(`data: ${BUILD_ID}\n\n`); }, 1000); }, cancel() { if (timerId !== undefined) { clearInterval(timerId); } }, }); return new Response(this.#body.pipeThrough(new TextEncoderStream()), { headers: { "content-type": "text/event-stream", }, }); }; this.#pushHandler("GET", refreshPath, refreshStream); };
#handleInit = () => { const initPath = `/__INITIAL_DATA__`; this.#patterns[initPath] = new URLPattern({ pathname: initPath, });
this.#pushHandler("GET", initPath, (req: HttpRequest) => { const ref = checkReferer(req); if (ref) return ref;
let s = btoa(req.record["salt"]); s = reverseString(s); return Response.json({ d: req.record["exportedKeyString"], s: `{${s}}`, }, { headers: new Headers({ "Access-Control-Allow-Origin": "null", "Access-Control-Allow-Methods": "GET", "Access-Control-Allow-Headers": "Content-Type", }), }); }); };
#handleError = (error: unknown) => { const err: Error = error as Error; console.error(error); return new Response(err.stack); };
#handleHook = async (h: Hook, r: Request, i: Info) => { const x = await h(this, r, i); if (this.#isResponse(x)) return x; };
// #checkReferer = (req: Request) => { // const referer = req.headers.get("referer"); // const host = req.headers.get("host") as string; // if (!referer || !referer?.includes(host)) { // return new Response(STATUS_TEXT[Status.NotFound], { // status: Status.NotFound, // }); // } // };
#handleRequest = async ( req: Request, i: Info, ) => { const m = await this.#findMiddleware(req, i); if (m) return this.#handleResponse(m);
const r = this.#findRoute(req.method, req.url); if (r?.handler) { const h = r?.handler as RequestHandler; const res = await h( this.#transformRequest(this.record, req, r.params, r.query, r.match), this.#initContext(i), ); return this.#handleResponse(res); }
const p = this.#findPage(req); if (p && p.handlers && p.match && p.element) { const result = await this.#handlePageMiddleware( p.handlers, req, i, p.match, p.element, );
if (result) return this.#handleResponse(result); }
const s = (await this.#findStaticFiles(this.#staticUrl, req.url)) as Static; if (s) { const ref = checkReferer(req); if (ref && this.#staticReferer) return ref; return new Response(s.file, { headers: { "Content-Type": s.contentType, "Cache-Control": `max-age=${this.#maxAge}`, }, }); }
const b = await this.#handleBinary(this.#staticUrl, req.url); if (b) { const ref = checkReferer(req); if (ref && this.#staticReferer) return ref; return this.#handleResponse(b); }
const h = this.#findHook(); if (h) { const res = await this.#handleHook(h, req, i); if (res) return this.#handleResponse(res); }
return new Response(STATUS_TEXT[Status.NotFound], { status: Status.NotFound, }); };
hook = (hook: Hook) => { this.#hook = hook; return this; };
#findHook = () => { return this.#hook; };
#findPage = (r: Request) => { if (this.#pages.length === 0) return null; let page: Page | undefined = undefined; const nestID = `p${r.url}`; const p = this.#nest[nestID]; if (p) return p as RouteNest; let pattern: URLPattern | null = null; for (let index = 0; index < this.#pages.length; index++) { const p = this.#pages[index]; const m = this.#patterns[p.path].test(r.url); if (m) { page = p; pattern = this.#patterns[p.path]; break; } }
const e = pattern?.exec(r.url); if (!e) return this.#nest[nestID] = null; return this.#nest[nestID] = { params: e.pathname.groups, query: this.#iterableToRecord(new URL(r.url).searchParams), handler: () => {}, match: e, handlers: page?.handlers, element: page?.element, }; };
#handlePageMiddleware = async ( handlers: MiddlewareArgument[], r: Request, i: Info, match: URLPatternResult, element: Component, ) => { const searchParams = new URL(r.url).searchParams; const ctx = this.#initContext(i, element, r); let result: unknown;
for (let index = 0; index < handlers.length; index++) { const h = handlers[index]; const req = this.#transformRequest( this.record, r, match?.pathname.groups, this.#iterableToRecord(searchParams), match, );
const x = await h(req, ctx, (error, data) => { if (error) throw error; return data; });
if (this.#isResponse(x)) { result = x; break; } }
return result; };
#findMiddleware = async (r: Request, i: Info) => { if (this.#middlewares.length === 0) return undefined; const method: string = r.method, url: string = r.url; const ctx = this.#initContext(i); const searchParams = new URL(r.url).searchParams; let result: unknown;
for (let index = 0; index < this.#middlewares.length; index++) { const m = this.#middlewares[index]; const nestId = method + m.method + m.path + url;
const match = this.#findMatch(m, nestId, url, method); if (!match) continue; const req = this.#transformRequest( this.record, r, match?.pathname.groups, this.#iterableToRecord(searchParams), match, ); const x = await m.handler( req, ctx, (error, data) => { if (error) throw error; return data; }, );
if (this.#isResponse(x)) { result = x; break; } }
return result; };
#findMatch( m: Middleware | Page, nestId: string, url: string, method: string, ) { const r = this.#nest[nestId]; if (r) return r as URLPatternResult;
const pattern = m.path ? this.#patterns[m.path] : this.#root; const result = pattern.exec(url); if (!result) return undefined; if ((m.path !== undefined) && (m.method !== method)) return undefined; return this.#nest[nestId] = result; }
#findRoute(method: string, url: string) { const nestID = `route${method + url}`; const r = this.#nest[nestID]; if (r) return r;
let h: Route | undefined = undefined; let p: URLPattern | null = null; for (let index = 0; index < this.#routes.length; index++) { const r = this.#routes[index]; const m = this.#patterns[r.path].test(url); if ((r.method === method) && m) { h = r; p = this.#patterns[r.path]; break; } } if (!h) return this.#nest[nestID] = null;
const e = p?.exec(url); if (!e) return this.#nest[nestID] = null; return this.#nest[nestID] = { params: e.pathname.groups, query: this.#iterableToRecord(new URL(url).searchParams), handler: h?.handler, match: e, }; }
#handleResponse(res: any, status = 200) { if (this.#isString(res)) return new Response(res, { status }); if (this.#isResponse(res)) { return res as Response; } if (this.#isJSX(res)) { return this.#renderToString(res, status); } if (this.#isJSON(res) || Array.isArray(res)) { return Response.json(res, { status }); } return new Response(res, { status }); }
getStaticPath() { return this.#staticUrl; }
getStaticFolder() { return this.#staticFolder; }
#findStaticFiles = async (url: string, reqUrl: string) => { const [nestID, pathname] = this.#nestIDPathname(url, reqUrl); if (this.#nest[nestID]) return this.#nest[nestID];
const pattern = new URLPattern({ pathname }); const match = pattern.exec(reqUrl); if (!match) return this.#nest[nestID] = null;
const input = match?.pathname.groups["0"]; const filePath = `${this.#staticFolder}/${input}`; const ct = contentType(extname(filePath)) || "application/octet-stream";
const binary = ["png", "jpeg", "jpg", "gif", "pdf", "aac"]; const b = binary.filter((v) => ct.includes(v)); if (b.length > 0) return this.#nest[nestID] = null;
let file; try { file = await Deno.readTextFile(`./${filePath}`); } catch { return this.#nest[nestID] = null; }
return this.#nest[nestID] = { contentType: ct, file }; };
#nestIDPathname = (url: string, reqUrl: string) => { const staticUrl = url === "/" ? "" : url; const pathname = `${staticUrl}/*`; const nestID = `${pathname}${reqUrl}`;
return [nestID, pathname]; };
#handleBinary = async (url: string, reqUrl: string) => { const [nestID, pathname] = this.#nestIDPathname(url, reqUrl); try { const match = new URLPattern({ pathname }).exec(reqUrl); const filePath = `${this.#staticFolder}/${match?.pathname.groups["0"]}`; const ct = contentType(extname(filePath)) || "application/octet-stream";
if (filePath === "/") return this.#nest[nestID] = null; const file = await Deno.open(`./${filePath}`, { read: true }); return new Response(file.readable, { headers: { "Content-Type": ct, "Cache-Control": `max-age=${this.#maxAge}`, }, }); } catch { return this.#nest[nestID] = null; } };
#iterableToRecord( params: URLSearchParams, ) { const record: Record<string, string> = {}; params.forEach((v, k) => (record[k] = v)); return record; }
#initContext( info: Info, element?: Component, req?: Request, ) { const ctx = new Context(); ctx.server = this; ctx.info = info; ctx.render = (options?: RenderOptions) => { if (!element) return new Response("Component not found"); return this.#renderElement(element, options, req); }; ctx.send = (data: unknown, status?: number) => { return this.#handleResponse(data, status); }; return ctx; }
#transformRequest( record: Record<string, any>, r: Request, params?: Record<string, string | undefined> | undefined, query?: Record<string, string>, match?: URLPatternResult | null, ) { const req = r as HttpRequest; req.record = record; req.match = match; req.params = params; req.query = query; return req; }
#renderToString = async (element: JSX.Element, status?: number) => { const component = await renderToReadableStream(element); return new Response(component, { status, headers: { "content-type": "text/html", }, }); };
#renderElement( element: Component, options?: RenderOptions, req?: Request, ) { const opt = options ?? {}; const r = new Render(element, opt, this.#nest, this, req); return r.render(); }
#isJSX(res: JSX.Element) { return res && res.props != undefined && res.type != undefined; }
#isString(res: any) { return typeof res === "string"; }
#isResponse(res: any) { return res instanceof Response; }
#isJSON(val: unknown) { try { const s = JSON.stringify(val); JSON.parse(s); return true; } catch { return false; } }
finished = () => { return this.#server?.finished; };
register = (mf: ModuleFunction) => { return Promise.resolve(mf(this)); };
close() { if (this.#server) { this.#server.finished.then(() => console.log("Server closed")); console.log("Closing server..."); this.#ac.abort(); } }}