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,333 +0,0 @@
1
- import { isRecord, quote } from "./value.mjs";
2
- import { entryKey, validateReferences } from "./references.mjs";
3
- import { OUTPUT_INDEX } from "./types.mjs";
4
- import { createOutput, defaultPaths, isEntryFile, isEntryId, toCollectionName, toEntryFile, toEntryId } from "./output.mjs";
5
- import { ContentError, META_KEYS, compilePattern, parseModule, validateSchema } from "./validate.mjs";
6
- import { existsSync, readdirSync } from "node:fs";
7
- import { mkdir, readFile, readdir, rename, rm, rmdir, writeFile } from "node:fs/promises";
8
- import { dirname, isAbsolute, join, relative, sep } from "node:path";
9
- import process from "node:process";
10
- import { randomUUID } from "node:crypto";
11
- import { execFileSync } from "node:child_process";
12
- const STATUSES = ["published", "unpublished"];
13
- const ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})(?:T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2}))?$/;
14
- const MEDIA_OPTIONS = {
15
- url: ["a string", (value) => typeof value === "string"],
16
- alt: ["a string", (value) => typeof value === "string"],
17
- width: ["a number", (value) => typeof value === "number"],
18
- height: ["a number", (value) => typeof value === "number"]
19
- };
20
- const BLOCK_KEYS = /* @__PURE__ */ new Set(["collection", "id"]);
21
- function isDate(value) {
22
- const match = typeof value === "string" ? ISO_DATE.exec(value) : null;
23
- if (!match || Number.isNaN(Date.parse(match[0]))) return false;
24
- const month = Number(match[2]) - 1;
25
- const day = Number(match[3]);
26
- const date = new Date(Date.UTC(Number(match[1]), month, day));
27
- return date.getUTCMonth() === month && date.getUTCDate() === day;
28
- }
29
- function checkMeta(report, entry) {
30
- if (entry.id === void 0) report([], "The entry needs an \"id\"");
31
- else if (typeof entry.id !== "string" || !isEntryId(entry.id)) report(["id"], "\"id\" has to be a string of letters, digits, \"_\" and \"-\"");
32
- if (entry.status === void 0) report([], "The entry needs a \"status\"");
33
- else if (!STATUSES.includes(entry.status)) report(["status"], "\"status\" has to be \"published\" or \"unpublished\"");
34
- for (const key of ["createdAt", "updatedAt"]) if (entry[key] === void 0) report([], `The entry needs ${quote(key)}`);
35
- else if (!isDate(entry[key])) report([key], `${quote(key)} has to be an ISO 8601 date such as "2024-01-31T09:30:00Z"`);
36
- }
37
- function checkList(report, path, value, message, item) {
38
- if (!Array.isArray(value)) return report(path, message);
39
- value.forEach((entry, index) => item([...path, index], entry));
40
- }
41
- function checkId(report, path, label, collection, value) {
42
- if (typeof value !== "string") report(path, `Field ${label} has to hold ids of ${quote(collection)} entries`);
43
- }
44
- function checkMedia(report, path, label, value) {
45
- if (!isRecord(value)) return report(path, `Field ${label} has to be a media object such as { url: "/uploads/photo.jpg" }`);
46
- if (value.url === void 0) report(path, `Field ${label} needs a "url"`);
47
- for (const [option, item] of Object.entries(value)) {
48
- const spec = MEDIA_OPTIONS[option];
49
- if (spec === void 0) report([...path, option], `Field ${label} has no media option ${quote(option)}`);
50
- else if (!spec[1](item)) report([...path, option], `${quote(option)} of field ${label} has to be ${spec[0]}`);
51
- }
52
- }
53
- function checkBlock(report, path, label, field, value) {
54
- if (!isRecord(value)) return report(path, `Field ${label} has to hold blocks such as { collection: "…", id: "…" }`);
55
- if (typeof value.collection !== "string") report(value.collection === void 0 ? path : [...path, "collection"], `A block in field ${label} needs a "collection"`);
56
- 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"}`);
57
- if (typeof value.id !== "string") report(value.id === void 0 ? path : [...path, "id"], `A block in field ${label} needs an "id"`);
58
- 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)}`);
59
- }
60
- function checkPattern(report, path, label, field, value) {
61
- const pattern = field.validation === void 0 ? void 0 : compilePattern(field.validation);
62
- if (pattern instanceof RegExp && !pattern.test(value)) report(path, `Field ${label} has to match the pattern ${field.validation}`);
63
- }
64
- function checkRange(report, path, label, field, value) {
65
- const { min, max, step } = field;
66
- if (min !== void 0 && value < min) report(path, `Field ${label} has to be at least ${min}`);
67
- if (max !== void 0 && value > max) report(path, `Field ${label} has to be at most ${max}`);
68
- if (step === void 0 || step <= 0) return;
69
- const base = min ?? 0;
70
- const steps = (value - base) / step;
71
- if (Math.abs(steps - Math.round(steps)) <= 1e-9 * Math.max(1, Math.abs(steps))) return;
72
- 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));
73
- report(path, `Field ${label} has to be in steps of ${step}${base === 0 ? "" : ` from ${base}`}${nearest.length > 0 ? `, such as ${nearest.join(" or ")}` : ""}`);
74
- }
75
- function checkValue(report, path, label, field, value) {
76
- switch (field.type) {
77
- case "text":
78
- if (typeof value !== "string") report(path, `Field ${label} has to be a string`);
79
- else checkPattern(report, path, label, field, value);
80
- return;
81
- case "richtext":
82
- if (typeof value !== "string") report(path, `Field ${label} has to be a string`);
83
- return;
84
- case "number":
85
- if (typeof value !== "number" || !Number.isFinite(value)) report(path, `Field ${label} has to be a number`);
86
- else checkRange(report, path, label, field, value);
87
- return;
88
- case "image":
89
- case "video":
90
- if (field.multiple) checkList(report, path, value, `Field ${label} has to be a list of media objects`, (at, item) => checkMedia(report, at, label, item));
91
- else checkMedia(report, path, label, value);
92
- return;
93
- case "relation":
94
- 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));
95
- else checkId(report, path, label, field.collection, value);
96
- return;
97
- case "dynamic": checkList(report, path, value, `Field ${label} has to be a list of blocks`, (at, item) => checkBlock(report, at, label, field, item));
98
- }
99
- }
100
- function checkTranslations(report, key, field, value, locales) {
101
- if (!isRecord(value)) return report([key], `Field ${quote(key)} is translated and has to hold one value per locale, such as { ${locales[0]}: … }`);
102
- for (const [locale, item] of Object.entries(value)) if (locales.includes(locale)) checkValue(report, [key, locale], `${quote(key)} (${locale})`, field, item);
103
- else report([key, locale], `Field ${quote(key)} has no locale ${quote(locale)}; the schema has ${locales.join(", ")}`);
104
- const missing = field.optional ? [] : locales.filter((locale) => value[locale] === void 0);
105
- if (missing.length > 0) report([key], `Field ${quote(key)} is missing its ${missing.join(", ")} ${missing.length === 1 ? "translation" : "translations"}`);
106
- }
107
- function validateEntry(schema, collection, entry) {
108
- const issues = [];
109
- const report = (path, message) => issues.push({
110
- path,
111
- message
112
- });
113
- const definition = schema.collections[collection];
114
- const locales = schema.locales ?? [];
115
- if (!isRecord(entry)) {
116
- report([], "An entry has to be an object");
117
- return issues;
118
- }
119
- if (!definition) {
120
- report([], `Collection ${quote(collection)} is not in the schema`);
121
- return issues;
122
- }
123
- checkMeta(report, entry);
124
- 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)}`);
125
- for (const [key, field] of Object.entries(definition.fields)) {
126
- const value = entry[key];
127
- if (value === void 0) {
128
- if (!field.optional) report([], `Field ${quote(key)} is required`);
129
- } else if (field.translate && locales.length > 0) checkTranslations(report, key, field, value, locales);
130
- else checkValue(report, [key], quote(key), field, value);
131
- }
132
- return issues;
133
- }
134
- function parse(file) {
135
- try {
136
- return parseModule(file.text, file.path);
137
- } catch (error) {
138
- if (error instanceof ContentError) return error.issues;
139
- throw error;
140
- }
141
- }
142
- function located(file, parsed, issue) {
143
- return {
144
- file,
145
- ...parsed.locate(issue.path),
146
- message: issue.message
147
- };
148
- }
149
- function byPosition(left, right) {
150
- return left.line - right.line || left.column - right.column;
151
- }
152
- function parseContent(schemaFile, entryFiles) {
153
- const schemaModule = parse(schemaFile);
154
- if (!("value" in schemaModule)) return {
155
- issues: [...schemaModule],
156
- schema: void 0,
157
- content: {}
158
- };
159
- const schemaIssues = validateSchema(schemaModule.value);
160
- if (schemaIssues.length > 0) return {
161
- issues: schemaIssues.map((issue) => located(schemaFile.path, schemaModule, issue)),
162
- schema: void 0,
163
- content: {}
164
- };
165
- const schema = schemaModule.value;
166
- const issues = new Map(entryFiles.map((file) => [file.path, []]));
167
- const sources = /* @__PURE__ */ new Map();
168
- const content = {};
169
- for (const file of entryFiles) {
170
- const { collection, id, path } = file;
171
- const found = issues.get(path);
172
- const parsed = parse(file);
173
- if (!("value" in parsed)) {
174
- found.push(...parsed);
175
- continue;
176
- }
177
- found.push(...validateEntry(schema, collection, parsed.value).map((issue) => located(path, parsed, issue)));
178
- if (!isRecord(parsed.value)) continue;
179
- if (typeof parsed.value.id === "string" && parsed.value.id !== id) found.push(located(path, parsed, {
180
- path: ["id"],
181
- message: `"id" is ${quote(parsed.value.id)}, but the file is named ${toEntryFile(id)}`
182
- }));
183
- content[collection] ??= {};
184
- content[collection][id] = parsed.value;
185
- sources.set(entryKey(collection, id), {
186
- path,
187
- parsed
188
- });
189
- }
190
- for (const issue of validateReferences(schema, content)) {
191
- const source = sources.get(entryKey(issue.collection, issue.id));
192
- issues.get(source.path).push(located(source.path, source.parsed, issue));
193
- }
194
- return {
195
- issues: [...issues.values()].flatMap((found) => found.sort(byPosition)),
196
- schema,
197
- content
198
- };
199
- }
200
- function readEntryIds(path) {
201
- if (!existsSync(path)) return [];
202
- return readdirSync(path).filter(isEntryFile).map(toEntryId).sort();
203
- }
204
- function readCollections(root, paths = defaultPaths) {
205
- const base = join(root, paths.content);
206
- if (!existsSync(base)) return [];
207
- return readdirSync(base, { withFileTypes: true }).filter((item) => item.isDirectory()).map((item) => ({
208
- collection: toCollectionName(item.name),
209
- directory: item.name,
210
- path: join(base, item.name),
211
- ids: readEntryIds(join(base, item.name))
212
- })).sort((left, right) => left.collection.localeCompare(right.collection));
213
- }
214
- async function loadContent(root, paths = defaultPaths) {
215
- const read = (path) => readFile(join(root, path), "utf8");
216
- const entries = await Promise.all(readCollections(root, paths).flatMap((directory) => directory.ids.map(async (id) => {
217
- const path = `${paths.content}/${directory.directory}/${toEntryFile(id)}`;
218
- return {
219
- collection: directory.collection,
220
- id,
221
- path,
222
- text: await read(path)
223
- };
224
- })));
225
- return parseContent({
226
- path: paths.schema,
227
- text: await read(paths.schema)
228
- }, entries);
229
- }
230
- async function checkContent(root, paths = defaultPaths) {
231
- return (await loadContent(root, paths)).issues;
232
- }
233
- const COMMIT = /^[\da-f]{40}(?:[\da-f]{24})?$/i;
234
- const COMMIT_VARIABLES = [
235
- "GITHUB_SHA",
236
- "CI_COMMIT_SHA",
237
- "COMMIT_REF",
238
- "VERCEL_GIT_COMMIT_SHA",
239
- "CF_PAGES_COMMIT_SHA",
240
- "RENDER_GIT_COMMIT",
241
- "AWS_COMMIT_ID",
242
- "SOURCE_VERSION"
243
- ];
244
- function gitHead(root) {
245
- try {
246
- return execFileSync("git", ["rev-parse", "HEAD"], {
247
- cwd: root,
248
- encoding: "utf8",
249
- stdio: [
250
- "ignore",
251
- "pipe",
252
- "ignore"
253
- ]
254
- }).trim();
255
- } catch {
256
- return;
257
- }
258
- }
259
- function readCommit(root, env = process.env) {
260
- return [gitHead(root), ...COMMIT_VARIABLES.map((name) => env[name]?.trim())].find((value) => value !== void 0 && COMMIT.test(value))?.toLowerCase() ?? null;
261
- }
262
- const HASHED_FILE = /^[^/]+(?:\/[^/]+)*\/[\w-]+\.[\da-f]{8}\.json$/;
263
- const queues = /* @__PURE__ */ new Map();
264
- function isOutputFile(path) {
265
- return path === "index.json" || HASHED_FILE.test(path);
266
- }
267
- async function place(path, text) {
268
- const temporary = `${path}.${randomUUID()}.tmp`;
269
- await mkdir(dirname(path), { recursive: true });
270
- await writeFile(temporary, text);
271
- await rename(temporary, path);
272
- }
273
- async function listFiles(dir) {
274
- return (await readdir(dir, {
275
- withFileTypes: true,
276
- recursive: true
277
- })).filter((item) => item.isFile()).map((item) => relative(dir, join(item.parentPath, item.name)).split(sep).join("/"));
278
- }
279
- function ancestors(folder) {
280
- return folder === "." || folder === "" ? [] : [folder, ...ancestors(dirname(folder))];
281
- }
282
- async function removeEmpty(path) {
283
- try {
284
- await rmdir(path);
285
- } catch (error) {
286
- const code = error.code;
287
- if (code !== "ENOTEMPTY" && code !== "EEXIST" && code !== "ENOENT") throw error;
288
- }
289
- }
290
- async function prune(dir, folders) {
291
- const candidates = [...new Set(folders.flatMap(ancestors))];
292
- for (const folder of candidates.sort((left, right) => right.split("/").length - left.split("/").length)) await removeEmpty(join(dir, folder));
293
- }
294
- function outputDir(root, config) {
295
- const dir = join(root, config.output.dir);
296
- const inside = relative(root, dir);
297
- if (inside === "" || inside === ".." || inside.startsWith(`..${sep}`) || isAbsolute(inside)) throw new Error(`[forgepress] the output folder has to be inside the project, not ${JSON.stringify(config.output.dir)}`);
298
- return dir;
299
- }
300
- async function write(dir, files) {
301
- const index = files.at(-1);
302
- if (index?.path !== "index.json") throw new Error(`[forgepress] the content output has to end with ${OUTPUT_INDEX}`);
303
- const hashed = files.slice(0, -1).filter((file) => !existsSync(join(dir, file.path)));
304
- await Promise.all(hashed.map((file) => place(join(dir, file.path), file.text)));
305
- await place(join(dir, index.path), index.text);
306
- const kept = new Set(files.map((file) => file.path));
307
- const stale = (await listFiles(dir)).filter((path) => isOutputFile(path) && !kept.has(path));
308
- await Promise.all(stale.map((path) => rm(join(dir, path), { force: true })));
309
- await prune(dir, stale.map((path) => dirname(path)));
310
- }
311
- function writeOutput(dir, files) {
312
- const next = (queues.get(dir) ?? Promise.resolve()).catch(() => void 0).then(() => write(dir, files));
313
- queues.set(dir, next);
314
- return next;
315
- }
316
- async function buildOutput(root, config, options = {}) {
317
- const dir = outputDir(root, config);
318
- const { issues, schema, content } = await loadContent(root, config.paths);
319
- if (!schema || issues.length > 0 && !options.dev) throw new ContentError(issues);
320
- const commit = options.dev ? null : readCommit(root);
321
- const files = await createOutput(schema, content, {
322
- commit,
323
- ...options.dev ? { dev: true } : {}
324
- });
325
- await writeOutput(dir, files);
326
- return {
327
- dir: config.output.dir,
328
- files: files.length,
329
- commit,
330
- issues
331
- };
332
- }
333
- export { buildOutput, checkContent, outputDir, readEntryIds };
@@ -1,22 +0,0 @@
1
- import { isRecord } from "./value.mjs";
2
- import { ContentError, parseModule, validateSchema } from "./validate.mjs";
3
- function parseSchema(text, file) {
4
- const { value, locate } = parseModule(text, file);
5
- const issues = validateSchema(value);
6
- if (issues.length > 0) throw new ContentError(issues.map((issue) => ({
7
- file,
8
- ...locate(issue.path),
9
- message: issue.message
10
- })));
11
- return value;
12
- }
13
- function parseEntry(text, file) {
14
- const { value, locate } = parseModule(text, file);
15
- if (!isRecord(value)) throw new ContentError([{
16
- file,
17
- ...locate([]),
18
- message: "An entry has to be an object"
19
- }]);
20
- return value;
21
- }
22
- export { parseEntry, parseSchema };
@@ -1,2 +0,0 @@
1
- const OUTPUT_INDEX = "index.json";
2
- export { OUTPUT_INDEX };