@llm-cms/core 0.0.1 → 0.0.2
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/package.json +1 -1
- package/src/block.ts +103 -0
- package/src/config.ts +9 -2
- package/src/doc-format.ts +32 -0
- package/src/fields.ts +4 -1
- package/src/index.ts +20 -0
- package/src/model.ts +56 -7
- package/src/node/cli.ts +30 -11
- package/src/node/create-llmcms.ts +5 -1
- package/src/node/discover.ts +69 -11
- package/src/node/eval-block.ts +202 -0
- package/src/node/eval-model.ts +143 -0
- package/src/node/fs-loader.ts +11 -4
- package/src/node/generate.ts +47 -11
- package/src/node/index.ts +10 -1
- package/src/path.ts +24 -7
- package/src/sync-schema.ts +3 -1
- package/src/validate.ts +67 -1
- package/src/zod.ts +12 -1
package/src/validate.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// packages/core/src/validate.ts
|
|
2
|
+
import { modelFormat, parseDoc } from "./doc-format";
|
|
2
3
|
import type { Frontmatter } from "./mdx";
|
|
3
4
|
import { parseMdx } from "./mdx";
|
|
4
5
|
import type { ModelSnapshot } from "./model";
|
|
6
|
+
import type { BlockInstance, BlockSnapshot } from "./block";
|
|
5
7
|
import { matchAnyModelPath } from "./path";
|
|
6
8
|
import { expectedTypeFor, frontmatterZod, normalizeFrontmatter } from "./zod";
|
|
7
9
|
|
|
@@ -43,6 +45,42 @@ export function validateFrontmatter(
|
|
|
43
45
|
return { ok: false, errors };
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
function asBlockList(value: unknown): BlockInstance[] {
|
|
49
|
+
if (!Array.isArray(value)) return [];
|
|
50
|
+
return value.filter((item): item is BlockInstance => {
|
|
51
|
+
if (!item || typeof item !== "object") return false;
|
|
52
|
+
const row = item as Record<string, unknown>;
|
|
53
|
+
return typeof row.id === "string" && typeof row.type === "string";
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Validate each block instance against the blocks catalog. */
|
|
58
|
+
export function validateBlocks(
|
|
59
|
+
blocks: unknown,
|
|
60
|
+
catalog: BlockSnapshot[],
|
|
61
|
+
prefix = "blocks",
|
|
62
|
+
): ValidationResult {
|
|
63
|
+
if (!Array.isArray(blocks)) return { ok: true };
|
|
64
|
+
const byName = new Map(catalog.map((b) => [b.name, b]));
|
|
65
|
+
const errors: string[] = [];
|
|
66
|
+
asBlockList(blocks).forEach((block, i) => {
|
|
67
|
+
const def = byName.get(block.type);
|
|
68
|
+
if (!def) {
|
|
69
|
+
errors.push(`${prefix}[${i}]: unknown type "${block.type}"`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const props = block.props && typeof block.props === "object" ? block.props : {};
|
|
73
|
+
const result = validateFrontmatter({ fields: def.fields }, props);
|
|
74
|
+
if (!result.ok) {
|
|
75
|
+
for (const err of result.errors) {
|
|
76
|
+
errors.push(`${prefix}[${i}].${err}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
if (errors.length > 0) return { ok: false, errors };
|
|
81
|
+
return { ok: true };
|
|
82
|
+
}
|
|
83
|
+
|
|
46
84
|
/**
|
|
47
85
|
* Parse a raw MDX string and validate its frontmatter. Never throws on
|
|
48
86
|
* malformed frontmatter (an unknown field kind in `model` still throws).
|
|
@@ -61,6 +99,32 @@ export function validateMdxFile(
|
|
|
61
99
|
return validateFrontmatter(model, frontmatter);
|
|
62
100
|
}
|
|
63
101
|
|
|
102
|
+
export function validateDocFile(
|
|
103
|
+
model: Pick<ModelSnapshot, "fields" | "format">,
|
|
104
|
+
raw: string,
|
|
105
|
+
catalog: BlockSnapshot[] = [],
|
|
106
|
+
): ValidationResult {
|
|
107
|
+
const format = modelFormat(model);
|
|
108
|
+
let frontmatter: Frontmatter;
|
|
109
|
+
try {
|
|
110
|
+
frontmatter = parseDoc(raw, format).frontmatter;
|
|
111
|
+
} catch (err) {
|
|
112
|
+
const msg = err instanceof Error ? err.message : "invalid";
|
|
113
|
+
const label = format === "page" ? "JSON" : "frontmatter";
|
|
114
|
+
return { ok: false, errors: [msg.startsWith("JSON:") ? msg : `${label}: ${msg}`] };
|
|
115
|
+
}
|
|
116
|
+
const base = validateFrontmatter(model, frontmatter);
|
|
117
|
+
if (format !== "page") return base;
|
|
118
|
+
const blockField = Object.entries(model.fields).find(([, f]) => f.kind === "blocks");
|
|
119
|
+
if (!blockField) return base;
|
|
120
|
+
const blockResult = validateBlocks(frontmatter[blockField[0]], catalog, blockField[0]);
|
|
121
|
+
if (base.ok && blockResult.ok) return { ok: true };
|
|
122
|
+
return {
|
|
123
|
+
ok: false,
|
|
124
|
+
errors: [...(base.ok ? [] : base.errors), ...(blockResult.ok ? [] : blockResult.errors)],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
64
128
|
export type ContentFileResult =
|
|
65
129
|
| { path: string; type: string; ok: true }
|
|
66
130
|
| { path: string; type: string; ok: false; errors: string[] }
|
|
@@ -70,13 +134,15 @@ export type ContentFileResult =
|
|
|
70
134
|
export function validateContentTree(input: {
|
|
71
135
|
models: ModelSnapshot[];
|
|
72
136
|
files: Array<{ path: string; raw: string }>;
|
|
137
|
+
catalog?: BlockSnapshot[];
|
|
73
138
|
}): ContentFileResult[] {
|
|
139
|
+
const catalog = input.catalog ?? [];
|
|
74
140
|
return input.files.map((file) => {
|
|
75
141
|
const hit = matchAnyModelPath(input.models, file.path);
|
|
76
142
|
if (!hit) return { path: file.path, type: null, ok: true, skipped: true };
|
|
77
143
|
const model = input.models.find((m) => m.type === hit.type);
|
|
78
144
|
if (!model) return { path: file.path, type: null, ok: true, skipped: true };
|
|
79
|
-
const result =
|
|
145
|
+
const result = validateDocFile(model, file.raw, catalog);
|
|
80
146
|
if (result.ok) return { path: file.path, type: model.type, ok: true };
|
|
81
147
|
return { path: file.path, type: model.type, ok: false, errors: result.errors };
|
|
82
148
|
});
|
package/src/zod.ts
CHANGED
|
@@ -5,12 +5,14 @@ import type { Frontmatter } from "./mdx";
|
|
|
5
5
|
import type { ModelSnapshot } from "./model";
|
|
6
6
|
|
|
7
7
|
/** Primitive a field kind is stored as in frontmatter; used in error messages. */
|
|
8
|
-
export function expectedTypeFor(kind: FieldKind): "string" | "number" | "boolean" {
|
|
8
|
+
export function expectedTypeFor(kind: FieldKind): "string" | "number" | "boolean" | "array" {
|
|
9
9
|
switch (kind) {
|
|
10
10
|
case "number":
|
|
11
11
|
return "number";
|
|
12
12
|
case "boolean":
|
|
13
13
|
return "boolean";
|
|
14
|
+
case "blocks":
|
|
15
|
+
return "array";
|
|
14
16
|
case "text":
|
|
15
17
|
case "slug":
|
|
16
18
|
case "locale":
|
|
@@ -25,6 +27,15 @@ export function expectedTypeFor(kind: FieldKind): "string" | "number" | "boolean
|
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
function fieldZod(field: FieldSnapshot): z.ZodType {
|
|
30
|
+
if (field.kind === "blocks") {
|
|
31
|
+
const item = z.object({
|
|
32
|
+
id: z.string().min(1),
|
|
33
|
+
type: z.string().min(1),
|
|
34
|
+
props: z.record(z.string(), z.unknown()).optional().default({}),
|
|
35
|
+
});
|
|
36
|
+
const arr = field.required ? z.array(item).min(1) : z.array(item).optional();
|
|
37
|
+
return arr;
|
|
38
|
+
}
|
|
28
39
|
let base: z.ZodType;
|
|
29
40
|
switch (expectedTypeFor(field.kind)) {
|
|
30
41
|
case "number":
|