forgepress 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.
Files changed (56) hide show
  1. package/dist/THIRD-PARTY-LICENSES.md +28 -83
  2. package/dist/_chunks/client.d.mts +1 -1
  3. package/dist/_chunks/config.mjs +61 -19
  4. package/dist/_chunks/{validate.mjs → content.mjs} +350 -217
  5. package/dist/_chunks/fetch.mjs +2 -2
  6. package/dist/_chunks/files.mjs +32 -0
  7. package/dist/_chunks/libs/diff.mjs +485 -0
  8. package/dist/_chunks/media.mjs +282 -0
  9. package/dist/_chunks/once.mjs +16 -0
  10. package/dist/_chunks/output.mjs +98 -252
  11. package/dist/_chunks/plugin.mjs +87 -0
  12. package/dist/_chunks/preview.mjs +248 -0
  13. package/dist/_chunks/project.d.mts +7 -0
  14. package/dist/_chunks/reader.mjs +5 -9
  15. package/dist/_chunks/reader2.mjs +150 -202
  16. package/dist/_chunks/references.mjs +8 -5
  17. package/dist/_chunks/resolve.d.mts +2 -0
  18. package/dist/_chunks/response.mjs +5 -0
  19. package/dist/_chunks/routes.mjs +9 -0
  20. package/dist/_chunks/serialize.mjs +82 -0
  21. package/dist/_chunks/settings.mjs +328 -0
  22. package/dist/_chunks/settings2.mjs +2 -0
  23. package/dist/_chunks/types.d.mts +229 -24
  24. package/dist/_chunks/types2.d.mts +25 -0
  25. package/dist/_chunks/value.mjs +123 -1
  26. package/dist/cli/bin.mjs +50 -37
  27. package/dist/editor/index.mjs +9961 -8653
  28. package/dist/index.d.mts +2 -3
  29. package/dist/index.mjs +14 -24
  30. package/dist/next/preview.d.mts +1 -0
  31. package/dist/next/preview.mjs +3 -0
  32. package/dist/next/reload.d.mts +1 -0
  33. package/dist/next/reload.mjs +20 -0
  34. package/dist/next/settings.d.mts +12 -0
  35. package/dist/next/settings.mjs +2 -0
  36. package/dist/plugin/next.d.mts +11 -0
  37. package/dist/plugin/next.mjs +213 -0
  38. package/dist/plugin/nuxt.d.mts +7 -0
  39. package/dist/plugin/nuxt.mjs +52 -0
  40. package/dist/plugin/watcher.d.mts +1 -0
  41. package/dist/plugin/watcher.mjs +23 -0
  42. package/dist/preview/index.d.mts +1 -2
  43. package/dist/preview/index.mjs +1 -243
  44. package/dist/preview/react.d.mts +1 -0
  45. package/dist/preview/react.mjs +34 -0
  46. package/dist/unplugin.d.mts +12 -17
  47. package/dist/unplugin.mjs +1 -500
  48. package/package.json +72 -8
  49. package/dist/_chunks/config.d.mts +0 -35
  50. package/dist/_chunks/entry.d.mts +0 -194
  51. package/dist/_chunks/libs/@oxc-project/types.d.mts +0 -1297
  52. package/dist/_chunks/libs/@rolldown/pluginutils.d.mts +0 -62
  53. package/dist/_chunks/libs/rolldown.d.mts +0 -4711
  54. package/dist/_chunks/output2.mjs +0 -333
  55. package/dist/_chunks/parse.mjs +0 -22
  56. package/dist/_chunks/types.mjs +0 -2
@@ -1,5 +1,136 @@
1
- import { isRecord, quote } from "./value.mjs";
2
- import { isCollectionName } from "./output.mjs";
1
+ import { ENTRY_STATUSES, META_KEYS, compilePattern, fieldTypeNames, fieldTypes, isRecord, isTranslated, quote } from "./value.mjs";
2
+ import { entryKey, validateReferences } from "./references.mjs";
3
+ import { isCollectionName, isEntryFile, isEntryId, sortByCreation, toEntryFile, toEntryId, toEntryRef } from "./media.mjs";
4
+ const LOCALE_CODE = /^[a-z][\w-]*$/i;
5
+ const RESERVED_FIELDS = META_KEYS;
6
+ const SCHEMA_KEYS = /* @__PURE__ */ new Set(["collections", "locales"]);
7
+ const COLLECTION_KEYS = /* @__PURE__ */ new Set([
8
+ "label",
9
+ "description",
10
+ "fields"
11
+ ]);
12
+ const BASE_OPTIONS = {
13
+ label: "text",
14
+ description: "text",
15
+ optional: "boolean",
16
+ translate: "boolean"
17
+ };
18
+ const INDEXABLE = fieldTypeNames.filter((type) => "index" in fieldTypes[type].options);
19
+ const KINDS = {
20
+ text: "a string",
21
+ number: "a number",
22
+ boolean: "true or false",
23
+ collection: "a collection name",
24
+ collections: "a list of collection names"
25
+ };
26
+ function isFieldType(type) {
27
+ return typeof type === "string" && fieldTypeNames.includes(type);
28
+ }
29
+ function finite(value) {
30
+ return typeof value === "number" && Number.isFinite(value);
31
+ }
32
+ function fits(kind, value) {
33
+ if (kind === "number") return finite(value);
34
+ if (kind === "boolean") return typeof value === "boolean";
35
+ if (kind === "collections") return Array.isArray(value) && value.every((item) => typeof item === "string");
36
+ return typeof value === "string";
37
+ }
38
+ function optionKinds(type) {
39
+ const options = Object.entries(fieldTypes[type].options).map(([option, spec]) => [option, spec.type]);
40
+ return {
41
+ ...BASE_OPTIONS,
42
+ ...Object.fromEntries(options)
43
+ };
44
+ }
45
+ function checkLocales(report, locales) {
46
+ if (locales === void 0) return [];
47
+ if (!Array.isArray(locales)) {
48
+ report(["locales"], "\"locales\" has to be a list of locale codes");
49
+ return [];
50
+ }
51
+ locales.forEach((locale, index) => {
52
+ if (typeof locale !== "string" || !LOCALE_CODE.test(locale)) report(["locales", index], `${quote(locale)} is not a locale code`);
53
+ else if (locales.indexOf(locale) < index) report(["locales", index], `Locale ${quote(locale)} is listed twice`);
54
+ });
55
+ return locales;
56
+ }
57
+ function checkReferences(context, path, label, targets, listed) {
58
+ targets.forEach((target, index) => {
59
+ const at = listed ? [...path, index] : path;
60
+ if (!context.collections.has(target)) context.report(at, `Field ${quote(label)} references unknown collection ${quote(target)}`);
61
+ else if (targets.indexOf(target) < index) context.report(at, `Field ${quote(label)} lists collection ${quote(target)} twice`);
62
+ });
63
+ }
64
+ function checkOption(context, path, label, option, kind, value) {
65
+ if (!fits(kind, value)) context.report(path, `${quote(option)} of field ${quote(label)} has to be ${KINDS[kind]}`);
66
+ else if (kind === "collection" || kind === "collections") checkReferences(context, path, label, kind === "collection" ? [value] : value, kind === "collections");
67
+ }
68
+ function checkConstraints(report, path, label, field) {
69
+ if (field.type === "text" && typeof field.validation === "string") {
70
+ const pattern = compilePattern(field.validation);
71
+ if (pattern instanceof SyntaxError) report([...path, "validation"], `"validation" of field ${quote(label)} is not a valid regular expression: ${pattern.message.replace(/^Invalid regular expression: /, "")}`);
72
+ }
73
+ if (field.type !== "number") return;
74
+ if (finite(field.step) && field.step <= 0) report([...path, "step"], `"step" of field ${quote(label)} has to be greater than 0`);
75
+ 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"`);
76
+ }
77
+ function checkField(context, path, collection, key, field) {
78
+ const { report } = context;
79
+ const label = `${collection}.${key}`;
80
+ if (RESERVED_FIELDS.includes(key)) report(path, `Field ${quote(label)} uses ${quote(key)}, which is reserved for entry metadata`);
81
+ if (!isRecord(field)) return report(path, `Field ${quote(label)} has to be an object`);
82
+ if (field.type === void 0) return report(path, `Field ${quote(label)} needs a type`);
83
+ if (!isFieldType(field.type)) return report([...path, "type"], `Field ${quote(label)} has unknown type ${quote(field.type)}; use one of ${fieldTypeNames.join(", ")}`);
84
+ const kinds = optionKinds(field.type);
85
+ for (const [option, value] of Object.entries(field)) {
86
+ const kind = kinds[option];
87
+ if (option === "type") continue;
88
+ if (kind) checkOption(context, [...path, option], label, option, kind, value);
89
+ 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`);
90
+ else report([...path, option], `Field ${quote(label)} has no option ${quote(option)}`);
91
+ }
92
+ 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)}`);
93
+ checkConstraints(report, path, label, field);
94
+ if (field.translate === true && context.locales.length === 0) report([...path, "translate"], `Field ${quote(label)} is translated, but the schema has no locales`);
95
+ }
96
+ function checkCollection(context, name, collection) {
97
+ const { report } = context;
98
+ const path = ["collections", name];
99
+ if (!isCollectionName(name)) report(path, `Collection ${quote(name)} has to start with a lowercase letter and contain only letters and digits`);
100
+ if (!isRecord(collection)) return report(path, `Collection ${quote(name)} has to be an object`);
101
+ for (const [key, value] of Object.entries(collection)) if (!COLLECTION_KEYS.has(key)) report([...path, key], `Collection ${quote(name)} has no option ${quote(key)}`);
102
+ else if (key !== "fields" && typeof value !== "string") report([...path, key], `${quote(key)} of collection ${quote(name)} has to be a string`);
103
+ if (!isRecord(collection.fields)) return report(collection.fields === void 0 ? path : [...path, "fields"], `Collection ${quote(name)} needs "fields" as an object`);
104
+ for (const [key, field] of Object.entries(collection.fields)) checkField(context, [
105
+ ...path,
106
+ "fields",
107
+ key
108
+ ], name, key, field);
109
+ }
110
+ function validateSchema(schema) {
111
+ const issues = [];
112
+ const report = (path, message) => issues.push({
113
+ path,
114
+ message
115
+ });
116
+ if (!isRecord(schema)) {
117
+ report([], "The schema has to be an object");
118
+ return issues;
119
+ }
120
+ for (const key of Object.keys(schema)) if (!SCHEMA_KEYS.has(key)) report([key], `The schema has no option ${quote(key)}`);
121
+ const locales = checkLocales(report, schema.locales);
122
+ if (!isRecord(schema.collections)) {
123
+ report(schema.collections === void 0 ? [] : ["collections"], "The schema needs \"collections\" as an object");
124
+ return issues;
125
+ }
126
+ const context = {
127
+ report,
128
+ collections: new Set(Object.keys(schema.collections)),
129
+ locales
130
+ };
131
+ for (const [name, collection] of Object.entries(schema.collections)) checkCollection(context, name, collection);
132
+ return issues;
133
+ }
3
134
  function formatIssue(issue) {
4
135
  return `${issue.file}:${issue.line}:${issue.column} ${issue.message}`;
5
136
  }
@@ -357,244 +488,246 @@ function parseModule(text, file) {
357
488
  }
358
489
  };
359
490
  }
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) {
491
+ function parseFile(file) {
447
492
  try {
448
- return new RegExp(validation, "u");
493
+ return parseModule(file.text, file.path);
449
494
  } 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";
495
+ if (error instanceof ContentError) return error.issues;
496
+ throw error;
497
+ }
503
498
  }
504
- function optionKinds(type) {
505
- const options = Object.entries(fieldTypes[type].options).map(([option, spec]) => [option, spec.type]);
499
+ function located(file, parsed, issue) {
506
500
  return {
507
- ...BASE_OPTIONS,
508
- ...Object.fromEntries(options)
501
+ file,
502
+ ...parsed.locate(issue.path),
503
+ message: issue.message
509
504
  };
510
505
  }
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;
506
+ function parseSchemaFile(file) {
507
+ const parsed = parseFile(file);
508
+ if (!("value" in parsed)) return { issues: [...parsed] };
509
+ const issues = validateSchema(parsed.value);
510
+ return issues.length > 0 ? { issues: issues.map((issue) => located(file.path, parsed, issue)) } : { schema: parsed.value };
522
511
  }
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`);
512
+ function parseSchema(text, file) {
513
+ const parsed = parseSchemaFile({
514
+ path: file,
515
+ text
528
516
  });
517
+ if ("issues" in parsed) throw new ContentError(parsed.issues);
518
+ return parsed.schema;
529
519
  }
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");
520
+ function parseEntry(text, file) {
521
+ const { value, locate } = parseModule(text, file);
522
+ if (!isRecord(value)) throw new ContentError([{
523
+ file,
524
+ ...locate([]),
525
+ message: "An entry has to be an object"
526
+ }]);
527
+ return value;
533
528
  }
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: /, "")}`);
529
+ const ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})(?:T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2}))?$/;
530
+ const MEDIA_OPTIONS = {
531
+ url: ["a string", (value) => typeof value === "string"],
532
+ alt: ["a string", (value) => typeof value === "string"],
533
+ width: ["a number", (value) => typeof value === "number"],
534
+ height: ["a number", (value) => typeof value === "number"]
535
+ };
536
+ const BLOCK_KEYS = /* @__PURE__ */ new Set(["collection", "id"]);
537
+ function isDate(value) {
538
+ const match = typeof value === "string" ? ISO_DATE.exec(value) : null;
539
+ if (!match || Number.isNaN(Date.parse(match[0]))) return false;
540
+ const month = Number(match[2]) - 1;
541
+ const day = Number(match[3]);
542
+ const date = new Date(Date.UTC(Number(match[1]), month, day));
543
+ return date.getUTCMonth() === month && date.getUTCDate() === day;
544
+ }
545
+ function checkMeta(report, entry) {
546
+ if (entry.id === void 0) report([], "The entry needs an \"id\"");
547
+ else if (typeof entry.id !== "string" || !isEntryId(entry.id)) report(["id"], "\"id\" has to be a string of letters, digits, \"_\" and \"-\"");
548
+ if (entry.status === void 0) report([], "The entry needs a \"status\"");
549
+ else if (!ENTRY_STATUSES.includes(entry.status)) report(["status"], `"status" has to be ${ENTRY_STATUSES.map(quote).join(" or ")}`);
550
+ for (const key of ["createdAt", "updatedAt"]) if (entry[key] === void 0) report([], `The entry needs ${quote(key)}`);
551
+ else if (!isDate(entry[key])) report([key], `${quote(key)} has to be an ISO 8601 date such as "2024-01-31T09:30:00Z"`);
552
+ }
553
+ function checkList(report, path, value, message, item) {
554
+ if (!Array.isArray(value)) return report(path, message);
555
+ value.forEach((entry, index) => item([...path, index], entry));
556
+ }
557
+ function checkId(report, path, label, collection, value) {
558
+ if (typeof value !== "string") report(path, `Field ${label} has to hold ids of ${quote(collection)} entries`);
559
+ }
560
+ function checkMedia(report, path, label, value) {
561
+ if (!isRecord(value)) return report(path, `Field ${label} has to be a media object such as { url: "/uploads/photo.jpg" }`);
562
+ if (value.url === void 0) report(path, `Field ${label} needs a "url"`);
563
+ for (const [option, item] of Object.entries(value)) {
564
+ const spec = MEDIA_OPTIONS[option];
565
+ if (spec === void 0) report([...path, option], `Field ${label} has no media option ${quote(option)}`);
566
+ else if (!spec[1](item)) report([...path, option], `${quote(option)} of field ${label} has to be ${spec[0]}`);
538
567
  }
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
568
  }
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)}`);
569
+ function checkBlock(report, path, label, field, value) {
570
+ if (!isRecord(value)) return report(path, `Field ${label} has to hold blocks such as { collection: "…", id: "…" }`);
571
+ if (typeof value.collection !== "string") report(value.collection === void 0 ? path : [...path, "collection"], `A block in field ${label} needs a "collection"`);
572
+ else if (!field.collections.includes(value.collection)) report([...path, "collection"], `Field ${label} can't hold ${quote(value.collection)} blocks; allowed are ${field.collections.join(", ") || "none"}`);
573
+ if (typeof value.id !== "string") report(value.id === void 0 ? path : [...path, "id"], `A block in field ${label} needs an "id"`);
574
+ for (const key of Object.keys(value)) if (!BLOCK_KEYS.has(key)) report([...path, key], `A block in field ${label} has no option ${quote(key)}`);
575
+ }
576
+ function checkPattern(report, path, label, field, value) {
577
+ const pattern = field.validation === void 0 ? void 0 : compilePattern(field.validation);
578
+ if (pattern instanceof RegExp && !pattern.test(value)) report(path, `Field ${label} has to match the pattern ${field.validation}`);
579
+ }
580
+ function checkRange(report, path, label, field, value) {
581
+ const { min, max, step } = field;
582
+ if (min !== void 0 && value < min) report(path, `Field ${label} has to be at least ${min}`);
583
+ if (max !== void 0 && value > max) report(path, `Field ${label} has to be at most ${max}`);
584
+ if (step === void 0 || step <= 0) return;
585
+ const base = min ?? 0;
586
+ const steps = (value - base) / step;
587
+ if (Math.abs(steps - Math.round(steps)) <= 1e-9 * Math.max(1, Math.abs(steps))) return;
588
+ const nearest = [Math.floor(steps), Math.ceil(steps)].map((count) => Number((base + count * step).toPrecision(12))).filter((candidate) => (min === void 0 || candidate >= min) && (max === void 0 || candidate <= max));
589
+ report(path, `Field ${label} has to be in steps of ${step}${base === 0 ? "" : ` from ${base}`}${nearest.length > 0 ? `, such as ${nearest.join(" or ")}` : ""}`);
590
+ }
591
+ function checkValue(report, path, label, field, value) {
592
+ switch (field.type) {
593
+ case "text":
594
+ if (typeof value !== "string") report(path, `Field ${label} has to be a string`);
595
+ else checkPattern(report, path, label, field, value);
596
+ return;
597
+ case "richtext":
598
+ if (typeof value !== "string") report(path, `Field ${label} has to be a string`);
599
+ return;
600
+ case "number":
601
+ if (typeof value !== "number" || !Number.isFinite(value)) report(path, `Field ${label} has to be a number`);
602
+ else checkRange(report, path, label, field, value);
603
+ return;
604
+ case "image":
605
+ case "video":
606
+ if (field.multiple) checkList(report, path, value, `Field ${label} has to be a list of media objects`, (at, item) => checkMedia(report, at, label, item));
607
+ else checkMedia(report, path, label, value);
608
+ return;
609
+ case "relation":
610
+ if (field.multiple) checkList(report, path, value, `Field ${label} has to be a list of ${quote(field.collection)} entry ids`, (at, item) => checkId(report, at, label, field.collection, item));
611
+ else checkId(report, path, label, field.collection, value);
612
+ return;
613
+ case "dynamic": checkList(report, path, value, `Field ${label} has to be a list of blocks`, (at, item) => checkBlock(report, at, label, field, item));
557
614
  }
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
615
  }
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);
616
+ function checkTranslations(report, key, field, value, locales) {
617
+ if (!isRecord(value)) return report([key], `Field ${quote(key)} is translated and has to hold one value per locale, such as { ${locales[0]}: … }`);
618
+ for (const [locale, item] of Object.entries(value)) if (locales.includes(locale)) checkValue(report, [key, locale], `${quote(key)} (${locale})`, field, item);
619
+ else report([key, locale], `Field ${quote(key)} has no locale ${quote(locale)}; the schema has ${locales.join(", ")}`);
620
+ const missing = field.optional ? [] : locales.filter((locale) => value[locale] === void 0);
621
+ if (missing.length > 0) report([key], `Field ${quote(key)} is missing its ${missing.join(", ")} ${missing.length === 1 ? "translation" : "translations"}`);
575
622
  }
576
- function validateSchema(schema) {
623
+ function validateEntry(schema, collection, entry) {
577
624
  const issues = [];
578
625
  const report = (path, message) => issues.push({
579
626
  path,
580
627
  message
581
628
  });
582
- if (!isRecord(schema)) {
583
- report([], "The schema has to be an object");
629
+ const definition = schema.collections[collection];
630
+ const locales = schema.locales ?? [];
631
+ if (!isRecord(entry)) {
632
+ report([], "An entry has to be an object");
584
633
  return issues;
585
634
  }
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");
635
+ if (!definition) {
636
+ report([], `Collection ${quote(collection)} is not in the schema`);
590
637
  return issues;
591
638
  }
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);
639
+ checkMeta(report, entry);
640
+ for (const [key, value] of Object.entries(entry)) if (value !== void 0 && !META_KEYS.includes(key) && !Object.hasOwn(definition.fields, key)) report([key], `${quote(key)} is not a field of collection ${quote(collection)}`);
641
+ for (const [key, field] of Object.entries(definition.fields)) {
642
+ const value = entry[key];
643
+ if (value === void 0) {
644
+ if (!field.optional) report([], `Field ${quote(key)} is required`);
645
+ } else if (isTranslated(field, locales)) checkTranslations(report, key, field, value, locales);
646
+ else checkValue(report, [key], quote(key), field, value);
647
+ }
598
648
  return issues;
599
649
  }
600
- export { ContentError, META_KEYS, compilePattern, formatIssue, parseModule, validateSchema };
650
+ function byPosition(left, right) {
651
+ return left.line - right.line || left.column - right.column;
652
+ }
653
+ function parseContent(schemaFile, entryFiles) {
654
+ const parsedSchema = schemaFile ? parseSchemaFile(schemaFile) : { schema: { collections: {} } };
655
+ if ("issues" in parsedSchema) return {
656
+ issues: parsedSchema.issues,
657
+ schema: void 0,
658
+ content: {}
659
+ };
660
+ const { schema } = parsedSchema;
661
+ const issues = new Map(entryFiles.map((file) => [file.path, []]));
662
+ const sources = /* @__PURE__ */ new Map();
663
+ const content = {};
664
+ for (const file of entryFiles) {
665
+ const { collection, id, path } = file;
666
+ const found = issues.get(path);
667
+ const parsed = parseFile(file);
668
+ if (!("value" in parsed)) {
669
+ found.push(...parsed);
670
+ continue;
671
+ }
672
+ found.push(...validateEntry(schema, collection, parsed.value).map((issue) => located(path, parsed, issue)));
673
+ if (!isRecord(parsed.value)) continue;
674
+ if (typeof parsed.value.id === "string" && parsed.value.id !== id) found.push(located(path, parsed, {
675
+ path: ["id"],
676
+ message: `"id" is ${quote(parsed.value.id)}, but the file is named ${toEntryFile(id)}`
677
+ }));
678
+ content[collection] ??= {};
679
+ content[collection][id] = parsed.value;
680
+ sources.set(entryKey(collection, id), {
681
+ path,
682
+ parsed
683
+ });
684
+ }
685
+ for (const issue of validateReferences(schema, content)) {
686
+ const source = sources.get(entryKey(issue.collection, issue.id));
687
+ issues.get(source.path).push(located(source.path, source.parsed, issue));
688
+ }
689
+ return {
690
+ issues: [...issues.values()].flatMap((found) => found.sort(byPosition)),
691
+ schema,
692
+ content
693
+ };
694
+ }
695
+ async function entryFile(files, content, path) {
696
+ const entry = toEntryRef(content, path);
697
+ const text = entry && await files.read(path);
698
+ return entry && text !== void 0 ? [{
699
+ ...entry,
700
+ path,
701
+ text
702
+ }] : [];
703
+ }
704
+ async function readContent(files, paths) {
705
+ const [schema, listed] = await Promise.all([files.read(paths.schema), files.list(paths.content)]);
706
+ const entries = await Promise.all(listed.sort().map((path) => entryFile(files, paths.content, path)));
707
+ return parseContent(schema === void 0 ? void 0 : {
708
+ path: paths.schema,
709
+ text: schema
710
+ }, entries.flat());
711
+ }
712
+ function createFileSource(files, paths) {
713
+ async function entry(collection, id) {
714
+ if (!isEntryId(id)) return void 0;
715
+ const path = paths.entry(collection, id);
716
+ const text = await files.read(path);
717
+ return text === void 0 ? void 0 : parseEntry(text, path);
718
+ }
719
+ return {
720
+ schema: async () => {
721
+ const text = await files.read(paths.schema);
722
+ return text === void 0 ? { collections: {} } : parseSchema(text, paths.schema);
723
+ },
724
+ list: async (collection) => {
725
+ const directory = paths.collection(collection);
726
+ const ids = (await files.list(directory)).map((path) => path.slice(directory.length + 1)).filter(isEntryFile).map(toEntryId);
727
+ const rows = await Promise.all(ids.map((id) => entry(collection, id)));
728
+ return sortByCreation(rows.filter((row) => row !== void 0));
729
+ },
730
+ entry
731
+ };
732
+ }
733
+ export { ContentError, createFileSource, formatIssue, readContent, validateSchema };
@@ -1,10 +1,10 @@
1
- import "./types.mjs";
1
+ import { isPage } from "./response.mjs";
2
2
  function fetchReader(url) {
3
3
  const base = url.replace(/\/+$/, "");
4
4
  return async (path) => {
5
5
  const location = `${base}/${path}`;
6
6
  const response = await fetch(location, path === "index.json" ? { cache: "no-cache" } : {});
7
- if (response.status === 404 || response.ok && response.headers.get("content-type")?.includes("text/html")) return void 0;
7
+ if (response.status === 404 || response.ok && isPage(response)) return void 0;
8
8
  if (!response.ok) throw new Error(`[forgepress] could not load ${location}: ${response.status} ${response.statusText}`.trim());
9
9
  return response.json();
10
10
  };
@@ -0,0 +1,32 @@
1
+ import { readFile, readdir } from "node:fs/promises";
2
+ import { isAbsolute, join, relative, sep } from "node:path";
3
+ import { existsSync } from "node:fs";
4
+ function toPosix(path) {
5
+ return path.split(sep).join("/");
6
+ }
7
+ function isInside(parent, child) {
8
+ const path = relative(parent, child);
9
+ return path !== "" && path !== ".." && !path.startsWith(`..${sep}`) && !isAbsolute(path);
10
+ }
11
+ async function listFiles(folder) {
12
+ if (!existsSync(folder)) return [];
13
+ return (await readdir(folder, {
14
+ withFileTypes: true,
15
+ recursive: true
16
+ })).filter((item) => item.isFile()).map((item) => toPosix(relative(folder, join(item.parentPath, item.name))));
17
+ }
18
+ async function readText(file) {
19
+ try {
20
+ return await readFile(file, "utf8");
21
+ } catch (error) {
22
+ if (error.code === "ENOENT") return void 0;
23
+ throw error;
24
+ }
25
+ }
26
+ function diskFiles(root) {
27
+ return {
28
+ list: async (directory) => (await listFiles(join(root, directory))).map((path) => `${directory}/${path}`),
29
+ read: (path) => readText(join(root, path))
30
+ };
31
+ }
32
+ export { diskFiles, isInside, listFiles, readText, toPosix };