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.
Files changed (57) hide show
  1. package/dist/THIRD-PARTY-LICENSES.md +42 -0
  2. package/dist/_chunks/client.d.mts +2 -0
  3. package/dist/_chunks/config.mjs +79 -0
  4. package/dist/_chunks/content.mjs +733 -0
  5. package/dist/_chunks/error.mjs +4 -0
  6. package/dist/_chunks/fetch.mjs +13 -0
  7. package/dist/_chunks/files.mjs +32 -0
  8. package/dist/_chunks/libs/diff.mjs +485 -0
  9. package/dist/_chunks/locate.mjs +7 -0
  10. package/dist/_chunks/media.mjs +282 -0
  11. package/dist/_chunks/once.mjs +16 -0
  12. package/dist/_chunks/output.mjs +104 -0
  13. package/dist/_chunks/overlay.mjs +8 -0
  14. package/dist/_chunks/plugin.mjs +87 -0
  15. package/dist/_chunks/preview.mjs +248 -0
  16. package/dist/_chunks/project.d.mts +7 -0
  17. package/dist/_chunks/reader.mjs +19 -0
  18. package/dist/_chunks/reader2.mjs +797 -0
  19. package/dist/_chunks/references.mjs +56 -0
  20. package/dist/_chunks/resolve.d.mts +2 -0
  21. package/dist/_chunks/response.mjs +5 -0
  22. package/dist/_chunks/routes.mjs +9 -0
  23. package/dist/_chunks/serialize.mjs +82 -0
  24. package/dist/_chunks/settings.mjs +328 -0
  25. package/dist/_chunks/settings2.mjs +2 -0
  26. package/dist/_chunks/types.d.mts +230 -0
  27. package/dist/_chunks/types2.d.mts +25 -0
  28. package/dist/_chunks/value.mjs +139 -0
  29. package/dist/cli/bin.d.mts +1 -0
  30. package/dist/cli/bin.mjs +61 -0
  31. package/dist/disk/reader.d.mts +4 -0
  32. package/dist/disk/reader.mjs +2 -0
  33. package/dist/editor/index.d.mts +3 -0
  34. package/dist/editor/index.mjs +61951 -0
  35. package/dist/index.d.mts +104 -0
  36. package/dist/index.mjs +237 -0
  37. package/dist/next/preview.d.mts +1 -0
  38. package/dist/next/preview.mjs +3 -0
  39. package/dist/next/reload.d.mts +1 -0
  40. package/dist/next/reload.mjs +20 -0
  41. package/dist/next/settings.d.mts +12 -0
  42. package/dist/next/settings.mjs +2 -0
  43. package/dist/plugin/next.d.mts +11 -0
  44. package/dist/plugin/next.mjs +213 -0
  45. package/dist/plugin/nuxt.d.mts +7 -0
  46. package/dist/plugin/nuxt.mjs +52 -0
  47. package/dist/plugin/watcher.d.mts +1 -0
  48. package/dist/plugin/watcher.mjs +23 -0
  49. package/dist/preview/index.d.mts +4 -0
  50. package/dist/preview/index.mjs +2 -0
  51. package/dist/preview/react.d.mts +1 -0
  52. package/dist/preview/react.mjs +34 -0
  53. package/dist/query/fetch.d.mts +3 -0
  54. package/dist/query/fetch.mjs +2 -0
  55. package/dist/unplugin.d.mts +13 -0
  56. package/dist/unplugin.mjs +2 -0
  57. 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 };