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
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { META_KEYS } from "./value.mjs";
|
|
2
|
+
const IDENTIFIER = /^[A-Z_$][\w$]*$/i;
|
|
3
|
+
const WIDTH = 80;
|
|
4
|
+
const ESCAPES = {
|
|
5
|
+
"\\": "\\\\",
|
|
6
|
+
"'": "\\'",
|
|
7
|
+
"\n": "\\n",
|
|
8
|
+
"\r": "\\r",
|
|
9
|
+
" ": "\\t"
|
|
10
|
+
};
|
|
11
|
+
function style(config) {
|
|
12
|
+
return {
|
|
13
|
+
indent: " ".repeat(config?.indent ?? 2),
|
|
14
|
+
semi: config?.semi ? ";" : ""
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function unsafe(code) {
|
|
18
|
+
return code < 32 || code >= 127 && code <= 159 || code === 8232 || code === 8233;
|
|
19
|
+
}
|
|
20
|
+
function string(value) {
|
|
21
|
+
let out = "";
|
|
22
|
+
for (const char of value) {
|
|
23
|
+
const escaped = ESCAPES[char];
|
|
24
|
+
const code = char.charCodeAt(0);
|
|
25
|
+
if (escaped !== void 0) out += escaped;
|
|
26
|
+
else if (unsafe(code)) out += `\\u${code.toString(16).padStart(4, "0")}`;
|
|
27
|
+
else out += char;
|
|
28
|
+
}
|
|
29
|
+
return `'${out}'`;
|
|
30
|
+
}
|
|
31
|
+
function key(value) {
|
|
32
|
+
return IDENTIFIER.test(value) ? value : string(value);
|
|
33
|
+
}
|
|
34
|
+
function storable(input) {
|
|
35
|
+
return input !== void 0 && input !== null && !(typeof input === "number" && !Number.isFinite(input));
|
|
36
|
+
}
|
|
37
|
+
function value(input, style, depth) {
|
|
38
|
+
if (typeof input === "string") return string(input);
|
|
39
|
+
if (typeof input === "number") return String(input);
|
|
40
|
+
if (typeof input === "boolean") return String(input);
|
|
41
|
+
const pad = style.indent.repeat(depth + 1);
|
|
42
|
+
const close = style.indent.repeat(depth);
|
|
43
|
+
if (Array.isArray(input)) {
|
|
44
|
+
const kept = input.filter(storable);
|
|
45
|
+
if (kept.length === 0) return "[]";
|
|
46
|
+
const items = kept.map((item) => value(item, style, depth + 1));
|
|
47
|
+
const inline = `[${items.join(", ")}]`;
|
|
48
|
+
if (kept.every((item) => typeof item !== "object") && close.length + inline.length <= WIDTH) return inline;
|
|
49
|
+
return `[\n${items.map((item) => `${pad}${item},`).join("\n")}\n${close}]`;
|
|
50
|
+
}
|
|
51
|
+
const entries = Object.entries(input).filter(([, item]) => storable(item));
|
|
52
|
+
if (entries.length === 0) return "{}";
|
|
53
|
+
return `{\n${entries.map(([name, item]) => `${pad}${key(name)}: ${value(item, style, depth + 1)},`).join("\n")}\n${close}}`;
|
|
54
|
+
}
|
|
55
|
+
function serializeSchema(schema, config) {
|
|
56
|
+
const current = style(config);
|
|
57
|
+
return [
|
|
58
|
+
`import type { ForgePressSchema } from 'forgepress'${current.semi}`,
|
|
59
|
+
"",
|
|
60
|
+
`export default ${value(schema, current, 0)} as const satisfies ForgePressSchema${current.semi}`,
|
|
61
|
+
""
|
|
62
|
+
].join("\n");
|
|
63
|
+
}
|
|
64
|
+
function ordered(row) {
|
|
65
|
+
const entries = Object.entries(row).filter(([, item]) => storable(item));
|
|
66
|
+
const rank = (name) => {
|
|
67
|
+
const index = META_KEYS.indexOf(name);
|
|
68
|
+
return index === -1 ? META_KEYS.length : index;
|
|
69
|
+
};
|
|
70
|
+
return entries.sort(([left], [right]) => rank(left) - rank(right));
|
|
71
|
+
}
|
|
72
|
+
function serializeEntry(collection, row, config) {
|
|
73
|
+
const current = style(config);
|
|
74
|
+
const body = ordered(row).map(([name, item]) => `${current.indent}${key(name)}: ${value(item, current, 1)},`).join("\n");
|
|
75
|
+
return [
|
|
76
|
+
`import type { ForgePressEntry } from 'forgepress'${current.semi}`,
|
|
77
|
+
"",
|
|
78
|
+
`export default {\n${body}\n} satisfies ForgePressEntry<${string(collection)}>${current.semi}`,
|
|
79
|
+
""
|
|
80
|
+
].join("\n");
|
|
81
|
+
}
|
|
82
|
+
export { serializeEntry, serializeSchema };
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { isRecord } from "./value.mjs";
|
|
2
|
+
import { errorMessage } from "./error.mjs";
|
|
3
|
+
import { assetName, checkUpload, defaultPaths, isAssetName, isCollectionName, isEntryFile, isEntryId, isMediaFile, mediaType, resolveMedia, sortAssets, storedAsset, toEntryId } from "./media.mjs";
|
|
4
|
+
import { ContentError, createFileSource, validateSchema } from "./content.mjs";
|
|
5
|
+
import { buildOutput, outputDir } from "./output.mjs";
|
|
6
|
+
import { diskFiles, listFiles } from "./files.mjs";
|
|
7
|
+
import { ENDPOINT, ROUTES } from "./routes.mjs";
|
|
8
|
+
import { serializeEntry, serializeSchema } from "./serialize.mjs";
|
|
9
|
+
import { findRoot, loadConfig, resolveConfig } from "./config.mjs";
|
|
10
|
+
import { OUTPUT_VARIABLE } from "./reader.mjs";
|
|
11
|
+
import { mkdir, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
12
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
13
|
+
import process from "node:process";
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { debounce } from "perfect-debounce";
|
|
16
|
+
import { Buffer } from "node:buffer";
|
|
17
|
+
function createMediaStore(root, config) {
|
|
18
|
+
const media = resolveMedia(config);
|
|
19
|
+
const dir = join(root, media.dir);
|
|
20
|
+
async function describe(name) {
|
|
21
|
+
const info = await stat(join(dir, name));
|
|
22
|
+
return {
|
|
23
|
+
...storedAsset(name, media.url),
|
|
24
|
+
size: info.size,
|
|
25
|
+
modifiedAt: info.mtime.toISOString()
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
async list() {
|
|
30
|
+
if (!existsSync(dir)) return [];
|
|
31
|
+
const files = (await readdir(dir)).filter(isMediaFile);
|
|
32
|
+
return sortAssets(await Promise.all(files.map(describe)));
|
|
33
|
+
},
|
|
34
|
+
async write({ name, data }) {
|
|
35
|
+
checkUpload(name, data.byteLength, media.maxSize);
|
|
36
|
+
const file = await assetName(name, data);
|
|
37
|
+
await mkdir(dir, { recursive: true });
|
|
38
|
+
await writeFile(join(dir, file), data);
|
|
39
|
+
return describe(file);
|
|
40
|
+
},
|
|
41
|
+
async remove(name) {
|
|
42
|
+
if (!isAssetName(name)) throw new Error(`[forgepress] "${name}" is not a valid asset name`);
|
|
43
|
+
await rm(join(dir, name), { force: true });
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function collectionName$1(collection) {
|
|
48
|
+
if (!isCollectionName(collection)) throw new Error(`[forgepress] ${JSON.stringify(collection)} is not a collection name`);
|
|
49
|
+
return collection;
|
|
50
|
+
}
|
|
51
|
+
function entryId$1(id) {
|
|
52
|
+
if (typeof id !== "string" || !isEntryId(id)) throw new Error(`[forgepress] ${JSON.stringify(id)} is not an entry id`);
|
|
53
|
+
return id;
|
|
54
|
+
}
|
|
55
|
+
function createWriter(root, paths = defaultPaths, config) {
|
|
56
|
+
const directory = (collection) => join(root, paths.collection(collectionName$1(collection)));
|
|
57
|
+
const file = (collection, id) => join(root, paths.entry(collectionName$1(collection), entryId$1(id)));
|
|
58
|
+
return {
|
|
59
|
+
async writeSchema(schema) {
|
|
60
|
+
await mkdir(join(root, paths.dir), { recursive: true });
|
|
61
|
+
await writeFile(join(root, paths.schema), serializeSchema(schema, config));
|
|
62
|
+
},
|
|
63
|
+
async writeEntry(collection, row) {
|
|
64
|
+
const target = file(collection, row.id);
|
|
65
|
+
await mkdir(directory(collection), { recursive: true });
|
|
66
|
+
await writeFile(target, serializeEntry(collection, row, config));
|
|
67
|
+
},
|
|
68
|
+
async removeEntry(collection, id) {
|
|
69
|
+
await rm(file(collection, id), { force: true });
|
|
70
|
+
},
|
|
71
|
+
async writeContent(collection, rows) {
|
|
72
|
+
const targets = rows.map((row) => [file(collection, row.id), serializeEntry(collection, row, config)]);
|
|
73
|
+
const kept = new Set(rows.map((row) => row.id));
|
|
74
|
+
await mkdir(directory(collection), { recursive: true });
|
|
75
|
+
const stale = (await listFiles(directory(collection))).filter(isEntryFile).map(toEntryId).filter((id) => !kept.has(id));
|
|
76
|
+
await Promise.all(stale.map((id) => rm(file(collection, id), { force: true })));
|
|
77
|
+
await Promise.all(targets.map(([target, text]) => writeFile(target, text)));
|
|
78
|
+
},
|
|
79
|
+
async removeCollection(collection) {
|
|
80
|
+
await rm(directory(collection), {
|
|
81
|
+
recursive: true,
|
|
82
|
+
force: true
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
var EndpointError = class extends Error {
|
|
88
|
+
status;
|
|
89
|
+
constructor(status, message) {
|
|
90
|
+
super(message);
|
|
91
|
+
this.name = "EndpointError";
|
|
92
|
+
this.status = status;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
function sameOrigin(request) {
|
|
96
|
+
const site = request.headers["sec-fetch-site"];
|
|
97
|
+
if (site !== void 0) return site === "same-origin" || site === "none";
|
|
98
|
+
const { origin, host } = request.headers;
|
|
99
|
+
if (origin === void 0) return true;
|
|
100
|
+
try {
|
|
101
|
+
return new URL(origin).host === host;
|
|
102
|
+
} catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const SAME_ORIGIN = {
|
|
107
|
+
accepts: sameOrigin,
|
|
108
|
+
refusal: "the dev endpoint only accepts requests from pages on its own origin"
|
|
109
|
+
};
|
|
110
|
+
function missing(path) {
|
|
111
|
+
return new EndpointError(404, `there is no endpoint ${JSON.stringify(path)}`);
|
|
112
|
+
}
|
|
113
|
+
async function bytes(request) {
|
|
114
|
+
const chunks = [];
|
|
115
|
+
for await (const chunk of request) chunks.push(chunk);
|
|
116
|
+
return Buffer.concat(chunks);
|
|
117
|
+
}
|
|
118
|
+
async function body(request) {
|
|
119
|
+
const text = (await bytes(request)).toString("utf8");
|
|
120
|
+
try {
|
|
121
|
+
return JSON.parse(text);
|
|
122
|
+
} catch {
|
|
123
|
+
throw new EndpointError(400, "the request body is not valid JSON");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function json(response, payload) {
|
|
127
|
+
response.statusCode = 200;
|
|
128
|
+
response.setHeader("content-type", "application/json");
|
|
129
|
+
response.end(JSON.stringify(payload));
|
|
130
|
+
}
|
|
131
|
+
function done(response) {
|
|
132
|
+
response.statusCode = 204;
|
|
133
|
+
response.end();
|
|
134
|
+
}
|
|
135
|
+
function decode(value, path) {
|
|
136
|
+
try {
|
|
137
|
+
return decodeURIComponent(value);
|
|
138
|
+
} catch {
|
|
139
|
+
throw new EndpointError(400, `${JSON.stringify(path)} is not a valid path`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function under(path, route) {
|
|
143
|
+
return path.startsWith(`${route}/`);
|
|
144
|
+
}
|
|
145
|
+
function segments(path, route, count) {
|
|
146
|
+
const found = path.slice(route.length + 1).split("/").filter(Boolean).map((segment) => decode(segment, path));
|
|
147
|
+
if (found.length > count) throw missing(path);
|
|
148
|
+
return found;
|
|
149
|
+
}
|
|
150
|
+
function collectionName(name) {
|
|
151
|
+
if (name === void 0 || !isCollectionName(name)) throw new EndpointError(400, `${JSON.stringify(name ?? "")} is not a collection name`);
|
|
152
|
+
return name;
|
|
153
|
+
}
|
|
154
|
+
function entryId(id) {
|
|
155
|
+
if (typeof id !== "string" || !isEntryId(id)) throw new EndpointError(400, `${JSON.stringify(id ?? "")} is not an entry id`);
|
|
156
|
+
return id;
|
|
157
|
+
}
|
|
158
|
+
function entry(value) {
|
|
159
|
+
if (!isRecord(value)) throw new EndpointError(400, "an entry has to be an object");
|
|
160
|
+
entryId(value.id);
|
|
161
|
+
return value;
|
|
162
|
+
}
|
|
163
|
+
async function media(config, root, path, request, response) {
|
|
164
|
+
const store = createMediaStore(root, config.media);
|
|
165
|
+
if (request.method === "GET") return json(response, await store.list());
|
|
166
|
+
const name = decode(path.slice(ROUTES.media.length + 1), path);
|
|
167
|
+
if (request.method === "DELETE") {
|
|
168
|
+
if (!isAssetName(name)) throw new EndpointError(400, `${JSON.stringify(name)} is not an asset name`);
|
|
169
|
+
await store.remove(name);
|
|
170
|
+
return done(response);
|
|
171
|
+
}
|
|
172
|
+
if (!mediaType(name)) throw new EndpointError(400, `${JSON.stringify(name)} is not a supported media file`);
|
|
173
|
+
return json(response, await store.write({
|
|
174
|
+
name,
|
|
175
|
+
data: await bytes(request)
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
async function read(config, root, path, response) {
|
|
179
|
+
const source = createFileSource(diskFiles(root), config.paths);
|
|
180
|
+
if (path === ROUTES.schema) return json(response, await source.schema());
|
|
181
|
+
if (under(path, ROUTES.content)) {
|
|
182
|
+
const [collection = ""] = segments(path, ROUTES.content, 1);
|
|
183
|
+
const schema = await source.schema();
|
|
184
|
+
return json(response, Object.hasOwn(schema.collections, collection) ? await source.list(collection) : []);
|
|
185
|
+
}
|
|
186
|
+
if (!under(path, ROUTES.entry)) throw missing(path);
|
|
187
|
+
const [collection = "", id = ""] = segments(path, ROUTES.entry, 2);
|
|
188
|
+
const schema = await source.schema();
|
|
189
|
+
const row = Object.hasOwn(schema.collections, collection) && isEntryId(id) ? await source.entry(collection, id) : void 0;
|
|
190
|
+
if (row) return json(response, row);
|
|
191
|
+
response.statusCode = 404;
|
|
192
|
+
response.end();
|
|
193
|
+
}
|
|
194
|
+
async function write(config, root, path, request, response) {
|
|
195
|
+
const writer = createWriter(root, config.paths, config.content);
|
|
196
|
+
const removing = request.method === "DELETE";
|
|
197
|
+
if (path === ROUTES.schema && !removing) {
|
|
198
|
+
const schema = await body(request);
|
|
199
|
+
const issues = validateSchema(schema);
|
|
200
|
+
if (issues.length > 0) throw new EndpointError(400, `the schema is not valid:\n${issues.map((issue) => issue.message).join("\n")}`);
|
|
201
|
+
await writer.writeSchema(schema);
|
|
202
|
+
} else if (under(path, ROUTES.entry)) {
|
|
203
|
+
const [name, id] = segments(path, ROUTES.entry, 2);
|
|
204
|
+
const collection = collectionName(name);
|
|
205
|
+
if (removing) await writer.removeEntry(collection, entryId(id));
|
|
206
|
+
else {
|
|
207
|
+
const row = entry(await body(request));
|
|
208
|
+
if (row.id !== entryId(id)) throw new EndpointError(400, `the entry id ${JSON.stringify(row.id)} doesn't match the path`);
|
|
209
|
+
await writer.writeEntry(collection, row);
|
|
210
|
+
}
|
|
211
|
+
} else if (under(path, ROUTES.content)) {
|
|
212
|
+
const [name] = segments(path, ROUTES.content, 1);
|
|
213
|
+
const collection = collectionName(name);
|
|
214
|
+
if (removing) await writer.removeCollection(collection);
|
|
215
|
+
else {
|
|
216
|
+
const rows = await body(request);
|
|
217
|
+
if (!Array.isArray(rows)) throw new EndpointError(400, "the entries have to be a list");
|
|
218
|
+
await writer.writeContent(collection, rows.map(entry));
|
|
219
|
+
}
|
|
220
|
+
} else throw missing(path);
|
|
221
|
+
done(response);
|
|
222
|
+
}
|
|
223
|
+
async function handle(config, root, request, response, origins = SAME_ORIGIN) {
|
|
224
|
+
if (!origins.accepts(request)) throw new EndpointError(403, origins.refusal);
|
|
225
|
+
const path = (request.url ?? "").slice(ENDPOINT.length);
|
|
226
|
+
if (path === ROUTES.media || under(path, ROUTES.media)) return media(config, root, path, request, response);
|
|
227
|
+
if (request.method === "GET") return read(config, root, path, response);
|
|
228
|
+
return write(config, root, path, request, response);
|
|
229
|
+
}
|
|
230
|
+
const TYPES = `// Generated by ForgePress. Do not edit.
|
|
231
|
+
import type schema from './schema'
|
|
232
|
+
|
|
233
|
+
declare module 'forgepress' {
|
|
234
|
+
interface ForgePressSchemaRegistry {
|
|
235
|
+
schema: typeof schema
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export {}
|
|
240
|
+
`;
|
|
241
|
+
async function loadProject(base, options) {
|
|
242
|
+
const root = options?.root ? resolve(base, options.root) : findRoot(base);
|
|
243
|
+
const loaded = await loadConfig(root);
|
|
244
|
+
const config = resolveConfig({
|
|
245
|
+
...loaded,
|
|
246
|
+
media: {
|
|
247
|
+
...loaded?.media,
|
|
248
|
+
...options?.media
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
const output = outputDir(root, config);
|
|
252
|
+
process.env[OUTPUT_VARIABLE] = output;
|
|
253
|
+
return {
|
|
254
|
+
root,
|
|
255
|
+
config,
|
|
256
|
+
output
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function syncTypes(root, config) {
|
|
260
|
+
const path = join(root, config.paths.types);
|
|
261
|
+
if (!existsSync(join(root, config.paths.schema))) {
|
|
262
|
+
rmSync(path, { force: true });
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (existsSync(path) && readFileSync(path, "utf8") === TYPES) return;
|
|
266
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
267
|
+
writeFileSync(path, TYPES);
|
|
268
|
+
}
|
|
269
|
+
function createDevContent(root, config, logger, reload, origins = SAME_ORIGIN) {
|
|
270
|
+
const schemaFile = join(root, config.paths.schema);
|
|
271
|
+
const contentDir = join(root, config.paths.content);
|
|
272
|
+
let reported = "";
|
|
273
|
+
function report(issues) {
|
|
274
|
+
const message = issues.length > 0 ? new ContentError(issues).message : "";
|
|
275
|
+
if (message === reported) return;
|
|
276
|
+
if (message) logger.warn(`${message}\n[forgepress] the build fails until ${issues.length === 1 ? "this problem is" : "these problems are"} fixed`);
|
|
277
|
+
else logger.info("[forgepress] content problems are fixed");
|
|
278
|
+
reported = message;
|
|
279
|
+
}
|
|
280
|
+
async function write() {
|
|
281
|
+
syncTypes(root, config);
|
|
282
|
+
try {
|
|
283
|
+
report((await buildOutput(root, config, { dev: true })).issues);
|
|
284
|
+
} catch (error) {
|
|
285
|
+
if (!(error instanceof ContentError)) throw error;
|
|
286
|
+
report(error.issues);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const rebuild = debounce(async (after) => {
|
|
290
|
+
await write().catch((error) => logger.error(`[forgepress] could not write the content output: ${errorMessage(error)}`));
|
|
291
|
+
after?.();
|
|
292
|
+
}, 100);
|
|
293
|
+
return {
|
|
294
|
+
watched: [schemaFile, contentDir],
|
|
295
|
+
changed: (file) => {
|
|
296
|
+
if (file === schemaFile || file.startsWith(`${contentDir}${sep}`)) rebuild(reload);
|
|
297
|
+
},
|
|
298
|
+
refresh: () => rebuild(),
|
|
299
|
+
endpoint: (request, response, next) => {
|
|
300
|
+
const method = request.method ?? "";
|
|
301
|
+
if (!(method === "POST" || method === "DELETE" || method === "GET") || !request.url?.startsWith(`/__forgepress/`)) return next();
|
|
302
|
+
handle(config, root, request, response, origins).then(() => {
|
|
303
|
+
if (method !== "GET") rebuild(reload);
|
|
304
|
+
}).catch((error) => {
|
|
305
|
+
response.statusCode = error instanceof EndpointError ? error.status : 500;
|
|
306
|
+
response.end(errorMessage(error));
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function editorSettings(local, config, devServer = "") {
|
|
312
|
+
return {
|
|
313
|
+
local,
|
|
314
|
+
devServer,
|
|
315
|
+
provider: config.provider,
|
|
316
|
+
format: config.content,
|
|
317
|
+
contentPath: config.paths.dir,
|
|
318
|
+
media: config.media
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
const SETTINGS_ID = "virtual:forgepress/settings";
|
|
322
|
+
function resolved(id) {
|
|
323
|
+
return `\0${id}`;
|
|
324
|
+
}
|
|
325
|
+
function generateSettings(local, config) {
|
|
326
|
+
return `export default ${JSON.stringify(editorSettings(local, config))}\n`;
|
|
327
|
+
}
|
|
328
|
+
export { SETTINGS_ID, createDevContent, editorSettings, generateSettings, loadProject, resolved, syncTypes };
|
package/dist/_chunks/types.d.mts
CHANGED
|
@@ -1,25 +1,230 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
interface FieldBase {
|
|
2
|
+
label?: string;
|
|
3
|
+
description?: string;
|
|
4
|
+
optional?: boolean;
|
|
5
|
+
translate?: boolean;
|
|
6
|
+
}
|
|
7
|
+
type FieldOptionType = 'text' | 'number' | 'boolean' | 'collection' | 'collections';
|
|
8
|
+
interface FieldOption {
|
|
9
|
+
label: string;
|
|
10
|
+
type: FieldOptionType;
|
|
11
|
+
description?: string;
|
|
12
|
+
required?: true;
|
|
13
|
+
}
|
|
14
|
+
interface FieldTypeDefinition {
|
|
15
|
+
type: string;
|
|
16
|
+
label: string;
|
|
17
|
+
options: Record<string, FieldOption>;
|
|
18
|
+
}
|
|
19
|
+
type OptionContent<TOption extends FieldOption> = TOption['type'] extends 'number' ? number : TOption['type'] extends 'boolean' ? boolean : TOption['type'] extends 'collections' ? readonly string[] : string;
|
|
20
|
+
type RequiredOptions<TOptions> = { [TKey in keyof TOptions]: TOptions[TKey] extends {
|
|
21
|
+
required: true;
|
|
22
|
+
} ? TKey : never; }[keyof TOptions];
|
|
23
|
+
type FieldOf<TDefinition extends FieldTypeDefinition> = TDefinition extends FieldTypeDefinition ? FieldBase & {
|
|
24
|
+
type: TDefinition['type'];
|
|
25
|
+
} & { [TKey in RequiredOptions<TDefinition['options']>]: OptionContent<TDefinition['options'][TKey]>; } & { [TKey in Exclude<keyof TDefinition['options'], RequiredOptions<TDefinition['options']>>]?: OptionContent<TDefinition['options'][TKey]>; } : never;
|
|
26
|
+
declare const dynamic: {
|
|
27
|
+
readonly type: "dynamic";
|
|
28
|
+
readonly label: "Dynamic";
|
|
29
|
+
readonly options: {
|
|
30
|
+
readonly collections: {
|
|
31
|
+
readonly label: "Collections";
|
|
32
|
+
readonly type: "collections";
|
|
33
|
+
readonly required: true;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
type DynamicField = FieldOf<typeof dynamic>;
|
|
38
|
+
interface DynamicBlock<TCollectionName extends string = string> {
|
|
39
|
+
collection: TCollectionName;
|
|
40
|
+
id: string;
|
|
41
|
+
}
|
|
42
|
+
type DynamicFieldContent<TField extends DynamicField> = DynamicBlock<TField['collections'][number]>[];
|
|
43
|
+
type ProviderType = 'github' | 'gitlab' | 'forgejo';
|
|
44
|
+
interface ProviderRepository {
|
|
45
|
+
owner: string;
|
|
46
|
+
name: string;
|
|
47
|
+
branch?: string;
|
|
48
|
+
}
|
|
49
|
+
export interface ProviderConfig {
|
|
50
|
+
type: ProviderType;
|
|
51
|
+
repository: ProviderRepository;
|
|
52
|
+
base?: string;
|
|
53
|
+
url?: string;
|
|
54
|
+
clientId?: string;
|
|
55
|
+
scopes?: readonly string[];
|
|
56
|
+
redirectUri?: string;
|
|
57
|
+
commitMessage?: string;
|
|
5
58
|
}
|
|
6
|
-
export interface
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
59
|
+
export interface ContentConfig {
|
|
60
|
+
indent?: number;
|
|
61
|
+
semi?: boolean;
|
|
62
|
+
}
|
|
63
|
+
export interface MediaConfig {
|
|
64
|
+
dir?: string;
|
|
65
|
+
url?: string;
|
|
66
|
+
maxSize?: number;
|
|
67
|
+
}
|
|
68
|
+
export interface OutputConfig {
|
|
69
|
+
dir?: string;
|
|
70
|
+
}
|
|
71
|
+
export interface ForgePressConfig {
|
|
72
|
+
path?: string;
|
|
73
|
+
provider?: ProviderConfig;
|
|
74
|
+
content?: ContentConfig;
|
|
75
|
+
media?: MediaConfig;
|
|
76
|
+
output?: OutputConfig;
|
|
77
|
+
}
|
|
78
|
+
interface MediaContent {
|
|
79
|
+
url: string;
|
|
80
|
+
alt?: string;
|
|
81
|
+
width?: number;
|
|
82
|
+
height?: number;
|
|
83
|
+
}
|
|
84
|
+
export type ResolvedMedia = Required<MediaConfig>;
|
|
85
|
+
declare const image: {
|
|
86
|
+
readonly type: "image";
|
|
87
|
+
readonly label: "Image";
|
|
88
|
+
readonly options: {
|
|
89
|
+
readonly multiple: {
|
|
90
|
+
readonly label: "Multiple";
|
|
91
|
+
readonly type: "boolean";
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
type ImageField = FieldOf<typeof image>;
|
|
96
|
+
type ImageFieldContent<TField extends ImageField> = TField['multiple'] extends true ? MediaContent[] : MediaContent;
|
|
97
|
+
declare const number: {
|
|
98
|
+
readonly type: "number";
|
|
99
|
+
readonly label: "Number";
|
|
100
|
+
readonly options: {
|
|
101
|
+
readonly min: {
|
|
102
|
+
readonly label: "Minimum";
|
|
103
|
+
readonly type: "number";
|
|
104
|
+
};
|
|
105
|
+
readonly max: {
|
|
106
|
+
readonly label: "Maximum";
|
|
107
|
+
readonly type: "number";
|
|
108
|
+
};
|
|
109
|
+
readonly step: {
|
|
110
|
+
readonly label: "Step";
|
|
111
|
+
readonly type: "number";
|
|
112
|
+
};
|
|
113
|
+
readonly index: {
|
|
114
|
+
readonly label: "Indexed";
|
|
115
|
+
readonly type: "boolean";
|
|
116
|
+
readonly description: "Listed in the collection manifest, so queries can filter and sort on it without loading every entry.";
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
type NumberField = FieldOf<typeof number>;
|
|
121
|
+
type NumberFieldContent = number;
|
|
122
|
+
declare const relation: {
|
|
123
|
+
readonly type: "relation";
|
|
124
|
+
readonly label: "Relation";
|
|
125
|
+
readonly options: {
|
|
126
|
+
readonly collection: {
|
|
127
|
+
readonly label: "Collection";
|
|
128
|
+
readonly type: "collection";
|
|
129
|
+
readonly required: true;
|
|
130
|
+
};
|
|
131
|
+
readonly multiple: {
|
|
132
|
+
readonly label: "Multiple";
|
|
133
|
+
readonly type: "boolean";
|
|
134
|
+
};
|
|
135
|
+
readonly index: {
|
|
136
|
+
readonly label: "Indexed";
|
|
137
|
+
readonly type: "boolean";
|
|
138
|
+
readonly description: "Listed in the collection manifest, so queries can filter and sort on it without loading every entry.";
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
type RelationField = FieldOf<typeof relation>;
|
|
143
|
+
type RelationFieldContent<TField extends RelationField> = TField['multiple'] extends true ? string[] : string;
|
|
144
|
+
declare const richtext: {
|
|
145
|
+
readonly type: "richtext";
|
|
146
|
+
readonly label: "Rich Text";
|
|
147
|
+
readonly options: {};
|
|
148
|
+
};
|
|
149
|
+
type RichTextField = FieldOf<typeof richtext>;
|
|
150
|
+
type RichTextFieldContent = string;
|
|
151
|
+
declare const text: {
|
|
152
|
+
readonly type: "text";
|
|
153
|
+
readonly label: "Text";
|
|
154
|
+
readonly options: {
|
|
155
|
+
readonly validation: {
|
|
156
|
+
readonly label: "Validation Pattern";
|
|
157
|
+
readonly type: "text";
|
|
158
|
+
};
|
|
159
|
+
readonly index: {
|
|
160
|
+
readonly label: "Indexed";
|
|
161
|
+
readonly type: "boolean";
|
|
162
|
+
readonly description: "Listed in the collection manifest, so queries can filter and sort on it without loading every entry.";
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
type TextField = FieldOf<typeof text>;
|
|
167
|
+
type TextFieldContent = string;
|
|
168
|
+
declare const video: {
|
|
169
|
+
readonly type: "video";
|
|
170
|
+
readonly label: "Video";
|
|
171
|
+
readonly options: {
|
|
172
|
+
readonly multiple: {
|
|
173
|
+
readonly label: "Multiple";
|
|
174
|
+
readonly type: "boolean";
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
type VideoField = FieldOf<typeof video>;
|
|
179
|
+
type VideoFieldContent<TField extends VideoField> = TField['multiple'] extends true ? MediaContent[] : MediaContent;
|
|
180
|
+
type Field = TextField | RichTextField | NumberField | ImageField | VideoField | RelationField | DynamicField;
|
|
181
|
+
type FieldContent<TField extends Field> = TField extends DynamicField ? DynamicFieldContent<TField> : TField extends RelationField ? RelationFieldContent<TField> : TField extends ImageField ? ImageFieldContent<TField> : TField extends VideoField ? VideoFieldContent<TField> : TField extends TextField ? TextFieldContent : TField extends NumberField ? NumberFieldContent : TField extends RichTextField ? RichTextFieldContent : never;
|
|
182
|
+
interface Collection {
|
|
183
|
+
label?: string;
|
|
184
|
+
description?: string;
|
|
185
|
+
fields: Record<string, Field>;
|
|
186
|
+
}
|
|
187
|
+
export interface ForgePressSchema {
|
|
188
|
+
collections: Record<string, Collection>;
|
|
189
|
+
locales?: readonly string[];
|
|
190
|
+
}
|
|
191
|
+
export type SchemaLocale<TSchema extends ForgePressSchema> = TSchema['locales'] extends readonly string[] ? TSchema['locales'][number] : never;
|
|
192
|
+
export interface ForgePressSchemaRegistry {}
|
|
193
|
+
export type RegisteredSchema = ForgePressSchemaRegistry extends {
|
|
194
|
+
schema: infer TSchema extends ForgePressSchema;
|
|
195
|
+
} ? TSchema : ForgePressSchema;
|
|
196
|
+
export type EntryStatus = 'published' | 'unpublished';
|
|
197
|
+
export interface EntryMeta {
|
|
198
|
+
id: string;
|
|
199
|
+
status: EntryStatus;
|
|
200
|
+
createdAt: string;
|
|
201
|
+
updatedAt: string;
|
|
202
|
+
}
|
|
203
|
+
export interface EntryRef<TCollectionName extends string = string> {
|
|
204
|
+
collection: TCollectionName;
|
|
205
|
+
id: string;
|
|
206
|
+
}
|
|
207
|
+
export interface OutputMeta {
|
|
208
|
+
id: string;
|
|
209
|
+
createdAt: string;
|
|
210
|
+
updatedAt: string;
|
|
211
|
+
}
|
|
212
|
+
export type Entry = EntryMeta & {
|
|
213
|
+
[field: string]: unknown;
|
|
214
|
+
};
|
|
215
|
+
type Translated<TSchema extends ForgePressSchema, TContent> = [SchemaLocale<TSchema>] extends [never] ? TContent : Record<SchemaLocale<TSchema>, TContent>;
|
|
216
|
+
type PartiallyTranslated<TSchema extends ForgePressSchema, TContent> = [SchemaLocale<TSchema>] extends [never] ? TContent : Partial<Record<SchemaLocale<TSchema>, TContent>>;
|
|
217
|
+
type FieldValue<TSchema extends ForgePressSchema, TField extends Field, TOptional extends boolean> = TField extends {
|
|
218
|
+
translate: true;
|
|
219
|
+
} ? TOptional extends true ? PartiallyTranslated<TSchema, FieldContent<TField>> : Translated<TSchema, FieldContent<TField>> : FieldContent<TField>;
|
|
220
|
+
type OptionalKeys<TCollection extends Collection> = { [TKey in keyof TCollection['fields']]-?: TCollection['fields'][TKey] extends {
|
|
221
|
+
optional: true;
|
|
222
|
+
} ? TKey : never; }[keyof TCollection['fields']];
|
|
223
|
+
type RequiredKeys<TCollection extends Collection> = Exclude<keyof TCollection['fields'], OptionalKeys<TCollection>>;
|
|
224
|
+
type CollectionContent<TSchema extends ForgePressSchema, TCollection extends Collection> = EntryMeta & { [TKey in RequiredKeys<TCollection>]: FieldValue<TSchema, TCollection['fields'][TKey], false>; } & { [TKey in OptionalKeys<TCollection>]?: FieldValue<TSchema, TCollection['fields'][TKey], true>; };
|
|
225
|
+
type EntryOf<TSchema extends ForgePressSchema, TCollectionName extends keyof TSchema['collections']> = CollectionContent<TSchema, TSchema['collections'][TCollectionName]>;
|
|
226
|
+
export type ForgePressEntry<TCollectionName extends keyof RegisteredSchema['collections']> = EntryOf<RegisteredSchema, TCollectionName>;
|
|
227
|
+
type OutputValue<TField extends Field> = TField extends RelationField ? TField['multiple'] extends true ? EntryRef<TField['collection']>[] : EntryRef<TField['collection']> : FieldContent<TField>;
|
|
228
|
+
type CollectionOutput<TCollection extends Collection> = OutputMeta & { [TKey in RequiredKeys<TCollection>]: OutputValue<TCollection['fields'][TKey]>; } & { [TKey in OptionalKeys<TCollection>]?: OutputValue<TCollection['fields'][TKey]>; };
|
|
229
|
+
export type OutputOf<TSchema extends ForgePressSchema, TCollectionName extends keyof TSchema['collections']> = CollectionOutput<TSchema['collections'][TCollectionName]>;
|
|
230
|
+
export type ForgePressOutput<TCollectionName extends keyof RegisteredSchema['collections']> = OutputOf<RegisteredSchema, TCollectionName>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { OutputMeta } from "./types.mjs";
|
|
2
|
+
type LinkKind = 'relation' | 'dynamic';
|
|
3
|
+
export interface OutputEntry extends OutputMeta {
|
|
4
|
+
[field: string]: unknown;
|
|
5
|
+
}
|
|
6
|
+
export interface OutputManifest {
|
|
7
|
+
indexed: string[];
|
|
8
|
+
links: Record<string, LinkKind>;
|
|
9
|
+
entries: OutputEntry[];
|
|
10
|
+
files: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
export type OutputCollection = {
|
|
13
|
+
localized: false;
|
|
14
|
+
manifest: string;
|
|
15
|
+
} | {
|
|
16
|
+
localized: true;
|
|
17
|
+
manifests: Record<string, string>;
|
|
18
|
+
};
|
|
19
|
+
export interface OutputIndex {
|
|
20
|
+
version: number;
|
|
21
|
+
commit: string | null;
|
|
22
|
+
dev?: true;
|
|
23
|
+
locales: string[];
|
|
24
|
+
collections: Record<string, OutputCollection>;
|
|
25
|
+
}
|