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}