canopycms 0.0.67 → 0.0.68-int.94
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/ai/handler.js +8 -0
- package/dist/api/admin-branch-health.js +12 -7
- package/dist/api/admin.d.ts +21 -9
- package/dist/api/admin.js +18 -2
- package/dist/api/branch-status.js +19 -4
- package/dist/api/branch.js +11 -11
- package/dist/api/client.d.ts +12 -0
- package/dist/api/client.js +34 -11
- package/dist/api/content.d.ts +6 -5
- package/dist/api/content.js +44 -41
- package/dist/api/entries.js +16 -15
- package/dist/api/github-sync.d.ts +10 -1
- package/dist/api/github-sync.js +20 -10
- package/dist/api/reference-options.js +1 -1
- package/dist/api/resolve-references.js +14 -40
- package/dist/api/settings-helpers.d.ts +1 -1
- package/dist/api/settings-helpers.js +1 -4
- package/dist/api/user.d.ts +3 -0
- package/dist/api/user.js +3 -0
- package/dist/assets/factory.d.ts +5 -0
- package/dist/assets/factory.js +5 -0
- package/dist/authorization/content.d.ts +5 -4
- package/dist/authorization/content.js +5 -5
- package/dist/authorization/path.d.ts +11 -6
- package/dist/authorization/path.js +11 -6
- package/dist/authorization/types.d.ts +2 -2
- package/dist/branch-health.js +3 -3
- package/dist/branch-schema-cache.d.ts +5 -3
- package/dist/branch-schema-cache.js +29 -11
- package/dist/branch-workspace.js +8 -3
- package/dist/build-identity.d.ts +3 -0
- package/dist/build-identity.js +13 -0
- package/dist/cli/cli.js +1070 -388
- package/dist/cli/generate-ai-content.js +537 -150
- package/dist/cli/github-app-manifest.d.ts +4 -3
- package/dist/cli/github-app-manifest.js +4 -3
- package/dist/cli/init.js +21 -11
- package/dist/cli/migrate.js +12 -8
- package/dist/cli/sync.js +83 -37
- package/dist/cli/template-files/Dockerfile.cms.template +8 -0
- package/dist/cli/template-files/cdk-app.ts.template +4 -0
- package/dist/cli/template-files/cms-stack.ts.template +6 -0
- package/dist/cli/template-files/deploy-cms.yml.template +2 -0
- package/dist/config/schemas/config.d.ts +11 -3
- package/dist/config/schemas/config.js +5 -1
- package/dist/config/schemas/field.js +1 -0
- package/dist/config/schemas/url.d.ts +2 -0
- package/dist/config/schemas/url.js +7 -0
- package/dist/config/types.d.ts +28 -4
- package/dist/config/validation.d.ts +6 -0
- package/dist/config/validation.js +37 -0
- package/dist/content-listing.d.ts +12 -12
- package/dist/content-listing.js +10 -6
- package/dist/content-reader.d.ts +11 -2
- package/dist/content-reader.js +22 -14
- package/dist/content-store.d.ts +37 -9
- package/dist/content-store.js +70 -43
- package/dist/content-tree.d.ts +1 -1
- package/dist/content-tree.js +2 -2
- package/dist/context.d.ts +27 -9
- package/dist/context.js +154 -70
- package/dist/editor/CanopyEditor.d.ts +1 -1
- package/dist/editor/CanopyEditor.js +1 -1
- package/dist/editor/Editor.d.ts +2 -0
- package/dist/editor/Editor.js +6 -5
- package/dist/editor/FormRenderer.js +20 -5
- package/dist/editor/admin/SystemHealthPanel.js +45 -7
- package/dist/editor/components/NoEditPermissionNotice.d.ts +10 -0
- package/dist/editor/components/NoEditPermissionNotice.js +17 -0
- package/dist/editor/editor-utils.d.ts +17 -15
- package/dist/editor/editor-utils.js +43 -36
- package/dist/editor/hooks/useBranchesData.js +3 -1
- package/dist/editor/hooks/useCommentSystem.js +5 -1
- package/dist/editor/hooks/useDraftManager.d.ts +1 -1
- package/dist/editor/hooks/useDraftManager.js +2 -1
- package/dist/editor/hooks/useEntriesData.js +2 -2
- package/dist/editor/hooks/useEntryManager.js +17 -11
- package/dist/editor/preview-bridge.js +4 -1
- package/dist/editor/preview-path.d.ts +10 -0
- package/dist/editor/preview-path.js +20 -0
- package/dist/entry-schema-registry.d.ts +4 -4
- package/dist/entry-schema-registry.js +10 -6
- package/dist/entry-schema.d.ts +27 -4
- package/dist/entry-schema.js +20 -3
- package/dist/git-manager.d.ts +79 -5
- package/dist/git-manager.js +308 -18
- package/dist/github-service.d.ts +8 -0
- package/dist/github-service.js +5 -1
- package/dist/http/handler.js +35 -13
- package/dist/http/index.d.ts +2 -0
- package/dist/http/index.js +2 -0
- package/dist/http/router.d.ts +2 -0
- package/dist/http/router.js +2 -1
- package/dist/http/worker-not-ready.d.ts +9 -0
- package/dist/http/worker-not-ready.js +17 -0
- package/dist/operating-mode/client-unsafe-strategy.js +0 -6
- package/dist/operating-mode/types.d.ts +0 -3
- package/dist/paths/branch-name.d.ts +5 -0
- package/dist/paths/branch-name.js +9 -0
- package/dist/paths/branch.d.ts +6 -1
- package/dist/paths/branch.js +15 -2
- package/dist/paths/index.d.ts +3 -3
- package/dist/paths/index.js +3 -3
- package/dist/paths/normalize.d.ts +7 -0
- package/dist/paths/normalize.js +9 -0
- package/dist/reference-resolver.d.ts +5 -21
- package/dist/reference-resolver.js +9 -44
- package/dist/resolve-canopy-user.js +2 -1
- package/dist/schema/schema-store.d.ts +48 -22
- package/dist/schema/schema-store.js +66 -22
- package/dist/services.d.ts +18 -6
- package/dist/services.js +85 -70
- package/dist/settings-workspace.js +42 -4
- package/dist/static/seo.d.ts +2 -14
- package/dist/static/seo.js +2 -25
- package/dist/submission-attribution.d.ts +73 -0
- package/dist/submission-attribution.js +221 -0
- package/dist/sync-core.d.ts +12 -1
- package/dist/sync-core.js +31 -13
- package/dist/task-queue/worker-status.d.ts +8 -0
- package/dist/task-queue/worker-status.js +17 -0
- package/dist/types.d.ts +31 -2
- package/dist/utils/content-write-lock.d.ts +18 -7
- package/dist/utils/content-write-lock.js +18 -9
- package/dist/utils/debug.d.ts +8 -0
- package/dist/utils/debug.js +10 -2
- package/dist/utils/git.d.ts +28 -0
- package/dist/utils/git.js +38 -0
- package/dist/utils/provisioning-lock.d.ts +15 -5
- package/dist/utils/provisioning-lock.js +31 -11
- package/dist/utils/request-timing.d.ts +25 -0
- package/dist/utils/request-timing.js +101 -0
- package/dist/utils/url-prefix.d.ts +30 -0
- package/dist/utils/url-prefix.js +61 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/dist/worker/canopy-state.d.ts +45 -0
- package/dist/worker/canopy-state.js +75 -0
- package/dist/worker/cms-worker.js +23 -2
- package/dist/worker/git-sync.d.ts +8 -2
- package/dist/worker/git-sync.js +138 -24
- package/dist/worker/provisioned-workspace.d.ts +35 -0
- package/dist/worker/provisioned-workspace.js +50 -0
- package/dist/worker/rebase.d.ts +1 -1
- package/dist/worker/rebase.js +83 -12
- package/dist/worker/task-runner.js +39 -4
- package/package.json +1 -1
|
@@ -89,6 +89,9 @@ function createLogicalPath(...segments) {
|
|
|
89
89
|
}
|
|
90
90
|
return normalized;
|
|
91
91
|
}
|
|
92
|
+
function entryLogicalPath(collectionLogicalPath, slug) {
|
|
93
|
+
return collectionLogicalPath ? `${collectionLogicalPath}/${slug}` : slug;
|
|
94
|
+
}
|
|
92
95
|
function trimSlashes(path22) {
|
|
93
96
|
let start = 0;
|
|
94
97
|
let end = path22.length;
|
|
@@ -310,7 +313,8 @@ var init_field = __esm({
|
|
|
310
313
|
});
|
|
311
314
|
objectFieldSchema = fieldBaseSchema.extend({
|
|
312
315
|
type: z2.literal("object"),
|
|
313
|
-
fields: z2.array(z2.lazy(() => fieldHolder[0])).min(1)
|
|
316
|
+
fields: z2.array(z2.lazy(() => fieldHolder[0])).min(1),
|
|
317
|
+
itemTitleField: z2.string().min(1).optional()
|
|
314
318
|
});
|
|
315
319
|
inlineGroupFieldSchema = z2.object({
|
|
316
320
|
type: z2.literal("group"),
|
|
@@ -375,7 +379,7 @@ var init_collection = __esm({
|
|
|
375
379
|
|
|
376
380
|
// dist/config/schemas/url.js
|
|
377
381
|
import { z as z4 } from "zod";
|
|
378
|
-
var uploadTargetUrlSchema, assetMountUrlSchema;
|
|
382
|
+
var uploadTargetUrlSchema, assetMountUrlSchema, previewPrefixSchema;
|
|
379
383
|
var init_url = __esm({
|
|
380
384
|
"dist/config/schemas/url.js"() {
|
|
381
385
|
"use strict";
|
|
@@ -386,6 +390,9 @@ var init_url = __esm({
|
|
|
386
390
|
assetMountUrlSchema = z4.string().trim().refine((value) => isHttpUrlOrSameOriginPath(value, { allowProtocolRelative: true }), {
|
|
387
391
|
message: 'must be an absolute http(s) URL, a protocol-relative "//host" URL, or a site-relative path beginning with a single "/", with no query or fragment'
|
|
388
392
|
});
|
|
393
|
+
previewPrefixSchema = z4.string().trim().refine((value) => isHttpUrlOrSameOriginPath(value), {
|
|
394
|
+
message: 'editor.previewPrefix must be an absolute http(s) URL or a site-relative path beginning with a single "/", with no query or fragment'
|
|
395
|
+
});
|
|
389
396
|
}
|
|
390
397
|
});
|
|
391
398
|
|
|
@@ -443,6 +450,7 @@ var init_config = __esm({
|
|
|
443
450
|
"use strict";
|
|
444
451
|
init_collection();
|
|
445
452
|
init_media();
|
|
453
|
+
init_url();
|
|
446
454
|
defaultBranchAccessSchema = z6.enum(["allow", "deny"]).default("deny");
|
|
447
455
|
defaultPathAccessLevelSchema = z6.enum(["allow", "deny"]);
|
|
448
456
|
defaultPathAccessLevelsSchema = z6.object({
|
|
@@ -474,6 +482,7 @@ var init_config = __esm({
|
|
|
474
482
|
subtitle: z6.string().optional(),
|
|
475
483
|
theme: z6.unknown().optional(),
|
|
476
484
|
previewBase: z6.record(z6.string()).optional(),
|
|
485
|
+
previewPrefix: previewPrefixSchema.optional(),
|
|
477
486
|
// UI handler functions (runtime only, don't serialize)
|
|
478
487
|
onAccountClick: z6.function().returns(z6.void()).optional(),
|
|
479
488
|
onLogoutClick: z6.function().returns(z6.void()).optional(),
|
|
@@ -501,6 +510,9 @@ var init_config = __esm({
|
|
|
501
510
|
defaultRemoteUrl: defaultRemoteUrlSchema.optional(),
|
|
502
511
|
gitBotAuthorName: gitBotAuthorNameSchema,
|
|
503
512
|
gitBotAuthorEmail: gitBotAuthorEmailSchema,
|
|
513
|
+
// Unset reads as on for Edited-by and off for Co-authored-by (services.ts submitBranch).
|
|
514
|
+
gitEditedByTrailers: z6.boolean().optional(),
|
|
515
|
+
gitCoAuthoredByTrailers: z6.boolean().optional(),
|
|
504
516
|
githubTokenEnvVar: githubTokenEnvVarSchema.optional(),
|
|
505
517
|
// Required by design (follow-up to SEC-C1): a prod deploy that omits `mode` must fail
|
|
506
518
|
// validation loudly rather than silently running header-trusting dev auth semantics.
|
|
@@ -512,7 +524,6 @@ var init_config = __esm({
|
|
|
512
524
|
// Default false/unset — the standard AWS Lambda+worker topology must leave this unset.
|
|
513
525
|
allowNetworkRemoteInProd: z6.boolean().optional(),
|
|
514
526
|
settingsBranch: z6.string().optional(),
|
|
515
|
-
autoCreateSettingsPR: z6.boolean().optional(),
|
|
516
527
|
// `.optional()`, NOT bare `deploymentNameSchema`: its own `.default('prod')` would make
|
|
517
528
|
// `parse(undefined)` resolve to 'prod' instead of staying `undefined`, collapsing the
|
|
518
529
|
// env > config > modeDefault precedence chain `resolveDeploymentName`
|
|
@@ -835,9 +846,6 @@ var init_client_unsafe_strategy = __esm({
|
|
|
835
846
|
throw new Error("gitBotAuthorName and gitBotAuthorEmail are required in prod mode");
|
|
836
847
|
}
|
|
837
848
|
}
|
|
838
|
-
shouldCreateSettingsPR(config) {
|
|
839
|
-
return config.autoCreateSettingsPR ?? true;
|
|
840
|
-
}
|
|
841
849
|
};
|
|
842
850
|
DevStrategy = class extends DevClientSafeStrategy {
|
|
843
851
|
getWorkspaceRoot(sourceRoot) {
|
|
@@ -884,9 +892,6 @@ var init_client_unsafe_strategy = __esm({
|
|
|
884
892
|
}
|
|
885
893
|
validateConfig(_config) {
|
|
886
894
|
}
|
|
887
|
-
shouldCreateSettingsPR(_config) {
|
|
888
|
-
return false;
|
|
889
|
-
}
|
|
890
895
|
};
|
|
891
896
|
strategyCache = /* @__PURE__ */ new Map();
|
|
892
897
|
}
|
|
@@ -1193,12 +1198,15 @@ import fs2 from "node:fs/promises";
|
|
|
1193
1198
|
import path2 from "node:path";
|
|
1194
1199
|
import lockfile from "proper-lockfile";
|
|
1195
1200
|
init_logger();
|
|
1196
|
-
|
|
1201
|
+
var PROVISIONING_LOCK_STALE_MS = 9e4;
|
|
1202
|
+
function provisioningLockOptions(lockPath, retries, onCompromised, staleMs) {
|
|
1197
1203
|
return {
|
|
1198
1204
|
lockfilePath: lockPath,
|
|
1199
1205
|
realpath: false,
|
|
1200
1206
|
retries,
|
|
1201
|
-
stale:
|
|
1207
|
+
stale: staleMs,
|
|
1208
|
+
// Fixed, not stale/2: every holder refreshes at 15s whatever threshold it judges others by.
|
|
1209
|
+
update: 15e3,
|
|
1202
1210
|
// proper-lockfile invokes this from inside its refresh timer, so ANY throw escaping here is
|
|
1203
1211
|
// an uncaught exception that kills the process. Call sites are told not to throw (see
|
|
1204
1212
|
// OnLockCompromised); this makes it structural rather than a convention, and also covers the
|
|
@@ -1234,16 +1242,19 @@ function releaseIgnoringAlreadyReleased(release, lockPath) {
|
|
|
1234
1242
|
}
|
|
1235
1243
|
};
|
|
1236
1244
|
}
|
|
1245
|
+
function branchProvisioningLockName(dirName) {
|
|
1246
|
+
return `.${dirName}.init.lock`;
|
|
1247
|
+
}
|
|
1237
1248
|
async function acquireProvisioningLock(lockTargetDir2, lockName, onCompromised) {
|
|
1238
1249
|
await fs2.mkdir(lockTargetDir2, { recursive: true });
|
|
1239
1250
|
const lockPath = path2.join(lockTargetDir2, lockName);
|
|
1240
|
-
const release = await lockfile.lock(lockPath, provisioningLockOptions(lockPath, { retries: 600, factor: 1, minTimeout: 300, maxTimeout: 800, randomize: true }, onCompromised));
|
|
1251
|
+
const release = await lockfile.lock(lockPath, provisioningLockOptions(lockPath, { retries: 600, factor: 1, minTimeout: 300, maxTimeout: 800, randomize: true }, onCompromised, PROVISIONING_LOCK_STALE_MS));
|
|
1241
1252
|
return releaseIgnoringAlreadyReleased(release, lockPath);
|
|
1242
1253
|
}
|
|
1243
|
-
async function tryAcquireProvisioningLock(lockTargetDir2, lockName, onCompromised) {
|
|
1254
|
+
async function tryAcquireProvisioningLock(lockTargetDir2, lockName, onCompromised, staleMs = PROVISIONING_LOCK_STALE_MS) {
|
|
1244
1255
|
await fs2.mkdir(lockTargetDir2, { recursive: true });
|
|
1245
1256
|
const lockPath = path2.join(lockTargetDir2, lockName);
|
|
1246
|
-
const release = await lockfile.lock(lockPath, provisioningLockOptions(lockPath, 0, onCompromised));
|
|
1257
|
+
const release = await lockfile.lock(lockPath, provisioningLockOptions(lockPath, 0, onCompromised, staleMs));
|
|
1247
1258
|
return releaseIgnoringAlreadyReleased(release, lockPath);
|
|
1248
1259
|
}
|
|
1249
1260
|
|
|
@@ -1252,8 +1263,9 @@ var META_DIR = ".canopy-meta";
|
|
|
1252
1263
|
var CONTENT_WRITE_LOCK_NAME = "content-write.lock";
|
|
1253
1264
|
var DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS = 2e3;
|
|
1254
1265
|
var ContentWriteLockBusyError = class extends Error {
|
|
1255
|
-
constructor(message = "This branch is busy (syncing with the base branch, or another save is in flight); the change was not saved. Try again in a moment.") {
|
|
1266
|
+
constructor(message = "This branch is busy (syncing with the base branch, or another save is in flight); the change was not saved. Try again in a moment.", outcome = "not-run") {
|
|
1256
1267
|
super(message);
|
|
1268
|
+
this.outcome = outcome;
|
|
1257
1269
|
this.name = "ContentWriteLockBusyError";
|
|
1258
1270
|
}
|
|
1259
1271
|
};
|
|
@@ -1263,8 +1275,9 @@ function lockTargetDir(branchRoot) {
|
|
|
1263
1275
|
var sleep = (ms) => new Promise((resolve2) => {
|
|
1264
1276
|
setTimeout(resolve2, ms);
|
|
1265
1277
|
});
|
|
1278
|
+
var CONTENT_WRITE_LOCK_STALE_MS = 3e4;
|
|
1266
1279
|
function tryAcquireContentWriteLock(branchRoot, onCompromised) {
|
|
1267
|
-
return tryAcquireProvisioningLock(lockTargetDir(branchRoot), CONTENT_WRITE_LOCK_NAME, onCompromised);
|
|
1280
|
+
return tryAcquireProvisioningLock(lockTargetDir(branchRoot), CONTENT_WRITE_LOCK_NAME, onCompromised, CONTENT_WRITE_LOCK_STALE_MS);
|
|
1268
1281
|
}
|
|
1269
1282
|
async function acquireContentWriteLock(branchRoot, waitMs = DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS, onCompromised) {
|
|
1270
1283
|
const startedAt = Date.now();
|
|
@@ -1295,7 +1308,7 @@ async function withContentWriteLock(branchRoot, fn, waitMs = DEFAULT_CONTENT_WRI
|
|
|
1295
1308
|
await release();
|
|
1296
1309
|
}
|
|
1297
1310
|
if (compromised) {
|
|
1298
|
-
throw new ContentWriteLockBusyError("This branch was being synced while your change was written, so the change may or may not have been recorded. Reload the entry to see the current state before saving again.");
|
|
1311
|
+
throw new ContentWriteLockBusyError("This branch was being synced while your change was written, so the change may or may not have been recorded. Reload the entry to see the current state before saving again.", "unknown");
|
|
1299
1312
|
}
|
|
1300
1313
|
return result;
|
|
1301
1314
|
}
|
|
@@ -1345,8 +1358,21 @@ function buildResolvedReference(data, meta, body) {
|
|
|
1345
1358
|
resolved.slug = meta.slug;
|
|
1346
1359
|
resolved.collection = meta.collection;
|
|
1347
1360
|
resolved.urlPath = meta.urlPath;
|
|
1361
|
+
delete resolved[RESTRICTED_REFERENCE_MARKER];
|
|
1348
1362
|
return resolved;
|
|
1349
1363
|
}
|
|
1364
|
+
var RESTRICTED_REFERENCE_MARKER = "unavailable";
|
|
1365
|
+
function buildRestrictedReference(meta, title) {
|
|
1366
|
+
return {
|
|
1367
|
+
id: meta.id,
|
|
1368
|
+
slug: meta.slug,
|
|
1369
|
+
collection: meta.collection,
|
|
1370
|
+
urlPath: meta.urlPath,
|
|
1371
|
+
title,
|
|
1372
|
+
unavailable: true,
|
|
1373
|
+
reason: "restricted"
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1350
1376
|
var SEO_GROUP_FIELDS = [
|
|
1351
1377
|
{
|
|
1352
1378
|
name: DEFAULT_SEO_FIELD_NAMES.title,
|
|
@@ -1399,6 +1425,50 @@ var SEO_GROUP_FIELDS = [
|
|
|
1399
1425
|
}
|
|
1400
1426
|
];
|
|
1401
1427
|
|
|
1428
|
+
// dist/utils/title-field.js
|
|
1429
|
+
function isGroupField(field) {
|
|
1430
|
+
return field.type === "group";
|
|
1431
|
+
}
|
|
1432
|
+
function extractTitleFromSchema(fields, data) {
|
|
1433
|
+
return findTitleValue(fields, data);
|
|
1434
|
+
}
|
|
1435
|
+
function findTitleValue(fields, data) {
|
|
1436
|
+
for (const field of fields) {
|
|
1437
|
+
if (isGroupField(field)) {
|
|
1438
|
+
const result = findTitleValue(field.fields, data);
|
|
1439
|
+
if (result !== void 0)
|
|
1440
|
+
return result;
|
|
1441
|
+
} else if ("isTitle" in field && field.isTitle) {
|
|
1442
|
+
const value = data[field.name];
|
|
1443
|
+
return typeof value === "string" ? value : void 0;
|
|
1444
|
+
} else if (field.type === "object" && "fields" in field && field.fields && !field.list) {
|
|
1445
|
+
const nested = data[field.name];
|
|
1446
|
+
if (nested != null && typeof nested === "object" && !Array.isArray(nested)) {
|
|
1447
|
+
const result = findTitleValue(field.fields, nested);
|
|
1448
|
+
if (result !== void 0)
|
|
1449
|
+
return result;
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
return void 0;
|
|
1454
|
+
}
|
|
1455
|
+
function humanizeSlug(slug) {
|
|
1456
|
+
return slug.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()).trim();
|
|
1457
|
+
}
|
|
1458
|
+
function resolveEntryTitle(data, options) {
|
|
1459
|
+
if (options?.schema) {
|
|
1460
|
+
const schemaTitle = extractTitleFromSchema(options.schema, data);
|
|
1461
|
+
if (schemaTitle)
|
|
1462
|
+
return schemaTitle;
|
|
1463
|
+
}
|
|
1464
|
+
const title = data.title ?? data.name;
|
|
1465
|
+
if (typeof title === "string")
|
|
1466
|
+
return title;
|
|
1467
|
+
if (options?.entryTypeLabel)
|
|
1468
|
+
return options.entryTypeLabel;
|
|
1469
|
+
return options?.slug ? humanizeSlug(options.slug) : "Untitled";
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1402
1472
|
// dist/utils/entry-url.js
|
|
1403
1473
|
init_normalize();
|
|
1404
1474
|
function isIndexSlug(slug) {
|
|
@@ -2048,6 +2118,10 @@ var DebugLogger = class {
|
|
|
2048
2118
|
throw new Error(errorMsg);
|
|
2049
2119
|
}
|
|
2050
2120
|
}
|
|
2121
|
+
/**
|
|
2122
|
+
* Keyed by label alone, so two overlapping timers with one label overwrite each other;
|
|
2123
|
+
* anything that can run concurrently uses {@link timed}.
|
|
2124
|
+
*/
|
|
2051
2125
|
time(label) {
|
|
2052
2126
|
this.timers.set(label, Date.now());
|
|
2053
2127
|
}
|
|
@@ -2062,12 +2136,16 @@ var DebugLogger = class {
|
|
|
2062
2136
|
this.debug(category, `${label} completed`, { durationMs: duration });
|
|
2063
2137
|
return duration;
|
|
2064
2138
|
}
|
|
2139
|
+
/**
|
|
2140
|
+
* Log `<label> completed {durationMs}` once `fn` settles. The start time is local to the
|
|
2141
|
+
* call, so concurrent spans with the same label on one logger never overwrite each other.
|
|
2142
|
+
*/
|
|
2065
2143
|
async timed(category, label, fn) {
|
|
2066
|
-
|
|
2144
|
+
const start = Date.now();
|
|
2067
2145
|
try {
|
|
2068
2146
|
return await fn();
|
|
2069
2147
|
} finally {
|
|
2070
|
-
this.
|
|
2148
|
+
this.debug(category, `${label} completed`, { durationMs: Date.now() - start });
|
|
2071
2149
|
}
|
|
2072
2150
|
}
|
|
2073
2151
|
};
|
|
@@ -2393,9 +2471,80 @@ var STATIC_DEPLOY_USER = Object.freeze({
|
|
|
2393
2471
|
name: "Static Deploy"
|
|
2394
2472
|
});
|
|
2395
2473
|
|
|
2474
|
+
// dist/utils/request-timing.js
|
|
2475
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2476
|
+
var storage = new AsyncLocalStorage();
|
|
2477
|
+
var log2 = createDebugLogger({ prefix: "CanopyCMS" });
|
|
2478
|
+
async function timeRequestPhase(phase, fn) {
|
|
2479
|
+
const parent = storage.getStore();
|
|
2480
|
+
if (!parent)
|
|
2481
|
+
return fn();
|
|
2482
|
+
const key = parent.path ? `${parent.path}>${phase}` : phase;
|
|
2483
|
+
let total = parent.phases.get(key);
|
|
2484
|
+
if (!total) {
|
|
2485
|
+
total = { count: 0, ms: 0 };
|
|
2486
|
+
parent.phases.set(key, total);
|
|
2487
|
+
}
|
|
2488
|
+
const start = performance.now();
|
|
2489
|
+
try {
|
|
2490
|
+
return await storage.run({ ...parent, path: key }, fn);
|
|
2491
|
+
} finally {
|
|
2492
|
+
total.count += 1;
|
|
2493
|
+
total.ms += performance.now() - start;
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
// dist/utils/git.js
|
|
2498
|
+
import { simpleGit } from "simple-git";
|
|
2499
|
+
var NETWORK_SCHEME_PATTERN = /^(https?|ssh|git):\/\//i;
|
|
2500
|
+
var TRANSPORT_HELPER_PATTERN = /^[a-z][a-z0-9+.-]*::/i;
|
|
2501
|
+
var LEADING_OPTION_PATTERN = /^-/;
|
|
2502
|
+
var WINDOWS_DRIVE_PATTERN = /^[a-zA-Z]:[\\/]/;
|
|
2503
|
+
var COLON_BEFORE_SLASH_PATTERN = /^[^/\\\s]+:/;
|
|
2504
|
+
function isNetworkRemoteUrl(url) {
|
|
2505
|
+
if (/^file:\/\//i.test(url))
|
|
2506
|
+
return false;
|
|
2507
|
+
if (NETWORK_SCHEME_PATTERN.test(url))
|
|
2508
|
+
return true;
|
|
2509
|
+
if (TRANSPORT_HELPER_PATTERN.test(url))
|
|
2510
|
+
return true;
|
|
2511
|
+
if (LEADING_OPTION_PATTERN.test(url))
|
|
2512
|
+
return true;
|
|
2513
|
+
if (WINDOWS_DRIVE_PATTERN.test(url))
|
|
2514
|
+
return false;
|
|
2515
|
+
return COLON_BEFORE_SLASH_PATTERN.test(url);
|
|
2516
|
+
}
|
|
2517
|
+
var MISSING_REMOTE_REF_REASONS = ["couldn't find remote ref", "Couldn't find remote ref"];
|
|
2518
|
+
function isMissingRemoteRefFailure(message) {
|
|
2519
|
+
return MISSING_REMOTE_REF_REASONS.some((reason) => message.includes(reason));
|
|
2520
|
+
}
|
|
2521
|
+
var CANOPY_META_DIR = ".canopy-meta";
|
|
2522
|
+
async function stageAllExceptCanopyState(git) {
|
|
2523
|
+
await git.add(["-A"]);
|
|
2524
|
+
await git.raw(["reset", "-q", "--", CANOPY_META_DIR]);
|
|
2525
|
+
}
|
|
2526
|
+
async function detectHeadBranch(repoRoot, fallback = "main") {
|
|
2527
|
+
try {
|
|
2528
|
+
const git = simpleGit({ baseDir: repoRoot });
|
|
2529
|
+
const head = (await git.revparse(["--abbrev-ref", "HEAD"])).trim();
|
|
2530
|
+
return head && head !== "HEAD" ? head : fallback;
|
|
2531
|
+
} catch {
|
|
2532
|
+
return fallback;
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
async function resolveBaseBranch(options) {
|
|
2536
|
+
if (options.defaultBaseBranch)
|
|
2537
|
+
return options.defaultBaseBranch;
|
|
2538
|
+
if (options.mode === "dev") {
|
|
2539
|
+
return detectHeadBranch(options.detectFrom ?? process.cwd());
|
|
2540
|
+
}
|
|
2541
|
+
return "main";
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2396
2544
|
// dist/branch-schema-cache.js
|
|
2397
2545
|
var SCHEMA_CACHE_VERSION = 3;
|
|
2398
2546
|
var MTIME_CHECK_DEBOUNCE_MS = 1e3;
|
|
2547
|
+
var SCHEMA_CACHE_FILE = "schema-cache.json";
|
|
2399
2548
|
var SCHEMA_GENERATION_RESOURCE = "schema";
|
|
2400
2549
|
async function isStaleByMtime(dir, cachedAt) {
|
|
2401
2550
|
let entries;
|
|
@@ -2418,6 +2567,11 @@ async function isStaleByMtime(dir, cachedAt) {
|
|
|
2418
2567
|
}
|
|
2419
2568
|
return false;
|
|
2420
2569
|
}
|
|
2570
|
+
async function schemaCacheDir(branchRoot) {
|
|
2571
|
+
const gitDir = path8.join(branchRoot, ".git");
|
|
2572
|
+
const isClone = await fs7.stat(gitDir).then((stat) => stat.isDirectory(), () => false);
|
|
2573
|
+
return isClone ? path8.join(gitDir, "canopycms") : path8.join(branchRoot, CANOPY_META_DIR);
|
|
2574
|
+
}
|
|
2421
2575
|
var BranchSchemaCache = class {
|
|
2422
2576
|
constructor(mode = "prod") {
|
|
2423
2577
|
this.lastMtimeCheck = /* @__PURE__ */ new Map();
|
|
@@ -2426,7 +2580,7 @@ var BranchSchemaCache = class {
|
|
|
2426
2580
|
/**
|
|
2427
2581
|
* Whether to skip the on-disk cache for this branchRoot.
|
|
2428
2582
|
*
|
|
2429
|
-
* Never write
|
|
2583
|
+
* Never write a schema cache at the project root, whichever entrypoint
|
|
2430
2584
|
* produced the cwd branchRoot. branchRoot equals process.cwd() only in the
|
|
2431
2585
|
* synthetic contexts static deployments and build phases use; a real branch
|
|
2432
2586
|
* root is always nested under the workspace.
|
|
@@ -2436,7 +2590,7 @@ var BranchSchemaCache = class {
|
|
|
2436
2590
|
}
|
|
2437
2591
|
/** Get schema for a branch, from the cache when fresh, else resolved fresh. */
|
|
2438
2592
|
async getSchema(branchRoot, entrySchemaRegistry, contentRootName = "content") {
|
|
2439
|
-
return this.loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName);
|
|
2593
|
+
return timeRequestPhase("schema", () => this.loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName));
|
|
2440
2594
|
}
|
|
2441
2595
|
/**
|
|
2442
2596
|
* Resolve the schema from disk. Protected rather than a direct resolveSchema
|
|
@@ -2450,8 +2604,7 @@ var BranchSchemaCache = class {
|
|
|
2450
2604
|
const contentRoot = path8.join(branchRoot, contentRootName);
|
|
2451
2605
|
const skipDiskCache = this.skipDiskCache(branchRoot);
|
|
2452
2606
|
if (!skipDiskCache) {
|
|
2453
|
-
const
|
|
2454
|
-
const cachePath = path8.join(cacheDir, "schema-cache.json");
|
|
2607
|
+
const cachePath = path8.join(await schemaCacheDir(branchRoot), SCHEMA_CACHE_FILE);
|
|
2455
2608
|
let cacheData = null;
|
|
2456
2609
|
try {
|
|
2457
2610
|
const cacheContent = await fs7.readFile(cachePath, "utf-8");
|
|
@@ -2493,7 +2646,7 @@ var BranchSchemaCache = class {
|
|
|
2493
2646
|
const { skipDiskCache } = options;
|
|
2494
2647
|
const contentRoot = path8.join(branchRoot, contentRootName);
|
|
2495
2648
|
const read = skipDiskCache ? null : await readResourceGeneration(branchRoot, SCHEMA_GENERATION_RESOURCE);
|
|
2496
|
-
const result = await this.resolveFresh(contentRoot, entrySchemaRegistry);
|
|
2649
|
+
const result = await timeRequestPhase("resolve", () => this.resolveFresh(contentRoot, entrySchemaRegistry));
|
|
2497
2650
|
if (!isValidSchema(result.schema)) {
|
|
2498
2651
|
throw new Error(`No schema found in ${contentRoot}. Create .collection.json files with references to field schemas defined in your entry schema registry.`);
|
|
2499
2652
|
}
|
|
@@ -2504,9 +2657,9 @@ var BranchSchemaCache = class {
|
|
|
2504
2657
|
}
|
|
2505
2658
|
const flatSchema = flattenSchema(result.schema, contentRootName);
|
|
2506
2659
|
if (!skipDiskCache) {
|
|
2507
|
-
const cacheDir =
|
|
2508
|
-
const cachePath = path8.join(cacheDir,
|
|
2509
|
-
await fs7.unlink(path8.join(
|
|
2660
|
+
const cacheDir = await schemaCacheDir(branchRoot);
|
|
2661
|
+
const cachePath = path8.join(cacheDir, SCHEMA_CACHE_FILE);
|
|
2662
|
+
await fs7.unlink(path8.join(branchRoot, CANOPY_META_DIR, "schema-cache.stale")).catch(() => {
|
|
2510
2663
|
});
|
|
2511
2664
|
if (read && read.ok) {
|
|
2512
2665
|
const newCache = {
|
|
@@ -2644,6 +2797,11 @@ function sanitizeBranchName(branchName) {
|
|
|
2644
2797
|
const trimmedDots = squashed.replace(/^\.+/, "").replace(/(?<!\.)\.+$/, "");
|
|
2645
2798
|
return trimmedDots || "branch";
|
|
2646
2799
|
}
|
|
2800
|
+
var RESERVED_SETTINGS_BRANCH_PREFIX = "canopycms-settings-";
|
|
2801
|
+
function isSettingsBranchName(branchName, settingsBranchName) {
|
|
2802
|
+
const sanitized = sanitizeBranchName(branchName);
|
|
2803
|
+
return sanitized.startsWith(RESERVED_SETTINGS_BRANCH_PREFIX) || settingsBranchName !== void 0 && sanitized === sanitizeBranchName(settingsBranchName);
|
|
2804
|
+
}
|
|
2647
2805
|
|
|
2648
2806
|
// dist/paths/branch.js
|
|
2649
2807
|
var BranchPathError = class extends Error {
|
|
@@ -2655,6 +2813,9 @@ function resolveBranchPath(options) {
|
|
|
2655
2813
|
if (options.branchName.includes("..")) {
|
|
2656
2814
|
throw new BranchPathError("Branch name cannot contain traversal segments");
|
|
2657
2815
|
}
|
|
2816
|
+
if (isSettingsBranchName(options.branchName)) {
|
|
2817
|
+
throw new BranchPathError("Settings branches are not content branches");
|
|
2818
|
+
}
|
|
2658
2819
|
const safeBranch = sanitizeBranchName(options.branchName);
|
|
2659
2820
|
const strategy = operatingStrategy(options.mode);
|
|
2660
2821
|
const baseRoot = resolveContentBranchesRoot(options.mode, options.basePathOverride);
|
|
@@ -3116,6 +3277,7 @@ var ContentStore = class {
|
|
|
3116
3277
|
return {
|
|
3117
3278
|
absolutePath: resolved,
|
|
3118
3279
|
relativePath: path11.relative(this.root, resolved),
|
|
3280
|
+
logicalPath: entryLogicalPath(schemaItem.logicalPath, safeSlug),
|
|
3119
3281
|
id,
|
|
3120
3282
|
entryTypeName: finalEntryTypeName,
|
|
3121
3283
|
existed
|
|
@@ -3145,6 +3307,10 @@ var ContentStore = class {
|
|
|
3145
3307
|
const schemaItem = this.assertSchemaItem(schemaPath);
|
|
3146
3308
|
return await this.buildPaths(schemaItem, slug);
|
|
3147
3309
|
}
|
|
3310
|
+
/**
|
|
3311
|
+
* Read one entry. References resolve unless `resolveReferences: false`; a target that
|
|
3312
|
+
* `referenceAccess` denies resolves to a `RestrictedReference` rather than its data.
|
|
3313
|
+
*/
|
|
3148
3314
|
async read(collectionPath, slug = "", options = {}) {
|
|
3149
3315
|
const schemaItem = this.assertSchemaItem(collectionPath);
|
|
3150
3316
|
const { absolutePath, relativePath, entryTypeName: resolvedEntryTypeName } = await this.buildPaths(schemaItem, slug);
|
|
@@ -3206,7 +3372,7 @@ var ContentStore = class {
|
|
|
3206
3372
|
};
|
|
3207
3373
|
}
|
|
3208
3374
|
if (options.resolveReferences !== false) {
|
|
3209
|
-
doc.data = await this.resolveReferencesInData(doc.data, fields);
|
|
3375
|
+
doc.data = await this.resolveReferencesInData(doc.data, fields, void 0, options.referenceAccess);
|
|
3210
3376
|
}
|
|
3211
3377
|
doc.version = stat.mtimeMs;
|
|
3212
3378
|
return doc;
|
|
@@ -3660,20 +3826,25 @@ var ContentStore = class {
|
|
|
3660
3826
|
* a target referenced by many entries is read once; omit `cache` for exactly `read()`'s
|
|
3661
3827
|
* unmemoized behavior.
|
|
3662
3828
|
*
|
|
3663
|
-
*
|
|
3664
|
-
* `read()`, below the per-entry permission check in content-reader.ts, so a resolved target
|
|
3665
|
-
* may be an entry the caller could not `read()` directly -- pre-existing `read()` behavior,
|
|
3666
|
-
* matched here on purpose so one rule covers both.
|
|
3829
|
+
* `access` applies to every target exactly as in `read()`; see {@link ReferenceTargetAccess}.
|
|
3667
3830
|
*/
|
|
3668
|
-
async resolveReferences(data, fields, cache) {
|
|
3669
|
-
return this.resolveReferencesInData(data, fields, cache);
|
|
3831
|
+
async resolveReferences(data, fields, cache, access) {
|
|
3832
|
+
return this.resolveReferencesInData(data, fields, cache, access);
|
|
3670
3833
|
}
|
|
3671
|
-
|
|
3834
|
+
/**
|
|
3835
|
+
* Resolve one reference id the way a reference field resolves it, for the editor's
|
|
3836
|
+
* live-preview endpoint (api/resolve-references.ts), which has ids but no field to walk. Null
|
|
3837
|
+
* when the id names no entry. No body is embedded, since there is no field to ask for one.
|
|
3838
|
+
*/
|
|
3839
|
+
async resolveReferenceTarget(id, access) {
|
|
3840
|
+
return this.resolveSingleReference(id, await this.idIndex(), false, void 0, access);
|
|
3841
|
+
}
|
|
3842
|
+
async resolveReferencesInData(data, fields, cache, access) {
|
|
3672
3843
|
const resolved = { ...data };
|
|
3673
3844
|
const idIndex = await this.idIndex();
|
|
3674
3845
|
for (const field of fields) {
|
|
3675
3846
|
if (field.type === "group") {
|
|
3676
|
-
const groupResolved = await this.resolveReferencesInData(resolved, field.fields, cache);
|
|
3847
|
+
const groupResolved = await this.resolveReferencesInData(resolved, field.fields, cache, access);
|
|
3677
3848
|
Object.assign(resolved, groupResolved);
|
|
3678
3849
|
continue;
|
|
3679
3850
|
}
|
|
@@ -3681,18 +3852,18 @@ var ContentStore = class {
|
|
|
3681
3852
|
if (field.type === "reference") {
|
|
3682
3853
|
const includeBody = field.includeBody === true;
|
|
3683
3854
|
if (typeof value === "string" && value) {
|
|
3684
|
-
resolved[field.name] = await this.resolveSingleReference(value, idIndex, includeBody, cache);
|
|
3855
|
+
resolved[field.name] = await this.resolveSingleReference(value, idIndex, includeBody, cache, access);
|
|
3685
3856
|
} else if (field.list && Array.isArray(value)) {
|
|
3686
|
-
resolved[field.name] = await Promise.all(value.map((id) => typeof id === "string" ? this.resolveSingleReference(id, idIndex, includeBody, cache) : null));
|
|
3857
|
+
resolved[field.name] = await Promise.all(value.map((id) => typeof id === "string" ? this.resolveSingleReference(id, idIndex, includeBody, cache, access) : null));
|
|
3687
3858
|
}
|
|
3688
3859
|
} else if (field.type === "object" && value) {
|
|
3689
3860
|
const objectField = field;
|
|
3690
3861
|
if (!objectField.fields)
|
|
3691
3862
|
continue;
|
|
3692
3863
|
if (objectField.list && Array.isArray(value)) {
|
|
3693
|
-
resolved[field.name] = await Promise.all(value.map((item) => typeof item === "object" && item !== null ? this.resolveReferencesInData(item, objectField.fields, cache) : item));
|
|
3864
|
+
resolved[field.name] = await Promise.all(value.map((item) => typeof item === "object" && item !== null ? this.resolveReferencesInData(item, objectField.fields, cache, access) : item));
|
|
3694
3865
|
} else if (typeof value === "object") {
|
|
3695
|
-
resolved[field.name] = await this.resolveReferencesInData(value, objectField.fields, cache);
|
|
3866
|
+
resolved[field.name] = await this.resolveReferencesInData(value, objectField.fields, cache, access);
|
|
3696
3867
|
}
|
|
3697
3868
|
} else if (field.type === "block" && Array.isArray(value)) {
|
|
3698
3869
|
const blockField = field;
|
|
@@ -3705,7 +3876,7 @@ var ContentStore = class {
|
|
|
3705
3876
|
return block;
|
|
3706
3877
|
return {
|
|
3707
3878
|
...b,
|
|
3708
|
-
value: await this.resolveReferencesInData(b.value, template.fields, cache)
|
|
3879
|
+
value: await this.resolveReferencesInData(b.value, template.fields, cache, access)
|
|
3709
3880
|
};
|
|
3710
3881
|
}));
|
|
3711
3882
|
}
|
|
@@ -3736,13 +3907,13 @@ var ContentStore = class {
|
|
|
3736
3907
|
* `includeBody` is opt-in per field: an embedded body sits inside the memoized object and is
|
|
3737
3908
|
* cloned once per referencing entry.
|
|
3738
3909
|
*/
|
|
3739
|
-
resolveSingleReference(id, idIndex, includeBody, cache) {
|
|
3910
|
+
resolveSingleReference(id, idIndex, includeBody, cache, access) {
|
|
3740
3911
|
if (!cache)
|
|
3741
|
-
return this.resolveSingleReferenceUncached(id, idIndex, includeBody);
|
|
3912
|
+
return this.resolveSingleReferenceUncached(id, idIndex, includeBody, access);
|
|
3742
3913
|
const key = includeBody ? `${id}:body` : id;
|
|
3743
3914
|
let pending = cache.get(key);
|
|
3744
3915
|
if (!pending) {
|
|
3745
|
-
pending = this.resolveSingleReferenceUncached(id, idIndex, includeBody);
|
|
3916
|
+
pending = this.resolveSingleReferenceUncached(id, idIndex, includeBody, access);
|
|
3746
3917
|
cache.set(key, pending);
|
|
3747
3918
|
}
|
|
3748
3919
|
return pending.then((resolved) => resolved === null ? null : structuredClone(resolved));
|
|
@@ -3760,15 +3931,27 @@ var ContentStore = class {
|
|
|
3760
3931
|
* A {@link ReferenceResolveCache} sits ABOVE this, never inside it, so every distinct id
|
|
3761
3932
|
* still runs the full refresh-and-retry; a cache only makes repeats of the SAME id share it.
|
|
3762
3933
|
*/
|
|
3763
|
-
async resolveSingleReferenceUncached(id, idIndex, includeBody) {
|
|
3764
|
-
const first = await this.resolveSingleReferenceOnce(id, idIndex, includeBody);
|
|
3934
|
+
async resolveSingleReferenceUncached(id, idIndex, includeBody, access) {
|
|
3935
|
+
const first = await this.resolveSingleReferenceOnce(id, idIndex, includeBody, access);
|
|
3765
3936
|
if (first !== STALE_LOOKUP)
|
|
3766
3937
|
return first;
|
|
3767
3938
|
await this.refreshIndexForSuspiciousLookup();
|
|
3768
|
-
const second = await this.resolveSingleReferenceOnce(id, await this.idIndex(), includeBody);
|
|
3939
|
+
const second = await this.resolveSingleReferenceOnce(id, await this.idIndex(), includeBody, access);
|
|
3769
3940
|
return second === STALE_LOOKUP ? null : second;
|
|
3770
3941
|
}
|
|
3771
|
-
|
|
3942
|
+
/**
|
|
3943
|
+
* The entry type an entry file was written as, by its filename's type token, falling back to
|
|
3944
|
+
* the collection's default the way `read()` does for a legacy untyped file.
|
|
3945
|
+
*/
|
|
3946
|
+
entryTypeOf(collectionPath, relativePath) {
|
|
3947
|
+
const schemaItem = this.assertSchemaItem(collectionPath);
|
|
3948
|
+
if (schemaItem.type === "entry-type")
|
|
3949
|
+
return schemaItem;
|
|
3950
|
+
const entries = schemaItem.entries;
|
|
3951
|
+
const typeName = extractEntryTypeFromFilename(path11.basename(relativePath));
|
|
3952
|
+
return entries?.find((e) => e.name === typeName) ?? getDefaultEntryType(schemaItem.entries);
|
|
3953
|
+
}
|
|
3954
|
+
async resolveSingleReferenceOnce(id, idIndex, includeBody, access) {
|
|
3772
3955
|
try {
|
|
3773
3956
|
const location = idIndex.findById(id);
|
|
3774
3957
|
if (!location)
|
|
@@ -3779,13 +3962,22 @@ var ContentStore = class {
|
|
|
3779
3962
|
const doc = await this.read(location.collection, location.slug, {
|
|
3780
3963
|
resolveReferences: false
|
|
3781
3964
|
});
|
|
3782
|
-
const
|
|
3783
|
-
return buildResolvedReference(doc.data, {
|
|
3965
|
+
const meta = {
|
|
3784
3966
|
id,
|
|
3785
3967
|
slug: location.slug,
|
|
3786
3968
|
collection: location.collection,
|
|
3787
3969
|
urlPath: computeEntryUrl(location.collection, location.slug, this.contentRootName)
|
|
3788
|
-
}
|
|
3970
|
+
};
|
|
3971
|
+
if (access && !access(entryLogicalPath(location.collection, location.slug))) {
|
|
3972
|
+
const entryType = this.entryTypeOf(location.collection, location.relativePath);
|
|
3973
|
+
return buildRestrictedReference(meta, resolveEntryTitle(doc.data, {
|
|
3974
|
+
schema: entryType?.schema,
|
|
3975
|
+
entryTypeLabel: entryType?.label,
|
|
3976
|
+
slug: location.slug
|
|
3977
|
+
}));
|
|
3978
|
+
}
|
|
3979
|
+
const bodyForEmbed = includeBody && "body" in doc ? { fieldName: doc.bodyFieldName, value: doc.body } : void 0;
|
|
3980
|
+
return buildResolvedReference(doc.data, meta, bodyForEmbed);
|
|
3789
3981
|
} catch (error) {
|
|
3790
3982
|
if (isNodeError(error) && error.code === "ENOENT")
|
|
3791
3983
|
return STALE_LOOKUP;
|
|
@@ -4353,7 +4545,7 @@ function yamlValue(value) {
|
|
|
4353
4545
|
}
|
|
4354
4546
|
|
|
4355
4547
|
// dist/entry-link-resolver.js
|
|
4356
|
-
var
|
|
4548
|
+
var log3 = createDebugLogger({ prefix: "EntryLinks" });
|
|
4357
4549
|
var BASE58_CHAR = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]";
|
|
4358
4550
|
var ENTRY_LINK_PATTERN = new RegExp(`entry:(${BASE58_CHAR}{12})(#[^\\s)>"']*)?`, "g");
|
|
4359
4551
|
function resolveEntryUrl(location, contentRoot) {
|
|
@@ -4367,7 +4559,7 @@ function resolveEntryLinksInText(text, idIndex, contentRoot, customResolver) {
|
|
|
4367
4559
|
return part.text.replace(ENTRY_LINK_PATTERN, (_match, id, anchor) => {
|
|
4368
4560
|
const location = idIndex.findById(id);
|
|
4369
4561
|
if (!location || location.type !== "entry" || !location.collection || !location.slug) {
|
|
4370
|
-
|
|
4562
|
+
log3.warn("resolve", `Entry link target not found: entry:${id}`);
|
|
4371
4563
|
return anchor ?? "#";
|
|
4372
4564
|
}
|
|
4373
4565
|
let url;
|
|
@@ -4765,7 +4957,7 @@ async function readBranchMetadataFile(branchRoot) {
|
|
|
4765
4957
|
|
|
4766
4958
|
// dist/branch-registry.js
|
|
4767
4959
|
init_logger();
|
|
4768
|
-
var
|
|
4960
|
+
var log4 = createDebugLogger({ prefix: "BranchRegistry" });
|
|
4769
4961
|
var REGISTRY_FILE = "branches.json";
|
|
4770
4962
|
var REGISTRY_STALE_FILE = "branches.stale.json";
|
|
4771
4963
|
var REGISTRY_TEMP_FILE = "branches.tmp.json";
|
|
@@ -4845,7 +5037,7 @@ var BranchRegistry = class {
|
|
|
4845
5037
|
});
|
|
4846
5038
|
await this.regenerate();
|
|
4847
5039
|
} catch (err) {
|
|
4848
|
-
|
|
5040
|
+
log4.warn("invalidate", "Eager regeneration after invalidate() failed", {
|
|
4849
5041
|
error: getErrorMessage(err)
|
|
4850
5042
|
});
|
|
4851
5043
|
}
|
|
@@ -4936,7 +5128,7 @@ import fs14 from "node:fs/promises";
|
|
|
4936
5128
|
import path15 from "node:path";
|
|
4937
5129
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
4938
5130
|
import lockfile2 from "proper-lockfile";
|
|
4939
|
-
var
|
|
5131
|
+
var log5 = createDebugLogger({ prefix: "OccJsonWrite" });
|
|
4940
5132
|
var OccWriteConflictError = class extends Error {
|
|
4941
5133
|
constructor(message = "Concurrent modification detected") {
|
|
4942
5134
|
super(message);
|
|
@@ -5031,7 +5223,7 @@ async function withOccFileLock(filePath, fn) {
|
|
|
5031
5223
|
// Our critical sections are short and idempotent-on-conflict (OCC
|
|
5032
5224
|
// verify inside); log and let the section finish.
|
|
5033
5225
|
onCompromised: (err) => {
|
|
5034
|
-
|
|
5226
|
+
log5.warn("lock", `Lock compromised mid-hold for ${filePath}`, {
|
|
5035
5227
|
error: getErrorMessage(err)
|
|
5036
5228
|
});
|
|
5037
5229
|
}
|
|
@@ -5053,7 +5245,7 @@ async function withOccFileLock(filePath, fn) {
|
|
|
5053
5245
|
await release().catch((err) => {
|
|
5054
5246
|
if (isNodeError(err) && err.code === "ENOENT")
|
|
5055
5247
|
return;
|
|
5056
|
-
|
|
5248
|
+
log5.warn("lock", `Failed to release lock for ${filePath}`, { error: getErrorMessage(err) });
|
|
5057
5249
|
});
|
|
5058
5250
|
}
|
|
5059
5251
|
}
|
|
@@ -5229,51 +5421,7 @@ init_operating_mode();
|
|
|
5229
5421
|
import fs16 from "node:fs/promises";
|
|
5230
5422
|
import path17 from "node:path";
|
|
5231
5423
|
import { simpleGit as simpleGit2 } from "simple-git";
|
|
5232
|
-
|
|
5233
|
-
// dist/utils/git.js
|
|
5234
|
-
import { simpleGit } from "simple-git";
|
|
5235
|
-
var NETWORK_SCHEME_PATTERN = /^(https?|ssh|git):\/\//i;
|
|
5236
|
-
var TRANSPORT_HELPER_PATTERN = /^[a-z][a-z0-9+.-]*::/i;
|
|
5237
|
-
var LEADING_OPTION_PATTERN = /^-/;
|
|
5238
|
-
var WINDOWS_DRIVE_PATTERN = /^[a-zA-Z]:[\\/]/;
|
|
5239
|
-
var COLON_BEFORE_SLASH_PATTERN = /^[^/\\\s]+:/;
|
|
5240
|
-
function isNetworkRemoteUrl(url) {
|
|
5241
|
-
if (/^file:\/\//i.test(url))
|
|
5242
|
-
return false;
|
|
5243
|
-
if (NETWORK_SCHEME_PATTERN.test(url))
|
|
5244
|
-
return true;
|
|
5245
|
-
if (TRANSPORT_HELPER_PATTERN.test(url))
|
|
5246
|
-
return true;
|
|
5247
|
-
if (LEADING_OPTION_PATTERN.test(url))
|
|
5248
|
-
return true;
|
|
5249
|
-
if (WINDOWS_DRIVE_PATTERN.test(url))
|
|
5250
|
-
return false;
|
|
5251
|
-
return COLON_BEFORE_SLASH_PATTERN.test(url);
|
|
5252
|
-
}
|
|
5253
|
-
var MISSING_REMOTE_REF_REASONS = ["couldn't find remote ref", "Couldn't find remote ref"];
|
|
5254
|
-
function isMissingRemoteRefFailure(message) {
|
|
5255
|
-
return MISSING_REMOTE_REF_REASONS.some((reason) => message.includes(reason));
|
|
5256
|
-
}
|
|
5257
|
-
async function detectHeadBranch(repoRoot, fallback = "main") {
|
|
5258
|
-
try {
|
|
5259
|
-
const git = simpleGit({ baseDir: repoRoot });
|
|
5260
|
-
const head = (await git.revparse(["--abbrev-ref", "HEAD"])).trim();
|
|
5261
|
-
return head && head !== "HEAD" ? head : fallback;
|
|
5262
|
-
} catch {
|
|
5263
|
-
return fallback;
|
|
5264
|
-
}
|
|
5265
|
-
}
|
|
5266
|
-
async function resolveBaseBranch(options) {
|
|
5267
|
-
if (options.defaultBaseBranch)
|
|
5268
|
-
return options.defaultBaseBranch;
|
|
5269
|
-
if (options.mode === "dev") {
|
|
5270
|
-
return detectHeadBranch(options.detectFrom ?? process.cwd());
|
|
5271
|
-
}
|
|
5272
|
-
return "main";
|
|
5273
|
-
}
|
|
5274
|
-
|
|
5275
|
-
// dist/git-manager.js
|
|
5276
|
-
var log5 = createDebugLogger({ prefix: "GitManager" });
|
|
5424
|
+
var log6 = createDebugLogger({ prefix: "GitManager" });
|
|
5277
5425
|
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_]*)$/;
|
|
5278
5426
|
var FORCE_C_LOCALE = { LC_ALL: "C", LANG: "C" };
|
|
5279
5427
|
function gitChildEnv(overrides) {
|
|
@@ -5298,13 +5446,13 @@ async function ensureGitExcludePattern(repoPath, pattern) {
|
|
|
5298
5446
|
}
|
|
5299
5447
|
const lines = content.split("\n");
|
|
5300
5448
|
if (lines.some((line) => line.trim() === pattern)) {
|
|
5301
|
-
|
|
5449
|
+
log6.debug("git", "Pattern already in .git/info/exclude", { pattern });
|
|
5302
5450
|
return;
|
|
5303
5451
|
}
|
|
5304
5452
|
const needsLeadingNewline = content.length > 0 && !content.endsWith("\n");
|
|
5305
5453
|
const newContent = content + (needsLeadingNewline ? "\n" : "") + pattern + "\n";
|
|
5306
5454
|
await fs16.writeFile(excludePath, newContent, "utf-8");
|
|
5307
|
-
|
|
5455
|
+
log6.debug("git", "Added pattern to .git/info/exclude", { pattern });
|
|
5308
5456
|
}
|
|
5309
5457
|
var GitConflictError = class extends Error {
|
|
5310
5458
|
constructor(conflictedFiles) {
|
|
@@ -5322,6 +5470,41 @@ var GitRemoteRefMissingError = class extends Error {
|
|
|
5322
5470
|
this.name = "GitRemoteRefMissingError";
|
|
5323
5471
|
}
|
|
5324
5472
|
};
|
|
5473
|
+
var RemoteNotReadyError = class extends Error {
|
|
5474
|
+
constructor(expectedRemotePath) {
|
|
5475
|
+
super(`CanopyCMS: no git remote is available yet. defaultRemoteUrl (or CANOPYCMS_REMOTE_URL) is not set and the CMS worker has not created ${expectedRemotePath}; the worker may still be starting.`);
|
|
5476
|
+
this.expectedRemotePath = expectedRemotePath;
|
|
5477
|
+
this.name = "RemoteNotReadyError";
|
|
5478
|
+
}
|
|
5479
|
+
};
|
|
5480
|
+
var SettingsRemoteUnreadableError = class extends Error {
|
|
5481
|
+
constructor(branch, remote, gitError) {
|
|
5482
|
+
super(`CanopyCMS: could not read settings branch '${branch}' from remote '${remote}', so the settings workspace was not provisioned: ${getErrorMessage(gitError)}`);
|
|
5483
|
+
this.branch = branch;
|
|
5484
|
+
this.remote = remote;
|
|
5485
|
+
this.gitError = gitError;
|
|
5486
|
+
this.name = "SettingsRemoteUnreadableError";
|
|
5487
|
+
}
|
|
5488
|
+
};
|
|
5489
|
+
var SettingsBranchDivergedError = class extends Error {
|
|
5490
|
+
constructor(branch, workspacePath, localWork) {
|
|
5491
|
+
super(`CanopyCMS: settings branch '${branch}' in ${workspacePath} shares no history with the remote's '${branch}', and holds ${localWork}. Settings saves cannot pull until one copy is chosen. To keep the remote's settings: copy anything you need out of ${workspacePath}, move it aside, and restart; the next start checks out the remote's '${branch}', and copied changes can then be re-applied in the editor.`);
|
|
5492
|
+
this.branch = branch;
|
|
5493
|
+
this.workspacePath = workspacePath;
|
|
5494
|
+
this.localWork = localWork;
|
|
5495
|
+
this.name = "SettingsBranchDivergedError";
|
|
5496
|
+
}
|
|
5497
|
+
};
|
|
5498
|
+
var SETTINGS_BRANCH_FILES = /* @__PURE__ */ new Set(["permissions.json", "groups.json"]);
|
|
5499
|
+
var SettingsBranchHasContentHistoryError = class extends Error {
|
|
5500
|
+
constructor(branch, remote, evidence) {
|
|
5501
|
+
super(`CanopyCMS: the settings branch '${branch}' on remote '${remote}' holds content, not settings (${evidence}), so it was not checked out. Restore '${branch}' on the remote from a copy of the settings (it may hold only permissions.json and groups.json), then restart.`);
|
|
5502
|
+
this.branch = branch;
|
|
5503
|
+
this.remote = remote;
|
|
5504
|
+
this.evidence = evidence;
|
|
5505
|
+
this.name = "SettingsBranchHasContentHistoryError";
|
|
5506
|
+
}
|
|
5507
|
+
};
|
|
5325
5508
|
var GitManager = class _GitManager {
|
|
5326
5509
|
constructor(options, gitOptions) {
|
|
5327
5510
|
this.repoPath = path17.resolve(options.repoPath);
|
|
@@ -5332,14 +5515,14 @@ var GitManager = class _GitManager {
|
|
|
5332
5515
|
this.git.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path17.dirname(this.repoPath) }));
|
|
5333
5516
|
}
|
|
5334
5517
|
static async cloneRepo(remoteUrl, targetPath, baseBranch = "main") {
|
|
5335
|
-
|
|
5518
|
+
log6.debug("git", "Cloning repository", {
|
|
5336
5519
|
remoteUrl,
|
|
5337
5520
|
targetPath,
|
|
5338
5521
|
baseBranch
|
|
5339
5522
|
});
|
|
5340
5523
|
const git = simpleGit2();
|
|
5341
5524
|
await git.clone(remoteUrl, targetPath, ["--branch", baseBranch, "--single-branch"]);
|
|
5342
|
-
|
|
5525
|
+
log6.debug("git", "Clone complete");
|
|
5343
5526
|
}
|
|
5344
5527
|
/**
|
|
5345
5528
|
* Initializes a local bare git repository to simulate a remote for dev mode,
|
|
@@ -5353,15 +5536,15 @@ var GitManager = class _GitManager {
|
|
|
5353
5536
|
static async ensureLocalSimulatedRemote(options) {
|
|
5354
5537
|
const existingLock = remoteInitLocks.get(options.remotePath);
|
|
5355
5538
|
if (existingLock) {
|
|
5356
|
-
|
|
5539
|
+
log6.debug("git", "Waiting for existing remote initialization", {
|
|
5357
5540
|
remotePath: options.remotePath
|
|
5358
5541
|
});
|
|
5359
5542
|
await existingLock;
|
|
5360
5543
|
}
|
|
5361
|
-
const lockPromise =
|
|
5544
|
+
const lockPromise = log6.timed("git", "ensureLocalSimulatedRemote", async () => {
|
|
5362
5545
|
let releaseLock;
|
|
5363
5546
|
try {
|
|
5364
|
-
|
|
5547
|
+
log6.debug("git", "Initializing local simulated remote", {
|
|
5365
5548
|
remotePath: options.remotePath,
|
|
5366
5549
|
baseBranch: options.baseBranch
|
|
5367
5550
|
});
|
|
@@ -5376,7 +5559,7 @@ var GitManager = class _GitManager {
|
|
|
5376
5559
|
throw err;
|
|
5377
5560
|
}
|
|
5378
5561
|
if (remoteExists && await _GitManager.bareRemoteHasBranch(options.remotePath, options.baseBranch)) {
|
|
5379
|
-
|
|
5562
|
+
log6.debug("git", "Remote already has base branch, skipping");
|
|
5380
5563
|
return;
|
|
5381
5564
|
}
|
|
5382
5565
|
let gitRoot = options.sourcePath;
|
|
@@ -5395,8 +5578,8 @@ var GitManager = class _GitManager {
|
|
|
5395
5578
|
}
|
|
5396
5579
|
let hasCommits = false;
|
|
5397
5580
|
try {
|
|
5398
|
-
const
|
|
5399
|
-
hasCommits =
|
|
5581
|
+
const log9 = await sourceGit.log(["-1"]);
|
|
5582
|
+
hasCommits = log9.total > 0;
|
|
5400
5583
|
} catch {
|
|
5401
5584
|
hasCommits = false;
|
|
5402
5585
|
}
|
|
@@ -5408,12 +5591,12 @@ var GitManager = class _GitManager {
|
|
|
5408
5591
|
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.`);
|
|
5409
5592
|
}
|
|
5410
5593
|
if (remoteExists) {
|
|
5411
|
-
|
|
5594
|
+
log6.debug("git", "Existing remote is missing base branch \u2014 pushing it from source", {
|
|
5412
5595
|
remotePath: options.remotePath,
|
|
5413
5596
|
baseBranch: options.baseBranch
|
|
5414
5597
|
});
|
|
5415
5598
|
} else {
|
|
5416
|
-
|
|
5599
|
+
log6.debug("git", "Creating bare remote repository");
|
|
5417
5600
|
await simpleGit2().raw([
|
|
5418
5601
|
"init",
|
|
5419
5602
|
"--bare",
|
|
@@ -5427,13 +5610,13 @@ var GitManager = class _GitManager {
|
|
|
5427
5610
|
baseBranch: options.baseBranch,
|
|
5428
5611
|
subdirectory: options.subdirectory
|
|
5429
5612
|
});
|
|
5430
|
-
|
|
5613
|
+
log6.debug("git", "Remote initialization complete");
|
|
5431
5614
|
} finally {
|
|
5432
5615
|
if (releaseLock) {
|
|
5433
5616
|
try {
|
|
5434
5617
|
await releaseLock();
|
|
5435
5618
|
} catch (err) {
|
|
5436
|
-
|
|
5619
|
+
log6.debug("git", "Failed to release remote-init lock", { err });
|
|
5437
5620
|
}
|
|
5438
5621
|
}
|
|
5439
5622
|
remoteInitLocks.delete(options.remotePath);
|
|
@@ -5501,7 +5684,7 @@ var GitManager = class _GitManager {
|
|
|
5501
5684
|
* deleteBranchHandler: a head left in `remote.git` forever makes the
|
|
5502
5685
|
* create -> publish -> squash-merge -> delete -> reuse-the-name cycle reject
|
|
5503
5686
|
* the reused branch's first publish non-fast-forward against the stale head
|
|
5504
|
-
* (`GitManager.push()` pushes
|
|
5687
|
+
* (`GitManager.push()` pushes the branch to the same name, and a squash-merged old tip is
|
|
5505
5688
|
* not an ancestor of the new branch), and a retried submit then skips the
|
|
5506
5689
|
* local push on a clean tree and enqueues the worker push of the STALE head,
|
|
5507
5690
|
* resurrecting the deleted branch's content on GitHub as an apparent success.
|
|
@@ -5632,6 +5815,12 @@ var GitManager = class _GitManager {
|
|
|
5632
5815
|
return;
|
|
5633
5816
|
throw new Error(`CanopyCMS: refusing to use a network remote URL in prod mode (from ${source}: "${url}"). The standard AWS Lambda+EC2-worker topology runs the CMS Lambda with no internet access \u2014 the EC2 worker owns all network git I/O, and the Lambda is expected to reach the EFS-local bare repo ({workspace}/remote.git) via auto-detect instead. Pointing a network URL here would make the internet-less Lambda try to clone/fetch/push it directly and hang until timeout. If this prod host genuinely has internet access and intentionally runs git against a network remote (e.g. a single-VM deployment), set config.allowNetworkRemoteInProd: true to acknowledge this.`);
|
|
5634
5817
|
}
|
|
5818
|
+
/** The not-ready error for a mode whose remote is auto-detected, or `undefined` for other modes. */
|
|
5819
|
+
static async remoteNotReadyError(mode) {
|
|
5820
|
+
const { operatingStrategy: operatingStrategy2 } = await Promise.resolve().then(() => (init_operating_mode(), operating_mode_exports));
|
|
5821
|
+
const { autoDetectRemotePath } = operatingStrategy2(mode).getRemoteUrlConfig();
|
|
5822
|
+
return autoDetectRemotePath ? new RemoteNotReadyError(autoDetectRemotePath) : void 0;
|
|
5823
|
+
}
|
|
5635
5824
|
/**
|
|
5636
5825
|
* Resolves the remote URL for git operations following the priority:
|
|
5637
5826
|
* 1. Explicit remoteUrl parameter
|
|
@@ -5668,7 +5857,7 @@ var GitManager = class _GitManager {
|
|
|
5668
5857
|
try {
|
|
5669
5858
|
const stat = await fs16.stat(config.autoDetectRemotePath);
|
|
5670
5859
|
if (stat.isDirectory()) {
|
|
5671
|
-
|
|
5860
|
+
log6.debug("git", "Auto-detected local remote", {
|
|
5672
5861
|
path: config.autoDetectRemotePath
|
|
5673
5862
|
});
|
|
5674
5863
|
return config.autoDetectRemotePath;
|
|
@@ -5732,7 +5921,7 @@ var GitManager = class _GitManager {
|
|
|
5732
5921
|
try {
|
|
5733
5922
|
const stat = await fs16.stat(gitPath);
|
|
5734
5923
|
if (stat.isDirectory()) {
|
|
5735
|
-
|
|
5924
|
+
log6.debug("git", "Removing corrupt .git directory", {
|
|
5736
5925
|
workspacePath: options.workspacePath
|
|
5737
5926
|
});
|
|
5738
5927
|
await fs16.rm(gitPath, { recursive: true });
|
|
@@ -5743,6 +5932,7 @@ var GitManager = class _GitManager {
|
|
|
5743
5932
|
}
|
|
5744
5933
|
}
|
|
5745
5934
|
let justCloned = false;
|
|
5935
|
+
let resolvedRemoteUrl;
|
|
5746
5936
|
if (!repoExists) {
|
|
5747
5937
|
const remoteUrl = await _GitManager.resolveRemoteUrl({
|
|
5748
5938
|
mode: options.mode,
|
|
@@ -5753,6 +5943,9 @@ var GitManager = class _GitManager {
|
|
|
5753
5943
|
allowNetworkRemoteInProd: options.allowNetworkRemoteInProd
|
|
5754
5944
|
});
|
|
5755
5945
|
if (!remoteUrl) {
|
|
5946
|
+
const notReady = await _GitManager.remoteNotReadyError(options.mode);
|
|
5947
|
+
if (notReady)
|
|
5948
|
+
throw notReady;
|
|
5756
5949
|
throw new Error("CanopyCMS: defaultRemoteUrl (or CANOPYCMS_REMOTE_URL) is required to initialize workspace");
|
|
5757
5950
|
}
|
|
5758
5951
|
try {
|
|
@@ -5761,6 +5954,7 @@ var GitManager = class _GitManager {
|
|
|
5761
5954
|
throw new Error(`Failed to clone branch workspace at ${options.workspacePath} from ${remoteUrl} (base branch '${baseBranch}'): ${getErrorMessage(err)}`);
|
|
5762
5955
|
}
|
|
5763
5956
|
justCloned = true;
|
|
5957
|
+
resolvedRemoteUrl = remoteUrl;
|
|
5764
5958
|
const freshGit = simpleGit2({ baseDir: options.workspacePath });
|
|
5765
5959
|
freshGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path17.dirname(options.workspacePath) }));
|
|
5766
5960
|
await freshGit.addConfig("canopycms.managed", "true");
|
|
@@ -5776,7 +5970,7 @@ var GitManager = class _GitManager {
|
|
|
5776
5970
|
await git.git.addConfig("canopycms.managed", "true");
|
|
5777
5971
|
await git.git.addConfig("user.name", options.gitBotAuthorName);
|
|
5778
5972
|
await git.git.addConfig("user.email", options.gitBotAuthorEmail);
|
|
5779
|
-
|
|
5973
|
+
log6.debug("git", "Marked workspace as CanopyCMS-managed", {
|
|
5780
5974
|
workspacePath: options.workspacePath
|
|
5781
5975
|
});
|
|
5782
5976
|
if (!justCloned) {
|
|
@@ -5791,9 +5985,17 @@ var GitManager = class _GitManager {
|
|
|
5791
5985
|
if (remoteUrl) {
|
|
5792
5986
|
await git.ensureRemote(remoteUrl);
|
|
5793
5987
|
}
|
|
5988
|
+
resolvedRemoteUrl = remoteUrl;
|
|
5794
5989
|
}
|
|
5795
5990
|
if (options.branchType === "orphan") {
|
|
5796
|
-
|
|
5991
|
+
try {
|
|
5992
|
+
await git.createOrphanSettingsBranch(options.branchName, {});
|
|
5993
|
+
} catch (err) {
|
|
5994
|
+
if (err instanceof SettingsRemoteUnreadableError && !resolvedRemoteUrl) {
|
|
5995
|
+
throw await _GitManager.remoteNotReadyError(options.mode) ?? err;
|
|
5996
|
+
}
|
|
5997
|
+
throw err;
|
|
5998
|
+
}
|
|
5797
5999
|
await git.ensureGitExclude("*.lock");
|
|
5798
6000
|
} else {
|
|
5799
6001
|
await git.checkoutBranch(options.branchName);
|
|
@@ -5902,7 +6104,7 @@ var GitManager = class _GitManager {
|
|
|
5902
6104
|
const branches = await this.git.branch();
|
|
5903
6105
|
const currentBranch = branches.current;
|
|
5904
6106
|
try {
|
|
5905
|
-
await this.git.fetch(this.remote, currentBranch);
|
|
6107
|
+
await this.git.fetch(this.remote, `refs/heads/${currentBranch}`);
|
|
5906
6108
|
} catch (err) {
|
|
5907
6109
|
if (!isMissingRemoteRefFailure(getErrorMessage(err)))
|
|
5908
6110
|
throw err;
|
|
@@ -5959,19 +6161,34 @@ var GitManager = class _GitManager {
|
|
|
5959
6161
|
const fileArray = Array.isArray(files) ? files : [files];
|
|
5960
6162
|
await this.git.add(fileArray);
|
|
5961
6163
|
}
|
|
6164
|
+
/** Stage every working-tree change except canopycms's own state. See {@link stageAllExceptCanopyState}. */
|
|
6165
|
+
async addAllExceptCanopyState() {
|
|
6166
|
+
await stageAllExceptCanopyState(this.git);
|
|
6167
|
+
}
|
|
5962
6168
|
async commit(message) {
|
|
5963
6169
|
await this.git.commit(message);
|
|
5964
6170
|
}
|
|
5965
6171
|
async push(branch) {
|
|
5966
|
-
const target = branch ?? await this.
|
|
6172
|
+
const target = branch ?? await this.currentBranchName();
|
|
5967
6173
|
await this.git.raw([
|
|
5968
6174
|
"push",
|
|
5969
6175
|
"--set-upstream",
|
|
5970
6176
|
"--end-of-options",
|
|
5971
6177
|
this.remote,
|
|
5972
|
-
|
|
6178
|
+
`refs/heads/${target}:refs/heads/${target}`
|
|
5973
6179
|
]);
|
|
5974
6180
|
}
|
|
6181
|
+
/**
|
|
6182
|
+
* The checked-out branch's plain name. `rev-parse --abbrev-ref HEAD` prints
|
|
6183
|
+
* `heads/<name>` when a same-named tag exists, which is not a branch name.
|
|
6184
|
+
*/
|
|
6185
|
+
async currentBranchName() {
|
|
6186
|
+
const branches = await this.git.branch();
|
|
6187
|
+
if (branches.detached || !branches.current) {
|
|
6188
|
+
throw new Error(`CanopyCMS: no branch is checked out (detached HEAD) in ${this.repoPath}`);
|
|
6189
|
+
}
|
|
6190
|
+
return branches.current;
|
|
6191
|
+
}
|
|
5975
6192
|
/**
|
|
5976
6193
|
* Whether the local branch has commits the remote mirror doesn't -- i.e.
|
|
5977
6194
|
* whether push() would move the remote ref forward.
|
|
@@ -5990,12 +6207,12 @@ var GitManager = class _GitManager {
|
|
|
5990
6207
|
* ("couldn't find remote ref"); that counts as "ahead", not an error.
|
|
5991
6208
|
*/
|
|
5992
6209
|
async hasUnpushedCommits(branch) {
|
|
5993
|
-
const target = branch ?? await this.git.revparse(["--abbrev-ref", "--end-of-options", "HEAD"]);
|
|
5994
|
-
const localSha = (await this.git.revparse(["--end-of-options", target])).trim();
|
|
6210
|
+
const target = branch ?? await this.git.revparse(["--verify", "--abbrev-ref", "--end-of-options", "HEAD"]);
|
|
6211
|
+
const localSha = (await this.git.revparse(["--verify", "--end-of-options", target])).trim();
|
|
5995
6212
|
let fetchedTip;
|
|
5996
6213
|
try {
|
|
5997
6214
|
await this.git.raw(["fetch", "--end-of-options", this.remote, target]);
|
|
5998
|
-
fetchedTip = (await this.git.revparse(["--end-of-options", "FETCH_HEAD"])).trim();
|
|
6215
|
+
fetchedTip = (await this.git.revparse(["--verify", "--end-of-options", "FETCH_HEAD"])).trim();
|
|
5999
6216
|
} catch {
|
|
6000
6217
|
return true;
|
|
6001
6218
|
}
|
|
@@ -6004,6 +6221,25 @@ var GitManager = class _GitManager {
|
|
|
6004
6221
|
const aheadCount = (await this.git.raw(["rev-list", "--count", `${fetchedTip}..${localSha}`])).trim();
|
|
6005
6222
|
return aheadCount !== "0";
|
|
6006
6223
|
}
|
|
6224
|
+
/**
|
|
6225
|
+
* Repo-relative paths that differ between HEAD and its merge base with the
|
|
6226
|
+
* base branch: everything the branch changes, across all of its commits.
|
|
6227
|
+
* Diffs against the just-fetched base tip pinned to a SHA — the pullBaseInner
|
|
6228
|
+
* constraint.
|
|
6229
|
+
*/
|
|
6230
|
+
async listChangedPathsSinceBase() {
|
|
6231
|
+
await this.git.raw(["fetch", "--end-of-options", this.remote, this.baseBranch]);
|
|
6232
|
+
const baseTip = (await this.git.revparse(["--verify", "--end-of-options", "FETCH_HEAD"])).trim();
|
|
6233
|
+
const output = await this.git.raw([
|
|
6234
|
+
"diff",
|
|
6235
|
+
"--name-only",
|
|
6236
|
+
"-z",
|
|
6237
|
+
"--no-renames",
|
|
6238
|
+
`${baseTip}...HEAD`,
|
|
6239
|
+
"--"
|
|
6240
|
+
]);
|
|
6241
|
+
return output.split("\0").filter((p) => p.length > 0);
|
|
6242
|
+
}
|
|
6007
6243
|
async ensureAuthor(author) {
|
|
6008
6244
|
const config = await this.git.listConfig();
|
|
6009
6245
|
const isManaged = config.all["canopycms.managed"] === "true";
|
|
@@ -6057,10 +6293,24 @@ var GitManager = class _GitManager {
|
|
|
6057
6293
|
await ensureGitExcludePattern(this.repoPath, pattern);
|
|
6058
6294
|
}
|
|
6059
6295
|
/**
|
|
6060
|
-
*
|
|
6061
|
-
* deployment-specific settings never pollute content history. It holds
|
|
6062
|
-
* settings files committed by explicit path (permissions.json,
|
|
6063
|
-
* the workspace root).
|
|
6296
|
+
* Put the workspace on the settings branch: an orphan (no shared history),
|
|
6297
|
+
* so deployment-specific settings never pollute content history. It holds
|
|
6298
|
+
* only settings files committed by explicit path (permissions.json,
|
|
6299
|
+
* groups.json at the workspace root).
|
|
6300
|
+
*
|
|
6301
|
+
* The remote is the settings branch's durable copy, and the workspace clone
|
|
6302
|
+
* is `--single-branch` at the base branch, so the remote is asked first:
|
|
6303
|
+
* - no local branch: check out the remote's, or create an empty orphan only
|
|
6304
|
+
* when the remote has none. An unreadable remote throws
|
|
6305
|
+
* {@link SettingsRemoteUnreadableError} rather than guess "none".
|
|
6306
|
+
* - local branch present: a history unrelated to the remote's is repaired
|
|
6307
|
+
* onto the remote's when the local branch is still its empty initial
|
|
6308
|
+
* commit with a clean working tree, and otherwise throws
|
|
6309
|
+
* {@link SettingsBranchDivergedError}; local commits are never discarded.
|
|
6310
|
+
* An unreadable remote throws only for that empty branch, since one that
|
|
6311
|
+
* holds settings can serve them and the next save surfaces the remote error.
|
|
6312
|
+
* Either way a remote branch holding content is never adopted:
|
|
6313
|
+
* {@link SettingsBranchHasContentHistoryError}.
|
|
6064
6314
|
*/
|
|
6065
6315
|
async createOrphanSettingsBranch(branchName, initialFiles) {
|
|
6066
6316
|
try {
|
|
@@ -6070,11 +6320,31 @@ var GitManager = class _GitManager {
|
|
|
6070
6320
|
}
|
|
6071
6321
|
}
|
|
6072
6322
|
async createOrphanSettingsBranchInner(branchName, initialFiles) {
|
|
6073
|
-
|
|
6323
|
+
log6.debug("git", "Ensuring settings branch", { branchName });
|
|
6074
6324
|
const branches = await this.git.branch();
|
|
6075
|
-
|
|
6076
|
-
|
|
6325
|
+
const localExists = branches.all.includes(branchName);
|
|
6326
|
+
if (localExists) {
|
|
6077
6327
|
await this.git.checkout(branchName);
|
|
6328
|
+
}
|
|
6329
|
+
let remoteTip;
|
|
6330
|
+
try {
|
|
6331
|
+
remoteTip = await this.remoteBranchTip(branchName);
|
|
6332
|
+
} catch (err) {
|
|
6333
|
+
if (localExists && !await this.isEmptyInitialBranch(branchName)) {
|
|
6334
|
+
log6.debug("git", "Remote unreadable; serving the local settings branch", { branchName });
|
|
6335
|
+
return;
|
|
6336
|
+
}
|
|
6337
|
+
throw new SettingsRemoteUnreadableError(branchName, this.remote, err);
|
|
6338
|
+
}
|
|
6339
|
+
if (localExists) {
|
|
6340
|
+
await this.reconcileLocalSettingsBranch(branchName, remoteTip);
|
|
6341
|
+
return;
|
|
6342
|
+
}
|
|
6343
|
+
if (remoteTip) {
|
|
6344
|
+
const fetchedTip = await this.fetchBranchTip(branchName);
|
|
6345
|
+
await this.assertNotContentHistory(branchName, fetchedTip);
|
|
6346
|
+
await this.git.raw(["checkout", "-b", branchName, fetchedTip]);
|
|
6347
|
+
log6.debug("git", "Checked out the remote settings branch", { branchName });
|
|
6078
6348
|
return;
|
|
6079
6349
|
}
|
|
6080
6350
|
await this.git.raw(["checkout", "--orphan", branchName]);
|
|
@@ -6089,19 +6359,129 @@ var GitManager = class _GitManager {
|
|
|
6089
6359
|
await this.git.add(filePath);
|
|
6090
6360
|
}
|
|
6091
6361
|
await this.git.commit("Initialize settings branch", ["--allow-empty"]);
|
|
6092
|
-
|
|
6362
|
+
log6.debug("git", "Orphan settings branch created", { branchName });
|
|
6363
|
+
}
|
|
6364
|
+
/**
|
|
6365
|
+
* Compares the checked-out local settings branch with the remote's tip.
|
|
6366
|
+
* Related histories are left for the next settings pull to reconcile; only
|
|
6367
|
+
* an unrelated one is acted on here.
|
|
6368
|
+
*/
|
|
6369
|
+
async reconcileLocalSettingsBranch(branchName, remoteTip) {
|
|
6370
|
+
if (!remoteTip)
|
|
6371
|
+
return;
|
|
6372
|
+
const localTip = (await this.git.revparse([`refs/heads/${branchName}`])).trim();
|
|
6373
|
+
if (localTip === remoteTip)
|
|
6374
|
+
return;
|
|
6375
|
+
const fetchedTip = await this.fetchBranchTip(branchName);
|
|
6376
|
+
const [localRoots, remoteRoots] = await Promise.all([
|
|
6377
|
+
this.rootCommits(localTip),
|
|
6378
|
+
this.rootCommits(fetchedTip)
|
|
6379
|
+
]);
|
|
6380
|
+
if (localRoots.some((root) => remoteRoots.includes(root)))
|
|
6381
|
+
return;
|
|
6382
|
+
const localWork = [];
|
|
6383
|
+
if (!await this.isEmptyInitialBranch(branchName)) {
|
|
6384
|
+
localWork.push("commits beyond its empty initial commit");
|
|
6385
|
+
}
|
|
6386
|
+
const uncommitted = await this.getUncommittedFiles();
|
|
6387
|
+
if (uncommitted.length > 0) {
|
|
6388
|
+
localWork.push(`uncommitted changes to ${uncommitted.join(", ")}`);
|
|
6389
|
+
}
|
|
6390
|
+
if (localWork.length > 0) {
|
|
6391
|
+
throw new SettingsBranchDivergedError(branchName, this.repoPath, localWork.join(" and "));
|
|
6392
|
+
}
|
|
6393
|
+
await this.assertNotContentHistory(branchName, fetchedTip);
|
|
6394
|
+
await this.git.raw(["checkout", "-B", branchName, fetchedTip]);
|
|
6395
|
+
log6.debug("git", "Replaced an empty settings branch with the remote one", { branchName });
|
|
6396
|
+
}
|
|
6397
|
+
/**
|
|
6398
|
+
* The remote's tip for `branch`, or `undefined` when the remote has no such
|
|
6399
|
+
* branch. A remote that cannot be read throws: `ls-remote` exits 0 with no
|
|
6400
|
+
* output only for a reachable remote without the ref.
|
|
6401
|
+
*/
|
|
6402
|
+
async remoteBranchTip(branch) {
|
|
6403
|
+
const ref = `refs/heads/${branch}`;
|
|
6404
|
+
const output = await this.git.raw(["ls-remote", "--heads", this.remote, ref]);
|
|
6405
|
+
for (const line of output.split("\n")) {
|
|
6406
|
+
const [sha, name] = line.trim().split(" ");
|
|
6407
|
+
if (sha && name === ref)
|
|
6408
|
+
return sha;
|
|
6409
|
+
}
|
|
6410
|
+
return void 0;
|
|
6411
|
+
}
|
|
6412
|
+
/**
|
|
6413
|
+
* Fetch `branch` from the remote and return the fetched commit, pinned as
|
|
6414
|
+
* pullBaseInner explains. The full ref: a bare name resolves to a
|
|
6415
|
+
* same-named tag first.
|
|
6416
|
+
*/
|
|
6417
|
+
async fetchBranchTip(branch) {
|
|
6418
|
+
await this.git.fetch(this.remote, `refs/heads/${branch}`);
|
|
6419
|
+
return (await this.git.revparse(["FETCH_HEAD"])).trim();
|
|
6420
|
+
}
|
|
6421
|
+
/**
|
|
6422
|
+
* Refuse to adopt a remote settings branch that holds content: its tip has a file other than
|
|
6423
|
+
* the settings files, or its roots include the base branch's (a settings branch is an orphan).
|
|
6424
|
+
* Neither alone suffices: a base can have a root of its own, and a content tree can be pruned.
|
|
6425
|
+
* The base comes from the remote when the clone lacks it (it was cloned at an earlier base).
|
|
6426
|
+
*/
|
|
6427
|
+
async assertNotContentHistory(branchName, tip) {
|
|
6428
|
+
const tree = await this.git.raw(["ls-tree", "-r", "--name-only", tip]);
|
|
6429
|
+
const strays = tree.split("\n").filter((file) => file && !SETTINGS_BRANCH_FILES.has(file));
|
|
6430
|
+
if (strays.length > 0) {
|
|
6431
|
+
const shown = strays.slice(0, 3).join(", ") + (strays.length > 3 ? ", \u2026" : "");
|
|
6432
|
+
throw new SettingsBranchHasContentHistoryError(branchName, this.remote, `it holds ${shown}`);
|
|
6433
|
+
}
|
|
6434
|
+
const [tipRoots, baseRoots] = await Promise.all([
|
|
6435
|
+
this.rootCommits(tip),
|
|
6436
|
+
this.baseBranchRev(branchName).then((rev) => this.rootCommits(rev))
|
|
6437
|
+
]);
|
|
6438
|
+
if (tipRoots.some((root) => baseRoots.includes(root))) {
|
|
6439
|
+
throw new SettingsBranchHasContentHistoryError(branchName, this.remote, `it shares history with the base branch '${this.baseBranch}'`);
|
|
6440
|
+
}
|
|
6441
|
+
}
|
|
6442
|
+
async baseBranchRev(settingsBranch) {
|
|
6443
|
+
for (const ref of [
|
|
6444
|
+
`refs/heads/${this.baseBranch}`,
|
|
6445
|
+
`refs/remotes/${this.remote}/${this.baseBranch}`
|
|
6446
|
+
]) {
|
|
6447
|
+
const sha = await this.git.raw(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]).catch(() => "");
|
|
6448
|
+
if (sha.trim())
|
|
6449
|
+
return sha.trim();
|
|
6450
|
+
}
|
|
6451
|
+
try {
|
|
6452
|
+
return await this.fetchBranchTip(this.baseBranch);
|
|
6453
|
+
} catch (err) {
|
|
6454
|
+
throw new Error(`CanopyCMS: could not read base branch '${this.baseBranch}' from remote '${this.remote}', so settings branch '${settingsBranch}' was not checked out: ${getErrorMessage(err)}`);
|
|
6455
|
+
}
|
|
6456
|
+
}
|
|
6457
|
+
async rootCommits(rev) {
|
|
6458
|
+
const output = await this.git.raw(["rev-list", "--max-parents=0", rev]);
|
|
6459
|
+
return output.split("\n").filter(Boolean);
|
|
6460
|
+
}
|
|
6461
|
+
/**
|
|
6462
|
+
* Whether `branch` is a single parentless commit with an empty tree, as
|
|
6463
|
+
* orphan creation leaves it. Full refs throughout: a bare name resolves to a
|
|
6464
|
+
* same-named tag first, and clones fetch tags.
|
|
6465
|
+
*/
|
|
6466
|
+
async isEmptyInitialBranch(branch) {
|
|
6467
|
+
const ref = `refs/heads/${branch}`;
|
|
6468
|
+
const commits = await this.git.raw(["rev-list", "--max-count=2", ref]);
|
|
6469
|
+
if (commits.split("\n").filter(Boolean).length !== 1)
|
|
6470
|
+
return false;
|
|
6471
|
+
const tree = await this.git.raw(["ls-tree", ref]);
|
|
6472
|
+
return tree.trim() === "";
|
|
6093
6473
|
}
|
|
6094
6474
|
};
|
|
6095
6475
|
|
|
6096
6476
|
// dist/branch-workspace.js
|
|
6097
|
-
var
|
|
6477
|
+
var log7 = createDebugLogger({ prefix: "BranchWorkspace" });
|
|
6098
6478
|
var workspaceInitLocks = /* @__PURE__ */ new Map();
|
|
6099
6479
|
var BranchWorkspaceManager = class {
|
|
6100
6480
|
constructor(config) {
|
|
6101
6481
|
this.config = config;
|
|
6102
6482
|
}
|
|
6103
6483
|
async ensureGitWorkspace(options) {
|
|
6104
|
-
return
|
|
6484
|
+
return log7.timed("workspace", "ensureGitWorkspace", async () => {
|
|
6105
6485
|
const existingLock = workspaceInitLocks.get(options.branchRoot);
|
|
6106
6486
|
if (existingLock) {
|
|
6107
6487
|
await existingLock;
|
|
@@ -6110,11 +6490,11 @@ var BranchWorkspaceManager = class {
|
|
|
6110
6490
|
const lockPromise = (async () => {
|
|
6111
6491
|
let releaseLock;
|
|
6112
6492
|
try {
|
|
6113
|
-
|
|
6493
|
+
log7.debug("workspace", "Ensuring git workspace", {
|
|
6114
6494
|
branchName: options.branchName,
|
|
6115
6495
|
mode: options.mode
|
|
6116
6496
|
});
|
|
6117
|
-
releaseLock = await acquireProvisioningLock(path18.dirname(options.branchRoot),
|
|
6497
|
+
releaseLock = await acquireProvisioningLock(path18.dirname(options.branchRoot), branchProvisioningLockName(path18.basename(options.branchRoot)));
|
|
6118
6498
|
await GitManager.initializeWorkspace({
|
|
6119
6499
|
workspacePath: options.branchRoot,
|
|
6120
6500
|
branchName: options.branchName,
|
|
@@ -6135,7 +6515,7 @@ var BranchWorkspaceManager = class {
|
|
|
6135
6515
|
try {
|
|
6136
6516
|
await releaseLock();
|
|
6137
6517
|
} catch (err) {
|
|
6138
|
-
|
|
6518
|
+
log7.debug("workspace", "Failed to release workspace-init lock", { err });
|
|
6139
6519
|
}
|
|
6140
6520
|
}
|
|
6141
6521
|
workspaceInitLocks.delete(options.branchRoot);
|
|
@@ -6147,6 +6527,9 @@ var BranchWorkspaceManager = class {
|
|
|
6147
6527
|
}
|
|
6148
6528
|
async openOrCreateBranch(options) {
|
|
6149
6529
|
const { branchName, mode, basePathOverride, title, description, access, createdBy, remoteUrl } = options;
|
|
6530
|
+
if (isSettingsBranchName(branchName, operatingStrategy(mode).getSettingsBranchName(this.config))) {
|
|
6531
|
+
throw new BranchPathError("Settings branches are not content branches");
|
|
6532
|
+
}
|
|
6150
6533
|
const { branchRoot, baseRoot, branchName: safeName } = await ensureBranchRoot({
|
|
6151
6534
|
mode,
|
|
6152
6535
|
branchName,
|
|
@@ -6243,7 +6626,7 @@ import fs17 from "node:fs/promises";
|
|
|
6243
6626
|
import path19 from "node:path";
|
|
6244
6627
|
import matter3 from "gray-matter";
|
|
6245
6628
|
import { parse as yamlParse2 } from "yaml";
|
|
6246
|
-
var
|
|
6629
|
+
var log8 = createDebugLogger({ prefix: "ContentListing" });
|
|
6247
6630
|
var normalizePath = (root, target) => {
|
|
6248
6631
|
const result = validateAndNormalizePath(root, target);
|
|
6249
6632
|
if (!result.valid) {
|
|
@@ -6269,13 +6652,17 @@ var readEntryData = async (filePath, format, bodyFieldName = "body") => {
|
|
|
6269
6652
|
} catch (err) {
|
|
6270
6653
|
if (isNotFoundError(err))
|
|
6271
6654
|
return {};
|
|
6272
|
-
|
|
6655
|
+
log8.warn("readEntryData", `Failed to read entry data from ${filePath}: ${getErrorMessage(err)}`);
|
|
6273
6656
|
return {};
|
|
6274
6657
|
}
|
|
6275
6658
|
};
|
|
6276
|
-
var createReferenceResolver = (branchRoot, flatSchema, contentRootName) => ({
|
|
6659
|
+
var createReferenceResolver = (branchRoot, flatSchema, contentRootName, visibility) => ({
|
|
6277
6660
|
store: new ContentStore(branchRoot, flatSchema, { contentRootName }),
|
|
6278
|
-
cache: createReferenceResolveCache()
|
|
6661
|
+
cache: createReferenceResolveCache(),
|
|
6662
|
+
// The predicate that hides an entry from the listing is the one that restricts it as a
|
|
6663
|
+
// reference target, so a target is never shown in a sibling's data that its own listing
|
|
6664
|
+
// row would have hidden.
|
|
6665
|
+
access: visibility?.shouldInclude
|
|
6279
6666
|
});
|
|
6280
6667
|
var resolveCollectionItemReferences = async (items, collection, resolver) => Promise.all(items.map(async (item) => {
|
|
6281
6668
|
const fields = collection.entries?.find((e) => e.name === item.entryType)?.schema;
|
|
@@ -6283,7 +6670,7 @@ var resolveCollectionItemReferences = async (items, collection, resolver) => Pro
|
|
|
6283
6670
|
return item;
|
|
6284
6671
|
return {
|
|
6285
6672
|
...item,
|
|
6286
|
-
data: await resolver.store.resolveReferences(item.data, fields, resolver.cache)
|
|
6673
|
+
data: await resolver.store.resolveReferences(item.data, fields, resolver.cache, resolver.access)
|
|
6287
6674
|
};
|
|
6288
6675
|
}));
|
|
6289
6676
|
async function listEntries(branchRoot, flatSchema, contentRootName, options, visibility) {
|
|
@@ -6294,10 +6681,10 @@ async function listEntries(branchRoot, flatSchema, contentRootName, options, vis
|
|
|
6294
6681
|
const collections = flatSchema.filter((item) => item.type === "collection" && item.entries !== void 0 && (item.logicalPath === rootPath || item.logicalPath.startsWith(`${rootPath}/`)));
|
|
6295
6682
|
const shouldInclude = visibility?.shouldInclude;
|
|
6296
6683
|
const skippedFiles = [];
|
|
6297
|
-
const resolver = options?.resolveReferences ? createReferenceResolver(branchRoot, flatSchema, contentRootName) : null;
|
|
6684
|
+
const resolver = options?.resolveReferences ? createReferenceResolver(branchRoot, flatSchema, contentRootName, visibility) : null;
|
|
6298
6685
|
const collectionResults = await Promise.all(collections.map(async (collection) => {
|
|
6299
6686
|
const entries = await listCollectionEntries(branchRoot, collection, (file) => skippedFiles.push(file));
|
|
6300
|
-
const visible = shouldInclude ? entries.filter((e) => shouldInclude(e.
|
|
6687
|
+
const visible = shouldInclude ? entries.filter((e) => shouldInclude(e.logicalPath)) : entries;
|
|
6301
6688
|
const resolved = resolver ? await resolveCollectionItemReferences(visible, collection, resolver) : visible;
|
|
6302
6689
|
return resolved.map((entry) => ({ entry, collection }));
|
|
6303
6690
|
}));
|
|
@@ -6387,7 +6774,7 @@ var listCollectionEntries = async (root, collection, onSkip) => {
|
|
|
6387
6774
|
const relativePath = normalizePath(root, absolutePath);
|
|
6388
6775
|
const parsed = parseTypedFilename(file.name, entryTypes);
|
|
6389
6776
|
if (!parsed) {
|
|
6390
|
-
|
|
6777
|
+
log8.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)`);
|
|
6391
6778
|
if (looksLikeMalformedEntry(file.name, entryTypes)) {
|
|
6392
6779
|
onSkip?.({ filename: file.name, collectionPath: collection.logicalPath });
|
|
6393
6780
|
}
|
|
@@ -6402,7 +6789,7 @@ var listCollectionEntries = async (root, collection, onSkip) => {
|
|
|
6402
6789
|
readEntryData(absolutePath, format, bodyField)
|
|
6403
6790
|
]);
|
|
6404
6791
|
const item = {
|
|
6405
|
-
logicalPath:
|
|
6792
|
+
logicalPath: entryLogicalPath(collection.logicalPath, slug),
|
|
6406
6793
|
contentId,
|
|
6407
6794
|
slug,
|
|
6408
6795
|
collectionPath: collection.logicalPath,
|