A simple, folder-driven static-site engine.
bun ssg fs
at dev 87 lines 2.7 kB view raw
1/** 2 * Fresh ESM module loader for files that must reflect disk changes in a long-lived process. 3 * 4 * Bun currently caches ESM modules by path and ignores URL query/hash. This helper 5 * loads a module via `Bun.build()` (in-memory) and imports the resulting bundle as 6 * a `data:` URL, while caching by file mtime/size to avoid rebuilding unnecessarily. 7 */ 8 9import { mkdir, rm, stat, writeFile } from "node:fs/promises"; 10import { createHash } from "node:crypto"; 11import { tmpdir } from "node:os"; 12import * as path from "node:path"; 13import { pathToFileURL } from "node:url"; 14 15type CachedModule = { 16 mtimeMs: number; 17 size: number; 18 exports: any; 19 outputPath?: string; 20}; 21 22const moduleCache = new Map<string, CachedModule>(); 23 24const cacheRoot = path.join(tmpdir(), "webette-module-cache"); 25 26function buildCachePath(absPath: string, stamp: string): string { 27 const hash = createHash("sha1").update(absPath).digest("hex").slice(0, 12); 28 const baseName = path.parse(absPath).name || "module"; 29 return path.join(cacheRoot, hash, `${baseName}.${stamp}.mjs`); 30} 31 32export async function loadDefaultExportFresh<T = unknown>( 33 filePath: string 34): Promise<T> { 35 const absPath = path.resolve(filePath); 36 37 const s = await stat(absPath); 38 const key = path.normalize(absPath); 39 const cached = moduleCache.get(key); 40 if (cached && cached.mtimeMs === s.mtimeMs && cached.size === s.size) { 41 return cached.exports as T; 42 } 43 if (cached?.outputPath) { 44 await rm(cached.outputPath, { force: true }).catch(() => undefined); 45 } 46 47 // Note: Bun's TS types for `Bun.build()` vary by version; keep this config runtime-safe. 48 const result = await Bun.build({ 49 entrypoints: [absPath], 50 format: "esm", 51 target: "bun", 52 splitting: false, 53 sourcemap: "none", 54 minify: false, 55 write: false, 56 throw: false, 57 }); 58 59 if (!result.success || !result.outputs?.length) { 60 const details = 61 result.logs?.length 62 ? result.logs.map((l) => l.message).join("\n") 63 : "unknown build error"; 64 throw new Error(`Failed to load module ${absPath}: ${details}`); 65 } 66 67 const first = result.outputs[0]; 68 if (!first) { 69 throw new Error(`Failed to load module ${absPath}: build produced no output`); 70 } 71 72 const js = await first.text(); 73 const stamp = `${s.mtimeMs}-${s.size}`; 74 const outputPath = buildCachePath(absPath, stamp); 75 await mkdir(path.dirname(outputPath), { recursive: true }); 76 await writeFile(outputPath, js, "utf8"); 77 const mod = await import(pathToFileURL(outputPath).href); 78 const exportsValue = (mod as any)?.default ?? mod; 79 80 moduleCache.set(key, { 81 mtimeMs: s.mtimeMs, 82 size: s.size, 83 exports: exportsValue, 84 outputPath, 85 }); 86 return exportsValue as T; 87}