canopycms 0.11.1 → 0.12.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/CHANGELOG.md +174 -48
- package/dist/api/admin.d.ts +4 -4
- package/dist/api/branch-changes.d.ts +1 -0
- package/dist/api/branch-changes.js +1 -1
- package/dist/api/branch-entry-changes.d.ts +92 -7
- package/dist/api/branch-entry-changes.js +272 -38
- package/dist/api/branch-review.js +45 -13
- package/dist/api/branch-status.d.ts +12 -0
- package/dist/api/branch-status.js +13 -0
- package/dist/api/branch-update-from-base.d.ts +23 -0
- package/dist/api/branch-update-from-base.js +176 -0
- package/dist/api/branch.d.ts +5 -0
- package/dist/api/branch.js +1 -0
- package/dist/api/client.d.ts +17 -2
- package/dist/api/client.js +26 -0
- package/dist/api/comments.d.ts +2 -2
- package/dist/api/content.d.ts +38 -1
- package/dist/api/content.js +41 -5
- package/dist/api/merge-conflicts.d.ts +226 -0
- package/dist/api/merge-conflicts.js +674 -0
- package/dist/api/routes.js +2 -0
- package/dist/api/types.d.ts +7 -3
- package/dist/base-update.d.ts +26 -0
- package/dist/base-update.js +31 -0
- package/dist/branch-metadata-file.d.ts +51 -0
- package/dist/branch-metadata-file.js +11 -0
- package/dist/branch-schema-cache.d.ts +12 -0
- package/dist/branch-schema-cache.js +27 -2
- package/dist/cli/backfill-item-keys.d.ts +77 -0
- package/dist/cli/backfill-item-keys.js +320 -0
- package/dist/cli/cli.d.ts +1 -1
- package/dist/cli/cli.js +9713 -1493
- package/dist/cli/generate-ai-content.js +415 -244
- package/dist/cli/init.js +6 -0
- package/dist/cli/site-config.d.ts +14 -2
- package/dist/cli/site-config.js +20 -4
- package/dist/comment-store.d.ts +8 -0
- package/dist/comment-store.js +28 -0
- package/dist/content-index-generation.d.ts +1 -4
- package/dist/content-index-generation.js +1 -4
- package/dist/content-store.d.ts +65 -3
- package/dist/content-store.js +121 -15
- package/dist/editor/BranchManager.d.ts +4 -1
- package/dist/editor/BranchManager.js +9 -5
- package/dist/editor/Editor.js +549 -53
- package/dist/editor/EntryNavigator.d.ts +10 -2
- package/dist/editor/EntryNavigator.js +12 -4
- package/dist/editor/FormRenderer.d.ts +8 -2
- package/dist/editor/FormRenderer.js +5 -6
- package/dist/editor/base-update-wait.d.ts +74 -0
- package/dist/editor/base-update-wait.js +91 -0
- package/dist/editor/branch-status.js +1 -1
- package/dist/editor/components/BaseUpdateNotice.d.ts +12 -0
- package/dist/editor/components/BaseUpdateNotice.js +24 -0
- package/dist/editor/components/BranchPill.d.ts +4 -1
- package/dist/editor/components/BranchPill.js +5 -2
- package/dist/editor/components/ConflictsGroup.d.ts +100 -0
- package/dist/editor/components/ConflictsGroup.js +364 -0
- package/dist/editor/components/EditorHeader.d.ts +16 -1
- package/dist/editor/components/EditorHeader.js +92 -30
- package/dist/editor/components/EntryChangesDrawer.d.ts +52 -2
- package/dist/editor/components/EntryChangesDrawer.js +164 -162
- package/dist/editor/components/EntryHeader.d.ts +3 -1
- package/dist/editor/components/EntryHeader.js +4 -6
- package/dist/editor/components/GetLatestChangesButton.d.ts +15 -0
- package/dist/editor/components/GetLatestChangesButton.js +29 -0
- package/dist/editor/components/ReleaseHoldButton.d.ts +20 -0
- package/dist/editor/components/ReleaseHoldButton.js +36 -0
- package/dist/editor/components/RetryToastMessage.js +1 -1
- package/dist/editor/components/change-parts.d.ts +51 -0
- package/dist/editor/components/change-parts.js +171 -0
- package/dist/editor/conflict-cards.d.ts +90 -0
- package/dist/editor/conflict-cards.js +279 -0
- package/dist/editor/conflict-choices.d.ts +60 -0
- package/dist/editor/conflict-choices.js +108 -0
- package/dist/editor/conflict-notice.d.ts +25 -0
- package/dist/editor/conflict-notice.js +39 -0
- package/dist/editor/copy.d.ts +20 -0
- package/dist/editor/copy.js +20 -0
- package/dist/editor/entry-diff.d.ts +18 -2
- package/dist/editor/entry-diff.js +26 -32
- package/dist/editor/hooks/useBaseUpdate.d.ts +36 -0
- package/dist/editor/hooks/useBaseUpdate.js +269 -0
- package/dist/editor/hooks/useBranchManager.d.ts +6 -1
- package/dist/editor/hooks/useBranchManager.js +28 -7
- package/dist/editor/hooks/useBranchesData.js +3 -0
- package/dist/editor/hooks/useConflictChoices.d.ts +24 -0
- package/dist/editor/hooks/useConflictChoices.js +75 -0
- package/dist/editor/hooks/useConflictsData.d.ts +26 -0
- package/dist/editor/hooks/useConflictsData.js +89 -0
- package/dist/editor/hooks/useDraftManager.d.ts +5 -0
- package/dist/editor/hooks/useDraftManager.js +24 -8
- package/dist/editor/hooks/useEntryManager.d.ts +9 -0
- package/dist/editor/hooks/useEntryManager.js +5 -0
- package/dist/editor/release-hold.d.ts +12 -0
- package/dist/editor/release-hold.js +39 -0
- package/dist/editor/server-clock.d.ts +4 -0
- package/dist/editor/server-clock.js +15 -0
- package/dist/editor/undo-change.d.ts +52 -0
- package/dist/editor/undo-change.js +241 -0
- package/dist/git-manager.d.ts +5 -0
- package/dist/git-manager.js +30 -0
- package/dist/item-keys.d.ts +13 -0
- package/dist/item-keys.js +36 -14
- package/dist/merge/conflict-path.d.ts +40 -0
- package/dist/merge/conflict-path.js +128 -0
- package/dist/merge/diff3.d.ts +27 -0
- package/dist/merge/diff3.js +113 -0
- package/dist/merge/entry-merge.d.ts +94 -0
- package/dist/merge/entry-merge.js +875 -0
- package/dist/merge/list-alignment.d.ts +52 -0
- package/dist/merge/list-alignment.js +441 -0
- package/dist/merge/text-merge.d.ts +29 -0
- package/dist/merge/text-merge.js +59 -0
- package/dist/merge/value-equality.d.ts +18 -0
- package/dist/merge/value-equality.js +50 -0
- package/dist/merge-conflicts.d.ts +232 -0
- package/dist/merge-conflicts.js +451 -0
- package/dist/operating-mode/client-safe-strategy.d.ts +2 -0
- package/dist/operating-mode/client-safe-strategy.js +6 -0
- package/dist/operating-mode/types.d.ts +5 -0
- package/dist/services.d.ts +16 -1
- package/dist/services.js +82 -22
- package/dist/task-queue/cms-task-queue.d.ts +3 -1
- package/dist/types.d.ts +28 -2
- package/dist/utils/content-serialize.d.ts +18 -1
- package/dist/utils/content-serialize.js +12 -0
- package/dist/utils/content-write-lock.d.ts +3 -3
- package/dist/utils/content-write-lock.js +3 -3
- package/dist/utils/markdown-body-splice.d.ts +1 -1
- package/dist/utils/markdown-body-splice.js +1 -19
- package/dist/utils/markdown-parse.d.ts +9 -0
- package/dist/utils/markdown-parse.js +27 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/branch-merge.d.ts +53 -37
- package/dist/worker/branch-merge.js +139 -54
- package/dist/worker/cms-worker.d.ts +16 -3
- package/dist/worker/cms-worker.js +59 -12
- package/dist/worker/field-merge.d.ts +65 -0
- package/dist/worker/field-merge.js +636 -0
- package/dist/worker/git-sync.d.ts +1 -1
- package/dist/worker/git-sync.js +7 -6
- package/dist/worker/github-commits.d.ts +42 -10
- package/dist/worker/github-commits.js +287 -47
- package/dist/worker/github-gateway-http.js +17 -9
- package/dist/worker/github-gateway-wire.d.ts +21 -12
- package/dist/worker/github-gateway-wire.js +5 -1
- package/dist/worker/github-gateway.d.ts +10 -1
- package/dist/worker/github-gateway.js +18 -6
- package/dist/worker/github-transfer.d.ts +5 -3
- package/dist/worker/github-transfer.js +8 -5
- package/dist/worker/rebase.d.ts +5 -4
- package/dist/worker/rebase.js +293 -156
- package/dist/worker/task-runner.d.ts +1 -1
- package/dist/worker/task-runner.js +14 -2
- package/dist/worker/worker-context.d.ts +18 -2
- package/package.json +1 -1
|
@@ -186,6 +186,21 @@ function withItemKeys(fields, data, fill) {
|
|
|
186
186
|
return out;
|
|
187
187
|
}
|
|
188
188
|
function keyList(field, items, fill) {
|
|
189
|
+
const keys = listKeys(items, fill);
|
|
190
|
+
let changed = false;
|
|
191
|
+
const out = items.map((item, i) => {
|
|
192
|
+
if (!isPlainRecord(item))
|
|
193
|
+
return item;
|
|
194
|
+
let next = keyInside(field, item, fill);
|
|
195
|
+
if (keys[i] !== item[ITEM_KEY])
|
|
196
|
+
next = { ...next, [ITEM_KEY]: keys[i] };
|
|
197
|
+
if (next !== item)
|
|
198
|
+
changed = true;
|
|
199
|
+
return next;
|
|
200
|
+
});
|
|
201
|
+
return changed ? out : items;
|
|
202
|
+
}
|
|
203
|
+
function listKeys(items, fill) {
|
|
189
204
|
const held = /* @__PURE__ */ new Set();
|
|
190
205
|
if (fill !== "fresh") {
|
|
191
206
|
for (const item of items) {
|
|
@@ -194,25 +209,19 @@ function keyList(field, items, fill) {
|
|
|
194
209
|
}
|
|
195
210
|
}
|
|
196
211
|
const kept = /* @__PURE__ */ new Set();
|
|
197
|
-
|
|
198
|
-
const out = items.map((item) => {
|
|
212
|
+
return items.map((item) => {
|
|
199
213
|
if (!isPlainRecord(item))
|
|
200
|
-
return
|
|
201
|
-
let next = keyInside(field, item, fill);
|
|
214
|
+
return void 0;
|
|
202
215
|
const own = item[ITEM_KEY];
|
|
203
216
|
if (fill !== "fresh" && isItemKey(own) && !kept.has(own)) {
|
|
204
217
|
kept.add(own);
|
|
205
|
-
|
|
206
|
-
const key = fill === "content" ? unheld(contentKey(item), held) : mintItemKey();
|
|
207
|
-
held.add(key);
|
|
208
|
-
kept.add(key);
|
|
209
|
-
next = { ...next, [ITEM_KEY]: key };
|
|
218
|
+
return own;
|
|
210
219
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
220
|
+
const key = fill === "content" ? unheld(contentKey(item), held) : mintItemKey();
|
|
221
|
+
held.add(key);
|
|
222
|
+
kept.add(key);
|
|
223
|
+
return key;
|
|
214
224
|
});
|
|
215
|
-
return changed ? out : items;
|
|
216
225
|
}
|
|
217
226
|
function unheld(key, held) {
|
|
218
227
|
let out = key;
|
|
@@ -314,11 +323,11 @@ var init_sanitize_href = __esm({
|
|
|
314
323
|
});
|
|
315
324
|
|
|
316
325
|
// dist/paths/normalize.js
|
|
317
|
-
function normalizeFilesystemPath(
|
|
318
|
-
return
|
|
326
|
+
function normalizeFilesystemPath(path28) {
|
|
327
|
+
return path28.split(/[\\/]+/).filter(Boolean).join("/");
|
|
319
328
|
}
|
|
320
|
-
function hasTraversalSequence(
|
|
321
|
-
const normalized = normalizeFilesystemPath(
|
|
329
|
+
function hasTraversalSequence(path28) {
|
|
330
|
+
const normalized = normalizeFilesystemPath(path28);
|
|
322
331
|
return normalized.includes("..");
|
|
323
332
|
}
|
|
324
333
|
function createLogicalPath(...segments) {
|
|
@@ -331,14 +340,14 @@ function createLogicalPath(...segments) {
|
|
|
331
340
|
function entryLogicalPath(collectionLogicalPath, slug) {
|
|
332
341
|
return collectionLogicalPath ? `${collectionLogicalPath}/${slug}` : slug;
|
|
333
342
|
}
|
|
334
|
-
function trimSlashes(
|
|
343
|
+
function trimSlashes(path28) {
|
|
335
344
|
let start = 0;
|
|
336
|
-
let end =
|
|
337
|
-
while (start < end &&
|
|
345
|
+
let end = path28.length;
|
|
346
|
+
while (start < end && path28[start] === "/")
|
|
338
347
|
start++;
|
|
339
|
-
while (end > start &&
|
|
348
|
+
while (end > start && path28[end - 1] === "/")
|
|
340
349
|
end--;
|
|
341
|
-
return
|
|
350
|
+
return path28.slice(start, end);
|
|
342
351
|
}
|
|
343
352
|
var init_normalize = __esm({
|
|
344
353
|
"dist/paths/normalize.js"() {
|
|
@@ -1175,6 +1184,9 @@ var init_client_safe_strategy = __esm({
|
|
|
1175
1184
|
supportsPullRequests() {
|
|
1176
1185
|
return true;
|
|
1177
1186
|
}
|
|
1187
|
+
syncsBranchesWithBase() {
|
|
1188
|
+
return true;
|
|
1189
|
+
}
|
|
1178
1190
|
getPermissionsFileName() {
|
|
1179
1191
|
return "permissions.json";
|
|
1180
1192
|
}
|
|
@@ -1201,6 +1213,9 @@ var init_client_safe_strategy = __esm({
|
|
|
1201
1213
|
supportsPullRequests() {
|
|
1202
1214
|
return false;
|
|
1203
1215
|
}
|
|
1216
|
+
syncsBranchesWithBase() {
|
|
1217
|
+
return false;
|
|
1218
|
+
}
|
|
1204
1219
|
getPermissionsFileName() {
|
|
1205
1220
|
return "permissions.json";
|
|
1206
1221
|
}
|
|
@@ -1610,8 +1625,7 @@ var init_worker_status = __esm({
|
|
|
1610
1625
|
});
|
|
1611
1626
|
|
|
1612
1627
|
// dist/cli/generate-ai-content.js
|
|
1613
|
-
import
|
|
1614
|
-
import { createJiti } from "jiti";
|
|
1628
|
+
import path27 from "node:path";
|
|
1615
1629
|
|
|
1616
1630
|
// dist/build/generate-ai-content.js
|
|
1617
1631
|
init_error();
|
|
@@ -1626,7 +1640,7 @@ import matter2 from "gray-matter";
|
|
|
1626
1640
|
import { parse as yamlParse } from "yaml";
|
|
1627
1641
|
|
|
1628
1642
|
// dist/utils/content-serialize.js
|
|
1629
|
-
import { isDeepStrictEqual as
|
|
1643
|
+
import { isDeepStrictEqual as isDeepStrictEqual3 } from "node:util";
|
|
1630
1644
|
import matter from "gray-matter";
|
|
1631
1645
|
import { isCollection as isCollection2, isMap as isMap2, isNode as isNode2, isScalar as isScalar2, isSeq as isSeq2, parseDocument as parseDocument2, Scalar, stringify as yamlStringify, visit as visit2 } from "yaml";
|
|
1632
1646
|
|
|
@@ -1805,13 +1819,112 @@ var testLogger = createDebugLogger({
|
|
|
1805
1819
|
// dist/utils/content-serialize.js
|
|
1806
1820
|
init_error();
|
|
1807
1821
|
|
|
1822
|
+
// dist/utils/json-source-splice.js
|
|
1823
|
+
import { isDeepStrictEqual } from "node:util";
|
|
1824
|
+
import { applyEdits, modify } from "jsonc-parser";
|
|
1825
|
+
init_error();
|
|
1826
|
+
var log = createDebugLogger({ prefix: "JsonSourceSplice" });
|
|
1827
|
+
function isRecord(value) {
|
|
1828
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1829
|
+
}
|
|
1830
|
+
function plainStringify(data) {
|
|
1831
|
+
return `${JSON.stringify(data, null, 2)}
|
|
1832
|
+
`;
|
|
1833
|
+
}
|
|
1834
|
+
function detectFormatting(text) {
|
|
1835
|
+
const indent = /^([ \t]+)\S/m.exec(text)?.[1];
|
|
1836
|
+
const eol = text.includes("\r\n") ? "\r\n" : "\n";
|
|
1837
|
+
if (indent?.startsWith(" "))
|
|
1838
|
+
return { insertSpaces: false, tabSize: 1, eol };
|
|
1839
|
+
return { insertSpaces: true, tabSize: indent?.length ?? 2, eol };
|
|
1840
|
+
}
|
|
1841
|
+
function insertMember(text, path28, key, value, formattingOptions) {
|
|
1842
|
+
const at = [...path28, key];
|
|
1843
|
+
const [edit] = modify(text, at, value, {});
|
|
1844
|
+
const closing = edit ? /^[ \t]*\r?\n([ \t]*)\}/.exec(text.slice(edit.offset)) : null;
|
|
1845
|
+
if (edit && closing && edit.length === 0 && edit.content.startsWith(",")) {
|
|
1846
|
+
const unit = formattingOptions.insertSpaces ? " ".repeat(formattingOptions.tabSize ?? 2) : " ";
|
|
1847
|
+
const eol = formattingOptions.eol ?? "\n";
|
|
1848
|
+
const indent = closing[1] + unit;
|
|
1849
|
+
const rendered = JSON.stringify(value, null, unit).split("\n").join(eol + indent);
|
|
1850
|
+
return applyEdits(text, [
|
|
1851
|
+
{ ...edit, content: `,${eol}${indent}${JSON.stringify(key)}: ${rendered}` }
|
|
1852
|
+
]);
|
|
1853
|
+
}
|
|
1854
|
+
return applyEdits(text, modify(text, at, value, { formattingOptions }));
|
|
1855
|
+
}
|
|
1856
|
+
function removeMember(text, path28) {
|
|
1857
|
+
return applyEdits(text, modify(text, path28, void 0, {}).map((edit) => {
|
|
1858
|
+
const removed = text.slice(edit.offset, edit.offset + edit.length);
|
|
1859
|
+
const lead = /^\s+/.exec(removed)?.[0].length ?? 0;
|
|
1860
|
+
const atMember = /\S/.test(text.charAt(edit.offset + edit.length));
|
|
1861
|
+
return edit.content === "" && lead > 0 && atMember ? { ...edit, offset: edit.offset + lead, length: edit.length - lead } : edit;
|
|
1862
|
+
}));
|
|
1863
|
+
}
|
|
1864
|
+
function reconcile(text, path28, before, after, formattingOptions) {
|
|
1865
|
+
if (isRecord(before) && isRecord(after)) {
|
|
1866
|
+
if (Object.keys(after).length === 0 && Object.keys(before).length > 0) {
|
|
1867
|
+
return applyEdits(text, modify(text, path28, {}, { formattingOptions }));
|
|
1868
|
+
}
|
|
1869
|
+
let next = text;
|
|
1870
|
+
for (const key of Object.keys(before)) {
|
|
1871
|
+
if (!Object.prototype.hasOwnProperty.call(after, key))
|
|
1872
|
+
next = removeMember(next, [...path28, key]);
|
|
1873
|
+
}
|
|
1874
|
+
for (const key of Object.keys(after)) {
|
|
1875
|
+
next = Object.prototype.hasOwnProperty.call(before, key) ? reconcile(next, [...path28, key], before[key], after[key], formattingOptions) : insertMember(next, path28, key, after[key], formattingOptions);
|
|
1876
|
+
}
|
|
1877
|
+
return next;
|
|
1878
|
+
}
|
|
1879
|
+
if (Array.isArray(before) && Array.isArray(after) && before.length === after.length) {
|
|
1880
|
+
return after.reduce((next, item, index) => reconcile(next, [...path28, index], before[index], item, formattingOptions), text);
|
|
1881
|
+
}
|
|
1882
|
+
if (JSON.stringify(before) === JSON.stringify(after))
|
|
1883
|
+
return text;
|
|
1884
|
+
return applyEdits(text, modify(text, path28, after, { formattingOptions }));
|
|
1885
|
+
}
|
|
1886
|
+
function serializeJson(data, existingRaw) {
|
|
1887
|
+
if (existingRaw === void 0)
|
|
1888
|
+
return plainStringify(data);
|
|
1889
|
+
try {
|
|
1890
|
+
const normalised = JSON.parse(JSON.stringify(data));
|
|
1891
|
+
const existing = JSON.parse(existingRaw);
|
|
1892
|
+
if (!isRecord(normalised) || !isRecord(existing))
|
|
1893
|
+
return plainStringify(data);
|
|
1894
|
+
const spliced = reconcile(existingRaw, [], existing, normalised, detectFormatting(existingRaw));
|
|
1895
|
+
if (isDeepStrictEqual(JSON.parse(spliced), normalised))
|
|
1896
|
+
return spliced;
|
|
1897
|
+
log.debug("json-source-splice", "splice does not read back as data; writing JSON.stringify");
|
|
1898
|
+
} catch (err) {
|
|
1899
|
+
log.debug("json-source-splice", "splice failed; writing JSON.stringify", {
|
|
1900
|
+
error: getErrorMessage(err)
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
return plainStringify(data);
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1808
1906
|
// dist/utils/markdown-body-splice.js
|
|
1907
|
+
init_error();
|
|
1908
|
+
|
|
1909
|
+
// dist/utils/markdown-parse.js
|
|
1809
1910
|
import { fromMarkdown } from "mdast-util-from-markdown";
|
|
1810
1911
|
import { gfmFromMarkdown } from "mdast-util-gfm";
|
|
1811
1912
|
import { mdxFromMarkdown } from "mdast-util-mdx";
|
|
1812
1913
|
import { gfm } from "micromark-extension-gfm";
|
|
1813
1914
|
import { mdxjs } from "micromark-extension-mdxjs";
|
|
1814
1915
|
init_error();
|
|
1916
|
+
var log2 = createDebugLogger({ prefix: "MarkdownParse" });
|
|
1917
|
+
function parseMarkdownBody(text, format) {
|
|
1918
|
+
try {
|
|
1919
|
+
return format === "mdx" ? fromMarkdown(text, {
|
|
1920
|
+
extensions: [gfm(), mdxjs()],
|
|
1921
|
+
mdastExtensions: [gfmFromMarkdown(), mdxFromMarkdown()]
|
|
1922
|
+
}) : fromMarkdown(text, { extensions: [gfm()], mdastExtensions: [gfmFromMarkdown()] });
|
|
1923
|
+
} catch (err) {
|
|
1924
|
+
log2.debug("markdown-parse", "body does not parse", { error: getErrorMessage(err) });
|
|
1925
|
+
return void 0;
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1815
1928
|
|
|
1816
1929
|
// dist/utils/markdown-export-options.js
|
|
1817
1930
|
var MARKDOWN_EXPORT_OPTIONS = {
|
|
@@ -2236,7 +2349,7 @@ function restyleMarkdown(source, tree, fixed, readsBack) {
|
|
|
2236
2349
|
}
|
|
2237
2350
|
|
|
2238
2351
|
// dist/utils/yaml-source-splice.js
|
|
2239
|
-
import { isDeepStrictEqual } from "node:util";
|
|
2352
|
+
import { isDeepStrictEqual as isDeepStrictEqual2 } from "node:util";
|
|
2240
2353
|
import { Document, parseDocument, isAlias, isCollection, isMap, isNode, isPair, isScalar, isSeq, YAMLMap, visit, YAMLSeq } from "yaml";
|
|
2241
2354
|
var END = Symbol("end");
|
|
2242
2355
|
var FRESH = Symbol("fresh");
|
|
@@ -2639,7 +2752,7 @@ function printsAs(candidate, reconciled, printed) {
|
|
|
2639
2752
|
const reparsed = parseDocument(candidate);
|
|
2640
2753
|
if (reparsed.errors.length > 0)
|
|
2641
2754
|
return false;
|
|
2642
|
-
if (!
|
|
2755
|
+
if (!isDeepStrictEqual2(reparsed.toJS(), reconciled.toJS()))
|
|
2643
2756
|
return false;
|
|
2644
2757
|
return layoutFree(reparsed.toString()) === layoutFree(parseDocument(printed).toString());
|
|
2645
2758
|
}
|
|
@@ -2648,19 +2761,8 @@ function layoutFree(text) {
|
|
|
2648
2761
|
}
|
|
2649
2762
|
|
|
2650
2763
|
// dist/utils/markdown-body-splice.js
|
|
2651
|
-
var
|
|
2764
|
+
var log3 = createDebugLogger({ prefix: "MarkdownBodySplice" });
|
|
2652
2765
|
var MAX_LCS_CELLS = 4e6;
|
|
2653
|
-
function parse(text, format) {
|
|
2654
|
-
try {
|
|
2655
|
-
return format === "mdx" ? fromMarkdown(text, {
|
|
2656
|
-
extensions: [gfm(), mdxjs()],
|
|
2657
|
-
mdastExtensions: [gfmFromMarkdown(), mdxFromMarkdown()]
|
|
2658
|
-
}) : fromMarkdown(text, { extensions: [gfm()], mdastExtensions: [gfmFromMarkdown()] });
|
|
2659
|
-
} catch (err) {
|
|
2660
|
-
log.debug("markdown-body-splice", "body does not parse", { error: getErrorMessage(err) });
|
|
2661
|
-
return void 0;
|
|
2662
|
-
}
|
|
2663
|
-
}
|
|
2664
2766
|
function canonical(node) {
|
|
2665
2767
|
return JSON.stringify(node, (key, value) => {
|
|
2666
2768
|
if (key === "position" || key === "data")
|
|
@@ -2941,7 +3043,7 @@ function preserveMarkdownSource(original, updated, format) {
|
|
|
2941
3043
|
try {
|
|
2942
3044
|
return splice(original, updated, format) ?? updated;
|
|
2943
3045
|
} catch (err) {
|
|
2944
|
-
|
|
3046
|
+
log3.debug("markdown-body-splice", "splice failed", { error: getErrorMessage(err) });
|
|
2945
3047
|
return updated;
|
|
2946
3048
|
}
|
|
2947
3049
|
}
|
|
@@ -2966,14 +3068,14 @@ function diskBlocks(oNodes, nNodes) {
|
|
|
2966
3068
|
return fromDisk;
|
|
2967
3069
|
}
|
|
2968
3070
|
function splice(original, updated, format) {
|
|
2969
|
-
const before = original === void 0 ? void 0 :
|
|
2970
|
-
const after =
|
|
3071
|
+
const before = original === void 0 ? void 0 : parseMarkdownBody(original, format);
|
|
3072
|
+
const after = parseMarkdownBody(updated, format);
|
|
2971
3073
|
if (after === void 0)
|
|
2972
3074
|
return void 0;
|
|
2973
3075
|
const target = canonical(after);
|
|
2974
3076
|
const fromDisk = diskBlocks(before?.children ?? [], after.children);
|
|
2975
3077
|
const restyled = restyleMarkdown(updated, after, (index) => fromDisk.has(index), (text) => {
|
|
2976
|
-
const tree =
|
|
3078
|
+
const tree = parseMarkdownBody(text, format);
|
|
2977
3079
|
return tree !== void 0 && canonical(tree) === target ? tree : void 0;
|
|
2978
3080
|
});
|
|
2979
3081
|
if (original === void 0 || before === void 0)
|
|
@@ -2985,10 +3087,10 @@ function splice(original, updated, format) {
|
|
|
2985
3087
|
const candidate = new Splicer(original, edit.text, deep).body(before, edit.tree);
|
|
2986
3088
|
if (candidate === void 0)
|
|
2987
3089
|
continue;
|
|
2988
|
-
const reparsed =
|
|
3090
|
+
const reparsed = parseMarkdownBody(candidate, format);
|
|
2989
3091
|
if (reparsed !== void 0 && canonical(reparsed) === target)
|
|
2990
3092
|
return candidate;
|
|
2991
|
-
|
|
3093
|
+
log3.debug("markdown-body-splice", "splice does not read back as the edit", {
|
|
2992
3094
|
deep,
|
|
2993
3095
|
restyled: edit !== asSent
|
|
2994
3096
|
});
|
|
@@ -2998,7 +3100,7 @@ function splice(original, updated, format) {
|
|
|
2998
3100
|
}
|
|
2999
3101
|
|
|
3000
3102
|
// dist/utils/content-serialize.js
|
|
3001
|
-
var
|
|
3103
|
+
var log4 = createDebugLogger({ prefix: "ContentSerialize" });
|
|
3002
3104
|
function createNode(ctx, value, asKey = false) {
|
|
3003
3105
|
const node = ctx.doc.createNode(value);
|
|
3004
3106
|
const { quote } = ctx;
|
|
@@ -3242,9 +3344,9 @@ function reconcileYamlSource(raw, data, { rootAtColumnZero = false, quote } = {}
|
|
|
3242
3344
|
if (spliced !== void 0 && !(rootAtColumnZero && /^\s*[ \t]\S/.test(spliced))) {
|
|
3243
3345
|
return spliced;
|
|
3244
3346
|
}
|
|
3245
|
-
|
|
3347
|
+
log4.debug("content-serialize", "source splice not used; writing as yaml prints it");
|
|
3246
3348
|
} catch (err) {
|
|
3247
|
-
|
|
3349
|
+
log4.debug("content-serialize", "source splice failed; writing as yaml prints it", {
|
|
3248
3350
|
error: getErrorMessage(err)
|
|
3249
3351
|
});
|
|
3250
3352
|
}
|
|
@@ -3252,7 +3354,7 @@ function reconcileYamlSource(raw, data, { rootAtColumnZero = false, quote } = {}
|
|
|
3252
3354
|
}
|
|
3253
3355
|
function readsBackAs(text, doc) {
|
|
3254
3356
|
const reparsed = parseDocument2(text);
|
|
3255
|
-
return reparsed.errors.length === 0 &&
|
|
3357
|
+
return reparsed.errors.length === 0 && isDeepStrictEqual3(reparsed.toJS(), doc.toJS());
|
|
3256
3358
|
}
|
|
3257
3359
|
function serializeYaml(data, existingRaw) {
|
|
3258
3360
|
if (existingRaw === void 0)
|
|
@@ -3389,94 +3491,17 @@ function serializeFrontmatter(editorBody, data, existingRaw, format, fields = []
|
|
|
3389
3491
|
}
|
|
3390
3492
|
}
|
|
3391
3493
|
});
|
|
3392
|
-
if (
|
|
3494
|
+
if (isDeepStrictEqual3(frontmatterAsRead(written, fields), asJson(data)))
|
|
3393
3495
|
return written;
|
|
3394
|
-
|
|
3496
|
+
log4.debug("content-serialize", "reconciled frontmatter reads back differently; writing as gray-matter prints it");
|
|
3395
3497
|
return matter.stringify(file, data);
|
|
3396
3498
|
}
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
function isRecord(value) {
|
|
3404
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3405
|
-
}
|
|
3406
|
-
function plainStringify(data) {
|
|
3407
|
-
return `${JSON.stringify(data, null, 2)}
|
|
3408
|
-
`;
|
|
3409
|
-
}
|
|
3410
|
-
function detectFormatting(text) {
|
|
3411
|
-
const indent = /^([ \t]+)\S/m.exec(text)?.[1];
|
|
3412
|
-
const eol = text.includes("\r\n") ? "\r\n" : "\n";
|
|
3413
|
-
if (indent?.startsWith(" "))
|
|
3414
|
-
return { insertSpaces: false, tabSize: 1, eol };
|
|
3415
|
-
return { insertSpaces: true, tabSize: indent?.length ?? 2, eol };
|
|
3416
|
-
}
|
|
3417
|
-
function insertMember(text, path27, key, value, formattingOptions) {
|
|
3418
|
-
const at = [...path27, key];
|
|
3419
|
-
const [edit] = modify(text, at, value, {});
|
|
3420
|
-
const closing = edit ? /^[ \t]*\r?\n([ \t]*)\}/.exec(text.slice(edit.offset)) : null;
|
|
3421
|
-
if (edit && closing && edit.length === 0 && edit.content.startsWith(",")) {
|
|
3422
|
-
const unit = formattingOptions.insertSpaces ? " ".repeat(formattingOptions.tabSize ?? 2) : " ";
|
|
3423
|
-
const eol = formattingOptions.eol ?? "\n";
|
|
3424
|
-
const indent = closing[1] + unit;
|
|
3425
|
-
const rendered = JSON.stringify(value, null, unit).split("\n").join(eol + indent);
|
|
3426
|
-
return applyEdits(text, [
|
|
3427
|
-
{ ...edit, content: `,${eol}${indent}${JSON.stringify(key)}: ${rendered}` }
|
|
3428
|
-
]);
|
|
3429
|
-
}
|
|
3430
|
-
return applyEdits(text, modify(text, at, value, { formattingOptions }));
|
|
3431
|
-
}
|
|
3432
|
-
function removeMember(text, path27) {
|
|
3433
|
-
return applyEdits(text, modify(text, path27, void 0, {}).map((edit) => {
|
|
3434
|
-
const removed = text.slice(edit.offset, edit.offset + edit.length);
|
|
3435
|
-
const lead = /^\s+/.exec(removed)?.[0].length ?? 0;
|
|
3436
|
-
const atMember = /\S/.test(text.charAt(edit.offset + edit.length));
|
|
3437
|
-
return edit.content === "" && lead > 0 && atMember ? { ...edit, offset: edit.offset + lead, length: edit.length - lead } : edit;
|
|
3438
|
-
}));
|
|
3439
|
-
}
|
|
3440
|
-
function reconcile(text, path27, before, after, formattingOptions) {
|
|
3441
|
-
if (isRecord(before) && isRecord(after)) {
|
|
3442
|
-
if (Object.keys(after).length === 0 && Object.keys(before).length > 0) {
|
|
3443
|
-
return applyEdits(text, modify(text, path27, {}, { formattingOptions }));
|
|
3444
|
-
}
|
|
3445
|
-
let next = text;
|
|
3446
|
-
for (const key of Object.keys(before)) {
|
|
3447
|
-
if (!Object.prototype.hasOwnProperty.call(after, key))
|
|
3448
|
-
next = removeMember(next, [...path27, key]);
|
|
3449
|
-
}
|
|
3450
|
-
for (const key of Object.keys(after)) {
|
|
3451
|
-
next = Object.prototype.hasOwnProperty.call(before, key) ? reconcile(next, [...path27, key], before[key], after[key], formattingOptions) : insertMember(next, path27, key, after[key], formattingOptions);
|
|
3452
|
-
}
|
|
3453
|
-
return next;
|
|
3454
|
-
}
|
|
3455
|
-
if (Array.isArray(before) && Array.isArray(after) && before.length === after.length) {
|
|
3456
|
-
return after.reduce((next, item, index) => reconcile(next, [...path27, index], before[index], item, formattingOptions), text);
|
|
3457
|
-
}
|
|
3458
|
-
if (JSON.stringify(before) === JSON.stringify(after))
|
|
3459
|
-
return text;
|
|
3460
|
-
return applyEdits(text, modify(text, path27, after, { formattingOptions }));
|
|
3461
|
-
}
|
|
3462
|
-
function serializeJson(data, existingRaw) {
|
|
3463
|
-
if (existingRaw === void 0)
|
|
3464
|
-
return plainStringify(data);
|
|
3465
|
-
try {
|
|
3466
|
-
const normalised = JSON.parse(JSON.stringify(data));
|
|
3467
|
-
const existing = JSON.parse(existingRaw);
|
|
3468
|
-
if (!isRecord(normalised) || !isRecord(existing))
|
|
3469
|
-
return plainStringify(data);
|
|
3470
|
-
const spliced = reconcile(existingRaw, [], existing, normalised, detectFormatting(existingRaw));
|
|
3471
|
-
if (isDeepStrictEqual3(JSON.parse(spliced), normalised))
|
|
3472
|
-
return spliced;
|
|
3473
|
-
log3.debug("json-source-splice", "splice does not read back as data; writing JSON.stringify");
|
|
3474
|
-
} catch (err) {
|
|
3475
|
-
log3.debug("json-source-splice", "splice failed; writing JSON.stringify", {
|
|
3476
|
-
error: getErrorMessage(err)
|
|
3477
|
-
});
|
|
3478
|
-
}
|
|
3479
|
-
return plainStringify(data);
|
|
3499
|
+
function serializeEntry(entry, existingRaw, fields) {
|
|
3500
|
+
if (entry.format === "json")
|
|
3501
|
+
return serializeJson(entry.data, existingRaw);
|
|
3502
|
+
if (entry.format === "yaml")
|
|
3503
|
+
return serializeYaml(entry.data, existingRaw);
|
|
3504
|
+
return serializeFrontmatter(entry.body, entry.data ?? {}, existingRaw, entry.format, fields);
|
|
3480
3505
|
}
|
|
3481
3506
|
|
|
3482
3507
|
// dist/utils/async-mutex.js
|
|
@@ -3922,8 +3947,8 @@ function computeEntryUrl(collection, slug, contentRoot) {
|
|
|
3922
3947
|
if (slug && !isIndexSlug(slug)) {
|
|
3923
3948
|
segments.push(slug);
|
|
3924
3949
|
}
|
|
3925
|
-
const
|
|
3926
|
-
return
|
|
3950
|
+
const path28 = segments.length > 0 ? `/${segments.join("/")}` : "/";
|
|
3951
|
+
return path28.toLowerCase();
|
|
3927
3952
|
}
|
|
3928
3953
|
|
|
3929
3954
|
// dist/url-collision.js
|
|
@@ -4286,7 +4311,7 @@ function extractIdFromFilename(filename) {
|
|
|
4286
4311
|
}
|
|
4287
4312
|
async function resolveCollectionPath(root, logicalPath) {
|
|
4288
4313
|
const fs22 = await import("node:fs/promises");
|
|
4289
|
-
const
|
|
4314
|
+
const path28 = await import("node:path");
|
|
4290
4315
|
const segments = logicalPath.split("/").filter(Boolean);
|
|
4291
4316
|
let currentPath = root;
|
|
4292
4317
|
for (const segment of segments) {
|
|
@@ -4299,7 +4324,7 @@ async function resolveCollectionPath(root, logicalPath) {
|
|
|
4299
4324
|
return logicalName === segment.toLowerCase();
|
|
4300
4325
|
});
|
|
4301
4326
|
if (matchingDir) {
|
|
4302
|
-
currentPath =
|
|
4327
|
+
currentPath = path28.join(currentPath, matchingDir.name);
|
|
4303
4328
|
} else {
|
|
4304
4329
|
return null;
|
|
4305
4330
|
}
|
|
@@ -4466,7 +4491,7 @@ import fs5 from "node:fs/promises";
|
|
|
4466
4491
|
import path7 from "node:path";
|
|
4467
4492
|
import { randomUUID } from "node:crypto";
|
|
4468
4493
|
init_logger();
|
|
4469
|
-
var
|
|
4494
|
+
var log5 = createDebugLogger({ prefix: "ResourceGeneration" });
|
|
4470
4495
|
var META_DIR2 = ".canopy-meta";
|
|
4471
4496
|
function resourceGenerationPath(root, resource) {
|
|
4472
4497
|
return path7.resolve(root, META_DIR2, `${resource}.generation`);
|
|
@@ -4490,7 +4515,7 @@ async function readResourceGeneration(root, resource) {
|
|
|
4490
4515
|
} catch (err) {
|
|
4491
4516
|
if (isNodeError(err) && err.code === "ENOENT")
|
|
4492
4517
|
return { ok: true, token: null };
|
|
4493
|
-
|
|
4518
|
+
log5.warn("generation", `Failed to read ${resource} generation for ${root}`, {
|
|
4494
4519
|
error: getErrorMessage(err)
|
|
4495
4520
|
});
|
|
4496
4521
|
return { ok: false };
|
|
@@ -4858,7 +4883,7 @@ var STATIC_DEPLOY_USER = Object.freeze({
|
|
|
4858
4883
|
// dist/utils/request-timing.js
|
|
4859
4884
|
import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
|
|
4860
4885
|
var storage = new AsyncLocalStorage2();
|
|
4861
|
-
var
|
|
4886
|
+
var log6 = createDebugLogger({ prefix: "CanopyCMS" });
|
|
4862
4887
|
async function timeRequestPhase(phase, fn) {
|
|
4863
4888
|
const parent = storage.getStore();
|
|
4864
4889
|
if (!parent)
|
|
@@ -5325,6 +5350,8 @@ var ContentStoreError = class extends Error {
|
|
|
5325
5350
|
this.code = code;
|
|
5326
5351
|
}
|
|
5327
5352
|
};
|
|
5353
|
+
var RESTORE_SLUG_TAKEN_MESSAGE = "Another entry now has this entry's URL. Change that entry's URL, then restore this one.";
|
|
5354
|
+
var RESTORE_ID_TAKEN_MESSAGE = "This entry is already on the branch, so there is nothing to restore. Refresh to see where it is.";
|
|
5328
5355
|
var ContentConflictError = class extends Error {
|
|
5329
5356
|
constructor(message = "Content was modified by another editor") {
|
|
5330
5357
|
super(message);
|
|
@@ -5387,13 +5414,13 @@ function entryRecord(parsed, emptyIsRecord) {
|
|
|
5387
5414
|
}
|
|
5388
5415
|
function fillEmptyTopLevelField(raw, format, field, value) {
|
|
5389
5416
|
if (format === "json" || format === "yaml") {
|
|
5390
|
-
const
|
|
5391
|
-
const data =
|
|
5417
|
+
const parse = (text) => format === "json" ? entryRecord(JSON.parse(text), false) : entryRecord(yamlParse(text), true);
|
|
5418
|
+
const data = parse(raw);
|
|
5392
5419
|
if (!isEmptyFieldValue(data[field]))
|
|
5393
5420
|
return void 0;
|
|
5394
5421
|
const next = { ...data, [field]: value };
|
|
5395
5422
|
const content2 = format === "json" ? serializeJson(next, raw) : serializeYaml(next, raw);
|
|
5396
|
-
if (
|
|
5423
|
+
if (parse(content2)[field] !== value)
|
|
5397
5424
|
throw new Error(`"${field}" does not read back as written`);
|
|
5398
5425
|
return content2;
|
|
5399
5426
|
}
|
|
@@ -5583,11 +5610,11 @@ var ContentStore = class {
|
|
|
5583
5610
|
getSchemaItems() {
|
|
5584
5611
|
return this.schemaIndex.values();
|
|
5585
5612
|
}
|
|
5586
|
-
assertSchemaItem(
|
|
5587
|
-
const normalized = normalizeFilesystemPath(
|
|
5613
|
+
assertSchemaItem(path28) {
|
|
5614
|
+
const normalized = normalizeFilesystemPath(path28);
|
|
5588
5615
|
const item = this.schemaIndex.get(normalized);
|
|
5589
5616
|
if (!item) {
|
|
5590
|
-
throw new ContentStoreError(`Unknown schema item: ${
|
|
5617
|
+
throw new ContentStoreError(`Unknown schema item: ${path28}`, "NO_SCHEMA_ITEM");
|
|
5591
5618
|
}
|
|
5592
5619
|
return item;
|
|
5593
5620
|
}
|
|
@@ -5905,7 +5932,7 @@ var ContentStore = class {
|
|
|
5905
5932
|
doc.version = stat.mtimeMs;
|
|
5906
5933
|
return doc;
|
|
5907
5934
|
}
|
|
5908
|
-
async write(collectionPath, slug = "", input, entryTypeName, existingId) {
|
|
5935
|
+
async write(collectionPath, slug = "", input, entryTypeName, existingId, options = {}) {
|
|
5909
5936
|
await this.idIndex();
|
|
5910
5937
|
const schemaItem = this.assertSchemaItem(collectionPath);
|
|
5911
5938
|
let expectedFormat;
|
|
@@ -5993,14 +6020,7 @@ var ContentStore = class {
|
|
|
5993
6020
|
}
|
|
5994
6021
|
}
|
|
5995
6022
|
const existingRaw = current?.content;
|
|
5996
|
-
|
|
5997
|
-
if (input.format === "json") {
|
|
5998
|
-
content = serializeJson(input.data ?? {}, existingRaw);
|
|
5999
|
-
} else if (input.format === "yaml") {
|
|
6000
|
-
content = serializeYaml(input.data ?? {}, existingRaw);
|
|
6001
|
-
} else {
|
|
6002
|
-
content = serializeFrontmatter(input.body, input.data ?? {}, existingRaw, input.format, fields);
|
|
6003
|
-
}
|
|
6023
|
+
const content = serializeEntry(input, existingRaw, fields);
|
|
6004
6024
|
await atomicWriteFile(absolutePath, content);
|
|
6005
6025
|
const liveIndex = this._idIndex;
|
|
6006
6026
|
let staleOldAbsPath = null;
|
|
@@ -6027,6 +6047,7 @@ var ContentStore = class {
|
|
|
6027
6047
|
});
|
|
6028
6048
|
}
|
|
6029
6049
|
await this.recordOwnMutation(liveIndex);
|
|
6050
|
+
await options.afterWrite?.({ id, relativePath });
|
|
6030
6051
|
const afterStat = await fs9.stat(absolutePath);
|
|
6031
6052
|
const base = {
|
|
6032
6053
|
collection: schemaItem.logicalPath,
|
|
@@ -6179,14 +6200,21 @@ var ContentStore = class {
|
|
|
6179
6200
|
*/
|
|
6180
6201
|
async afterPrePassForTesting() {
|
|
6181
6202
|
}
|
|
6182
|
-
/**
|
|
6183
|
-
|
|
6203
|
+
/**
|
|
6204
|
+
* Delete an entry and remove it from the index. With `expectId`, only the entry of that content
|
|
6205
|
+
* ID: another at the slug, or none, is `EntryGoneError`. `beforeDelete` runs under both of the
|
|
6206
|
+
* delete's locks before anything changes, and throwing refuses; `afterDelete` runs once it is
|
|
6207
|
+
* gone, under them too.
|
|
6208
|
+
*/
|
|
6209
|
+
async delete(collectionPath, slug, options = {}) {
|
|
6184
6210
|
await this.idIndex();
|
|
6185
6211
|
const collection = this.assertCollection(collectionPath);
|
|
6186
6212
|
const prePass = await this.buildPaths(collection, slug);
|
|
6187
6213
|
if (prePass.existed)
|
|
6188
6214
|
this.assertEntryTypeAvailable(collection, prePass.entryTypeName);
|
|
6189
|
-
if (!prePass.existed) {
|
|
6215
|
+
if (!prePass.existed || options.expectId !== void 0 && prePass.id !== options.expectId) {
|
|
6216
|
+
if (options.expectId !== void 0)
|
|
6217
|
+
throw new EntryGoneError();
|
|
6190
6218
|
await fs9.unlink(prePass.absolutePath);
|
|
6191
6219
|
return;
|
|
6192
6220
|
}
|
|
@@ -6203,6 +6231,10 @@ var ContentStore = class {
|
|
|
6203
6231
|
return RETRY_KEY;
|
|
6204
6232
|
}
|
|
6205
6233
|
const { absolutePath, relativePath } = inLock;
|
|
6234
|
+
if (options.expectId !== void 0 && (!inLock.existed || inLock.id !== options.expectId)) {
|
|
6235
|
+
throw new EntryGoneError();
|
|
6236
|
+
}
|
|
6237
|
+
await options.beforeDelete?.();
|
|
6206
6238
|
await fs9.unlink(absolutePath);
|
|
6207
6239
|
const liveIndex = this._idIndex;
|
|
6208
6240
|
const id = liveIndex.findByPath(relativePath);
|
|
@@ -6210,6 +6242,7 @@ var ContentStore = class {
|
|
|
6210
6242
|
liveIndex.remove(id);
|
|
6211
6243
|
}
|
|
6212
6244
|
await this.recordOwnMutation(liveIndex);
|
|
6245
|
+
await options.afterDelete?.({ id: id ?? inLock.id });
|
|
6213
6246
|
return void 0;
|
|
6214
6247
|
});
|
|
6215
6248
|
if (outcome !== RETRY_KEY)
|
|
@@ -6218,6 +6251,93 @@ var ContentStore = class {
|
|
|
6218
6251
|
throw new ContentConflictError();
|
|
6219
6252
|
});
|
|
6220
6253
|
}
|
|
6254
|
+
/**
|
|
6255
|
+
* Puts back an entry deleted on this branch: `raw`, its file as the base holds it, verbatim,
|
|
6256
|
+
* under its old slug, entry type and content ID, so references and comment threads find it
|
|
6257
|
+
* again and the branch no longer differs from the base there.
|
|
6258
|
+
*
|
|
6259
|
+
* A create under write()'s locks and create-only guards that never replaces or relocates
|
|
6260
|
+
* anything. It refuses with `ContentConflictError` while a file holds the slug, or the index
|
|
6261
|
+
* or the target directory holds the ID: unlike write(), it has no index repair, so an ID
|
|
6262
|
+
* found elsewhere is a moved entry, not a deleted one, and is left alone. The index is
|
|
6263
|
+
* warmed outside the lock, as write() warms it, and rescanned under the lock only when the
|
|
6264
|
+
* generation marker says another host changed the tree.
|
|
6265
|
+
*/
|
|
6266
|
+
async restore(collectionPath, slug, entry, options = {}) {
|
|
6267
|
+
await this.idIndex();
|
|
6268
|
+
const collection = this.assertCollection(collectionPath);
|
|
6269
|
+
if (!collection.entries?.some((e) => e.name === entry.entryTypeName)) {
|
|
6270
|
+
throw new ContentStoreError(`Entry type '${entry.entryTypeName}' not found in collection`, "NO_SCHEMA_ITEM");
|
|
6271
|
+
}
|
|
6272
|
+
this.assertEntryTypeAvailable(collection, entry.entryTypeName);
|
|
6273
|
+
return this.withContentWriteExclusion(async () => {
|
|
6274
|
+
await options.beforeWrite?.();
|
|
6275
|
+
if (await readContentIndexGeneration(this.root) !== this.loadedDiskGeneration) {
|
|
6276
|
+
this.invalidateIndex();
|
|
6277
|
+
}
|
|
6278
|
+
await this.idIndex();
|
|
6279
|
+
return withLock(this.createLockKey(collection, slug), async () => {
|
|
6280
|
+
const target = await this.buildPaths(collection, slug, {
|
|
6281
|
+
existingId: entry.id,
|
|
6282
|
+
entryTypeName: entry.entryTypeName
|
|
6283
|
+
});
|
|
6284
|
+
const dir = path11.dirname(target.absolutePath);
|
|
6285
|
+
const liveIndex = this._idIndex;
|
|
6286
|
+
if (liveIndex.findById(entry.id) || liveIndex.getDuplicateFor(entry.id) || (await this.findEntryPathsById(dir, entry.id)).length > 0) {
|
|
6287
|
+
throw new ContentConflictError(RESTORE_ID_TAKEN_MESSAGE);
|
|
6288
|
+
}
|
|
6289
|
+
if ((await this.buildPaths(collection, slug)).existed) {
|
|
6290
|
+
throw new ContentConflictError(RESTORE_SLUG_TAKEN_MESSAGE);
|
|
6291
|
+
}
|
|
6292
|
+
const routable = parseSlug(target.slug);
|
|
6293
|
+
if (!routable.ok) {
|
|
6294
|
+
throw new ContentStoreError(`Cannot restore entry "${target.slug}": ${routable.error}.`, "VALIDATION");
|
|
6295
|
+
}
|
|
6296
|
+
await this.assertUrlPathAvailable(dir, target.slug);
|
|
6297
|
+
await fs9.mkdir(dir, { recursive: true });
|
|
6298
|
+
await atomicWriteFile(target.absolutePath, entry.raw);
|
|
6299
|
+
const index = this._idIndex;
|
|
6300
|
+
if (!index.findById(entry.id)) {
|
|
6301
|
+
index.add({
|
|
6302
|
+
type: "entry",
|
|
6303
|
+
relativePath: target.relativePath,
|
|
6304
|
+
collection: collectionPath,
|
|
6305
|
+
slug
|
|
6306
|
+
});
|
|
6307
|
+
}
|
|
6308
|
+
await this.recordOwnMutation(index);
|
|
6309
|
+
await options.afterWrite?.();
|
|
6310
|
+
return { relativePath: target.relativePath };
|
|
6311
|
+
});
|
|
6312
|
+
});
|
|
6313
|
+
}
|
|
6314
|
+
/**
|
|
6315
|
+
* Replace the file of entry `id`, at `slug` in `collectionPath`, with `raw`, verbatim, under
|
|
6316
|
+
* write()'s locks; `beforeWrite` runs under them first, and throwing refuses, and `afterWrite`
|
|
6317
|
+
* after. For a file no parse can be trusted with, so nothing is re-serialised. `EntryGoneError`
|
|
6318
|
+
* when no file of that id is there.
|
|
6319
|
+
*/
|
|
6320
|
+
async replaceRaw(collectionPath, slug, entry, options = {}) {
|
|
6321
|
+
await this.idIndex();
|
|
6322
|
+
const collection = this.assertCollection(collectionPath);
|
|
6323
|
+
const prePass = await this.buildPaths(collection, slug);
|
|
6324
|
+
if (!prePass.existed || prePass.id !== entry.id)
|
|
6325
|
+
throw new EntryGoneError();
|
|
6326
|
+
this.assertEntryTypeAvailable(collection, prePass.entryTypeName);
|
|
6327
|
+
const lockKey = this.entryLockKey(collection, slug, prePass);
|
|
6328
|
+
return this.withContentWriteExclusion(() => withLock(lockKey, async () => {
|
|
6329
|
+
const inLock = await this.buildPaths(collection, slug);
|
|
6330
|
+
if (!inLock.existed || inLock.id !== entry.id)
|
|
6331
|
+
throw new EntryGoneError();
|
|
6332
|
+
if (this.entryLockKey(collection, slug, inLock) !== lockKey) {
|
|
6333
|
+
throw new ContentConflictError();
|
|
6334
|
+
}
|
|
6335
|
+
await options.beforeWrite?.();
|
|
6336
|
+
await atomicWriteFile(inLock.absolutePath, entry.raw);
|
|
6337
|
+
await this.recordOwnMutation(this._idIndex);
|
|
6338
|
+
await options.afterWrite?.();
|
|
6339
|
+
}));
|
|
6340
|
+
}
|
|
6221
6341
|
/**
|
|
6222
6342
|
* Rename an entry by changing its slug (middle segment of filename).
|
|
6223
6343
|
* Entry filename pattern: {entryTypeName}.{slug}.{id}.{ext}
|
|
@@ -7178,7 +7298,7 @@ function yamlValue(value) {
|
|
|
7178
7298
|
import path12 from "node:path";
|
|
7179
7299
|
|
|
7180
7300
|
// dist/entry-link-resolver.js
|
|
7181
|
-
var
|
|
7301
|
+
var log7 = createDebugLogger({ prefix: "EntryLinks" });
|
|
7182
7302
|
var BASE58_CHAR = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]";
|
|
7183
7303
|
var ENTRY_LINK_PATTERN = new RegExp(`entry:(${BASE58_CHAR}{12})(#[^\\s)>"']*)?`, "g");
|
|
7184
7304
|
function resolveEntryUrl(location, contentRoot) {
|
|
@@ -7194,7 +7314,7 @@ function resolveEntryLinksInText(text, idIndex, contentRoot, customResolver, opt
|
|
|
7194
7314
|
return partText.replace(ENTRY_LINK_PATTERN, (_match, id, anchor) => {
|
|
7195
7315
|
const location = findEntry(idIndex, id);
|
|
7196
7316
|
if (!location) {
|
|
7197
|
-
|
|
7317
|
+
log7.warn("resolve", `Entry link target not found: entry:${id}`);
|
|
7198
7318
|
return isLinkable ? "#" : anchor ?? "#";
|
|
7199
7319
|
}
|
|
7200
7320
|
if (isLinkable && !isLinkable(id))
|
|
@@ -7930,6 +8050,14 @@ var branchMetadataShape = {
|
|
|
7930
8050
|
state: z7.enum(["approved", "changes-requested"]),
|
|
7931
8051
|
by: z7.object({ userId: z7.string(), name: z7.string().optional() }),
|
|
7932
8052
|
at: z7.string()
|
|
8053
|
+
}).optional().catch(void 0),
|
|
8054
|
+
baseUpdateRequestedAt: z7.string().optional().catch(void 0),
|
|
8055
|
+
// Dropped when malformed: it only ends the editor's wait, which then times out instead.
|
|
8056
|
+
lastBaseUpdate: z7.object({
|
|
8057
|
+
at: z7.string(),
|
|
8058
|
+
outcome: z7.enum(["merged", "up-to-date", "conflicts", "failed"]),
|
|
8059
|
+
baseSha: z7.string().optional(),
|
|
8060
|
+
keptEntries: z7.number().int().nonnegative().optional()
|
|
7933
8061
|
}).optional().catch(void 0)
|
|
7934
8062
|
};
|
|
7935
8063
|
var branchMetadataSchema = z7.object(branchMetadataShape).passthrough();
|
|
@@ -7985,7 +8113,7 @@ function parseBranchMetadataFile(branchRoot, raw) {
|
|
|
7985
8113
|
// dist/branch-registry.js
|
|
7986
8114
|
init_error();
|
|
7987
8115
|
init_logger();
|
|
7988
|
-
var
|
|
8116
|
+
var log8 = createDebugLogger({ prefix: "BranchRegistry" });
|
|
7989
8117
|
var REGISTRY_FILE = "branches.json";
|
|
7990
8118
|
var REGISTRY_STALE_FILE = "branches.stale.json";
|
|
7991
8119
|
var REGISTRY_TEMP_FILE = "branches.tmp.json";
|
|
@@ -8111,7 +8239,7 @@ var BranchRegistry = class {
|
|
|
8111
8239
|
});
|
|
8112
8240
|
await this.regenerate();
|
|
8113
8241
|
} catch (err) {
|
|
8114
|
-
|
|
8242
|
+
log8.warn("invalidate", "Eager regeneration after invalidate() failed", {
|
|
8115
8243
|
error: getErrorMessage(err)
|
|
8116
8244
|
});
|
|
8117
8245
|
}
|
|
@@ -8207,7 +8335,7 @@ import path16 from "node:path";
|
|
|
8207
8335
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
8208
8336
|
import lockfile2 from "proper-lockfile";
|
|
8209
8337
|
init_logger();
|
|
8210
|
-
var
|
|
8338
|
+
var log9 = createDebugLogger({ prefix: "OccJsonWrite" });
|
|
8211
8339
|
var OccWriteConflictError = class extends Error {
|
|
8212
8340
|
constructor(message = "Concurrent modification detected") {
|
|
8213
8341
|
super(message);
|
|
@@ -8325,7 +8453,7 @@ async function withOccFileLock(filePath, fn) {
|
|
|
8325
8453
|
await release().catch((err) => {
|
|
8326
8454
|
if (isNodeError(err) && err.code === "ENOENT")
|
|
8327
8455
|
return;
|
|
8328
|
-
|
|
8456
|
+
log9.warn("lock", `Failed to release lock for ${filePath}`, { error: getErrorMessage(err) });
|
|
8329
8457
|
});
|
|
8330
8458
|
}
|
|
8331
8459
|
}
|
|
@@ -8778,7 +8906,7 @@ function feedStdin(child, source, touch, onSourceError) {
|
|
|
8778
8906
|
}
|
|
8779
8907
|
|
|
8780
8908
|
// dist/git-manager.js
|
|
8781
|
-
var
|
|
8909
|
+
var log10 = createDebugLogger({ prefix: "GitManager" });
|
|
8782
8910
|
var GIT_ENV_PASSTHROUGH = /^(PATH|HOME|USER|LANG|LC_[A-Z]+|TZ|TMPDIR|GIT_(AUTHOR|COMMITTER)_(NAME|EMAIL|DATE)|GIT_TERMINAL_PROMPT|GIT_TRACE[0-9A-Z_]*)$/;
|
|
8783
8911
|
var FORCE_C_LOCALE = { LC_ALL: "C", LANG: "C" };
|
|
8784
8912
|
function gitChildEnv(overrides) {
|
|
@@ -8900,13 +9028,13 @@ async function ensureGitExcludePattern(repoPath, pattern) {
|
|
|
8900
9028
|
}
|
|
8901
9029
|
const lines = content.split("\n");
|
|
8902
9030
|
if (lines.some((line) => line.trim() === pattern)) {
|
|
8903
|
-
|
|
9031
|
+
log10.debug("git", "Pattern already in .git/info/exclude", { pattern });
|
|
8904
9032
|
return;
|
|
8905
9033
|
}
|
|
8906
9034
|
const needsLeadingNewline = content.length > 0 && !content.endsWith("\n");
|
|
8907
9035
|
const newContent = content + (needsLeadingNewline ? "\n" : "") + pattern + "\n";
|
|
8908
9036
|
await fs16.writeFile(excludePath, newContent, "utf-8");
|
|
8909
|
-
|
|
9037
|
+
log10.debug("git", "Added pattern to .git/info/exclude", { pattern });
|
|
8910
9038
|
}
|
|
8911
9039
|
var BaseBranchMissingError = class extends Error {
|
|
8912
9040
|
constructor(remote, baseBranch) {
|
|
@@ -8992,7 +9120,7 @@ var GitManager = class _GitManager {
|
|
|
8992
9120
|
* target that held files beforehand is never touched.
|
|
8993
9121
|
*/
|
|
8994
9122
|
static async cloneRepo(remoteUrl, targetPath, baseBranch = "main", options = {}) {
|
|
8995
|
-
|
|
9123
|
+
log10.debug("git", "Cloning repository", {
|
|
8996
9124
|
remoteUrl,
|
|
8997
9125
|
targetPath,
|
|
8998
9126
|
baseBranch
|
|
@@ -9024,7 +9152,7 @@ var GitManager = class _GitManager {
|
|
|
9024
9152
|
await fs16.mkdir(targetPath);
|
|
9025
9153
|
await git.clone(remoteUrl, targetPath, args);
|
|
9026
9154
|
}
|
|
9027
|
-
|
|
9155
|
+
log10.debug("git", "Clone complete");
|
|
9028
9156
|
}
|
|
9029
9157
|
/**
|
|
9030
9158
|
* Initializes a local bare git repository to simulate a remote for dev mode,
|
|
@@ -9038,15 +9166,15 @@ var GitManager = class _GitManager {
|
|
|
9038
9166
|
static async ensureLocalSimulatedRemote(options) {
|
|
9039
9167
|
const existingLock = remoteInitLocks.get(options.remotePath);
|
|
9040
9168
|
if (existingLock) {
|
|
9041
|
-
|
|
9169
|
+
log10.debug("git", "Waiting for existing remote initialization", {
|
|
9042
9170
|
remotePath: options.remotePath
|
|
9043
9171
|
});
|
|
9044
9172
|
await existingLock;
|
|
9045
9173
|
}
|
|
9046
|
-
const lockPromise =
|
|
9174
|
+
const lockPromise = log10.timed("git", "ensureLocalSimulatedRemote", async () => {
|
|
9047
9175
|
let releaseLock;
|
|
9048
9176
|
try {
|
|
9049
|
-
|
|
9177
|
+
log10.debug("git", "Initializing local simulated remote", {
|
|
9050
9178
|
remotePath: options.remotePath,
|
|
9051
9179
|
baseBranch: options.baseBranch
|
|
9052
9180
|
});
|
|
@@ -9064,15 +9192,15 @@ var GitManager = class _GitManager {
|
|
|
9064
9192
|
try {
|
|
9065
9193
|
const repack = await repackBareRemoteIfNeeded(options.remotePath);
|
|
9066
9194
|
if (repack.repacked)
|
|
9067
|
-
|
|
9195
|
+
log10.debug("git", "Repacked local simulated remote", repack);
|
|
9068
9196
|
} catch (err) {
|
|
9069
|
-
|
|
9197
|
+
log10.debug("git", "Could not repack local simulated remote", {
|
|
9070
9198
|
error: getErrorMessage(err)
|
|
9071
9199
|
});
|
|
9072
9200
|
}
|
|
9073
9201
|
}
|
|
9074
9202
|
if (remoteExists && await _GitManager.bareRemoteHasBranch(options.remotePath, options.baseBranch)) {
|
|
9075
|
-
|
|
9203
|
+
log10.debug("git", "Remote already has base branch, skipping");
|
|
9076
9204
|
await ensureRemoteGitConfig(options.remotePath);
|
|
9077
9205
|
return;
|
|
9078
9206
|
}
|
|
@@ -9092,8 +9220,8 @@ var GitManager = class _GitManager {
|
|
|
9092
9220
|
}
|
|
9093
9221
|
let hasCommits = false;
|
|
9094
9222
|
try {
|
|
9095
|
-
const
|
|
9096
|
-
hasCommits =
|
|
9223
|
+
const log12 = await sourceGit.log(["-1"]);
|
|
9224
|
+
hasCommits = log12.total > 0;
|
|
9097
9225
|
} catch {
|
|
9098
9226
|
hasCommits = false;
|
|
9099
9227
|
}
|
|
@@ -9105,12 +9233,12 @@ var GitManager = class _GitManager {
|
|
|
9105
9233
|
throw new Error(`Cannot initialize local simulated remote: base branch '${options.baseBranch}' does not exist locally. Please checkout '${options.baseBranch}' first or provide an explicit remoteUrl.`);
|
|
9106
9234
|
}
|
|
9107
9235
|
if (remoteExists) {
|
|
9108
|
-
|
|
9236
|
+
log10.debug("git", "Existing remote is missing base branch \u2014 pushing it from source", {
|
|
9109
9237
|
remotePath: options.remotePath,
|
|
9110
9238
|
baseBranch: options.baseBranch
|
|
9111
9239
|
});
|
|
9112
9240
|
} else {
|
|
9113
|
-
|
|
9241
|
+
log10.debug("git", "Creating bare remote repository");
|
|
9114
9242
|
await simpleGit2().raw([
|
|
9115
9243
|
"init",
|
|
9116
9244
|
"--bare",
|
|
@@ -9125,13 +9253,13 @@ var GitManager = class _GitManager {
|
|
|
9125
9253
|
baseBranch: options.baseBranch,
|
|
9126
9254
|
subdirectory: options.subdirectory
|
|
9127
9255
|
});
|
|
9128
|
-
|
|
9256
|
+
log10.debug("git", "Remote initialization complete");
|
|
9129
9257
|
} finally {
|
|
9130
9258
|
if (releaseLock) {
|
|
9131
9259
|
try {
|
|
9132
9260
|
await releaseLock();
|
|
9133
9261
|
} catch (err) {
|
|
9134
|
-
|
|
9262
|
+
log10.debug("git", "Failed to release remote-init lock", { err });
|
|
9135
9263
|
}
|
|
9136
9264
|
}
|
|
9137
9265
|
remoteInitLocks.delete(options.remotePath);
|
|
@@ -9413,7 +9541,7 @@ var GitManager = class _GitManager {
|
|
|
9413
9541
|
throw new Error(`CanopyCMS: cannot read the git remote at ${remotePath}: ${getErrorMessage(err)}`);
|
|
9414
9542
|
});
|
|
9415
9543
|
if (stat?.isDirectory()) {
|
|
9416
|
-
|
|
9544
|
+
log10.debug("git", "Auto-detected local remote", { path: remotePath });
|
|
9417
9545
|
return remotePath;
|
|
9418
9546
|
}
|
|
9419
9547
|
if (stat) {
|
|
@@ -9539,7 +9667,7 @@ var GitManager = class _GitManager {
|
|
|
9539
9667
|
try {
|
|
9540
9668
|
const stat = await fs16.stat(gitPath);
|
|
9541
9669
|
if (stat.isDirectory()) {
|
|
9542
|
-
|
|
9670
|
+
log10.debug("git", "Removing corrupt .git directory", {
|
|
9543
9671
|
workspacePath: options.workspacePath
|
|
9544
9672
|
});
|
|
9545
9673
|
await fs16.rm(gitPath, { recursive: true });
|
|
@@ -10005,17 +10133,17 @@ var GitManager = class _GitManager {
|
|
|
10005
10133
|
const changes = [];
|
|
10006
10134
|
for (let i = 0; i + 1 < tokens.length; i += 2) {
|
|
10007
10135
|
const [srcMode, dstMode, srcSha, dstSha, code] = tokens[i].slice(1).split(" ");
|
|
10008
|
-
const
|
|
10136
|
+
const path28 = tokens[i + 1];
|
|
10009
10137
|
const letter = code?.charAt(0);
|
|
10010
10138
|
if (letter === "A" || letter === "D")
|
|
10011
|
-
changes.push({ status: letter, path:
|
|
10139
|
+
changes.push({ status: letter, path: path28 });
|
|
10012
10140
|
else if (letter === "M" || letter === "T") {
|
|
10013
10141
|
const unhashed = /^0+$/.test(dstSha) && srcMode === dstMode ? srcSha : void 0;
|
|
10014
|
-
changes.push({ status: "M", path:
|
|
10142
|
+
changes.push({ status: "M", path: path28, unhashed });
|
|
10015
10143
|
}
|
|
10016
10144
|
}
|
|
10017
10145
|
const unchanged = await this.matchingBlobs(changes.flatMap((c) => c.unhashed ? [{ path: c.path, sha: c.unhashed }] : []));
|
|
10018
|
-
return changes.filter((c) => !unchanged.has(c.path)).map(({ status, path:
|
|
10146
|
+
return changes.filter((c) => !unchanged.has(c.path)).map(({ status, path: path28 }) => ({ status, path: path28 }));
|
|
10019
10147
|
}
|
|
10020
10148
|
/**
|
|
10021
10149
|
* The paths whose working-tree file hashes to `sha`, as git hashes it: a symlink's blob is its
|
|
@@ -10110,6 +10238,32 @@ var GitManager = class _GitManager {
|
|
|
10110
10238
|
throw err;
|
|
10111
10239
|
}
|
|
10112
10240
|
}
|
|
10241
|
+
/**
|
|
10242
|
+
* The text of the blob `sha`, `'too-large'` when it is over `maxBytes`, or `undefined` when this
|
|
10243
|
+
* clone has no such blob. Any other failure throws.
|
|
10244
|
+
*/
|
|
10245
|
+
async readBlob(sha, maxBytes) {
|
|
10246
|
+
if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(sha))
|
|
10247
|
+
return void 0;
|
|
10248
|
+
let sizeText;
|
|
10249
|
+
try {
|
|
10250
|
+
sizeText = (await this.git.raw(["cat-file", "-s", "--end-of-options", sha])).trim();
|
|
10251
|
+
} catch (err) {
|
|
10252
|
+
if (/could not get object info|Not a valid object name/.test(getErrorMessage(err))) {
|
|
10253
|
+
return void 0;
|
|
10254
|
+
}
|
|
10255
|
+
throw err;
|
|
10256
|
+
}
|
|
10257
|
+
if (!/^\d+$/.test(sizeText))
|
|
10258
|
+
throw new Error(`git cat-file -s ${sha} printed no size`);
|
|
10259
|
+
const size = Number(sizeText);
|
|
10260
|
+
if (size > maxBytes)
|
|
10261
|
+
return "too-large";
|
|
10262
|
+
const text = await this.git.raw(["cat-file", "blob", "--end-of-options", sha]);
|
|
10263
|
+
if (size > 0 && text === "")
|
|
10264
|
+
throw new Error(`git cat-file blob ${sha} printed nothing`);
|
|
10265
|
+
return text;
|
|
10266
|
+
}
|
|
10113
10267
|
async ensureAuthor(author) {
|
|
10114
10268
|
const config = await this.git.listConfig();
|
|
10115
10269
|
const isManaged = config.all["canopycms.managed"] === "true";
|
|
@@ -10198,7 +10352,7 @@ var GitManager = class _GitManager {
|
|
|
10198
10352
|
}
|
|
10199
10353
|
}
|
|
10200
10354
|
async createOrphanSettingsBranchInner(branchName, initialFiles) {
|
|
10201
|
-
|
|
10355
|
+
log10.debug("git", "Ensuring settings branch", { branchName });
|
|
10202
10356
|
const branches = await this.git.branch();
|
|
10203
10357
|
const localExists = branches.all.includes(branchName);
|
|
10204
10358
|
if (localExists) {
|
|
@@ -10209,7 +10363,7 @@ var GitManager = class _GitManager {
|
|
|
10209
10363
|
remoteTip = await this.remoteBranchTip(branchName);
|
|
10210
10364
|
} catch (err) {
|
|
10211
10365
|
if (localExists && !await this.isEmptyInitialBranch(branchName)) {
|
|
10212
|
-
|
|
10366
|
+
log10.debug("git", "Remote unreadable; serving the local settings branch", { branchName });
|
|
10213
10367
|
return;
|
|
10214
10368
|
}
|
|
10215
10369
|
throw new SettingsRemoteUnreadableError(branchName, this.remote, err);
|
|
@@ -10222,7 +10376,7 @@ var GitManager = class _GitManager {
|
|
|
10222
10376
|
const fetchedTip = await this.fetchBranchTip(branchName);
|
|
10223
10377
|
await this.assertNotContentHistory(branchName, fetchedTip);
|
|
10224
10378
|
await this.git.raw(["checkout", "-b", branchName, fetchedTip]);
|
|
10225
|
-
|
|
10379
|
+
log10.debug("git", "Checked out the remote settings branch", { branchName });
|
|
10226
10380
|
return;
|
|
10227
10381
|
}
|
|
10228
10382
|
await this.git.raw(["switch", "--orphan", branchName]);
|
|
@@ -10238,7 +10392,7 @@ var GitManager = class _GitManager {
|
|
|
10238
10392
|
await this.git.add(filePath);
|
|
10239
10393
|
}
|
|
10240
10394
|
await this.git.commit("Initialize settings branch", ["--allow-empty"]);
|
|
10241
|
-
|
|
10395
|
+
log10.debug("git", "Orphan settings branch created", { branchName });
|
|
10242
10396
|
}
|
|
10243
10397
|
/**
|
|
10244
10398
|
* Compares the checked-out local settings branch with the remote's tip.
|
|
@@ -10271,7 +10425,7 @@ var GitManager = class _GitManager {
|
|
|
10271
10425
|
}
|
|
10272
10426
|
await this.assertNotContentHistory(branchName, fetchedTip);
|
|
10273
10427
|
await this.git.raw(["checkout", "-B", branchName, fetchedTip]);
|
|
10274
|
-
|
|
10428
|
+
log10.debug("git", "Replaced an empty settings branch with the remote one", { branchName });
|
|
10275
10429
|
}
|
|
10276
10430
|
/**
|
|
10277
10431
|
* The remote's tip for `branch`, or `undefined` when the remote has no such
|
|
@@ -10541,10 +10695,10 @@ async function hasBranchMetadata(branchRoot) {
|
|
|
10541
10695
|
return fs17.access(path21.join(branchRoot, BRANCH_META_DIR, BRANCH_META_FILE)).then(() => true, () => false);
|
|
10542
10696
|
}
|
|
10543
10697
|
async function stageBranchWorkspace(options) {
|
|
10544
|
-
const { provisionLog:
|
|
10698
|
+
const { provisionLog: log12 } = options;
|
|
10545
10699
|
const stagingPath = path21.join(options.baseRoot, stagingDirName(options.dirName));
|
|
10546
10700
|
try {
|
|
10547
|
-
await
|
|
10701
|
+
await log12.step("clone", () => GitManager.cloneWorkspace(options.remoteUrl, stagingPath, options.baseBranch, {
|
|
10548
10702
|
...options.clone,
|
|
10549
10703
|
noCheckout: true
|
|
10550
10704
|
}));
|
|
@@ -10555,14 +10709,14 @@ async function stageBranchWorkspace(options) {
|
|
|
10555
10709
|
});
|
|
10556
10710
|
const cone = options.sparseCone;
|
|
10557
10711
|
if (cone)
|
|
10558
|
-
await
|
|
10712
|
+
await log12.step("sparse", () => git.setSparseCone(cone));
|
|
10559
10713
|
if (await hasBranchMetadata(path21.join(options.baseRoot, options.dirName))) {
|
|
10560
10714
|
await removeLeftoverDir(stagingPath);
|
|
10561
10715
|
return { kind: "exists" };
|
|
10562
10716
|
}
|
|
10563
|
-
await
|
|
10564
|
-
await
|
|
10565
|
-
const written = await
|
|
10717
|
+
await log12.step("checkout", () => git.checkoutFreshClone(options.branchName));
|
|
10718
|
+
await log12.step("exclude", () => git.ensureGitExclude(options.gitExcludePattern));
|
|
10719
|
+
const written = await log12.step("metadata", () => writeOccJsonFile(path21.join(stagingPath, BRANCH_META_DIR, BRANCH_META_FILE), { ...options.metadata }, { expectedVersion: null, trailingNewline: true }));
|
|
10566
10720
|
return {
|
|
10567
10721
|
kind: "staged",
|
|
10568
10722
|
stagingPath,
|
|
@@ -11034,7 +11188,7 @@ import path24 from "node:path";
|
|
|
11034
11188
|
import matter3 from "gray-matter";
|
|
11035
11189
|
import { parse as yamlParse2 } from "yaml";
|
|
11036
11190
|
init_error();
|
|
11037
|
-
var
|
|
11191
|
+
var log11 = createDebugLogger({ prefix: "ContentListing" });
|
|
11038
11192
|
var normalizePath = (root, target) => {
|
|
11039
11193
|
const result = validateAndNormalizePath(root, target);
|
|
11040
11194
|
if (!result.valid) {
|
|
@@ -11062,7 +11216,7 @@ var readEntryData = async (filePath, format, bodyFieldName = "body") => {
|
|
|
11062
11216
|
} catch (err) {
|
|
11063
11217
|
if (isNotFoundError(err))
|
|
11064
11218
|
return {};
|
|
11065
|
-
|
|
11219
|
+
log11.warn("readEntryData", `Failed to read entry data from ${filePath}: ${getErrorMessage(err)}`);
|
|
11066
11220
|
return {};
|
|
11067
11221
|
}
|
|
11068
11222
|
};
|
|
@@ -11188,7 +11342,7 @@ var listCollectionEntries = async (root, collection, onSkip, readsId) => {
|
|
|
11188
11342
|
const relativePath = normalizePath(root, absolutePath);
|
|
11189
11343
|
const parsed = parseTypedFilename(file.name, entryTypes);
|
|
11190
11344
|
if (!parsed) {
|
|
11191
|
-
|
|
11345
|
+
log11.warn("listCollectionEntries", `Skipping file with unrecognized filename format: ${file.name} (expected {type}.{slug}.{id}.{ext} with a known entry type and valid 12-char Base58 ID)`);
|
|
11192
11346
|
if (looksLikeMalformedEntry(file.name, entryTypes)) {
|
|
11193
11347
|
onSkip?.({ filename: file.name, collectionPath: collection.logicalPath });
|
|
11194
11348
|
}
|
|
@@ -11277,77 +11431,77 @@ function isValidImageCropValue(value) {
|
|
|
11277
11431
|
const rounded = roundCropRect({ x, y, w, h });
|
|
11278
11432
|
return isValidCropRect(rounded.x, rounded.y, rounded.w, rounded.h);
|
|
11279
11433
|
}
|
|
11280
|
-
function validateImageValue(field, value,
|
|
11434
|
+
function validateImageValue(field, value, path28) {
|
|
11281
11435
|
if (!isPlainRecord3(value)) {
|
|
11282
|
-
return [{ fieldPath:
|
|
11436
|
+
return [{ fieldPath: path28, message: "Expected an image object with { src, alt }" }];
|
|
11283
11437
|
}
|
|
11284
11438
|
const errors = [];
|
|
11285
11439
|
if (typeof value.src !== "string" || value.src.trim() === "") {
|
|
11286
|
-
errors.push({ fieldPath: `${
|
|
11440
|
+
errors.push({ fieldPath: `${path28}.src`, message: "Image src is required" });
|
|
11287
11441
|
}
|
|
11288
11442
|
if (typeof value.alt !== "string") {
|
|
11289
|
-
errors.push({ fieldPath: `${
|
|
11443
|
+
errors.push({ fieldPath: `${path28}.alt`, message: "Image alt text is required" });
|
|
11290
11444
|
} else if (value.alt.trim() === "" && field.altOptional !== true) {
|
|
11291
|
-
errors.push({ fieldPath: `${
|
|
11445
|
+
errors.push({ fieldPath: `${path28}.alt`, message: "Image alt text is required" });
|
|
11292
11446
|
}
|
|
11293
11447
|
if (value.width !== void 0 && !isPositiveInt(value.width)) {
|
|
11294
|
-
errors.push({ fieldPath: `${
|
|
11448
|
+
errors.push({ fieldPath: `${path28}.width`, message: "Image width must be a positive integer" });
|
|
11295
11449
|
}
|
|
11296
11450
|
if (value.height !== void 0 && !isPositiveInt(value.height)) {
|
|
11297
11451
|
errors.push({
|
|
11298
|
-
fieldPath: `${
|
|
11452
|
+
fieldPath: `${path28}.height`,
|
|
11299
11453
|
message: "Image height must be a positive integer"
|
|
11300
11454
|
});
|
|
11301
11455
|
}
|
|
11302
11456
|
if (value.crop !== void 0 && !isValidImageCropValue(value.crop)) {
|
|
11303
|
-
errors.push({ fieldPath: `${
|
|
11457
|
+
errors.push({ fieldPath: `${path28}.crop`, message: "Invalid image crop rect" });
|
|
11304
11458
|
}
|
|
11305
11459
|
return errors;
|
|
11306
11460
|
}
|
|
11307
11461
|
var selectOptionValues = (field) => (field.options ?? []).map((opt) => typeof opt === "string" ? opt : opt.value);
|
|
11308
|
-
function validateScalar(field, value,
|
|
11462
|
+
function validateScalar(field, value, path28) {
|
|
11309
11463
|
switch (field.type) {
|
|
11310
11464
|
case "number":
|
|
11311
11465
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
11312
|
-
return [{ fieldPath:
|
|
11466
|
+
return [{ fieldPath: path28, message: "Expected a number" }];
|
|
11313
11467
|
}
|
|
11314
11468
|
return [];
|
|
11315
11469
|
case "boolean":
|
|
11316
11470
|
if (typeof value !== "boolean") {
|
|
11317
|
-
return [{ fieldPath:
|
|
11471
|
+
return [{ fieldPath: path28, message: "Expected true or false" }];
|
|
11318
11472
|
}
|
|
11319
11473
|
return [];
|
|
11320
11474
|
case "date":
|
|
11321
11475
|
if (value !== "" && !isCalendarDay(value)) {
|
|
11322
|
-
return [{ fieldPath:
|
|
11476
|
+
return [{ fieldPath: path28, message: "Expected a real date, written YYYY-MM-DD" }];
|
|
11323
11477
|
}
|
|
11324
11478
|
return [];
|
|
11325
11479
|
case "datetime":
|
|
11326
11480
|
if (typeof value !== "string" || value !== "" && Number.isNaN(Date.parse(value))) {
|
|
11327
|
-
return [{ fieldPath:
|
|
11481
|
+
return [{ fieldPath: path28, message: "Expected a valid date/time string" }];
|
|
11328
11482
|
}
|
|
11329
11483
|
return [];
|
|
11330
11484
|
case "select": {
|
|
11331
11485
|
if (typeof value !== "string") {
|
|
11332
|
-
return [{ fieldPath:
|
|
11486
|
+
return [{ fieldPath: path28, message: "Expected a selection" }];
|
|
11333
11487
|
}
|
|
11334
11488
|
const options = selectOptionValues(field);
|
|
11335
11489
|
if (value !== "" && !options.includes(value)) {
|
|
11336
|
-
return [{ fieldPath:
|
|
11490
|
+
return [{ fieldPath: path28, message: `Must be one of: ${options.join(", ")}` }];
|
|
11337
11491
|
}
|
|
11338
11492
|
return [];
|
|
11339
11493
|
}
|
|
11340
11494
|
case "reference":
|
|
11341
11495
|
if (referenceValueId(value) === void 0) {
|
|
11342
|
-
return [{ fieldPath:
|
|
11496
|
+
return [{ fieldPath: path28, message: "Expected a reference id" }];
|
|
11343
11497
|
}
|
|
11344
11498
|
return [];
|
|
11345
11499
|
case "image":
|
|
11346
|
-
return validateImageValue(field, value,
|
|
11500
|
+
return validateImageValue(field, value, path28);
|
|
11347
11501
|
default:
|
|
11348
11502
|
if (STRING_FIELD_TYPES.has(field.type)) {
|
|
11349
11503
|
if (typeof value !== "string") {
|
|
11350
|
-
return [{ fieldPath:
|
|
11504
|
+
return [{ fieldPath: path28, message: "Expected text" }];
|
|
11351
11505
|
}
|
|
11352
11506
|
}
|
|
11353
11507
|
return [];
|
|
@@ -11370,12 +11524,12 @@ function validateEntryData(fields, data, pathPrefix = "", options = {}) {
|
|
|
11370
11524
|
errors.push(...validateEntryData(field.fields, data, pathPrefix, options));
|
|
11371
11525
|
continue;
|
|
11372
11526
|
}
|
|
11373
|
-
const
|
|
11527
|
+
const path28 = pathPrefix ? `${pathPrefix}.${field.name}` : field.name;
|
|
11374
11528
|
const value = data[field.name];
|
|
11375
11529
|
const isRequired = options.required !== false && "required" in field && field.required === true;
|
|
11376
11530
|
if (isRequired && isEmptyForRequired(field, value)) {
|
|
11377
11531
|
const message = Array.isArray(value) ? "At least one item is required" : "This field is required";
|
|
11378
|
-
errors.push({ fieldPath:
|
|
11532
|
+
errors.push({ fieldPath: path28, message });
|
|
11379
11533
|
continue;
|
|
11380
11534
|
}
|
|
11381
11535
|
if (value === void 0 || value === null)
|
|
@@ -11383,7 +11537,7 @@ function validateEntryData(fields, data, pathPrefix = "", options = {}) {
|
|
|
11383
11537
|
if (Array.isArray(value) && "maxItems" in field && typeof field.maxItems === "number" && value.length > field.maxItems) {
|
|
11384
11538
|
const cap = field.maxItems;
|
|
11385
11539
|
errors.push({
|
|
11386
|
-
fieldPath:
|
|
11540
|
+
fieldPath: path28,
|
|
11387
11541
|
message: `At most ${cap} ${cap === 1 ? "item" : "items"}; this list has ${value.length}`,
|
|
11388
11542
|
maxItems: { count: value.length, cap }
|
|
11389
11543
|
});
|
|
@@ -11391,11 +11545,11 @@ function validateEntryData(fields, data, pathPrefix = "", options = {}) {
|
|
|
11391
11545
|
if (field.type === "block") {
|
|
11392
11546
|
const blockField = field;
|
|
11393
11547
|
if (!Array.isArray(value)) {
|
|
11394
|
-
errors.push({ fieldPath:
|
|
11548
|
+
errors.push({ fieldPath: path28, message: "Expected a list of blocks" });
|
|
11395
11549
|
continue;
|
|
11396
11550
|
}
|
|
11397
11551
|
value.forEach((item, index) => {
|
|
11398
|
-
const itemPath = `${
|
|
11552
|
+
const itemPath = `${path28}[${index}]`;
|
|
11399
11553
|
if (!isPlainRecord3(item)) {
|
|
11400
11554
|
errors.push({ fieldPath: itemPath, message: "Expected a block object" });
|
|
11401
11555
|
return;
|
|
@@ -11417,11 +11571,11 @@ function validateEntryData(fields, data, pathPrefix = "", options = {}) {
|
|
|
11417
11571
|
const objectField = field;
|
|
11418
11572
|
if (objectField.list) {
|
|
11419
11573
|
if (!Array.isArray(value)) {
|
|
11420
|
-
errors.push({ fieldPath:
|
|
11574
|
+
errors.push({ fieldPath: path28, message: "Expected a list of items" });
|
|
11421
11575
|
continue;
|
|
11422
11576
|
}
|
|
11423
11577
|
value.forEach((item, index) => {
|
|
11424
|
-
const itemPath = `${
|
|
11578
|
+
const itemPath = `${path28}[${index}]`;
|
|
11425
11579
|
if (!isPlainRecord3(item)) {
|
|
11426
11580
|
errors.push({ fieldPath: itemPath, message: "Expected an object" });
|
|
11427
11581
|
return;
|
|
@@ -11431,27 +11585,27 @@ function validateEntryData(fields, data, pathPrefix = "", options = {}) {
|
|
|
11431
11585
|
}
|
|
11432
11586
|
});
|
|
11433
11587
|
} else if (!isPlainRecord3(value)) {
|
|
11434
|
-
errors.push({ fieldPath:
|
|
11588
|
+
errors.push({ fieldPath: path28, message: "Expected an object" });
|
|
11435
11589
|
} else if (objectField.fields) {
|
|
11436
|
-
errors.push(...validateEntryData(objectField.fields, value,
|
|
11590
|
+
errors.push(...validateEntryData(objectField.fields, value, path28, options));
|
|
11437
11591
|
}
|
|
11438
11592
|
continue;
|
|
11439
11593
|
}
|
|
11440
11594
|
if ("list" in field && field.list) {
|
|
11441
11595
|
if (!Array.isArray(value)) {
|
|
11442
|
-
errors.push({ fieldPath:
|
|
11596
|
+
errors.push({ fieldPath: path28, message: "Expected a list of values" });
|
|
11443
11597
|
continue;
|
|
11444
11598
|
}
|
|
11445
11599
|
value.forEach((item, index) => {
|
|
11446
|
-
errors.push(...validateScalar(field, item, `${
|
|
11600
|
+
errors.push(...validateScalar(field, item, `${path28}[${index}]`));
|
|
11447
11601
|
});
|
|
11448
11602
|
continue;
|
|
11449
11603
|
}
|
|
11450
11604
|
if (Array.isArray(value) && KNOWN_FIELD_TYPES.has(field.type)) {
|
|
11451
|
-
errors.push({ fieldPath:
|
|
11605
|
+
errors.push({ fieldPath: path28, message: "Expected a single value, not a list" });
|
|
11452
11606
|
continue;
|
|
11453
11607
|
}
|
|
11454
|
-
errors.push(...validateScalar(field, value,
|
|
11608
|
+
errors.push(...validateScalar(field, value, path28));
|
|
11455
11609
|
}
|
|
11456
11610
|
return errors;
|
|
11457
11611
|
}
|
|
@@ -11653,33 +11807,50 @@ ${cached.issues.map((issue) => ` - ${issue.message}`).join("\n")}`);
|
|
|
11653
11807
|
|
|
11654
11808
|
// dist/cli/generate-ai-content.js
|
|
11655
11809
|
init_error();
|
|
11810
|
+
|
|
11811
|
+
// dist/cli/site-config.js
|
|
11812
|
+
import path26 from "node:path";
|
|
11813
|
+
import { createJiti } from "jiti";
|
|
11656
11814
|
var jiti = createJiti(import.meta.url);
|
|
11815
|
+
async function importSiteModule(filePath) {
|
|
11816
|
+
return await jiti.import(filePath);
|
|
11817
|
+
}
|
|
11818
|
+
async function importSiteServerConfig(projectDir) {
|
|
11819
|
+
const configPath = path26.join(projectDir, "canopycms.config.ts");
|
|
11820
|
+
const mod = await importSiteModule(configPath);
|
|
11821
|
+
const configExport = mod.default ?? mod.config ?? mod;
|
|
11822
|
+
const server = typeof configExport === "object" && configExport !== null && "server" in configExport ? configExport.server : configExport;
|
|
11823
|
+
return { configPath, server };
|
|
11824
|
+
}
|
|
11825
|
+
async function importSiteEntrySchemaRegistry(projectDir, appDir) {
|
|
11826
|
+
const schemasPath = path26.join(projectDir, appDir, "schemas.ts");
|
|
11827
|
+
const mod = await importSiteModule(schemasPath);
|
|
11828
|
+
const registry2 = mod.entrySchemaRegistry ?? mod;
|
|
11829
|
+
return { schemasPath, registry: registry2 };
|
|
11830
|
+
}
|
|
11831
|
+
|
|
11832
|
+
// dist/cli/generate-ai-content.js
|
|
11657
11833
|
async function generateAIContentCLI(options) {
|
|
11658
11834
|
const { projectDir, outputDir = "public/ai", configPath, appDir = "app" } = options;
|
|
11659
11835
|
console.log("\nCanopyCMS generate-ai-content\n");
|
|
11660
|
-
|
|
11661
|
-
let canopyConfigModule;
|
|
11836
|
+
let serverConfig;
|
|
11662
11837
|
try {
|
|
11663
|
-
|
|
11838
|
+
serverConfig = (await importSiteServerConfig(projectDir)).server;
|
|
11664
11839
|
} catch (err) {
|
|
11665
|
-
console.error(`Could not load config from ${
|
|
11840
|
+
console.error(`Could not load config from ${path27.join(projectDir, "canopycms.config.ts")}`);
|
|
11666
11841
|
console.error(getErrorMessage(err));
|
|
11667
11842
|
process.exit(1);
|
|
11668
11843
|
}
|
|
11669
|
-
const configExport = canopyConfigModule.default ?? canopyConfigModule.config ?? canopyConfigModule;
|
|
11670
|
-
const serverConfig = typeof configExport === "object" && configExport !== null && "server" in configExport ? configExport.server : configExport;
|
|
11671
|
-
const schemasPath = path26.join(projectDir, appDir, "schemas.ts");
|
|
11672
11844
|
let entrySchemaRegistry = {};
|
|
11673
11845
|
try {
|
|
11674
|
-
|
|
11675
|
-
entrySchemaRegistry = schemasModule.entrySchemaRegistry ?? schemasModule;
|
|
11846
|
+
entrySchemaRegistry = (await importSiteEntrySchemaRegistry(projectDir, appDir)).registry;
|
|
11676
11847
|
} catch {
|
|
11677
11848
|
console.warn(` No ${appDir}/schemas.ts found, using empty entry schema registry`);
|
|
11678
11849
|
}
|
|
11679
11850
|
let aiConfig;
|
|
11680
11851
|
if (configPath) {
|
|
11681
11852
|
try {
|
|
11682
|
-
const aiConfigModule = await
|
|
11853
|
+
const aiConfigModule = await importSiteModule(path27.resolve(configPath));
|
|
11683
11854
|
aiConfig = aiConfigModule.aiContentConfig ?? aiConfigModule.default ?? aiConfigModule.config;
|
|
11684
11855
|
} catch (err) {
|
|
11685
11856
|
console.error(`Could not load AI config from ${configPath}`);
|
|
@@ -11696,7 +11867,7 @@ async function generateAIContentCLI(options) {
|
|
|
11696
11867
|
console.error("Make sure canopycms.config.ts uses defineCanopyConfig().");
|
|
11697
11868
|
process.exit(1);
|
|
11698
11869
|
}
|
|
11699
|
-
const resolvedOutput =
|
|
11870
|
+
const resolvedOutput = path27.resolve(projectDir, outputDir);
|
|
11700
11871
|
console.log(` Output: ${resolvedOutput}`);
|
|
11701
11872
|
console.log(` Mode: ${serverConfig.mode ?? "dev"}`);
|
|
11702
11873
|
const result = await generateAIContentFiles({
|