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.
- package/dist/THIRD-PARTY-LICENSES.md +28 -83
- package/dist/_chunks/client.d.mts +1 -1
- package/dist/_chunks/config.mjs +61 -19
- package/dist/_chunks/{validate.mjs → content.mjs} +350 -217
- package/dist/_chunks/fetch.mjs +2 -2
- package/dist/_chunks/files.mjs +32 -0
- package/dist/_chunks/libs/diff.mjs +485 -0
- package/dist/_chunks/media.mjs +282 -0
- package/dist/_chunks/once.mjs +16 -0
- package/dist/_chunks/output.mjs +98 -252
- package/dist/_chunks/plugin.mjs +87 -0
- package/dist/_chunks/preview.mjs +248 -0
- package/dist/_chunks/project.d.mts +7 -0
- package/dist/_chunks/reader.mjs +5 -9
- package/dist/_chunks/reader2.mjs +150 -202
- package/dist/_chunks/references.mjs +8 -5
- package/dist/_chunks/resolve.d.mts +2 -0
- package/dist/_chunks/response.mjs +5 -0
- package/dist/_chunks/routes.mjs +9 -0
- package/dist/_chunks/serialize.mjs +82 -0
- package/dist/_chunks/settings.mjs +328 -0
- package/dist/_chunks/settings2.mjs +2 -0
- package/dist/_chunks/types.d.mts +229 -24
- package/dist/_chunks/types2.d.mts +25 -0
- package/dist/_chunks/value.mjs +123 -1
- package/dist/cli/bin.mjs +50 -37
- package/dist/editor/index.mjs +9961 -8653
- package/dist/index.d.mts +2 -3
- package/dist/index.mjs +14 -24
- package/dist/next/preview.d.mts +1 -0
- package/dist/next/preview.mjs +3 -0
- package/dist/next/reload.d.mts +1 -0
- package/dist/next/reload.mjs +20 -0
- package/dist/next/settings.d.mts +12 -0
- package/dist/next/settings.mjs +2 -0
- package/dist/plugin/next.d.mts +11 -0
- package/dist/plugin/next.mjs +213 -0
- package/dist/plugin/nuxt.d.mts +7 -0
- package/dist/plugin/nuxt.mjs +52 -0
- package/dist/plugin/watcher.d.mts +1 -0
- package/dist/plugin/watcher.mjs +23 -0
- package/dist/preview/index.d.mts +1 -2
- package/dist/preview/index.mjs +1 -243
- package/dist/preview/react.d.mts +1 -0
- package/dist/preview/react.mjs +34 -0
- package/dist/unplugin.d.mts +12 -17
- package/dist/unplugin.mjs +1 -500
- package/package.json +72 -8
- package/dist/_chunks/config.d.mts +0 -35
- package/dist/_chunks/entry.d.mts +0 -194
- package/dist/_chunks/libs/@oxc-project/types.d.mts +0 -1297
- package/dist/_chunks/libs/@rolldown/pluginutils.d.mts +0 -62
- package/dist/_chunks/libs/rolldown.d.mts +0 -4711
- package/dist/_chunks/output2.mjs +0 -333
- package/dist/_chunks/parse.mjs +0 -22
- package/dist/_chunks/types.mjs +0 -2
|
@@ -1,5 +1,136 @@
|
|
|
1
|
-
import { isRecord, quote } from "./value.mjs";
|
|
2
|
-
import {
|
|
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
|
-
|
|
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
|
|
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
|
|
505
|
-
const options = Object.entries(fieldTypes[type].options).map(([option, spec]) => [option, spec.type]);
|
|
499
|
+
function located(file, parsed, issue) {
|
|
506
500
|
return {
|
|
507
|
-
|
|
508
|
-
...
|
|
501
|
+
file,
|
|
502
|
+
...parsed.locate(issue.path),
|
|
503
|
+
message: issue.message
|
|
509
504
|
};
|
|
510
505
|
}
|
|
511
|
-
function
|
|
512
|
-
|
|
513
|
-
if (!
|
|
514
|
-
|
|
515
|
-
|
|
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
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
if (
|
|
547
|
-
if (
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
|
563
|
-
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
|
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
|
-
|
|
583
|
-
|
|
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
|
-
|
|
587
|
-
|
|
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
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
|
|
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 };
|
package/dist/_chunks/fetch.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import "./
|
|
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
|
|
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 };
|