Something went wrong. Try again.
Original-language reader for Kant, Aristotle & more: select any passage to gloss, translate, or hear it read aloud, with exact citations.
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788import { createHash } from "node:crypto"import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync,} from "node:fs"import { join } from "node:path"
/** * Content-hash cache for the expensive build steps (`bun run ingest`, * `bun run deploy`). Each step hashes its inputs — source files plus the env * flags that change the output — and stores the digest under * `node_modules/.cache/lekterion/`. A matching digest on the next run means the * inputs are byte-identical, so the step can be skipped. The cache lives in * `node_modules` so it is disposable (a fresh `bun install` rebuilds it) and is * never committed. */const CACHE_DIR = join("node_modules", ".cache", "lekterion")
export type CacheInputs = { /** Files and directories (recursed) whose contents feed the digest. */ paths: string[] /** Path prefixes to skip while recursing (e.g. generated/huge subtrees). */ exclude?: string[] /** Env vars that change the output, mixed in as `KEY=value`. */ env?: Record<string, string | undefined> /** Extra literal tokens to mix in (version constants, etc.). */ extra?: string[]}
function excluded(p: string, exclude: string[]): boolean { return exclude.some((e) => p === e || p.startsWith(e + "/"))}
function collectFiles(root: string, exclude: string[]): string[] { if (!existsSync(root) || excluded(root, exclude)) return [] if (statSync(root).isFile()) return [root] const out: string[] = [] for (const rel of readdirSync(root, { recursive: true, encoding: "utf8" })) { const p = join(root, rel) if (excluded(p, exclude)) continue if (statSync(p).isFile()) out.push(p) } return out}
/** SHA-256 over each input file's path + contents, then the env and extras. */export function inputHash(inputs: CacheInputs): string { const exclude = inputs.exclude ?? [] const files = [ ...new Set(inputs.paths.flatMap((p) => collectFiles(p, exclude))), ].sort() const h = createHash("sha256") for (const f of files) { h.update(f) h.update("\0") h.update(readFileSync(f)) h.update("\0") } for (const [k, v] of Object.entries(inputs.env ?? {})) h.update(`env:${k}=${v ?? ""}\0`) for (const e of inputs.extra ?? []) h.update(`extra:${e}\0`) return h.digest("hex")}
export type BuildCache = { /** True when the stored digest matches — inputs are unchanged. */ hit: boolean /** Persist the current digest so the next run can hit. */ save: () => void}
/** Compare `hash` against the stored digest for `name` under the cache dir. */export function checkCache(name: string, hash: string): BuildCache { const file = join(CACHE_DIR, `${name}.hash`) const prev = existsSync(file) ? readFileSync(file, "utf8").trim() : null return { hit: prev === hash, save() { mkdirSync(CACHE_DIR, { recursive: true }) writeFileSync(file, hash + "\n") }, }}