@rigel-cms/rigel 1.0.0 → 1.1.0
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/README.md +4 -0
- package/docs/extensions.md +194 -0
- package/package.json +15 -3
- package/src/article-body.ts +49 -37
- package/src/article-render.ts +25 -2
- package/src/article.ts +44 -0
- package/src/config.ts +12 -2
- package/src/editor/App.svelte +98 -97
- package/src/editor/blocks/extension.ts +29 -0
- package/src/editor/blocks/index.ts +17 -15
- package/src/editor/blocks/video.ts +2 -1
- package/src/editor/i18n/README.md +23 -0
- package/src/editor/i18n/de/connection.json +136 -0
- package/src/editor/i18n/de/defaults.json +6 -0
- package/src/editor/i18n/de/fields.json +130 -0
- package/src/editor/i18n/de/local.json +49 -0
- package/src/editor/i18n/de/media.json +166 -0
- package/src/editor/i18n/de/richtext.json +116 -0
- package/src/editor/i18n/de/shell.json +181 -0
- package/src/editor/i18n/de/system.json +14 -0
- package/src/editor/i18n/de/validation.json +82 -0
- package/src/editor/i18n/en/connection.json +136 -0
- package/src/editor/i18n/en/defaults.json +6 -0
- package/src/editor/i18n/en/fields.json +130 -0
- package/src/editor/i18n/en/local.json +49 -0
- package/src/editor/i18n/en/media.json +166 -0
- package/src/editor/i18n/en/richtext.json +116 -0
- package/src/editor/i18n/en/shell.json +181 -0
- package/src/editor/i18n/en/system.json +14 -0
- package/src/editor/i18n/en/validation.json +82 -0
- package/src/editor/i18n/es/connection.json +136 -0
- package/src/editor/i18n/es/defaults.json +6 -0
- package/src/editor/i18n/es/fields.json +130 -0
- package/src/editor/i18n/es/local.json +49 -0
- package/src/editor/i18n/es/media.json +166 -0
- package/src/editor/i18n/es/richtext.json +116 -0
- package/src/editor/i18n/es/shell.json +181 -0
- package/src/editor/i18n/es/system.json +14 -0
- package/src/editor/i18n/es/validation.json +82 -0
- package/src/editor/i18n/format.ts +26 -0
- package/src/editor/i18n/fr/connection.json +136 -0
- package/src/editor/i18n/fr/defaults.json +6 -0
- package/src/editor/i18n/fr/fields.json +130 -0
- package/src/editor/i18n/fr/local.json +49 -0
- package/src/editor/i18n/fr/media.json +166 -0
- package/src/editor/i18n/fr/richtext.json +116 -0
- package/src/editor/i18n/fr/shell.json +181 -0
- package/src/editor/i18n/fr/system.json +14 -0
- package/src/editor/i18n/fr/validation.json +82 -0
- package/src/editor/i18n/index.ts +18 -0
- package/src/editor/i18n/it/connection.json +136 -0
- package/src/editor/i18n/it/defaults.json +6 -0
- package/src/editor/i18n/it/fields.json +130 -0
- package/src/editor/i18n/it/local.json +49 -0
- package/src/editor/i18n/it/media.json +166 -0
- package/src/editor/i18n/it/richtext.json +116 -0
- package/src/editor/i18n/it/shell.json +181 -0
- package/src/editor/i18n/it/system.json +14 -0
- package/src/editor/i18n/it/validation.json +82 -0
- package/src/editor/i18n/ja/connection.json +136 -0
- package/src/editor/i18n/ja/defaults.json +6 -0
- package/src/editor/i18n/ja/fields.json +130 -0
- package/src/editor/i18n/ja/local.json +49 -0
- package/src/editor/i18n/ja/media.json +166 -0
- package/src/editor/i18n/ja/richtext.json +116 -0
- package/src/editor/i18n/ja/shell.json +181 -0
- package/src/editor/i18n/ja/system.json +14 -0
- package/src/editor/i18n/ja/validation.json +82 -0
- package/src/editor/i18n/locales.ts +13 -0
- package/src/editor/i18n/nl/connection.json +136 -0
- package/src/editor/i18n/nl/defaults.json +6 -0
- package/src/editor/i18n/nl/fields.json +130 -0
- package/src/editor/i18n/nl/local.json +49 -0
- package/src/editor/i18n/nl/media.json +166 -0
- package/src/editor/i18n/nl/richtext.json +116 -0
- package/src/editor/i18n/nl/shell.json +181 -0
- package/src/editor/i18n/nl/system.json +14 -0
- package/src/editor/i18n/nl/validation.json +82 -0
- package/src/editor/i18n/ru/connection.json +138 -0
- package/src/editor/i18n/ru/defaults.json +6 -0
- package/src/editor/i18n/ru/fields.json +132 -0
- package/src/editor/i18n/ru/local.json +49 -0
- package/src/editor/i18n/ru/media.json +166 -0
- package/src/editor/i18n/ru/richtext.json +116 -0
- package/src/editor/i18n/ru/shell.json +181 -0
- package/src/editor/i18n/ru/system.json +14 -0
- package/src/editor/i18n/ru/validation.json +82 -0
- package/src/editor/i18n/server.ts +13 -0
- package/src/editor/i18n/zh/connection.json +136 -0
- package/src/editor/i18n/zh/defaults.json +6 -0
- package/src/editor/i18n/zh/fields.json +130 -0
- package/src/editor/i18n/zh/local.json +49 -0
- package/src/editor/i18n/zh/media.json +166 -0
- package/src/editor/i18n/zh/richtext.json +116 -0
- package/src/editor/i18n/zh/shell.json +181 -0
- package/src/editor/i18n/zh/system.json +14 -0
- package/src/editor/i18n/zh/validation.json +82 -0
- package/src/editor/lib/ActionDialog.svelte +5 -4
- package/src/editor/lib/BlockEditor.svelte +305 -88
- package/src/editor/lib/CollectionPanel.svelte +17 -15
- package/src/editor/lib/DatePicker.svelte +15 -12
- package/src/editor/lib/ExtensionMount.svelte +12 -0
- package/src/editor/lib/Field.svelte +3 -2
- package/src/editor/lib/ImagePicker.svelte +82 -81
- package/src/editor/lib/MarkdownEditor.svelte +25 -24
- package/src/editor/lib/MediaPicker.svelte +6 -5
- package/src/editor/lib/MenuEditor.svelte +31 -30
- package/src/editor/lib/PreviewFrame.svelte +11 -6
- package/src/editor/lib/PropertiesTabs.svelte +3 -2
- package/src/editor/lib/PublishDialog.svelte +15 -14
- package/src/editor/lib/ReferenceSelect.svelte +3 -2
- package/src/editor/lib/SchemaField.svelte +15 -13
- package/src/editor/lib/SchemaPanel.svelte +20 -19
- package/src/editor/lib/Select.svelte +3 -2
- package/src/editor/lib/SeoGuidance.svelte +6 -5
- package/src/editor/lib/Settings.svelte +54 -54
- package/src/editor/lib/Shortcuts.svelte +7 -6
- package/src/editor/lib/SitePreview.svelte +10 -9
- package/src/editor/lib/TopBar.svelte +18 -17
- package/src/editor/lib/UnsavedChanges.svelte +7 -6
- package/src/editor/lib/VisualEditor.svelte +14 -13
- package/src/editor/lib/calendar.ts +10 -4
- package/src/editor/lib/embed-preview.ts +23 -5
- package/src/editor/lib/extension-preview.ts +121 -0
- package/src/editor/lib/github-auth.ts +23 -22
- package/src/editor/lib/github-backend.ts +96 -91
- package/src/editor/lib/images.ts +33 -31
- package/src/editor/lib/menu-tree.ts +5 -4
- package/src/editor/lib/preview-frame.ts +4 -3
- package/src/editor/lib/schema-edit.ts +3 -2
- package/src/editor/lib/settings.ts +5 -3
- package/src/editor/lib/slash-command.ts +1 -0
- package/src/editor/lib/tooltip.ts +1 -1
- package/src/editor/main.ts +9 -0
- package/src/editor/styles.css +6 -3
- package/src/env.d.ts +11 -1
- package/src/extensions.ts +160 -0
- package/src/manifest/collections.ts +6 -5
- package/src/manifest/menu.ts +12 -10
- package/src/manifest/navigation.ts +9 -8
- package/src/manifest/references.ts +8 -7
- package/src/manifest/seo.ts +4 -3
- package/src/manifest/types.ts +1 -0
- package/src/preview.ts +22 -2
- package/src/routes/local-api.ts +5 -3
- package/src/routes/local-documents.ts +20 -16
- package/src/routes/local-media.ts +21 -16
- package/src/routes/local-menu.ts +9 -7
- package/src/routes/local-post.ts +18 -12
- package/src/routes/media-files.ts +10 -8
- package/src/routes/rigel-page.astro +2 -1
- package/src/schema/store.ts +22 -20
- package/src/schema/types.ts +1 -0
- package/src/schema/validate.ts +73 -69
- package/src/slugs.ts +11 -10
- package/src/vite/manifest-plugin.ts +33 -1
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { translate } from "../i18n";
|
|
2
|
+
import extensions from "virtual:rigel-extensions";
|
|
3
|
+
import { resolveExtensions, validateExtensionBlocks } from "../../extensions";
|
|
1
4
|
import type { Manifest } from "../../manifest/types";
|
|
2
5
|
import { serializeContentJson } from "../../json-output";
|
|
3
6
|
import type { Selection } from "../../manifest/navigation";
|
|
@@ -23,25 +26,27 @@ class GitError extends Error {
|
|
|
23
26
|
constructor(message: string, public status: number) { super(message); }
|
|
24
27
|
}
|
|
25
28
|
|
|
29
|
+
export class PublishReviewChangedError extends Error {}
|
|
30
|
+
|
|
26
31
|
function safePath(path: string, empty = false): string {
|
|
27
|
-
if (typeof path !== "string" || (!path && !empty) || (path && !path.split("/").every(part => /^[A-Za-z0-9_-][A-Za-z0-9_. -]*$/.test(part)))) throw new Error("
|
|
32
|
+
if (typeof path !== "string" || (!path && !empty) || (path && !path.split("/").every(part => /^[A-Za-z0-9_-][A-Za-z0-9_. -]*$/.test(part)))) throw new Error(translate("connection.unsafePath"));
|
|
28
33
|
return path;
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
function branch(name: string): string {
|
|
32
|
-
if (typeof name !== "string" || !/^[A-Za-z0-9_-][A-Za-z0-9_./-]*$/.test(name) || name.includes("..") || name.includes("//") || name.endsWith("/") || name.endsWith(".") || name.split("/").some(part => part.startsWith(".") || part.endsWith(".lock"))) throw new Error("
|
|
37
|
+
if (typeof name !== "string" || !/^[A-Za-z0-9_-][A-Za-z0-9_./-]*$/.test(name) || name.includes("..") || name.includes("//") || name.endsWith("/") || name.endsWith(".") || name.split("/").some(part => part.startsWith(".") || part.endsWith(".lock"))) throw new Error(translate("connection.invalidBranch"));
|
|
33
38
|
return name;
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
function config(ctx: GitContext, manifest: Manifest) {
|
|
37
42
|
const cfg = manifest.github;
|
|
38
|
-
if (!cfg || !/^[A-Za-z0-9_-]+\/[A-Za-z0-9_.-]+$/.test(ctx.repo) || ctx.repo !== cfg.repo || !ctx.token.trim()) throw new Error("
|
|
43
|
+
if (!cfg || !/^[A-Za-z0-9_-]+\/[A-Za-z0-9_.-]+$/.test(ctx.repo) || ctx.repo !== cfg.repo || !ctx.token.trim()) throw new Error(translate("connection.connectFirst"));
|
|
39
44
|
branch(ctx.branch); branch(cfg.publishBranch);
|
|
40
|
-
if (ctx.branch !== (cfg.branch || "drafts") || ctx.branch === cfg.publishBranch || ctx.branch === "main" || ctx.branch === "master") throw new Error("
|
|
45
|
+
if (ctx.branch !== (cfg.branch || "drafts") || ctx.branch === cfg.publishBranch || ctx.branch === "main" || ctx.branch === "master") throw new Error(translate("connection.dedicatedDraftBranch"));
|
|
41
46
|
safePath(cfg.repoRoot, true); safePath(cfg.copyDir);
|
|
42
47
|
const folders = [cfg.copyDir, ...cfg.collections.map(c => c.folder), ...manifest.schema?.documents.map(d => d.folder) ?? [], ...(cfg.menus ? [cfg.menus.folder] : [])];
|
|
43
48
|
folders.forEach(folder => safePath(folder));
|
|
44
|
-
if (folders.some((folder, index) => folders.some((other, j) => j !== index && (folder === other || folder.startsWith(other + "/"))))) throw new Error("
|
|
49
|
+
if (folders.some((folder, index) => folders.some((other, j) => j !== index && (folder === other || folder.startsWith(other + "/"))))) throw new Error(translate("connection.overlappingDirectories"));
|
|
45
50
|
if (manifest.schema) validateSchema(manifest.schema);
|
|
46
51
|
return cfg;
|
|
47
52
|
}
|
|
@@ -49,7 +54,7 @@ function config(ctx: GitContext, manifest: Manifest) {
|
|
|
49
54
|
function target(ctx: GitContext, manifest: Manifest) { return JSON.stringify([ctx.repo, ctx.branch, manifest.github, manifest.schema]); }
|
|
50
55
|
function filePath(ctx: GitContext, manifest: Manifest, folder: string, slug: string, extension = "json", pattern = SLUG) {
|
|
51
56
|
const cfg = config(ctx, manifest);
|
|
52
|
-
if (!pattern.test(slug)) throw new Error("
|
|
57
|
+
if (!pattern.test(slug)) throw new Error(translate("connection.invalidFilename"));
|
|
53
58
|
return [cfg.repoRoot, safePath(folder), `${slug}.${extension}`].filter(Boolean).join("/");
|
|
54
59
|
}
|
|
55
60
|
|
|
@@ -59,17 +64,17 @@ async function gh(ctx: GitContext, path: string, init: RequestInit = {}): Promis
|
|
|
59
64
|
response = await fetch(`https://api.github.com/repos/${ctx.repo}${path}`, {
|
|
60
65
|
...init, headers: {Authorization: `Bearer ${ctx.token}`, Accept: "application/vnd.github+json", "X-GitHub-Api-Version":"2022-11-28", "Content-Type":"application/json"}, redirect:"error", cache:"no-store", credentials:"omit", signal:AbortSignal.timeout(15000),
|
|
61
66
|
});
|
|
62
|
-
} catch { throw new Error("
|
|
67
|
+
} catch { throw new Error(translate("connection.requestFailed")); }
|
|
63
68
|
if (!response.ok) {
|
|
64
69
|
const rateLimited = response.status === 429 || response.headers.get("x-ratelimit-remaining") === "0" || response.headers.has("retry-after");
|
|
65
|
-
const
|
|
66
|
-
throw new GitError(
|
|
70
|
+
const key = rateLimited ? "connection.requestRateLimited" : ({401:"connection.requestSessionExpired",403:"connection.requestAccessDenied",404:"connection.requestUnavailable",409:"connection.requestConflict",422:"connection.requestRejected"} as Record<number,string>)[response.status] ?? "connection.requestError";
|
|
71
|
+
throw new GitError(translate(key, { status: response.status }), response.status);
|
|
67
72
|
}
|
|
68
|
-
try { return await response.json(); } catch { throw new Error("
|
|
73
|
+
try { return await response.json(); } catch { throw new Error(translate("connection.malformedResponse")); }
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
function sha(value: unknown): string {
|
|
72
|
-
if (typeof value !== "string" || !SHA.test(value)) throw new Error("
|
|
77
|
+
if (typeof value !== "string" || !SHA.test(value)) throw new Error(translate("connection.malformedSha"));
|
|
73
78
|
return value;
|
|
74
79
|
}
|
|
75
80
|
|
|
@@ -85,39 +90,39 @@ async function draftHead(ctx: GitContext, manifest: Manifest, create = true): Pr
|
|
|
85
90
|
}
|
|
86
91
|
|
|
87
92
|
function decode(payload: any): string {
|
|
88
|
-
if (!payload || payload.encoding !== "base64" || typeof payload.content !== "string") throw new Error("
|
|
93
|
+
if (!payload || payload.encoding !== "base64" || typeof payload.content !== "string") throw new Error(translate("connection.malformedPayload"));
|
|
89
94
|
const base64 = payload.content.replace(/[\r\n]/g, "");
|
|
90
|
-
if (base64.length > Math.ceil(MAX_BYTES / 3) * 4 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(base64)) throw new Error("
|
|
95
|
+
if (base64.length > Math.ceil(MAX_BYTES / 3) * 4 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(base64)) throw new Error(translate("connection.invalidPayload"));
|
|
91
96
|
try {
|
|
92
97
|
const bytes = Uint8Array.from(atob(base64), char => char.charCodeAt(0));
|
|
93
98
|
if (bytes.length > MAX_BYTES) throw new Error();
|
|
94
99
|
return new TextDecoder("utf-8", {fatal:true}).decode(bytes);
|
|
95
|
-
} catch { throw new Error("
|
|
100
|
+
} catch { throw new Error(translate("connection.invalidUtf8")); }
|
|
96
101
|
}
|
|
97
102
|
|
|
98
103
|
function encode(text: string): string {
|
|
99
104
|
const bytes = new TextEncoder().encode(text);
|
|
100
|
-
if (bytes.length > MAX_BYTES) throw new Error("
|
|
105
|
+
if (bytes.length > MAX_BYTES) throw new Error(translate("connection.contentTooLarge"));
|
|
101
106
|
let binary = "";
|
|
102
107
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
103
108
|
return btoa(binary);
|
|
104
109
|
}
|
|
105
110
|
|
|
106
111
|
function parseJson(text: string): unknown {
|
|
107
|
-
try { return JSON.parse(text); } catch { throw new Error("
|
|
112
|
+
try { return JSON.parse(text); } catch { throw new Error(translate("connection.invalidRemoteJson")); }
|
|
108
113
|
}
|
|
109
114
|
|
|
110
115
|
export async function loadManifestRemote(ctx: GitContext, blueprint: Manifest, createDraft = true): Promise<Manifest> {
|
|
111
116
|
const cfg = config(ctx, blueprint);
|
|
112
117
|
const head = await draftHead(ctx, blueprint, createDraft);
|
|
113
118
|
const tree = await gh(ctx, `/git/trees/${head}?recursive=1`);
|
|
114
|
-
if (!tree || tree.truncated !== false || !Array.isArray(tree.tree)) throw new Error("
|
|
119
|
+
if (!tree || tree.truncated !== false || !Array.isArray(tree.tree)) throw new Error(translate("connection.incompleteTree"));
|
|
115
120
|
const manifest = structuredClone(blueprint);
|
|
116
121
|
const snapshot: Snapshot = {target:target(ctx,manifest),head,writes:Promise.resolve(),files:new Map(),paths:new Set(),pending:new Map()};
|
|
117
122
|
const entries = new Map<string, any>();
|
|
118
123
|
for (const entry of tree.tree) {
|
|
119
|
-
if (!entry || typeof entry.path !== "string" || entry.path.split("/").some((part: string) => !part || part === "." || part === "..") || /[\\\u0000-\u001f]/.test(entry.path) || !["blob","tree","commit"].includes(entry.type) || !SHA.test(entry.sha) || !["100644","100755","120000","040000","160000"].includes(entry.mode)) throw new Error("
|
|
120
|
-
if (entries.has(entry.path)) throw new Error("
|
|
124
|
+
if (!entry || typeof entry.path !== "string" || entry.path.split("/").some((part: string) => !part || part === "." || part === "..") || /[\\\u0000-\u001f]/.test(entry.path) || !["blob","tree","commit"].includes(entry.type) || !SHA.test(entry.sha) || !["100644","100755","120000","040000","160000"].includes(entry.mode)) throw new Error(translate("connection.malformedTreeEntry"));
|
|
125
|
+
if (entries.has(entry.path)) throw new Error(translate("connection.duplicateTreePath"));
|
|
121
126
|
entries.set(entry.path, entry); snapshot.paths.add(entry.path);
|
|
122
127
|
}
|
|
123
128
|
const folders = [cfg.copyDir, ...cfg.collections.map(c => c.folder), ...(manifest.schema?.documents.map(d => d.folder) ?? []), ...(cfg.menus ? [cfg.menus.folder] : [])];
|
|
@@ -125,57 +130,57 @@ export async function loadManifestRemote(ctx: GitContext, blueprint: Manifest, c
|
|
|
125
130
|
const parts = [cfg.repoRoot,folder].filter(Boolean).join("/").split("/");
|
|
126
131
|
for (let count = 1; count <= parts.length; count++) {
|
|
127
132
|
const entry = entries.get(parts.slice(0,count).join("/"));
|
|
128
|
-
if (entry && (entry.type !== "tree" || entry.mode !== "040000")) throw new Error("
|
|
133
|
+
if (entry && (entry.type !== "tree" || entry.mode !== "040000")) throw new Error(translate("connection.invalidDirectory"));
|
|
129
134
|
}
|
|
130
135
|
}
|
|
131
136
|
const read = async (path: string): Promise<string> => {
|
|
132
137
|
const entry = entries.get(path);
|
|
133
|
-
if (!entry || entry.type !== "blob" || !["100644","100755"].includes(entry.mode)) throw new Error("
|
|
138
|
+
if (!entry || entry.type !== "blob" || !["100644","100755"].includes(entry.mode)) throw new Error(translate("connection.invalidFile"));
|
|
134
139
|
const expected = sha(entry.sha);
|
|
135
140
|
const blob = await gh(ctx, `/git/blobs/${expected}`);
|
|
136
|
-
if (sha(blob?.sha) !== expected) throw new Error("
|
|
141
|
+
if (sha(blob?.sha) !== expected) throw new Error(translate("connection.blobShaMismatch"));
|
|
137
142
|
const text = decode(blob); snapshot.files.set(path,{sha:expected,text,mode:entry.mode}); return text;
|
|
138
143
|
};
|
|
139
144
|
const list = (folder: string, pattern = SLUG) => {
|
|
140
145
|
const prefix = [cfg.repoRoot,folder].filter(Boolean).join("/") + "/";
|
|
141
146
|
return [...entries.keys()].filter(path => path.startsWith(prefix) && !path.slice(prefix.length).includes("/") && path.endsWith(".json")).sort().map(path => {
|
|
142
147
|
const slug = path.slice(prefix.length,-5);
|
|
143
|
-
if (!pattern.test(slug)) throw new Error("
|
|
148
|
+
if (!pattern.test(slug)) throw new Error(translate("connection.invalidRemoteFilename"));
|
|
144
149
|
return {path,slug};
|
|
145
150
|
});
|
|
146
151
|
};
|
|
147
152
|
for (const page of manifest.pages) {
|
|
148
153
|
const path = filePath(ctx,manifest,cfg.copyDir,page.slug,"ts");
|
|
149
154
|
const values = readValues(await read(path));
|
|
150
|
-
if (!Object.values(values).every(isPlainObject)) throw new Error("
|
|
155
|
+
if (!Object.values(values).every(isPlainObject)) throw new Error(translate("connection.invalidCopyGroups"));
|
|
151
156
|
page.values = values as typeof page.values;
|
|
152
157
|
}
|
|
153
158
|
manifest.documents = [];
|
|
154
159
|
for (const type of manifest.schema?.documents ?? []) for (const file of list(type.folder)) {
|
|
155
160
|
const data = parseJson(await read(file.path));
|
|
156
|
-
if (!isPlainObject(data)) throw new Error("
|
|
161
|
+
if (!isPlainObject(data)) throw new Error(translate("connection.invalidDocument"));
|
|
157
162
|
manifest.documents.push({id:`${type.name}:${file.slug}`,type:type.name,slug:file.slug,data});
|
|
158
163
|
}
|
|
159
|
-
documentIndex(manifest.documents);
|
|
160
|
-
for (const doc of manifest.documents) validateDocument(manifest.schema!,doc.type,doc.slug,doc.data,manifest.documents);
|
|
161
|
-
if (manifest.schema) validateDocumentSlugs(manifest.schema, manifest.documents, manifest.preview);
|
|
164
|
+
documentIndex(manifest.documents, translate);
|
|
165
|
+
for (const doc of manifest.documents) validateDocument(manifest.schema!,doc.type,doc.slug,doc.data,manifest.documents,translate);
|
|
166
|
+
if (manifest.schema) validateDocumentSlugs(manifest.schema, manifest.documents, manifest.preview, translate);
|
|
162
167
|
for (const collection of manifest.collections ?? []) {
|
|
163
168
|
const folder = cfg.collections.find(c => c.name === collection.name)?.folder;
|
|
164
|
-
if (!folder) throw new Error("
|
|
169
|
+
if (!folder) throw new Error(translate("connection.missingCollection"));
|
|
165
170
|
collection.posts = [];
|
|
166
171
|
for (const file of list(folder,POST_SLUG)) {
|
|
167
172
|
const post = parseJson(await read(file.path));
|
|
168
|
-
if (!isPlainObject(post) || !isPlainObject(post.frontmatter)) throw new Error("
|
|
169
|
-
validateBody(post.body); validateCollectionValues(collection.fields,post.frontmatter,manifest.documents);
|
|
173
|
+
if (!isPlainObject(post) || !isPlainObject(post.frontmatter)) throw new Error(translate("connection.invalidArticle"));
|
|
174
|
+
validateBody(post.body,translate); validateCollectionValues(collection.fields,post.frontmatter,manifest.documents,translate);
|
|
170
175
|
collection.posts.push({slug:file.slug,frontmatter:post.frontmatter,body:post.body});
|
|
171
176
|
}
|
|
172
|
-
assertUniqueUrlSlugs(collection.posts.map(post => ({ slug: post.slug, values: post.frontmatter })));
|
|
177
|
+
assertUniqueUrlSlugs(collection.posts.map(post => ({ slug: post.slug, values: post.frontmatter })), translate);
|
|
173
178
|
}
|
|
174
179
|
if (cfg.menus) {
|
|
175
180
|
manifest.menus = {maxDepth:cfg.menus.maxDepth,entries:[]};
|
|
176
181
|
for (const file of list(cfg.menus.folder)) {
|
|
177
|
-
const menu = validateMenu(parseJson(await read(file.path)),cfg.menus.maxDepth);
|
|
178
|
-
if (menu.slug !== file.slug) throw new Error("
|
|
182
|
+
const menu = validateMenu(parseJson(await read(file.path)),cfg.menus.maxDepth,translate);
|
|
183
|
+
if (menu.slug !== file.slug) throw new Error(translate("connection.menuSlugMismatch"));
|
|
179
184
|
manifest.menus.entries.push(menu);
|
|
180
185
|
}
|
|
181
186
|
}
|
|
@@ -186,7 +191,7 @@ export async function loadManifestRemote(ctx: GitContext, blueprint: Manifest, c
|
|
|
186
191
|
function loaded(ctx: GitContext, manifest: Manifest): Snapshot {
|
|
187
192
|
config(ctx,manifest);
|
|
188
193
|
const snapshot = (manifest as Manifest & { [REMOTE_SNAPSHOT]?: Snapshot })[REMOTE_SNAPSHOT];
|
|
189
|
-
if (!snapshot || snapshot.target !== target(ctx,manifest)) throw new Error("
|
|
194
|
+
if (!snapshot || snapshot.target !== target(ctx,manifest)) throw new Error(translate("connection.reloadDraft"));
|
|
190
195
|
return snapshot;
|
|
191
196
|
}
|
|
192
197
|
|
|
@@ -194,14 +199,14 @@ async function mutate(ctx: GitContext, manifest: Manifest, path: string, text: s
|
|
|
194
199
|
const snapshot = loaded(ctx,manifest);
|
|
195
200
|
const head = sha((await gh(ctx, `/git/ref/heads/${encodeURIComponent(ctx.branch)}`))?.object?.sha);
|
|
196
201
|
const uncertain = snapshot.pending.get(path);
|
|
197
|
-
if (head !== snapshot.head && head !== uncertain?.head) throw new Error("
|
|
202
|
+
if (head !== snapshot.head && head !== uncertain?.head) throw new Error(translate("connection.draftConflictUnsaved"));
|
|
198
203
|
const endpoint = `/contents/${path.split("/").map(encodeURIComponent).join("/")}`;
|
|
199
204
|
const verify = async (pending: PendingWrite) => {
|
|
200
205
|
const current = sha((await gh(ctx, `/git/ref/heads/${encodeURIComponent(ctx.branch)}`))?.object?.sha);
|
|
201
206
|
if (current !== pending.head) {
|
|
202
|
-
if (current !== snapshot.head) throw new Error("
|
|
207
|
+
if (current !== snapshot.head) throw new Error(translate("connection.draftConflict"));
|
|
203
208
|
const result = await gh(ctx, `/git/refs/heads/${encodeURIComponent(ctx.branch)}`, { method: "PATCH", body: JSON.stringify({ sha: pending.head, force: false }) });
|
|
204
|
-
if (sha(result?.object?.sha) !== pending.head) throw new Error("
|
|
209
|
+
if (sha(result?.object?.sha) !== pending.head) throw new Error(translate("connection.branchUpdateUnverified"));
|
|
205
210
|
}
|
|
206
211
|
let verified;
|
|
207
212
|
try { verified = await gh(ctx,`${endpoint}?ref=${encodeURIComponent(ctx.branch)}`); }
|
|
@@ -209,10 +214,10 @@ async function mutate(ctx: GitContext, manifest: Manifest, path: string, text: s
|
|
|
209
214
|
if (pending.text !== null || !(error instanceof GitError) || error.status !== 404) throw error;
|
|
210
215
|
}
|
|
211
216
|
if (pending.text === null) {
|
|
212
|
-
if (verified !== undefined) throw new Error("
|
|
217
|
+
if (verified !== undefined) throw new Error(translate("connection.deletionUnverified"));
|
|
213
218
|
snapshot.files.delete(path); snapshot.paths.delete(path);
|
|
214
219
|
} else {
|
|
215
|
-
if (verified?.type !== "file" || sha(verified.sha) !== pending.sha || decode(verified) !== pending.text) throw new Error("
|
|
220
|
+
if (verified?.type !== "file" || sha(verified.sha) !== pending.sha || decode(verified) !== pending.text) throw new Error(translate("connection.saveChanged"));
|
|
216
221
|
snapshot.files.set(path,{sha:pending.sha,text:pending.text,mode:pending.mode}); snapshot.paths.add(path);
|
|
217
222
|
}
|
|
218
223
|
snapshot.head = pending.head;
|
|
@@ -224,7 +229,7 @@ async function mutate(ctx: GitContext, manifest: Manifest, path: string, text: s
|
|
|
224
229
|
if (creating && uncertain.creating) creating = false;
|
|
225
230
|
}
|
|
226
231
|
const previous = snapshot.files.get(path);
|
|
227
|
-
if (creating ? snapshot.paths.has(path) : !previous) throw new Error(creating ? "
|
|
232
|
+
if (creating ? snapshot.paths.has(path) : !previous) throw new Error(creating ? translate("connection.contentExists") : translate("connection.reloadContent"));
|
|
228
233
|
const blob = text === null ? null : sha((await gh(ctx, "/git/blobs", { method: "POST", body: JSON.stringify({ content: encode(text), encoding: "base64" }) }))?.sha);
|
|
229
234
|
const mode = previous?.mode ?? "100644";
|
|
230
235
|
const base = sha((await gh(ctx, `/git/commits/${snapshot.head}`))?.tree?.sha);
|
|
@@ -237,7 +242,7 @@ async function mutate(ctx: GitContext, manifest: Manifest, path: string, text: s
|
|
|
237
242
|
|
|
238
243
|
export async function saveRemoteSelection(...args: Parameters<typeof saveSelection>): Promise<void> {
|
|
239
244
|
const key = writeKey(args[0]);
|
|
240
|
-
if (publishing.has(key)) throw new Error("
|
|
245
|
+
if (publishing.has(key)) throw new Error(translate("connection.waitBeforeSaving"));
|
|
241
246
|
const snapshot = loaded(args[0], args[1]);
|
|
242
247
|
saving.set(key, (saving.get(key) ?? 0) + 1);
|
|
243
248
|
const result = snapshot.writes.then(() => saveSelection(...args));
|
|
@@ -248,48 +253,48 @@ export async function saveRemoteSelection(...args: Parameters<typeof saveSelecti
|
|
|
248
253
|
async function saveSelection(ctx: GitContext, manifest: Manifest, selection: Selection, values: Record<string, unknown>, body: ArticleBody | "", creating: boolean): Promise<void> {
|
|
249
254
|
const cfg = config(ctx,manifest);
|
|
250
255
|
const snapshot = loaded(ctx,manifest);
|
|
251
|
-
if (!isPlainObject(values)) throw new Error("
|
|
256
|
+
if (!isPlainObject(values)) throw new Error(translate("connection.invalidValues"));
|
|
252
257
|
let path: string;
|
|
253
258
|
let data: unknown;
|
|
254
259
|
if (selection.kind === "page") {
|
|
255
|
-
if (creating || !manifest.pages.some(page => page.slug === selection.slug)) throw new Error("
|
|
256
|
-
if (!Object.values(values).every(isPlainObject)) throw new Error("
|
|
260
|
+
if (creating || !manifest.pages.some(page => page.slug === selection.slug)) throw new Error(translate("connection.unknownCopyPage"));
|
|
261
|
+
if (!Object.values(values).every(isPlainObject)) throw new Error(translate("connection.invalidCopyGroups"));
|
|
257
262
|
path = filePath(ctx,manifest,cfg.copyDir,selection.slug,"ts");
|
|
258
263
|
const source = snapshot.files.get(path)?.text;
|
|
259
|
-
if (!source) throw new Error("
|
|
264
|
+
if (!source) throw new Error(translate("connection.reloadCopyPage"));
|
|
260
265
|
await mutate(ctx,manifest,path,applyValues(source,values),false); return;
|
|
261
266
|
} else if (selection.kind === "post") {
|
|
262
267
|
const folder = cfg.collections.find(c => c.name === selection.collection)?.folder;
|
|
263
268
|
const collection = manifest.collections?.find(c => c.name === selection.collection);
|
|
264
|
-
if (!folder || !collection) throw new Error("
|
|
265
|
-
validateBody(body); validateCollectionValues(collection.fields,values,manifest.documents ?? []);
|
|
269
|
+
if (!folder || !collection) throw new Error(translate("connection.unknownCollection"));
|
|
270
|
+
validateBody(body,translate); validateExtensionBlocks(body, resolveExtensions(extensions).blocks); validateCollectionValues(collection.fields,values,manifest.documents ?? [],translate);
|
|
266
271
|
path = filePath(ctx,manifest,folder,selection.slug,"json",POST_SLUG);
|
|
267
272
|
const frontmatter = { ...values };
|
|
268
|
-
if (Object.hasOwn(frontmatter, "slug")) frontmatter.slug = normalizeUrlSlug(frontmatter.slug);
|
|
273
|
+
if (Object.hasOwn(frontmatter, "slug")) frontmatter.slug = normalizeUrlSlug(frontmatter.slug, translate);
|
|
269
274
|
const prefix = filePath(ctx,manifest,folder,"placeholder","json",POST_SLUG).replace(/placeholder\.json$/, "");
|
|
270
275
|
const others = [...snapshot.files].filter(([file]) => file.startsWith(prefix) && file !== path && file.endsWith(".json")).map(([file, value]) => ({ slug: file.slice(prefix.length, -5), values: (parseJson(value.text) as { frontmatter: Record<string, unknown> }).frontmatter }));
|
|
271
|
-
assertUniqueUrlSlugs([...others, { slug: selection.slug, values: frontmatter }]);
|
|
276
|
+
assertUniqueUrlSlugs([...others, { slug: selection.slug, values: frontmatter }], translate);
|
|
272
277
|
data = {frontmatter,body};
|
|
273
278
|
} else if (selection.kind === "document") {
|
|
274
|
-
if (!manifest.schema) throw new Error("
|
|
279
|
+
if (!manifest.schema) throw new Error(translate("connection.missingSchema"));
|
|
275
280
|
const document = validateDocumentId(manifest.schema,selection.id);
|
|
276
|
-
if (!document || document.type.name !== selection.type) throw new Error("
|
|
277
|
-
if (creating && (document.type.creatable === false || document.type.singleton)) throw new Error("
|
|
281
|
+
if (!document || document.type.name !== selection.type) throw new Error(translate("connection.unknownDocument"));
|
|
282
|
+
if (creating && (document.type.creatable === false || document.type.singleton)) throw new Error(translate("connection.creationDisabled"));
|
|
278
283
|
const next = {...values,_id:selection.id,_type:selection.type} as Record<string, unknown>;
|
|
279
|
-
if (!document.type.shared && Object.hasOwn(next, "slug")) next.slug = normalizeUrlSlug(next.slug);
|
|
284
|
+
if (!document.type.shared && Object.hasOwn(next, "slug")) next.slug = normalizeUrlSlug(next.slug, translate);
|
|
280
285
|
data = next;
|
|
281
|
-
validateDocument(manifest.schema,selection.type,document.slug,data as Record<string,unknown>,manifest.documents ?? []);
|
|
286
|
+
validateDocument(manifest.schema,selection.type,document.slug,data as Record<string,unknown>,manifest.documents ?? [],translate);
|
|
282
287
|
path = filePath(ctx,manifest,document.type.folder,document.slug);
|
|
283
288
|
const documents = manifest.schema.documents.flatMap(type => {
|
|
284
289
|
const prefix = filePath(ctx,manifest,type.folder,"placeholder").replace(/placeholder\.json$/, "");
|
|
285
290
|
return [...snapshot.files].filter(([file]) => file.startsWith(prefix) && file !== path && file.endsWith(".json")).map(([file, value]) => ({ id: `${type.name}:${file.slice(prefix.length,-5)}`, type: type.name, slug: file.slice(prefix.length,-5), data: parseJson(value.text) as Record<string, unknown> }));
|
|
286
291
|
});
|
|
287
|
-
validateDocumentSlugs(manifest.schema, [...documents, { id: selection.id, type: selection.type, slug: document.slug, data: next }], manifest.preview);
|
|
292
|
+
validateDocumentSlugs(manifest.schema, [...documents, { id: selection.id, type: selection.type, slug: document.slug, data: next }], manifest.preview, translate);
|
|
288
293
|
} else if (selection.kind === "menu") {
|
|
289
|
-
if (!cfg.menus || creating) throw new Error("
|
|
290
|
-
data = validateMenu({...values,slug:selection.slug},cfg.menus.maxDepth);
|
|
294
|
+
if (!cfg.menus || creating) throw new Error(translate("connection.menusNotConfigured"));
|
|
295
|
+
data = validateMenu({...values,slug:selection.slug},cfg.menus.maxDepth,translate);
|
|
291
296
|
path = filePath(ctx,manifest,cfg.menus.folder,selection.slug);
|
|
292
|
-
} else throw new Error("
|
|
297
|
+
} else throw new Error(translate("connection.selectContent"));
|
|
293
298
|
await mutate(ctx,manifest,path,serializeContentJson(data,cfg.jsonOutput),creating);
|
|
294
299
|
}
|
|
295
300
|
|
|
@@ -307,12 +312,12 @@ async function publishHeads(ctx: GitContext, publishBranch: string, request: typ
|
|
|
307
312
|
|
|
308
313
|
async function publishTree(ctx: GitContext, head: string, request: typeof gh) {
|
|
309
314
|
const data = await request(ctx, `/git/trees/${head}?recursive=1`);
|
|
310
|
-
if (data?.truncated !== false || !Array.isArray(data.tree)) throw new Error("
|
|
315
|
+
if (data?.truncated !== false || !Array.isArray(data.tree)) throw new Error(translate("connection.incompletePublishTree"));
|
|
311
316
|
const files = new Map<string, string>();
|
|
312
317
|
const paths = new Set<string>();
|
|
313
318
|
for (const entry of data.tree) {
|
|
314
|
-
if (!entry || typeof entry.path !== "string" || !entry.path || /[\u0000-\u001f\u007f\\\\]/.test(entry.path) || entry.path.split("/").some((p: string) => !p || p === "." || p === "..") || paths.has(entry.path) || !["blob", "tree", "commit"].includes(entry.type) || !["100644", "100755", "120000", "040000", "160000"].includes(entry.mode)) throw new Error("
|
|
315
|
-
if (!({ blob: ["100644", "100755", "120000"], tree: ["040000"], commit: ["160000"] } as Record<string, string[]>)[entry.type]?.includes(entry.mode)) throw new Error("
|
|
319
|
+
if (!entry || typeof entry.path !== "string" || !entry.path || /[\u0000-\u001f\u007f\\\\]/.test(entry.path) || entry.path.split("/").some((p: string) => !p || p === "." || p === "..") || paths.has(entry.path) || !["blob", "tree", "commit"].includes(entry.type) || !["100644", "100755", "120000", "040000", "160000"].includes(entry.mode)) throw new Error(translate("connection.malformedPublishTree"));
|
|
320
|
+
if (!({ blob: ["100644", "100755", "120000"], tree: ["040000"], commit: ["160000"] } as Record<string, string[]>)[entry.type]?.includes(entry.mode)) throw new Error(translate("connection.malformedPublishTreeMode"));
|
|
316
321
|
paths.add(entry.path);
|
|
317
322
|
const id = sha(entry.sha);
|
|
318
323
|
if (entry.type !== "tree") files.set(entry.path, `${entry.mode}:${entry.type}:${id}`);
|
|
@@ -324,37 +329,37 @@ export async function reviewRemotePublish(ctx: GitContext, manifest: Manifest, a
|
|
|
324
329
|
const cfg = config(ctx, manifest);
|
|
325
330
|
const request: typeof gh = (context, path, init) => { assertActive(); return gh(context, path, init); };
|
|
326
331
|
const repository = await request(ctx, "");
|
|
327
|
-
if (repository?.permissions?.push !== true) throw new Error("
|
|
328
|
-
if (repository.allow_squash_merge !== true) throw new Error("
|
|
329
|
-
if (repository.delete_branch_on_merge !== false) throw new Error("
|
|
332
|
+
if (repository?.permissions?.push !== true) throw new Error(translate("connection.publishWriteRequired"));
|
|
333
|
+
if (repository.allow_squash_merge !== true) throw new Error(translate("connection.squashDisabled"));
|
|
334
|
+
if (repository.delete_branch_on_merge !== false) throw new Error(translate("connection.automaticDeletionEnabled"));
|
|
330
335
|
const { head, base } = await publishHeads(ctx, cfg.publishBranch, request);
|
|
331
336
|
const [source, destination] = await Promise.all([publishTree(ctx, head, request), publishTree(ctx, base, request)]);
|
|
332
337
|
const changed = new Set([...source.keys(), ...destination.keys()].filter(path => source.get(path) !== destination.get(path)));
|
|
333
338
|
const review: PublishReview = { repo: ctx.repo, branch: ctx.branch, publishBranch: cfg.publishBranch, head, base, files: [], needsSync: false };
|
|
334
339
|
if (changed.size) {
|
|
335
340
|
const comparison = await request(ctx, `/compare/${base}...${head}?per_page=1&page=1`);
|
|
336
|
-
if (sha(comparison?.base_commit?.sha) !== base) throw new Error("
|
|
341
|
+
if (sha(comparison?.base_commit?.sha) !== base) throw new Error(translate("connection.comparisonTargetMismatch"));
|
|
337
342
|
review.needsSync = sha(comparison?.merge_base_commit?.sha) !== base;
|
|
338
343
|
if (review.needsSync) {
|
|
339
344
|
review.files = [...changed].sort().map(filename => ({ filename, status: !source.has(filename) ? "removed" : !destination.has(filename) ? "added" : "modified" }));
|
|
340
345
|
} else {
|
|
341
|
-
if (!Array.isArray(comparison.files) || comparison.files.length >= 300) throw new Error("
|
|
346
|
+
if (!Array.isArray(comparison.files) || comparison.files.length >= 300) throw new Error(translate("connection.comparisonLimit"));
|
|
342
347
|
const covered = new Set<string>();
|
|
343
348
|
for (const file of comparison.files) {
|
|
344
|
-
if (!file || typeof file.filename !== "string" || covered.has(file.filename) || !changed.has(file.filename) || !["added", "removed", "modified", "renamed"].includes(file.status)) throw new Error("
|
|
349
|
+
if (!file || typeof file.filename !== "string" || covered.has(file.filename) || !changed.has(file.filename) || !["added", "removed", "modified", "renamed"].includes(file.status)) throw new Error(translate("connection.unsupportedComparison"));
|
|
345
350
|
covered.add(file.filename);
|
|
346
351
|
if (file.status === "renamed") {
|
|
347
|
-
if (typeof file.previous_filename !== "string" || !changed.has(file.previous_filename) || covered.has(file.previous_filename)) throw new Error("
|
|
352
|
+
if (typeof file.previous_filename !== "string" || !changed.has(file.previous_filename) || covered.has(file.previous_filename)) throw new Error(translate("connection.incompleteRename"));
|
|
348
353
|
covered.add(file.previous_filename);
|
|
349
354
|
}
|
|
350
355
|
review.files.push({ filename: file.filename, status: file.status, ...(file.status === "renamed" ? { previous_filename: file.previous_filename } : {}) });
|
|
351
356
|
}
|
|
352
|
-
if (covered.size !== changed.size) throw new Error("
|
|
357
|
+
if (covered.size !== changed.size) throw new Error(translate("connection.incompleteComparison"));
|
|
353
358
|
}
|
|
354
|
-
if (review.files.length >= 300) throw new Error("
|
|
359
|
+
if (review.files.length >= 300) throw new Error(translate("connection.publishFileLimit"));
|
|
355
360
|
}
|
|
356
361
|
const current = await publishHeads(ctx, cfg.publishBranch, request);
|
|
357
|
-
if (current.head !== head || current.base !== base) throw new
|
|
362
|
+
if (current.head !== head || current.base !== base) throw new PublishReviewChangedError(translate("connection.branchesChangedDuringReview"));
|
|
358
363
|
assertActive();
|
|
359
364
|
return review;
|
|
360
365
|
}
|
|
@@ -365,18 +370,18 @@ const writeKey = (ctx: GitContext) => `${ctx.repo}:${ctx.branch}`;
|
|
|
365
370
|
|
|
366
371
|
export async function confirmRemotePublish(ctx: GitContext, manifest: Manifest, review: PublishReview, assertActive: () => void = () => {}): Promise<"published" | "synced"> {
|
|
367
372
|
const cfg = config(ctx, manifest), key = writeKey(ctx);
|
|
368
|
-
if (publishing.has(key) || saving.get(key)) throw new Error("
|
|
369
|
-
if (review.repo !== ctx.repo || review.branch !== ctx.branch || review.publishBranch !== cfg.publishBranch || !review.files.length) throw new Error("
|
|
373
|
+
if (publishing.has(key) || saving.get(key)) throw new Error(translate("connection.waitForWrite"));
|
|
374
|
+
if (review.repo !== ctx.repo || review.branch !== ctx.branch || review.publishBranch !== cfg.publishBranch || !review.files.length) throw new Error(translate("connection.reviewFirst"));
|
|
370
375
|
sha(review.head); sha(review.base);
|
|
371
376
|
publishing.add(key);
|
|
372
377
|
const request: typeof gh = (context, path, init) => { assertActive(); return gh(context, path, init); };
|
|
373
378
|
const unchanged = async () => {
|
|
374
379
|
const current = await publishHeads(ctx, cfg.publishBranch, request);
|
|
375
|
-
if (current.head !== review.head || current.base !== review.base) throw new
|
|
380
|
+
if (current.head !== review.head || current.base !== review.base) throw new PublishReviewChangedError(translate("connection.branchesChanged"));
|
|
376
381
|
};
|
|
377
382
|
const matches = (pr: any) => pr?.head?.repo?.full_name?.toLowerCase() === ctx.repo.toLowerCase() && pr?.base?.repo?.full_name?.toLowerCase() === ctx.repo.toLowerCase() && pr.head.ref === ctx.branch && pr.base.ref === cfg.publishBranch;
|
|
378
383
|
const validate = (pr: any) => {
|
|
379
|
-
if (!matches(pr) || !Number.isSafeInteger(pr.number) || pr.number <= 0 || pr.head.sha !== review.head) throw new
|
|
384
|
+
if (!matches(pr) || !Number.isSafeInteger(pr.number) || pr.number <= 0 || pr.head.sha !== review.head) throw new PublishReviewChangedError(translate("connection.pullRequestMismatch"));
|
|
380
385
|
return pr;
|
|
381
386
|
};
|
|
382
387
|
const merged = (pr: any) => {
|
|
@@ -389,12 +394,12 @@ export async function confirmRemotePublish(ctx: GitContext, manifest: Manifest,
|
|
|
389
394
|
for (let page = 1; page <= 100; page++) {
|
|
390
395
|
const query = new URLSearchParams({ state: "open", head: `${ctx.repo.split("/")[0]}:${ctx.branch}`, base: cfg.publishBranch, per_page: "100", page: String(page) });
|
|
391
396
|
const list = await request(ctx, `/pulls?${query}`);
|
|
392
|
-
if (!Array.isArray(list)) throw new Error("
|
|
397
|
+
if (!Array.isArray(list)) throw new Error(translate("connection.incompletePullRequests"));
|
|
393
398
|
found.push(...list.filter(matches));
|
|
394
|
-
if (found.length > 1) throw new Error("
|
|
399
|
+
if (found.length > 1) throw new Error(translate("connection.multiplePullRequests"));
|
|
395
400
|
if (list.length < 100) return found[0];
|
|
396
401
|
}
|
|
397
|
-
throw new Error("
|
|
402
|
+
throw new Error(translate("connection.pullRequestListingStopped"));
|
|
398
403
|
};
|
|
399
404
|
try {
|
|
400
405
|
if (review.prNumber) {
|
|
@@ -403,10 +408,10 @@ export async function confirmRemotePublish(ctx: GitContext, manifest: Manifest,
|
|
|
403
408
|
}
|
|
404
409
|
await unchanged();
|
|
405
410
|
const repository = await request(ctx, "");
|
|
406
|
-
if (repository?.permissions?.push !== true || repository.allow_squash_merge !== true || repository.delete_branch_on_merge !== false) throw new
|
|
411
|
+
if (repository?.permissions?.push !== true || repository.allow_squash_merge !== true || repository.delete_branch_on_merge !== false) throw new PublishReviewChangedError(translate("connection.publishSettingsChanged"));
|
|
407
412
|
let pr = review.prNumber ? await request(ctx, `/pulls/${review.prNumber}`) : await find();
|
|
408
413
|
if (!pr) {
|
|
409
|
-
if (review.creationUncertain) throw new Error("
|
|
414
|
+
if (review.creationUncertain) throw new Error(translate("connection.pullRequestCreationUnconfirmed"));
|
|
410
415
|
await unchanged();
|
|
411
416
|
review.creationUncertain = true;
|
|
412
417
|
try {
|
|
@@ -420,43 +425,43 @@ export async function confirmRemotePublish(ctx: GitContext, manifest: Manifest,
|
|
|
420
425
|
review.prNumber = pr.number;
|
|
421
426
|
pr = validate(await request(ctx, `/pulls/${pr.number}`));
|
|
422
427
|
if (merged(pr)) return "published";
|
|
423
|
-
if (pr.state !== "open" || pr.draft || pr.base.sha !== review.base) throw new
|
|
428
|
+
if (pr.state !== "open" || pr.draft || pr.base.sha !== review.base) throw new PublishReviewChangedError(translate("connection.pullRequestTargetChanged"));
|
|
424
429
|
if (review.needsSync) {
|
|
425
430
|
await unchanged();
|
|
426
431
|
await request(ctx, `/pulls/${pr.number}/update-branch`, { method: "PUT", body: JSON.stringify({ expected_head_sha: review.head }) });
|
|
427
432
|
const updated = await request(ctx, `/pulls/${pr.number}`);
|
|
428
|
-
if (!matches(updated) || updated.state !== "open" || sha(updated.head.sha) === review.head) throw new Error("
|
|
433
|
+
if (!matches(updated) || updated.state !== "open" || sha(updated.head.sha) === review.head) throw new Error(translate("connection.syncUnverified"));
|
|
429
434
|
return "synced";
|
|
430
435
|
}
|
|
431
|
-
if (pr.mergeable !== true || pr.mergeable_state !== "clean") throw new Error("
|
|
436
|
+
if (pr.mergeable !== true || pr.mergeable_state !== "clean") throw new Error(translate("connection.mergeBlocked"));
|
|
432
437
|
await unchanged();
|
|
433
438
|
let failure: unknown;
|
|
434
439
|
try {
|
|
435
440
|
const result = await request(ctx, `/pulls/${pr.number}/merge`, { method: "PUT", body: JSON.stringify({ merge_method: "squash", sha: review.head }) });
|
|
436
|
-
if (result?.merged !== true) failure = new Error("
|
|
441
|
+
if (result?.merged !== true) failure = new Error(translate("connection.mergeFailed"));
|
|
437
442
|
} catch (error) { failure = error; }
|
|
438
443
|
let verified;
|
|
439
444
|
try { verified = await request(ctx, `/pulls/${pr.number}`); }
|
|
440
445
|
catch (error) {
|
|
441
446
|
if (error instanceof GitError && error.status === 401) throw error;
|
|
442
|
-
throw new Error(
|
|
447
|
+
throw new Error(translate("connection.publishOutcomeUnconfirmed", { number: pr.number }));
|
|
443
448
|
}
|
|
444
449
|
if (merged(verified)) return "published";
|
|
445
450
|
if (failure) throw failure;
|
|
446
|
-
throw new Error("
|
|
451
|
+
throw new Error(translate("connection.publishUnverified"));
|
|
447
452
|
} catch (error) {
|
|
448
|
-
if (error instanceof GitError && [403, 404].includes(error.status)) throw new GitError("
|
|
449
|
-
if (error instanceof GitError && [405, 409, 422].includes(error.status)) throw new GitError("
|
|
453
|
+
if (error instanceof GitError && [403, 404].includes(error.status)) throw new GitError(translate("connection.publishPermissions"), error.status);
|
|
454
|
+
if (error instanceof GitError && [405, 409, 422].includes(error.status)) throw new GitError(translate("connection.publishRejected"), error.status);
|
|
450
455
|
throw error;
|
|
451
456
|
} finally { publishing.delete(key); }
|
|
452
457
|
}
|
|
453
458
|
|
|
454
459
|
export async function deleteRemotePost(ctx: GitContext, manifest: Manifest, collection: string, slug: string): Promise<void> {
|
|
455
460
|
const key = writeKey(ctx);
|
|
456
|
-
if (publishing.has(key)) throw new Error("
|
|
461
|
+
if (publishing.has(key)) throw new Error(translate("connection.waitBeforeDeleting"));
|
|
457
462
|
const cfg = config(ctx,manifest);
|
|
458
463
|
const folder = cfg.collections.find(c => c.name === collection)?.folder;
|
|
459
|
-
if (!folder || !manifest.collections?.some(c => c.name === collection)) throw new Error("
|
|
464
|
+
if (!folder || !manifest.collections?.some(c => c.name === collection)) throw new Error(translate("connection.unknownCollection"));
|
|
460
465
|
saving.set(key, (saving.get(key) ?? 0) + 1);
|
|
461
466
|
try { await mutate(ctx,manifest,filePath(ctx,manifest,folder,slug,"json",POST_SLUG),null,false); }
|
|
462
467
|
finally { saving.set(key, (saving.get(key) ?? 1) - 1); }
|