serve.ts
3.4 kB · typescript · 98 lines
1import { extname } from "node:path";2import { globals, render, type Output, type Route, type Site, type Spec } from "./ssg/build.ts";34/* TYPES */56const TYPES: Record<string, string> = {7 html: "text/html; charset=utf-8",8 js: "text/javascript; charset=utf-8",9 mjs: "text/javascript; charset=utf-8",10 css: "text/css; charset=utf-8",11 json: "application/json",12 map: "application/json",13 webmanifest: "application/manifest+json",14 xml: "application/xml",15 txt: "text/plain; charset=utf-8",16 md: "text/markdown; charset=utf-8",17 tex: "text/plain; charset=utf-8",18 wasm: "application/wasm",19 svg: "image/svg+xml",20 png: "image/png",21 jpg: "image/jpeg",22 jpeg: "image/jpeg",23 gif: "image/gif",24 webp: "image/webp",25 ico: "image/x-icon",26 pdf: "application/pdf",27 woff2: "font/woff2",28 mp4: "video/mp4",29};3031export const type = (path: string) => TYPES[extname(path).slice(1)] ?? "application/octet-stream";3233export const reply = (item: Output, status = 200) =>34 new Response(item.bytes as string | Uint8Array, { status, headers: { "content-type": item.type ?? type(item.path) } });3536/* PATHS */3738export const clean = (path: string) =>39 path.startsWith("/") && !path.includes("\0") && !path.split("/").some((part) => part === "." || part === "..");4041const file = (path: string) => (path.endsWith("/") ? `${path.slice(1)}index.html` : path.slice(1));4243const owners = (site: Site, path: string) =>44 site.routes.filter((one) => one.route === path || one.urls?.some((url) => url.route === path));4546const holder = (site: Site, path: string) =>47 site.routes48 .filter((one) => one.route.endsWith("/") && !one.urls && one.route !== path && path.startsWith(one.route))49 .sort((a, b) => b.route.length - a.route.length)[0] ?? null;5051/* OUTPUTS */5253async function seek(spec: Spec, site: Site, route: Route, want: string): Promise<Output | null> {54 const outputs = await render(site, route, spec);55 return outputs.find((item) => item.path === want) ?? null;56}5758const shared = new WeakMap<Site, Promise<Map<string, Output>>>();5960function extras(spec: Spec, site: Site) {61 let hit = shared.get(site);62 if (!hit) {63 hit = globals(site, spec).then((list) => new Map(list.map((item) => [item.path, item])));64 shared.set(site, hit);65 }66 return hit;67}6869/* SERVE */7071export async function find(spec: Spec, site: Site, path: string): Promise<Response | null> {72 if (!clean(path)) return null;73 if (!path.endsWith("/") && site.routes.some((one) => one.route === `${path}/`)) return Response.redirect(`${path}/`, 302);74 const want = file(path);75 const tried = owners(site, path);76 for (const route of tried) {77 const hit = await seek(spec, site, route, want);78 if (hit) return reply(hit);79 }80 const copy = (await extras(spec, site)).get(want);81 if (copy) return reply(copy);82 const above = holder(site, path);83 if (above && !tried.includes(above)) {84 const hit = await seek(spec, site, above, want);85 if (hit) return reply(hit);86 }87 return null;88}8990export async function lost(spec: Spec, site: Site): Promise<Response> {91 const route = site.routes.find((one) => one.route === "/404.html");92 const hit = route ? await seek(spec, site, route, "404.html") : null;93 return hit ? reply(hit, 404) : new Response("not found", { status: 404 });94}9596export async function serve(spec: Spec, site: Site, path: string): Promise<Response> {97 return (await find(spec, site, path)) ?? (await lost(spec, site));98}