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