@contentrain/query 7.0.15 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -0
- package/dist/astro/index.cjs +142 -0
- package/dist/astro/index.d.cts +81 -0
- package/dist/astro/index.d.cts.map +1 -0
- package/dist/astro/index.d.mts +81 -0
- package/dist/astro/index.d.mts.map +1 -0
- package/dist/astro/index.mjs +143 -0
- package/dist/astro/index.mjs.map +1 -0
- package/dist/cdn/index.d.mts +1 -1
- package/dist/cli.cjs +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/frontmatter-CVjPyhOq.mjs +248 -0
- package/dist/frontmatter-CVjPyhOq.mjs.map +1 -0
- package/dist/frontmatter-DSFLOTwS.cjs +287 -0
- package/dist/{generate-mpKW9Y8v.mjs → generate-B7jooE0f.mjs} +4 -244
- package/dist/generate-B7jooE0f.mjs.map +1 -0
- package/dist/{generate-DN3mAbvC.cjs → generate-D3C6itur.cjs} +14 -254
- package/dist/generator/generate.cjs +1 -1
- package/dist/generator/generate.mjs +1 -1
- package/dist/{index-TgljULTn.d.mts → index-DYWwX7xL.d.mts} +1 -1
- package/dist/{index-TgljULTn.d.mts.map → index-DYWwX7xL.d.mts.map} +1 -1
- package/dist/index.d.mts +1 -1
- package/package.json +38 -14
- package/dist/generate-mpKW9Y8v.mjs.map +0 -1
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
|
+
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
3
|
+
//#region src/generator/utils.ts
|
|
4
|
+
async function readJson(filePath) {
|
|
5
|
+
try {
|
|
6
|
+
const raw = await readFile(filePath, "utf-8");
|
|
7
|
+
return JSON.parse(raw);
|
|
8
|
+
} catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
async function readDir(dirPath) {
|
|
13
|
+
try {
|
|
14
|
+
return await readdir(dirPath);
|
|
15
|
+
} catch {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async function readText(filePath) {
|
|
20
|
+
try {
|
|
21
|
+
return await readFile(filePath, "utf-8");
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function writeText(filePath, content) {
|
|
27
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
28
|
+
await writeFile(filePath, content, "utf-8");
|
|
29
|
+
}
|
|
30
|
+
function contentrainDir(projectRoot) {
|
|
31
|
+
return join(projectRoot, ".contentrain");
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/generator/config-reader.ts
|
|
35
|
+
async function readProjectManifest(projectRoot) {
|
|
36
|
+
const crDir = contentrainDir(projectRoot);
|
|
37
|
+
const rawConfig = await readJson(join(crDir, "config.json"));
|
|
38
|
+
const config = {
|
|
39
|
+
version: rawConfig?.version ?? 1,
|
|
40
|
+
stack: rawConfig?.stack ?? "other",
|
|
41
|
+
workflow: rawConfig?.workflow ?? "auto-merge",
|
|
42
|
+
locales: {
|
|
43
|
+
default: rawConfig?.locales?.default ?? "en",
|
|
44
|
+
supported: rawConfig?.locales?.supported ?? [rawConfig?.locales?.default ?? "en"]
|
|
45
|
+
},
|
|
46
|
+
domains: rawConfig?.domains ?? [],
|
|
47
|
+
repository: rawConfig?.repository,
|
|
48
|
+
assets_path: rawConfig?.assets_path,
|
|
49
|
+
cdn: rawConfig?.cdn,
|
|
50
|
+
branchRetention: rawConfig?.branchRetention
|
|
51
|
+
};
|
|
52
|
+
const modelsDir = join(crDir, "models");
|
|
53
|
+
const modelFiles = (await readDir(modelsDir)).filter((f) => f.endsWith(".json"));
|
|
54
|
+
const models = (await Promise.all(modelFiles.map((file) => readJson(join(modelsDir, file))))).filter((m) => m != null && Boolean(m.id)).toSorted((a, b) => a.id.localeCompare(b.id, "en"));
|
|
55
|
+
return {
|
|
56
|
+
config,
|
|
57
|
+
models,
|
|
58
|
+
contentFiles: (await Promise.all(models.map((model) => mapContentFiles(projectRoot, model, config)))).flat()
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function getContentDir(projectRoot, model) {
|
|
62
|
+
if (model.content_path) return join(projectRoot, model.content_path);
|
|
63
|
+
return join(contentrainDir(projectRoot), "content", model.domain, model.id);
|
|
64
|
+
}
|
|
65
|
+
function getLocaleStrategy(model) {
|
|
66
|
+
return model.locale_strategy ?? "file";
|
|
67
|
+
}
|
|
68
|
+
function jsonFilePath(dir, model, locale) {
|
|
69
|
+
switch (getLocaleStrategy(model)) {
|
|
70
|
+
case "suffix": return join(dir, `${model.id}.${locale}.json`);
|
|
71
|
+
case "directory": return join(dir, locale, `${model.id}.json`);
|
|
72
|
+
case "none": return join(dir, `${model.id}.json`);
|
|
73
|
+
default: return join(dir, `${locale}.json`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function mapContentFiles(projectRoot, model, config) {
|
|
77
|
+
const dir = getContentDir(projectRoot, model);
|
|
78
|
+
if (model.kind === "document") return mapDocumentFiles(dir, model, config);
|
|
79
|
+
if (model.i18n) return (await Promise.all(config.locales.supported.map(async (locale) => {
|
|
80
|
+
const filePath = jsonFilePath(dir, model, locale);
|
|
81
|
+
if (await readJson(filePath) === null) return null;
|
|
82
|
+
return {
|
|
83
|
+
modelId: model.id,
|
|
84
|
+
locale,
|
|
85
|
+
filePath,
|
|
86
|
+
kind: model.kind
|
|
87
|
+
};
|
|
88
|
+
}))).filter(Boolean);
|
|
89
|
+
const filePath = join(dir, "data.json");
|
|
90
|
+
if (await readJson(filePath) === null) return [];
|
|
91
|
+
return [{
|
|
92
|
+
modelId: model.id,
|
|
93
|
+
locale: null,
|
|
94
|
+
filePath,
|
|
95
|
+
kind: model.kind
|
|
96
|
+
}];
|
|
97
|
+
}
|
|
98
|
+
async function mapDocumentFiles(dir, model, config) {
|
|
99
|
+
const strategy = getLocaleStrategy(model);
|
|
100
|
+
if (!model.i18n) return (await readDir(dir)).filter((f) => f.endsWith(".md")).map((f) => ({
|
|
101
|
+
modelId: model.id,
|
|
102
|
+
locale: null,
|
|
103
|
+
filePath: join(dir, f),
|
|
104
|
+
kind: "document",
|
|
105
|
+
slug: f.replace(".md", "")
|
|
106
|
+
}));
|
|
107
|
+
return (await Promise.all(config.locales.supported.map((locale) => mapDocumentLocale(dir, model, locale, strategy)))).flat();
|
|
108
|
+
}
|
|
109
|
+
async function mapDocumentLocale(dir, model, locale, strategy) {
|
|
110
|
+
if (strategy === "file") {
|
|
111
|
+
const entries = await readDir(dir);
|
|
112
|
+
return (await Promise.all(entries.map(async (entry) => {
|
|
113
|
+
const filePath = join(dir, entry, `${locale}.md`);
|
|
114
|
+
if (await readText(filePath) === null) return null;
|
|
115
|
+
return {
|
|
116
|
+
modelId: model.id,
|
|
117
|
+
locale,
|
|
118
|
+
filePath,
|
|
119
|
+
kind: "document",
|
|
120
|
+
slug: entry
|
|
121
|
+
};
|
|
122
|
+
}))).filter(Boolean);
|
|
123
|
+
}
|
|
124
|
+
if (strategy === "directory") {
|
|
125
|
+
const localeDir = join(dir, locale);
|
|
126
|
+
return (await readDir(localeDir)).filter((f) => f.endsWith(".md")).map((f) => ({
|
|
127
|
+
modelId: model.id,
|
|
128
|
+
locale,
|
|
129
|
+
filePath: join(localeDir, f),
|
|
130
|
+
kind: "document",
|
|
131
|
+
slug: f.replace(".md", "")
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
if (strategy === "suffix") {
|
|
135
|
+
const suffix = `.${locale}.md`;
|
|
136
|
+
return (await readDir(dir)).filter((f) => f.endsWith(suffix)).map((f) => ({
|
|
137
|
+
modelId: model.id,
|
|
138
|
+
locale,
|
|
139
|
+
filePath: join(dir, f),
|
|
140
|
+
kind: "document",
|
|
141
|
+
slug: f.slice(0, -suffix.length)
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region src/shared/frontmatter.ts
|
|
148
|
+
const STRING_LIKE_TYPES = new Set([
|
|
149
|
+
"string",
|
|
150
|
+
"text",
|
|
151
|
+
"email",
|
|
152
|
+
"url",
|
|
153
|
+
"slug",
|
|
154
|
+
"color",
|
|
155
|
+
"phone",
|
|
156
|
+
"code",
|
|
157
|
+
"icon",
|
|
158
|
+
"markdown",
|
|
159
|
+
"richtext",
|
|
160
|
+
"date",
|
|
161
|
+
"datetime",
|
|
162
|
+
"image",
|
|
163
|
+
"video",
|
|
164
|
+
"file",
|
|
165
|
+
"select",
|
|
166
|
+
"relation"
|
|
167
|
+
]);
|
|
168
|
+
function stringLikeFieldKeys(model) {
|
|
169
|
+
const keys = /* @__PURE__ */ new Set();
|
|
170
|
+
if (model.fields) {
|
|
171
|
+
for (const [name, field] of Object.entries(model.fields)) if (STRING_LIKE_TYPES.has(field.type)) keys.add(name);
|
|
172
|
+
}
|
|
173
|
+
return keys;
|
|
174
|
+
}
|
|
175
|
+
function parseFrontmatter(text, stringKeys = /* @__PURE__ */ new Set()) {
|
|
176
|
+
const normalized = text.replace(/\r\n/g, "\n");
|
|
177
|
+
const match = normalized.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
178
|
+
if (!match) return {
|
|
179
|
+
frontmatter: {},
|
|
180
|
+
body: normalized
|
|
181
|
+
};
|
|
182
|
+
const fmStr = match[1];
|
|
183
|
+
const body = match[2].trim();
|
|
184
|
+
const frontmatter = {};
|
|
185
|
+
const lines = fmStr.split("\n");
|
|
186
|
+
const stack = [{
|
|
187
|
+
obj: frontmatter,
|
|
188
|
+
indent: -1
|
|
189
|
+
}];
|
|
190
|
+
for (const line of lines) {
|
|
191
|
+
if (line.trim() === "") continue;
|
|
192
|
+
const arrayMatch = line.match(/^(\s*)-\s+(.*)$/);
|
|
193
|
+
if (arrayMatch) {
|
|
194
|
+
const arrIndent = arrayMatch[1].length;
|
|
195
|
+
const value = arrayMatch[2].trim();
|
|
196
|
+
while (stack.length > 1 && stack[stack.length - 1].indent >= arrIndent) stack.pop();
|
|
197
|
+
const parent = stack[stack.length - 1].obj;
|
|
198
|
+
const lastKey = Object.keys(parent).pop();
|
|
199
|
+
if (lastKey && Array.isArray(parent[lastKey])) parent[lastKey].push(parseValue(value));
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const kvMatch = line.match(/^(\s*)([\w][\w.-]*)\s*:\s*(.*)$/);
|
|
203
|
+
if (!kvMatch) continue;
|
|
204
|
+
const kvIndent = kvMatch[1].length;
|
|
205
|
+
const key = kvMatch[2];
|
|
206
|
+
const rawValue = kvMatch[3].trim();
|
|
207
|
+
while (stack.length > 1 && stack[stack.length - 1].indent >= kvIndent) stack.pop();
|
|
208
|
+
const current = stack[stack.length - 1].obj;
|
|
209
|
+
if (rawValue === "") {
|
|
210
|
+
const nextLineIdx = lines.indexOf(line) + 1;
|
|
211
|
+
if ((nextLineIdx < lines.length ? lines[nextLineIdx] : "").trim().startsWith("-")) current[key] = [];
|
|
212
|
+
else {
|
|
213
|
+
const nested = {};
|
|
214
|
+
current[key] = nested;
|
|
215
|
+
stack.push({
|
|
216
|
+
obj: nested,
|
|
217
|
+
indent: kvIndent
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (rawValue.startsWith("[") && rawValue.endsWith("]")) {
|
|
223
|
+
current[key] = rawValue.slice(1, -1).split(",").map((s) => s.trim()).filter(Boolean);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
current[key] = parseValue(rawValue, current === frontmatter && stringKeys.has(key));
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
frontmatter,
|
|
230
|
+
body
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function parseValue(raw, forceString = false) {
|
|
234
|
+
const isQuoted = raw.startsWith("\"") && raw.endsWith("\"") || raw.startsWith("'") && raw.endsWith("'");
|
|
235
|
+
const unquoted = isQuoted ? raw.slice(1, -1) : raw;
|
|
236
|
+
if (forceString) return unquoted;
|
|
237
|
+
if (isQuoted) return unquoted;
|
|
238
|
+
if (raw === "true") return true;
|
|
239
|
+
if (raw === "false") return false;
|
|
240
|
+
if (raw === "null") return null;
|
|
241
|
+
if (/^-?\d+$/.test(raw)) return parseInt(raw, 10);
|
|
242
|
+
if (/^-?\d+\.\d+$/.test(raw)) return parseFloat(raw);
|
|
243
|
+
return raw;
|
|
244
|
+
}
|
|
245
|
+
//#endregion
|
|
246
|
+
export { readJson as a, readDir as i, stringLikeFieldKeys as n, readText as o, readProjectManifest as r, writeText as s, parseFrontmatter as t };
|
|
247
|
+
|
|
248
|
+
//# sourceMappingURL=frontmatter-CVjPyhOq.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frontmatter-CVjPyhOq.mjs","names":[],"sources":["../src/generator/utils.ts","../src/generator/config-reader.ts","../src/shared/frontmatter.ts"],"sourcesContent":["import { readFile, readdir, writeFile, mkdir } from 'node:fs/promises'\nimport { join, dirname } from 'node:path'\n\nexport async function readJson<T>(filePath: string): Promise<T | null> {\n try {\n const raw = await readFile(filePath, 'utf-8')\n return JSON.parse(raw) as T\n } catch {\n return null\n }\n}\n\nexport async function readDir(dirPath: string): Promise<string[]> {\n try {\n return await readdir(dirPath)\n } catch {\n return []\n }\n}\n\nexport async function readText(filePath: string): Promise<string | null> {\n try {\n return await readFile(filePath, 'utf-8')\n } catch {\n return null\n }\n}\n\nexport async function writeText(filePath: string, content: string): Promise<void> {\n await mkdir(dirname(filePath), { recursive: true })\n await writeFile(filePath, content, 'utf-8')\n}\n\nexport function contentrainDir(projectRoot: string): string {\n return join(projectRoot, '.contentrain')\n}\n","import type { ContentrainConfig, ModelDefinition, LocaleStrategy } from '@contentrain/types'\nimport { join } from 'node:path'\nimport { readJson, readDir, readText, contentrainDir } from './utils.js'\n\nexport interface ContentFileRef {\n modelId: string\n locale: string | null\n filePath: string\n kind: ModelDefinition['kind']\n /** Extracted slug for document kind (from filename, not frontmatter) */\n slug?: string\n}\n\nexport interface ProjectManifest {\n config: ContentrainConfig\n models: ModelDefinition[]\n contentFiles: ContentFileRef[]\n}\n\nexport async function readProjectManifest(projectRoot: string): Promise<ProjectManifest> {\n const crDir = contentrainDir(projectRoot)\n\n // 1. Read config\n const rawConfig = await readJson<Partial<ContentrainConfig>>(join(crDir, 'config.json'))\n const config: ContentrainConfig = {\n version: rawConfig?.version ?? 1,\n stack: (rawConfig?.stack ?? 'other') as ContentrainConfig['stack'],\n workflow: (rawConfig?.workflow ?? 'auto-merge') as ContentrainConfig['workflow'],\n locales: {\n default: rawConfig?.locales?.default ?? 'en',\n supported: rawConfig?.locales?.supported ?? [rawConfig?.locales?.default ?? 'en'],\n },\n domains: rawConfig?.domains ?? [],\n repository: rawConfig?.repository,\n assets_path: rawConfig?.assets_path,\n cdn: rawConfig?.cdn,\n branchRetention: rawConfig?.branchRetention,\n }\n\n // 2. Read all models (parallel)\n const modelsDir = join(crDir, 'models')\n const modelFiles = (await readDir(modelsDir)).filter(f => f.endsWith('.json'))\n const modelResults = await Promise.all(\n modelFiles.map(file => readJson<ModelDefinition>(join(modelsDir, file))),\n )\n const models = modelResults\n .filter((m): m is ModelDefinition => m != null && Boolean(m.id))\n .toSorted((a, b) => a.id.localeCompare(b.id, 'en'))\n\n // 3. Map content files (parallel per model)\n const allRefs = await Promise.all(\n models.map(model => mapContentFiles(projectRoot, model, config)),\n )\n const contentFiles = allRefs.flat()\n\n return { config, models, contentFiles }\n}\n\nfunction getContentDir(projectRoot: string, model: ModelDefinition): string {\n if (model.content_path) return join(projectRoot, model.content_path)\n return join(contentrainDir(projectRoot), 'content', model.domain, model.id)\n}\n\nfunction getLocaleStrategy(model: ModelDefinition): LocaleStrategy {\n return model.locale_strategy ?? 'file'\n}\n\nfunction jsonFilePath(dir: string, model: ModelDefinition, locale: string): string {\n const strategy = getLocaleStrategy(model)\n switch (strategy) {\n case 'suffix': return join(dir, `${model.id}.${locale}.json`)\n case 'directory': return join(dir, locale, `${model.id}.json`)\n case 'none': return join(dir, `${model.id}.json`)\n case 'file':\n default: return join(dir, `${locale}.json`)\n }\n}\n\nasync function mapContentFiles(\n projectRoot: string,\n model: ModelDefinition,\n config: ContentrainConfig,\n): Promise<ContentFileRef[]> {\n const dir = getContentDir(projectRoot, model)\n\n if (model.kind === 'document') {\n return mapDocumentFiles(dir, model, config)\n }\n\n // JSON-based models (singleton, collection, dictionary)\n if (model.i18n) {\n const results = await Promise.all(\n config.locales.supported.map(async (locale) => {\n const filePath = jsonFilePath(dir, model, locale)\n const content = await readJson(filePath)\n if (content === null) return null\n return { modelId: model.id, locale, filePath, kind: model.kind } as ContentFileRef\n }),\n )\n return results.filter(Boolean) as ContentFileRef[]\n }\n\n // Non-i18n: always data.json\n const filePath = join(dir, 'data.json')\n const content = await readJson(filePath)\n if (content === null) return []\n return [{ modelId: model.id, locale: null, filePath, kind: model.kind }]\n}\n\nasync function mapDocumentFiles(\n dir: string,\n model: ModelDefinition,\n config: ContentrainConfig,\n): Promise<ContentFileRef[]> {\n const strategy = getLocaleStrategy(model)\n\n if (!model.i18n) {\n // Non-i18n documents: {dir}/{slug}.md\n const files = (await readDir(dir)).filter(f => f.endsWith('.md'))\n return files.map(f => ({\n modelId: model.id,\n locale: null,\n filePath: join(dir, f),\n kind: 'document' as const,\n slug: f.replace('.md', ''),\n }))\n }\n\n // i18n documents — resolve per locale in parallel\n const allLocaleRefs = await Promise.all(\n config.locales.supported.map(locale => mapDocumentLocale(dir, model, locale, strategy)),\n )\n return allLocaleRefs.flat()\n}\n\nasync function mapDocumentLocale(\n dir: string,\n model: ModelDefinition,\n locale: string,\n strategy: LocaleStrategy,\n): Promise<ContentFileRef[]> {\n if (strategy === 'file') {\n // Each slug is a directory: {dir}/{slug}/{locale}.md\n const entries = await readDir(dir)\n const results = await Promise.all(\n entries.map(async (entry) => {\n const filePath = join(dir, entry, `${locale}.md`)\n const content = await readText(filePath)\n if (content === null) return null\n return { modelId: model.id, locale, filePath, kind: 'document', slug: entry } as ContentFileRef\n }),\n )\n return results.filter(Boolean) as ContentFileRef[]\n }\n\n if (strategy === 'directory') {\n // {dir}/{locale}/{slug}.md\n const localeDir = join(dir, locale)\n const files = (await readDir(localeDir)).filter(f => f.endsWith('.md'))\n return files.map(f => ({\n modelId: model.id,\n locale,\n filePath: join(localeDir, f),\n kind: 'document' as const,\n slug: f.replace('.md', ''),\n }))\n }\n\n if (strategy === 'suffix') {\n // {dir}/{slug}.{locale}.md\n const suffix = `.${locale}.md`\n const files = (await readDir(dir)).filter(f => f.endsWith(suffix))\n return files.map(f => ({\n modelId: model.id,\n locale,\n filePath: join(dir, f),\n kind: 'document' as const,\n slug: f.slice(0, -suffix.length),\n }))\n }\n\n return []\n}\n","// Frontmatter parsing shared by the client generator and the Astro loader.\n//\n// Both read the same `.contentrain` documents, so the parser lives here rather\n// than being replicated: a divergence would mean the generated client and the\n// Astro collection disagree about the same file's fields.\n\nimport type { ModelDefinition } from '@contentrain/types'\n\n// Field types that map to `string` in the generated types — their frontmatter\n// values must NOT be numerically coerced (e.g. a string SKU \"007\").\nconst STRING_LIKE_TYPES = new Set([\n 'string', 'text', 'email', 'url', 'slug', 'color', 'phone', 'code', 'icon',\n 'markdown', 'richtext', 'date', 'datetime', 'image', 'video', 'file',\n 'select', 'relation',\n])\n\nexport function stringLikeFieldKeys(model: ModelDefinition): Set<string> {\n const keys = new Set<string>()\n if (model.fields) {\n for (const [name, field] of Object.entries(model.fields)) {\n if (STRING_LIKE_TYPES.has(field.type)) keys.add(name)\n }\n }\n return keys\n}\n\n// Minimal frontmatter parser (replicated from MCP pattern)\nexport function parseFrontmatter(text: string, stringKeys: Set<string> = new Set()): { frontmatter: Record<string, unknown>; body: string } {\n const normalized = text.replace(/\\r\\n/g, '\\n')\n const match = normalized.match(/^---\\n([\\s\\S]*?)\\n---\\n?([\\s\\S]*)$/)\n if (!match) return { frontmatter: {}, body: normalized }\n\n const fmStr = match[1]!\n const body = match[2]!.trim()\n const frontmatter: Record<string, unknown> = {}\n\n // Stack-based parser that handles nested objects and arrays\n const lines = fmStr.split('\\n')\n const stack: Array<{ obj: Record<string, unknown>; indent: number }> = [{ obj: frontmatter, indent: -1 }]\n\n for (const line of lines) {\n // Skip empty lines\n if (line.trim() === '') continue\n\n // Array item\n const arrayMatch = line.match(/^(\\s*)-\\s+(.*)$/)\n if (arrayMatch) {\n const arrIndent = arrayMatch[1]!.length\n const value = arrayMatch[2]!.trim()\n // Find the parent that owns this array\n while (stack.length > 1 && stack[stack.length - 1]!.indent >= arrIndent) {\n stack.pop()\n }\n const parent = stack[stack.length - 1]!.obj\n const lastKey = Object.keys(parent).pop()\n if (lastKey && Array.isArray(parent[lastKey])) {\n (parent[lastKey] as unknown[]).push(parseValue(value))\n }\n continue\n }\n\n // Key-value pair\n const kvMatch = line.match(/^(\\s*)([\\w][\\w.-]*)\\s*:\\s*(.*)$/)\n if (!kvMatch) continue\n\n const kvIndent = kvMatch[1]!.length\n const key = kvMatch[2]!\n const rawValue = kvMatch[3]!.trim()\n\n // Pop stack to find correct parent based on indentation\n while (stack.length > 1 && stack[stack.length - 1]!.indent >= kvIndent) {\n stack.pop()\n }\n const current = stack[stack.length - 1]!.obj\n\n if (rawValue === '') {\n // Could be nested object or array — peek next line\n const nextLineIdx = lines.indexOf(line) + 1\n const nextLine = nextLineIdx < lines.length ? lines[nextLineIdx]! : ''\n if (nextLine.trim().startsWith('-')) {\n current[key] = []\n } else {\n const nested: Record<string, unknown> = {}\n current[key] = nested\n stack.push({ obj: nested, indent: kvIndent })\n }\n continue\n }\n\n if (rawValue.startsWith('[') && rawValue.endsWith(']')) {\n current[key] = rawValue.slice(1, -1).split(',').map(s => s.trim()).filter(Boolean)\n continue\n }\n\n // Only top-level keys are matched against the model's declared field types.\n const forceString = current === frontmatter && stringKeys.has(key)\n current[key] = parseValue(rawValue, forceString)\n }\n\n return { frontmatter, body }\n}\n\nfunction parseValue(raw: string, forceString = false): unknown {\n const isQuoted = (raw.startsWith('\"') && raw.endsWith('\"')) || (raw.startsWith(\"'\") && raw.endsWith(\"'\"))\n const unquoted = isQuoted ? raw.slice(1, -1) : raw\n if (forceString) return unquoted\n if (isQuoted) return unquoted\n if (raw === 'true') return true\n if (raw === 'false') return false\n if (raw === 'null') return null\n if (/^-?\\d+$/.test(raw)) return parseInt(raw, 10)\n if (/^-?\\d+\\.\\d+$/.test(raw)) return parseFloat(raw)\n return raw\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
let node_path = require("node:path");
|
|
2
|
+
let node_fs_promises = require("node:fs/promises");
|
|
3
|
+
//#region src/generator/utils.ts
|
|
4
|
+
async function readJson(filePath) {
|
|
5
|
+
try {
|
|
6
|
+
const raw = await (0, node_fs_promises.readFile)(filePath, "utf-8");
|
|
7
|
+
return JSON.parse(raw);
|
|
8
|
+
} catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
async function readDir(dirPath) {
|
|
13
|
+
try {
|
|
14
|
+
return await (0, node_fs_promises.readdir)(dirPath);
|
|
15
|
+
} catch {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async function readText(filePath) {
|
|
20
|
+
try {
|
|
21
|
+
return await (0, node_fs_promises.readFile)(filePath, "utf-8");
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function writeText(filePath, content) {
|
|
27
|
+
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(filePath), { recursive: true });
|
|
28
|
+
await (0, node_fs_promises.writeFile)(filePath, content, "utf-8");
|
|
29
|
+
}
|
|
30
|
+
function contentrainDir(projectRoot) {
|
|
31
|
+
return (0, node_path.join)(projectRoot, ".contentrain");
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/generator/config-reader.ts
|
|
35
|
+
async function readProjectManifest(projectRoot) {
|
|
36
|
+
const crDir = contentrainDir(projectRoot);
|
|
37
|
+
const rawConfig = await readJson((0, node_path.join)(crDir, "config.json"));
|
|
38
|
+
const config = {
|
|
39
|
+
version: rawConfig?.version ?? 1,
|
|
40
|
+
stack: rawConfig?.stack ?? "other",
|
|
41
|
+
workflow: rawConfig?.workflow ?? "auto-merge",
|
|
42
|
+
locales: {
|
|
43
|
+
default: rawConfig?.locales?.default ?? "en",
|
|
44
|
+
supported: rawConfig?.locales?.supported ?? [rawConfig?.locales?.default ?? "en"]
|
|
45
|
+
},
|
|
46
|
+
domains: rawConfig?.domains ?? [],
|
|
47
|
+
repository: rawConfig?.repository,
|
|
48
|
+
assets_path: rawConfig?.assets_path,
|
|
49
|
+
cdn: rawConfig?.cdn,
|
|
50
|
+
branchRetention: rawConfig?.branchRetention
|
|
51
|
+
};
|
|
52
|
+
const modelsDir = (0, node_path.join)(crDir, "models");
|
|
53
|
+
const modelFiles = (await readDir(modelsDir)).filter((f) => f.endsWith(".json"));
|
|
54
|
+
const models = (await Promise.all(modelFiles.map((file) => readJson((0, node_path.join)(modelsDir, file))))).filter((m) => m != null && Boolean(m.id)).toSorted((a, b) => a.id.localeCompare(b.id, "en"));
|
|
55
|
+
return {
|
|
56
|
+
config,
|
|
57
|
+
models,
|
|
58
|
+
contentFiles: (await Promise.all(models.map((model) => mapContentFiles(projectRoot, model, config)))).flat()
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function getContentDir(projectRoot, model) {
|
|
62
|
+
if (model.content_path) return (0, node_path.join)(projectRoot, model.content_path);
|
|
63
|
+
return (0, node_path.join)(contentrainDir(projectRoot), "content", model.domain, model.id);
|
|
64
|
+
}
|
|
65
|
+
function getLocaleStrategy(model) {
|
|
66
|
+
return model.locale_strategy ?? "file";
|
|
67
|
+
}
|
|
68
|
+
function jsonFilePath(dir, model, locale) {
|
|
69
|
+
switch (getLocaleStrategy(model)) {
|
|
70
|
+
case "suffix": return (0, node_path.join)(dir, `${model.id}.${locale}.json`);
|
|
71
|
+
case "directory": return (0, node_path.join)(dir, locale, `${model.id}.json`);
|
|
72
|
+
case "none": return (0, node_path.join)(dir, `${model.id}.json`);
|
|
73
|
+
default: return (0, node_path.join)(dir, `${locale}.json`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function mapContentFiles(projectRoot, model, config) {
|
|
77
|
+
const dir = getContentDir(projectRoot, model);
|
|
78
|
+
if (model.kind === "document") return mapDocumentFiles(dir, model, config);
|
|
79
|
+
if (model.i18n) return (await Promise.all(config.locales.supported.map(async (locale) => {
|
|
80
|
+
const filePath = jsonFilePath(dir, model, locale);
|
|
81
|
+
if (await readJson(filePath) === null) return null;
|
|
82
|
+
return {
|
|
83
|
+
modelId: model.id,
|
|
84
|
+
locale,
|
|
85
|
+
filePath,
|
|
86
|
+
kind: model.kind
|
|
87
|
+
};
|
|
88
|
+
}))).filter(Boolean);
|
|
89
|
+
const filePath = (0, node_path.join)(dir, "data.json");
|
|
90
|
+
if (await readJson(filePath) === null) return [];
|
|
91
|
+
return [{
|
|
92
|
+
modelId: model.id,
|
|
93
|
+
locale: null,
|
|
94
|
+
filePath,
|
|
95
|
+
kind: model.kind
|
|
96
|
+
}];
|
|
97
|
+
}
|
|
98
|
+
async function mapDocumentFiles(dir, model, config) {
|
|
99
|
+
const strategy = getLocaleStrategy(model);
|
|
100
|
+
if (!model.i18n) return (await readDir(dir)).filter((f) => f.endsWith(".md")).map((f) => ({
|
|
101
|
+
modelId: model.id,
|
|
102
|
+
locale: null,
|
|
103
|
+
filePath: (0, node_path.join)(dir, f),
|
|
104
|
+
kind: "document",
|
|
105
|
+
slug: f.replace(".md", "")
|
|
106
|
+
}));
|
|
107
|
+
return (await Promise.all(config.locales.supported.map((locale) => mapDocumentLocale(dir, model, locale, strategy)))).flat();
|
|
108
|
+
}
|
|
109
|
+
async function mapDocumentLocale(dir, model, locale, strategy) {
|
|
110
|
+
if (strategy === "file") {
|
|
111
|
+
const entries = await readDir(dir);
|
|
112
|
+
return (await Promise.all(entries.map(async (entry) => {
|
|
113
|
+
const filePath = (0, node_path.join)(dir, entry, `${locale}.md`);
|
|
114
|
+
if (await readText(filePath) === null) return null;
|
|
115
|
+
return {
|
|
116
|
+
modelId: model.id,
|
|
117
|
+
locale,
|
|
118
|
+
filePath,
|
|
119
|
+
kind: "document",
|
|
120
|
+
slug: entry
|
|
121
|
+
};
|
|
122
|
+
}))).filter(Boolean);
|
|
123
|
+
}
|
|
124
|
+
if (strategy === "directory") {
|
|
125
|
+
const localeDir = (0, node_path.join)(dir, locale);
|
|
126
|
+
return (await readDir(localeDir)).filter((f) => f.endsWith(".md")).map((f) => ({
|
|
127
|
+
modelId: model.id,
|
|
128
|
+
locale,
|
|
129
|
+
filePath: (0, node_path.join)(localeDir, f),
|
|
130
|
+
kind: "document",
|
|
131
|
+
slug: f.replace(".md", "")
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
if (strategy === "suffix") {
|
|
135
|
+
const suffix = `.${locale}.md`;
|
|
136
|
+
return (await readDir(dir)).filter((f) => f.endsWith(suffix)).map((f) => ({
|
|
137
|
+
modelId: model.id,
|
|
138
|
+
locale,
|
|
139
|
+
filePath: (0, node_path.join)(dir, f),
|
|
140
|
+
kind: "document",
|
|
141
|
+
slug: f.slice(0, -suffix.length)
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region src/shared/frontmatter.ts
|
|
148
|
+
const STRING_LIKE_TYPES = new Set([
|
|
149
|
+
"string",
|
|
150
|
+
"text",
|
|
151
|
+
"email",
|
|
152
|
+
"url",
|
|
153
|
+
"slug",
|
|
154
|
+
"color",
|
|
155
|
+
"phone",
|
|
156
|
+
"code",
|
|
157
|
+
"icon",
|
|
158
|
+
"markdown",
|
|
159
|
+
"richtext",
|
|
160
|
+
"date",
|
|
161
|
+
"datetime",
|
|
162
|
+
"image",
|
|
163
|
+
"video",
|
|
164
|
+
"file",
|
|
165
|
+
"select",
|
|
166
|
+
"relation"
|
|
167
|
+
]);
|
|
168
|
+
function stringLikeFieldKeys(model) {
|
|
169
|
+
const keys = /* @__PURE__ */ new Set();
|
|
170
|
+
if (model.fields) {
|
|
171
|
+
for (const [name, field] of Object.entries(model.fields)) if (STRING_LIKE_TYPES.has(field.type)) keys.add(name);
|
|
172
|
+
}
|
|
173
|
+
return keys;
|
|
174
|
+
}
|
|
175
|
+
function parseFrontmatter(text, stringKeys = /* @__PURE__ */ new Set()) {
|
|
176
|
+
const normalized = text.replace(/\r\n/g, "\n");
|
|
177
|
+
const match = normalized.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
178
|
+
if (!match) return {
|
|
179
|
+
frontmatter: {},
|
|
180
|
+
body: normalized
|
|
181
|
+
};
|
|
182
|
+
const fmStr = match[1];
|
|
183
|
+
const body = match[2].trim();
|
|
184
|
+
const frontmatter = {};
|
|
185
|
+
const lines = fmStr.split("\n");
|
|
186
|
+
const stack = [{
|
|
187
|
+
obj: frontmatter,
|
|
188
|
+
indent: -1
|
|
189
|
+
}];
|
|
190
|
+
for (const line of lines) {
|
|
191
|
+
if (line.trim() === "") continue;
|
|
192
|
+
const arrayMatch = line.match(/^(\s*)-\s+(.*)$/);
|
|
193
|
+
if (arrayMatch) {
|
|
194
|
+
const arrIndent = arrayMatch[1].length;
|
|
195
|
+
const value = arrayMatch[2].trim();
|
|
196
|
+
while (stack.length > 1 && stack[stack.length - 1].indent >= arrIndent) stack.pop();
|
|
197
|
+
const parent = stack[stack.length - 1].obj;
|
|
198
|
+
const lastKey = Object.keys(parent).pop();
|
|
199
|
+
if (lastKey && Array.isArray(parent[lastKey])) parent[lastKey].push(parseValue(value));
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const kvMatch = line.match(/^(\s*)([\w][\w.-]*)\s*:\s*(.*)$/);
|
|
203
|
+
if (!kvMatch) continue;
|
|
204
|
+
const kvIndent = kvMatch[1].length;
|
|
205
|
+
const key = kvMatch[2];
|
|
206
|
+
const rawValue = kvMatch[3].trim();
|
|
207
|
+
while (stack.length > 1 && stack[stack.length - 1].indent >= kvIndent) stack.pop();
|
|
208
|
+
const current = stack[stack.length - 1].obj;
|
|
209
|
+
if (rawValue === "") {
|
|
210
|
+
const nextLineIdx = lines.indexOf(line) + 1;
|
|
211
|
+
if ((nextLineIdx < lines.length ? lines[nextLineIdx] : "").trim().startsWith("-")) current[key] = [];
|
|
212
|
+
else {
|
|
213
|
+
const nested = {};
|
|
214
|
+
current[key] = nested;
|
|
215
|
+
stack.push({
|
|
216
|
+
obj: nested,
|
|
217
|
+
indent: kvIndent
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (rawValue.startsWith("[") && rawValue.endsWith("]")) {
|
|
223
|
+
current[key] = rawValue.slice(1, -1).split(",").map((s) => s.trim()).filter(Boolean);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
current[key] = parseValue(rawValue, current === frontmatter && stringKeys.has(key));
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
frontmatter,
|
|
230
|
+
body
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function parseValue(raw, forceString = false) {
|
|
234
|
+
const isQuoted = raw.startsWith("\"") && raw.endsWith("\"") || raw.startsWith("'") && raw.endsWith("'");
|
|
235
|
+
const unquoted = isQuoted ? raw.slice(1, -1) : raw;
|
|
236
|
+
if (forceString) return unquoted;
|
|
237
|
+
if (isQuoted) return unquoted;
|
|
238
|
+
if (raw === "true") return true;
|
|
239
|
+
if (raw === "false") return false;
|
|
240
|
+
if (raw === "null") return null;
|
|
241
|
+
if (/^-?\d+$/.test(raw)) return parseInt(raw, 10);
|
|
242
|
+
if (/^-?\d+\.\d+$/.test(raw)) return parseFloat(raw);
|
|
243
|
+
return raw;
|
|
244
|
+
}
|
|
245
|
+
//#endregion
|
|
246
|
+
Object.defineProperty(exports, "parseFrontmatter", {
|
|
247
|
+
enumerable: true,
|
|
248
|
+
get: function() {
|
|
249
|
+
return parseFrontmatter;
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
Object.defineProperty(exports, "readDir", {
|
|
253
|
+
enumerable: true,
|
|
254
|
+
get: function() {
|
|
255
|
+
return readDir;
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
Object.defineProperty(exports, "readJson", {
|
|
259
|
+
enumerable: true,
|
|
260
|
+
get: function() {
|
|
261
|
+
return readJson;
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
Object.defineProperty(exports, "readProjectManifest", {
|
|
265
|
+
enumerable: true,
|
|
266
|
+
get: function() {
|
|
267
|
+
return readProjectManifest;
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
Object.defineProperty(exports, "readText", {
|
|
271
|
+
enumerable: true,
|
|
272
|
+
get: function() {
|
|
273
|
+
return readText;
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
Object.defineProperty(exports, "stringLikeFieldKeys", {
|
|
277
|
+
enumerable: true,
|
|
278
|
+
get: function() {
|
|
279
|
+
return stringLikeFieldKeys;
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
Object.defineProperty(exports, "writeText", {
|
|
283
|
+
enumerable: true,
|
|
284
|
+
get: function() {
|
|
285
|
+
return writeText;
|
|
286
|
+
}
|
|
287
|
+
});
|