import { merge, mimes } from "../core/utils.ts";import { posix } from "../deps/path.ts";
import type { FileResponse, Page, Site } from "../core.ts";
export interface Options { router?: Router;}
export type Router = (url: URL) => Promise<string | undefined>;
export const defaults: Options = {};
export default function (userOptions?: Partial<Options>) { const options = merge(defaults, userOptions);
return (site: Site) => { if (!options.router) { const router = new JsonRouter(site, site.src("_routes.json")); options.router = router.match.bind(router); }
site.options.server.router ||= (url) => serve(url, site, options.router!); };}
async function serve( url: URL, site: Site, router: Router,): Promise<FileResponse | undefined> { const file = await router(url);
if (!file) { return; }
const page = await site.renderPage(file);
if (!page) { return undefined; }
const pageUrl = page.data.url as string; if (!url.pathname.endsWith("/") && pageUrl.endsWith("/")) { return [ null, { status: 301, headers: { "location": posix.join(url.pathname, "/"), }, }, ]; }
const body = page.content as string | Uint8Array; const response = { status: 200, headers: { "content-type": mimes.get(page.dest.ext) || mimes.get(".html")!, }, };
return [body, response];}
export class JsonRouter { #routesFile: string;
#routes?: Map<string, string>;
constructor(site: Site, routesFile: string) { site.addEventListener( "afterRender", () => this.#collectRoutes(site.pages), ); site.addEventListener("afterBuild", () => this.#saveRoutes());
site.options.watcher.ignore.push(routesFile); this.#routesFile = routesFile; }
async match(url: URL): Promise<string | undefined> { if (!this.#routes) { await this.#loadRoutes(); }
const { pathname } = url; const path = this.#routes?.get(pathname);
if (!path && !pathname.endsWith("/")) { return this.#routes?.get(pathname + "/"); }
return path; }
#collectRoutes(pages: Page[]): void { const routes = new Map<string, string>();
pages.forEach((page) => { if (page.data.ondemand) { routes.set(page.data.url as string, page.src.path + page.src.ext); } });
this.#routes = routes; }
async #loadRoutes(): Promise<void> { try { const pages = JSON.parse( await Deno.readTextFile(this.#routesFile), ) as Record<string, string>; this.#routes = new Map(Object.entries(pages)); } catch { this.#routes = new Map(); } }
async #saveRoutes(): Promise<void> { if (!this.#routes?.size) { return; }
const data: Record<string, string> = {}; this.#routes.forEach((path, url) => data[url] = path);
await Deno.writeTextFile( this.#routesFile, JSON.stringify(data, null, 2) + "\n", ); }}