canopycms 0.0.68-int.98 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2131 -0
- package/README.md +47 -63
- package/dist/ai/generate.d.ts +5 -0
- package/dist/ai/generate.js +244 -115
- package/dist/ai/index.d.ts +1 -1
- package/dist/ai/json-to-markdown.d.ts +21 -1
- package/dist/ai/json-to-markdown.js +120 -57
- package/dist/ai/references.d.ts +72 -0
- package/dist/ai/references.js +223 -0
- package/dist/ai/resolve-branch.js +9 -2
- package/dist/ai/to-plain-text.d.ts +17 -8
- package/dist/ai/to-plain-text.js +18 -15
- package/dist/ai/types.d.ts +84 -3
- package/dist/api/admin-branch-health.d.ts +24 -1
- package/dist/api/admin-branch-health.js +52 -20
- package/dist/api/admin.d.ts +23 -5
- package/dist/api/admin.js +36 -1
- package/dist/api/assets.js +88 -78
- package/dist/api/branch-create-window.d.ts +12 -0
- package/dist/api/branch-create-window.js +13 -0
- package/dist/api/branch-merge.d.ts +1 -1
- package/dist/api/branch-merge.js +1 -1
- package/dist/api/branch-review.d.ts +3 -3
- package/dist/api/branch-review.js +16 -7
- package/dist/api/branch-status.d.ts +16 -6
- package/dist/api/branch-status.js +57 -12
- package/dist/api/branch-withdraw.d.ts +2 -2
- package/dist/api/branch-withdraw.js +8 -3
- package/dist/api/branch.d.ts +25 -12
- package/dist/api/branch.js +164 -103
- package/dist/api/client.d.ts +78 -58
- package/dist/api/client.js +150 -117
- package/dist/api/comments.d.ts +12 -3
- package/dist/api/comments.js +118 -9
- package/dist/api/content.d.ts +9 -9
- package/dist/api/content.js +131 -22
- package/dist/api/entries-constants.d.ts +14 -3
- package/dist/api/entries-constants.js +14 -3
- package/dist/api/entries.d.ts +40 -2
- package/dist/api/entries.js +102 -6
- package/dist/api/github-sync.d.ts +19 -0
- package/dist/api/github-sync.js +50 -3
- package/dist/api/groups.d.ts +4 -4
- package/dist/api/groups.js +2 -1
- package/dist/api/guards.js +23 -3
- package/dist/api/index.d.ts +3 -1
- package/dist/api/index.js +1 -0
- package/dist/api/permissions.d.ts +21 -2
- package/dist/api/permissions.js +50 -3
- package/dist/api/request-deadline.d.ts +59 -0
- package/dist/api/request-deadline.js +73 -0
- package/dist/api/resolve-references.js +8 -7
- package/dist/api/route-builder.d.ts +14 -0
- package/dist/api/route-builder.js +14 -1
- package/dist/api/schema.d.ts +11 -9
- package/dist/api/schema.js +2 -0
- package/dist/api/settings-helpers.d.ts +5 -1
- package/dist/api/settings-helpers.js +10 -2
- package/dist/api/types.d.ts +18 -0
- package/dist/api/users-constants.d.ts +6 -0
- package/dist/api/users-constants.js +6 -0
- package/dist/assets/asset-url.d.ts +28 -16
- package/dist/assets/asset-url.js +51 -25
- package/dist/assets/finalize.js +3 -0
- package/dist/assets/index.d.ts +1 -1
- package/dist/assets/materialize.d.ts +153 -0
- package/dist/assets/materialize.js +402 -0
- package/dist/assets/store-local.d.ts +19 -5
- package/dist/assets/store-local.js +69 -10
- package/dist/assets/store-s3.d.ts +38 -7
- package/dist/assets/store-s3.js +128 -22
- package/dist/assets/transform-directives.d.ts +52 -13
- package/dist/assets/transform-directives.js +87 -27
- package/dist/assets/transform.d.ts +3 -3
- package/dist/assets/transform.js +13 -8
- package/dist/assets/types.d.ts +31 -4
- package/dist/auth/cache.d.ts +1 -1
- package/dist/auth/cache.js +1 -1
- package/dist/auth/caching-auth-plugin.d.ts +2 -0
- package/dist/auth/caching-auth-plugin.js +12 -2
- package/dist/auth/file-based-auth-cache.d.ts +8 -0
- package/dist/auth/file-based-auth-cache.js +18 -8
- package/dist/auth/person-name.d.ts +5 -0
- package/dist/auth/person-name.js +8 -0
- package/dist/auth/plugin.d.ts +10 -1
- package/dist/auth/types.d.ts +2 -0
- package/dist/auth/user-metadata-lookup.d.ts +21 -0
- package/dist/auth/user-metadata-lookup.js +116 -0
- package/dist/authorization/branch.d.ts +1 -1
- package/dist/authorization/branch.js +12 -13
- package/dist/authorization/groups/schema.d.ts +4 -4
- package/dist/authorization/helpers.d.ts +8 -0
- package/dist/authorization/helpers.js +8 -0
- package/dist/authorization/index.d.ts +1 -1
- package/dist/authorization/index.js +1 -1
- package/dist/authorization/protected-branch.d.ts +5 -4
- package/dist/authorization/protected-branch.js +7 -5
- package/dist/branch-health.d.ts +35 -8
- package/dist/branch-health.js +54 -18
- package/dist/branch-metadata-error.d.ts +27 -0
- package/dist/branch-metadata-error.js +23 -0
- package/dist/branch-metadata-file.d.ts +205 -24
- package/dist/branch-metadata-file.js +126 -25
- package/dist/branch-metadata.d.ts +45 -6
- package/dist/branch-metadata.js +142 -50
- package/dist/branch-provisioning.d.ts +167 -0
- package/dist/branch-provisioning.js +501 -0
- package/dist/branch-registry.d.ts +8 -0
- package/dist/branch-registry.js +61 -9
- package/dist/branch-schema-cache.d.ts +29 -12
- package/dist/branch-schema-cache.js +64 -21
- package/dist/branch-sparse.d.ts +23 -0
- package/dist/branch-sparse.js +118 -0
- package/dist/branch-workspace.d.ts +42 -3
- package/dist/branch-workspace.js +239 -100
- package/dist/build/asset-refs.d.ts +94 -0
- package/dist/build/asset-refs.js +267 -0
- package/dist/build/generate-ai-content.js +5 -0
- package/dist/build/inline-flight.d.ts +37 -0
- package/dist/build/inline-flight.js +142 -0
- package/dist/cli/asset-refs.d.ts +52 -0
- package/dist/cli/asset-refs.js +194 -0
- package/dist/cli/cli.d.ts +15 -1
- package/dist/cli/cli.js +8543 -3193
- package/dist/cli/configured-asset-store.d.ts +12 -0
- package/dist/cli/configured-asset-store.js +46 -0
- package/dist/cli/generate-ai-content.js +4244 -1072
- package/dist/cli/init.js +217 -126
- package/dist/cli/project-detect.d.ts +2 -0
- package/dist/cli/project-detect.js +2 -0
- package/dist/cli/template-files/Dockerfile.cms.template +13 -5
- package/dist/cli/template-files/cms-stack.ts.template +16 -13
- package/dist/client.d.ts +1 -2
- package/dist/client.js +1 -2
- package/dist/comment-store.d.ts +25 -0
- package/dist/comment-store.js +43 -8
- package/dist/config/flatten.js +2 -0
- package/dist/config/helpers.js +2 -2
- package/dist/config/index.d.ts +1 -1
- package/dist/config/schemas/config.d.ts +12 -29
- package/dist/config/schemas/config.js +9 -12
- package/dist/config/schemas/field.d.ts +3 -0
- package/dist/config/schemas/field.js +58 -4
- package/dist/config/schemas/media.d.ts +4 -13
- package/dist/config/schemas/media.js +5 -8
- package/dist/config/schemas/url.d.ts +2 -20
- package/dist/config/schemas/url.js +2 -25
- package/dist/config/types.d.ts +116 -22
- package/dist/config/types.js +3 -9
- package/dist/config/validation.d.ts +25 -4
- package/dist/config/validation.js +116 -3
- package/dist/content-id-index.js +2 -2
- package/dist/content-index-generation.d.ts +1 -1
- package/dist/content-index-generation.js +1 -1
- package/dist/content-listing.d.ts +10 -1
- package/dist/content-listing.js +22 -5
- package/dist/content-reader.d.ts +9 -0
- package/dist/content-reader.js +16 -11
- package/dist/content-store.d.ts +75 -12
- package/dist/content-store.js +249 -86
- package/dist/content-tree.js +2 -2
- package/dist/context.js +5 -2
- package/dist/dangling-reference-log.d.ts +17 -0
- package/dist/dangling-reference-log.js +29 -0
- package/dist/dev-content-watcher.d.ts +1 -1
- package/dist/dev-content-watcher.js +1 -1
- package/dist/editor/BranchManager.d.ts +23 -7
- package/dist/editor/BranchManager.js +119 -54
- package/dist/editor/CanopyEditor.d.ts +1 -1
- package/dist/editor/CanopyEditor.js +1 -2
- package/dist/editor/CommentsPanel.d.ts +14 -0
- package/dist/editor/CommentsPanel.js +68 -43
- package/dist/editor/Editor.d.ts +11 -5
- package/dist/editor/Editor.js +891 -218
- package/dist/editor/EditorAuthGate.d.ts +7 -1
- package/dist/editor/EditorAuthGate.js +41 -9
- package/dist/editor/EntryNavigator.d.ts +22 -0
- package/dist/editor/EntryNavigator.js +353 -105
- package/dist/editor/FormHeader.d.ts +25 -0
- package/dist/editor/FormHeader.js +82 -0
- package/dist/editor/FormRenderer.d.ts +27 -3
- package/dist/editor/FormRenderer.js +171 -76
- package/dist/editor/PreviewFrame.d.ts +40 -0
- package/dist/editor/PreviewFrame.js +215 -0
- package/dist/editor/PreviewPane.d.ts +47 -0
- package/dist/editor/PreviewPane.js +147 -0
- package/dist/editor/PreviewToolbarTools.d.ts +22 -0
- package/dist/editor/PreviewToolbarTools.js +73 -0
- package/dist/editor/admin/SystemHealthPanel.d.ts +2 -1
- package/dist/editor/admin/SystemHealthPanel.js +163 -33
- package/dist/editor/admin/useSystemHealth.d.ts +18 -0
- package/dist/editor/admin/useSystemHealth.js +70 -1
- package/dist/editor/branch-status.d.ts +17 -0
- package/dist/editor/branch-status.js +29 -0
- package/dist/editor/canopy-path.d.ts +9 -6
- package/dist/editor/canopy-path.js +8 -11
- package/dist/editor/client-reference-resolver.d.ts +43 -16
- package/dist/editor/client-reference-resolver.js +155 -64
- package/dist/editor/comments/BranchComments.d.ts +1 -1
- package/dist/editor/comments/BranchComments.js +2 -2
- package/dist/editor/comments/CloseRefusedNotice.d.ts +4 -0
- package/dist/editor/comments/CloseRefusedNotice.js +7 -0
- package/dist/editor/comments/CommentThreadsPopover.d.ts +47 -0
- package/dist/editor/comments/CommentThreadsPopover.js +274 -0
- package/dist/editor/comments/CommentWriteAlert.d.ts +11 -0
- package/dist/editor/comments/CommentWriteAlert.js +13 -0
- package/dist/editor/comments/EntryComments.d.ts +10 -4
- package/dist/editor/comments/EntryComments.js +14 -17
- package/dist/editor/comments/FieldWrapper.d.ts +18 -4
- package/dist/editor/comments/FieldWrapper.js +62 -28
- package/dist/editor/comments/InlineCommentThread.d.ts +9 -2
- package/dist/editor/comments/InlineCommentThread.js +11 -47
- package/dist/editor/comments/ThreadCarousel.d.ts +1 -1
- package/dist/editor/comments/ThreadCarousel.js +18 -21
- package/dist/editor/comments/ThreadReplyControls.d.ts +28 -0
- package/dist/editor/comments/ThreadReplyControls.js +92 -0
- package/dist/editor/comments/field-comment-context.d.ts +14 -0
- package/dist/editor/comments/field-comment-context.js +14 -0
- package/dist/editor/comments/form-comments.d.ts +48 -0
- package/dist/editor/comments/form-comments.js +178 -0
- package/dist/editor/comments/item-anchor-copy.d.ts +6 -0
- package/dist/editor/comments/item-anchor-copy.js +6 -0
- package/dist/editor/comments/move-focus-if-unmoved.d.ts +6 -0
- package/dist/editor/comments/move-focus-if-unmoved.js +11 -0
- package/dist/editor/comments/popover-placement.d.ts +18 -0
- package/dist/editor/comments/popover-placement.js +58 -0
- package/dist/editor/comments/use-tracked-writes.d.ts +16 -0
- package/dist/editor/comments/use-tracked-writes.js +53 -0
- package/dist/editor/components/BranchAccessList.d.ts +15 -0
- package/dist/editor/components/BranchAccessList.js +52 -0
- package/dist/editor/components/BranchLockBanner.d.ts +22 -0
- package/dist/editor/components/BranchLockBanner.js +39 -0
- package/dist/editor/components/BranchPill.d.ts +32 -0
- package/dist/editor/components/BranchPill.js +33 -0
- package/dist/editor/components/BranchesDrawer.d.ts +11 -0
- package/dist/editor/components/BranchesDrawer.js +5 -0
- package/dist/editor/components/ConfirmDeleteModal.d.ts +6 -1
- package/dist/editor/components/ConfirmDeleteModal.js +3 -2
- package/dist/editor/components/DestructiveMenuItem.d.ts +7 -0
- package/dist/editor/components/DestructiveMenuItem.js +8 -0
- package/dist/editor/components/EditorCrashScreen.d.ts +17 -0
- package/dist/editor/components/EditorCrashScreen.js +22 -0
- package/dist/editor/components/EditorErrorBoundary.d.ts +38 -0
- package/dist/editor/components/EditorErrorBoundary.js +40 -0
- package/dist/editor/components/EditorHeader.d.ts +56 -106
- package/dist/editor/components/EditorHeader.js +130 -131
- package/dist/editor/components/EditorRail.d.ts +49 -0
- package/dist/editor/components/EditorRail.js +65 -0
- package/dist/editor/components/EntryBreadcrumb.d.ts +33 -0
- package/dist/editor/components/EntryBreadcrumb.js +56 -0
- package/dist/editor/components/EntryCreateModal.d.ts +12 -7
- package/dist/editor/components/EntryCreateModal.js +51 -57
- package/dist/editor/components/NoEditPermissionNotice.d.ts +6 -5
- package/dist/editor/components/NoEditPermissionNotice.js +5 -4
- package/dist/editor/components/QuickOpenPalette.d.ts +22 -0
- package/dist/editor/components/QuickOpenPalette.js +66 -0
- package/dist/editor/components/ReadOnlyDraftNotice.d.ts +11 -0
- package/dist/editor/components/ReadOnlyDraftNotice.js +12 -0
- package/dist/editor/components/ReasonedMenuItem.d.ts +15 -0
- package/dist/editor/components/ReasonedMenuItem.js +15 -0
- package/dist/editor/components/ReferencedByList.d.ts +11 -0
- package/dist/editor/components/ReferencedByList.js +28 -0
- package/dist/editor/components/RelativeTime.d.ts +10 -0
- package/dist/editor/components/RelativeTime.js +10 -0
- package/dist/editor/components/RenameEntryModal.d.ts +9 -1
- package/dist/editor/components/RenameEntryModal.js +19 -31
- package/dist/editor/components/RetryToastMessage.d.ts +9 -0
- package/dist/editor/components/RetryToastMessage.js +6 -0
- package/dist/editor/components/SaveAllResult.d.ts +25 -0
- package/dist/editor/components/SaveAllResult.js +86 -0
- package/dist/editor/components/SaveSplitButton.d.ts +30 -0
- package/dist/editor/components/SaveSplitButton.js +108 -0
- package/dist/editor/components/StagedChangesDrawer.d.ts +39 -0
- package/dist/editor/components/StagedChangesDrawer.js +92 -0
- package/dist/editor/components/UnavailableEntryNotice.d.ts +6 -0
- package/dist/editor/components/UnavailableEntryNotice.js +9 -0
- package/dist/editor/components/UnavailableTypeMessage.d.ts +6 -0
- package/dist/editor/components/UnavailableTypeMessage.js +9 -0
- package/dist/editor/components/UndoToastMessage.d.ts +18 -0
- package/dist/editor/components/UndoToastMessage.js +10 -0
- package/dist/editor/components/UserBadge.d.ts +2 -5
- package/dist/editor/components/UserBadge.js +62 -33
- package/dist/editor/components/ValidationSummary.d.ts +14 -0
- package/dist/editor/components/ValidationSummary.js +24 -0
- package/dist/editor/components/index.d.ts +2 -2
- package/dist/editor/components/index.js +2 -2
- package/dist/editor/context/ApiClientContext.d.ts +8 -2
- package/dist/editor/context/ApiClientContext.js +19 -8
- package/dist/editor/context/AssetContext.d.ts +13 -35
- package/dist/editor/context/AssetContext.js +12 -31
- package/dist/editor/context/EditorStateContext.d.ts +4 -4
- package/dist/editor/context/EditorStateContext.js +4 -4
- package/dist/editor/context/SWRProvider.d.ts +3 -3
- package/dist/editor/context/SiteMdxAllowContext.d.ts +4 -0
- package/dist/editor/context/SiteMdxAllowContext.js +7 -0
- package/dist/editor/context/index.d.ts +2 -1
- package/dist/editor/context/index.js +2 -1
- package/dist/editor/contrast.d.ts +19 -0
- package/dist/editor/contrast.js +41 -0
- package/dist/editor/copy.d.ts +18 -0
- package/dist/editor/copy.js +18 -0
- package/dist/editor/editor-config.js +16 -0
- package/dist/editor/editor-utils.d.ts +34 -8
- package/dist/editor/editor-utils.js +42 -20
- package/dist/editor/entry-search.d.ts +20 -0
- package/dist/editor/entry-search.js +34 -0
- package/dist/editor/field-breadcrumb.d.ts +12 -0
- package/dist/editor/field-breadcrumb.js +85 -0
- package/dist/editor/field-props.d.ts +86 -0
- package/dist/editor/field-props.js +27 -0
- package/dist/editor/field-target.d.ts +30 -0
- package/dist/editor/field-target.js +77 -0
- package/dist/editor/fields/BlockField.d.ts +12 -0
- package/dist/editor/fields/BlockField.js +89 -73
- package/dist/editor/fields/CodeField.d.ts +2 -0
- package/dist/editor/fields/CodeField.js +6 -2
- package/dist/editor/fields/DateTimeField.d.ts +4 -29
- package/dist/editor/fields/DateTimeField.js +79 -41
- package/dist/editor/fields/ExpandAllButton.d.ts +6 -0
- package/dist/editor/fields/ExpandAllButton.js +4 -0
- package/dist/editor/fields/FieldCrashFallback.d.ts +17 -0
- package/dist/editor/fields/FieldCrashFallback.js +30 -0
- package/dist/editor/fields/FieldLabel.d.ts +37 -0
- package/dist/editor/fields/FieldLabel.js +19 -0
- package/dist/editor/fields/ImageField.d.ts +4 -0
- package/dist/editor/fields/ImageField.js +29 -14
- package/dist/editor/fields/InlineGroupField.js +4 -3
- package/dist/editor/fields/ItemList.d.ts +42 -0
- package/dist/editor/fields/ItemList.js +130 -0
- package/dist/editor/fields/MarkdownField.d.ts +32 -0
- package/dist/editor/fields/MarkdownField.js +223 -46
- package/dist/editor/fields/MdxImageDialog.js +1 -1
- package/dist/editor/fields/NumberField.d.ts +2 -0
- package/dist/editor/fields/NumberField.js +8 -5
- package/dist/editor/fields/NumberListField.d.ts +2 -0
- package/dist/editor/fields/NumberListField.js +19 -16
- package/dist/editor/fields/ObjectField.d.ts +2 -7
- package/dist/editor/fields/ObjectField.js +15 -5
- package/dist/editor/fields/ObjectListField.d.ts +29 -0
- package/dist/editor/fields/ObjectListField.js +57 -0
- package/dist/editor/fields/ReferenceField.d.ts +2 -0
- package/dist/editor/fields/ReferenceField.js +34 -9
- package/dist/editor/fields/SelectField.d.ts +2 -0
- package/dist/editor/fields/SelectField.js +11 -2
- package/dist/editor/fields/StringListField.d.ts +4 -1
- package/dist/editor/fields/StringListField.js +22 -8
- package/dist/editor/fields/TextField.d.ts +2 -0
- package/dist/editor/fields/TextField.js +6 -2
- package/dist/editor/fields/ToggleField.d.ts +2 -0
- package/dist/editor/fields/ToggleField.js +8 -3
- package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +1 -1
- package/dist/editor/fields/entry-link/InsertEntryLink.js +1 -1
- package/dist/editor/fields/form-expansion.d.ts +9 -0
- package/dist/editor/fields/form-expansion.js +2 -0
- package/dist/editor/fields/item-expansion.d.ts +15 -0
- package/dist/editor/fields/item-expansion.js +60 -0
- package/dist/editor/fields/list-item-keys.d.ts +8 -0
- package/dist/editor/fields/list-item-keys.js +50 -0
- package/dist/editor/fields/list-item-title.d.ts +20 -0
- package/dist/editor/fields/list-item-title.js +85 -0
- package/dist/editor/fields/markdown-export-options.d.ts +2 -0
- package/dist/editor/fields/markdown-export-options.js +8 -0
- package/dist/editor/fields/markdown-fidelity-visitors.d.ts +11 -0
- package/dist/editor/fields/markdown-fidelity-visitors.js +160 -0
- package/dist/editor/fields/mdx-jsx-support.d.ts +5 -1
- package/dist/editor/fields/mdx-jsx-support.js +157 -69
- package/dist/editor/fields/rich-text-failures.d.ts +5 -0
- package/dist/editor/fields/rich-text-failures.js +26 -0
- package/dist/editor/fields/toolbar-wrap.d.ts +7 -0
- package/dist/editor/fields/toolbar-wrap.js +35 -0
- package/dist/editor/form-outline.d.ts +13 -0
- package/dist/editor/form-outline.js +70 -0
- package/dist/editor/group-manager/GroupCard.d.ts +4 -1
- package/dist/editor/group-manager/GroupCard.js +12 -3
- package/dist/editor/group-manager/GroupForm.js +1 -1
- package/dist/editor/group-manager/InternalGroupsTab.d.ts +2 -1
- package/dist/editor/group-manager/InternalGroupsTab.js +4 -4
- package/dist/editor/group-manager/MemberList.d.ts +2 -1
- package/dist/editor/group-manager/MemberList.js +15 -4
- package/dist/editor/group-manager/hooks/useGroupState.d.ts +5 -0
- package/dist/editor/group-manager/hooks/useGroupState.js +30 -7
- package/dist/editor/group-manager/index.js +34 -5
- package/dist/editor/group-manager/types.d.ts +5 -2
- package/dist/editor/hooks/create-branch-request.d.ts +20 -0
- package/dist/editor/hooks/create-branch-request.js +54 -0
- package/dist/editor/hooks/save-all.d.ts +68 -0
- package/dist/editor/hooks/save-all.js +61 -0
- package/dist/editor/hooks/useBranchActions.d.ts +11 -1
- package/dist/editor/hooks/useBranchActions.js +42 -19
- package/dist/editor/hooks/useBranchManager.d.ts +21 -0
- package/dist/editor/hooks/useBranchManager.js +321 -125
- package/dist/editor/hooks/useBranchesData.d.ts +8 -4
- package/dist/editor/hooks/useBranchesData.js +9 -5
- package/dist/editor/hooks/useCommentSystem.d.ts +14 -2
- package/dist/editor/hooks/useCommentSystem.js +79 -55
- package/dist/editor/hooks/useCommentsData.d.ts +11 -7
- package/dist/editor/hooks/useCommentsData.js +20 -19
- package/dist/editor/hooks/useDraftManager.d.ts +77 -3
- package/dist/editor/hooks/useDraftManager.js +887 -94
- package/dist/editor/hooks/useEditorLayout.d.ts +53 -7
- package/dist/editor/hooks/useEditorLayout.js +98 -20
- package/dist/editor/hooks/useEntriesData.d.ts +2 -1
- package/dist/editor/hooks/useEntriesData.js +18 -4
- package/dist/editor/hooks/useEntryManager.d.ts +41 -2
- package/dist/editor/hooks/useEntryManager.js +227 -41
- package/dist/editor/hooks/useGroupManager.js +13 -16
- package/dist/editor/hooks/usePermissionManager.js +14 -6
- package/dist/editor/hooks/usePreviewMarks.d.ts +21 -0
- package/dist/editor/hooks/usePreviewMarks.js +33 -0
- package/dist/editor/hooks/useReferenceResolution.d.ts +10 -3
- package/dist/editor/hooks/useReferenceResolution.js +34 -177
- package/dist/editor/hooks/useSchemaManager.d.ts +25 -6
- package/dist/editor/hooks/useSchemaManager.js +97 -39
- package/dist/editor/hooks/useUserMetadata.d.ts +14 -4
- package/dist/editor/hooks/useUserMetadata.js +76 -47
- package/dist/editor/hooks/user-metadata-batcher.d.ts +8 -0
- package/dist/editor/hooks/user-metadata-batcher.js +33 -0
- package/dist/editor/media/AssetCard.d.ts +6 -7
- package/dist/editor/media/AssetCard.js +24 -31
- package/dist/editor/media/AssetListRow.d.ts +16 -0
- package/dist/editor/media/AssetListRow.js +41 -0
- package/dist/editor/media/CropStep.js +6 -1
- package/dist/editor/media/MediaLibrary.d.ts +4 -3
- package/dist/editor/media/MediaLibrary.js +11 -4
- package/dist/editor/media/MediaLibraryBody.d.ts +3 -3
- package/dist/editor/media/MediaLibraryBody.js +195 -80
- package/dist/editor/media/MediaLibrarySkeleton.d.ts +6 -0
- package/dist/editor/media/MediaLibrarySkeleton.js +14 -0
- package/dist/editor/media/crop-math.d.ts +3 -4
- package/dist/editor/media/crop-math.js +11 -22
- package/dist/editor/media/editor-image-src.d.ts +8 -0
- package/dist/editor/media/editor-image-src.js +10 -0
- package/dist/editor/media/format-bytes.d.ts +2 -0
- package/dist/editor/media/format-bytes.js +23 -0
- package/dist/editor/media/media-library-prefs.d.ts +23 -0
- package/dist/editor/media/media-library-prefs.js +49 -0
- package/dist/editor/media/media-library-view.d.ts +34 -0
- package/dist/editor/media/media-library-view.js +40 -0
- package/dist/editor/media/useAssetLibrary.d.ts +30 -0
- package/dist/editor/media/useAssetLibrary.js +126 -0
- package/dist/editor/media/useStableAssetOrder.d.ts +11 -0
- package/dist/editor/media/useStableAssetOrder.js +37 -0
- package/dist/editor/media/useThumbnailFailure.d.ts +6 -0
- package/dist/editor/media/useThumbnailFailure.js +16 -0
- package/dist/editor/permission-manager/GroupSelector.js +2 -2
- package/dist/editor/permission-manager/PermissionEditor.d.ts +1 -1
- package/dist/editor/permission-manager/PermissionEditor.js +15 -4
- package/dist/editor/permission-manager/PermissionLevelBadge.js +1 -1
- package/dist/editor/permission-manager/PermissionTree.d.ts +1 -1
- package/dist/editor/permission-manager/UserSelector.d.ts +1 -1
- package/dist/editor/permission-manager/UserSelector.js +3 -3
- package/dist/editor/permission-manager/index.js +12 -3
- package/dist/editor/permission-manager/types.d.ts +5 -2
- package/dist/editor/platform.d.ts +3 -0
- package/dist/editor/platform.js +9 -0
- package/dist/editor/preview-asset-base.d.ts +9 -0
- package/dist/editor/preview-asset-base.js +17 -0
- package/dist/editor/preview-bridge.d.ts +51 -27
- package/dist/editor/preview-bridge.js +103 -144
- package/dist/editor/preview-marks.d.ts +15 -0
- package/dist/editor/preview-marks.js +74 -0
- package/dist/editor/preview-path.d.ts +12 -0
- package/dist/editor/preview-path.js +45 -1
- package/dist/editor/preview-scale.d.ts +27 -0
- package/dist/editor/preview-scale.js +34 -0
- package/dist/editor/raw-asset-base.d.ts +5 -0
- package/dist/editor/raw-asset-base.js +14 -0
- package/dist/editor/relative-time.d.ts +4 -4
- package/dist/editor/relative-time.js +16 -8
- package/dist/editor/schema-editor/CollectionEditor.d.ts +20 -3
- package/dist/editor/schema-editor/CollectionEditor.js +361 -62
- package/dist/editor/schema-editor/EntryTypeEditor.d.ts +5 -1
- package/dist/editor/schema-editor/EntryTypeEditor.js +11 -6
- package/dist/editor/schema-editor/collection-draft.d.ts +22 -0
- package/dist/editor/schema-editor/collection-draft.js +40 -0
- package/dist/editor/schema-editor/index.d.ts +1 -1
- package/dist/editor/schema-label.d.ts +10 -0
- package/dist/editor/schema-label.js +17 -0
- package/dist/editor/settings-conflict.d.ts +5 -0
- package/dist/editor/settings-conflict.js +8 -0
- package/dist/editor/shell/ActivityRail.d.ts +37 -0
- package/dist/editor/shell/ActivityRail.js +91 -0
- package/dist/editor/shell/EditorShell.d.ts +17 -0
- package/dist/editor/shell/EditorShell.js +31 -0
- package/dist/editor/shell/PaneLayout.d.ts +28 -0
- package/dist/editor/shell/PaneLayout.js +116 -0
- package/dist/editor/shell/SidePanelHost.d.ts +35 -0
- package/dist/editor/shell/SidePanelHost.js +184 -0
- package/dist/editor/shell/TopBar.d.ts +12 -0
- package/dist/editor/shell/TopBar.js +14 -0
- package/dist/editor/shell/host-chrome.d.ts +16 -0
- package/dist/editor/shell/host-chrome.js +69 -0
- package/dist/editor/slug.d.ts +16 -0
- package/dist/editor/slug.js +43 -0
- package/dist/editor/theme.d.ts +2 -1
- package/dist/editor/theme.js +314 -13
- package/dist/editor/unanswered.d.ts +17 -0
- package/dist/editor/unanswered.js +26 -0
- package/dist/editor/unavailable-entry-type.d.ts +20 -0
- package/dist/editor/unavailable-entry-type.js +28 -0
- package/dist/editor/undo-removal.d.ts +57 -0
- package/dist/editor/undo-removal.js +163 -0
- package/dist/editor/user-display.d.ts +4 -0
- package/dist/editor/user-display.js +5 -0
- package/dist/editor/utils/confirm-modal.d.ts +8 -0
- package/dist/editor/utils/confirm-modal.js +12 -0
- package/dist/editor/utils/editor-errors.d.ts +19 -0
- package/dist/editor/utils/editor-errors.js +32 -0
- package/dist/editor/utils/focus-return.d.ts +56 -0
- package/dist/editor/utils/focus-return.js +106 -0
- package/dist/entry-link-resolver.d.ts +11 -1
- package/dist/entry-link-resolver.js +65 -5
- package/dist/entry-schema-registry.d.ts +3 -3
- package/dist/entry-schema-registry.js +7 -4
- package/dist/entry-schema.d.ts +49 -3
- package/dist/entry-schema.js +18 -0
- package/dist/git-manager.d.ts +172 -10
- package/dist/git-manager.js +449 -107
- package/dist/github-service.d.ts +39 -2
- package/dist/github-service.js +101 -7
- package/dist/http/handler.js +74 -9
- package/dist/http/types.d.ts +2 -0
- package/dist/http/worker-not-ready.d.ts +9 -4
- package/dist/http/worker-not-ready.js +59 -6
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/item-keys.d.ts +46 -0
- package/dist/item-keys.js +235 -0
- package/dist/operating-mode/editor-mode-check.d.ts +15 -0
- package/dist/operating-mode/editor-mode-check.js +24 -0
- package/dist/operating-mode/mode-env.d.ts +13 -14
- package/dist/operating-mode/mode-env.js +13 -14
- package/dist/operating-mode/types.d.ts +2 -2
- package/dist/paths/branch.d.ts +0 -1
- package/dist/paths/branch.js +1 -8
- package/dist/paths/index.d.ts +1 -1
- package/dist/paths/index.js +1 -1
- package/dist/paths/validation.js +0 -2
- package/dist/preview.d.ts +8 -0
- package/dist/preview.js +7 -0
- package/dist/reference-resolver.js +1 -0
- package/dist/resolve-canopy-user.d.ts +1 -1
- package/dist/resolve-canopy-user.js +1 -1
- package/dist/schema/meta-loader.d.ts +18 -8
- package/dist/schema/meta-loader.js +42 -9
- package/dist/schema/registry-fingerprint.d.ts +14 -0
- package/dist/schema/registry-fingerprint.js +63 -0
- package/dist/schema/resolver.d.ts +7 -4
- package/dist/schema/resolver.js +7 -6
- package/dist/schema/schema-store.d.ts +7 -10
- package/dist/schema/schema-store.js +13 -11
- package/dist/schema/schema-unavailable-error.d.ts +11 -0
- package/dist/schema/schema-unavailable-error.js +15 -0
- package/dist/schema/types.d.ts +22 -0
- package/dist/schema-registry-record.d.ts +35 -0
- package/dist/schema-registry-record.js +134 -0
- package/dist/server.d.ts +79 -9
- package/dist/server.js +62 -8
- package/dist/services.d.ts +33 -5
- package/dist/services.js +155 -37
- package/dist/settings-workspace.d.ts +23 -3
- package/dist/settings-workspace.js +159 -45
- package/dist/stamp-on-publish.d.ts +36 -0
- package/dist/stamp-on-publish.js +71 -0
- package/dist/static/index.d.ts +58 -5
- package/dist/static/index.js +133 -3
- package/dist/static/seo.d.ts +5 -5
- package/dist/static/seo.js +5 -5
- package/dist/submission-attribution.d.ts +8 -0
- package/dist/submission-attribution.js +22 -0
- package/dist/system-users.d.ts +12 -0
- package/dist/system-users.js +12 -0
- package/dist/task-queue/cms-task-queue.d.ts +1 -1
- package/dist/task-queue/cms-task-queue.js +1 -1
- package/dist/task-queue/index.d.ts +1 -1
- package/dist/task-queue/index.js +1 -1
- package/dist/task-queue/task-queue.d.ts +6 -0
- package/dist/task-queue/task-queue.js +29 -0
- package/dist/task-queue/worker-status.d.ts +35 -9
- package/dist/task-queue/worker-status.js +84 -17
- package/dist/types.d.ts +90 -9
- package/dist/url-collision.js +3 -3
- package/dist/url-exclusivity-fixtures.d.ts +8 -1
- package/dist/url-exclusivity-fixtures.js +11 -1
- package/dist/user.js +2 -1
- package/dist/utils/atomic-write.js +2 -1
- package/dist/utils/base-branch.d.ts +13 -0
- package/dist/utils/base-branch.js +23 -0
- package/dist/utils/content-serialize.d.ts +15 -7
- package/dist/utils/content-serialize.js +353 -51
- package/dist/utils/content-write-lock.d.ts +6 -1
- package/dist/utils/content-write-lock.js +21 -6
- package/dist/utils/debug.d.ts +1 -1
- package/dist/utils/debug.js +5 -3
- package/dist/utils/error.js +13 -2
- package/dist/utils/escape-text.d.ts +7 -0
- package/dist/utils/escape-text.js +27 -0
- package/dist/utils/fs.d.ts +12 -4
- package/dist/utils/fs.js +18 -5
- package/dist/utils/git.d.ts +21 -1
- package/dist/utils/git.js +63 -8
- package/dist/utils/json-source-splice.d.ts +14 -0
- package/dist/utils/json-source-splice.js +120 -0
- package/dist/utils/markdown-body-splice.d.ts +39 -0
- package/dist/utils/markdown-body-splice.js +421 -0
- package/dist/utils/occ-json-write.js +18 -12
- package/dist/utils/provision-log.d.ts +25 -0
- package/dist/utils/provision-log.js +49 -0
- package/dist/utils/provisioning-lock.d.ts +32 -1
- package/dist/utils/provisioning-lock.js +78 -32
- package/dist/utils/sanitize-href.d.ts +9 -23
- package/dist/utils/sanitize-href.js +14 -28
- package/dist/utils/title-field.d.ts +10 -0
- package/dist/utils/title-field.js +47 -0
- package/dist/utils/yaml-source-splice.d.ts +58 -0
- package/dist/utils/yaml-source-splice.js +515 -0
- package/dist/validation/block-structural-keys.d.ts +1 -1
- package/dist/validation/block-structural-keys.js +1 -1
- package/dist/validation/deletion-checker.d.ts +21 -29
- package/dist/validation/deletion-checker.js +41 -117
- package/dist/validation/entry-validator.d.ts +6 -10
- package/dist/validation/entry-validator.js +19 -26
- package/dist/validation/field-traversal.d.ts +22 -0
- package/dist/validation/field-traversal.js +39 -8
- package/dist/validation/markdown-safety.d.ts +68 -0
- package/dist/validation/markdown-safety.js +596 -0
- package/dist/validation/mdx-allowlist.d.ts +55 -0
- package/dist/validation/mdx-allowlist.js +183 -0
- package/dist/validation/reference-validator.d.ts +2 -0
- package/dist/validation/reference-validator.js +3 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/branch-policy.d.ts +124 -0
- package/dist/worker/branch-policy.js +243 -0
- package/dist/worker/bundle-header.d.ts +117 -0
- package/dist/worker/bundle-header.js +253 -0
- package/dist/worker/cms-worker.d.ts +236 -142
- package/dist/worker/cms-worker.js +772 -285
- package/dist/worker/gateway-egress.d.ts +43 -0
- package/dist/worker/gateway-egress.js +209 -0
- package/dist/worker/gateway-instance.d.ts +42 -0
- package/dist/worker/gateway-instance.js +129 -0
- package/dist/worker/git-process.d.ts +81 -0
- package/dist/worker/git-process.js +234 -0
- package/dist/worker/git-sync.d.ts +34 -17
- package/dist/worker/git-sync.js +394 -131
- package/dist/worker/github-auth.d.ts +9 -9
- package/dist/worker/github-auth.js +13 -15
- package/dist/worker/github-gateway-http.d.ts +62 -0
- package/dist/worker/github-gateway-http.js +977 -0
- package/dist/worker/github-gateway-wire.d.ts +590 -0
- package/dist/worker/github-gateway-wire.js +488 -0
- package/dist/worker/github-gateway.d.ts +238 -0
- package/dist/worker/github-gateway.js +726 -0
- package/dist/worker/github-mirror-upkeep.d.ts +75 -0
- package/dist/worker/github-mirror-upkeep.js +266 -0
- package/dist/worker/github-mirror.d.ts +364 -0
- package/dist/worker/github-mirror.js +1030 -0
- package/dist/worker/github-transfer.d.ts +56 -0
- package/dist/worker/github-transfer.js +444 -0
- package/dist/worker/history-rewrite.d.ts +4 -5
- package/dist/worker/history-rewrite.js +12 -17
- package/dist/worker/provisioned-workspace.d.ts +5 -5
- package/dist/worker/provisioned-workspace.js +5 -5
- package/dist/worker/rebase.d.ts +1 -1
- package/dist/worker/rebase.js +58 -54
- package/dist/worker/remote-git-maintenance.d.ts +3 -0
- package/dist/worker/remote-git-maintenance.js +12 -0
- package/dist/worker/schema-gate.d.ts +49 -0
- package/dist/worker/schema-gate.js +226 -0
- package/dist/worker/self-checks.d.ts +107 -0
- package/dist/worker/self-checks.js +358 -0
- package/dist/worker/shared-repo-git.d.ts +96 -0
- package/dist/worker/shared-repo-git.js +459 -0
- package/dist/worker/sparse-cone.d.ts +20 -0
- package/dist/worker/sparse-cone.js +136 -0
- package/dist/worker/stale-mirror.d.ts +82 -0
- package/dist/worker/stale-mirror.js +130 -0
- package/dist/worker/task-runner.d.ts +5 -3
- package/dist/worker/task-runner.js +287 -186
- package/dist/worker/worker-context.d.ts +36 -37
- package/package.json +35 -7
- package/dist/editor/EditorPanes.d.ts +0 -10
- package/dist/editor/EditorPanes.js +0 -98
- package/dist/editor/components/EditorFooter.d.ts +0 -11
- package/dist/editor/components/EditorFooter.js +0 -9
- package/dist/editor/components/EditorSidebar.d.ts +0 -67
- package/dist/editor/components/EditorSidebar.js +0 -22
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { GitManager } from './git-manager.js';
|
|
4
|
+
import { removeLeftoverDir, settingsStagingDirName, settingsTrashDirName, } from './branch-provisioning.js';
|
|
4
5
|
import { createDebugLogger } from './utils/debug.js';
|
|
5
|
-
import { getErrorMessage } from './utils/error.js';
|
|
6
|
+
import { getErrorMessage, isNodeError } from './utils/error.js';
|
|
7
|
+
import { canopyLogWarn } from './utils/logger.js';
|
|
8
|
+
import { ProvisionLog } from './utils/provision-log.js';
|
|
6
9
|
import { acquireProvisioningLock } from './utils/provisioning-lock.js';
|
|
7
10
|
import { RESERVED_SETTINGS_BRANCH_PREFIX } from './paths/index.js';
|
|
8
11
|
const log = createDebugLogger({ prefix: 'SettingsWorkspace' });
|
|
@@ -30,13 +33,27 @@ const ensuredSettingsWorkspaces = new Set();
|
|
|
30
33
|
function ensuredKey(options) {
|
|
31
34
|
return `${path.resolve(options.settingsRoot)}\0${options.branchName}`;
|
|
32
35
|
}
|
|
36
|
+
async function headBranch(settingsRoot) {
|
|
37
|
+
try {
|
|
38
|
+
const head = (await fs.readFile(path.join(settingsRoot, '.git', 'HEAD'), 'utf-8')).trim();
|
|
39
|
+
return head.startsWith('ref: refs/heads/') ? head.slice('ref: refs/heads/'.length) : undefined;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
33
45
|
async function checkedOutOn(settingsRoot, branchName) {
|
|
46
|
+
return (await headBranch(settingsRoot)) === branchName;
|
|
47
|
+
}
|
|
48
|
+
async function hasLocalBranch(settingsRoot, branchName) {
|
|
49
|
+
const gitDir = path.join(settingsRoot, '.git');
|
|
34
50
|
try {
|
|
35
|
-
|
|
36
|
-
return
|
|
51
|
+
await fs.access(path.join(gitDir, 'refs', 'heads', branchName));
|
|
52
|
+
return true;
|
|
37
53
|
}
|
|
38
54
|
catch {
|
|
39
|
-
|
|
55
|
+
const packed = await fs.readFile(path.join(gitDir, 'packed-refs'), 'utf-8').catch(() => '');
|
|
56
|
+
return packed.split('\n').some((line) => line.endsWith(` refs/heads/${branchName}`));
|
|
40
57
|
}
|
|
41
58
|
}
|
|
42
59
|
const SETTINGS_INIT_LOCK_DIR = '.settings-init';
|
|
@@ -49,8 +66,8 @@ const SETTINGS_INIT_LOCK_NAME = 'lock';
|
|
|
49
66
|
* its target, and `GitManager.initializeWorkspace` clones INTO the settings
|
|
50
67
|
* root, which `git clone` refuses if anything is already there. A dedicated
|
|
51
68
|
* dot-directory also keeps this marker out of `path.dirname(settingsRoot)`,
|
|
52
|
-
* where `ensureLocalSimulatedRemote` puts `.remote-init.lock` —
|
|
53
|
-
* calls into that while holding this lock, so separate directories make the
|
|
69
|
+
* where `ensureLocalSimulatedRemote` puts `.remote-init.lock` — an in-place
|
|
70
|
+
* settings re-init calls into that while holding this lock, so separate directories make the
|
|
54
71
|
* nesting obvious rather than incidental. For how a lock's anchor path is
|
|
55
72
|
* chosen, see utils/provisioning-lock.ts and docs/concurrency.md.
|
|
56
73
|
* @internal Exported for tests.
|
|
@@ -78,6 +95,41 @@ async function settingsFilesPresent(settingsRoot) {
|
|
|
78
95
|
}
|
|
79
96
|
return false;
|
|
80
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* What occupies the settings root.
|
|
100
|
+
*
|
|
101
|
+
* `interrupted` is a first init that died before its settings branch existed: not on this
|
|
102
|
+
* deployment's settings branch or any `canopycms-settings-*` one, no local ref for it, and no
|
|
103
|
+
* settings files -- what the rename guard lets through, minus a local settings branch that could
|
|
104
|
+
* hold unpushed commits. A clone killed in place leaves this behind, often with a stale
|
|
105
|
+
* `config.lock` that fails every later git config write.
|
|
106
|
+
*/
|
|
107
|
+
async function settingsRootState(options) {
|
|
108
|
+
let entries;
|
|
109
|
+
try {
|
|
110
|
+
entries = await fs.readdir(options.settingsRoot);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
if (isNodeError(err) && err.code === 'ENOENT')
|
|
114
|
+
return 'vacant';
|
|
115
|
+
if (isNodeError(err) && err.code === 'ENOTDIR')
|
|
116
|
+
return 'interrupted';
|
|
117
|
+
throw err;
|
|
118
|
+
}
|
|
119
|
+
if (entries.length === 0)
|
|
120
|
+
return 'vacant';
|
|
121
|
+
if (await settingsFilesPresent(options.settingsRoot))
|
|
122
|
+
return 'in-use';
|
|
123
|
+
if (!(await GitManager.repoExistsAt(options.settingsRoot)))
|
|
124
|
+
return 'interrupted';
|
|
125
|
+
const head = await headBranch(options.settingsRoot);
|
|
126
|
+
if (head === options.branchName || head?.startsWith(RESERVED_SETTINGS_BRANCH_PREFIX)) {
|
|
127
|
+
return 'in-use';
|
|
128
|
+
}
|
|
129
|
+
if (await hasLocalBranch(options.settingsRoot, options.branchName))
|
|
130
|
+
return 'in-use';
|
|
131
|
+
return 'interrupted';
|
|
132
|
+
}
|
|
81
133
|
/**
|
|
82
134
|
* Rename guard (settings-branch protection): refuse to boot when an
|
|
83
135
|
* ALREADY-POPULATED settings workspace would be re-orphaned under a different
|
|
@@ -144,9 +196,14 @@ async function assertSettingsWorkspaceIdentity(options) {
|
|
|
144
196
|
* BranchWorkspaceManager this writes no metadata files and never touches the
|
|
145
197
|
* branch registry.
|
|
146
198
|
*
|
|
199
|
+
* A first clone follows [PROV-1] (branch-provisioning.ts): it is built in a
|
|
200
|
+
* staging sibling and renamed into place, so a process killed mid-clone never
|
|
201
|
+
* leaves a half-made workspace at the settings root. Never sparse: the orphan
|
|
202
|
+
* init's `git rm -rf .` would leave out-of-cone base files in the tree.
|
|
203
|
+
*
|
|
147
204
|
* Two locking layers, mirroring BranchWorkspaceManager: an in-memory promise
|
|
148
205
|
* lock within the process, and `acquireProvisioningLock` (proper-lockfile)
|
|
149
|
-
* across processes and hosts.
|
|
206
|
+
* across processes and hosts around the publish and every in-place init.
|
|
150
207
|
*/
|
|
151
208
|
export class SettingsWorkspaceManager {
|
|
152
209
|
constructor(config) {
|
|
@@ -178,45 +235,11 @@ export class SettingsWorkspaceManager {
|
|
|
178
235
|
// Running it here also means no path reaches initializeWorkspace
|
|
179
236
|
// without the guard having run in THIS process.
|
|
180
237
|
await assertSettingsWorkspaceIdentity(options);
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
// loser WAITS instead of racing into a concurrent clone).
|
|
185
|
-
const releaseLock = await acquireProvisioningLock(settingsInitLockTarget(options.settingsRoot), SETTINGS_INIT_LOCK_NAME);
|
|
186
|
-
try {
|
|
187
|
-
// Re-run the guard on the now-stable state: a previous holder may
|
|
188
|
-
// have created the workspace, or moved it onto its own settings
|
|
189
|
-
// branch, after we sampled it above, and acting on that stale
|
|
190
|
-
// sample is the destructive path the guard exists to stop. Not
|
|
191
|
-
// gated on any "did I win the race" flag — by design there is none;
|
|
192
|
-
// every process here either holds the lock or has already thrown.
|
|
193
|
-
await assertSettingsWorkspaceIdentity(options);
|
|
194
|
-
// initializeWorkspace is idempotent (it checks for .git), so this
|
|
195
|
-
// is safe even if another process just finished init.
|
|
196
|
-
await GitManager.initializeWorkspace({
|
|
197
|
-
workspacePath: options.settingsRoot,
|
|
198
|
-
branchName: options.branchName,
|
|
199
|
-
mode: options.mode,
|
|
200
|
-
baseBranch: this.config.defaultBaseBranch,
|
|
201
|
-
sourceRoot: this.config.sourceRoot,
|
|
202
|
-
defaultRemoteUrl: this.config.defaultRemoteUrl,
|
|
203
|
-
remoteUrl: options.remoteUrl,
|
|
204
|
-
remoteName: this.config.defaultRemoteName,
|
|
205
|
-
allowNetworkRemoteInProd: this.config.allowNetworkRemoteInProd,
|
|
206
|
-
branchType: 'orphan', // Key difference: orphan branch for settings
|
|
207
|
-
gitBotAuthorName: this.config.gitBotAuthorName,
|
|
208
|
-
gitBotAuthorEmail: this.config.gitBotAuthorEmail,
|
|
209
|
-
});
|
|
210
|
-
ensuredSettingsWorkspaces.add(key);
|
|
211
|
-
}
|
|
212
|
-
finally {
|
|
213
|
-
try {
|
|
214
|
-
await releaseLock();
|
|
215
|
-
}
|
|
216
|
-
catch (err) {
|
|
217
|
-
log.debug('workspace', 'Failed to release settings-init lock', { err });
|
|
218
|
-
}
|
|
238
|
+
if ((await settingsRootState(options)) !== 'in-use' ||
|
|
239
|
+
!(await this.ensureInPlace(options))) {
|
|
240
|
+
await this.provisionStaged(options);
|
|
219
241
|
}
|
|
242
|
+
ensuredSettingsWorkspaces.add(key);
|
|
220
243
|
}
|
|
221
244
|
finally {
|
|
222
245
|
settingsInitLock = null;
|
|
@@ -225,4 +248,95 @@ export class SettingsWorkspaceManager {
|
|
|
225
248
|
await settingsInitLock;
|
|
226
249
|
});
|
|
227
250
|
}
|
|
251
|
+
initOptions(options, workspacePath) {
|
|
252
|
+
return {
|
|
253
|
+
workspacePath,
|
|
254
|
+
branchName: options.branchName,
|
|
255
|
+
mode: options.mode,
|
|
256
|
+
baseBranch: this.config.defaultBaseBranch,
|
|
257
|
+
sourceRoot: this.config.sourceRoot,
|
|
258
|
+
defaultRemoteUrl: this.config.defaultRemoteUrl,
|
|
259
|
+
remoteUrl: options.remoteUrl,
|
|
260
|
+
remoteName: this.config.defaultRemoteName,
|
|
261
|
+
allowNetworkRemoteInProd: this.config.allowNetworkRemoteInProd,
|
|
262
|
+
branchType: 'orphan',
|
|
263
|
+
gitBotAuthorName: this.config.gitBotAuthorName,
|
|
264
|
+
gitBotAuthorEmail: this.config.gitBotAuthorEmail,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Layer 2 around the init itself (proper-lockfile: heartbeat-refreshed while the holder lives,
|
|
269
|
+
* so a slow EFS operation is not mistaken for a crash, and patient retries so a loser WAITS
|
|
270
|
+
* instead of racing into a concurrent init).
|
|
271
|
+
*/
|
|
272
|
+
async withInitLock(options, run) {
|
|
273
|
+
const releaseLock = await acquireProvisioningLock(settingsInitLockTarget(options.settingsRoot), SETTINGS_INIT_LOCK_NAME);
|
|
274
|
+
try {
|
|
275
|
+
// Re-run the guard on the now-stable state: a previous holder may
|
|
276
|
+
// have created the workspace, or moved it onto its own settings
|
|
277
|
+
// branch, after we sampled it above, and acting on that stale
|
|
278
|
+
// sample is the destructive path the guard exists to stop. Not
|
|
279
|
+
// gated on any "did I win the race" flag — by design there is none;
|
|
280
|
+
// every process here either holds the lock or has already thrown.
|
|
281
|
+
await assertSettingsWorkspaceIdentity(options);
|
|
282
|
+
return await run();
|
|
283
|
+
}
|
|
284
|
+
finally {
|
|
285
|
+
try {
|
|
286
|
+
await releaseLock();
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
log.debug('workspace', 'Failed to release settings-init lock', { err });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/** Re-init an existing workspace where it is; false when, under the lock, there is none. */
|
|
294
|
+
async ensureInPlace(options) {
|
|
295
|
+
return this.withInitLock(options, async () => {
|
|
296
|
+
if ((await settingsRootState(options)) !== 'in-use')
|
|
297
|
+
return false;
|
|
298
|
+
await GitManager.initializeWorkspace(this.initOptions(options, options.settingsRoot));
|
|
299
|
+
return true;
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Clone and set up the settings branch in a staging sibling, then publish it under the init
|
|
304
|
+
* lock, moving an interrupted first init aside to `.trash-settings-*`. A workspace someone
|
|
305
|
+
* else published meanwhile wins and the staging copy is dropped.
|
|
306
|
+
*/
|
|
307
|
+
async provisionStaged(options) {
|
|
308
|
+
const settingsRoot = path.resolve(options.settingsRoot);
|
|
309
|
+
const workspaceRoot = path.dirname(settingsRoot);
|
|
310
|
+
await fs.mkdir(workspaceRoot, { recursive: true });
|
|
311
|
+
const stagingPath = path.join(workspaceRoot, settingsStagingDirName());
|
|
312
|
+
const provisionLog = new ProvisionLog('settings');
|
|
313
|
+
let published = false;
|
|
314
|
+
try {
|
|
315
|
+
await GitManager.initializeWorkspace({
|
|
316
|
+
...this.initOptions(options, stagingPath),
|
|
317
|
+
provisionLog,
|
|
318
|
+
});
|
|
319
|
+
published = await provisionLog.step('publish', () => this.withInitLock(options, async () => {
|
|
320
|
+
const state = await settingsRootState(options);
|
|
321
|
+
if (state === 'in-use')
|
|
322
|
+
return false;
|
|
323
|
+
if (state === 'interrupted') {
|
|
324
|
+
const trashName = settingsTrashDirName();
|
|
325
|
+
await fs.rename(settingsRoot, path.join(workspaceRoot, trashName));
|
|
326
|
+
canopyLogWarn(`[canopy] Moved an unfinished settings workspace clone aside as ${trashName}`);
|
|
327
|
+
}
|
|
328
|
+
await fs.rename(stagingPath, settingsRoot);
|
|
329
|
+
return true;
|
|
330
|
+
}));
|
|
331
|
+
}
|
|
332
|
+
catch (err) {
|
|
333
|
+
provisionLog.finish('error');
|
|
334
|
+
throw err;
|
|
335
|
+
}
|
|
336
|
+
finally {
|
|
337
|
+
if (!published)
|
|
338
|
+
await removeLeftoverDir(stagingPath);
|
|
339
|
+
}
|
|
340
|
+
provisionLog.finish(published ? 'ok' : 'exists');
|
|
341
|
+
}
|
|
228
342
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { FlatSchemaItem } from './config/index.js';
|
|
2
|
+
import type { ContentStore } from './content-store.js';
|
|
3
|
+
import type { GitManager } from './git-manager.js';
|
|
4
|
+
import type { ContentId } from './paths/types.js';
|
|
5
|
+
/**
|
|
6
|
+
* One entry a submit stamped: the editor moves its loaded value, draft and OCC token from
|
|
7
|
+
* `previousVersion` onto `version` with these fields set, instead of finding its next save refused.
|
|
8
|
+
*/
|
|
9
|
+
export interface StampedEntry {
|
|
10
|
+
contentId: ContentId;
|
|
11
|
+
/** Stamp field name to the value written. */
|
|
12
|
+
fields: Record<string, string>;
|
|
13
|
+
/** The file's version (`read()`'s mtime) before the stamp. */
|
|
14
|
+
previousVersion: number;
|
|
15
|
+
/** The file's version after the stamp. */
|
|
16
|
+
version: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Fill every empty stamp field of every entry the base lacks with `stampedAt`, never
|
|
20
|
+
* overwriting a value, and return the entries written. Lists the base only when the schema has
|
|
21
|
+
* a stamp field, and reads only the entry files the base lacks. Rejects when the base cannot be
|
|
22
|
+
* fetched or listed; a single entry that cannot be written is reported through `warn` and
|
|
23
|
+
* skipped.
|
|
24
|
+
*
|
|
25
|
+
* The caller holds the branch's content-write lock, as `store.fillEmptyFieldWhileHoldingWriteLock`
|
|
26
|
+
* requires.
|
|
27
|
+
*/
|
|
28
|
+
export declare function stampNewEntries(options: {
|
|
29
|
+
git: Pick<GitManager, 'listBaseFiles'>;
|
|
30
|
+
store: ContentStore;
|
|
31
|
+
branchRoot: string;
|
|
32
|
+
flatSchema: readonly FlatSchemaItem[];
|
|
33
|
+
contentRoot: string;
|
|
34
|
+
stampedAt: string;
|
|
35
|
+
warn: (message: string) => void;
|
|
36
|
+
}): Promise<StampedEntry[]>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `stampOnPublish` (config/types.ts): at submit, each entry new to the base branch gets the
|
|
3
|
+
* submit's time in its empty stamp fields. Canopy never merges and never writes the base branch,
|
|
4
|
+
* and adopters build from the merged tree, so its own submit is the last point it can put the
|
|
5
|
+
* date into the content. An entry counts as new when neither the base tip nor the fork point
|
|
6
|
+
* holds a file with its ContentId, so a renamed or moved entry, which keeps its ID, is never
|
|
7
|
+
* stamped.
|
|
8
|
+
*/
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { listCollectionEntries } from './content-listing.js';
|
|
11
|
+
import { getErrorMessage } from './utils/error.js';
|
|
12
|
+
import { flattenGroupFields } from './utils/flatten-group-fields.js';
|
|
13
|
+
import { parseTypedFilename } from './utils/typed-filename.js';
|
|
14
|
+
/** The names of `schema`'s top-level fields that carry `stampOnPublish`. */
|
|
15
|
+
const stampFieldNames = (schema) => flattenGroupFields(schema)
|
|
16
|
+
.filter((f) => f.type === 'datetime' && 'stampOnPublish' in f && f.stampOnPublish === true)
|
|
17
|
+
.map((f) => f.name);
|
|
18
|
+
const isEmpty = (value) => value === undefined || value === null || value === '';
|
|
19
|
+
/** `contentRoot` as a repo-relative directory, or undefined when it is the repository root. */
|
|
20
|
+
const contentRootDir = (contentRoot) => contentRoot
|
|
21
|
+
.split('/')
|
|
22
|
+
.filter((segment) => segment && segment !== '.')
|
|
23
|
+
.join('/') || undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Fill every empty stamp field of every entry the base lacks with `stampedAt`, never
|
|
26
|
+
* overwriting a value, and return the entries written. Lists the base only when the schema has
|
|
27
|
+
* a stamp field, and reads only the entry files the base lacks. Rejects when the base cannot be
|
|
28
|
+
* fetched or listed; a single entry that cannot be written is reported through `warn` and
|
|
29
|
+
* skipped.
|
|
30
|
+
*
|
|
31
|
+
* The caller holds the branch's content-write lock, as `store.fillEmptyFieldWhileHoldingWriteLock`
|
|
32
|
+
* requires.
|
|
33
|
+
*/
|
|
34
|
+
export async function stampNewEntries(options) {
|
|
35
|
+
const collections = options.flatSchema.filter((item) => item.type === 'collection' &&
|
|
36
|
+
(item.entries ?? []).some((entryType) => stampFieldNames(entryType.schema).length > 0));
|
|
37
|
+
if (collections.length === 0)
|
|
38
|
+
return [];
|
|
39
|
+
const baseIds = new Set();
|
|
40
|
+
for (const file of await options.git.listBaseFiles(contentRootDir(options.contentRoot))) {
|
|
41
|
+
const id = parseTypedFilename(path.posix.basename(file))?.id;
|
|
42
|
+
if (id)
|
|
43
|
+
baseIds.add(id);
|
|
44
|
+
}
|
|
45
|
+
const stamped = [];
|
|
46
|
+
for (const collection of collections) {
|
|
47
|
+
const entries = await listCollectionEntries(options.branchRoot, collection, undefined, (id) => !baseIds.has(id));
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
const schema = collection.entries?.find((e) => e.name === entry.entryType)?.schema ?? [];
|
|
50
|
+
const empty = stampFieldNames(schema).filter((name) => isEmpty(entry.data[name]));
|
|
51
|
+
let written;
|
|
52
|
+
for (const field of empty) {
|
|
53
|
+
try {
|
|
54
|
+
const versions = await options.store.fillEmptyFieldWhileHoldingWriteLock(entry.physicalPath, entry.format, field, options.stampedAt);
|
|
55
|
+
if (!versions)
|
|
56
|
+
continue;
|
|
57
|
+
written ??= { contentId: entry.contentId, fields: {}, ...versions };
|
|
58
|
+
written.fields[field] = options.stampedAt;
|
|
59
|
+
written.version = versions.version;
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
options.warn(`CanopyCMS: Could not stamp "${field}" on ${entry.physicalPath}; it is submitted ` +
|
|
63
|
+
`without it: ${getErrorMessage(err)}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (written)
|
|
67
|
+
stamped.push(written);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return stamped;
|
|
71
|
+
}
|
package/dist/static/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { MdxAllowlist } from '../config/index.js';
|
|
1
2
|
import type { CanopyBuildContext } from '../context.js';
|
|
2
3
|
import type { ListEntriesItem } from '../content-listing.js';
|
|
3
4
|
import { type EntryFieldError } from '../validation/entry-validator.js';
|
|
@@ -81,6 +82,11 @@ export interface CollectRoutableEntriesOptions<T = Record<string, unknown>> {
|
|
|
81
82
|
*/
|
|
82
83
|
resolveReferences?: boolean;
|
|
83
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* What the enumeration helpers take: a listing, plus the services whose config sets
|
|
87
|
+
* `danglingReferences`. A context without `services` gets the default, `'error'`.
|
|
88
|
+
*/
|
|
89
|
+
type StaticBuildContext = Pick<CanopyBuildContext, 'listEntries'> & Partial<Pick<CanopyBuildContext, 'services'>>;
|
|
84
90
|
/**
|
|
85
91
|
* Enumerate routable content entries WITH their data — the input to any surface that must read
|
|
86
92
|
* inside an entry to decide what to publish (sitemap, RSS, search index, index grids).
|
|
@@ -101,7 +107,7 @@ export interface CollectRoutableEntriesOptions<T = Record<string, unknown>> {
|
|
|
101
107
|
* const entries = await collectRoutableEntries(await getCanopyForBuild())
|
|
102
108
|
* const published = entries.filter((e) => !isNoindexEntry(e.data))
|
|
103
109
|
*/
|
|
104
|
-
export declare function collectRoutableEntries<T = Record<string, unknown>>(ctx:
|
|
110
|
+
export declare function collectRoutableEntries<T = Record<string, unknown>>(ctx: StaticBuildContext, options?: CollectRoutableEntriesOptions<T>): Promise<RoutableEntry<T>[]>;
|
|
105
111
|
/**
|
|
106
112
|
* Enumerate routable content entries as neutral path descriptors for static generation.
|
|
107
113
|
*
|
|
@@ -115,7 +121,7 @@ export declare function collectRoutableEntries<T = Record<string, unknown>>(ctx:
|
|
|
115
121
|
* // Single [slug] scoped to one collection:
|
|
116
122
|
* const posts = await collectStaticPaths(await getCanopyForBuild(), { rootPath: 'content/posts' })
|
|
117
123
|
*/
|
|
118
|
-
export declare function collectStaticPaths(ctx:
|
|
124
|
+
export declare function collectStaticPaths(ctx: StaticBuildContext, options?: CollectStaticPathsOptions): Promise<StaticPathEntry[]>;
|
|
119
125
|
/**
|
|
120
126
|
* One schema-invalid entry found during a build-time content scan.
|
|
121
127
|
*/
|
|
@@ -132,9 +138,10 @@ export interface InvalidBuildEntry {
|
|
|
132
138
|
type BuildScanItem = Pick<ListEntriesItem, 'entryPath' | 'schema'> & {
|
|
133
139
|
data: unknown;
|
|
134
140
|
/**
|
|
135
|
-
*
|
|
136
|
-
* merges into `data`. Optional so a caller assembling items by hand still typechecks; an
|
|
137
|
-
* absent format
|
|
141
|
+
* The unknown-key and unsafe-markdown scans read this, to recognise the md/mdx body key that
|
|
142
|
+
* `listEntries` merges into `data`. Optional so a caller assembling items by hand still typechecks; an
|
|
143
|
+
* absent format errs toward under-reporting: the unknown-key scan treats it as markdown-shaped,
|
|
144
|
+
* and the unsafe-markdown scan checks no body.
|
|
138
145
|
*/
|
|
139
146
|
format?: ListEntriesItem['format'];
|
|
140
147
|
};
|
|
@@ -181,6 +188,19 @@ export declare function findEntriesWithUnknownKeys(items: readonly BuildScanItem
|
|
|
181
188
|
* value at a time.
|
|
182
189
|
*/
|
|
183
190
|
export declare function warnUnknownEntryKeys(items: readonly BuildScanItem[], phaseLabel: string): void;
|
|
191
|
+
/** An entry holding code in a markdown or MDX field that is not `executable`. */
|
|
192
|
+
export interface EntryWithUnsafeMarkdown {
|
|
193
|
+
entryPath: string;
|
|
194
|
+
fieldPaths: string[];
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Scan listEntries-shaped items for markdown and MDX that runs code where the field is not
|
|
198
|
+
* `executable`. A save keeps such code only in a field saved unchanged, so the build is where it
|
|
199
|
+
* is listed. Non-fatal, like the unknown-key scan.
|
|
200
|
+
*/
|
|
201
|
+
export declare function findEntriesWithUnsafeMarkdown(items: readonly BuildScanItem[], siteAllow?: MdxAllowlist): EntryWithUnsafeMarkdown[];
|
|
202
|
+
/** Warn — never throw — about entries holding markdown or MDX that a save of a changed field refuses. */
|
|
203
|
+
export declare function warnUnsafeMarkdown(items: readonly BuildScanItem[], phaseLabel: string, siteAllow?: MdxAllowlist): void;
|
|
184
204
|
/**
|
|
185
205
|
* Throw a single, descriptive Error if any item is schema-invalid.
|
|
186
206
|
*
|
|
@@ -273,3 +293,36 @@ export declare function findUnroutableSlugs(items: readonly SlugScanItem[]): Unr
|
|
|
273
293
|
* red build.
|
|
274
294
|
*/
|
|
275
295
|
export declare function assertRoutableSlugs(items: readonly SlugScanItem[], phaseLabel: string): void;
|
|
296
|
+
/** A reference whose id names no listed entry. */
|
|
297
|
+
export interface DanglingReference {
|
|
298
|
+
/** Logical path of the referring entry. */
|
|
299
|
+
entryPath: string;
|
|
300
|
+
/** Field path in `traverseFields`' format, e.g. `author`, `blocks[2].author`, `reviewers[1]`. */
|
|
301
|
+
fieldPath: string;
|
|
302
|
+
/** The id that names no entry. */
|
|
303
|
+
id: string;
|
|
304
|
+
}
|
|
305
|
+
/** What the dangling-reference scan needs off a listing item. */
|
|
306
|
+
type ReferenceScanItem = Pick<ListEntriesItem, 'entryPath' | 'entryId' | 'schema'> & {
|
|
307
|
+
data: unknown;
|
|
308
|
+
};
|
|
309
|
+
/**
|
|
310
|
+
* Find every reference, at any depth (objects, lists, blocks), whose id names none of
|
|
311
|
+
* `knownIds`. `knownIds` defaults to the items' own ids, which is the whole id universe for an
|
|
312
|
+
* unscoped `listEntries()`.
|
|
313
|
+
*
|
|
314
|
+
* Reads ids through `referenceValueId`, so a listing that resolved its references scans the same
|
|
315
|
+
* as a raw one. An id naming a collection is dangling too, since it resolves to no entry.
|
|
316
|
+
* Exported for adopters to assert on directly; `assertNoDanglingReferences` is what the build
|
|
317
|
+
* uses. Sorted by entry, then field path.
|
|
318
|
+
*/
|
|
319
|
+
export declare function findDanglingReferences(items: readonly ReferenceScanItem[], knownIds?: ReadonlySet<string>): DanglingReference[];
|
|
320
|
+
/**
|
|
321
|
+
* Fail the build (or, under `danglingReferences: 'warn'`, warn) when a reference names no entry.
|
|
322
|
+
*
|
|
323
|
+
* A dangling reference resolves to a `MissingReference`, so the page builds without whatever the
|
|
324
|
+
* reference supplied (a byline, its structured-data author) and nothing else notices. The save
|
|
325
|
+
* path refuses to introduce one, but a deleted target, a merge, a hand edit or a `sync pull`
|
|
326
|
+
* leaves one behind.
|
|
327
|
+
*/
|
|
328
|
+
export declare function assertNoDanglingReferences(items: readonly ReferenceScanItem[], knownIds: ReadonlySet<string>, phaseLabel: string, mode: 'error' | 'warn'): void;
|
package/dist/static/index.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import { isBuildMode } from '../build-mode.js';
|
|
2
3
|
import { findUnknownKeys, validateEntryData, } from '../validation/entry-validator.js';
|
|
3
4
|
import { findBodyFieldName } from '../utils/body-field.js';
|
|
4
5
|
import { isDataOnlyFormat } from '../utils/format.js';
|
|
5
6
|
import { parseSlug } from '../paths/index.js';
|
|
7
|
+
import { collectReferenceIds } from '../validation/field-traversal.js';
|
|
8
|
+
import { findMarkdownSafetyIssues } from '../validation/markdown-safety.js';
|
|
6
9
|
/**
|
|
7
10
|
* Framework-agnostic helpers for static-site generation. These produce neutral data structures
|
|
8
11
|
* (no Next.js types) so that any framework adapter — e.g. canopycms-next — can map them to its own
|
|
@@ -27,17 +30,20 @@ async function enumerateRoutableEntries(ctx, rootPath, phaseLabel, resolveRefere
|
|
|
27
30
|
// an abandoned schema-invalid scaffold shipping into a static build silently drops that page's
|
|
28
31
|
// route (or worse, renders broken), which is worse than a red build.
|
|
29
32
|
//
|
|
30
|
-
// These
|
|
33
|
+
// These five guards run in a fixed order on the RAW listing, before the caller's `filter` —
|
|
31
34
|
// `rootPath` narrows them, `filter` deliberately does not. The unknown-key warning runs first,
|
|
32
35
|
// so a build about to go red still reports everything found. Slug routability runs next: an
|
|
33
|
-
// unroutable entry has no URL at all as far as the other
|
|
36
|
+
// unroutable entry has no URL at all as far as the other four are concerned, the most
|
|
34
37
|
// fundamental problem a listed entry can have. Schema validity runs before duplicate URLs for
|
|
35
|
-
// the same "most fundamental first" reason, one level down
|
|
38
|
+
// the same "most fundamental first" reason, one level down, and dangling references last: a
|
|
39
|
+
// page whose reference is broken still has its own route and data.
|
|
36
40
|
if (isBuildMode()) {
|
|
37
41
|
warnUnknownEntryKeys(entries, phaseLabel);
|
|
42
|
+
warnUnsafeMarkdown(entries, phaseLabel, ctx.services?.config.mdxAllow);
|
|
38
43
|
assertRoutableSlugs(entries, phaseLabel);
|
|
39
44
|
assertBuildEntriesValid(entries, phaseLabel);
|
|
40
45
|
assertNoDuplicateUrlPaths(entries, phaseLabel);
|
|
46
|
+
assertNoDanglingReferences(entries, await knownEntryIds(ctx, entries, rootPath), phaseLabel, ctx.services?.config.danglingReferences ?? 'error');
|
|
41
47
|
}
|
|
42
48
|
return entries.map((entry) => ({
|
|
43
49
|
urlPath: entry.urlPath,
|
|
@@ -221,6 +227,57 @@ export function warnUnknownEntryKeys(items, phaseLabel) {
|
|
|
221
227
|
`These are usually left over from a renamed or reshaped field. Nothing reads them, and they are kept in the file on every save. ` +
|
|
222
228
|
`Add them to the schema or remove them from the content.`);
|
|
223
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* Scan listEntries-shaped items for markdown and MDX that runs code where the field is not
|
|
232
|
+
* `executable`. A save keeps such code only in a field saved unchanged, so the build is where it
|
|
233
|
+
* is listed. Non-fatal, like the unknown-key scan.
|
|
234
|
+
*/
|
|
235
|
+
export function findEntriesWithUnsafeMarkdown(items, siteAllow) {
|
|
236
|
+
const found = [];
|
|
237
|
+
for (const item of items) {
|
|
238
|
+
if (!item.schema || typeof item.data !== 'object' || item.data === null)
|
|
239
|
+
continue;
|
|
240
|
+
const fieldPaths = unsafeFieldPaths(item.schema, item.format, item.data, siteAllow);
|
|
241
|
+
if (fieldPaths.length > 0)
|
|
242
|
+
found.push({ entryPath: item.entryPath, fieldPaths });
|
|
243
|
+
}
|
|
244
|
+
return found;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* A build lists its entries several times (every catch-all route, the sitemap), and parsing each
|
|
248
|
+
* body costs milliseconds, so results are kept per schema object and hash of an entry's content.
|
|
249
|
+
*/
|
|
250
|
+
const unsafeFieldPathsMemo = new WeakMap();
|
|
251
|
+
function unsafeFieldPaths(schema, format, data, siteAllow) {
|
|
252
|
+
let bySchema = unsafeFieldPathsMemo.get(schema);
|
|
253
|
+
if (bySchema === undefined) {
|
|
254
|
+
bySchema = new Map();
|
|
255
|
+
unsafeFieldPathsMemo.set(schema, bySchema);
|
|
256
|
+
}
|
|
257
|
+
const key = createHash('sha256')
|
|
258
|
+
.update(`${format ?? ''}\0${JSON.stringify(siteAllow ?? null)}\0${JSON.stringify(data)}`)
|
|
259
|
+
.digest('hex');
|
|
260
|
+
let fieldPaths = bySchema.get(key);
|
|
261
|
+
if (fieldPaths === undefined) {
|
|
262
|
+
fieldPaths = findMarkdownSafetyIssues(schema, format, data, siteAllow).map((f) => f.fieldPath);
|
|
263
|
+
bySchema.set(key, fieldPaths);
|
|
264
|
+
}
|
|
265
|
+
return fieldPaths;
|
|
266
|
+
}
|
|
267
|
+
/** Warn — never throw — about entries holding markdown or MDX that a save of a changed field refuses. */
|
|
268
|
+
export function warnUnsafeMarkdown(items, phaseLabel, siteAllow) {
|
|
269
|
+
const found = findEntriesWithUnsafeMarkdown(items, siteAllow);
|
|
270
|
+
if (found.length === 0)
|
|
271
|
+
return;
|
|
272
|
+
// Capped and counted for the reason `warnUnknownEntryKeys` gives.
|
|
273
|
+
const shown = found.slice(0, UNKNOWN_KEY_REPORT_LIMIT);
|
|
274
|
+
const lines = shown.map(({ entryPath, fieldPaths }) => ` - ${entryPath} — ${fieldPaths.join(', ')}`);
|
|
275
|
+
if (found.length > shown.length) {
|
|
276
|
+
lines.push(` …and ${found.length - shown.length} more`);
|
|
277
|
+
}
|
|
278
|
+
console.warn(`CanopyCMS static build: ${found.length} ${found.length === 1 ? 'entry holds' : 'entries hold'} markdown or MDX that runs code, or that \`mdxAllow\` excludes, during ${phaseLabel}:\n${lines.join('\n')}\n` +
|
|
279
|
+
`A save keeps it but refuses adding more. Move code into a component, allow a component or tag in \`mdxAllow\`, or set \`executable: true\` on a field whose editors you trust as code authors.`);
|
|
280
|
+
}
|
|
224
281
|
/**
|
|
225
282
|
* Throw a single, descriptive Error if any item is schema-invalid.
|
|
226
283
|
*
|
|
@@ -366,3 +423,76 @@ export function assertRoutableSlugs(items, phaseLabel) {
|
|
|
366
423
|
'above would build, appear in generateStaticParams and any sitemap, and then 404 on every ' +
|
|
367
424
|
'visit. Rename the slug (the file itself, not just its content), then rebuild.');
|
|
368
425
|
}
|
|
426
|
+
/**
|
|
427
|
+
* How many dangling references the warning lists before summarising the rest; the error lists
|
|
428
|
+
* every one, for the reason `warnUnknownEntryKeys` gives.
|
|
429
|
+
*/
|
|
430
|
+
const DANGLING_REFERENCE_REPORT_LIMIT = 20;
|
|
431
|
+
/**
|
|
432
|
+
* Find every reference, at any depth (objects, lists, blocks), whose id names none of
|
|
433
|
+
* `knownIds`. `knownIds` defaults to the items' own ids, which is the whole id universe for an
|
|
434
|
+
* unscoped `listEntries()`.
|
|
435
|
+
*
|
|
436
|
+
* Reads ids through `referenceValueId`, so a listing that resolved its references scans the same
|
|
437
|
+
* as a raw one. An id naming a collection is dangling too, since it resolves to no entry.
|
|
438
|
+
* Exported for adopters to assert on directly; `assertNoDanglingReferences` is what the build
|
|
439
|
+
* uses. Sorted by entry, then field path.
|
|
440
|
+
*/
|
|
441
|
+
export function findDanglingReferences(items, knownIds = new Set(items.map((item) => item.entryId))) {
|
|
442
|
+
const found = [];
|
|
443
|
+
for (const item of items) {
|
|
444
|
+
if (!item.schema || typeof item.data !== 'object' || item.data === null)
|
|
445
|
+
continue;
|
|
446
|
+
const data = item.data;
|
|
447
|
+
for (const { id, path } of collectReferenceIds(item.schema, data)) {
|
|
448
|
+
if (id && !knownIds.has(id))
|
|
449
|
+
found.push({ entryPath: item.entryPath, fieldPath: path, id });
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return found.sort((a, b) => a.entryPath.localeCompare(b.entryPath) || a.fieldPath.localeCompare(b.fieldPath));
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Fail the build (or, under `danglingReferences: 'warn'`, warn) when a reference names no entry.
|
|
456
|
+
*
|
|
457
|
+
* A dangling reference resolves to a `MissingReference`, so the page builds without whatever the
|
|
458
|
+
* reference supplied (a byline, its structured-data author) and nothing else notices. The save
|
|
459
|
+
* path refuses to introduce one, but a deleted target, a merge, a hand edit or a `sync pull`
|
|
460
|
+
* leaves one behind.
|
|
461
|
+
*/
|
|
462
|
+
export function assertNoDanglingReferences(items, knownIds, phaseLabel, mode) {
|
|
463
|
+
const found = findDanglingReferences(items, knownIds);
|
|
464
|
+
if (found.length === 0)
|
|
465
|
+
return;
|
|
466
|
+
const shown = mode === 'warn' ? found.slice(0, DANGLING_REFERENCE_REPORT_LIMIT) : found;
|
|
467
|
+
const lines = shown.map(({ entryPath, fieldPath, id }) => ` - ${entryPath} — ${fieldPath} → ${id}`);
|
|
468
|
+
if (found.length > shown.length)
|
|
469
|
+
lines.push(` …and ${found.length - shown.length} more`);
|
|
470
|
+
const message = `CanopyCMS static build: found ${found.length} ${found.length === 1 ? 'reference' : 'references'} to a missing entry during ${phaseLabel}:\n${lines.join('\n')}\n` +
|
|
471
|
+
'Each resolves as unavailable, so the page renders without what it references. Repoint or ' +
|
|
472
|
+
'clear each reference, or restore the entry it names, then rebuild.';
|
|
473
|
+
if (mode === 'warn')
|
|
474
|
+
console.warn(message);
|
|
475
|
+
else
|
|
476
|
+
throw new Error(message + " Set `danglingReferences: 'warn'` to build anyway.");
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Every entry id a build context can resolve a reference to, computed at most once per context.
|
|
480
|
+
*
|
|
481
|
+
* Targets live anywhere in the content tree, so a `rootPath`-scoped listing is not the id
|
|
482
|
+
* universe; that case lists the whole tree once and keeps the result for the context's lifetime.
|
|
483
|
+
* Safe to keep, unlike a request-time memo, because these guards run only in build mode, which
|
|
484
|
+
* reads a checkout that does not change mid-build (`build-mode.ts`, `readsFromCheckout`). Keyed weakly on the context, so it lives
|
|
485
|
+
* exactly as long as the context does; a rejected listing is forgotten so the next call retries.
|
|
486
|
+
*/
|
|
487
|
+
const entryIdUniverse = new WeakMap();
|
|
488
|
+
async function knownEntryIds(ctx, listed, rootPath) {
|
|
489
|
+
const cached = entryIdUniverse.get(ctx);
|
|
490
|
+
if (cached)
|
|
491
|
+
return cached;
|
|
492
|
+
const universe = rootPath === undefined
|
|
493
|
+
? Promise.resolve(new Set(listed.map((item) => item.entryId)))
|
|
494
|
+
: ctx.listEntries().then((all) => new Set(all.map((item) => item.entryId)));
|
|
495
|
+
entryIdUniverse.set(ctx, universe);
|
|
496
|
+
universe.catch(() => entryIdUniverse.delete(ctx));
|
|
497
|
+
return universe;
|
|
498
|
+
}
|
package/dist/static/seo.d.ts
CHANGED
|
@@ -75,12 +75,12 @@ export declare function extractSeoFields(entryData: unknown, opts?: ExtractSeoFi
|
|
|
75
75
|
* THE `noindex` predicate. Every surface that suppresses an entry must route through this one
|
|
76
76
|
* function rather than re-deriving the flag:
|
|
77
77
|
*
|
|
78
|
-
* - the page's own `robots: { index: false }` (canopycms-next's `entryToMetadata`),
|
|
79
|
-
* - exclusion from the sitemap (canopycms-next's `generateContentSitemap`)
|
|
78
|
+
* - the page's own `robots: { index: false }` (canopycms-next's `entryToMetadata`),
|
|
79
|
+
* - exclusion from the sitemap (canopycms-next's `generateContentSitemap`), and
|
|
80
|
+
* - exclusion from AI content (`ai/generate.ts`, unless `exclude.noindex: false`).
|
|
80
81
|
*
|
|
81
|
-
* Those
|
|
82
|
-
*
|
|
83
|
-
* from the other, and nothing warned.
|
|
82
|
+
* Those surfaces shipping from one predicate is the point. Derived separately, a `noindex` entry
|
|
83
|
+
* stays advertised in one surface while correctly absent from another, and nothing warns.
|
|
84
84
|
*
|
|
85
85
|
* NOT applied to enumeration: `noindex` means "don't advertise this", not "don't build this".
|
|
86
86
|
* `collectRoutableEntries` and `generateStaticParams` must still emit the entry, so its URL
|