forgepress 0.0.0 → 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 +42 -0
- package/dist/_chunks/client.d.mts +2 -0
- package/dist/_chunks/config.mjs +79 -0
- package/dist/_chunks/content.mjs +733 -0
- package/dist/_chunks/error.mjs +4 -0
- package/dist/_chunks/fetch.mjs +13 -0
- package/dist/_chunks/files.mjs +32 -0
- package/dist/_chunks/libs/diff.mjs +485 -0
- package/dist/_chunks/locate.mjs +7 -0
- package/dist/_chunks/media.mjs +282 -0
- package/dist/_chunks/once.mjs +16 -0
- package/dist/_chunks/output.mjs +104 -0
- package/dist/_chunks/overlay.mjs +8 -0
- 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 +19 -0
- package/dist/_chunks/reader2.mjs +797 -0
- package/dist/_chunks/references.mjs +56 -0
- 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 +230 -0
- package/dist/_chunks/types2.d.mts +25 -0
- package/dist/_chunks/value.mjs +139 -0
- package/dist/cli/bin.d.mts +1 -0
- package/dist/cli/bin.mjs +61 -0
- package/dist/disk/reader.d.mts +4 -0
- package/dist/disk/reader.mjs +2 -0
- package/dist/editor/index.d.mts +3 -0
- package/dist/editor/index.mjs +61951 -0
- package/dist/index.d.mts +104 -0
- package/dist/index.mjs +237 -0
- 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 +4 -0
- package/dist/preview/index.mjs +2 -0
- package/dist/preview/react.d.mts +1 -0
- package/dist/preview/react.mjs +34 -0
- package/dist/query/fetch.d.mts +3 -0
- package/dist/query/fetch.mjs +2 -0
- package/dist/unplugin.d.mts +13 -0
- package/dist/unplugin.mjs +2 -0
- package/package.json +146 -2
|
@@ -0,0 +1,733 @@
|
|
|
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
|
+
}
|
|
134
|
+
function formatIssue(issue) {
|
|
135
|
+
return `${issue.file}:${issue.line}:${issue.column} ${issue.message}`;
|
|
136
|
+
}
|
|
137
|
+
var ContentError = class extends Error {
|
|
138
|
+
issues;
|
|
139
|
+
constructor(issues) {
|
|
140
|
+
super(issues.map((issue) => `[forgepress] ${formatIssue(issue)}`).join("\n"));
|
|
141
|
+
this.name = "ContentError";
|
|
142
|
+
this.issues = issues;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const ID_START = /[$_\p{ID_Start}]/u;
|
|
146
|
+
const ID_CONTINUE = /[$\u200C\u200D\p{ID_Continue}]/u;
|
|
147
|
+
const SPACE = /\s/;
|
|
148
|
+
const DECIMAL = /\d/;
|
|
149
|
+
const HEX = /[\da-f]/i;
|
|
150
|
+
const OCTAL = /[0-7]/;
|
|
151
|
+
const BINARY = /[01]/;
|
|
152
|
+
const HEX_DIGITS = /^[\da-f]+$/i;
|
|
153
|
+
const RADIX = /[box]/i;
|
|
154
|
+
const BREAKS = /* @__PURE__ */ new Set([
|
|
155
|
+
"\n",
|
|
156
|
+
"\r",
|
|
157
|
+
"\u2028",
|
|
158
|
+
"\u2029"
|
|
159
|
+
]);
|
|
160
|
+
const TYPE_IMPORT = "Only type imports are allowed, e.g. `import type { ForgePressEntry } from 'forgepress'`";
|
|
161
|
+
const ESCAPES = /* @__PURE__ */ new Map([
|
|
162
|
+
["n", "\n"],
|
|
163
|
+
["r", "\r"],
|
|
164
|
+
["t", " "],
|
|
165
|
+
["b", "\b"],
|
|
166
|
+
["f", "\f"],
|
|
167
|
+
["v", "\v"]
|
|
168
|
+
]);
|
|
169
|
+
function locationAt(text, offset) {
|
|
170
|
+
let line = 1;
|
|
171
|
+
let start = 0;
|
|
172
|
+
for (let position = 0; position < offset; position += 1) {
|
|
173
|
+
const char = text[position];
|
|
174
|
+
if (BREAKS.has(char) && !(char === "\r" && text[position + 1] === "\n")) {
|
|
175
|
+
line += 1;
|
|
176
|
+
start = position + 1;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
line,
|
|
181
|
+
column: offset - start + 1
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function pathKey(path) {
|
|
185
|
+
return JSON.stringify(path);
|
|
186
|
+
}
|
|
187
|
+
function parseModule(text, file) {
|
|
188
|
+
const offsets = /* @__PURE__ */ new Map();
|
|
189
|
+
let index = text.startsWith("") ? 1 : 0;
|
|
190
|
+
function fail(message, at = index) {
|
|
191
|
+
throw new ContentError([{
|
|
192
|
+
file,
|
|
193
|
+
...locationAt(text, at),
|
|
194
|
+
message
|
|
195
|
+
}]);
|
|
196
|
+
}
|
|
197
|
+
function char(offset = 0) {
|
|
198
|
+
return text[index + offset] ?? "";
|
|
199
|
+
}
|
|
200
|
+
function skip() {
|
|
201
|
+
while (index < text.length) if (SPACE.test(char())) index += 1;
|
|
202
|
+
else if (text.startsWith("//", index)) while (index < text.length && !BREAKS.has(char())) index += 1;
|
|
203
|
+
else if (text.startsWith("/*", index)) {
|
|
204
|
+
const end = text.indexOf("*/", index + 2);
|
|
205
|
+
if (end === -1) fail("Unterminated comment");
|
|
206
|
+
index = end + 2;
|
|
207
|
+
} else return;
|
|
208
|
+
}
|
|
209
|
+
function width(pattern) {
|
|
210
|
+
const point = text.codePointAt(index);
|
|
211
|
+
if (point === void 0 || !pattern.test(String.fromCodePoint(point))) return 0;
|
|
212
|
+
return point > 65535 ? 2 : 1;
|
|
213
|
+
}
|
|
214
|
+
function identifier() {
|
|
215
|
+
const start = index;
|
|
216
|
+
for (let step = width(ID_START); step > 0; step = width(ID_CONTINUE)) index += step;
|
|
217
|
+
return index > start ? text.slice(start, index) : void 0;
|
|
218
|
+
}
|
|
219
|
+
function word(expected) {
|
|
220
|
+
skip();
|
|
221
|
+
const start = index;
|
|
222
|
+
if (identifier() === expected) return true;
|
|
223
|
+
index = start;
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
function expectWord(expected, message) {
|
|
227
|
+
if (!word(expected)) fail(message);
|
|
228
|
+
}
|
|
229
|
+
function name(message) {
|
|
230
|
+
skip();
|
|
231
|
+
return identifier() ?? fail(message);
|
|
232
|
+
}
|
|
233
|
+
function list(close, item) {
|
|
234
|
+
index += 1;
|
|
235
|
+
for (skip(); char() !== close; skip()) {
|
|
236
|
+
item();
|
|
237
|
+
skip();
|
|
238
|
+
if (char() === ",") index += 1;
|
|
239
|
+
else if (char() !== close) fail(index < text.length ? `Expected \`,\` or \`${close}\`` : "Unexpected end of file");
|
|
240
|
+
}
|
|
241
|
+
index += 1;
|
|
242
|
+
}
|
|
243
|
+
function hex(count, start) {
|
|
244
|
+
const digits = text.slice(index, index + count);
|
|
245
|
+
if (digits.length < count || !HEX_DIGITS.test(digits)) fail("Invalid escape sequence", start);
|
|
246
|
+
index += count;
|
|
247
|
+
return Number.parseInt(digits, 16);
|
|
248
|
+
}
|
|
249
|
+
function unicode(start) {
|
|
250
|
+
if (char() !== "{") return String.fromCharCode(hex(4, start));
|
|
251
|
+
const end = text.indexOf("}", index);
|
|
252
|
+
const digits = end === -1 ? "" : text.slice(index + 1, end);
|
|
253
|
+
if (!HEX_DIGITS.test(digits) || Number.parseInt(digits, 16) > 1114111) fail("Invalid escape sequence", start);
|
|
254
|
+
index = end + 1;
|
|
255
|
+
return String.fromCodePoint(Number.parseInt(digits, 16));
|
|
256
|
+
}
|
|
257
|
+
function escape(unterminated, opening) {
|
|
258
|
+
const start = index - 1;
|
|
259
|
+
const current = char();
|
|
260
|
+
if (current === "") fail(unterminated, opening);
|
|
261
|
+
index += 1;
|
|
262
|
+
const simple = ESCAPES.get(current);
|
|
263
|
+
if (simple !== void 0) return simple;
|
|
264
|
+
if (current === "\r" && char() === "\n") index += 1;
|
|
265
|
+
if (BREAKS.has(current)) return "";
|
|
266
|
+
if (current === "x") return String.fromCharCode(hex(2, start));
|
|
267
|
+
if (current === "u") return unicode(start);
|
|
268
|
+
if (DECIMAL.test(current) && (current !== "0" || DECIMAL.test(char()))) fail(`\`\\${current}\` is not a valid escape sequence`, start);
|
|
269
|
+
if (current === "0") return "\0";
|
|
270
|
+
const point = text.codePointAt(start + 1);
|
|
271
|
+
index = start + 1 + (point > 65535 ? 2 : 1);
|
|
272
|
+
return String.fromCodePoint(point);
|
|
273
|
+
}
|
|
274
|
+
function string(quote) {
|
|
275
|
+
const start = index;
|
|
276
|
+
let result = "";
|
|
277
|
+
let run = index + 1;
|
|
278
|
+
index = run;
|
|
279
|
+
while (char() !== quote) {
|
|
280
|
+
const current = char();
|
|
281
|
+
if (current === "" || current === "\n" || current === "\r") fail("Unterminated string", start);
|
|
282
|
+
if (current === "\\") {
|
|
283
|
+
result += text.slice(run, index);
|
|
284
|
+
index += 1;
|
|
285
|
+
result += escape("Unterminated string", start);
|
|
286
|
+
run = index;
|
|
287
|
+
} else index += 1;
|
|
288
|
+
}
|
|
289
|
+
result += text.slice(run, index);
|
|
290
|
+
index += 1;
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
293
|
+
function template() {
|
|
294
|
+
const start = index;
|
|
295
|
+
let result = "";
|
|
296
|
+
let run = index + 1;
|
|
297
|
+
index = run;
|
|
298
|
+
while (char() !== "`") {
|
|
299
|
+
const current = char();
|
|
300
|
+
if (current === "") fail("Unterminated template literal", start);
|
|
301
|
+
if (current === "$" && char(1) === "{") fail("Template literals cannot contain substitutions");
|
|
302
|
+
if (current === "\\" || current === "\r") {
|
|
303
|
+
result += text.slice(run, index);
|
|
304
|
+
index += 1;
|
|
305
|
+
if (current === "\\") result += escape("Unterminated template literal", start);
|
|
306
|
+
else {
|
|
307
|
+
result += "\n";
|
|
308
|
+
if (char() === "\n") index += 1;
|
|
309
|
+
}
|
|
310
|
+
run = index;
|
|
311
|
+
} else index += 1;
|
|
312
|
+
}
|
|
313
|
+
result += text.slice(run, index);
|
|
314
|
+
index += 1;
|
|
315
|
+
return result;
|
|
316
|
+
}
|
|
317
|
+
function digits(pattern, start) {
|
|
318
|
+
const from = index;
|
|
319
|
+
while (pattern.test(char()) || char() === "_") {
|
|
320
|
+
if (char() === "_" && (index === from || !pattern.test(char(1)))) fail("Invalid numeric separator", start);
|
|
321
|
+
index += 1;
|
|
322
|
+
}
|
|
323
|
+
if (index === from) fail("Invalid number", start);
|
|
324
|
+
return text.slice(from, index).replaceAll("_", "");
|
|
325
|
+
}
|
|
326
|
+
function numberStart() {
|
|
327
|
+
return DECIMAL.test(char()) || char() === "." && DECIMAL.test(char(1));
|
|
328
|
+
}
|
|
329
|
+
function number() {
|
|
330
|
+
const start = index;
|
|
331
|
+
let source = "";
|
|
332
|
+
if (char() === "0" && RADIX.test(char(1))) {
|
|
333
|
+
const radix = char(1).toLowerCase();
|
|
334
|
+
index += 2;
|
|
335
|
+
source = `0${radix}${digits(radix === "x" ? HEX : radix === "o" ? OCTAL : BINARY, start)}`;
|
|
336
|
+
} else {
|
|
337
|
+
if (char() === "0" && (DECIMAL.test(char(1)) || char(1) === "_")) fail("Numbers cannot start with a leading zero", start);
|
|
338
|
+
if (char() !== ".") source = digits(DECIMAL, start);
|
|
339
|
+
if (char() === ".") {
|
|
340
|
+
index += 1;
|
|
341
|
+
source += `.${DECIMAL.test(char()) ? digits(DECIMAL, start) : ""}`;
|
|
342
|
+
}
|
|
343
|
+
if (char() === "e" || char() === "E") {
|
|
344
|
+
index += 1;
|
|
345
|
+
const sign = char() === "+" || char() === "-" ? char() : "";
|
|
346
|
+
index += sign.length;
|
|
347
|
+
source += `e${sign}${digits(DECIMAL, start)}`;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (char() === "n") fail("BigInt values are not allowed", start);
|
|
351
|
+
if (DECIMAL.test(char()) || width(ID_START) > 0) fail("Invalid number", start);
|
|
352
|
+
const value = Number(source);
|
|
353
|
+
if (!Number.isFinite(value)) fail("Number is out of range", start);
|
|
354
|
+
return value;
|
|
355
|
+
}
|
|
356
|
+
function propertyKey() {
|
|
357
|
+
const start = index;
|
|
358
|
+
const current = char();
|
|
359
|
+
if (current === "[") fail("Computed keys are not allowed");
|
|
360
|
+
if (text.startsWith("...", index)) fail("Spread syntax is not allowed");
|
|
361
|
+
const key = current === "'" || current === "\"" ? string(current) : numberStart() ? String(number()) : identifier();
|
|
362
|
+
if (key === void 0) fail(index < text.length ? "Expected a property name" : "Unexpected end of file");
|
|
363
|
+
if (key === "__proto__") fail("`__proto__` cannot be used as a key", start);
|
|
364
|
+
return key;
|
|
365
|
+
}
|
|
366
|
+
function object(path) {
|
|
367
|
+
const result = {};
|
|
368
|
+
list("}", () => {
|
|
369
|
+
const start = index;
|
|
370
|
+
const key = propertyKey();
|
|
371
|
+
skip();
|
|
372
|
+
if (char() === "," || char() === "}") fail(`Shorthand properties are not allowed; write \`${key}: value\``, start);
|
|
373
|
+
if (char() === "(") fail("Methods are not allowed");
|
|
374
|
+
if (char() !== ":") fail(index < text.length ? `Expected \`:\` after \`${key}\`` : "Unexpected end of file");
|
|
375
|
+
index += 1;
|
|
376
|
+
if (Object.hasOwn(result, key)) fail(`Duplicate key \`${key}\``, start);
|
|
377
|
+
const child = [...path, key];
|
|
378
|
+
offsets.set(pathKey(child), start);
|
|
379
|
+
result[key] = literal(child);
|
|
380
|
+
});
|
|
381
|
+
return result;
|
|
382
|
+
}
|
|
383
|
+
function array(path) {
|
|
384
|
+
const result = [];
|
|
385
|
+
list("]", () => {
|
|
386
|
+
if (char() === ",") fail("Empty array slots are not allowed");
|
|
387
|
+
const child = [...path, result.length];
|
|
388
|
+
offsets.set(pathKey(child), index);
|
|
389
|
+
result.push(literal(child));
|
|
390
|
+
});
|
|
391
|
+
return result;
|
|
392
|
+
}
|
|
393
|
+
function literal(path) {
|
|
394
|
+
skip();
|
|
395
|
+
const start = index;
|
|
396
|
+
const current = char();
|
|
397
|
+
if (current === "{") return object(path);
|
|
398
|
+
if (current === "[") return array(path);
|
|
399
|
+
if (current === "'" || current === "\"") return string(current);
|
|
400
|
+
if (current === "`") return template();
|
|
401
|
+
if (current === "-" || current === "+") {
|
|
402
|
+
index += 1;
|
|
403
|
+
skip();
|
|
404
|
+
if (!numberStart()) fail(`Expected a number after \`${current}\``);
|
|
405
|
+
return current === "-" ? -number() : number();
|
|
406
|
+
}
|
|
407
|
+
if (numberStart()) return number();
|
|
408
|
+
if (text.startsWith("...", index)) fail("Spread syntax is not allowed");
|
|
409
|
+
const found = identifier();
|
|
410
|
+
if (found === "true" || found === "false") return found === "true";
|
|
411
|
+
if (found === "null" || found === "undefined") fail(`\`${found}\` is not supported; leave the value out instead`, start);
|
|
412
|
+
if (found === "NaN" || found === "Infinity") fail(`\`${found}\` cannot be stored`, start);
|
|
413
|
+
if (found !== void 0) fail(`\`${found}\` is not a literal value; variables, calls and expressions are not allowed`, start);
|
|
414
|
+
return fail(index < text.length ? "Expected a literal value" : "Unexpected end of file");
|
|
415
|
+
}
|
|
416
|
+
function typeReference() {
|
|
417
|
+
word("typeof");
|
|
418
|
+
name("Expected a type");
|
|
419
|
+
skip();
|
|
420
|
+
while (char() === ".") {
|
|
421
|
+
index += 1;
|
|
422
|
+
name("Expected a type");
|
|
423
|
+
skip();
|
|
424
|
+
}
|
|
425
|
+
if (char() === "<") list(">", () => {
|
|
426
|
+
const quote = char();
|
|
427
|
+
if (quote === "'" || quote === "\"") string(quote);
|
|
428
|
+
else typeReference();
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
function importDeclaration(start) {
|
|
432
|
+
if (!word("type")) fail(TYPE_IMPORT, start);
|
|
433
|
+
skip();
|
|
434
|
+
if (char() === "{") list("}", () => {
|
|
435
|
+
name("Expected an import name");
|
|
436
|
+
if (word("as")) name("Expected an import name");
|
|
437
|
+
});
|
|
438
|
+
else if (char() === "*") {
|
|
439
|
+
index += 1;
|
|
440
|
+
expectWord("as", "Expected `as`");
|
|
441
|
+
name("Expected an import name");
|
|
442
|
+
} else if (name("Expected an import clause") === "from") {
|
|
443
|
+
skip();
|
|
444
|
+
if (char() === "'" || char() === "\"") fail(TYPE_IMPORT, start);
|
|
445
|
+
}
|
|
446
|
+
expectWord("from", "Expected `from`");
|
|
447
|
+
skip();
|
|
448
|
+
if (char() !== "'" && char() !== "\"") fail("Expected a module name");
|
|
449
|
+
string(char());
|
|
450
|
+
}
|
|
451
|
+
function exportDefault() {
|
|
452
|
+
expectWord("default", "Only `export default` is allowed");
|
|
453
|
+
skip();
|
|
454
|
+
offsets.set(pathKey([]), index);
|
|
455
|
+
const value = literal([]);
|
|
456
|
+
if (word("as")) expectWord("const", "Only `as const` is allowed; use `satisfies` to type the value");
|
|
457
|
+
if (word("satisfies")) typeReference();
|
|
458
|
+
return value;
|
|
459
|
+
}
|
|
460
|
+
function statements() {
|
|
461
|
+
let exported = false;
|
|
462
|
+
let value;
|
|
463
|
+
for (skip(); index < text.length; skip()) {
|
|
464
|
+
const start = index;
|
|
465
|
+
if (char() === ";") {
|
|
466
|
+
index += 1;
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
if (exported) fail("Nothing may follow `export default`");
|
|
470
|
+
const keyword = identifier();
|
|
471
|
+
if (keyword === "import") importDeclaration(start);
|
|
472
|
+
else if (keyword === "export") {
|
|
473
|
+
value = exportDefault();
|
|
474
|
+
exported = true;
|
|
475
|
+
} else fail("Only `import type` and `export default` are allowed", start);
|
|
476
|
+
}
|
|
477
|
+
if (!exported) fail("Missing `export default`");
|
|
478
|
+
return value;
|
|
479
|
+
}
|
|
480
|
+
return {
|
|
481
|
+
value: statements(),
|
|
482
|
+
locate(path) {
|
|
483
|
+
for (let depth = path.length; depth >= 0; depth -= 1) {
|
|
484
|
+
const offset = offsets.get(pathKey(path.slice(0, depth)));
|
|
485
|
+
if (offset !== void 0) return locationAt(text, offset);
|
|
486
|
+
}
|
|
487
|
+
return locationAt(text, 0);
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
function parseFile(file) {
|
|
492
|
+
try {
|
|
493
|
+
return parseModule(file.text, file.path);
|
|
494
|
+
} catch (error) {
|
|
495
|
+
if (error instanceof ContentError) return error.issues;
|
|
496
|
+
throw error;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
function located(file, parsed, issue) {
|
|
500
|
+
return {
|
|
501
|
+
file,
|
|
502
|
+
...parsed.locate(issue.path),
|
|
503
|
+
message: issue.message
|
|
504
|
+
};
|
|
505
|
+
}
|
|
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 };
|
|
511
|
+
}
|
|
512
|
+
function parseSchema(text, file) {
|
|
513
|
+
const parsed = parseSchemaFile({
|
|
514
|
+
path: file,
|
|
515
|
+
text
|
|
516
|
+
});
|
|
517
|
+
if ("issues" in parsed) throw new ContentError(parsed.issues);
|
|
518
|
+
return parsed.schema;
|
|
519
|
+
}
|
|
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;
|
|
528
|
+
}
|
|
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]}`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
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));
|
|
614
|
+
}
|
|
615
|
+
}
|
|
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"}`);
|
|
622
|
+
}
|
|
623
|
+
function validateEntry(schema, collection, entry) {
|
|
624
|
+
const issues = [];
|
|
625
|
+
const report = (path, message) => issues.push({
|
|
626
|
+
path,
|
|
627
|
+
message
|
|
628
|
+
});
|
|
629
|
+
const definition = schema.collections[collection];
|
|
630
|
+
const locales = schema.locales ?? [];
|
|
631
|
+
if (!isRecord(entry)) {
|
|
632
|
+
report([], "An entry has to be an object");
|
|
633
|
+
return issues;
|
|
634
|
+
}
|
|
635
|
+
if (!definition) {
|
|
636
|
+
report([], `Collection ${quote(collection)} is not in the schema`);
|
|
637
|
+
return issues;
|
|
638
|
+
}
|
|
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
|
+
}
|
|
648
|
+
return issues;
|
|
649
|
+
}
|
|
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 };
|