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}