forgepress 0.0.0 → 0.0.1

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.
@@ -0,0 +1,53 @@
1
+ import { isRecord, quote } from "./value.mjs";
2
+ function entryKey(collection, id) {
3
+ return `${collection}/${id}`;
4
+ }
5
+ function localized(value, path, translated) {
6
+ if (!translated) return [[path, value]];
7
+ return isRecord(value) ? Object.entries(value).map(([locale, item]) => [[...path, locale], item]) : [];
8
+ }
9
+ function references(field, value, path) {
10
+ if (field.type === "relation" && !field.multiple) return typeof value === "string" ? [{
11
+ path,
12
+ collection: field.collection,
13
+ id: value
14
+ }] : [];
15
+ if (!Array.isArray(value)) return [];
16
+ if (field.type === "relation") return value.flatMap((id, index) => typeof id === "string" ? [{
17
+ path: [...path, index],
18
+ collection: field.collection,
19
+ id
20
+ }] : []);
21
+ if (field.type === "dynamic") return value.flatMap((block, index) => isRecord(block) && typeof block.id === "string" && typeof block.collection === "string" && field.collections.includes(block.collection) ? [{
22
+ path: [...path, index],
23
+ collection: block.collection,
24
+ id: block.id
25
+ }] : []);
26
+ return [];
27
+ }
28
+ function entryReferences(schema, collection, row) {
29
+ const translatable = (schema.locales ?? []).length > 0;
30
+ return Object.entries(schema.collections[collection]?.fields ?? {}).flatMap(([key, field]) => localized(row[key], [key], translatable && field.translate === true).flatMap(([path, value]) => references(field, value, path)));
31
+ }
32
+ function validateReferences(schema, content) {
33
+ const issues = [];
34
+ for (const [collection, entries] of Object.entries(content)) for (const [id, row] of Object.entries(entries)) for (const reference of entryReferences(schema, collection, row)) {
35
+ const target = content[reference.collection]?.[reference.id];
36
+ const name = entryKey(reference.collection, reference.id);
37
+ const field = quote(reference.path[0]);
38
+ if (!target) issues.push({
39
+ collection,
40
+ id,
41
+ path: reference.path,
42
+ message: `Field ${field} references ${name}, which doesn't exist`
43
+ });
44
+ else if (row.status === "published" && target.status !== "published") issues.push({
45
+ collection,
46
+ id,
47
+ path: reference.path,
48
+ message: `Field ${field} references ${name}, which is unpublished; publish it or remove the reference`
49
+ });
50
+ }
51
+ return issues;
52
+ }
53
+ export { entryKey, validateReferences };
@@ -0,0 +1,25 @@
1
+ import { OutputMeta } from "./entry.mjs";
2
+ type LinkKind = 'relation' | 'dynamic';
3
+ export interface OutputEntry extends OutputMeta {
4
+ [field: string]: unknown;
5
+ }
6
+ export interface OutputManifest {
7
+ indexed: string[];
8
+ links: Record<string, LinkKind>;
9
+ entries: OutputEntry[];
10
+ files: Record<string, string>;
11
+ }
12
+ export type OutputCollection = {
13
+ localized: false;
14
+ manifest: string;
15
+ } | {
16
+ localized: true;
17
+ manifests: Record<string, string>;
18
+ };
19
+ export interface OutputIndex {
20
+ version: number;
21
+ commit: string | null;
22
+ dev?: true;
23
+ locales: string[];
24
+ collections: Record<string, OutputCollection>;
25
+ }
@@ -0,0 +1,2 @@
1
+ const OUTPUT_INDEX = "index.json";
2
+ export { OUTPUT_INDEX };
@@ -0,0 +1,600 @@
1
+ import { isRecord, quote } from "./value.mjs";
2
+ import { isCollectionName } from "./output.mjs";
3
+ function formatIssue(issue) {
4
+ return `${issue.file}:${issue.line}:${issue.column} ${issue.message}`;
5
+ }
6
+ var ContentError = class extends Error {
7
+ issues;
8
+ constructor(issues) {
9
+ super(issues.map((issue) => `[forgepress] ${formatIssue(issue)}`).join("\n"));
10
+ this.name = "ContentError";
11
+ this.issues = issues;
12
+ }
13
+ };
14
+ const ID_START = /[$_\p{ID_Start}]/u;
15
+ const ID_CONTINUE = /[$\u200C\u200D\p{ID_Continue}]/u;
16
+ const SPACE = /\s/;
17
+ const DECIMAL = /\d/;
18
+ const HEX = /[\da-f]/i;
19
+ const OCTAL = /[0-7]/;
20
+ const BINARY = /[01]/;
21
+ const HEX_DIGITS = /^[\da-f]+$/i;
22
+ const RADIX = /[box]/i;
23
+ const BREAKS = /* @__PURE__ */ new Set([
24
+ "\n",
25
+ "\r",
26
+ "\u2028",
27
+ "\u2029"
28
+ ]);
29
+ const TYPE_IMPORT = "Only type imports are allowed, e.g. `import type { ForgePressEntry } from 'forgepress'`";
30
+ const ESCAPES = /* @__PURE__ */ new Map([
31
+ ["n", "\n"],
32
+ ["r", "\r"],
33
+ ["t", " "],
34
+ ["b", "\b"],
35
+ ["f", "\f"],
36
+ ["v", "\v"]
37
+ ]);
38
+ function locationAt(text, offset) {
39
+ let line = 1;
40
+ let start = 0;
41
+ for (let position = 0; position < offset; position += 1) {
42
+ const char = text[position];
43
+ if (BREAKS.has(char) && !(char === "\r" && text[position + 1] === "\n")) {
44
+ line += 1;
45
+ start = position + 1;
46
+ }
47
+ }
48
+ return {
49
+ line,
50
+ column: offset - start + 1
51
+ };
52
+ }
53
+ function pathKey(path) {
54
+ return JSON.stringify(path);
55
+ }
56
+ function parseModule(text, file) {
57
+ const offsets = /* @__PURE__ */ new Map();
58
+ let index = text.startsWith("") ? 1 : 0;
59
+ function fail(message, at = index) {
60
+ throw new ContentError([{
61
+ file,
62
+ ...locationAt(text, at),
63
+ message
64
+ }]);
65
+ }
66
+ function char(offset = 0) {
67
+ return text[index + offset] ?? "";
68
+ }
69
+ function skip() {
70
+ while (index < text.length) if (SPACE.test(char())) index += 1;
71
+ else if (text.startsWith("//", index)) while (index < text.length && !BREAKS.has(char())) index += 1;
72
+ else if (text.startsWith("/*", index)) {
73
+ const end = text.indexOf("*/", index + 2);
74
+ if (end === -1) fail("Unterminated comment");
75
+ index = end + 2;
76
+ } else return;
77
+ }
78
+ function width(pattern) {
79
+ const point = text.codePointAt(index);
80
+ if (point === void 0 || !pattern.test(String.fromCodePoint(point))) return 0;
81
+ return point > 65535 ? 2 : 1;
82
+ }
83
+ function identifier() {
84
+ const start = index;
85
+ for (let step = width(ID_START); step > 0; step = width(ID_CONTINUE)) index += step;
86
+ return index > start ? text.slice(start, index) : void 0;
87
+ }
88
+ function word(expected) {
89
+ skip();
90
+ const start = index;
91
+ if (identifier() === expected) return true;
92
+ index = start;
93
+ return false;
94
+ }
95
+ function expectWord(expected, message) {
96
+ if (!word(expected)) fail(message);
97
+ }
98
+ function name(message) {
99
+ skip();
100
+ return identifier() ?? fail(message);
101
+ }
102
+ function list(close, item) {
103
+ index += 1;
104
+ for (skip(); char() !== close; skip()) {
105
+ item();
106
+ skip();
107
+ if (char() === ",") index += 1;
108
+ else if (char() !== close) fail(index < text.length ? `Expected \`,\` or \`${close}\`` : "Unexpected end of file");
109
+ }
110
+ index += 1;
111
+ }
112
+ function hex(count, start) {
113
+ const digits = text.slice(index, index + count);
114
+ if (digits.length < count || !HEX_DIGITS.test(digits)) fail("Invalid escape sequence", start);
115
+ index += count;
116
+ return Number.parseInt(digits, 16);
117
+ }
118
+ function unicode(start) {
119
+ if (char() !== "{") return String.fromCharCode(hex(4, start));
120
+ const end = text.indexOf("}", index);
121
+ const digits = end === -1 ? "" : text.slice(index + 1, end);
122
+ if (!HEX_DIGITS.test(digits) || Number.parseInt(digits, 16) > 1114111) fail("Invalid escape sequence", start);
123
+ index = end + 1;
124
+ return String.fromCodePoint(Number.parseInt(digits, 16));
125
+ }
126
+ function escape(unterminated, opening) {
127
+ const start = index - 1;
128
+ const current = char();
129
+ if (current === "") fail(unterminated, opening);
130
+ index += 1;
131
+ const simple = ESCAPES.get(current);
132
+ if (simple !== void 0) return simple;
133
+ if (current === "\r" && char() === "\n") index += 1;
134
+ if (BREAKS.has(current)) return "";
135
+ if (current === "x") return String.fromCharCode(hex(2, start));
136
+ if (current === "u") return unicode(start);
137
+ if (DECIMAL.test(current) && (current !== "0" || DECIMAL.test(char()))) fail(`\`\\${current}\` is not a valid escape sequence`, start);
138
+ if (current === "0") return "\0";
139
+ const point = text.codePointAt(start + 1);
140
+ index = start + 1 + (point > 65535 ? 2 : 1);
141
+ return String.fromCodePoint(point);
142
+ }
143
+ function string(quote) {
144
+ const start = index;
145
+ let result = "";
146
+ let run = index + 1;
147
+ index = run;
148
+ while (char() !== quote) {
149
+ const current = char();
150
+ if (current === "" || current === "\n" || current === "\r") fail("Unterminated string", start);
151
+ if (current === "\\") {
152
+ result += text.slice(run, index);
153
+ index += 1;
154
+ result += escape("Unterminated string", start);
155
+ run = index;
156
+ } else index += 1;
157
+ }
158
+ result += text.slice(run, index);
159
+ index += 1;
160
+ return result;
161
+ }
162
+ function template() {
163
+ const start = index;
164
+ let result = "";
165
+ let run = index + 1;
166
+ index = run;
167
+ while (char() !== "`") {
168
+ const current = char();
169
+ if (current === "") fail("Unterminated template literal", start);
170
+ if (current === "$" && char(1) === "{") fail("Template literals cannot contain substitutions");
171
+ if (current === "\\" || current === "\r") {
172
+ result += text.slice(run, index);
173
+ index += 1;
174
+ if (current === "\\") result += escape("Unterminated template literal", start);
175
+ else {
176
+ result += "\n";
177
+ if (char() === "\n") index += 1;
178
+ }
179
+ run = index;
180
+ } else index += 1;
181
+ }
182
+ result += text.slice(run, index);
183
+ index += 1;
184
+ return result;
185
+ }
186
+ function digits(pattern, start) {
187
+ const from = index;
188
+ while (pattern.test(char()) || char() === "_") {
189
+ if (char() === "_" && (index === from || !pattern.test(char(1)))) fail("Invalid numeric separator", start);
190
+ index += 1;
191
+ }
192
+ if (index === from) fail("Invalid number", start);
193
+ return text.slice(from, index).replaceAll("_", "");
194
+ }
195
+ function numberStart() {
196
+ return DECIMAL.test(char()) || char() === "." && DECIMAL.test(char(1));
197
+ }
198
+ function number() {
199
+ const start = index;
200
+ let source = "";
201
+ if (char() === "0" && RADIX.test(char(1))) {
202
+ const radix = char(1).toLowerCase();
203
+ index += 2;
204
+ source = `0${radix}${digits(radix === "x" ? HEX : radix === "o" ? OCTAL : BINARY, start)}`;
205
+ } else {
206
+ if (char() === "0" && (DECIMAL.test(char(1)) || char(1) === "_")) fail("Numbers cannot start with a leading zero", start);
207
+ if (char() !== ".") source = digits(DECIMAL, start);
208
+ if (char() === ".") {
209
+ index += 1;
210
+ source += `.${DECIMAL.test(char()) ? digits(DECIMAL, start) : ""}`;
211
+ }
212
+ if (char() === "e" || char() === "E") {
213
+ index += 1;
214
+ const sign = char() === "+" || char() === "-" ? char() : "";
215
+ index += sign.length;
216
+ source += `e${sign}${digits(DECIMAL, start)}`;
217
+ }
218
+ }
219
+ if (char() === "n") fail("BigInt values are not allowed", start);
220
+ if (DECIMAL.test(char()) || width(ID_START) > 0) fail("Invalid number", start);
221
+ const value = Number(source);
222
+ if (!Number.isFinite(value)) fail("Number is out of range", start);
223
+ return value;
224
+ }
225
+ function propertyKey() {
226
+ const start = index;
227
+ const current = char();
228
+ if (current === "[") fail("Computed keys are not allowed");
229
+ if (text.startsWith("...", index)) fail("Spread syntax is not allowed");
230
+ const key = current === "'" || current === "\"" ? string(current) : numberStart() ? String(number()) : identifier();
231
+ if (key === void 0) fail(index < text.length ? "Expected a property name" : "Unexpected end of file");
232
+ if (key === "__proto__") fail("`__proto__` cannot be used as a key", start);
233
+ return key;
234
+ }
235
+ function object(path) {
236
+ const result = {};
237
+ list("}", () => {
238
+ const start = index;
239
+ const key = propertyKey();
240
+ skip();
241
+ if (char() === "," || char() === "}") fail(`Shorthand properties are not allowed; write \`${key}: value\``, start);
242
+ if (char() === "(") fail("Methods are not allowed");
243
+ if (char() !== ":") fail(index < text.length ? `Expected \`:\` after \`${key}\`` : "Unexpected end of file");
244
+ index += 1;
245
+ if (Object.hasOwn(result, key)) fail(`Duplicate key \`${key}\``, start);
246
+ const child = [...path, key];
247
+ offsets.set(pathKey(child), start);
248
+ result[key] = literal(child);
249
+ });
250
+ return result;
251
+ }
252
+ function array(path) {
253
+ const result = [];
254
+ list("]", () => {
255
+ if (char() === ",") fail("Empty array slots are not allowed");
256
+ const child = [...path, result.length];
257
+ offsets.set(pathKey(child), index);
258
+ result.push(literal(child));
259
+ });
260
+ return result;
261
+ }
262
+ function literal(path) {
263
+ skip();
264
+ const start = index;
265
+ const current = char();
266
+ if (current === "{") return object(path);
267
+ if (current === "[") return array(path);
268
+ if (current === "'" || current === "\"") return string(current);
269
+ if (current === "`") return template();
270
+ if (current === "-" || current === "+") {
271
+ index += 1;
272
+ skip();
273
+ if (!numberStart()) fail(`Expected a number after \`${current}\``);
274
+ return current === "-" ? -number() : number();
275
+ }
276
+ if (numberStart()) return number();
277
+ if (text.startsWith("...", index)) fail("Spread syntax is not allowed");
278
+ const found = identifier();
279
+ if (found === "true" || found === "false") return found === "true";
280
+ if (found === "null" || found === "undefined") fail(`\`${found}\` is not supported; leave the value out instead`, start);
281
+ if (found === "NaN" || found === "Infinity") fail(`\`${found}\` cannot be stored`, start);
282
+ if (found !== void 0) fail(`\`${found}\` is not a literal value; variables, calls and expressions are not allowed`, start);
283
+ return fail(index < text.length ? "Expected a literal value" : "Unexpected end of file");
284
+ }
285
+ function typeReference() {
286
+ word("typeof");
287
+ name("Expected a type");
288
+ skip();
289
+ while (char() === ".") {
290
+ index += 1;
291
+ name("Expected a type");
292
+ skip();
293
+ }
294
+ if (char() === "<") list(">", () => {
295
+ const quote = char();
296
+ if (quote === "'" || quote === "\"") string(quote);
297
+ else typeReference();
298
+ });
299
+ }
300
+ function importDeclaration(start) {
301
+ if (!word("type")) fail(TYPE_IMPORT, start);
302
+ skip();
303
+ if (char() === "{") list("}", () => {
304
+ name("Expected an import name");
305
+ if (word("as")) name("Expected an import name");
306
+ });
307
+ else if (char() === "*") {
308
+ index += 1;
309
+ expectWord("as", "Expected `as`");
310
+ name("Expected an import name");
311
+ } else if (name("Expected an import clause") === "from") {
312
+ skip();
313
+ if (char() === "'" || char() === "\"") fail(TYPE_IMPORT, start);
314
+ }
315
+ expectWord("from", "Expected `from`");
316
+ skip();
317
+ if (char() !== "'" && char() !== "\"") fail("Expected a module name");
318
+ string(char());
319
+ }
320
+ function exportDefault() {
321
+ expectWord("default", "Only `export default` is allowed");
322
+ skip();
323
+ offsets.set(pathKey([]), index);
324
+ const value = literal([]);
325
+ if (word("as")) expectWord("const", "Only `as const` is allowed; use `satisfies` to type the value");
326
+ if (word("satisfies")) typeReference();
327
+ return value;
328
+ }
329
+ function statements() {
330
+ let exported = false;
331
+ let value;
332
+ for (skip(); index < text.length; skip()) {
333
+ const start = index;
334
+ if (char() === ";") {
335
+ index += 1;
336
+ continue;
337
+ }
338
+ if (exported) fail("Nothing may follow `export default`");
339
+ const keyword = identifier();
340
+ if (keyword === "import") importDeclaration(start);
341
+ else if (keyword === "export") {
342
+ value = exportDefault();
343
+ exported = true;
344
+ } else fail("Only `import type` and `export default` are allowed", start);
345
+ }
346
+ if (!exported) fail("Missing `export default`");
347
+ return value;
348
+ }
349
+ return {
350
+ value: statements(),
351
+ locate(path) {
352
+ for (let depth = path.length; depth >= 0; depth -= 1) {
353
+ const offset = offsets.get(pathKey(path.slice(0, depth)));
354
+ if (offset !== void 0) return locationAt(text, offset);
355
+ }
356
+ return locationAt(text, 0);
357
+ }
358
+ };
359
+ }
360
+ const META_KEYS = [
361
+ "id",
362
+ "status",
363
+ "createdAt",
364
+ "updatedAt"
365
+ ];
366
+ const dynamic = {
367
+ type: "dynamic",
368
+ label: "Dynamic",
369
+ options: { collections: {
370
+ label: "Collections",
371
+ type: "collections",
372
+ required: true
373
+ } }
374
+ };
375
+ const image = {
376
+ type: "image",
377
+ label: "Image",
378
+ options: { multiple: {
379
+ label: "Multiple",
380
+ type: "boolean"
381
+ } }
382
+ };
383
+ const number = {
384
+ type: "number",
385
+ label: "Number",
386
+ options: {
387
+ min: {
388
+ label: "Minimum",
389
+ type: "number"
390
+ },
391
+ max: {
392
+ label: "Maximum",
393
+ type: "number"
394
+ },
395
+ step: {
396
+ label: "Step",
397
+ type: "number"
398
+ },
399
+ index: {
400
+ label: "Indexed",
401
+ type: "boolean",
402
+ description: "Listed in the collection manifest, so queries can filter and sort on it without loading every entry."
403
+ }
404
+ }
405
+ };
406
+ const relation = {
407
+ type: "relation",
408
+ label: "Relation",
409
+ options: {
410
+ collection: {
411
+ label: "Collection",
412
+ type: "collection",
413
+ required: true
414
+ },
415
+ multiple: {
416
+ label: "Multiple",
417
+ type: "boolean"
418
+ },
419
+ index: {
420
+ label: "Indexed",
421
+ type: "boolean",
422
+ description: "Listed in the collection manifest, so queries can filter and sort on it without loading every entry."
423
+ }
424
+ }
425
+ };
426
+ const richtext = {
427
+ type: "richtext",
428
+ label: "Rich Text",
429
+ options: {}
430
+ };
431
+ const text = {
432
+ type: "text",
433
+ label: "Text",
434
+ options: {
435
+ validation: {
436
+ label: "Validation Pattern",
437
+ type: "text"
438
+ },
439
+ index: {
440
+ label: "Indexed",
441
+ type: "boolean",
442
+ description: "Listed in the collection manifest, so queries can filter and sort on it without loading every entry."
443
+ }
444
+ }
445
+ };
446
+ function compilePattern(validation) {
447
+ try {
448
+ return new RegExp(validation, "u");
449
+ } catch (error) {
450
+ return error;
451
+ }
452
+ }
453
+ const fieldTypes = {
454
+ text,
455
+ richtext,
456
+ number,
457
+ image,
458
+ video: {
459
+ type: "video",
460
+ label: "Video",
461
+ options: { multiple: {
462
+ label: "Multiple",
463
+ type: "boolean"
464
+ } }
465
+ },
466
+ relation,
467
+ dynamic
468
+ };
469
+ const fieldTypeNames = Object.keys(fieldTypes);
470
+ const LOCALE_CODE = /^[a-z][\w-]*$/i;
471
+ const RESERVED_FIELDS = META_KEYS;
472
+ const SCHEMA_KEYS = /* @__PURE__ */ new Set(["collections", "locales"]);
473
+ const COLLECTION_KEYS = /* @__PURE__ */ new Set([
474
+ "label",
475
+ "description",
476
+ "fields"
477
+ ]);
478
+ const BASE_OPTIONS = {
479
+ label: "text",
480
+ description: "text",
481
+ optional: "boolean",
482
+ translate: "boolean"
483
+ };
484
+ const INDEXABLE = fieldTypeNames.filter((type) => "index" in fieldTypes[type].options);
485
+ const KINDS = {
486
+ text: "a string",
487
+ number: "a number",
488
+ boolean: "true or false",
489
+ collection: "a collection name",
490
+ collections: "a list of collection names"
491
+ };
492
+ function isFieldType(type) {
493
+ return typeof type === "string" && fieldTypeNames.includes(type);
494
+ }
495
+ function finite(value) {
496
+ return typeof value === "number" && Number.isFinite(value);
497
+ }
498
+ function fits(kind, value) {
499
+ if (kind === "number") return finite(value);
500
+ if (kind === "boolean") return typeof value === "boolean";
501
+ if (kind === "collections") return Array.isArray(value) && value.every((item) => typeof item === "string");
502
+ return typeof value === "string";
503
+ }
504
+ function optionKinds(type) {
505
+ const options = Object.entries(fieldTypes[type].options).map(([option, spec]) => [option, spec.type]);
506
+ return {
507
+ ...BASE_OPTIONS,
508
+ ...Object.fromEntries(options)
509
+ };
510
+ }
511
+ function checkLocales(report, locales) {
512
+ if (locales === void 0) return [];
513
+ if (!Array.isArray(locales)) {
514
+ report(["locales"], "\"locales\" has to be a list of locale codes");
515
+ return [];
516
+ }
517
+ locales.forEach((locale, index) => {
518
+ if (typeof locale !== "string" || !LOCALE_CODE.test(locale)) report(["locales", index], `${quote(locale)} is not a locale code`);
519
+ else if (locales.indexOf(locale) < index) report(["locales", index], `Locale ${quote(locale)} is listed twice`);
520
+ });
521
+ return locales;
522
+ }
523
+ function checkReferences(context, path, label, targets, listed) {
524
+ targets.forEach((target, index) => {
525
+ const at = listed ? [...path, index] : path;
526
+ if (!context.collections.has(target)) context.report(at, `Field ${quote(label)} references unknown collection ${quote(target)}`);
527
+ else if (targets.indexOf(target) < index) context.report(at, `Field ${quote(label)} lists collection ${quote(target)} twice`);
528
+ });
529
+ }
530
+ function checkOption(context, path, label, option, kind, value) {
531
+ if (!fits(kind, value)) context.report(path, `${quote(option)} of field ${quote(label)} has to be ${KINDS[kind]}`);
532
+ else if (kind === "collection" || kind === "collections") checkReferences(context, path, label, kind === "collection" ? [value] : value, kind === "collections");
533
+ }
534
+ function checkConstraints(report, path, label, field) {
535
+ if (field.type === "text" && typeof field.validation === "string") {
536
+ const pattern = compilePattern(field.validation);
537
+ if (pattern instanceof SyntaxError) report([...path, "validation"], `"validation" of field ${quote(label)} is not a valid regular expression: ${pattern.message.replace(/^Invalid regular expression: /, "")}`);
538
+ }
539
+ if (field.type !== "number") return;
540
+ if (finite(field.step) && field.step <= 0) report([...path, "step"], `"step" of field ${quote(label)} has to be greater than 0`);
541
+ if (finite(field.min) && finite(field.max) && field.min > field.max) report([...path, "min"], `"min" of field ${quote(label)} can't be greater than "max"`);
542
+ }
543
+ function checkField(context, path, collection, key, field) {
544
+ const { report } = context;
545
+ const label = `${collection}.${key}`;
546
+ if (RESERVED_FIELDS.includes(key)) report(path, `Field ${quote(label)} uses ${quote(key)}, which is reserved for entry metadata`);
547
+ if (!isRecord(field)) return report(path, `Field ${quote(label)} has to be an object`);
548
+ if (field.type === void 0) return report(path, `Field ${quote(label)} needs a type`);
549
+ if (!isFieldType(field.type)) return report([...path, "type"], `Field ${quote(label)} has unknown type ${quote(field.type)}; use one of ${fieldTypeNames.join(", ")}`);
550
+ const kinds = optionKinds(field.type);
551
+ for (const [option, value] of Object.entries(field)) {
552
+ const kind = kinds[option];
553
+ if (option === "type") continue;
554
+ if (kind) checkOption(context, [...path, option], label, option, kind, value);
555
+ else if (option === "index") report([...path, option], `Field ${quote(label)} can't be indexed; only ${INDEXABLE.slice(0, -1).join(", ")} and ${INDEXABLE.at(-1)} fields can`);
556
+ else report([...path, option], `Field ${quote(label)} has no option ${quote(option)}`);
557
+ }
558
+ for (const [option, spec] of Object.entries(fieldTypes[field.type].options)) if ("required" in spec && field[option] === void 0) report(path, `Field ${quote(label)} needs ${quote(option)}`);
559
+ checkConstraints(report, path, label, field);
560
+ if (field.translate === true && context.locales.length === 0) report([...path, "translate"], `Field ${quote(label)} is translated, but the schema has no locales`);
561
+ }
562
+ function checkCollection(context, name, collection) {
563
+ const { report } = context;
564
+ const path = ["collections", name];
565
+ if (!isCollectionName(name)) report(path, `Collection ${quote(name)} has to start with a lowercase letter and contain only letters and digits`);
566
+ if (!isRecord(collection)) return report(path, `Collection ${quote(name)} has to be an object`);
567
+ for (const [key, value] of Object.entries(collection)) if (!COLLECTION_KEYS.has(key)) report([...path, key], `Collection ${quote(name)} has no option ${quote(key)}`);
568
+ else if (key !== "fields" && typeof value !== "string") report([...path, key], `${quote(key)} of collection ${quote(name)} has to be a string`);
569
+ if (!isRecord(collection.fields)) return report(collection.fields === void 0 ? path : [...path, "fields"], `Collection ${quote(name)} needs "fields" as an object`);
570
+ for (const [key, field] of Object.entries(collection.fields)) checkField(context, [
571
+ ...path,
572
+ "fields",
573
+ key
574
+ ], name, key, field);
575
+ }
576
+ function validateSchema(schema) {
577
+ const issues = [];
578
+ const report = (path, message) => issues.push({
579
+ path,
580
+ message
581
+ });
582
+ if (!isRecord(schema)) {
583
+ report([], "The schema has to be an object");
584
+ return issues;
585
+ }
586
+ for (const key of Object.keys(schema)) if (!SCHEMA_KEYS.has(key)) report([key], `The schema has no option ${quote(key)}`);
587
+ const locales = checkLocales(report, schema.locales);
588
+ if (!isRecord(schema.collections)) {
589
+ report(schema.collections === void 0 ? [] : ["collections"], "The schema needs \"collections\" as an object");
590
+ return issues;
591
+ }
592
+ const context = {
593
+ report,
594
+ collections: new Set(Object.keys(schema.collections)),
595
+ locales
596
+ };
597
+ for (const [name, collection] of Object.entries(schema.collections)) checkCollection(context, name, collection);
598
+ return issues;
599
+ }
600
+ export { ContentError, META_KEYS, compilePattern, formatIssue, parseModule, validateSchema };
@@ -0,0 +1,17 @@
1
+ function isRecord(value) {
2
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3
+ }
4
+ function plain(value) {
5
+ return JSON.parse(JSON.stringify(value));
6
+ }
7
+ function same(left, right) {
8
+ return JSON.stringify(left ?? null) === JSON.stringify(right ?? null);
9
+ }
10
+ function defined(value) {
11
+ if (!value) return {};
12
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== void 0));
13
+ }
14
+ function quote(value) {
15
+ return JSON.stringify(value) ?? String(value);
16
+ }
17
+ export { defined, isRecord, plain, quote, same };