A simple, folder-driven static-site engine.
bun ssg fs
at dev 120 lines 3.2 kB view raw
1/** 2 * Writes the exported JSON model to `<outputDir>/_model`. 3 * 4 * Emits `site.json` plus one JSON per entry under `entries/<collection>/`. 5 */ 6 7import { mkdir, rm } from "node:fs/promises"; 8import * as path from "node:path"; 9import type { Logger } from "../../logging/logger"; 10import type { Site } from "../model"; 11import { writeIfChanged } from "../../utils/fs"; 12 13const PREFERRED_KEYS = [ 14 "rootDir", 15 "site", 16 "config", 17 "collections", 18 "id", 19 "rawName", 20 "path", 21 "name", 22 "displayName", 23 "description", 24 "type", 25 "ext", 26 "slug", 27 "route", 28 "isDraft", 29 "createdAt", 30 "updatedAt", 31 "fingerprint", 32 "entries", 33 "blocks", 34 "subBlocks", 35] as const; 36 37function orderKeys(value: unknown): unknown { 38 if (Array.isArray(value)) return value.map(orderKeys); 39 if (!value || typeof value !== "object") return value; 40 41 const obj = value as Record<string, unknown>; 42 const ordered: Record<string, unknown> = {}; 43 44 for (const key of PREFERRED_KEYS) { 45 if (Object.prototype.hasOwnProperty.call(obj, key)) { 46 ordered[key] = orderKeys(obj[key]); 47 } 48 } 49 50 for (const key of Object.keys(obj)) { 51 if ((PREFERRED_KEYS as readonly string[]).includes(key)) continue; 52 ordered[key] = orderKeys(obj[key]); 53 } 54 55 return ordered; 56} 57 58export async function writeModelExport(options: { 59 exportDir: string; 60 site: Site; 61 logger: Logger; 62}): Promise<void> { 63 const { exportDir, site, logger } = options; 64 65 const siteSummary = { 66 ...site, 67 collections: site.collections?.map((collection) => { 68 const { entries, ...rest } = collection; 69 const entrySummaries = entries.map((entry) => ({ 70 id: entry.id, 71 rawName: entry.rawName, 72 path: entry.path, 73 name: entry.name, 74 displayName: entry.displayName, 75 description: entry.description, 76 slug: entry.slug, 77 route: entry.route, 78 isDraft: entry.isDraft, 79 isUnlisted: entry.isUnlisted, 80 createdAt: entry.createdAt, 81 updatedAt: entry.updatedAt, 82 fingerprint: entry.fingerprint, 83 })); 84 return { 85 ...rest, 86 entries: entrySummaries, 87 }; 88 }), 89 }; 90 91 try { 92 await rm(exportDir, { recursive: true, force: true }); 93 await mkdir(exportDir, { recursive: true }); 94 await writeIfChanged( 95 path.join(exportDir, "site.json"), 96 JSON.stringify(orderKeys(siteSummary), null, 2), 97 logger 98 ); 99 100 const entriesDir = path.join(exportDir, "entries"); 101 await mkdir(entriesDir, { recursive: true }); 102 103 for (const collection of site.collections ?? []) { 104 const collectionDirName = collection.slug || collection.id; 105 const collectionDir = path.join(entriesDir, collectionDirName); 106 await mkdir(collectionDir, { recursive: true }); 107 108 for (const entry of collection.entries) { 109 const entryFileName = `${entry.slug || entry.id}.json`; 110 const entryPath = path.join(collectionDir, entryFileName); 111 await writeIfChanged(entryPath, JSON.stringify(orderKeys(entry), null, 2), logger); 112 } 113 } 114 115 logger.info("build.exportModel.success", { path: exportDir }); 116 } catch (err: any) { 117 logger.error("build.exportModel.error", { path: exportDir, error: String(err) }); 118 throw err; 119 } 120}