UNPKG

@thi.ng/server

Version:

Minimal HTTP server with declarative routing, static file serving and freely extensible via pre/post interceptors

72 lines (71 loc) 2.19 kB
import { isFunction, isString } from "@thi.ng/checks"; import { fileHash as $fileHash } from "@thi.ng/file-io"; import { preferredTypeForPath } from "@thi.ng/mime"; import { existsSync, statSync } from "node:fs"; import { join, resolve } from "node:path"; import { isUnmodified } from "./utils/cache.js"; const staticFiles = ({ prefix = "static", rootDir = ".", intercept = [], filters = [pathFilterASCII], compress = false, auth = false, etag, headers } = {}) => { rootDir = resolve(rootDir); const filter = (path) => filters.every((f) => f(path)); return { id: "__static", match: isString(prefix) ? prefix + "/+" : [...prefix, "+"], auth, handlers: { get: { fn: async (ctx) => { const path = resolve(join(rootDir, ...ctx.match.rest)); const $headers = await __fileHeaders( rootDir, path, ctx, filter, etag, headers ); if (!$headers) return; if (ctx.origMethod === "head") { ctx.res.writeHead(200, { "content-type": preferredTypeForPath(path), ...$headers }); return; } return ctx.server.sendFile(ctx, path, $headers, compress); }, intercept } } }; }; const __fileHeaders = async (rootDir, path, ctx, filter, etag, headers) => { if (!(path.length > rootDir.length && filter(path) && existsSync(path))) { return ctx.res.missing(); } const $headers = isFunction(headers) ? headers(path) : headers; if (etag) { const etagValue = await etag(path); return isUnmodified(etagValue, ctx.req.headers["if-none-match"]) ? ctx.res.unmodified() : { ...$headers, etag: etagValue }; } return { ...$headers }; }; const etagFileTimeModified = (path) => statSync(path).mtimeMs.toString(36); const etagFileHash = (algo = "md5") => (path) => $fileHash(path, void 0, algo); const pathFilterASCII = (path) => !/[^\x20-\x7e]/.test(path); const pathMaxLength = (limit) => (path) => path.length < limit; export { etagFileHash, etagFileTimeModified, pathFilterASCII, pathMaxLength, staticFiles };