@contentrain/query 3.2.0 → 5.0.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/LICENSE +21 -21
- package/README.md +246 -268
- package/dist/cli.cjs +78 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +81 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/generate-C5Qz8qKt.mjs +855 -0
- package/dist/generate-C5Qz8qKt.mjs.map +1 -0
- package/dist/generate-CPKYh6ZU.cjs +858 -0
- package/dist/generator/generate.cjs +3 -0
- package/dist/generator/generate.d.cts +14 -0
- package/dist/generator/generate.d.cts.map +1 -0
- package/dist/generator/generate.d.mts +14 -0
- package/dist/generator/generate.d.mts.map +1 -0
- package/dist/generator/generate.mjs +2 -0
- package/dist/index.cjs +356 -0
- package/dist/index.d.cts +108 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +107 -290
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +343 -859
- package/dist/index.mjs.map +1 -1
- package/package.json +46 -29
- package/dist/index.d.ts +0 -291
- package/dist/index.js +0 -872
- package/dist/index.js.map +0 -1
|
@@ -0,0 +1,858 @@
|
|
|
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
|
+
branchRetention: rawConfig?.branchRetention
|
|
50
|
+
};
|
|
51
|
+
const modelsDir = (0, node_path.join)(crDir, "models");
|
|
52
|
+
const modelFiles = (await readDir(modelsDir)).filter((f) => f.endsWith(".json"));
|
|
53
|
+
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"));
|
|
54
|
+
return {
|
|
55
|
+
config,
|
|
56
|
+
models,
|
|
57
|
+
contentFiles: (await Promise.all(models.map((model) => mapContentFiles(projectRoot, model, config)))).flat()
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function getContentDir(projectRoot, model) {
|
|
61
|
+
if (model.content_path) return (0, node_path.join)(projectRoot, model.content_path);
|
|
62
|
+
return (0, node_path.join)(contentrainDir(projectRoot), "content", model.domain, model.id);
|
|
63
|
+
}
|
|
64
|
+
function getLocaleStrategy(model) {
|
|
65
|
+
return model.locale_strategy ?? "file";
|
|
66
|
+
}
|
|
67
|
+
function jsonFilePath(dir, model, locale) {
|
|
68
|
+
switch (getLocaleStrategy(model)) {
|
|
69
|
+
case "suffix": return (0, node_path.join)(dir, `${model.id}.${locale}.json`);
|
|
70
|
+
case "directory": return (0, node_path.join)(dir, locale, `${model.id}.json`);
|
|
71
|
+
case "none": return (0, node_path.join)(dir, `${model.id}.json`);
|
|
72
|
+
default: return (0, node_path.join)(dir, `${locale}.json`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function mapContentFiles(projectRoot, model, config) {
|
|
76
|
+
const dir = getContentDir(projectRoot, model);
|
|
77
|
+
if (model.kind === "document") return mapDocumentFiles(dir, model, config);
|
|
78
|
+
if (model.i18n) return (await Promise.all(config.locales.supported.map(async (locale) => {
|
|
79
|
+
const filePath = jsonFilePath(dir, model, locale);
|
|
80
|
+
if (await readJson(filePath) === null) return null;
|
|
81
|
+
return {
|
|
82
|
+
modelId: model.id,
|
|
83
|
+
locale,
|
|
84
|
+
filePath,
|
|
85
|
+
kind: model.kind
|
|
86
|
+
};
|
|
87
|
+
}))).filter(Boolean);
|
|
88
|
+
const filePath = (0, node_path.join)(dir, "data.json");
|
|
89
|
+
if (await readJson(filePath) === null) return [];
|
|
90
|
+
return [{
|
|
91
|
+
modelId: model.id,
|
|
92
|
+
locale: null,
|
|
93
|
+
filePath,
|
|
94
|
+
kind: model.kind
|
|
95
|
+
}];
|
|
96
|
+
}
|
|
97
|
+
async function mapDocumentFiles(dir, model, config) {
|
|
98
|
+
const strategy = getLocaleStrategy(model);
|
|
99
|
+
if (!model.i18n) return (await readDir(dir)).filter((f) => f.endsWith(".md")).map((f) => ({
|
|
100
|
+
modelId: model.id,
|
|
101
|
+
locale: null,
|
|
102
|
+
filePath: (0, node_path.join)(dir, f),
|
|
103
|
+
kind: "document",
|
|
104
|
+
slug: f.replace(".md", "")
|
|
105
|
+
}));
|
|
106
|
+
return (await Promise.all(config.locales.supported.map((locale) => mapDocumentLocale(dir, model, locale, strategy)))).flat();
|
|
107
|
+
}
|
|
108
|
+
async function mapDocumentLocale(dir, model, locale, strategy) {
|
|
109
|
+
if (strategy === "file") {
|
|
110
|
+
const entries = await readDir(dir);
|
|
111
|
+
return (await Promise.all(entries.map(async (entry) => {
|
|
112
|
+
const filePath = (0, node_path.join)(dir, entry, `${locale}.md`);
|
|
113
|
+
if (await readText(filePath) === null) return null;
|
|
114
|
+
return {
|
|
115
|
+
modelId: model.id,
|
|
116
|
+
locale,
|
|
117
|
+
filePath,
|
|
118
|
+
kind: "document",
|
|
119
|
+
slug: entry
|
|
120
|
+
};
|
|
121
|
+
}))).filter(Boolean);
|
|
122
|
+
}
|
|
123
|
+
if (strategy === "directory") {
|
|
124
|
+
const localeDir = (0, node_path.join)(dir, locale);
|
|
125
|
+
return (await readDir(localeDir)).filter((f) => f.endsWith(".md")).map((f) => ({
|
|
126
|
+
modelId: model.id,
|
|
127
|
+
locale,
|
|
128
|
+
filePath: (0, node_path.join)(localeDir, f),
|
|
129
|
+
kind: "document",
|
|
130
|
+
slug: f.replace(".md", "")
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
if (strategy === "suffix") {
|
|
134
|
+
const suffix = `.${locale}.md`;
|
|
135
|
+
return (await readDir(dir)).filter((f) => f.endsWith(suffix)).map((f) => ({
|
|
136
|
+
modelId: model.id,
|
|
137
|
+
locale,
|
|
138
|
+
filePath: (0, node_path.join)(dir, f),
|
|
139
|
+
kind: "document",
|
|
140
|
+
slug: f.slice(0, -suffix.length)
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
//#endregion
|
|
146
|
+
//#region src/generator/type-emitter.ts
|
|
147
|
+
function emitTypes(models) {
|
|
148
|
+
const lines = [
|
|
149
|
+
"/* eslint-disable */",
|
|
150
|
+
"/* oxlint-disable */",
|
|
151
|
+
"// Auto-generated by @contentrain/query — do not edit manually",
|
|
152
|
+
"// Re-generate with: npx contentrain-query generate",
|
|
153
|
+
"",
|
|
154
|
+
"export type ContentStatus = 'draft' | 'published' | 'in_review' | 'rejected' | 'archived'",
|
|
155
|
+
""
|
|
156
|
+
];
|
|
157
|
+
for (const model of models) {
|
|
158
|
+
if (model.kind === "dictionary") lines.push(`export type ${kebabToPascal(model.id)} = Record<string, string>`);
|
|
159
|
+
else {
|
|
160
|
+
lines.push(`export interface ${kebabToPascal(model.id)} {`);
|
|
161
|
+
if (model.kind === "collection") lines.push(" id: string");
|
|
162
|
+
if (model.kind === "document") {
|
|
163
|
+
lines.push(" slug: string");
|
|
164
|
+
lines.push(" content: string");
|
|
165
|
+
}
|
|
166
|
+
if (model.fields) for (const [name, field] of Object.entries(model.fields)) {
|
|
167
|
+
const tsType = fieldToTS(field);
|
|
168
|
+
const optional = field.required ? "" : "?";
|
|
169
|
+
lines.push(` ${name}${optional}: ${tsType}`);
|
|
170
|
+
}
|
|
171
|
+
lines.push("}");
|
|
172
|
+
}
|
|
173
|
+
lines.push("");
|
|
174
|
+
}
|
|
175
|
+
lines.push(`export interface QueryBuilder<T> {
|
|
176
|
+
locale(lang: string): QueryBuilder<T>
|
|
177
|
+
where<K extends keyof T>(field: K, value: T[K]): QueryBuilder<T>
|
|
178
|
+
sort<K extends keyof T>(field: K, order?: 'asc' | 'desc'): QueryBuilder<T>
|
|
179
|
+
limit(n: number): QueryBuilder<T>
|
|
180
|
+
offset(n: number): QueryBuilder<T>
|
|
181
|
+
include(...fields: string[]): QueryBuilder<T>
|
|
182
|
+
first(): T | undefined
|
|
183
|
+
all(): T[]
|
|
184
|
+
}`);
|
|
185
|
+
lines.push("");
|
|
186
|
+
lines.push(`export interface SingletonAccessor<T> {
|
|
187
|
+
locale(lang: string): SingletonAccessor<T>
|
|
188
|
+
include(...fields: string[]): SingletonAccessor<T>
|
|
189
|
+
get(): T
|
|
190
|
+
}`);
|
|
191
|
+
lines.push("");
|
|
192
|
+
lines.push(`export interface DictionaryAccessor {
|
|
193
|
+
locale(lang: string): DictionaryAccessor
|
|
194
|
+
get(): Record<string, string>
|
|
195
|
+
get(key: string): string | undefined
|
|
196
|
+
get(key: string, params: Record<string, string | number>): string
|
|
197
|
+
}`);
|
|
198
|
+
lines.push("");
|
|
199
|
+
lines.push(`export interface DocumentQuery<T> {
|
|
200
|
+
locale(lang: string): DocumentQuery<T>
|
|
201
|
+
where<K extends keyof T>(field: K, value: T[K]): DocumentQuery<T>
|
|
202
|
+
include(...fields: string[]): DocumentQuery<T>
|
|
203
|
+
bySlug(slug: string): T | undefined
|
|
204
|
+
first(): T | undefined
|
|
205
|
+
all(): T[]
|
|
206
|
+
}`);
|
|
207
|
+
lines.push("");
|
|
208
|
+
const collections = models.filter((m) => m.kind === "collection");
|
|
209
|
+
const singletons = models.filter((m) => m.kind === "singleton");
|
|
210
|
+
const dictionaries = models.filter((m) => m.kind === "dictionary");
|
|
211
|
+
const documents = models.filter((m) => m.kind === "document");
|
|
212
|
+
for (const m of collections) lines.push(`export declare function query(model: '${m.id}'): QueryBuilder<${kebabToPascal(m.id)}>`);
|
|
213
|
+
lines.push("export declare function query(model: string): QueryBuilder<Record<string, unknown>>");
|
|
214
|
+
lines.push("");
|
|
215
|
+
for (const m of singletons) lines.push(`export declare function singleton(model: '${m.id}'): SingletonAccessor<${kebabToPascal(m.id)}>`);
|
|
216
|
+
lines.push("export declare function singleton(model: string): SingletonAccessor<Record<string, unknown>>");
|
|
217
|
+
lines.push("");
|
|
218
|
+
for (const m of dictionaries) lines.push(`export declare function dictionary(model: '${m.id}'): DictionaryAccessor`);
|
|
219
|
+
lines.push("export declare function dictionary(model: string): DictionaryAccessor");
|
|
220
|
+
lines.push("");
|
|
221
|
+
for (const m of documents) lines.push(`export declare function document(model: '${m.id}'): DocumentQuery<${kebabToPascal(m.id)}>`);
|
|
222
|
+
lines.push("export declare function document(model: string): DocumentQuery<Record<string, unknown>>");
|
|
223
|
+
lines.push("");
|
|
224
|
+
lines.push("export interface ContentrainClient {");
|
|
225
|
+
for (const m of collections) lines.push(` query(model: '${m.id}'): QueryBuilder<${kebabToPascal(m.id)}>`);
|
|
226
|
+
lines.push(" query(model: string): QueryBuilder<Record<string, unknown>>");
|
|
227
|
+
for (const m of singletons) lines.push(` singleton(model: '${m.id}'): SingletonAccessor<${kebabToPascal(m.id)}>`);
|
|
228
|
+
lines.push(" singleton(model: string): SingletonAccessor<Record<string, unknown>>");
|
|
229
|
+
for (const m of dictionaries) lines.push(` dictionary(model: '${m.id}'): DictionaryAccessor`);
|
|
230
|
+
lines.push(" dictionary(model: string): DictionaryAccessor");
|
|
231
|
+
for (const m of documents) lines.push(` document(model: '${m.id}'): DocumentQuery<${kebabToPascal(m.id)}>`);
|
|
232
|
+
lines.push(" document(model: string): DocumentQuery<Record<string, unknown>>");
|
|
233
|
+
lines.push("}");
|
|
234
|
+
lines.push("");
|
|
235
|
+
lines.push("export declare function createContentrainClient(): ContentrainClient");
|
|
236
|
+
lines.push("");
|
|
237
|
+
return lines.join("\n") + "\n";
|
|
238
|
+
}
|
|
239
|
+
function fieldToTS(field) {
|
|
240
|
+
switch (field.type) {
|
|
241
|
+
case "string":
|
|
242
|
+
case "text":
|
|
243
|
+
case "email":
|
|
244
|
+
case "url":
|
|
245
|
+
case "slug":
|
|
246
|
+
case "color":
|
|
247
|
+
case "phone":
|
|
248
|
+
case "code":
|
|
249
|
+
case "icon":
|
|
250
|
+
case "markdown":
|
|
251
|
+
case "richtext":
|
|
252
|
+
case "date":
|
|
253
|
+
case "datetime":
|
|
254
|
+
case "image":
|
|
255
|
+
case "video":
|
|
256
|
+
case "file":
|
|
257
|
+
case "relation":
|
|
258
|
+
if (Array.isArray(field.model) && field.model.length > 1) return `{ model: ${field.model.map((m) => `'${m}'`).join(" | ")}; ref: string }`;
|
|
259
|
+
return "string";
|
|
260
|
+
case "relations":
|
|
261
|
+
if (Array.isArray(field.model) && field.model.length > 1) return `Array<{ model: ${field.model.map((m) => `'${m}'`).join(" | ")}; ref: string }>`;
|
|
262
|
+
return "string[]";
|
|
263
|
+
case "number":
|
|
264
|
+
case "integer":
|
|
265
|
+
case "decimal":
|
|
266
|
+
case "percent":
|
|
267
|
+
case "rating": return "number";
|
|
268
|
+
case "boolean": return "boolean";
|
|
269
|
+
case "select":
|
|
270
|
+
if (field.options && field.options.length > 0) return field.options.map((o) => `'${o}'`).join(" | ");
|
|
271
|
+
return "string";
|
|
272
|
+
case "array":
|
|
273
|
+
if (field.items) {
|
|
274
|
+
if (typeof field.items === "string") return `${primitiveToTS(field.items)}[]`;
|
|
275
|
+
return `${fieldToTS(field.items)}[]`;
|
|
276
|
+
}
|
|
277
|
+
return "unknown[]";
|
|
278
|
+
case "object":
|
|
279
|
+
if (field.fields) return `{ ${Object.entries(field.fields).map(([k, v]) => `${k}${v.required ? "" : "?"}: ${fieldToTS(v)}`).join("; ")} }`;
|
|
280
|
+
return "Record<string, unknown>";
|
|
281
|
+
default: return "unknown";
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function primitiveToTS(type) {
|
|
285
|
+
if ([
|
|
286
|
+
"string",
|
|
287
|
+
"text",
|
|
288
|
+
"email",
|
|
289
|
+
"url",
|
|
290
|
+
"slug",
|
|
291
|
+
"color",
|
|
292
|
+
"phone",
|
|
293
|
+
"code",
|
|
294
|
+
"icon",
|
|
295
|
+
"markdown",
|
|
296
|
+
"richtext",
|
|
297
|
+
"date",
|
|
298
|
+
"datetime",
|
|
299
|
+
"image",
|
|
300
|
+
"video",
|
|
301
|
+
"file",
|
|
302
|
+
"relation"
|
|
303
|
+
].includes(type)) return "string";
|
|
304
|
+
if ([
|
|
305
|
+
"number",
|
|
306
|
+
"integer",
|
|
307
|
+
"decimal",
|
|
308
|
+
"percent",
|
|
309
|
+
"rating"
|
|
310
|
+
].includes(type)) return "number";
|
|
311
|
+
if (type === "boolean") return "boolean";
|
|
312
|
+
return "unknown";
|
|
313
|
+
}
|
|
314
|
+
function kebabToPascal(s) {
|
|
315
|
+
return s.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
316
|
+
}
|
|
317
|
+
//#endregion
|
|
318
|
+
//#region src/generator/data-emitter.ts
|
|
319
|
+
/** Deterministic JSON: sorted keys, 2-space indent, trailing newline */
|
|
320
|
+
function canonicalStringify(data) {
|
|
321
|
+
return JSON.stringify(data, (_, v) => {
|
|
322
|
+
if (v && typeof v === "object" && !Array.isArray(v)) return Object.keys(v).toSorted().reduce((acc, k) => {
|
|
323
|
+
acc[k] = v[k];
|
|
324
|
+
return acc;
|
|
325
|
+
}, {});
|
|
326
|
+
return v;
|
|
327
|
+
}, 2);
|
|
328
|
+
}
|
|
329
|
+
async function emitDataModules(models, contentFiles) {
|
|
330
|
+
return (await Promise.all(contentFiles.map((ref) => emitSingleModule(ref, models)))).filter((r) => r !== null);
|
|
331
|
+
}
|
|
332
|
+
async function emitSingleModule(ref, models) {
|
|
333
|
+
const model = models.find((m) => m.id === ref.modelId);
|
|
334
|
+
if (!model) return null;
|
|
335
|
+
const localeSuffix = ref.locale ? `.${ref.locale}` : "";
|
|
336
|
+
switch (model.kind) {
|
|
337
|
+
case "collection": {
|
|
338
|
+
const raw = await readJson(ref.filePath);
|
|
339
|
+
if (!raw) return null;
|
|
340
|
+
const entries = Object.entries(raw).toSorted(([a], [b]) => a.localeCompare(b, "en")).map(([id, fields]) => Object.assign({ id }, fields));
|
|
341
|
+
return {
|
|
342
|
+
fileName: `${model.id}${localeSuffix}.mjs`,
|
|
343
|
+
content: `export default ${canonicalStringify(entries)}\n`
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
case "singleton": {
|
|
347
|
+
const raw = await readJson(ref.filePath);
|
|
348
|
+
if (!raw) return null;
|
|
349
|
+
return {
|
|
350
|
+
fileName: `${model.id}${localeSuffix}.mjs`,
|
|
351
|
+
content: `export default ${canonicalStringify(raw)}\n`
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
case "dictionary": {
|
|
355
|
+
const raw = await readJson(ref.filePath);
|
|
356
|
+
if (!raw) return null;
|
|
357
|
+
return {
|
|
358
|
+
fileName: `${model.id}${localeSuffix}.mjs`,
|
|
359
|
+
content: `export default ${canonicalStringify(raw)}\n`
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
case "document": {
|
|
363
|
+
const rawText = await readText(ref.filePath);
|
|
364
|
+
if (!rawText) return null;
|
|
365
|
+
const { frontmatter, body } = parseFrontmatter(rawText);
|
|
366
|
+
const slug = ref.slug ?? model.id;
|
|
367
|
+
const data = {
|
|
368
|
+
slug,
|
|
369
|
+
...frontmatter,
|
|
370
|
+
content: body
|
|
371
|
+
};
|
|
372
|
+
return {
|
|
373
|
+
fileName: `${model.id}--${slug}${localeSuffix}.mjs`,
|
|
374
|
+
content: `export default ${canonicalStringify(data)}\n`
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
default: return null;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function parseFrontmatter(text) {
|
|
381
|
+
const normalized = text.replace(/\r\n/g, "\n");
|
|
382
|
+
const match = normalized.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
383
|
+
if (!match) return {
|
|
384
|
+
frontmatter: {},
|
|
385
|
+
body: normalized
|
|
386
|
+
};
|
|
387
|
+
const fmStr = match[1];
|
|
388
|
+
const body = match[2].trim();
|
|
389
|
+
const frontmatter = {};
|
|
390
|
+
const lines = fmStr.split("\n");
|
|
391
|
+
const stack = [{
|
|
392
|
+
obj: frontmatter,
|
|
393
|
+
indent: -1
|
|
394
|
+
}];
|
|
395
|
+
for (const line of lines) {
|
|
396
|
+
if (line.trim() === "") continue;
|
|
397
|
+
const arrayMatch = line.match(/^(\s*)-\s+(.*)$/);
|
|
398
|
+
if (arrayMatch) {
|
|
399
|
+
const arrIndent = arrayMatch[1].length;
|
|
400
|
+
const value = arrayMatch[2].trim();
|
|
401
|
+
while (stack.length > 1 && stack[stack.length - 1].indent >= arrIndent) stack.pop();
|
|
402
|
+
const parent = stack[stack.length - 1].obj;
|
|
403
|
+
const lastKey = Object.keys(parent).pop();
|
|
404
|
+
if (lastKey && Array.isArray(parent[lastKey])) parent[lastKey].push(parseValue(value));
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
const kvMatch = line.match(/^(\s*)([\w][\w.-]*)\s*:\s*(.*)$/);
|
|
408
|
+
if (!kvMatch) continue;
|
|
409
|
+
const kvIndent = kvMatch[1].length;
|
|
410
|
+
const key = kvMatch[2];
|
|
411
|
+
const rawValue = kvMatch[3].trim();
|
|
412
|
+
while (stack.length > 1 && stack[stack.length - 1].indent >= kvIndent) stack.pop();
|
|
413
|
+
const current = stack[stack.length - 1].obj;
|
|
414
|
+
if (rawValue === "") {
|
|
415
|
+
const nextLineIdx = lines.indexOf(line) + 1;
|
|
416
|
+
if ((nextLineIdx < lines.length ? lines[nextLineIdx] : "").trim().startsWith("-")) current[key] = [];
|
|
417
|
+
else {
|
|
418
|
+
const nested = {};
|
|
419
|
+
current[key] = nested;
|
|
420
|
+
stack.push({
|
|
421
|
+
obj: nested,
|
|
422
|
+
indent: kvIndent
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (rawValue.startsWith("[") && rawValue.endsWith("]")) {
|
|
428
|
+
current[key] = rawValue.slice(1, -1).split(",").map((s) => s.trim()).filter(Boolean);
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
current[key] = parseValue(rawValue);
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
frontmatter,
|
|
435
|
+
body
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
function parseValue(raw) {
|
|
439
|
+
if (raw === "true") return true;
|
|
440
|
+
if (raw === "false") return false;
|
|
441
|
+
if (raw === "null") return null;
|
|
442
|
+
if (/^-?\d+$/.test(raw)) return parseInt(raw, 10);
|
|
443
|
+
if (/^-?\d+\.\d+$/.test(raw)) return parseFloat(raw);
|
|
444
|
+
if (raw.startsWith("\"") && raw.endsWith("\"") || raw.startsWith("'") && raw.endsWith("'")) return raw.slice(1, -1);
|
|
445
|
+
return raw;
|
|
446
|
+
}
|
|
447
|
+
//#endregion
|
|
448
|
+
//#region src/generator/runtime-emitter.ts
|
|
449
|
+
function emitRuntimeModule(models, dataModules, defaultLocale) {
|
|
450
|
+
const lines = [
|
|
451
|
+
"/* eslint-disable */",
|
|
452
|
+
"/* oxlint-disable */",
|
|
453
|
+
"// Auto-generated by @contentrain/query — do not edit manually",
|
|
454
|
+
""
|
|
455
|
+
];
|
|
456
|
+
if (defaultLocale) {
|
|
457
|
+
lines.push(`const _defaultLocale = '${defaultLocale}'`);
|
|
458
|
+
lines.push("");
|
|
459
|
+
}
|
|
460
|
+
for (const dm of dataModules) {
|
|
461
|
+
const varName = fileNameToVar(dm.fileName);
|
|
462
|
+
lines.push(`import ${varName} from './data/${dm.fileName}'`);
|
|
463
|
+
}
|
|
464
|
+
lines.push("");
|
|
465
|
+
lines.push(RUNTIME_CODE);
|
|
466
|
+
lines.push("");
|
|
467
|
+
lines.push("// ─── Data Registry ───");
|
|
468
|
+
lines.push("");
|
|
469
|
+
const modelDataMap = /* @__PURE__ */ new Map();
|
|
470
|
+
for (const dm of dataModules) {
|
|
471
|
+
const { modelId, locale } = parseDataFileName(dm.fileName);
|
|
472
|
+
if (!modelDataMap.has(modelId)) modelDataMap.set(modelId, []);
|
|
473
|
+
modelDataMap.get(modelId).push({
|
|
474
|
+
varName: fileNameToVar(dm.fileName),
|
|
475
|
+
locale
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
const collections = models.filter((m) => m.kind === "collection");
|
|
479
|
+
const singletons = models.filter((m) => m.kind === "singleton");
|
|
480
|
+
const dictionaries = models.filter((m) => m.kind === "dictionary");
|
|
481
|
+
const documents = models.filter((m) => m.kind === "document");
|
|
482
|
+
const relationMetaMap = buildRelationMetaMap(models);
|
|
483
|
+
if (relationMetaMap.size > 0) {
|
|
484
|
+
lines.push("// ─── Relation Resolver ───");
|
|
485
|
+
lines.push("");
|
|
486
|
+
lines.push("function _resolveEntry(model, id, locale) {");
|
|
487
|
+
lines.push(" const localeKey = locale ?? '_default'");
|
|
488
|
+
if (collections.length > 0) {
|
|
489
|
+
lines.push(" const colData = _collectionRegistry[model]?.get(localeKey)");
|
|
490
|
+
lines.push(" if (colData) { const e = colData.find(x => x.id === id); if (e) return e; }");
|
|
491
|
+
}
|
|
492
|
+
if (documents.length > 0) {
|
|
493
|
+
lines.push(" const docData = _documentRegistry[model]?.get(localeKey)");
|
|
494
|
+
lines.push(" if (docData) { const e = docData.find(x => x.slug === id); if (e) return e; }");
|
|
495
|
+
}
|
|
496
|
+
lines.push(" return undefined");
|
|
497
|
+
lines.push("}");
|
|
498
|
+
lines.push("");
|
|
499
|
+
lines.push("const _relationMeta = {");
|
|
500
|
+
for (const [modelId, meta] of relationMetaMap) {
|
|
501
|
+
lines.push(` '${modelId}': {`);
|
|
502
|
+
for (const [field, info] of Object.entries(meta)) {
|
|
503
|
+
const targetStr = Array.isArray(info.target) ? `[${info.target.map((t) => `'${t}'`).join(", ")}]` : `'${info.target}'`;
|
|
504
|
+
lines.push(` '${field}': { target: ${targetStr}, multi: ${info.multi} },`);
|
|
505
|
+
}
|
|
506
|
+
lines.push(" },");
|
|
507
|
+
}
|
|
508
|
+
lines.push("}");
|
|
509
|
+
lines.push("");
|
|
510
|
+
}
|
|
511
|
+
if (collections.length > 0) {
|
|
512
|
+
lines.push("const _collectionRegistry = {");
|
|
513
|
+
for (const m of collections) {
|
|
514
|
+
const entries = modelDataMap.get(m.id) ?? [];
|
|
515
|
+
lines.push(` '${m.id}': new Map([`);
|
|
516
|
+
for (const e of entries) {
|
|
517
|
+
const localeKey = e.locale ?? "_default";
|
|
518
|
+
lines.push(` ['${localeKey}', ${e.varName}],`);
|
|
519
|
+
}
|
|
520
|
+
lines.push(" ]),");
|
|
521
|
+
}
|
|
522
|
+
lines.push("}");
|
|
523
|
+
lines.push("");
|
|
524
|
+
lines.push("export function query(model) {");
|
|
525
|
+
lines.push(" const data = _collectionRegistry[model]");
|
|
526
|
+
lines.push(" if (!data) throw new Error(`Unknown collection model: \"${model}\"`)");
|
|
527
|
+
if (relationMetaMap.size > 0 && defaultLocale) lines.push(" return new QueryBuilder(data, _relationMeta[model], _resolveEntry, _defaultLocale)");
|
|
528
|
+
else if (relationMetaMap.size > 0) lines.push(" return new QueryBuilder(data, _relationMeta[model], _resolveEntry)");
|
|
529
|
+
else if (defaultLocale) lines.push(" return new QueryBuilder(data, undefined, undefined, _defaultLocale)");
|
|
530
|
+
else lines.push(" return new QueryBuilder(data)");
|
|
531
|
+
lines.push("}");
|
|
532
|
+
lines.push("");
|
|
533
|
+
}
|
|
534
|
+
if (singletons.length > 0) {
|
|
535
|
+
lines.push("const _singletonRegistry = {");
|
|
536
|
+
for (const m of singletons) {
|
|
537
|
+
const entries = modelDataMap.get(m.id) ?? [];
|
|
538
|
+
lines.push(` '${m.id}': new Map([`);
|
|
539
|
+
for (const e of entries) {
|
|
540
|
+
const localeKey = e.locale ?? "_default";
|
|
541
|
+
lines.push(` ['${localeKey}', ${e.varName}],`);
|
|
542
|
+
}
|
|
543
|
+
lines.push(" ]),");
|
|
544
|
+
}
|
|
545
|
+
lines.push("}");
|
|
546
|
+
lines.push("");
|
|
547
|
+
lines.push("export function singleton(model) {");
|
|
548
|
+
lines.push(" const data = _singletonRegistry[model]");
|
|
549
|
+
lines.push(" if (!data) throw new Error(`Unknown singleton model: \"${model}\"`)");
|
|
550
|
+
if (relationMetaMap.size > 0 && defaultLocale) lines.push(" return new SingletonAccessor(data, _defaultLocale, _relationMeta[model], _resolveEntry)");
|
|
551
|
+
else if (relationMetaMap.size > 0) lines.push(" return new SingletonAccessor(data, undefined, _relationMeta[model], _resolveEntry)");
|
|
552
|
+
else if (defaultLocale) lines.push(" return new SingletonAccessor(data, _defaultLocale)");
|
|
553
|
+
else lines.push(" return new SingletonAccessor(data)");
|
|
554
|
+
lines.push("}");
|
|
555
|
+
lines.push("");
|
|
556
|
+
}
|
|
557
|
+
if (dictionaries.length > 0) {
|
|
558
|
+
lines.push("const _dictionaryRegistry = {");
|
|
559
|
+
for (const m of dictionaries) {
|
|
560
|
+
const entries = modelDataMap.get(m.id) ?? [];
|
|
561
|
+
lines.push(` '${m.id}': new Map([`);
|
|
562
|
+
for (const e of entries) {
|
|
563
|
+
const localeKey = e.locale ?? "_default";
|
|
564
|
+
lines.push(` ['${localeKey}', ${e.varName}],`);
|
|
565
|
+
}
|
|
566
|
+
lines.push(" ]),");
|
|
567
|
+
}
|
|
568
|
+
lines.push("}");
|
|
569
|
+
lines.push("");
|
|
570
|
+
lines.push("export function dictionary(model) {");
|
|
571
|
+
lines.push(" const data = _dictionaryRegistry[model]");
|
|
572
|
+
lines.push(" if (!data) throw new Error(`Unknown dictionary model: \"${model}\"`)");
|
|
573
|
+
lines.push(defaultLocale ? " return new DictionaryAccessor(data, _defaultLocale)" : " return new DictionaryAccessor(data)");
|
|
574
|
+
lines.push("}");
|
|
575
|
+
lines.push("");
|
|
576
|
+
}
|
|
577
|
+
if (documents.length > 0) {
|
|
578
|
+
for (const m of documents) {
|
|
579
|
+
const entries = modelDataMap.get(m.id) ?? [];
|
|
580
|
+
const byLocale = /* @__PURE__ */ new Map();
|
|
581
|
+
for (const e of entries) {
|
|
582
|
+
const localeKey = e.locale ?? "_default";
|
|
583
|
+
if (!byLocale.has(localeKey)) byLocale.set(localeKey, []);
|
|
584
|
+
byLocale.get(localeKey).push(e.varName);
|
|
585
|
+
}
|
|
586
|
+
lines.push(`const _doc_${m.id.replace(/-/g, "_")} = new Map([`);
|
|
587
|
+
for (const [localeKey, varNames] of byLocale) lines.push(` ['${localeKey}', [${varNames.join(", ")}]],`);
|
|
588
|
+
lines.push("])");
|
|
589
|
+
}
|
|
590
|
+
lines.push("");
|
|
591
|
+
lines.push("const _documentRegistry = {");
|
|
592
|
+
for (const m of documents) lines.push(` '${m.id}': _doc_${m.id.replace(/-/g, "_")},`);
|
|
593
|
+
lines.push("}");
|
|
594
|
+
lines.push("");
|
|
595
|
+
lines.push("export function document(model) {");
|
|
596
|
+
lines.push(" const data = _documentRegistry[model]");
|
|
597
|
+
lines.push(" if (!data) throw new Error(`Unknown document model: \"${model}\"`)");
|
|
598
|
+
if (relationMetaMap.size > 0 && defaultLocale) lines.push(" return new DocumentQuery(data, _relationMeta[model], _resolveEntry, _defaultLocale)");
|
|
599
|
+
else if (relationMetaMap.size > 0) lines.push(" return new DocumentQuery(data, _relationMeta[model], _resolveEntry)");
|
|
600
|
+
else if (defaultLocale) lines.push(" return new DocumentQuery(data, undefined, undefined, _defaultLocale)");
|
|
601
|
+
else lines.push(" return new DocumentQuery(data)");
|
|
602
|
+
lines.push("}");
|
|
603
|
+
lines.push("");
|
|
604
|
+
}
|
|
605
|
+
return lines.join("\n") + "\n";
|
|
606
|
+
}
|
|
607
|
+
function emitCjsWrapper(models) {
|
|
608
|
+
const exports = [];
|
|
609
|
+
if (models.some((m) => m.kind === "collection")) exports.push("query");
|
|
610
|
+
if (models.some((m) => m.kind === "singleton")) exports.push("singleton");
|
|
611
|
+
if (models.some((m) => m.kind === "dictionary")) exports.push("dictionary");
|
|
612
|
+
if (models.some((m) => m.kind === "document")) exports.push("document");
|
|
613
|
+
return `/* eslint-disable */
|
|
614
|
+
/* oxlint-disable */
|
|
615
|
+
// Auto-generated CJS proxy — delegates to ESM via dynamic import()
|
|
616
|
+
// Sync usage: const client = require('#contentrain'); client.query('model')
|
|
617
|
+
// Async usage: const client = await require('#contentrain').init()
|
|
618
|
+
'use strict'
|
|
619
|
+
let _mod = null
|
|
620
|
+
let _promise = null
|
|
621
|
+
|
|
622
|
+
function _ensure() {
|
|
623
|
+
if (_mod) return _mod
|
|
624
|
+
throw new Error(
|
|
625
|
+
'Contentrain client not initialized. Call .init() first, then access exports.\\n'
|
|
626
|
+
+ 'Example: require("#contentrain").init().then(c => c.query("model"))'
|
|
627
|
+
)
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
module.exports.init = function() {
|
|
631
|
+
if (!_promise) _promise = import('./index.mjs').then(function(m) {
|
|
632
|
+
_mod = m
|
|
633
|
+
${exports.map((e) => ` module.exports.${e} = m.${e}`).join("\n")}
|
|
634
|
+
return module.exports
|
|
635
|
+
})
|
|
636
|
+
return _promise
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Eagerly start loading so subsequent sync calls work after first await
|
|
640
|
+
_promise = import('./index.mjs').then(function(m) {
|
|
641
|
+
_mod = m
|
|
642
|
+
${exports.map((e) => ` module.exports.${e} = m.${e}`).join("\n")}
|
|
643
|
+
return module.exports
|
|
644
|
+
}).catch(function() { _promise = null; /* retry on next init() call */ })
|
|
645
|
+
`;
|
|
646
|
+
}
|
|
647
|
+
function buildRelationMetaMap(models) {
|
|
648
|
+
const result = /* @__PURE__ */ new Map();
|
|
649
|
+
for (const model of models) {
|
|
650
|
+
if (!model.fields) continue;
|
|
651
|
+
const meta = {};
|
|
652
|
+
let hasRelations = false;
|
|
653
|
+
for (const [fieldName, field] of Object.entries(model.fields)) if (field.type === "relation" && field.model) {
|
|
654
|
+
meta[fieldName] = {
|
|
655
|
+
target: field.model,
|
|
656
|
+
multi: false
|
|
657
|
+
};
|
|
658
|
+
hasRelations = true;
|
|
659
|
+
} else if (field.type === "relations" && field.model) {
|
|
660
|
+
meta[fieldName] = {
|
|
661
|
+
target: field.model,
|
|
662
|
+
multi: true
|
|
663
|
+
};
|
|
664
|
+
hasRelations = true;
|
|
665
|
+
}
|
|
666
|
+
if (hasRelations) result.set(model.id, meta);
|
|
667
|
+
}
|
|
668
|
+
return result;
|
|
669
|
+
}
|
|
670
|
+
function fileNameToVar(fileName) {
|
|
671
|
+
return "_" + fileName.replace(".mjs", "").split(/[-.]/).map((p, i) => i === 0 ? p : p.charAt(0).toUpperCase() + p.slice(1)).join("");
|
|
672
|
+
}
|
|
673
|
+
function parseDataFileName(fileName) {
|
|
674
|
+
const base = fileName.replace(".mjs", "");
|
|
675
|
+
let nameForLocale = base;
|
|
676
|
+
if (base.includes("--")) nameForLocale = base;
|
|
677
|
+
const parts = nameForLocale.split(".");
|
|
678
|
+
let locale = null;
|
|
679
|
+
let nameWithoutLocale = nameForLocale;
|
|
680
|
+
if (parts.length >= 2) {
|
|
681
|
+
const possibleLocale = parts[parts.length - 1];
|
|
682
|
+
if (possibleLocale.length === 2 || possibleLocale.includes("-")) {
|
|
683
|
+
locale = possibleLocale;
|
|
684
|
+
nameWithoutLocale = parts.slice(0, -1).join(".");
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return {
|
|
688
|
+
modelId: nameWithoutLocale.includes("--") ? nameWithoutLocale.split("--")[0] : nameWithoutLocale,
|
|
689
|
+
locale
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
const RUNTIME_CODE = `
|
|
693
|
+
// ─── Runtime Classes ───
|
|
694
|
+
|
|
695
|
+
class QueryBuilder {
|
|
696
|
+
constructor(data, relationMeta, resolver, defaultLocale) { this._data = data; this._filters = []; this._sortField = null; this._sortOrder = 'asc'; this._limit = null; this._offset = 0; this._locale = null; this._includes = []; this._relationMeta = relationMeta || {}; this._resolver = resolver || null; this._defaultLocale = defaultLocale || null; }
|
|
697
|
+
locale(lang) { this._locale = lang; return this; }
|
|
698
|
+
where(field, value) { this._filters.push(item => { const v = item[field]; return Array.isArray(v) ? v.includes(value) : v === value; }); return this; }
|
|
699
|
+
sort(field, order = 'asc') { this._sortField = field; this._sortOrder = order; return this; }
|
|
700
|
+
limit(n) { this._limit = n; return this; }
|
|
701
|
+
offset(n) { this._offset = n; return this; }
|
|
702
|
+
include(...fields) { this._includes.push(...fields); return this; }
|
|
703
|
+
all() {
|
|
704
|
+
let items; if (this._locale) { items = [...(this._data.get(this._locale) ?? [])]; } else if (this._defaultLocale && this._data.has(this._defaultLocale)) { items = [...this._data.get(this._defaultLocale)]; } else { items = [...(this._data.get(this._data.keys().next().value) ?? [])]; }
|
|
705
|
+
for (const f of this._filters) items = items.filter(f);
|
|
706
|
+
if (this._sortField) { const sf = this._sortField; const d = this._sortOrder === 'asc' ? 1 : -1; items.sort((a, b) => { const va = a[sf], vb = b[sf]; if (va == null && vb == null) return 0; if (va == null) return d; if (vb == null) return -d; return va < vb ? -d : va > vb ? d : 0; }); }
|
|
707
|
+
if (this._offset > 0 || this._limit !== null) { const end = this._limit !== null ? this._offset + this._limit : undefined; items = items.slice(this._offset, end); }
|
|
708
|
+
if (this._includes.length > 0 && this._resolver) { items = items.map(item => this._resolveIncludes(item)); }
|
|
709
|
+
return items;
|
|
710
|
+
}
|
|
711
|
+
first() { return this.all()[0]; }
|
|
712
|
+
_resolveIncludes(item) {
|
|
713
|
+
const resolved = { ...item };
|
|
714
|
+
for (const field of this._includes) {
|
|
715
|
+
const meta = this._relationMeta[field]; if (!meta) continue;
|
|
716
|
+
const targets = Array.isArray(meta.target) ? meta.target : [meta.target];
|
|
717
|
+
if (meta.multi) { const ids = item[field]; if (Array.isArray(ids)) { resolved[field] = ids.map(id => { if (typeof id === 'string') { for (const t of targets) { const r = this._resolver(t, id, this._locale); if (r) return r; } return id; } if (typeof id === 'object' && id !== null && 'model' in id && 'ref' in id) { const r = this._resolver(id.model, id.ref, this._locale); if (r) return r; } return id; }); } }
|
|
718
|
+
else { const id = item[field]; if (typeof id === 'string') { for (const t of targets) { const r = this._resolver(t, id, this._locale); if (r) { resolved[field] = r; break; } } } else if (typeof id === 'object' && id !== null && 'model' in id && 'ref' in id) { const r = this._resolver(id.model, id.ref, this._locale); if (r) resolved[field] = r; } }
|
|
719
|
+
}
|
|
720
|
+
return resolved;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
class SingletonAccessor {
|
|
725
|
+
constructor(data, defaultLocale, relationMeta, resolveEntry) { this._data = data; this._locale = null; this._defaultLocale = defaultLocale || null; this._includes = []; this._relationMeta = relationMeta || {}; this._resolver = resolveEntry; }
|
|
726
|
+
locale(lang) { this._locale = lang; return this; }
|
|
727
|
+
include(...fields) { this._includes.push(...fields); return this; }
|
|
728
|
+
get() {
|
|
729
|
+
const locale = this._locale || this._defaultLocale;
|
|
730
|
+
let d = locale ? this._data.get(locale) : undefined;
|
|
731
|
+
if (!d) { const key = this._data.keys().next().value; d = key !== undefined ? this._data.get(key) : undefined; }
|
|
732
|
+
if (!d) throw new Error('No data available');
|
|
733
|
+
if (this._includes.length > 0 && this._resolver) return this._resolveIncludes(d, locale || 'en');
|
|
734
|
+
return d;
|
|
735
|
+
}
|
|
736
|
+
_resolveIncludes(item, locale) {
|
|
737
|
+
const resolved = { ...item };
|
|
738
|
+
for (const field of this._includes) {
|
|
739
|
+
const meta = this._relationMeta[field]; if (!meta) continue;
|
|
740
|
+
const targets = Array.isArray(meta.target) ? meta.target : [meta.target];
|
|
741
|
+
if (meta.multi) { const ids = item[field]; if (Array.isArray(ids)) { resolved[field] = ids.map(id => { if (typeof id === 'string') { for (const t of targets) { const r = this._resolver(t, id, locale); if (r) return r; } return id; } if (typeof id === 'object' && id !== null && 'model' in id && 'ref' in id) { const r = this._resolver(id.model, id.ref, locale); if (r) return r; } return id; }); } }
|
|
742
|
+
else { const id = item[field]; if (typeof id === 'string') { for (const t of targets) { const r = this._resolver(t, id, locale); if (r) { resolved[field] = r; break; } } } else if (typeof id === 'object' && id !== null && 'model' in id && 'ref' in id) { const r = this._resolver(id.model, id.ref, locale); if (r) resolved[field] = r; } }
|
|
743
|
+
}
|
|
744
|
+
return resolved;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
class DictionaryAccessor {
|
|
749
|
+
constructor(data, defaultLocale) { this._data = data; this._locale = null; this._defaultLocale = defaultLocale || null; }
|
|
750
|
+
locale(lang) { this._locale = lang; return this; }
|
|
751
|
+
get(key, params) {
|
|
752
|
+
let dict; if (this._locale) { dict = this._data.get(this._locale) ?? {}; } else if (this._defaultLocale && this._data.has(this._defaultLocale)) { dict = this._data.get(this._defaultLocale); } else { const loc = this._data.keys().next().value; dict = this._data.get(loc) ?? {}; }
|
|
753
|
+
if (key === undefined) return dict;
|
|
754
|
+
const val = dict[key];
|
|
755
|
+
if (val === undefined) return undefined;
|
|
756
|
+
if (params) return val.replace(/{(w+)}/g, (m, k) => { const v = params[k]; return v !== undefined ? String(v) : m; });
|
|
757
|
+
return val;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
class DocumentQuery {
|
|
762
|
+
constructor(data, relationMeta, resolver, defaultLocale) { this._data = data; this._filters = []; this._locale = null; this._includes = []; this._relationMeta = relationMeta || {}; this._resolver = resolver || null; this._defaultLocale = defaultLocale || null; }
|
|
763
|
+
locale(lang) { this._locale = lang; return this; }
|
|
764
|
+
where(field, value) { this._filters.push(item => item[field] === value); return this; }
|
|
765
|
+
include(...fields) { this._includes.push(...fields); return this; }
|
|
766
|
+
bySlug(slug) {
|
|
767
|
+
const items = this._resolveData(); const item = items.find(x => x.slug === slug);
|
|
768
|
+
if (item && this._includes.length > 0 && this._resolver) return this._resolveIncludes(item);
|
|
769
|
+
return item;
|
|
770
|
+
}
|
|
771
|
+
first() { return this.all()[0]; }
|
|
772
|
+
all() { let items = this._resolveData(); for (const f of this._filters) items = items.filter(f); if (this._includes.length > 0 && this._resolver) { items = items.map(item => this._resolveIncludes(item)); } return items; }
|
|
773
|
+
_resolveData() { let key; if (this._locale) { key = this._locale; } else if (this._defaultLocale && this._data.has(this._defaultLocale)) { key = this._defaultLocale; } else { key = this._data.keys().next().value; } return [...(this._data.get(key) ?? [])]; }
|
|
774
|
+
_resolveIncludes(item) {
|
|
775
|
+
const resolved = { ...item };
|
|
776
|
+
for (const field of this._includes) {
|
|
777
|
+
const meta = this._relationMeta[field]; if (!meta) continue;
|
|
778
|
+
const targets = Array.isArray(meta.target) ? meta.target : [meta.target];
|
|
779
|
+
if (meta.multi) { const ids = item[field]; if (Array.isArray(ids)) { resolved[field] = ids.map(id => { if (typeof id === 'string') { for (const t of targets) { const r = this._resolver(t, id, this._locale); if (r) return r; } return id; } if (typeof id === 'object' && id !== null && 'model' in id && 'ref' in id) { const r = this._resolver(id.model, id.ref, this._locale); if (r) return r; } return id; }); } }
|
|
780
|
+
else { const id = item[field]; if (typeof id === 'string') { for (const t of targets) { const r = this._resolver(t, id, this._locale); if (r) { resolved[field] = r; break; } } } else if (typeof id === 'object' && id !== null && 'model' in id && 'ref' in id) { const r = this._resolver(id.model, id.ref, this._locale); if (r) resolved[field] = r; } }
|
|
781
|
+
}
|
|
782
|
+
return resolved;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
`.trim();
|
|
786
|
+
//#endregion
|
|
787
|
+
//#region src/generator/package-json.ts
|
|
788
|
+
const IMPORTS_CONFIG = {
|
|
789
|
+
"#contentrain": {
|
|
790
|
+
types: "./.contentrain/client/index.d.ts",
|
|
791
|
+
import: "./.contentrain/client/index.mjs",
|
|
792
|
+
require: "./.contentrain/client/index.cjs",
|
|
793
|
+
default: "./.contentrain/client/index.mjs"
|
|
794
|
+
},
|
|
795
|
+
"#contentrain/*": {
|
|
796
|
+
types: "./.contentrain/client/*.d.ts",
|
|
797
|
+
import: "./.contentrain/client/*.mjs",
|
|
798
|
+
require: "./.contentrain/client/*.cjs",
|
|
799
|
+
default: "./.contentrain/client/*.mjs"
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
async function injectImports(projectRoot) {
|
|
803
|
+
const pkgPath = (0, node_path.join)(projectRoot, "package.json");
|
|
804
|
+
const pkg = await readJson(pkgPath);
|
|
805
|
+
if (!pkg) return false;
|
|
806
|
+
const existing = pkg["imports"] ?? {};
|
|
807
|
+
const updated = {
|
|
808
|
+
...existing,
|
|
809
|
+
...IMPORTS_CONFIG
|
|
810
|
+
};
|
|
811
|
+
if (JSON.stringify(existing["#contentrain"]) === JSON.stringify(IMPORTS_CONFIG["#contentrain"]) && JSON.stringify(existing["#contentrain/*"]) === JSON.stringify(IMPORTS_CONFIG["#contentrain/*"])) return false;
|
|
812
|
+
pkg["imports"] = updated;
|
|
813
|
+
await writeText(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
814
|
+
return true;
|
|
815
|
+
}
|
|
816
|
+
//#endregion
|
|
817
|
+
//#region src/generator/generate.ts
|
|
818
|
+
async function generate(options) {
|
|
819
|
+
const { projectRoot } = options;
|
|
820
|
+
const clientDir = (0, node_path.join)(projectRoot, ".contentrain", "client");
|
|
821
|
+
const dataDir = (0, node_path.join)(clientDir, "data");
|
|
822
|
+
const manifest = await readProjectManifest(projectRoot);
|
|
823
|
+
const dataModules = await emitDataModules(manifest.models, manifest.contentFiles);
|
|
824
|
+
const typesContent = emitTypes(manifest.models);
|
|
825
|
+
const runtimeContent = emitRuntimeModule(manifest.models, dataModules, manifest.config.locales.default);
|
|
826
|
+
const cjsContent = emitCjsWrapper(manifest.models);
|
|
827
|
+
const newFileNames = new Set(dataModules.map((dm) => dm.fileName));
|
|
828
|
+
try {
|
|
829
|
+
const existing = await readDir(dataDir);
|
|
830
|
+
await Promise.all(existing.filter((f) => !newFileNames.has(f)).map((f) => (0, node_fs_promises.rm)((0, node_path.join)(dataDir, f), { force: true })));
|
|
831
|
+
} catch {}
|
|
832
|
+
const dataFileNames = dataModules.map((dm) => `data/${dm.fileName}`);
|
|
833
|
+
await Promise.all([
|
|
834
|
+
writeText((0, node_path.join)(clientDir, "index.d.ts"), typesContent),
|
|
835
|
+
writeText((0, node_path.join)(clientDir, "index.mjs"), runtimeContent),
|
|
836
|
+
writeText((0, node_path.join)(clientDir, "index.cjs"), cjsContent),
|
|
837
|
+
...dataModules.map((dm) => writeText((0, node_path.join)(dataDir, dm.fileName), dm.content))
|
|
838
|
+
]);
|
|
839
|
+
const packageJsonUpdated = await injectImports(projectRoot);
|
|
840
|
+
return {
|
|
841
|
+
generatedFiles: [
|
|
842
|
+
"index.d.ts",
|
|
843
|
+
...dataFileNames,
|
|
844
|
+
"index.mjs",
|
|
845
|
+
"index.cjs"
|
|
846
|
+
],
|
|
847
|
+
typesCount: manifest.models.length,
|
|
848
|
+
dataModulesCount: dataModules.length,
|
|
849
|
+
packageJsonUpdated
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
//#endregion
|
|
853
|
+
Object.defineProperty(exports, "generate", {
|
|
854
|
+
enumerable: true,
|
|
855
|
+
get: function() {
|
|
856
|
+
return generate;
|
|
857
|
+
}
|
|
858
|
+
});
|