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,797 @@
|
|
|
1
|
+
import { isRecord, plain, same } from "./value.mjs";
|
|
2
|
+
import { once } from "./once.mjs";
|
|
3
|
+
import { assetUrl, base64ToText, createOutput, createPaths, isEntryId, isMediaFile, repositoryPaths, sortByCreation, textToBase64 } from "./media.mjs";
|
|
4
|
+
import { createFileSource } from "./content.mjs";
|
|
5
|
+
import "./serialize.mjs";
|
|
6
|
+
import "./libs/diff.mjs";
|
|
7
|
+
import { AuthorizationResponseError, None, ResponseBodyError, allowInsecureRequests, processRefreshTokenResponse, refreshTokenGrantRequest } from "oauth4webapi";
|
|
8
|
+
import { clear, createStore, del, delMany, entries, get, set } from "idb-keyval";
|
|
9
|
+
function mergeEntries(rows, overlay) {
|
|
10
|
+
if (!overlay) return [...rows];
|
|
11
|
+
const merged = new Map(rows.map((row) => [row.id, row]));
|
|
12
|
+
for (const [id, row] of Object.entries(overlay)) if (row === null) merged.delete(id);
|
|
13
|
+
else merged.set(id, row);
|
|
14
|
+
return [...merged.values()];
|
|
15
|
+
}
|
|
16
|
+
function stage(overlay, before, row) {
|
|
17
|
+
if (same(before, row)) delete overlay[row.id];
|
|
18
|
+
else overlay[row.id] = row;
|
|
19
|
+
}
|
|
20
|
+
function createContentChanges(base, ready, mutate) {
|
|
21
|
+
return {
|
|
22
|
+
schema: () => base.schema(),
|
|
23
|
+
list: async (collection) => sortByCreation(mergeEntries(await base.list(collection), (await ready()).entries[collection])),
|
|
24
|
+
entry: async (collection, id) => {
|
|
25
|
+
const staged = (await ready()).entries[collection]?.[id];
|
|
26
|
+
if (staged !== void 0) return staged ?? void 0;
|
|
27
|
+
return base.entry(collection, id);
|
|
28
|
+
},
|
|
29
|
+
writeEntry: async (collection, row) => {
|
|
30
|
+
const before = await base.entry(collection, row.id);
|
|
31
|
+
await mutate((changes) => {
|
|
32
|
+
stage(changes.entries[collection] ??= {}, before, plain(row));
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
removeEntry: async (collection, id) => {
|
|
36
|
+
const before = await base.entry(collection, id);
|
|
37
|
+
await mutate((changes) => {
|
|
38
|
+
const overlay = changes.entries[collection] ??= {};
|
|
39
|
+
if (before) overlay[id] = null;
|
|
40
|
+
else delete overlay[id];
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function publishedUploads(changes) {
|
|
46
|
+
return Object.values(changes.publishedMedia ?? {}).filter(Boolean);
|
|
47
|
+
}
|
|
48
|
+
function localUploads(changes) {
|
|
49
|
+
const removed = new Set(changes.removed);
|
|
50
|
+
return [...new Map([...Object.values(changes.uploads), ...publishedUploads(changes)].map((upload) => [upload.name, upload])).values()].filter((upload) => !removed.has(upload.name));
|
|
51
|
+
}
|
|
52
|
+
function createPreviews() {
|
|
53
|
+
const urls = /* @__PURE__ */ new Map();
|
|
54
|
+
function preview(upload) {
|
|
55
|
+
if (typeof URL.createObjectURL !== "function") return "";
|
|
56
|
+
let url = urls.get(upload.name);
|
|
57
|
+
if (!url) {
|
|
58
|
+
url = URL.createObjectURL(new Blob([upload.data], { type: upload.type }));
|
|
59
|
+
urls.set(upload.name, url);
|
|
60
|
+
}
|
|
61
|
+
return url;
|
|
62
|
+
}
|
|
63
|
+
function forget(name) {
|
|
64
|
+
const url = urls.get(name);
|
|
65
|
+
if (!url) return;
|
|
66
|
+
URL.revokeObjectURL(url);
|
|
67
|
+
urls.delete(name);
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
forget,
|
|
71
|
+
asset: (upload, prefix) => {
|
|
72
|
+
const local = preview(upload);
|
|
73
|
+
return {
|
|
74
|
+
name: upload.name,
|
|
75
|
+
url: assetUrl(prefix, upload.name),
|
|
76
|
+
type: upload.type,
|
|
77
|
+
size: upload.size,
|
|
78
|
+
modifiedAt: upload.modifiedAt,
|
|
79
|
+
...local ? { preview: local } : {}
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
clear: () => {
|
|
83
|
+
for (const name of [...urls.keys()]) forget(name);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function emptyChanges() {
|
|
88
|
+
return {
|
|
89
|
+
entries: {},
|
|
90
|
+
uploads: {},
|
|
91
|
+
removed: []
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
async function readChanges(store) {
|
|
95
|
+
return {
|
|
96
|
+
...emptyChanges(),
|
|
97
|
+
...await store.read()
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const PROVIDERS = {
|
|
101
|
+
github: {
|
|
102
|
+
name: "GitHub",
|
|
103
|
+
root: "https://github.com",
|
|
104
|
+
tokens: "/settings/personal-access-tokens/new",
|
|
105
|
+
permissions: "read and write access to contents, and read access to actions and commit statuses",
|
|
106
|
+
commits: "/commit/"
|
|
107
|
+
},
|
|
108
|
+
gitlab: {
|
|
109
|
+
name: "GitLab",
|
|
110
|
+
root: "https://gitlab.com",
|
|
111
|
+
tokens: "/-/user_settings/personal_access_tokens",
|
|
112
|
+
permissions: "the api scope",
|
|
113
|
+
commits: "/-/commit/"
|
|
114
|
+
},
|
|
115
|
+
forgejo: {
|
|
116
|
+
name: "Forgejo",
|
|
117
|
+
root: "https://codeberg.org",
|
|
118
|
+
tokens: "/user/settings/applications",
|
|
119
|
+
permissions: "read access to your user, and read and write access to repositories",
|
|
120
|
+
commits: "/commit/"
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
function describe(config) {
|
|
124
|
+
const provider = PROVIDERS[config.type];
|
|
125
|
+
const root = (config.url ?? provider.root).replace(/\/+$/, "");
|
|
126
|
+
const shared = {
|
|
127
|
+
name: provider.name,
|
|
128
|
+
root,
|
|
129
|
+
tokens: `${root}${provider.tokens}`,
|
|
130
|
+
permissions: provider.permissions
|
|
131
|
+
};
|
|
132
|
+
if (config.type === "github") return {
|
|
133
|
+
...shared,
|
|
134
|
+
api: root === PROVIDERS.github.root ? "https://api.github.com" : `${root}/api/v3`,
|
|
135
|
+
scopes: config.scopes ?? []
|
|
136
|
+
};
|
|
137
|
+
if (config.type === "gitlab") return {
|
|
138
|
+
...shared,
|
|
139
|
+
api: `${root}/api/v4`,
|
|
140
|
+
scopes: config.scopes ?? ["api"],
|
|
141
|
+
oauth: {
|
|
142
|
+
issuer: root,
|
|
143
|
+
authorize: `${root}/oauth/authorize`,
|
|
144
|
+
token: `${root}/oauth/token`
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
return {
|
|
148
|
+
...shared,
|
|
149
|
+
api: `${root}/api/v1`,
|
|
150
|
+
scopes: config.scopes ?? ["read:user", "write:repository"],
|
|
151
|
+
oauth: {
|
|
152
|
+
issuer: root,
|
|
153
|
+
authorize: `${root}/login/oauth/authorize`,
|
|
154
|
+
token: `${root}/login/oauth/access_token`
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function publishable(files) {
|
|
159
|
+
if (files.length === 0) throw new Error("[forgepress] there is nothing to publish");
|
|
160
|
+
return files;
|
|
161
|
+
}
|
|
162
|
+
function toIdentity(login, name, avatar) {
|
|
163
|
+
return {
|
|
164
|
+
login,
|
|
165
|
+
...name ? { name } : {},
|
|
166
|
+
...avatar ? { avatar } : {}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function blobs(directory, items) {
|
|
170
|
+
return items.filter((item) => item.type === "blob").map((item) => ({
|
|
171
|
+
path: `${directory}/${item.path}`,
|
|
172
|
+
sha: item.sha
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
function createRepositoryApi(config, base, token, headers = {}) {
|
|
176
|
+
const { name } = describe(config);
|
|
177
|
+
let known = config.repository.branch;
|
|
178
|
+
async function send(path, init = {}) {
|
|
179
|
+
return fetch(path.startsWith("http") ? path : `${base}${path}`, {
|
|
180
|
+
...init,
|
|
181
|
+
headers: {
|
|
182
|
+
...headers,
|
|
183
|
+
authorization: `Bearer ${await token()}`,
|
|
184
|
+
...init.body === void 0 ? {} : { "content-type": "application/json" },
|
|
185
|
+
...init.headers
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
async function check(response) {
|
|
190
|
+
if (!response.ok) throw new Error(`[forgepress] ${name} ${response.status}: ${(await response.text()).slice(0, 300)}`);
|
|
191
|
+
return response;
|
|
192
|
+
}
|
|
193
|
+
async function call(path, init) {
|
|
194
|
+
const response = await check(await send(path, init));
|
|
195
|
+
return response.status === 204 ? void 0 : await response.json();
|
|
196
|
+
}
|
|
197
|
+
async function find(path, missing = [404]) {
|
|
198
|
+
const response = await send(path);
|
|
199
|
+
if (missing.includes(response.status)) return void 0;
|
|
200
|
+
return await (await check(response)).json();
|
|
201
|
+
}
|
|
202
|
+
function post(path, body) {
|
|
203
|
+
return call(path, {
|
|
204
|
+
method: "POST",
|
|
205
|
+
body: JSON.stringify(body)
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
async function details() {
|
|
209
|
+
const found = await call("");
|
|
210
|
+
known ??= found.default_branch;
|
|
211
|
+
return found;
|
|
212
|
+
}
|
|
213
|
+
async function branch() {
|
|
214
|
+
return known ?? (await details()).default_branch;
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
send,
|
|
218
|
+
check,
|
|
219
|
+
call,
|
|
220
|
+
find,
|
|
221
|
+
post,
|
|
222
|
+
details,
|
|
223
|
+
branch
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
async function findTree(commit, directory, list) {
|
|
227
|
+
let tree = commit;
|
|
228
|
+
for (const segment of directory.split("/")) {
|
|
229
|
+
const found = (await list(tree)).find((item) => item.path === segment && item.type === "tree");
|
|
230
|
+
if (!found) return void 0;
|
|
231
|
+
tree = found.sha;
|
|
232
|
+
}
|
|
233
|
+
return tree;
|
|
234
|
+
}
|
|
235
|
+
const PAGE_SIZE$1 = 1e3;
|
|
236
|
+
const STATUS_LIMIT = 50;
|
|
237
|
+
const SCHEDULED = /\(schedule\)$/;
|
|
238
|
+
function statusState$1(status) {
|
|
239
|
+
if (status === "success" || status === "pending") return status;
|
|
240
|
+
return status === "failure" || status === "error" ? "failure" : "skipped";
|
|
241
|
+
}
|
|
242
|
+
function encodePath(path) {
|
|
243
|
+
return path.split("/").map(encodeURIComponent).join("/");
|
|
244
|
+
}
|
|
245
|
+
function createForgejoForge(config, token) {
|
|
246
|
+
const { api, root } = describe(config);
|
|
247
|
+
const { owner, name } = config.repository;
|
|
248
|
+
const repo = createRepositoryApi(config, `${api}/repos/${owner}/${name}`, token, { accept: "application/json" });
|
|
249
|
+
async function entries(tree, recursive) {
|
|
250
|
+
const items = [];
|
|
251
|
+
for (let page = 1;; page += 1) {
|
|
252
|
+
const listed = await repo.call(`/git/trees/${tree}?recursive=${recursive}&per_page=${PAGE_SIZE$1}&page=${page}`);
|
|
253
|
+
items.push(...listed.tree);
|
|
254
|
+
if (listed.tree.length === 0 || items.length >= listed.total_count) return items;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
async function sha(path, ref) {
|
|
258
|
+
return (await repo.find(`/contents/${encodePath(path)}?ref=${encodeURIComponent(ref)}`))?.sha;
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
async access() {
|
|
262
|
+
const [user, repository] = await Promise.all([repo.call(`${api}/user`), repo.details()]);
|
|
263
|
+
return {
|
|
264
|
+
identity: toIdentity(user.login, user.full_name, user.avatar_url),
|
|
265
|
+
writable: repository.permissions?.push === true,
|
|
266
|
+
branch: await repo.branch()
|
|
267
|
+
};
|
|
268
|
+
},
|
|
269
|
+
async head() {
|
|
270
|
+
return (await repo.call(`/branches/${encodeURIComponent(await repo.branch())}`)).commit.id;
|
|
271
|
+
},
|
|
272
|
+
async files(commit, directory) {
|
|
273
|
+
const tree = await findTree(commit, directory, (found) => entries(found, false));
|
|
274
|
+
if (!tree) return [];
|
|
275
|
+
return blobs(directory, await entries(tree, true));
|
|
276
|
+
},
|
|
277
|
+
async read(blob) {
|
|
278
|
+
return base64ToText((await repo.call(`/git/blobs/${blob}`)).content);
|
|
279
|
+
},
|
|
280
|
+
async commit(files, message, parent) {
|
|
281
|
+
const operations = publishable((await Promise.all(files.map(async (file) => {
|
|
282
|
+
const found = await sha(file.path, parent);
|
|
283
|
+
if ("removed" in file) return found ? {
|
|
284
|
+
operation: "delete",
|
|
285
|
+
path: file.path,
|
|
286
|
+
sha: found
|
|
287
|
+
} : void 0;
|
|
288
|
+
const content = file.encoding === "base64" ? file.data : textToBase64(file.data);
|
|
289
|
+
return found ? {
|
|
290
|
+
operation: "update",
|
|
291
|
+
path: file.path,
|
|
292
|
+
content,
|
|
293
|
+
sha: found
|
|
294
|
+
} : {
|
|
295
|
+
operation: "create",
|
|
296
|
+
path: file.path,
|
|
297
|
+
content
|
|
298
|
+
};
|
|
299
|
+
}))).filter((operation) => operation !== void 0));
|
|
300
|
+
return (await repo.post("/contents", {
|
|
301
|
+
branch: await repo.branch(),
|
|
302
|
+
message,
|
|
303
|
+
files: operations
|
|
304
|
+
})).commit.sha;
|
|
305
|
+
},
|
|
306
|
+
async checks(commit) {
|
|
307
|
+
return ((await repo.call(`/commits/${commit}/status?limit=${STATUS_LIMIT}`)).statuses ?? []).filter((status) => !SCHEDULED.test(status.context)).map((status) => ({
|
|
308
|
+
name: status.context,
|
|
309
|
+
state: statusState$1(status.status),
|
|
310
|
+
...status.target_url ? { url: new URL(status.target_url, `${root}/`).href } : {}
|
|
311
|
+
}));
|
|
312
|
+
},
|
|
313
|
+
async contains(commit, ancestor) {
|
|
314
|
+
return commit === ancestor || (await repo.call(`/compare/${commit}...${ancestor}`)).total_commits === 0;
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
const BUILD_EVENTS = /* @__PURE__ */ new Set(["push", "workflow_run"]);
|
|
319
|
+
const FAILED = /* @__PURE__ */ new Set([
|
|
320
|
+
"failure",
|
|
321
|
+
"timed_out",
|
|
322
|
+
"startup_failure"
|
|
323
|
+
]);
|
|
324
|
+
const UNREADABLE = [403, 404];
|
|
325
|
+
function runState(status, conclusion) {
|
|
326
|
+
if (status !== "completed" || conclusion === "action_required") return "pending";
|
|
327
|
+
if (conclusion === "success") return "success";
|
|
328
|
+
return conclusion !== null && FAILED.has(conclusion) ? "failure" : "skipped";
|
|
329
|
+
}
|
|
330
|
+
function statusState(state) {
|
|
331
|
+
return state === "success" || state === "pending" ? state : "failure";
|
|
332
|
+
}
|
|
333
|
+
function linked(url) {
|
|
334
|
+
return url ? { url } : {};
|
|
335
|
+
}
|
|
336
|
+
function createGitHubForge(config, token) {
|
|
337
|
+
const { api } = describe(config);
|
|
338
|
+
const { owner, name } = config.repository;
|
|
339
|
+
const repo = createRepositoryApi(config, `${api}/repos/${owner}/${name}`, token, {
|
|
340
|
+
"accept": "application/vnd.github+json",
|
|
341
|
+
"x-github-api-version": "2022-11-28"
|
|
342
|
+
});
|
|
343
|
+
let apps = true;
|
|
344
|
+
async function runs(commit) {
|
|
345
|
+
return (await repo.find(`/actions/runs?head_sha=${commit}&per_page=100`, UNREADABLE))?.workflow_runs.filter((run) => BUILD_EVENTS.has(run.event) || run.path.startsWith("dynamic/pages/")).map((run) => ({
|
|
346
|
+
name: run.name ?? run.path,
|
|
347
|
+
state: runState(run.status, run.conclusion),
|
|
348
|
+
url: run.html_url
|
|
349
|
+
}));
|
|
350
|
+
}
|
|
351
|
+
async function statuses(commit) {
|
|
352
|
+
return (await repo.find(`/commits/${commit}/status?per_page=100`, UNREADABLE))?.statuses.map((status) => ({
|
|
353
|
+
name: status.context,
|
|
354
|
+
state: statusState(status.state),
|
|
355
|
+
...linked(status.target_url)
|
|
356
|
+
}));
|
|
357
|
+
}
|
|
358
|
+
async function others(commit) {
|
|
359
|
+
const found = apps ? await repo.find(`/commits/${commit}/check-runs?per_page=100`, UNREADABLE) : void 0;
|
|
360
|
+
if (!found) {
|
|
361
|
+
apps = false;
|
|
362
|
+
return [];
|
|
363
|
+
}
|
|
364
|
+
return found.check_runs.filter((run) => run.app?.slug !== "github-actions").map((run) => ({
|
|
365
|
+
name: run.name,
|
|
366
|
+
state: runState(run.status, run.conclusion),
|
|
367
|
+
...linked(run.html_url ?? run.details_url)
|
|
368
|
+
}));
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
async access() {
|
|
372
|
+
const [user, repository] = await Promise.all([repo.call(`${api}/user`), repo.details()]);
|
|
373
|
+
return {
|
|
374
|
+
identity: toIdentity(user.login, user.name, user.avatar_url),
|
|
375
|
+
writable: repository.permissions?.push === true,
|
|
376
|
+
branch: await repo.branch()
|
|
377
|
+
};
|
|
378
|
+
},
|
|
379
|
+
async head() {
|
|
380
|
+
return (await repo.call(`/git/ref/heads/${encodeURIComponent(await repo.branch())}`)).object.sha;
|
|
381
|
+
},
|
|
382
|
+
async files(commit, directory) {
|
|
383
|
+
const sha = await findTree(commit, directory, async (tree) => (await repo.call(`/git/trees/${tree}`)).tree);
|
|
384
|
+
if (!sha) return [];
|
|
385
|
+
const tree = await repo.call(`/git/trees/${sha}?recursive=1`);
|
|
386
|
+
if (tree.truncated) throw new Error(`[forgepress] GitHub lists too many files in ${directory} to read them in one request`);
|
|
387
|
+
return blobs(directory, tree.tree);
|
|
388
|
+
},
|
|
389
|
+
async read(sha) {
|
|
390
|
+
return (await repo.check(await repo.send(`/git/blobs/${sha}`, { headers: { accept: "application/vnd.github.raw+json" } }))).text();
|
|
391
|
+
},
|
|
392
|
+
async commit(files, message, parent) {
|
|
393
|
+
const pending = publishable(files);
|
|
394
|
+
const current = await repo.call(`/git/commits/${parent}`);
|
|
395
|
+
const tree = await Promise.all(pending.map(async (file) => {
|
|
396
|
+
if ("removed" in file) return {
|
|
397
|
+
path: file.path,
|
|
398
|
+
mode: "100644",
|
|
399
|
+
type: "blob",
|
|
400
|
+
sha: null
|
|
401
|
+
};
|
|
402
|
+
const blob = await repo.post("/git/blobs", {
|
|
403
|
+
content: file.data,
|
|
404
|
+
encoding: file.encoding
|
|
405
|
+
});
|
|
406
|
+
return {
|
|
407
|
+
path: file.path,
|
|
408
|
+
mode: "100644",
|
|
409
|
+
type: "blob",
|
|
410
|
+
sha: blob.sha
|
|
411
|
+
};
|
|
412
|
+
}));
|
|
413
|
+
const next = await repo.post("/git/trees", {
|
|
414
|
+
base_tree: current.tree.sha,
|
|
415
|
+
tree
|
|
416
|
+
});
|
|
417
|
+
const created = await repo.post("/git/commits", {
|
|
418
|
+
message,
|
|
419
|
+
tree: next.sha,
|
|
420
|
+
parents: [parent]
|
|
421
|
+
});
|
|
422
|
+
await repo.call(`/git/refs/heads/${encodeURIComponent(await repo.branch())}`, {
|
|
423
|
+
method: "PATCH",
|
|
424
|
+
body: JSON.stringify({
|
|
425
|
+
sha: created.sha,
|
|
426
|
+
force: false
|
|
427
|
+
})
|
|
428
|
+
});
|
|
429
|
+
return created.sha;
|
|
430
|
+
},
|
|
431
|
+
async checks(commit) {
|
|
432
|
+
const [built, reported, rest] = await Promise.all([
|
|
433
|
+
runs(commit),
|
|
434
|
+
statuses(commit),
|
|
435
|
+
others(commit)
|
|
436
|
+
]);
|
|
437
|
+
if (!built && !reported) throw new Error("[forgepress] the GitHub token can't read the builds of this repository; give it read access to actions and commit statuses");
|
|
438
|
+
return [
|
|
439
|
+
...built ?? [],
|
|
440
|
+
...reported ?? [],
|
|
441
|
+
...rest
|
|
442
|
+
];
|
|
443
|
+
},
|
|
444
|
+
async contains(commit, ancestor) {
|
|
445
|
+
return commit === ancestor || (await repo.call(`/compare/${commit}...${ancestor}?per_page=1`)).ahead_by === 0;
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
const DEVELOPER = 30;
|
|
450
|
+
const PAGE_SIZE = 100;
|
|
451
|
+
const RUNNING = /* @__PURE__ */ new Set([
|
|
452
|
+
"created",
|
|
453
|
+
"waiting_for_resource",
|
|
454
|
+
"preparing",
|
|
455
|
+
"pending",
|
|
456
|
+
"running",
|
|
457
|
+
"scheduled",
|
|
458
|
+
"waiting_for_callback"
|
|
459
|
+
]);
|
|
460
|
+
function writable(project) {
|
|
461
|
+
return [project.permissions?.project_access?.access_level, project.permissions?.group_access?.access_level].some((level) => typeof level === "number" && level >= DEVELOPER);
|
|
462
|
+
}
|
|
463
|
+
function pipelineState(status) {
|
|
464
|
+
if (status === "success") return "success";
|
|
465
|
+
if (status === "failed") return "failure";
|
|
466
|
+
return RUNNING.has(status) ? "pending" : "skipped";
|
|
467
|
+
}
|
|
468
|
+
function nextPage(link) {
|
|
469
|
+
return link?.split(",").map((part) => /<([^>]+)>;\s*rel="next"/.exec(part)?.[1]).find((url) => url !== void 0);
|
|
470
|
+
}
|
|
471
|
+
function createGitLabForge(config, token) {
|
|
472
|
+
const { api } = describe(config);
|
|
473
|
+
const repo = createRepositoryApi(config, `${api}/projects/${encodeURIComponent(`${config.repository.owner}/${config.repository.name}`)}`, token);
|
|
474
|
+
function existing(path, ref) {
|
|
475
|
+
return repo.find(`/repository/files/${encodeURIComponent(path)}?ref=${encodeURIComponent(ref)}`);
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
async access() {
|
|
479
|
+
const [user, project] = await Promise.all([repo.call(`${api}/user`), repo.details()]);
|
|
480
|
+
return {
|
|
481
|
+
identity: toIdentity(user.username, user.name, user.avatar_url),
|
|
482
|
+
writable: writable(project),
|
|
483
|
+
branch: await repo.branch()
|
|
484
|
+
};
|
|
485
|
+
},
|
|
486
|
+
async head() {
|
|
487
|
+
return (await repo.call(`/repository/branches/${encodeURIComponent(await repo.branch())}`)).commit.id;
|
|
488
|
+
},
|
|
489
|
+
async files(commit, directory) {
|
|
490
|
+
const files = [];
|
|
491
|
+
let page = `/repository/tree?path=${encodeURIComponent(directory)}&ref=${encodeURIComponent(commit)}&recursive=true&per_page=${PAGE_SIZE}&pagination=keyset`;
|
|
492
|
+
while (page) {
|
|
493
|
+
const response = await repo.send(page);
|
|
494
|
+
if (response.status === 404) return [];
|
|
495
|
+
const items = await (await repo.check(response)).json();
|
|
496
|
+
files.push(...items.filter((item) => item.type === "blob").map((item) => ({
|
|
497
|
+
path: item.path,
|
|
498
|
+
sha: item.id
|
|
499
|
+
})));
|
|
500
|
+
page = nextPage(response.headers.get("link"));
|
|
501
|
+
}
|
|
502
|
+
return files;
|
|
503
|
+
},
|
|
504
|
+
async read(sha) {
|
|
505
|
+
return (await repo.check(await repo.send(`/repository/blobs/${sha}/raw`))).text();
|
|
506
|
+
},
|
|
507
|
+
async commit(files, message, parent) {
|
|
508
|
+
const actions = publishable((await Promise.all(files.map(async (file) => {
|
|
509
|
+
const found = await existing(file.path, parent);
|
|
510
|
+
if ("removed" in file) return found ? {
|
|
511
|
+
action: "delete",
|
|
512
|
+
file_path: file.path,
|
|
513
|
+
last_commit_id: found.last_commit_id
|
|
514
|
+
} : void 0;
|
|
515
|
+
return found ? {
|
|
516
|
+
action: "update",
|
|
517
|
+
file_path: file.path,
|
|
518
|
+
content: file.data,
|
|
519
|
+
encoding: file.encoding === "base64" ? "base64" : "text",
|
|
520
|
+
last_commit_id: found.last_commit_id
|
|
521
|
+
} : {
|
|
522
|
+
action: "create",
|
|
523
|
+
file_path: file.path,
|
|
524
|
+
content: file.data,
|
|
525
|
+
encoding: file.encoding === "base64" ? "base64" : "text"
|
|
526
|
+
};
|
|
527
|
+
}))).filter((action) => action !== void 0));
|
|
528
|
+
return (await repo.post("/repository/commits", {
|
|
529
|
+
branch: await repo.branch(),
|
|
530
|
+
commit_message: message,
|
|
531
|
+
actions
|
|
532
|
+
})).id;
|
|
533
|
+
},
|
|
534
|
+
async checks(commit) {
|
|
535
|
+
return (await repo.call(`/pipelines?sha=${commit}&per_page=${PAGE_SIZE}`)).filter((pipeline) => pipeline.source !== "schedule").map((pipeline) => ({
|
|
536
|
+
name: pipeline.name || `Pipeline #${pipeline.iid ?? pipeline.id}`,
|
|
537
|
+
state: pipelineState(pipeline.status),
|
|
538
|
+
url: pipeline.web_url
|
|
539
|
+
}));
|
|
540
|
+
},
|
|
541
|
+
async contains(commit, ancestor) {
|
|
542
|
+
const refs = new URLSearchParams([["refs[]", commit], ["refs[]", ancestor]]);
|
|
543
|
+
return commit === ancestor || (await repo.call(`/repository/merge_base?${refs.toString()}`)).id === ancestor;
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
function createForge(config, token) {
|
|
548
|
+
if (config.type === "gitlab") return createGitLabForge(config, token);
|
|
549
|
+
if (config.type === "forgejo") return createForgejoForge(config, token);
|
|
550
|
+
return createGitHubForge(config, token);
|
|
551
|
+
}
|
|
552
|
+
const SKEW = 3e4;
|
|
553
|
+
function toTokens(response, now = Date.now()) {
|
|
554
|
+
return {
|
|
555
|
+
access: response.access_token,
|
|
556
|
+
...response.refresh_token ? { refresh: response.refresh_token } : {},
|
|
557
|
+
...response.expires_in === void 0 ? {} : { expires: now + response.expires_in * 1e3 }
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
function expired(tokens, now = Date.now()) {
|
|
561
|
+
return tokens.expires !== void 0 && tokens.expires - SKEW <= now;
|
|
562
|
+
}
|
|
563
|
+
function server(endpoints) {
|
|
564
|
+
return {
|
|
565
|
+
issuer: endpoints.issuer,
|
|
566
|
+
authorization_endpoint: endpoints.authorize,
|
|
567
|
+
token_endpoint: endpoints.token
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
function requestOptions(endpoints) {
|
|
571
|
+
return { [allowInsecureRequests]: new URL(endpoints.token).protocol === "http:" };
|
|
572
|
+
}
|
|
573
|
+
function refusal(error) {
|
|
574
|
+
if (error instanceof AuthorizationResponseError) return new Error(`[forgepress] the forge refused the sign-in: ${error.error_description || error.error}`, { cause: error });
|
|
575
|
+
if (error instanceof ResponseBodyError) return new Error(`[forgepress] the forge rejected the token request: ${error.error_description || error.error}`, { cause: error });
|
|
576
|
+
return error;
|
|
577
|
+
}
|
|
578
|
+
async function renew(endpoints, clientId, refresh) {
|
|
579
|
+
const as = server(endpoints);
|
|
580
|
+
const client = { client_id: clientId };
|
|
581
|
+
try {
|
|
582
|
+
const response = await refreshTokenGrantRequest(as, client, None(), refresh, requestOptions(endpoints));
|
|
583
|
+
return toTokens(await processRefreshTokenResponse(as, client, response));
|
|
584
|
+
} catch (error) {
|
|
585
|
+
throw refusal(error);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
function storedTokens(value) {
|
|
589
|
+
if (typeof value === "string") return value ? { access: value } : void 0;
|
|
590
|
+
return value;
|
|
591
|
+
}
|
|
592
|
+
async function refreshed(tokens, config) {
|
|
593
|
+
const oauth = describe(config).oauth;
|
|
594
|
+
if (!expired(tokens) || !tokens.refresh || !oauth || !config.clientId) return tokens;
|
|
595
|
+
return renew(oauth, config.clientId, tokens.refresh);
|
|
596
|
+
}
|
|
597
|
+
function createTokenGetter(config, read, write) {
|
|
598
|
+
let renewal;
|
|
599
|
+
async function renewed(tokens) {
|
|
600
|
+
const next = await refreshed(tokens, config);
|
|
601
|
+
if (next !== tokens) await write(next);
|
|
602
|
+
return next;
|
|
603
|
+
}
|
|
604
|
+
return async () => {
|
|
605
|
+
const tokens = await read();
|
|
606
|
+
if (!expired(tokens)) return tokens.access;
|
|
607
|
+
renewal ??= renewed(tokens).finally(() => {
|
|
608
|
+
renewal = void 0;
|
|
609
|
+
});
|
|
610
|
+
return (await renewal).access;
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
const CONCURRENCY = 8;
|
|
614
|
+
function limit(size) {
|
|
615
|
+
const waiting = [];
|
|
616
|
+
let active = 0;
|
|
617
|
+
return async (task) => {
|
|
618
|
+
if (active < size) active += 1;
|
|
619
|
+
else await new Promise((resolve) => waiting.push(resolve));
|
|
620
|
+
try {
|
|
621
|
+
return await task();
|
|
622
|
+
} finally {
|
|
623
|
+
const next = waiting.shift();
|
|
624
|
+
if (next) next();
|
|
625
|
+
else active -= 1;
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
function createForgeSource(forge, target, cache) {
|
|
630
|
+
const { paths, mediaDir } = target;
|
|
631
|
+
const queue = limit(CONCURRENCY);
|
|
632
|
+
const texts = /* @__PURE__ */ new Map();
|
|
633
|
+
let pinned;
|
|
634
|
+
let snapshot = once(load);
|
|
635
|
+
function client() {
|
|
636
|
+
const current = forge();
|
|
637
|
+
if (!current) throw new Error("[forgepress] sign in to read the content from the repository");
|
|
638
|
+
return current;
|
|
639
|
+
}
|
|
640
|
+
async function restore(hashes) {
|
|
641
|
+
const cached = await cache?.keep(hashes).catch(() => void 0);
|
|
642
|
+
for (const [sha, content] of cached ?? []) if (!texts.has(sha)) texts.set(sha, Promise.resolve(content));
|
|
643
|
+
}
|
|
644
|
+
async function listing(commit, directory) {
|
|
645
|
+
const stored = await cache?.readListing(commit, directory).catch(() => void 0);
|
|
646
|
+
if (stored) return stored;
|
|
647
|
+
const listed = new Map((await client().files(commit, directory)).map((file) => [file.path, file.sha]));
|
|
648
|
+
if (forge()) cache?.writeListing(commit, directory, listed).catch(() => void 0);
|
|
649
|
+
return listed;
|
|
650
|
+
}
|
|
651
|
+
async function uploads(commit) {
|
|
652
|
+
if (mediaDir === void 0) return [];
|
|
653
|
+
return [...(await listing(commit, mediaDir)).keys()].map((path) => path.slice(mediaDir.length + 1)).filter(isMediaFile);
|
|
654
|
+
}
|
|
655
|
+
async function load() {
|
|
656
|
+
const commit = pinned ?? await client().head();
|
|
657
|
+
const listed = await listing(commit, paths.dir);
|
|
658
|
+
await restore(new Set(listed.values()));
|
|
659
|
+
return {
|
|
660
|
+
commit,
|
|
661
|
+
files: listed,
|
|
662
|
+
media: once(() => uploads(commit))
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
function text(sha) {
|
|
666
|
+
let pending = texts.get(sha);
|
|
667
|
+
if (!pending) {
|
|
668
|
+
pending = queue(() => client().read(sha));
|
|
669
|
+
texts.set(sha, pending);
|
|
670
|
+
pending.then((content) => forge() && cache?.writeFile(sha, content), () => texts.delete(sha)).catch(() => void 0);
|
|
671
|
+
}
|
|
672
|
+
return pending;
|
|
673
|
+
}
|
|
674
|
+
async function hash(path) {
|
|
675
|
+
return (await snapshot()).files.get(path);
|
|
676
|
+
}
|
|
677
|
+
return {
|
|
678
|
+
...createFileSource({
|
|
679
|
+
list: async (directory) => [...(await snapshot()).files.keys()].filter((path) => path.startsWith(`${directory}/`)),
|
|
680
|
+
read: async (path) => {
|
|
681
|
+
const sha = await hash(path);
|
|
682
|
+
return sha === void 0 ? void 0 : text(sha);
|
|
683
|
+
}
|
|
684
|
+
}, paths),
|
|
685
|
+
hashes: { entry: async (collection, id) => isEntryId(id) ? hash(paths.entry(collection, id)) : void 0 },
|
|
686
|
+
media: async () => mediaDir === void 0 ? [] : (await snapshot()).media(),
|
|
687
|
+
read: text,
|
|
688
|
+
reset: (commit) => {
|
|
689
|
+
pinned = commit;
|
|
690
|
+
snapshot = once(load);
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
const CHANGES = createStore("forgepress", "changes");
|
|
695
|
+
const FILES = createStore("forgepress-cache", "files");
|
|
696
|
+
const LISTINGS = createStore("forgepress-listings", "listings");
|
|
697
|
+
function createIdbStore(key) {
|
|
698
|
+
return {
|
|
699
|
+
read: () => get(key, CHANGES),
|
|
700
|
+
write: (value) => set(key, value, CHANGES),
|
|
701
|
+
clear: () => del(key, CHANGES)
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
function createIdbCache() {
|
|
705
|
+
return {
|
|
706
|
+
readListing: async (commit, directory) => {
|
|
707
|
+
const stored = await get(directory, LISTINGS);
|
|
708
|
+
return stored?.commit === commit ? stored.files : void 0;
|
|
709
|
+
},
|
|
710
|
+
writeListing: (commit, directory, files) => set(directory, {
|
|
711
|
+
commit,
|
|
712
|
+
files
|
|
713
|
+
}, LISTINGS),
|
|
714
|
+
keep: async (hashes) => {
|
|
715
|
+
const kept = /* @__PURE__ */ new Map();
|
|
716
|
+
const stale = [];
|
|
717
|
+
for (const [key, text] of await entries(FILES)) if (typeof key === "string" && hashes.has(key)) kept.set(key, text);
|
|
718
|
+
else stale.push(key);
|
|
719
|
+
if (stale.length > 0) await delMany(stale, FILES);
|
|
720
|
+
return kept;
|
|
721
|
+
},
|
|
722
|
+
writeFile: (hash, text) => set(hash, text, FILES),
|
|
723
|
+
clear: async () => {
|
|
724
|
+
await Promise.all([clear(FILES), clear(LISTINGS)]);
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
function createMemoryStore() {
|
|
729
|
+
let value;
|
|
730
|
+
return {
|
|
731
|
+
read: async () => value,
|
|
732
|
+
write: async (next) => {
|
|
733
|
+
value = next;
|
|
734
|
+
},
|
|
735
|
+
clear: async () => {
|
|
736
|
+
value = void 0;
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
const TOKEN_KEY = "token";
|
|
741
|
+
const CHANGES_KEY = "changes";
|
|
742
|
+
function persist(key) {
|
|
743
|
+
return typeof indexedDB === "undefined" ? createMemoryStore() : createIdbStore(key);
|
|
744
|
+
}
|
|
745
|
+
function repositoryCache() {
|
|
746
|
+
return typeof indexedDB === "undefined" ? void 0 : createIdbCache();
|
|
747
|
+
}
|
|
748
|
+
function swap(value, urls) {
|
|
749
|
+
if (typeof value === "string") return [...urls].reduce((text, [url, local]) => text.split(url).join(local), value);
|
|
750
|
+
if (Array.isArray(value)) return value.map((item) => swap(item, urls));
|
|
751
|
+
if (isRecord(value)) return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, swap(item, urls)]));
|
|
752
|
+
return value;
|
|
753
|
+
}
|
|
754
|
+
function createPreviewReader(settings) {
|
|
755
|
+
const tokens = persist(TOKEN_KEY);
|
|
756
|
+
const pending = persist(CHANGES_KEY);
|
|
757
|
+
const previews = createPreviews();
|
|
758
|
+
const shown = /* @__PURE__ */ new Set();
|
|
759
|
+
let token;
|
|
760
|
+
async function saved() {
|
|
761
|
+
const found = storedTokens(await tokens.read());
|
|
762
|
+
if (!found) throw new Error("[forgepress] sign in to the editor to preview unpublished content");
|
|
763
|
+
return found;
|
|
764
|
+
}
|
|
765
|
+
const access = createTokenGetter(settings.provider, saved, (next) => tokens.write(next));
|
|
766
|
+
const forge = createForge(settings.provider, () => {
|
|
767
|
+
token ??= access();
|
|
768
|
+
return token;
|
|
769
|
+
});
|
|
770
|
+
const source = createForgeSource(() => forge, { paths: repositoryPaths(createPaths(settings.contentPath), settings.provider.base) }, repositoryCache());
|
|
771
|
+
function local(changes) {
|
|
772
|
+
const assets = localUploads(changes).map((upload) => previews.asset(upload, settings.mediaUrl));
|
|
773
|
+
const names = new Set(assets.map((asset) => asset.name));
|
|
774
|
+
for (const name of shown) if (!names.has(name)) previews.forget(name);
|
|
775
|
+
shown.clear();
|
|
776
|
+
for (const name of names) shown.add(name);
|
|
777
|
+
return new Map(assets.flatMap((asset) => asset.preview ? [[asset.url, asset.preview]] : []));
|
|
778
|
+
}
|
|
779
|
+
return { build: async () => {
|
|
780
|
+
token = void 0;
|
|
781
|
+
source.reset();
|
|
782
|
+
const changes = await readChanges(pending);
|
|
783
|
+
const content = createContentChanges(source, async () => changes, async () => {
|
|
784
|
+
throw new Error("[forgepress] the preview only reads content");
|
|
785
|
+
});
|
|
786
|
+
const schema = await content.schema();
|
|
787
|
+
const urls = local(changes);
|
|
788
|
+
const listed = await Promise.all(Object.keys(schema.collections).map(async (collection) => [collection, await content.list(collection)]));
|
|
789
|
+
const entries = Object.fromEntries(listed.map(([collection, rows]) => [collection, Object.fromEntries(rows.map((row) => [row.id, urls.size > 0 ? swap(row, urls) : row]))]));
|
|
790
|
+
const files = await createOutput(schema, entries, {
|
|
791
|
+
commit: null,
|
|
792
|
+
unpublished: true
|
|
793
|
+
});
|
|
794
|
+
return new Map(files.map((file) => [file.path, file.text]));
|
|
795
|
+
} };
|
|
796
|
+
}
|
|
797
|
+
export { createPreviewReader };
|