120 lines
3.1 KiB
TypeScript
120 lines
3.1 KiB
TypeScript
interface Loaded {
|
|
map: BuiltAssetMap;
|
|
buf: Buffer;
|
|
}
|
|
let assets: Loaded | null = null;
|
|
|
|
export type StaticPageId = string;
|
|
|
|
export async function reload() {
|
|
const [map, buf] = await Promise.all([
|
|
fs.readFile(path.join(import.meta.dirname, "static.json"), "utf8"),
|
|
fs.readFile(path.join(import.meta.dirname, "static.blob")),
|
|
]);
|
|
assets = {
|
|
map: JSON.parse(map),
|
|
buf,
|
|
};
|
|
}
|
|
|
|
export async function reloadSync() {
|
|
const map = fs.readFileSync(
|
|
path.join(import.meta.dirname, "static.json"),
|
|
"utf8",
|
|
);
|
|
const buf = fs.readFileSync(path.join(import.meta.dirname, "static.blob"));
|
|
assets = {
|
|
map: JSON.parse(map),
|
|
buf,
|
|
};
|
|
}
|
|
|
|
export async function middleware(c: Context, next: Next) {
|
|
if (!assets) await reload();
|
|
const asset = assets!.map[c.req.path];
|
|
if (asset) {
|
|
return assetInner(c, asset, 200);
|
|
}
|
|
return next();
|
|
}
|
|
|
|
export async function notFound(c: Context) {
|
|
if (!assets) await reload();
|
|
let pathname = c.req.path;
|
|
do {
|
|
const asset = assets!.map[pathname + "/404"];
|
|
if (asset) return assetInner(c, asset, 404);
|
|
pathname = pathname.slice(0, pathname.lastIndexOf("/"));
|
|
} while (pathname);
|
|
const asset = assets!.map["/404"];
|
|
if (asset) return assetInner(c, asset, 404);
|
|
return c.text("the 'Not Found' page was not found", 404);
|
|
}
|
|
|
|
export async function serveAsset(
|
|
c: Context,
|
|
id: StaticPageId,
|
|
status: StatusCode,
|
|
) {
|
|
assets ?? await reload();
|
|
return assetInner(c, assets!.map[id], status);
|
|
}
|
|
|
|
export function hasAsset(id: string) {
|
|
if (!assets) reloadSync();
|
|
return assets!.map[id] !== undefined;
|
|
}
|
|
|
|
export function etagMatches(etag: string, ifNoneMatch: string) {
|
|
return ifNoneMatch === etag || ifNoneMatch.split(/,\s*/).indexOf(etag) > -1;
|
|
}
|
|
|
|
function subarrayAsset([start, end]: View) {
|
|
return assets!.buf.subarray(start, end);
|
|
}
|
|
|
|
function assetInner(c: Context, asset: BuiltAsset, status: StatusCode) {
|
|
const ifnonematch = c.req.header("If-None-Match");
|
|
if (ifnonematch) {
|
|
const etag = asset.headers.ETag;
|
|
if (etagMatches(etag, ifnonematch)) {
|
|
return c.res = new Response(null, {
|
|
status: 304,
|
|
statusText: "Not Modified",
|
|
headers: {
|
|
ETag: etag,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
const acceptEncoding = c.req.header("Accept-Encoding") ?? "";
|
|
let body;
|
|
let headers = asset.headers;
|
|
if (acceptEncoding.includes("zstd") && asset.zstd) {
|
|
body = subarrayAsset(asset.zstd);
|
|
headers = {
|
|
...asset.headers,
|
|
"Content-Encoding": "zstd",
|
|
};
|
|
} else if (acceptEncoding.includes("gzip") && asset.gzip) {
|
|
body = subarrayAsset(asset.gzip);
|
|
headers = {
|
|
...asset.headers,
|
|
"Content-Encoding": "gzip",
|
|
};
|
|
} else {
|
|
body = subarrayAsset(asset.raw);
|
|
}
|
|
return c.res = new Response(body, { headers, status });
|
|
}
|
|
|
|
process.on("message", (msg: any) => {
|
|
if (msg?.type === "clover.assets.reload") reload();
|
|
});
|
|
|
|
import * as fs from "#sitegen/fs";
|
|
import type { Context, Next } from "hono";
|
|
import type { StatusCode } from "hono/utils/http-status";
|
|
import type { BuiltAsset, BuiltAssetMap, View } from "../incremental.ts";
|
|
import { Buffer } from "node:buffer";
|
|
import * as path from "node:path";
|