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
@@ -0,0 +1,282 @@
1
+ import { OUTPUT_META_KEYS, defined, isRecord, isTranslated, pick } from "./value.mjs";
2
+ import { OUTPUT_INDEX } from "./response.mjs";
3
+ const DEFAULT_CONTENT_PATH = ".forgepress";
4
+ const ENTRY_FILE = /\.ts$/;
5
+ const ENTRY_ID = /^[\w-]+$/;
6
+ const COLLECTION_NAME = /^[a-z][a-zA-Z\d]*$/;
7
+ const EDGE_SLASHES = /^\/+|\/+$/g;
8
+ function normalizeDir(path) {
9
+ return (path ?? ".forgepress").replace(/\\/g, "/").replace(EDGE_SLASHES, "") || ".forgepress";
10
+ }
11
+ function toCollectionName(directory) {
12
+ return directory.replace(/-(\w)/g, (_, char) => char.toUpperCase());
13
+ }
14
+ function toCollectionDir(collection) {
15
+ return collection.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`).replace(/^-/, "");
16
+ }
17
+ function toEntryFile(id) {
18
+ return `${id}.ts`;
19
+ }
20
+ function toEntryId(file) {
21
+ return file.replace(ENTRY_FILE, "");
22
+ }
23
+ function isEntryId(id) {
24
+ return ENTRY_ID.test(id);
25
+ }
26
+ function isCollectionName(name) {
27
+ return COLLECTION_NAME.test(name);
28
+ }
29
+ function isEntryFile(file) {
30
+ return ENTRY_FILE.test(file) && isEntryId(toEntryId(file));
31
+ }
32
+ function toEntryRef(content, path) {
33
+ const [directory, file, ...deeper] = path.startsWith(`${content}/`) ? path.slice(content.length + 1).split("/") : [];
34
+ return directory && file && deeper.length === 0 && isEntryFile(file) ? {
35
+ collection: toCollectionName(directory),
36
+ id: toEntryId(file)
37
+ } : void 0;
38
+ }
39
+ function repositoryPath(path) {
40
+ const segments = [];
41
+ for (const segment of path.replace(/\\/g, "/").split("/")) {
42
+ if (segment === "" || segment === ".") continue;
43
+ if (segment !== "..") segments.push(segment);
44
+ else if (segments.pop() === void 0) throw new Error(`[forgepress] ${JSON.stringify(path)} points outside the repository`);
45
+ }
46
+ return segments.join("/");
47
+ }
48
+ function prefixer(base) {
49
+ const prefix = base?.replace(/\\/g, "/").replace(EDGE_SLASHES, "") ?? "";
50
+ return (path) => repositoryPath(prefix ? `${prefix}/${path}` : path);
51
+ }
52
+ function repositoryPaths(paths, base) {
53
+ const at = prefixer(base);
54
+ return {
55
+ dir: at(paths.dir),
56
+ content: at(paths.content),
57
+ schema: at(paths.schema),
58
+ types: at(paths.types),
59
+ collection: (name) => at(paths.collection(name)),
60
+ entry: (name, id) => at(paths.entry(name, id))
61
+ };
62
+ }
63
+ function createPaths(path) {
64
+ const dir = normalizeDir(path);
65
+ const content = `${dir}/content`;
66
+ const collection = (name) => `${content}/${toCollectionDir(name)}`;
67
+ return {
68
+ dir,
69
+ content,
70
+ schema: `${dir}/schema.ts`,
71
+ types: `${dir}/forgepress.d.ts`,
72
+ collection,
73
+ entry: (name, id) => `${collection(name)}/${toEntryFile(id)}`
74
+ };
75
+ }
76
+ const defaultPaths = createPaths();
77
+ function compareCreation(left, right) {
78
+ if (left.createdAt !== right.createdAt) return left.createdAt < right.createdAt ? -1 : 1;
79
+ return left.id < right.id ? -1 : left.id > right.id ? 1 : 0;
80
+ }
81
+ function sortByCreation(entries) {
82
+ return [...entries].sort(compareCreation);
83
+ }
84
+ const CHUNK = 32768;
85
+ function bytesOf(data) {
86
+ return data instanceof Uint8Array ? data : new Uint8Array(data);
87
+ }
88
+ function toBase64(data) {
89
+ const bytes = bytesOf(data);
90
+ const parts = [];
91
+ for (let offset = 0; offset < bytes.length; offset += CHUNK) parts.push(String.fromCharCode(...bytes.subarray(offset, offset + CHUNK)));
92
+ return btoa(parts.join(""));
93
+ }
94
+ function textToBase64(text) {
95
+ return toBase64(new TextEncoder().encode(text));
96
+ }
97
+ function base64ToBytes(data) {
98
+ return Uint8Array.from(atob(data.replace(/\s/g, "")), (char) => char.charCodeAt(0));
99
+ }
100
+ function base64ToText(data) {
101
+ return new TextDecoder().decode(base64ToBytes(data));
102
+ }
103
+ function toHex(data) {
104
+ return [...bytesOf(data)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
105
+ }
106
+ async function digest(algorithm, data) {
107
+ return toHex(await crypto.subtle.digest(algorithm, data));
108
+ }
109
+ function fieldsOf(schema, collection) {
110
+ return Object.entries(schema.collections[collection]?.fields ?? {});
111
+ }
112
+ function translation(stored, locale) {
113
+ return locale !== void 0 && isRecord(stored) ? stored[locale] : void 0;
114
+ }
115
+ function linked(field, value) {
116
+ if (field.type !== "relation") return value;
117
+ const link = (id) => ({
118
+ collection: field.collection,
119
+ id
120
+ });
121
+ return Array.isArray(value) ? value.map(link) : link(value);
122
+ }
123
+ function isLocalized(schema, collection) {
124
+ return fieldsOf(schema, collection).some(([, field]) => isTranslated(field, schema.locales ?? []));
125
+ }
126
+ function indexedFields(schema, collection) {
127
+ return fieldsOf(schema, collection).filter(([, field]) => "index" in field && field.index === true).map(([key]) => key);
128
+ }
129
+ function linkFields(schema, collection) {
130
+ return Object.fromEntries(fieldsOf(schema, collection).flatMap(([key, field]) => field.type === "relation" || field.type === "dynamic" ? [[key, field.type]] : []));
131
+ }
132
+ function toOutputEntry(schema, collection, entry, locale) {
133
+ const locales = schema.locales ?? [];
134
+ const output = pick(entry, OUTPUT_META_KEYS);
135
+ for (const [key, field] of fieldsOf(schema, collection)) {
136
+ const value = isTranslated(field, locales) ? translation(entry[key], locale) : entry[key];
137
+ if (value !== void 0) output[key] = linked(field, value);
138
+ }
139
+ return output;
140
+ }
141
+ const OUTPUT_DEFAULTS = { dir: "public/content" };
142
+ function resolveOutput(config) {
143
+ return {
144
+ ...OUTPUT_DEFAULTS,
145
+ ...defined(config)
146
+ };
147
+ }
148
+ async function hashed(folder, name, value) {
149
+ const text = JSON.stringify(value);
150
+ return {
151
+ path: `${folder}/${name}.${(await digest("SHA-256", new TextEncoder().encode(text))).slice(0, 8)}.json`,
152
+ text
153
+ };
154
+ }
155
+ async function collectionOutput(schema, collection, entries, locale) {
156
+ const folder = locale === void 0 ? toCollectionDir(collection) : `${toCollectionDir(collection)}/${locale}`;
157
+ const indexed = indexedFields(schema, collection);
158
+ const listed = [...OUTPUT_META_KEYS, ...indexed];
159
+ const converted = entries.map((entry) => toOutputEntry(schema, collection, entry, locale));
160
+ const files = await Promise.all(converted.map((entry) => hashed(folder, entry.id, entry)));
161
+ return {
162
+ files,
163
+ manifest: await hashed(folder, "index", {
164
+ indexed,
165
+ links: linkFields(schema, collection),
166
+ entries: converted.map((entry) => pick(entry, listed)),
167
+ files: Object.fromEntries(converted.map((entry, index) => [entry.id, files[index].path]))
168
+ })
169
+ };
170
+ }
171
+ async function createOutput(schema, content, options) {
172
+ const locales = [...schema.locales ?? []];
173
+ const files = [];
174
+ const collections = {};
175
+ for (const collection of Object.keys(schema.collections)) {
176
+ const stored = Object.entries(content[collection] ?? {}).map(([id, entry]) => entry.id === id ? entry : {
177
+ ...entry,
178
+ id
179
+ });
180
+ const entries = sortByCreation(options.unpublished ? stored : stored.filter((entry) => entry.status === "published"));
181
+ if (!isLocalized(schema, collection)) {
182
+ const output = await collectionOutput(schema, collection, entries);
183
+ files.push(...output.files, output.manifest);
184
+ collections[collection] = {
185
+ localized: false,
186
+ manifest: output.manifest.path
187
+ };
188
+ continue;
189
+ }
190
+ const manifests = {};
191
+ for (const locale of locales) {
192
+ const output = await collectionOutput(schema, collection, entries, locale);
193
+ files.push(...output.files, output.manifest);
194
+ manifests[locale] = output.manifest.path;
195
+ }
196
+ collections[collection] = {
197
+ localized: true,
198
+ manifests
199
+ };
200
+ }
201
+ const index = {
202
+ version: 1,
203
+ commit: options.commit,
204
+ ...options.dev ? { dev: true } : {},
205
+ locales,
206
+ collections
207
+ };
208
+ return [...files, {
209
+ path: OUTPUT_INDEX,
210
+ text: `${JSON.stringify(index, null, 2)}\n`
211
+ }];
212
+ }
213
+ const MEDIA_TYPES = {
214
+ avif: "image/avif",
215
+ gif: "image/gif",
216
+ jpeg: "image/jpeg",
217
+ jpg: "image/jpeg",
218
+ png: "image/png",
219
+ svg: "image/svg+xml",
220
+ webp: "image/webp",
221
+ mov: "video/quicktime",
222
+ mp4: "video/mp4",
223
+ ogv: "video/ogg",
224
+ webm: "video/webm"
225
+ };
226
+ const MEDIA_DEFAULTS = {
227
+ dir: "public/uploads",
228
+ url: "/uploads",
229
+ maxSize: 8388608
230
+ };
231
+ const EXTENSION = /\.([a-z0-9]+)$/i;
232
+ const SEPARATOR = /[^a-z0-9]+/gi;
233
+ const TRIM = /^-+|-+$/g;
234
+ const NAME = /^[a-z0-9][\w.-]*$/i;
235
+ function resolveMedia(config) {
236
+ return {
237
+ ...MEDIA_DEFAULTS,
238
+ ...defined(config)
239
+ };
240
+ }
241
+ function extension(file) {
242
+ return EXTENSION.exec(file)?.[1]?.toLowerCase() ?? "";
243
+ }
244
+ function mediaType(file) {
245
+ return MEDIA_TYPES[extension(file)] ?? "";
246
+ }
247
+ function checkUpload(name, size, maxSize) {
248
+ const type = mediaType(name);
249
+ if (!type) throw new Error(`[forgepress] "${name}" is not a supported media file`);
250
+ if (size > maxSize) throw new Error(`[forgepress] "${name}" is larger than the ${Math.round(maxSize / 1024 / 1024)} MB upload limit`);
251
+ return type;
252
+ }
253
+ function sortAssets(assets) {
254
+ return [...assets].sort((left, right) => (right.modifiedAt ?? "").localeCompare(left.modifiedAt ?? "") || left.name.localeCompare(right.name));
255
+ }
256
+ function slugify(value) {
257
+ return value.replace(SEPARATOR, "-").replace(TRIM, "").toLowerCase().slice(0, 48) || "file";
258
+ }
259
+ function toAssetName(file, hash) {
260
+ const found = EXTENSION.exec(file);
261
+ return `${slugify(found ? file.slice(0, -found[0].length) : file)}.${hash.slice(0, 8)}${found ? `.${found[1].toLowerCase()}` : ""}`;
262
+ }
263
+ async function assetName(file, data) {
264
+ return toAssetName(file, await digest("SHA-256", data));
265
+ }
266
+ function isAssetName(name) {
267
+ return NAME.test(name) && !name.includes("..");
268
+ }
269
+ function isMediaFile(name) {
270
+ return isAssetName(name) && mediaType(name) !== "";
271
+ }
272
+ function assetUrl(prefix, name) {
273
+ return `${prefix.replace(/\/+$/, "")}/${name}`;
274
+ }
275
+ function storedAsset(name, prefix) {
276
+ return {
277
+ name,
278
+ url: assetUrl(prefix, name),
279
+ type: mediaType(name)
280
+ };
281
+ }
282
+ export { DEFAULT_CONTENT_PATH, assetName, assetUrl, base64ToText, checkUpload, createOutput, createPaths, defaultPaths, isAssetName, isCollectionName, isEntryFile, isEntryId, isMediaFile, mediaType, prefixer, repositoryPaths, resolveMedia, resolveOutput, sortAssets, sortByCreation, storedAsset, textToBase64, toEntryFile, toEntryId, toEntryRef };
@@ -0,0 +1,16 @@
1
+ function keyed() {
2
+ const pending = /* @__PURE__ */ new Map();
3
+ return (key, load) => {
4
+ const found = pending.get(key);
5
+ if (found) return found;
6
+ const loading = load();
7
+ pending.set(key, loading);
8
+ loading.catch(() => pending.delete(key));
9
+ return loading;
10
+ };
11
+ }
12
+ function once(load) {
13
+ const loads = keyed();
14
+ return () => loads("", load);
15
+ }
16
+ export { keyed, once };
@@ -1,258 +1,104 @@
1
- import { defined, isRecord } from "./value.mjs";
2
- import { OUTPUT_INDEX } from "./types.mjs";
3
- const DEFAULT_CONTENT_PATH = ".forgepress";
4
- const ENDPOINT = "/__forgepress";
5
- const ENTRY_FILE = /\.ts$/;
6
- const ENTRY_ID = /^[\w-]+$/;
7
- const COLLECTION_NAME = /^[a-z][a-zA-Z\d]*$/;
8
- const EDGE_SLASHES = /^\/+|\/+$/g;
9
- function normalizeDir(path) {
10
- return (path ?? ".forgepress").replace(/\\/g, "/").replace(EDGE_SLASHES, "") || ".forgepress";
11
- }
12
- function toCollectionName(directory) {
13
- return directory.replace(/-(\w)/g, (_, char) => char.toUpperCase());
14
- }
15
- function toCollectionDir(collection) {
16
- return collection.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`).replace(/^-/, "");
17
- }
18
- function toEntryFile(id) {
19
- return `${id}.ts`;
20
- }
21
- function toEntryId(file) {
22
- return file.replace(ENTRY_FILE, "");
23
- }
24
- function isEntryId(id) {
25
- return ENTRY_ID.test(id);
26
- }
27
- function isCollectionName(name) {
28
- return COLLECTION_NAME.test(name);
29
- }
30
- function isEntryFile(file) {
31
- return ENTRY_FILE.test(file) && isEntryId(toEntryId(file));
32
- }
33
- function prefixer(base) {
34
- const prefix = base?.replace(EDGE_SLASHES, "") ?? "";
35
- return (path) => prefix ? `${prefix}/${path}` : path;
36
- }
37
- function createPaths(path) {
38
- const dir = normalizeDir(path);
39
- const content = `${dir}/content`;
40
- const collection = (name) => `${content}/${toCollectionDir(name)}`;
41
- return {
42
- dir,
43
- content,
44
- schema: `${dir}/schema.ts`,
45
- types: `${dir}/forgepress.d.ts`,
46
- collection,
47
- entry: (name, id) => `${collection(name)}/${toEntryFile(id)}`
48
- };
49
- }
50
- const defaultPaths = createPaths();
51
- const MEDIA_TYPES = {
52
- avif: "image/avif",
53
- gif: "image/gif",
54
- jpeg: "image/jpeg",
55
- jpg: "image/jpeg",
56
- png: "image/png",
57
- svg: "image/svg+xml",
58
- webp: "image/webp",
59
- mov: "video/quicktime",
60
- mp4: "video/mp4",
61
- ogv: "video/ogg",
62
- webm: "video/webm"
63
- };
64
- const MEDIA_DEFAULTS = {
65
- dir: "public/uploads",
66
- url: "/uploads",
67
- maxSize: 8388608
68
- };
69
- const EXTENSION = /\.([a-z0-9]+)$/i;
70
- const SEPARATOR = /[^a-z0-9]+/gi;
71
- const TRIM = /^-+|-+$/g;
72
- const NAME = /^[a-z0-9][\w.-]*$/i;
73
- function resolveMedia(config) {
74
- return {
75
- ...MEDIA_DEFAULTS,
76
- ...defined(config)
77
- };
78
- }
79
- function extension(file) {
80
- return EXTENSION.exec(file)?.[1]?.toLowerCase() ?? "";
81
- }
82
- function mediaType(file) {
83
- return MEDIA_TYPES[extension(file)] ?? "";
84
- }
85
- function checkUpload(name, size, maxSize) {
86
- const type = mediaType(name);
87
- if (!type) throw new Error(`[forgepress] "${name}" is not a supported media file`);
88
- if (size > maxSize) throw new Error(`[forgepress] "${name}" is larger than the ${Math.round(maxSize / 1024 / 1024)} MB upload limit`);
89
- return type;
90
- }
91
- function sortAssets(assets) {
92
- return [...assets].sort((left, right) => (right.modifiedAt ?? "").localeCompare(left.modifiedAt ?? "") || left.name.localeCompare(right.name));
93
- }
94
- function slugify(value) {
95
- return value.replace(SEPARATOR, "-").replace(TRIM, "").toLowerCase().slice(0, 48) || "file";
96
- }
97
- function toAssetName(file, hash) {
98
- const found = EXTENSION.exec(file);
99
- return `${slugify(found ? file.slice(0, -found[0].length) : file)}.${hash.slice(0, 8)}${found ? `.${found[1].toLowerCase()}` : ""}`;
100
- }
101
- function isAssetName(name) {
102
- return NAME.test(name) && !name.includes("..");
103
- }
104
- function isMediaFile(name) {
105
- return isAssetName(name) && mediaType(name) !== "";
106
- }
107
- function assetUrl(prefix, name) {
108
- return `${prefix.replace(/\/+$/, "")}/${name}`;
109
- }
110
- function compareCreation(left, right) {
111
- if (left.createdAt !== right.createdAt) return left.createdAt < right.createdAt ? -1 : 1;
112
- return left.id < right.id ? -1 : left.id > right.id ? 1 : 0;
113
- }
114
- function sortByCreation(entries) {
115
- return [...entries].sort(compareCreation);
116
- }
117
- const CHUNK = 32768;
118
- function bytesOf(data) {
119
- return data instanceof Uint8Array ? data : new Uint8Array(data);
120
- }
121
- function toBase64(data) {
122
- const bytes = bytesOf(data);
123
- const parts = [];
124
- for (let offset = 0; offset < bytes.length; offset += CHUNK) parts.push(String.fromCharCode(...bytes.subarray(offset, offset + CHUNK)));
125
- return btoa(parts.join(""));
126
- }
127
- function textToBase64(text) {
128
- return toBase64(new TextEncoder().encode(text));
129
- }
130
- function base64ToBytes(data) {
131
- return Uint8Array.from(atob(data.replace(/\s/g, "")), (char) => char.charCodeAt(0));
132
- }
133
- function base64ToText(data) {
134
- return new TextDecoder().decode(base64ToBytes(data));
135
- }
136
- function toHex(data) {
137
- return [...bytesOf(data)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
138
- }
139
- async function digest(algorithm, data) {
140
- return toHex(await crypto.subtle.digest(algorithm, data));
141
- }
142
- function fieldsOf(schema, collection) {
143
- return Object.entries(schema.collections[collection]?.fields ?? {});
144
- }
145
- function localized(field, stored, locale) {
146
- if (field.translate !== true) return stored;
147
- return locale !== void 0 && isRecord(stored) ? stored[locale] : void 0;
148
- }
149
- function linked(field, value) {
150
- if (field.type !== "relation") return value;
151
- const link = (id) => ({
152
- collection: field.collection,
153
- id
154
- });
155
- return Array.isArray(value) ? value.map(link) : link(value);
156
- }
157
- function isLocalized(schema, collection) {
158
- return (schema.locales ?? []).length > 0 && fieldsOf(schema, collection).some(([, field]) => field.translate === true);
159
- }
160
- function indexedFields(schema, collection) {
161
- return fieldsOf(schema, collection).filter(([, field]) => "index" in field && field.index === true).map(([key]) => key);
162
- }
163
- function linkFields(schema, collection) {
164
- return Object.fromEntries(fieldsOf(schema, collection).flatMap(([key, field]) => field.type === "relation" || field.type === "dynamic" ? [[key, field.type]] : []));
165
- }
166
- function toOutputEntry(schema, collection, entry, locale) {
167
- const output = {
168
- id: entry.id,
169
- createdAt: entry.createdAt,
170
- updatedAt: entry.updatedAt
171
- };
172
- for (const [key, field] of fieldsOf(schema, collection)) {
173
- const value = localized(field, entry[key], locale);
174
- if (value !== void 0) output[key] = linked(field, value);
1
+ import { OUTPUT_INDEX } from "./response.mjs";
2
+ import { createOutput } from "./media.mjs";
3
+ import { ContentError, readContent } from "./content.mjs";
4
+ import { diskFiles, isInside, listFiles } from "./files.mjs";
5
+ import { mkdir, rename, rm, rmdir, writeFile } from "node:fs/promises";
6
+ import { dirname, join } from "node:path";
7
+ import process from "node:process";
8
+ import { randomUUID } from "node:crypto";
9
+ import { existsSync } from "node:fs";
10
+ import { execFileSync } from "node:child_process";
11
+ const COMMIT = /^[\da-f]{40}(?:[\da-f]{24})?$/i;
12
+ const COMMIT_VARIABLES = [
13
+ "GITHUB_SHA",
14
+ "CI_COMMIT_SHA",
15
+ "COMMIT_REF",
16
+ "VERCEL_GIT_COMMIT_SHA",
17
+ "CF_PAGES_COMMIT_SHA",
18
+ "RENDER_GIT_COMMIT",
19
+ "AWS_COMMIT_ID",
20
+ "SOURCE_VERSION"
21
+ ];
22
+ function gitHead(root) {
23
+ try {
24
+ return execFileSync("git", ["rev-parse", "HEAD"], {
25
+ cwd: root,
26
+ encoding: "utf8",
27
+ stdio: [
28
+ "ignore",
29
+ "pipe",
30
+ "ignore"
31
+ ]
32
+ }).trim();
33
+ } catch {
34
+ return;
175
35
  }
176
- return output;
177
- }
178
- const OUTPUT_DEFAULTS = { dir: "public/content" };
179
- function resolveOutput(config) {
180
- return {
181
- ...OUTPUT_DEFAULTS,
182
- ...defined(config)
183
- };
184
- }
185
- async function hashed(folder, name, value) {
186
- const text = JSON.stringify(value);
187
- return {
188
- path: `${folder}/${name}.${(await digest("SHA-256", new TextEncoder().encode(text))).slice(0, 8)}.json`,
189
- text
190
- };
191
36
  }
192
- function listed(entry, indexed) {
193
- const item = {
194
- id: entry.id,
195
- createdAt: entry.createdAt,
196
- updatedAt: entry.updatedAt
197
- };
198
- for (const field of indexed) if (entry[field] !== void 0) item[field] = entry[field];
199
- return item;
37
+ function readCommit(root, env = process.env) {
38
+ return [gitHead(root), ...COMMIT_VARIABLES.map((name) => env[name]?.trim())].find((value) => value !== void 0 && COMMIT.test(value))?.toLowerCase() ?? null;
39
+ }
40
+ const HASHED_FILE = /^[^/]+(?:\/[^/]+)*\/[\w-]+\.[\da-f]{8}\.json$/;
41
+ const queues = /* @__PURE__ */ new Map();
42
+ function isOutputFile(path) {
43
+ return path === "index.json" || HASHED_FILE.test(path);
44
+ }
45
+ async function place(path, text) {
46
+ const temporary = `${path}.${randomUUID()}.tmp`;
47
+ await mkdir(dirname(path), { recursive: true });
48
+ await writeFile(temporary, text);
49
+ await rename(temporary, path);
50
+ }
51
+ function ancestors(folder) {
52
+ return folder === "." || folder === "" ? [] : [folder, ...ancestors(dirname(folder))];
53
+ }
54
+ async function removeEmpty(path) {
55
+ try {
56
+ await rmdir(path);
57
+ } catch (error) {
58
+ const code = error.code;
59
+ if (code !== "ENOTEMPTY" && code !== "EEXIST" && code !== "ENOENT") throw error;
60
+ }
200
61
  }
201
- async function collectionOutput(schema, collection, entries, locale) {
202
- const folder = locale === void 0 ? toCollectionDir(collection) : `${toCollectionDir(collection)}/${locale}`;
203
- const indexed = indexedFields(schema, collection);
204
- const converted = entries.map((entry) => toOutputEntry(schema, collection, entry, locale));
205
- const files = await Promise.all(converted.map((entry) => hashed(folder, entry.id, entry)));
62
+ async function prune(dir, folders) {
63
+ const candidates = [...new Set(folders.flatMap(ancestors))];
64
+ for (const folder of candidates.sort((left, right) => right.split("/").length - left.split("/").length)) await removeEmpty(join(dir, folder));
65
+ }
66
+ function outputDir(root, config) {
67
+ const dir = join(root, config.output.dir);
68
+ if (!isInside(root, dir)) throw new Error(`[forgepress] the output folder has to be inside the project, not ${JSON.stringify(config.output.dir)}`);
69
+ return dir;
70
+ }
71
+ async function write(dir, files) {
72
+ const index = files.at(-1);
73
+ if (index?.path !== "index.json") throw new Error(`[forgepress] the content output has to end with ${OUTPUT_INDEX}`);
74
+ const hashed = files.slice(0, -1).filter((file) => !existsSync(join(dir, file.path)));
75
+ await Promise.all(hashed.map((file) => place(join(dir, file.path), file.text)));
76
+ await place(join(dir, index.path), index.text);
77
+ const kept = new Set(files.map((file) => file.path));
78
+ const stale = (await listFiles(dir)).filter((path) => isOutputFile(path) && !kept.has(path));
79
+ await Promise.all(stale.map((path) => rm(join(dir, path), { force: true })));
80
+ await prune(dir, stale.map((path) => dirname(path)));
81
+ }
82
+ function writeOutput(dir, files) {
83
+ const next = (queues.get(dir) ?? Promise.resolve()).catch(() => void 0).then(() => write(dir, files));
84
+ queues.set(dir, next);
85
+ return next;
86
+ }
87
+ async function buildOutput(root, config, options = {}) {
88
+ const dir = outputDir(root, config);
89
+ const { issues, schema, content } = await readContent(diskFiles(root), config.paths);
90
+ if (!schema || issues.length > 0 && !options.dev) throw new ContentError(issues);
91
+ const commit = options.dev ? null : readCommit(root);
92
+ const files = await createOutput(schema, content, {
93
+ commit,
94
+ ...options.dev ? { dev: true } : {}
95
+ });
96
+ await writeOutput(dir, files);
206
97
  return {
207
- files,
208
- manifest: await hashed(folder, "index", {
209
- indexed,
210
- links: linkFields(schema, collection),
211
- entries: converted.map((entry) => listed(entry, indexed)),
212
- files: Object.fromEntries(converted.map((entry, index) => [entry.id, files[index].path]))
213
- })
214
- };
215
- }
216
- async function createOutput(schema, content, options) {
217
- const locales = [...schema.locales ?? []];
218
- const files = [];
219
- const collections = {};
220
- for (const collection of Object.keys(schema.collections)) {
221
- const stored = Object.entries(content[collection] ?? {}).map(([id, entry]) => entry.id === id ? entry : {
222
- ...entry,
223
- id
224
- });
225
- const entries = sortByCreation(options.unpublished ? stored : stored.filter((entry) => entry.status === "published"));
226
- if (!isLocalized(schema, collection)) {
227
- const output = await collectionOutput(schema, collection, entries);
228
- files.push(...output.files, output.manifest);
229
- collections[collection] = {
230
- localized: false,
231
- manifest: output.manifest.path
232
- };
233
- continue;
234
- }
235
- const manifests = {};
236
- for (const locale of locales) {
237
- const output = await collectionOutput(schema, collection, entries, locale);
238
- files.push(...output.files, output.manifest);
239
- manifests[locale] = output.manifest.path;
240
- }
241
- collections[collection] = {
242
- localized: true,
243
- manifests
244
- };
245
- }
246
- const index = {
247
- version: 1,
248
- commit: options.commit,
249
- ...options.dev ? { dev: true } : {},
250
- locales,
251
- collections
98
+ dir: config.output.dir,
99
+ files: files.length,
100
+ commit,
101
+ issues
252
102
  };
253
- return [...files, {
254
- path: OUTPUT_INDEX,
255
- text: `${JSON.stringify(index, null, 2)}\n`
256
- }];
257
103
  }
258
- export { DEFAULT_CONTENT_PATH, ENDPOINT, assetUrl, base64ToText, checkUpload, createOutput, createPaths, defaultPaths, isAssetName, isCollectionName, isEntryFile, isEntryId, isMediaFile, mediaType, prefixer, resolveMedia, resolveOutput, sortAssets, sortByCreation, textToBase64, toAssetName, toCollectionName, toEntryFile, toEntryId };
104
+ export { buildOutput, outputDir };