canopycms 0.12.0 → 0.13.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/CHANGELOG.md +326 -48
- package/dist/api/admin.d.ts +4 -4
- package/dist/api/branch-review.d.ts +20 -2
- package/dist/api/branch-review.js +153 -32
- package/dist/api/branch-status.d.ts +23 -2
- package/dist/api/branch-status.js +16 -2
- package/dist/api/branch-update-from-base.d.ts +23 -0
- package/dist/api/branch-update-from-base.js +227 -0
- package/dist/api/branch.d.ts +10 -0
- package/dist/api/branch.js +28 -2
- package/dist/api/client.d.ts +15 -2
- package/dist/api/client.js +24 -2
- 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 +235 -0
- package/dist/api/merge-conflicts.js +709 -0
- package/dist/api/routes.js +2 -0
- package/dist/api/types.d.ts +8 -3
- package/dist/base-update.d.ts +32 -0
- package/dist/base-update.js +31 -0
- package/dist/branch-metadata-file.d.ts +66 -0
- package/dist/branch-metadata-file.js +16 -1
- 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 +9798 -1509
- package/dist/cli/generate-ai-content.js +373 -249
- package/dist/cli/github-app-manifest.d.ts +4 -1
- package/dist/cli/github-app-manifest.js +5 -1
- package/dist/cli/init.js +12 -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 +39 -3
- package/dist/content-store.js +48 -15
- package/dist/editor/BranchManager.d.ts +4 -1
- package/dist/editor/BranchManager.js +9 -5
- package/dist/editor/Editor.js +462 -62
- 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/admin/ConflictsStatusAlert.d.ts +8 -0
- package/dist/editor/admin/ConflictsStatusAlert.js +22 -0
- package/dist/editor/admin/SystemHealthPanel.js +4 -3
- package/dist/editor/base-update-wait.d.ts +78 -0
- package/dist/editor/base-update-wait.js +98 -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 +112 -0
- package/dist/editor/components/ConflictsGroup.js +404 -0
- package/dist/editor/components/EditorHeader.d.ts +18 -1
- package/dist/editor/components/EditorHeader.js +99 -33
- package/dist/editor/components/EntryChangesDrawer.d.ts +16 -2
- package/dist/editor/components/EntryChangesDrawer.js +85 -167
- package/dist/editor/components/EntryHeader.d.ts +3 -1
- package/dist/editor/components/EntryHeader.js +2 -2
- 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 +93 -0
- package/dist/editor/conflict-cards.js +284 -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 +12 -0
- package/dist/editor/copy.js +12 -0
- package/dist/editor/entry-diff.d.ts +2 -1
- package/dist/editor/entry-diff.js +5 -21
- package/dist/editor/hooks/useBaseUpdate.d.ts +38 -0
- package/dist/editor/hooks/useBaseUpdate.js +279 -0
- package/dist/editor/hooks/useBranchChangesData.d.ts +7 -1
- package/dist/editor/hooks/useBranchChangesData.js +13 -0
- package/dist/editor/hooks/useBranchManager.d.ts +8 -1
- package/dist/editor/hooks/useBranchManager.js +108 -13
- 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 +13 -1
- package/dist/editor/hooks/useDraftManager.js +40 -9
- package/dist/editor/hooks/useEntryManager.d.ts +17 -3
- package/dist/editor/hooks/useEntryManager.js +11 -5
- 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 +7 -0
- package/dist/editor/undo-change.js +19 -4
- package/dist/git-manager.d.ts +6 -0
- package/dist/git-manager.js +38 -3
- 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 +876 -0
- package/dist/merge/list-alignment.d.ts +55 -0
- package/dist/merge/list-alignment.js +490 -0
- package/dist/merge/rekey.d.ts +11 -0
- package/dist/merge/rekey.js +75 -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/client-unsafe-strategy.js +6 -0
- package/dist/operating-mode/types.d.ts +13 -0
- package/dist/services.d.ts +24 -1
- package/dist/services.js +138 -22
- package/dist/task-queue/cms-task-queue.d.ts +3 -1
- package/dist/types.d.ts +63 -5
- 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/git.d.ts +7 -0
- package/dist/utils/git.js +35 -0
- 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 +80 -40
- package/dist/worker/branch-merge.js +244 -58
- package/dist/worker/branch-policy.d.ts +18 -2
- package/dist/worker/branch-policy.js +36 -0
- package/dist/worker/cms-worker.d.ts +37 -3
- package/dist/worker/cms-worker.js +88 -16
- package/dist/worker/conflicts-status.d.ts +20 -0
- package/dist/worker/conflicts-status.js +204 -0
- package/dist/worker/field-merge.d.ts +80 -0
- package/dist/worker/field-merge.js +729 -0
- package/dist/worker/git-sync.d.ts +3 -2
- package/dist/worker/git-sync.js +16 -6
- package/dist/worker/github-commits.d.ts +42 -10
- package/dist/worker/github-commits.js +444 -52
- package/dist/worker/github-errors.d.ts +15 -0
- package/dist/worker/github-errors.js +47 -0
- package/dist/worker/github-gateway-http.js +24 -9
- package/dist/worker/github-gateway-wire.d.ts +49 -20
- package/dist/worker/github-gateway-wire.js +19 -3
- package/dist/worker/github-gateway.d.ts +27 -1
- package/dist/worker/github-gateway.js +55 -7
- package/dist/worker/github-transfer.d.ts +5 -3
- package/dist/worker/github-transfer.js +8 -5
- package/dist/worker/history-rewrite.d.ts +40 -8
- package/dist/worker/history-rewrite.js +51 -23
- package/dist/worker/publish-decision.d.ts +37 -0
- package/dist/worker/publish-decision.js +16 -0
- package/dist/worker/rebase.d.ts +29 -8
- package/dist/worker/rebase.js +492 -211
- package/dist/worker/task-runner.d.ts +1 -1
- package/dist/worker/task-runner.js +30 -41
- package/dist/worker/worker-context.d.ts +24 -2
- package/package.json +1 -1
package/dist/services.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import fs from 'node:fs/promises';
|
|
19
19
|
import path from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
20
21
|
import { getConfigDefaults } from './config/index.js';
|
|
21
22
|
import { createCheckBranchAccess, createCheckContentAccess, createContentAccessChecker, loadPathPermissions, } from './authorization/index.js';
|
|
22
23
|
import { GitManager, GitRemoteRefMissingError } from './git-manager.js';
|
|
@@ -30,14 +31,15 @@ import { enqueueTask } from './task-queue/cms-task-queue.js';
|
|
|
30
31
|
import { getTaskQueueDir } from './task-queue/task-queue-config.js';
|
|
31
32
|
import { baseBranchOf } from './utils/base-branch.js';
|
|
32
33
|
import { shortNameOf } from './branch-param.js';
|
|
33
|
-
import { detectHeadBranch, isCanopyInternalPath } from './utils/git.js';
|
|
34
|
+
import { detectHeadBranch, isCanopyInternalPath, isRebaseInProgress } from './utils/git.js';
|
|
34
35
|
import { readsFromCheckout } from './build-mode.js';
|
|
35
36
|
import { timeRequestPhase } from './utils/request-timing.js';
|
|
36
37
|
import { appendTrailers, buildEditorTrailers, describeEditors, MAX_LISTED_ENTRIES, } from './submission-attribution.js';
|
|
37
38
|
import { getBranchMetadataFileManager, recordBranchEditor } from './branch-metadata.js';
|
|
38
39
|
import { readBranchMetadataFile } from './branch-metadata-file.js';
|
|
39
40
|
import { getErrorMessage, redactCredentials } from './utils/error.js';
|
|
40
|
-
import { withContentWriteLock } from './utils/content-write-lock.js';
|
|
41
|
+
import { ContentWriteLockBusyError, withContentWriteLock } from './utils/content-write-lock.js';
|
|
42
|
+
import { assertNoOpenConflicts } from './merge-conflicts.js';
|
|
41
43
|
import { ContentStore } from './content-store.js';
|
|
42
44
|
import { entryLogicalPathOfFile } from './content-id-index.js';
|
|
43
45
|
import { stampNewEntries } from './stamp-on-publish.js';
|
|
@@ -138,6 +140,33 @@ export const createTestCanopyServices = async (config, options = {}) => {
|
|
|
138
140
|
};
|
|
139
141
|
return _createCanopyServicesInternal(testConfig, options);
|
|
140
142
|
};
|
|
143
|
+
/**
|
|
144
|
+
* A remote URL's local path as git reads it: a `file://` URL, a drive path, or a path relative to
|
|
145
|
+
* `cwd` with its links resolved; undefined for a scheme or git's scp form (a colon before any
|
|
146
|
+
* slash), which name another host.
|
|
147
|
+
*/
|
|
148
|
+
const localPathOf = async (url, cwd) => {
|
|
149
|
+
if (url.startsWith('file://')) {
|
|
150
|
+
try {
|
|
151
|
+
return fileURLToPath(url);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (process.platform === 'win32' && /^[A-Za-z]:[\\/]/.test(url))
|
|
158
|
+
return path.resolve(url);
|
|
159
|
+
const colon = url.indexOf(':');
|
|
160
|
+
const slash = url.search(/[\\/]/);
|
|
161
|
+
if (colon >= 0 && (slash < 0 || colon < slash))
|
|
162
|
+
return undefined;
|
|
163
|
+
return path.resolve(await fs.realpath(cwd).catch(() => cwd), url);
|
|
164
|
+
};
|
|
165
|
+
/** Whether two paths name one directory, symlinks resolved where they exist. */
|
|
166
|
+
const samePath = async (a, b) => {
|
|
167
|
+
const real = (p) => fs.realpath(p).catch(() => path.resolve(p));
|
|
168
|
+
return (await real(a)) === (await real(b));
|
|
169
|
+
};
|
|
141
170
|
/** Shared implementation; call createCanopyServices or createTestCanopyServices. */
|
|
142
171
|
async function _createCanopyServicesInternal(config, options) {
|
|
143
172
|
// Mode-specific requirements, e.g. prod's git bot credentials for GitHub.
|
|
@@ -213,10 +242,81 @@ async function _createCanopyServicesInternal(config, options) {
|
|
|
213
242
|
// lands on its detached head with the rebase's half-resolved index, and the
|
|
214
243
|
// rebase's `--abort` discards it.
|
|
215
244
|
await withContentWriteLock(options.context.branchRoot, async () => {
|
|
245
|
+
await refuseDuringRebase(options.context.branchRoot);
|
|
216
246
|
await git.add(options.files);
|
|
217
247
|
await git.commit(options.message);
|
|
218
248
|
});
|
|
219
249
|
};
|
|
250
|
+
/**
|
|
251
|
+
* Inside the content-write lock, before a commit: a rebase left by a worker that died holds no
|
|
252
|
+
* lock, and its recovery (`rebase --abort`, worker/rebase.ts) would reset the branch past the
|
|
253
|
+
* commit, silently. In prod the worker clears it on its next cycle, so this is a retry.
|
|
254
|
+
*/
|
|
255
|
+
const refuseDuringRebase = async (branchRoot) => {
|
|
256
|
+
if (await isRebaseInProgress(branchRoot))
|
|
257
|
+
throw new ContentWriteLockBusyError();
|
|
258
|
+
};
|
|
259
|
+
/**
|
|
260
|
+
* Commit every saved change but canopycms's own state, if there is one, with trailers naming
|
|
261
|
+
* `credited` and then the editors who saved since the last commit. Runs inside the caller's
|
|
262
|
+
* content-write lock. Resolves whether it committed.
|
|
263
|
+
*/
|
|
264
|
+
const commitSavedChanges = async (git, status, commit) => {
|
|
265
|
+
if (!status.files.some((f) => !isCanopyInternalPath(f.path)))
|
|
266
|
+
return false;
|
|
267
|
+
await git.addAllExceptCanopyState();
|
|
268
|
+
const committers = commit.uncommitted.map((userId) => commit.named.get(userId) ?? { userId });
|
|
269
|
+
const trailers = buildEditorTrailers(commit.credited ? [commit.credited, ...committers] : committers, {
|
|
270
|
+
editedBy: config.gitEditedByTrailers ?? true,
|
|
271
|
+
coAuthoredBy: config.gitCoAuthoredByTrailers ?? false,
|
|
272
|
+
});
|
|
273
|
+
await git.commit(appendTrailers(commit.message, trailers));
|
|
274
|
+
return true;
|
|
275
|
+
};
|
|
276
|
+
/** Clear the editors a commit named; a failure leaves them for the next commit to name again. */
|
|
277
|
+
const markCommitted = async (context, uncommitted) => {
|
|
278
|
+
if (uncommitted.length === 0)
|
|
279
|
+
return;
|
|
280
|
+
try {
|
|
281
|
+
await getBranchMetadataFileManager(context.branchRoot, context.baseRoot).markEditorsCommitted(uncommitted);
|
|
282
|
+
}
|
|
283
|
+
catch (err) {
|
|
284
|
+
console.warn(`CanopyCMS: Could not clear the committed editors of ${context.branch.name}:`, getErrorMessage(err));
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
/** The editors recorded on the branch, each with the name `lookupEditor` gives. */
|
|
288
|
+
const namedEditors = async (context, lookupEditor) => new Map((await describeEditors((await readRecordedEditors(context))?.editors ?? [], memoizeLookup(lookupEditor))).map((e) => [e.userId, e]));
|
|
289
|
+
const commitSavedWork = async (options) => {
|
|
290
|
+
const git = createGitManagerFor(options.context.branchRoot, {
|
|
291
|
+
baseBranch: options.context.branch.baseBranch,
|
|
292
|
+
});
|
|
293
|
+
await git.ensureAuthor({ name: config.gitBotAuthorName, email: config.gitBotAuthorEmail });
|
|
294
|
+
// Names are looked up before taking the lock, so a slow auth provider never holds it.
|
|
295
|
+
const named = await namedEditors(options.context, options.lookupEditor);
|
|
296
|
+
// [SYNC-C1] Locked for the same reason as submitBranch: a commit landing while a rebase is
|
|
297
|
+
// stopped on a conflict is discarded by its abort.
|
|
298
|
+
const committed = await withContentWriteLock(options.context.branchRoot, async () => {
|
|
299
|
+
await refuseDuringRebase(options.context.branchRoot);
|
|
300
|
+
// A submit since the caller's read: a branch in review takes no commit until it is withdrawn.
|
|
301
|
+
const branchStatus = (await readBranchMetadataFile(options.context.branchRoot))?.branch.status;
|
|
302
|
+
if (branchStatus !== undefined && branchStatus !== 'editing')
|
|
303
|
+
return false;
|
|
304
|
+
await git.checkoutBranch(options.context.branch.name);
|
|
305
|
+
const status = await git.status();
|
|
306
|
+
const uncommitted = (await readRecordedEditors(options.context))?.uncommittedEditors ?? [];
|
|
307
|
+
const done = await commitSavedChanges(git, status, {
|
|
308
|
+
message: options.message,
|
|
309
|
+
uncommitted,
|
|
310
|
+
named,
|
|
311
|
+
});
|
|
312
|
+
// Inside the lock, as submitBranch does: a save after it would record its editor as still
|
|
313
|
+
// uncommitted, and this would then clear them with the save left out of the commit.
|
|
314
|
+
if (done)
|
|
315
|
+
await markCommitted(options.context, uncommitted);
|
|
316
|
+
return done;
|
|
317
|
+
});
|
|
318
|
+
return committed;
|
|
319
|
+
};
|
|
220
320
|
/** Runs inside submitBranch's content-write lock; a stamp that fails never blocks the submit. */
|
|
221
321
|
const stampNewEntriesOrWarn = async (context, git) => {
|
|
222
322
|
const contentRoot = services.config.contentRoot || 'content';
|
|
@@ -286,6 +386,9 @@ async function _createCanopyServicesInternal(config, options) {
|
|
|
286
386
|
let recorded = {};
|
|
287
387
|
let stamped = [];
|
|
288
388
|
const submitted = await withContentWriteLock(options.context.branchRoot, async () => {
|
|
389
|
+
await refuseDuringRebase(options.context.branchRoot);
|
|
390
|
+
// In the lock, as the worker records a merge's conflicts in it before the merge lands.
|
|
391
|
+
await assertNoOpenConflicts(options.context.branchRoot);
|
|
289
392
|
await git.checkoutBranch(options.context.branch.name);
|
|
290
393
|
// Before the status read, so the stamp joins this commit even when an earlier attempt
|
|
291
394
|
// already committed the entry and only its push failed.
|
|
@@ -303,18 +406,13 @@ async function _createCanopyServicesInternal(config, options) {
|
|
|
303
406
|
// branch already had unpushed commits from an earlier attempt.
|
|
304
407
|
// canopycms's own state under .canopy-meta/ is never content, so it neither
|
|
305
408
|
// makes a commit worth creating nor gets staged into one.
|
|
306
|
-
let committed = false;
|
|
307
409
|
const preCommitSha = await git.headSha();
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
});
|
|
315
|
-
await git.commit(appendTrailers(options.message ?? `Submit ${options.context.branch.name}`, trailers));
|
|
316
|
-
committed = true;
|
|
317
|
-
}
|
|
410
|
+
const committed = await commitSavedChanges(git, status, {
|
|
411
|
+
message: options.message ?? `Submit ${options.context.branch.name}`,
|
|
412
|
+
credited: options.submitter,
|
|
413
|
+
uncommitted,
|
|
414
|
+
named,
|
|
415
|
+
});
|
|
318
416
|
// The diff runs after the commit so it covers saved-but-uncommitted edits too, and
|
|
319
417
|
// before the push so a branch with nothing to submit never reaches the remote. A diff
|
|
320
418
|
// that cannot be computed lets the submit through: the list only feeds the PR body, and
|
|
@@ -333,15 +431,8 @@ async function _createCanopyServicesInternal(config, options) {
|
|
|
333
431
|
await git.resetKeepingChanges(preCommitSha);
|
|
334
432
|
throw new NothingToSubmitError(shortNameOf(config, options.context.branch.name), effectiveBase);
|
|
335
433
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
try {
|
|
339
|
-
await getBranchMetadataFileManager(options.context.branchRoot, options.context.baseRoot).markEditorsCommitted(uncommitted);
|
|
340
|
-
}
|
|
341
|
-
catch (err) {
|
|
342
|
-
console.warn(`CanopyCMS: Could not clear the committed editors of ${options.context.branch.name}:`, getErrorMessage(err));
|
|
343
|
-
}
|
|
344
|
-
}
|
|
434
|
+
if (committed)
|
|
435
|
+
await markCommitted(options.context, uncommitted);
|
|
345
436
|
if (committed || (await git.hasUnpushedCommits(options.context.branch.name))) {
|
|
346
437
|
await git.push(options.context.branch.name);
|
|
347
438
|
}
|
|
@@ -450,6 +541,29 @@ async function _createCanopyServicesInternal(config, options) {
|
|
|
450
541
|
};
|
|
451
542
|
}
|
|
452
543
|
};
|
|
544
|
+
const runBaseUpdateNow = async (context) => {
|
|
545
|
+
const mode = services.config.mode;
|
|
546
|
+
const strategy = operatingStrategy(mode);
|
|
547
|
+
if (!strategy.baseUpdateRunsInRequest()) {
|
|
548
|
+
throw new Error(`CanopyCMS: in ${mode} mode the worker answers requests for the latest changes, never a request.`);
|
|
549
|
+
}
|
|
550
|
+
// baseRoot is the workspace's content-branches directory, where the worker looks.
|
|
551
|
+
const workspacePath = path.dirname(context.baseRoot);
|
|
552
|
+
const origin = await createGitManagerFor(context.branchRoot).getRemoteUrl();
|
|
553
|
+
const originPath = origin && (await localPathOf(origin, context.branchRoot));
|
|
554
|
+
if (!originPath || !(await samePath(originPath, path.join(workspacePath, 'remote.git')))) {
|
|
555
|
+
return 'other-remote';
|
|
556
|
+
}
|
|
557
|
+
// Loaded only when a run is asked for: a server that never runs one never loads the worker.
|
|
558
|
+
const { createLocalWorker } = await import('./worker/cms-worker.js');
|
|
559
|
+
const worker = createLocalWorker({
|
|
560
|
+
workspacePath,
|
|
561
|
+
baseBranch: context.branch.baseBranch ?? baseBranchOf(services.config),
|
|
562
|
+
contentRoot: services.config.contentRoot || 'content',
|
|
563
|
+
settingsBranch: strategy.getSettingsBranchName(services.config),
|
|
564
|
+
});
|
|
565
|
+
return worker.syncBranchNow(context.branch.name);
|
|
566
|
+
};
|
|
453
567
|
const operatingMode = config.mode;
|
|
454
568
|
const modeStrategy = operatingStrategy(operatingMode);
|
|
455
569
|
const registry = modeStrategy.supportsBranching()
|
|
@@ -467,7 +581,9 @@ async function _createCanopyServicesInternal(config, options) {
|
|
|
467
581
|
githubService,
|
|
468
582
|
bootstrapAdminIds,
|
|
469
583
|
commitFiles,
|
|
584
|
+
commitSavedWork,
|
|
470
585
|
submitBranch,
|
|
586
|
+
runBaseUpdateNow,
|
|
471
587
|
commitToSettingsBranch,
|
|
472
588
|
getSettingsBranchRoot,
|
|
473
589
|
recordBranchEditor,
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { Task, TaskQueueLogger } from './types.js';
|
|
7
7
|
/** Actions the EC2 worker can execute on behalf of Lambda. */
|
|
8
|
-
export type TaskAction = 'push-and-create-pr' | 'push-and-update-pr' | 'push-and-create-or-update-pr' | 'convert-to-draft' | 'close-pr' | 'delete-remote-branch' | 'push-branch' | 'sync-pr-comments'
|
|
8
|
+
export type TaskAction = 'push-and-create-pr' | 'push-and-update-pr' | 'push-and-create-or-update-pr' | 'convert-to-draft' | 'close-pr' | 'delete-remote-branch' | 'push-branch' | 'sync-pr-comments'
|
|
9
|
+
/** Wake the git sync early for `payload.branch` (base-update.ts). */
|
|
10
|
+
| 'sync-branch';
|
|
9
11
|
/** A task with a CMS-specific action. */
|
|
10
12
|
export type WorkerTask = Task & {
|
|
11
13
|
action: TaskAction;
|
package/dist/types.d.ts
CHANGED
|
@@ -49,6 +49,8 @@ export interface BranchReview {
|
|
|
49
49
|
};
|
|
50
50
|
/** ISO timestamp of the verdict. */
|
|
51
51
|
at: string;
|
|
52
|
+
/** The head commit the reviewer's page showed, which the API checked against the clone's. */
|
|
53
|
+
commit?: string;
|
|
52
54
|
}
|
|
53
55
|
/** What keeps a pull request behind its base. `base` is the branch GitHub says it targets now. */
|
|
54
56
|
export type PullRequestHold = {
|
|
@@ -108,16 +110,20 @@ export interface BranchMetadata {
|
|
|
108
110
|
*/
|
|
109
111
|
pushedToGitHubAt?: string;
|
|
110
112
|
/**
|
|
111
|
-
* The
|
|
112
|
-
*
|
|
113
|
-
* comment summary with config.editorUrl or this, so a commenter never supplies the
|
|
113
|
+
* The editor URL the latest submit resolved (editor-url.ts): config.editorUrl, else one taken
|
|
114
|
+
* from its request; a submit that resolved none leaves it as it was. A comment change re-renders
|
|
115
|
+
* the PR's comment summary with config.editorUrl or this, so a commenter never supplies the
|
|
116
|
+
* links. The worker links the conflicts status with it (worker/conflicts-status.ts).
|
|
114
117
|
*/
|
|
115
118
|
editorUrl?: string;
|
|
116
119
|
/** Sync status for async GitHub operations (used when Lambda has no internet) */
|
|
117
120
|
syncStatus?: SyncStatus;
|
|
118
|
-
/** Whether
|
|
121
|
+
/** Whether {@link conflictFiles} names any entry */
|
|
119
122
|
conflictStatus?: ConflictStatus;
|
|
120
|
-
/**
|
|
123
|
+
/**
|
|
124
|
+
* ContentIds of the entries with a merge conflict a person has yet to resolve, derived from the
|
|
125
|
+
* clone's merge-conflicts.json (merge-conflicts.ts); cleared when the branch merges
|
|
126
|
+
*/
|
|
121
127
|
conflictFiles?: ContentId[];
|
|
122
128
|
/**
|
|
123
129
|
* Lifecycle state of the branch's PR as last observed by the worker's merge-poll
|
|
@@ -162,6 +168,12 @@ export interface BranchMetadata {
|
|
|
162
168
|
* rebase loop's self-heal pass.
|
|
163
169
|
*/
|
|
164
170
|
historyRewrittenFrom?: string;
|
|
171
|
+
/**
|
|
172
|
+
* The rebased tip of the latest rewrite while `historyRewrittenFrom` is set: the one commit a
|
|
173
|
+
* rewrite's publish moves `remote.git` to, since the clone can hold a commit made after it that
|
|
174
|
+
* no one has submitted. Advanced by each rewrite, cleared with the marker.
|
|
175
|
+
*/
|
|
176
|
+
historyRewrittenTo?: string;
|
|
165
177
|
/**
|
|
166
178
|
* The newest commit this deployment has seen on GitHub's branch, or knows it will hold once a
|
|
167
179
|
* queued push of a rewrite lands: the last one it pushed, took in or published, and any
|
|
@@ -217,6 +229,35 @@ export interface BranchMetadata {
|
|
|
217
229
|
uncommittedEditors?: CanopyUserId[];
|
|
218
230
|
/** The reviewer's verdict on the last submit; absent until a reviewer acts. */
|
|
219
231
|
review?: BranchReview;
|
|
232
|
+
/**
|
|
233
|
+
* When someone last asked for the latest changes (api/branch-update-from-base.ts). The request
|
|
234
|
+
* stands while this is newer than `lastBaseUpdate.at`; the editor holds the form read-only and
|
|
235
|
+
* polls meanwhile, and the worker answers it even after a restart lost its wake-up.
|
|
236
|
+
*/
|
|
237
|
+
baseUpdateRequestedAt?: string;
|
|
238
|
+
/** The worker's answer to the latest request for the latest changes (worker/rebase.ts). */
|
|
239
|
+
lastBaseUpdate?: LastBaseUpdate;
|
|
240
|
+
/**
|
|
241
|
+
* The first base commit the worker's automatic sync left out because the branch holds saved but
|
|
242
|
+
* uncommitted work, which it never commits (worker/rebase.ts). The editor shows that the latest
|
|
243
|
+
* changes are waiting; cleared once the sync finds the branch holding the base, or in review.
|
|
244
|
+
*/
|
|
245
|
+
latestChangesWaiting?: string;
|
|
246
|
+
}
|
|
247
|
+
/** How a requested update from the base went. */
|
|
248
|
+
export type BaseUpdateOutcome = 'merged' | 'up-to-date' | 'conflicts' | 'failed';
|
|
249
|
+
export interface LastBaseUpdate {
|
|
250
|
+
/**
|
|
251
|
+
* When the GitHub fetch began that the branch was brought up to, as the gateway reports it
|
|
252
|
+
* (worker/github-gateway.ts, `GitHubFetchResult.startedAt`): a request is answered only from a
|
|
253
|
+
* fetch begun at or after it, so a cycle already running when it arrived never ends it.
|
|
254
|
+
*/
|
|
255
|
+
at: string;
|
|
256
|
+
outcome: BaseUpdateOutcome;
|
|
257
|
+
/** The base commit the branch now contains; absent when it failed before reading one. */
|
|
258
|
+
baseSha?: string;
|
|
259
|
+
/** For `conflicts`: how many entries the merge left a conflict in for a person. */
|
|
260
|
+
keptEntries?: number;
|
|
220
261
|
}
|
|
221
262
|
export interface BranchPaths {
|
|
222
263
|
/** Root where all branches live (e.g., /mnt/efs/site, ~/.canopycms/branches) */
|
|
@@ -308,6 +349,21 @@ export interface UnpublishableBranchesReport {
|
|
|
308
349
|
count: number;
|
|
309
350
|
at: string;
|
|
310
351
|
}
|
|
352
|
+
/**
|
|
353
|
+
* The last git sync's failures to set the "CanopyCMS conflicts" status on pull request heads
|
|
354
|
+
* (worker/conflicts-status.ts): `permission` when GitHub refused the credential (it lacks the Commit
|
|
355
|
+
* statuses permission), `unreachable` when GitHub or the gateway could not be reached or GitHub was
|
|
356
|
+
* busy, `other` for anything else, a gateway refusal or an unreadable conflict record included.
|
|
357
|
+
* `branches` lists at most 50 names, `count` all of them.
|
|
358
|
+
*/
|
|
359
|
+
export interface ConflictsStatusFailure {
|
|
360
|
+
reason: 'permission' | 'unreachable' | 'other';
|
|
361
|
+
/** [REDACT] Served to the admin panel, so sanitized. */
|
|
362
|
+
message: string;
|
|
363
|
+
branches: string[];
|
|
364
|
+
count: number;
|
|
365
|
+
at: string;
|
|
366
|
+
}
|
|
311
367
|
/**
|
|
312
368
|
* Wire shape of the worker's self-reported status file (worker-status.json, under
|
|
313
369
|
* the task queue dir), written by the CmsWorker daemon. Read-only here: GET
|
|
@@ -406,6 +462,8 @@ export interface WorkerStatusReport {
|
|
|
406
462
|
* into a restarted worker's first snapshot, so the bound survives restarts.
|
|
407
463
|
*/
|
|
408
464
|
baseHold?: BaseSchemaHold;
|
|
465
|
+
/** Present while the last git sync failed to set a conflicts status; not carried over a restart. */
|
|
466
|
+
conflictsStatusFailure?: ConflictsStatusFailure;
|
|
409
467
|
/**
|
|
410
468
|
* How the last worker that ran stopped, carried into each new worker's snapshot
|
|
411
469
|
* (task-queue/worker-status.ts's `readCarriedOverStatus`). A worker that fails at startup
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* (validation/entry-validator.ts) at the API boundary, not by a schema-blind serialiser.
|
|
18
18
|
*/
|
|
19
19
|
import type { FieldConfig } from '../config/index.js';
|
|
20
|
-
import {
|
|
20
|
+
import type { MarkdownBodyFormat } from './markdown-parse.js';
|
|
21
21
|
/**
|
|
22
22
|
* Serialise entry data as a YAML file, carrying the comments of `existingRaw` through.
|
|
23
23
|
*
|
|
@@ -39,3 +39,20 @@ export declare function serializeYaml(data: Record<string, unknown>, existingRaw
|
|
|
39
39
|
* what the read path reads, so a `date` field's bare date counts as the day it writes.
|
|
40
40
|
*/
|
|
41
41
|
export declare function serializeFrontmatter(editorBody: string, data: Record<string, unknown>, existingRaw: string | undefined, format: MarkdownBodyFormat, fields?: readonly FieldConfig[]): string;
|
|
42
|
+
/** What a save hands the serialiser: `ContentStore.write`'s input, less its version token. */
|
|
43
|
+
export type SerializableEntry = {
|
|
44
|
+
format: MarkdownBodyFormat;
|
|
45
|
+
data?: Record<string, unknown>;
|
|
46
|
+
body: string;
|
|
47
|
+
} | {
|
|
48
|
+
format: 'json';
|
|
49
|
+
data: Record<string, unknown>;
|
|
50
|
+
} | {
|
|
51
|
+
format: 'yaml';
|
|
52
|
+
data: Record<string, unknown>;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* An entry's file text as a save writes it: `entry` serialised onto `existingRaw`, the file's
|
|
56
|
+
* current text (absent for a new file). `fields` is the entry type's schema.
|
|
57
|
+
*/
|
|
58
|
+
export declare function serializeEntry(entry: SerializableEntry, existingRaw: string | undefined, fields: readonly FieldConfig[]): string;
|
|
@@ -24,6 +24,7 @@ import { ITEM_KEY, isItemKey } from '../item-keys.js';
|
|
|
24
24
|
import { withCalendarDays } from './calendar-day.js';
|
|
25
25
|
import { createDebugLogger } from './debug.js';
|
|
26
26
|
import { getErrorMessage } from './error.js';
|
|
27
|
+
import { serializeJson } from './json-source-splice.js';
|
|
27
28
|
import { preserveMarkdownSource } from './markdown-body-splice.js';
|
|
28
29
|
import { snapshotDocument, spliceSource, withSourceLineEndings } from './yaml-source-splice.js';
|
|
29
30
|
const log = createDebugLogger({ prefix: 'ContentSerialize' });
|
|
@@ -681,3 +682,14 @@ export function serializeFrontmatter(editorBody, data, existingRaw, format, fiel
|
|
|
681
682
|
log.debug('content-serialize', 'reconciled frontmatter reads back differently; writing as gray-matter prints it');
|
|
682
683
|
return matter.stringify(file, data);
|
|
683
684
|
}
|
|
685
|
+
/**
|
|
686
|
+
* An entry's file text as a save writes it: `entry` serialised onto `existingRaw`, the file's
|
|
687
|
+
* current text (absent for a new file). `fields` is the entry type's schema.
|
|
688
|
+
*/
|
|
689
|
+
export function serializeEntry(entry, existingRaw, fields) {
|
|
690
|
+
if (entry.format === 'json')
|
|
691
|
+
return serializeJson(entry.data, existingRaw);
|
|
692
|
+
if (entry.format === 'yaml')
|
|
693
|
+
return serializeYaml(entry.data, existingRaw);
|
|
694
|
+
return serializeFrontmatter(entry.body, entry.data ?? {}, existingRaw, entry.format, fields);
|
|
695
|
+
}
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Production runs a Lambda (API) and an EC2 worker over one EFS filesystem, so `ContentStore`'s
|
|
6
6
|
* in-process mutex (`utils/async-mutex`) does not cover the worker's rebase (`rebaseOneBranch` in
|
|
7
|
-
* worker/rebase.ts) rewriting the same tree. A write landing after `git rebase` started
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* worker/rebase.ts) rewriting the same tree. A write landing after `git rebase` started makes the
|
|
8
|
+
* next replayed commit fail, and the `git rebase --abort` that follows hard-resets the tree,
|
|
9
|
+
* discarding it -- and the editor already got its 200.
|
|
10
10
|
*
|
|
11
11
|
* **Asymmetric by design.** The worker retries every branch on its next sync cycle (~5 minutes)
|
|
12
12
|
* while the editor is a person waiting on a save, so the worker yields and the writer waits:
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Production runs a Lambda (API) and an EC2 worker over one EFS filesystem, so `ContentStore`'s
|
|
6
6
|
* in-process mutex (`utils/async-mutex`) does not cover the worker's rebase (`rebaseOneBranch` in
|
|
7
|
-
* worker/rebase.ts) rewriting the same tree. A write landing after `git rebase` started
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* worker/rebase.ts) rewriting the same tree. A write landing after `git rebase` started makes the
|
|
8
|
+
* next replayed commit fail, and the `git rebase --abort` that follows hard-resets the tree,
|
|
9
|
+
* discarding it -- and the editor already got its 200.
|
|
10
10
|
*
|
|
11
11
|
* **Asymmetric by design.** The worker retries every branch on its next sync cycle (~5 minutes)
|
|
12
12
|
* while the editor is a person waiting on a save, so the worker yields and the writer waits:
|
package/dist/utils/git.d.ts
CHANGED
|
@@ -99,6 +99,13 @@ export declare function stageAllExceptCanopyState(git: SimpleGit): Promise<void>
|
|
|
99
99
|
* for).
|
|
100
100
|
*/
|
|
101
101
|
export declare function isRebaseInProgress(repoPath: string): Promise<boolean>;
|
|
102
|
+
/**
|
|
103
|
+
* The commit HEAD names, read from the files: a detached HEAD's sha, or the branch's loose ref,
|
|
104
|
+
* else its line in `packed-refs`. Null for any other layout, a reftable store among them, which
|
|
105
|
+
* the caller reads with git instead. Files only, as {@link resolveGitDir} is, for the branch
|
|
106
|
+
* listing that reads every clone in review (api/branch.ts).
|
|
107
|
+
*/
|
|
108
|
+
export declare function readHeadCommitFromFiles(repoPath: string): Promise<string | null>;
|
|
102
109
|
/**
|
|
103
110
|
* The repository's current HEAD branch name, or `fallback` when detection fails or HEAD is
|
|
104
111
|
* detached.
|
package/dist/utils/git.js
CHANGED
|
@@ -223,6 +223,41 @@ export async function isRebaseInProgress(repoPath) {
|
|
|
223
223
|
.catch(() => false)));
|
|
224
224
|
return results.some(Boolean);
|
|
225
225
|
}
|
|
226
|
+
const FULL_SHA = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
227
|
+
/**
|
|
228
|
+
* The commit HEAD names, read from the files: a detached HEAD's sha, or the branch's loose ref,
|
|
229
|
+
* else its line in `packed-refs`. Null for any other layout, a reftable store among them, which
|
|
230
|
+
* the caller reads with git instead. Files only, as {@link resolveGitDir} is, for the branch
|
|
231
|
+
* listing that reads every clone in review (api/branch.ts).
|
|
232
|
+
*/
|
|
233
|
+
export async function readHeadCommitFromFiles(repoPath) {
|
|
234
|
+
const gitDir = await resolveGitDir(repoPath);
|
|
235
|
+
if (!gitDir)
|
|
236
|
+
return null;
|
|
237
|
+
try {
|
|
238
|
+
const head = (await fs.readFile(path.join(gitDir, 'HEAD'), 'utf-8')).trim();
|
|
239
|
+
if (FULL_SHA.test(head))
|
|
240
|
+
return head;
|
|
241
|
+
const ref = /^ref: (refs\/heads\/\S+)$/.exec(head)?.[1];
|
|
242
|
+
if (!ref || ref.split('/').includes('..'))
|
|
243
|
+
return null;
|
|
244
|
+
const commonFile = await fs.readFile(path.join(gitDir, 'commondir'), 'utf-8').catch(() => null);
|
|
245
|
+
const refsDir = commonFile ? path.resolve(gitDir, commonFile.trim()) : gitDir;
|
|
246
|
+
const loose = await fs.readFile(path.join(refsDir, ref), 'utf-8').catch(() => null);
|
|
247
|
+
if (loose !== null)
|
|
248
|
+
return FULL_SHA.test(loose.trim()) ? loose.trim() : null;
|
|
249
|
+
const packed = await fs.readFile(path.join(refsDir, 'packed-refs'), 'utf-8').catch(() => '');
|
|
250
|
+
for (const line of packed.split('\n')) {
|
|
251
|
+
const [sha, name] = line.split(' ');
|
|
252
|
+
if (name === ref && FULL_SHA.test(sha))
|
|
253
|
+
return sha;
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
226
261
|
/**
|
|
227
262
|
* The repository's current HEAD branch name, or `fallback` when detection fails or HEAD is
|
|
228
263
|
* detached.
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* then both again from the edit as sent; failing all four, the restyled edit is written whole,
|
|
24
24
|
* or the edit as sent when it did not read back: the worst case is never different content.
|
|
25
25
|
*/
|
|
26
|
-
|
|
26
|
+
import { type MarkdownBodyFormat } from './markdown-parse.js';
|
|
27
27
|
/**
|
|
28
28
|
* `text`'s top-level blocks, each as its source text and its `canonical` form, or undefined when
|
|
29
29
|
* `text` does not parse: two bodies mean the same to the splice exactly when their `meaning`
|
|
@@ -23,32 +23,14 @@
|
|
|
23
23
|
* then both again from the edit as sent; failing all four, the restyled edit is written whole,
|
|
24
24
|
* or the edit as sent when it did not read back: the worst case is never different content.
|
|
25
25
|
*/
|
|
26
|
-
import { fromMarkdown } from 'mdast-util-from-markdown';
|
|
27
|
-
import { gfmFromMarkdown } from 'mdast-util-gfm';
|
|
28
|
-
import { mdxFromMarkdown } from 'mdast-util-mdx';
|
|
29
|
-
import { gfm } from 'micromark-extension-gfm';
|
|
30
|
-
import { mdxjs } from 'micromark-extension-mdxjs';
|
|
31
26
|
import { createDebugLogger } from './debug.js';
|
|
32
27
|
import { getErrorMessage } from './error.js';
|
|
28
|
+
import { parseMarkdownBody as parse, } from './markdown-parse.js';
|
|
33
29
|
import { orderedNumber, restyleMarkdown } from './markdown-restyle.js';
|
|
34
30
|
import { withSourceLineEndings } from './yaml-source-splice.js';
|
|
35
31
|
const log = createDebugLogger({ prefix: 'MarkdownBodySplice' });
|
|
36
32
|
/** Above this many LCS cells the middle of a sequence is treated as one changed run. */
|
|
37
33
|
const MAX_LCS_CELLS = 4_000_000;
|
|
38
|
-
function parse(text, format) {
|
|
39
|
-
try {
|
|
40
|
-
return format === 'mdx'
|
|
41
|
-
? fromMarkdown(text, {
|
|
42
|
-
extensions: [gfm(), mdxjs()],
|
|
43
|
-
mdastExtensions: [gfmFromMarkdown(), mdxFromMarkdown()],
|
|
44
|
-
})
|
|
45
|
-
: fromMarkdown(text, { extensions: [gfm()], mdastExtensions: [gfmFromMarkdown()] });
|
|
46
|
-
}
|
|
47
|
-
catch (err) {
|
|
48
|
-
log.debug('markdown-body-splice', 'body does not parse', { error: getErrorMessage(err) });
|
|
49
|
-
return undefined;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
34
|
/**
|
|
53
35
|
* A node's meaning as a string: its tree without source positions. `data` goes too, because
|
|
54
36
|
* the parser fills it only with derived information (an MDX expression's estree, which carries
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The md/mdx body parser the save path and the merge engine share, so both read a body the same
|
|
3
|
+
* way. Dependency-free of node built-ins: the merge engine runs in the browser bundle too.
|
|
4
|
+
*/
|
|
5
|
+
import { fromMarkdown } from 'mdast-util-from-markdown';
|
|
6
|
+
export type MarkdownBodyFormat = 'md' | 'mdx';
|
|
7
|
+
export type MarkdownRoot = ReturnType<typeof fromMarkdown>;
|
|
8
|
+
/** `text`'s mdast with GFM, and MDX for `mdx`; undefined when it does not parse. */
|
|
9
|
+
export declare function parseMarkdownBody(text: string, format: MarkdownBodyFormat): MarkdownRoot | undefined;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The md/mdx body parser the save path and the merge engine share, so both read a body the same
|
|
3
|
+
* way. Dependency-free of node built-ins: the merge engine runs in the browser bundle too.
|
|
4
|
+
*/
|
|
5
|
+
import { fromMarkdown } from 'mdast-util-from-markdown';
|
|
6
|
+
import { gfmFromMarkdown } from 'mdast-util-gfm';
|
|
7
|
+
import { mdxFromMarkdown } from 'mdast-util-mdx';
|
|
8
|
+
import { gfm } from 'micromark-extension-gfm';
|
|
9
|
+
import { mdxjs } from 'micromark-extension-mdxjs';
|
|
10
|
+
import { createDebugLogger } from './debug.js';
|
|
11
|
+
import { getErrorMessage } from './error.js';
|
|
12
|
+
const log = createDebugLogger({ prefix: 'MarkdownParse' });
|
|
13
|
+
/** `text`'s mdast with GFM, and MDX for `mdx`; undefined when it does not parse. */
|
|
14
|
+
export function parseMarkdownBody(text, format) {
|
|
15
|
+
try {
|
|
16
|
+
return format === 'mdx'
|
|
17
|
+
? fromMarkdown(text, {
|
|
18
|
+
extensions: [gfm(), mdxjs()],
|
|
19
|
+
mdastExtensions: [gfmFromMarkdown(), mdxFromMarkdown()],
|
|
20
|
+
})
|
|
21
|
+
: fromMarkdown(text, { extensions: [gfm()], mdastExtensions: [gfmFromMarkdown()] });
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
log.debug('markdown-parse', 'body does not parse', { error: getErrorMessage(err) });
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CANOPYCMS_VERSION = "0.
|
|
1
|
+
export declare const CANOPYCMS_VERSION = "0.13.0";
|
package/dist/version.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Generated by scripts/bump-version.mjs, its only writer. The package builds with plain
|
|
2
2
|
// tsc (rootDir: src), so src/ cannot import ../package.json; version.test.ts keeps the two equal.
|
|
3
|
-
export const CANOPYCMS_VERSION = '0.
|
|
3
|
+
export const CANOPYCMS_VERSION = '0.13.0';
|