canopycms 0.0.67 → 0.0.68-int.101
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 +24 -17
- package/dist/ai/handler.js +8 -0
- package/dist/api/admin-branch-health.js +17 -20
- package/dist/api/admin.d.ts +30 -9
- package/dist/api/admin.js +32 -2
- package/dist/api/assets.js +84 -76
- package/dist/api/branch-create-window.d.ts +12 -0
- package/dist/api/branch-create-window.js +13 -0
- package/dist/api/branch-review.d.ts +3 -3
- package/dist/api/branch-review.js +14 -5
- package/dist/api/branch-status.d.ts +5 -5
- package/dist/api/branch-status.js +26 -6
- package/dist/api/branch-withdraw.d.ts +2 -2
- package/dist/api/branch-withdraw.js +7 -2
- package/dist/api/branch.d.ts +15 -2
- package/dist/api/branch.js +118 -92
- package/dist/api/client.d.ts +18 -6
- package/dist/api/client.js +79 -12
- 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 +19 -1
- package/dist/api/github-sync.js +57 -10
- package/dist/api/index.d.ts +1 -1
- 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/asset-url.d.ts +22 -16
- package/dist/assets/asset-url.js +42 -25
- package/dist/assets/factory.d.ts +5 -0
- package/dist/assets/factory.js +5 -0
- 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/file-based-auth-cache.js +7 -8
- 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.d.ts +7 -1
- package/dist/branch-health.js +8 -3
- package/dist/branch-metadata.d.ts +5 -5
- package/dist/branch-metadata.js +44 -36
- package/dist/branch-provisioning.d.ts +167 -0
- package/dist/branch-provisioning.js +501 -0
- package/dist/branch-schema-cache.d.ts +5 -3
- package/dist/branch-schema-cache.js +29 -11
- 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 +240 -94
- package/dist/build/asset-refs.d.ts +93 -0
- package/dist/build/asset-refs.js +251 -0
- package/dist/build-identity.d.ts +3 -0
- package/dist/build-identity.js +13 -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 +5928 -2195
- 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 +2185 -587
- package/dist/cli/github-app-manifest.d.ts +4 -3
- package/dist/cli/github-app-manifest.js +4 -3
- package/dist/cli/init.js +30 -27
- 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 +23 -12
- package/dist/cli/template-files/deploy-cms.yml.template +2 -0
- package/dist/client.d.ts +1 -1
- package/dist/client.js +1 -1
- package/dist/config/helpers.js +1 -2
- package/dist/config/schemas/config.d.ts +19 -32
- package/dist/config/schemas/config.js +9 -11
- package/dist/config/schemas/field.js +1 -0
- package/dist/config/schemas/media.d.ts +4 -13
- package/dist/config/schemas/media.js +5 -8
- package/dist/config/schemas/url.d.ts +4 -20
- package/dist/config/schemas/url.js +6 -22
- package/dist/config/types.d.ts +30 -20
- 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/BranchManager.d.ts +7 -1
- package/dist/editor/BranchManager.js +29 -14
- package/dist/editor/CanopyEditor.d.ts +1 -1
- package/dist/editor/CanopyEditor.js +2 -6
- package/dist/editor/Editor.d.ts +4 -3
- package/dist/editor/Editor.js +32 -22
- package/dist/editor/FormRenderer.js +34 -18
- package/dist/editor/PreviewFrame.d.ts +24 -0
- package/dist/editor/PreviewFrame.js +140 -0
- package/dist/editor/admin/SystemHealthPanel.js +47 -7
- package/dist/editor/components/BranchesDrawer.d.ts +9 -0
- package/dist/editor/components/BranchesDrawer.js +4 -0
- package/dist/editor/components/NoEditPermissionNotice.d.ts +10 -0
- package/dist/editor/components/NoEditPermissionNotice.js +17 -0
- package/dist/editor/context/AssetContext.d.ts +18 -35
- package/dist/editor/context/AssetContext.js +20 -29
- package/dist/editor/context/index.d.ts +1 -1
- package/dist/editor/context/index.js +1 -1
- package/dist/editor/editor-config.d.ts +1 -2
- package/dist/editor/editor-config.js +0 -23
- package/dist/editor/editor-utils.d.ts +24 -21
- package/dist/editor/editor-utils.js +75 -62
- package/dist/editor/fields/BlockField.d.ts +1 -0
- package/dist/editor/fields/BlockField.js +6 -4
- package/dist/editor/fields/CodeField.d.ts +1 -0
- package/dist/editor/fields/CodeField.js +2 -2
- package/dist/editor/fields/DateTimeField.d.ts +1 -0
- package/dist/editor/fields/DateTimeField.js +2 -2
- package/dist/editor/fields/FieldDescription.d.ts +16 -0
- package/dist/editor/fields/FieldDescription.js +11 -0
- package/dist/editor/fields/ImageField.d.ts +1 -0
- package/dist/editor/fields/ImageField.js +3 -2
- package/dist/editor/fields/InlineGroupField.js +4 -1
- package/dist/editor/fields/MarkdownField.d.ts +1 -0
- package/dist/editor/fields/MarkdownField.js +115 -20
- package/dist/editor/fields/NumberField.d.ts +1 -0
- package/dist/editor/fields/NumberField.js +2 -2
- package/dist/editor/fields/NumberListField.d.ts +1 -0
- package/dist/editor/fields/NumberListField.js +2 -2
- package/dist/editor/fields/ObjectField.d.ts +1 -0
- package/dist/editor/fields/ObjectField.js +5 -2
- package/dist/editor/fields/ReferenceField.d.ts +1 -0
- package/dist/editor/fields/ReferenceField.js +5 -4
- package/dist/editor/fields/SelectField.d.ts +1 -0
- package/dist/editor/fields/SelectField.js +2 -2
- package/dist/editor/fields/StringListField.d.ts +1 -0
- package/dist/editor/fields/StringListField.js +2 -2
- package/dist/editor/fields/TextField.d.ts +1 -0
- package/dist/editor/fields/TextField.js +2 -2
- package/dist/editor/fields/ToggleField.d.ts +1 -0
- package/dist/editor/fields/ToggleField.js +7 -4
- package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +1 -1
- package/dist/editor/fields/entry-link/InsertEntryLink.js +3 -2
- package/dist/editor/fields/mdx-jsx-support.d.ts +1 -0
- package/dist/editor/fields/mdx-jsx-support.js +137 -0
- package/dist/editor/hooks/create-branch-request.d.ts +20 -0
- package/dist/editor/hooks/create-branch-request.js +52 -0
- package/dist/editor/hooks/useBranchActions.d.ts +13 -2
- package/dist/editor/hooks/useBranchActions.js +48 -17
- package/dist/editor/hooks/useBranchManager.d.ts +21 -0
- package/dist/editor/hooks/useBranchManager.js +204 -37
- package/dist/editor/hooks/useBranchesData.d.ts +6 -0
- package/dist/editor/hooks/useBranchesData.js +9 -3
- package/dist/editor/hooks/useCommentSystem.js +25 -6
- package/dist/editor/hooks/useDraftManager.d.ts +16 -1
- package/dist/editor/hooks/useDraftManager.js +193 -36
- package/dist/editor/hooks/useEntriesData.js +3 -3
- package/dist/editor/hooks/useEntryManager.d.ts +2 -1
- package/dist/editor/hooks/useEntryManager.js +34 -19
- package/dist/editor/media/AssetCard.d.ts +1 -1
- 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 +7 -0
- package/dist/editor/media/editor-image-src.js +12 -0
- package/dist/editor/preview-asset-base.d.ts +8 -0
- package/dist/editor/preview-asset-base.js +13 -0
- package/dist/editor/preview-bridge.d.ts +9 -21
- package/dist/editor/preview-bridge.js +19 -139
- package/dist/editor/preview-path.d.ts +10 -0
- package/dist/editor/preview-path.js +20 -0
- package/dist/editor/theme.js +2 -2
- 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 +173 -6
- package/dist/git-manager.js +594 -76
- package/dist/github-service.d.ts +15 -0
- package/dist/github-service.js +19 -1
- package/dist/http/handler.js +37 -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/types.d.ts +2 -0
- package/dist/http/worker-not-ready.d.ts +10 -0
- package/dist/http/worker-not-ready.js +24 -0
- package/dist/operating-mode/client-unsafe-strategy.js +0 -6
- package/dist/operating-mode/types.d.ts +1 -4
- package/dist/paths/branch-name.d.ts +5 -0
- package/dist/paths/branch-name.js +9 -0
- package/dist/paths/branch.d.ts +6 -2
- package/dist/paths/branch.js +16 -10
- 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/paths/validation.js +0 -2
- package/dist/preview.d.ts +8 -0
- package/dist/preview.js +7 -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 +54 -31
- package/dist/schema/schema-store.js +77 -31
- package/dist/server.d.ts +64 -9
- package/dist/server.js +48 -8
- package/dist/services.d.ts +18 -6
- package/dist/services.js +85 -70
- package/dist/settings-workspace.d.ts +23 -3
- package/dist/settings-workspace.js +197 -45
- 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-serialize.d.ts +5 -2
- package/dist/utils/content-serialize.js +98 -24
- 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 +32 -0
- package/dist/utils/git.js +42 -0
- package/dist/utils/occ-json-write.js +10 -2
- package/dist/utils/provision-log.d.ts +25 -0
- package/dist/utils/provision-log.js +49 -0
- package/dist/utils/provisioning-lock.d.ts +35 -5
- package/dist/utils/provisioning-lock.js +73 -12
- package/dist/utils/request-timing.d.ts +25 -0
- package/dist/utils/request-timing.js +101 -0
- package/dist/utils/sanitize-href.d.ts +8 -22
- package/dist/utils/sanitize-href.js +11 -28
- package/dist/utils/url-prefix.d.ts +30 -0
- package/dist/utils/url-prefix.js +61 -0
- package/dist/utils/yaml-source-splice.d.ts +58 -0
- package/dist/utils/yaml-source-splice.js +515 -0
- package/dist/validation/entry-validator.js +9 -3
- 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.d.ts +8 -3
- package/dist/worker/cms-worker.js +38 -2
- package/dist/worker/git-sync.d.ts +30 -12
- package/dist/worker/git-sync.js +253 -58
- package/dist/worker/history-rewrite.d.ts +1 -1
- package/dist/worker/history-rewrite.js +1 -1
- 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 +95 -23
- package/dist/worker/remote-git-maintenance.d.ts +3 -0
- package/dist/worker/remote-git-maintenance.js +11 -0
- package/dist/worker/sparse-cone.d.ts +20 -0
- package/dist/worker/sparse-cone.js +132 -0
- package/dist/worker/task-runner.js +99 -14
- package/dist/worker/worker-context.d.ts +6 -1
- package/package.json +8 -2
|
@@ -6,13 +6,7 @@
|
|
|
6
6
|
* (no react-easy-crop import) so it's trivially unit-testable and reusable
|
|
7
7
|
* without pulling in the cropper UI library.
|
|
8
8
|
*/
|
|
9
|
-
import { isValidCropRect } from '../../assets/transform-directives.js';
|
|
10
|
-
/** Matches transform-directives.ts's CROP_PRECISION so a crop rect round-trips through the URL directive unchanged. */
|
|
11
|
-
const CROP_PRECISION = 4;
|
|
12
|
-
function roundTo(value, decimals) {
|
|
13
|
-
const factor = 10 ** decimals;
|
|
14
|
-
return Math.round(value * factor) / factor;
|
|
15
|
-
}
|
|
9
|
+
import { isValidCropRect, roundCropRect } from '../../assets/transform-directives.js';
|
|
16
10
|
function clamp01(value) {
|
|
17
11
|
if (!Number.isFinite(value))
|
|
18
12
|
return 0;
|
|
@@ -20,26 +14,21 @@ function clamp01(value) {
|
|
|
20
14
|
}
|
|
21
15
|
/**
|
|
22
16
|
* Convert react-easy-crop's `onCropComplete(croppedArea, croppedAreaPixels)`
|
|
23
|
-
* percentage area into a normalized `CropRect`,
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* space rather than reject a rect that was valid before rounding.
|
|
17
|
+
* percentage area into a normalized `CropRect`, clamped to [0,1] and then
|
|
18
|
+
* rounded by `roundCropRect` — the same rounding the transform URL's canonical
|
|
19
|
+
* form applies, so the stored crop is already canonical.
|
|
27
20
|
*
|
|
28
21
|
* Returns `null` if the input can't be coerced into a valid rect (e.g. a
|
|
29
22
|
* zero-area selection).
|
|
30
23
|
*/
|
|
31
24
|
export function cropAreaPercentToRect(area) {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
h = roundTo(1 - y, CROP_PRECISION);
|
|
40
|
-
if (!isValidCropRect(x, y, w, h))
|
|
41
|
-
return null;
|
|
42
|
-
return { x, y, w, h };
|
|
25
|
+
const rect = roundCropRect({
|
|
26
|
+
x: clamp01(area.x / 100),
|
|
27
|
+
y: clamp01(area.y / 100),
|
|
28
|
+
w: clamp01(area.width / 100),
|
|
29
|
+
h: clamp01(area.height / 100),
|
|
30
|
+
});
|
|
31
|
+
return isValidCropRect(rect.x, rect.y, rect.w, rect.h) ? rect : null;
|
|
43
32
|
}
|
|
44
33
|
/**
|
|
45
34
|
* Inverse of `cropAreaPercentToRect` - seeds react-easy-crop's
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The URL the editor displays a content image at. A src in canopycms's own `/assets` space is put
|
|
3
|
+
* behind `baseUrl` (the authenticated raw route, see `AssetContext`); any other src - a site's own
|
|
4
|
+
* static file, an off-site URL, a `data:` URI - is shown exactly as written, because the raw route
|
|
5
|
+
* serves only the asset store.
|
|
6
|
+
*/
|
|
7
|
+
export declare function editorImageSrc(src: string, baseUrl: string): string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { assetUrl } from '../../assets/asset-url.js';
|
|
2
|
+
import { ASSET_PREFIXES } from '../../assets/asset-prefixes.js';
|
|
3
|
+
const ASSET_SPACE = `/${ASSET_PREFIXES.public}/`;
|
|
4
|
+
/**
|
|
5
|
+
* The URL the editor displays a content image at. A src in canopycms's own `/assets` space is put
|
|
6
|
+
* behind `baseUrl` (the authenticated raw route, see `AssetContext`); any other src - a site's own
|
|
7
|
+
* static file, an off-site URL, a `data:` URI - is shown exactly as written, because the raw route
|
|
8
|
+
* serves only the asset store.
|
|
9
|
+
*/
|
|
10
|
+
export function editorImageSrc(src, baseUrl) {
|
|
11
|
+
return src.startsWith(ASSET_SPACE) ? assetUrl({ src }, { baseUrl }) : src;
|
|
12
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The asset prefix a framing editor hands its live preview, held for `assets/asset-url.ts`. Its
|
|
3
|
+
* only writer outside tests is `usePreviewData`, on a trusted draft message; the read is gated on `window` too,
|
|
4
|
+
* because `assetUrl` also runs in server renders and static builds. Imports nothing.
|
|
5
|
+
*/
|
|
6
|
+
export declare function setPreviewAssetBase(base: string | undefined): void;
|
|
7
|
+
/** The stored prefix in a browser; always `undefined` on the server. */
|
|
8
|
+
export declare function getPreviewAssetBase(): string | undefined;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The asset prefix a framing editor hands its live preview, held for `assets/asset-url.ts`. Its
|
|
3
|
+
* only writer outside tests is `usePreviewData`, on a trusted draft message; the read is gated on `window` too,
|
|
4
|
+
* because `assetUrl` also runs in server renders and static builds. Imports nothing.
|
|
5
|
+
*/
|
|
6
|
+
let previewAssetBase;
|
|
7
|
+
export function setPreviewAssetBase(base) {
|
|
8
|
+
previewAssetBase = base;
|
|
9
|
+
}
|
|
10
|
+
/** The stored prefix in a browser; always `undefined` on the server. */
|
|
11
|
+
export function getPreviewAssetBase() {
|
|
12
|
+
return typeof window === 'undefined' ? undefined : previewAssetBase;
|
|
13
|
+
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import type { CSSProperties } from 'react';
|
|
2
1
|
import { type CanopyPathSegment } from './canopy-path.js';
|
|
3
|
-
export declare const __CANOPY_PREVIEW_CLIENT__ = true;
|
|
4
2
|
export declare const CANOPY_PREVIEW_MESSAGE = "canopycms:draft:update";
|
|
5
3
|
export declare const CANOPY_PREVIEW_FOCUS = "canopycms:preview:focus";
|
|
6
4
|
export declare const CANOPY_PREVIEW_HIGHLIGHT = "canopycms:preview:highlight";
|
|
@@ -11,6 +9,8 @@ export interface DraftUpdateMessage {
|
|
|
11
9
|
path: string;
|
|
12
10
|
data?: unknown;
|
|
13
11
|
isLoading?: unknown;
|
|
12
|
+
/** The editor's authenticated asset route; see `PreviewFrame`'s `assetBase`. */
|
|
13
|
+
assetBase?: unknown;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
16
|
* Resolve a (possibly relative) URL to an origin for postMessage targeting.
|
|
@@ -18,6 +18,13 @@ export interface DraftUpdateMessage {
|
|
|
18
18
|
* need no configuration.
|
|
19
19
|
*/
|
|
20
20
|
export declare const resolveMessageOrigin: (url?: string) => string;
|
|
21
|
+
/**
|
|
22
|
+
* Opaque origins (sandboxed iframes, data: URLs) serialize to the string 'null':
|
|
23
|
+
* never trust them for inbound messages, and never post to them — every window in
|
|
24
|
+
* a sandboxed embed shares that string, so it identifies nothing, and postMessage
|
|
25
|
+
* throws on a literal 'null' targetOrigin. '' covers the SSR fallback.
|
|
26
|
+
*/
|
|
27
|
+
export declare const isOpaqueOrigin: (origin: string) => boolean;
|
|
21
28
|
/**
|
|
22
29
|
* True only for messages from the embedding editor window with the expected origin.
|
|
23
30
|
*
|
|
@@ -28,7 +35,6 @@ export declare const resolveMessageOrigin: (url?: string) => string;
|
|
|
28
35
|
* expected editor origin (same-origin unless `editorOrigin` is configured).
|
|
29
36
|
*/
|
|
30
37
|
export declare const isTrustedEditorMessage: (event: MessageEvent, editorOrigin?: string) => boolean;
|
|
31
|
-
export declare const sendDraftUpdate: (iframe: HTMLIFrameElement | null, message: DraftUpdateMessage, targetOrigin?: string) => void;
|
|
32
38
|
export interface PreviewFocusMessage {
|
|
33
39
|
type: typeof CANOPY_PREVIEW_FOCUS;
|
|
34
40
|
entryPath: string;
|
|
@@ -88,21 +94,3 @@ export declare const usePreviewHighlight: (opts?: {
|
|
|
88
94
|
export declare const usePreviewFocusEmitter: (entryPath: string, opts?: {
|
|
89
95
|
editorOrigin?: string;
|
|
90
96
|
}) => void;
|
|
91
|
-
/**
|
|
92
|
-
* Lightweight iframe wrapper to keep the preview in sync with form state.
|
|
93
|
-
* It posts the latest draft data to the iframe after load and when data changes.
|
|
94
|
-
*/
|
|
95
|
-
export declare const PreviewFrame: ({ src, path, data, isLoading, className, style, highlightEnabled, onPreviewError, }: {
|
|
96
|
-
src: string;
|
|
97
|
-
path: string;
|
|
98
|
-
data?: unknown;
|
|
99
|
-
isLoading?: unknown;
|
|
100
|
-
className?: string;
|
|
101
|
-
style?: CSSProperties;
|
|
102
|
-
highlightEnabled?: boolean;
|
|
103
|
-
/** Called when the preview reports a draft compile/render error; null clears it. */
|
|
104
|
-
onPreviewError?: (error: {
|
|
105
|
-
message: string;
|
|
106
|
-
fieldPath?: string;
|
|
107
|
-
} | null) => void;
|
|
108
|
-
}) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* The preview bridge's protocol and its host-page side: the hooks a framed page runs. Served to
|
|
4
|
+
* hosts through `canopycms/preview`, so everything it reaches stays free of CSS and editor UI
|
|
5
|
+
* (`pnpm lint:bundle`); the editor's side is `PreviewFrame.tsx`.
|
|
6
|
+
*/
|
|
7
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
8
|
+
import { toSameOriginPath } from '../utils/url-prefix.js';
|
|
4
9
|
import { formatCanopyPath } from './canopy-path.js';
|
|
5
|
-
|
|
10
|
+
import { setPreviewAssetBase } from './preview-asset-base.js';
|
|
11
|
+
import { isSamePreviewPath } from './preview-path.js';
|
|
6
12
|
export const CANOPY_PREVIEW_MESSAGE = 'canopycms:draft:update';
|
|
7
13
|
export const CANOPY_PREVIEW_FOCUS = 'canopycms:preview:focus';
|
|
8
14
|
export const CANOPY_PREVIEW_HIGHLIGHT = 'canopycms:preview:highlight';
|
|
9
15
|
export const CANOPY_PREVIEW_READY = 'canopycms:preview:ready';
|
|
10
16
|
export const CANOPY_PREVIEW_ERROR = 'canopycms:preview:error';
|
|
17
|
+
/** `assetBase` from a draft, only if it is a same-origin path: nothing else may steer `<img>`s. */
|
|
18
|
+
const readAssetBase = (value) => typeof value === 'string' && value.startsWith('/') && toSameOriginPath(value) === value
|
|
19
|
+
? value
|
|
20
|
+
: undefined;
|
|
11
21
|
/**
|
|
12
22
|
* Resolve a (possibly relative) URL to an origin for postMessage targeting.
|
|
13
23
|
* Falls back to the page's own origin, so same-origin editor/preview setups
|
|
@@ -31,7 +41,7 @@ export const resolveMessageOrigin = (url) => {
|
|
|
31
41
|
* a sandboxed embed shares that string, so it identifies nothing, and postMessage
|
|
32
42
|
* throws on a literal 'null' targetOrigin. '' covers the SSR fallback.
|
|
33
43
|
*/
|
|
34
|
-
const isOpaqueOrigin = (origin) => origin === 'null' || origin === '';
|
|
44
|
+
export const isOpaqueOrigin = (origin) => origin === 'null' || origin === '';
|
|
35
45
|
/**
|
|
36
46
|
* True only for messages from the embedding editor window with the expected origin.
|
|
37
47
|
*
|
|
@@ -49,14 +59,6 @@ export const isTrustedEditorMessage = (event, editorOrigin) => {
|
|
|
49
59
|
return false;
|
|
50
60
|
return event.origin === expected && event.source === window.parent;
|
|
51
61
|
};
|
|
52
|
-
export const sendDraftUpdate = (iframe, message, targetOrigin) => {
|
|
53
|
-
if (!iframe?.contentWindow)
|
|
54
|
-
return;
|
|
55
|
-
const target = targetOrigin ?? resolveMessageOrigin(iframe.src);
|
|
56
|
-
if (isOpaqueOrigin(target))
|
|
57
|
-
return;
|
|
58
|
-
iframe.contentWindow.postMessage(message, target);
|
|
59
|
-
};
|
|
60
62
|
const resolvePreviewPath = (explicit) => {
|
|
61
63
|
if (explicit)
|
|
62
64
|
return explicit;
|
|
@@ -116,8 +118,12 @@ export const usePreviewData = (path, initialData, opts) => {
|
|
|
116
118
|
if (!isTrustedEditorMessage(event, editorOrigin))
|
|
117
119
|
return;
|
|
118
120
|
const msg = event.data;
|
|
119
|
-
if (!msg || msg.type !== CANOPY_PREVIEW_MESSAGE
|
|
121
|
+
if (!msg || msg.type !== CANOPY_PREVIEW_MESSAGE)
|
|
122
|
+
return;
|
|
123
|
+
if (typeof msg.path !== 'string' || !isSamePreviewPath(msg.path, path))
|
|
120
124
|
return;
|
|
125
|
+
// Before setData, so the render the draft triggers already reads it.
|
|
126
|
+
setPreviewAssetBase(readAssetBase(msg.assetBase));
|
|
121
127
|
setData(msg.data);
|
|
122
128
|
if (msg.isLoading !== undefined) {
|
|
123
129
|
setIsLoading(msg.isLoading);
|
|
@@ -204,129 +210,3 @@ export const usePreviewFocusEmitter = (entryPath, opts) => {
|
|
|
204
210
|
return () => document.removeEventListener('click', handleClick);
|
|
205
211
|
}, [entryPath, editorOrigin]);
|
|
206
212
|
};
|
|
207
|
-
/**
|
|
208
|
-
* Lightweight iframe wrapper to keep the preview in sync with form state.
|
|
209
|
-
* It posts the latest draft data to the iframe after load and when data changes.
|
|
210
|
-
*/
|
|
211
|
-
export const PreviewFrame = ({ src, path, data, isLoading, className, style, highlightEnabled, onPreviewError, }) => {
|
|
212
|
-
const iframeRef = useRef(null);
|
|
213
|
-
// Pin the preview origin from the src prop: outbound messages target it (never '*'),
|
|
214
|
-
// and inbound messages must come from it. An iframe that navigates cross-origin
|
|
215
|
-
// silently stops participating in the bridge.
|
|
216
|
-
const previewOrigin = resolveMessageOrigin(src);
|
|
217
|
-
// Show progress bar while waiting for the preview's ready handshake.
|
|
218
|
-
const [syncPending, setSyncPending] = useState(data !== undefined);
|
|
219
|
-
// Reset when navigating to a different entry (src change = new iframe page load).
|
|
220
|
-
const [prevSrc, setPrevSrc] = useState(src);
|
|
221
|
-
if (src !== prevSrc) {
|
|
222
|
-
setPrevSrc(src);
|
|
223
|
-
setSyncPending(data !== undefined);
|
|
224
|
-
}
|
|
225
|
-
// Inject the progress bar keyframe animation once per page.
|
|
226
|
-
useEffect(() => {
|
|
227
|
-
const styleId = 'canopycms-preview-sync-style';
|
|
228
|
-
if (!document.getElementById(styleId)) {
|
|
229
|
-
const el = document.createElement('style');
|
|
230
|
-
el.id = styleId;
|
|
231
|
-
el.textContent = `@keyframes canopy-preview-sync { 0% { transform: translateX(-100%); } 100% { transform: translateX(250%); } }`;
|
|
232
|
-
document.head.appendChild(el);
|
|
233
|
-
}
|
|
234
|
-
}, []);
|
|
235
|
-
const post = () => {
|
|
236
|
-
if (data === undefined)
|
|
237
|
-
return;
|
|
238
|
-
sendDraftUpdate(iframeRef.current, {
|
|
239
|
-
type: CANOPY_PREVIEW_MESSAGE,
|
|
240
|
-
path,
|
|
241
|
-
data,
|
|
242
|
-
isLoading,
|
|
243
|
-
}, previewOrigin);
|
|
244
|
-
};
|
|
245
|
-
const postHighlight = () => {
|
|
246
|
-
if (!iframeRef.current?.contentWindow)
|
|
247
|
-
return;
|
|
248
|
-
if (isOpaqueOrigin(previewOrigin))
|
|
249
|
-
return;
|
|
250
|
-
const msg = {
|
|
251
|
-
type: CANOPY_PREVIEW_HIGHLIGHT,
|
|
252
|
-
enabled: Boolean(highlightEnabled),
|
|
253
|
-
};
|
|
254
|
-
iframeRef.current.contentWindow.postMessage(msg, previewOrigin);
|
|
255
|
-
};
|
|
256
|
-
// Keep refs pointing at the latest closures so the message handler below never goes stale.
|
|
257
|
-
const postRef = useRef(post);
|
|
258
|
-
const postHighlightRef = useRef(postHighlight);
|
|
259
|
-
const onPreviewErrorRef = useRef(onPreviewError);
|
|
260
|
-
useEffect(() => {
|
|
261
|
-
postRef.current = post;
|
|
262
|
-
postHighlightRef.current = postHighlight;
|
|
263
|
-
onPreviewErrorRef.current = onPreviewError;
|
|
264
|
-
});
|
|
265
|
-
useEffect(() => {
|
|
266
|
-
post();
|
|
267
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps -- post is tracked via ref
|
|
268
|
-
}, [data, isLoading]);
|
|
269
|
-
useEffect(() => {
|
|
270
|
-
postHighlight();
|
|
271
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps -- postHighlight is tracked via ref
|
|
272
|
-
}, [highlightEnabled]);
|
|
273
|
-
// When the preview page's React effects have run and its message listener is ready,
|
|
274
|
-
// it sends CANOPY_PREVIEW_READY. We respond with the current data so the preview
|
|
275
|
-
// receives the draft even if it wasn't ready when onLoad fired.
|
|
276
|
-
useEffect(() => {
|
|
277
|
-
const handleMessage = (event) => {
|
|
278
|
-
if (!iframeRef.current || event.source !== iframeRef.current.contentWindow)
|
|
279
|
-
return;
|
|
280
|
-
// Source check alone is insufficient if the iframe navigated cross-origin.
|
|
281
|
-
if (isOpaqueOrigin(previewOrigin) || event.origin !== previewOrigin)
|
|
282
|
-
return;
|
|
283
|
-
const type = event.data?.type;
|
|
284
|
-
if (type === CANOPY_PREVIEW_READY) {
|
|
285
|
-
postRef.current();
|
|
286
|
-
postHighlightRef.current();
|
|
287
|
-
setSyncPending(false);
|
|
288
|
-
}
|
|
289
|
-
else if (type === CANOPY_PREVIEW_ERROR) {
|
|
290
|
-
const msg = event.data;
|
|
291
|
-
// Shape-check the payload: an adopter passing an Error object (instead of
|
|
292
|
-
// err.message) must not crash the editor when rendered as a React child.
|
|
293
|
-
if (msg.message != null && typeof msg.message !== 'string')
|
|
294
|
-
return;
|
|
295
|
-
const fieldPath = typeof msg.fieldPath === 'string' ? msg.fieldPath : undefined;
|
|
296
|
-
onPreviewErrorRef.current?.(msg.message == null
|
|
297
|
-
? null
|
|
298
|
-
: { message: msg.message, ...(fieldPath ? { fieldPath } : {}) });
|
|
299
|
-
}
|
|
300
|
-
};
|
|
301
|
-
window.addEventListener('message', handleMessage);
|
|
302
|
-
return () => window.removeEventListener('message', handleMessage);
|
|
303
|
-
}, [previewOrigin]);
|
|
304
|
-
return (_jsxs("div", { className: className, style: { position: 'relative', overflow: 'hidden', ...style }, children: [syncPending && (_jsx("div", { style: {
|
|
305
|
-
position: 'absolute',
|
|
306
|
-
top: 0,
|
|
307
|
-
left: 0,
|
|
308
|
-
right: 0,
|
|
309
|
-
height: 3,
|
|
310
|
-
zIndex: 1,
|
|
311
|
-
overflow: 'hidden',
|
|
312
|
-
}, children: _jsx("div", { style: {
|
|
313
|
-
position: 'absolute',
|
|
314
|
-
top: 0,
|
|
315
|
-
left: 0,
|
|
316
|
-
height: '100%',
|
|
317
|
-
width: '40%',
|
|
318
|
-
background: 'var(--mantine-color-blue-filled, #228be6)',
|
|
319
|
-
borderRadius: '0 2px 2px 0',
|
|
320
|
-
animation: 'canopy-preview-sync 1.5s ease-in-out infinite',
|
|
321
|
-
} }) })), _jsx("iframe", { ref: iframeRef, src: src, style: {
|
|
322
|
-
display: 'block',
|
|
323
|
-
position: 'absolute',
|
|
324
|
-
inset: 0,
|
|
325
|
-
width: '100%',
|
|
326
|
-
height: '100%',
|
|
327
|
-
border: 'none',
|
|
328
|
-
}, onLoad: () => {
|
|
329
|
-
post();
|
|
330
|
-
postHighlight();
|
|
331
|
-
} })] }));
|
|
332
|
-
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reduces a preview URL to what identifies the page, its path and query, with no origin,
|
|
3
|
+
* fragment or trailing slash. The editor names a page by the `src` it built and the framed page
|
|
4
|
+
* by its own location. An absolute `src`, or a host redirect that adds or drops a trailing slash,
|
|
5
|
+
* spells the same page two ways, and the bridge must still match them.
|
|
6
|
+
* @internal Exported for tests.
|
|
7
|
+
*/
|
|
8
|
+
export declare const normalizePreviewPath: (url: string) => string;
|
|
9
|
+
/** Whether two preview URLs name the same page; see `normalizePreviewPath`. */
|
|
10
|
+
export declare const isSamePreviewPath: (a: string, b: string) => boolean;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { stripTrailingSlashes } from '../utils/url-prefix.js';
|
|
2
|
+
/**
|
|
3
|
+
* Reduces a preview URL to what identifies the page, its path and query, with no origin,
|
|
4
|
+
* fragment or trailing slash. The editor names a page by the `src` it built and the framed page
|
|
5
|
+
* by its own location. An absolute `src`, or a host redirect that adds or drops a trailing slash,
|
|
6
|
+
* spells the same page two ways, and the bridge must still match them.
|
|
7
|
+
* @internal Exported for tests.
|
|
8
|
+
*/
|
|
9
|
+
export const normalizePreviewPath = (url) => {
|
|
10
|
+
let parsed;
|
|
11
|
+
try {
|
|
12
|
+
parsed = new URL(url, 'http://preview.invalid');
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return url;
|
|
16
|
+
}
|
|
17
|
+
return `${stripTrailingSlashes(parsed.pathname) || '/'}${parsed.search}`;
|
|
18
|
+
};
|
|
19
|
+
/** Whether two preview URLs name the same page; see `normalizePreviewPath`. */
|
|
20
|
+
export const isSamePreviewPath = (a, b) => normalizePreviewPath(a) === normalizePreviewPath(b);
|
package/dist/editor/theme.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useMemo } from 'react';
|
|
4
|
-
import { DEFAULT_THEME, MantineProvider, createTheme, } from '@mantine/core';
|
|
4
|
+
import { DEFAULT_THEME, MantineProvider, getDefaultZIndex, createTheme, } from '@mantine/core';
|
|
5
5
|
import { Notifications } from '@mantine/notifications';
|
|
6
6
|
import { ModalsProvider } from '@mantine/modals';
|
|
7
7
|
import { generateColors } from '@mantine/colors-generator';
|
|
@@ -62,5 +62,5 @@ export const createCanopyTheme = (options) => {
|
|
|
62
62
|
};
|
|
63
63
|
export const CanopyCMSProvider = ({ children, colors, colorScheme = 'light', themeOverride, withNotifications = true, }) => {
|
|
64
64
|
const theme = useMemo(() => createCanopyTheme({ colors, themeOverride }), [colors, themeOverride]);
|
|
65
|
-
return (_jsx(MantineProvider, { theme: theme, defaultColorScheme: colorScheme, children: _jsxs(ModalsProvider, { children: [withNotifications ? _jsx(Notifications, { position: "bottom-left" }) : null, children] }) }));
|
|
65
|
+
return (_jsx(MantineProvider, { theme: theme, defaultColorScheme: colorScheme, children: _jsxs(ModalsProvider, { modalProps: { zIndex: getDefaultZIndex('popover') }, children: [withNotifications ? _jsx(Notifications, { position: "bottom-left" }) : null, children] }) }));
|
|
66
66
|
};
|
|
@@ -12,10 +12,10 @@ import type { EntrySchemaRegistry } from './schema/types.js';
|
|
|
12
12
|
* interface to maintain. Keyed any other way, the derived map is keyed by that
|
|
13
13
|
* string instead and will not plug into `TEntryTypes`.
|
|
14
14
|
*
|
|
15
|
-
* Rejected at call time: an empty registry; a schema that is not a non-empty
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* Rejected at call time: an empty registry; a schema that is not a non-empty `EntrySchema`
|
|
16
|
+
* array; more than one `isTitle` per schema, or one on a non-string field or inside a list;
|
|
17
|
+
* more than one `isBody`, or one on a field that is not markdown/mdx or is named one of
|
|
18
|
+
* `RESOLVED_REFERENCE_KEYS`; a top-level or inline-group field named `unavailable`. This is also the one
|
|
19
19
|
* place the shared field-shape checks run: select fields must have options,
|
|
20
20
|
* reference fields must have `collections` or `entryTypes`, no inline groups
|
|
21
21
|
* inside object/block fields, no field-name collisions after group flattening.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { countTitleFields, findInvalidTitleFields, findTitleFieldsInLists, } from './utils/title-field.js';
|
|
2
2
|
import { countBodyFields, findInvalidBodyFields, findReservedBodyFieldName, } from './utils/body-field.js';
|
|
3
|
-
import { RESOLVED_REFERENCE_KEYS } from './entry-schema.js';
|
|
3
|
+
import { RESOLVED_REFERENCE_KEYS, RESTRICTED_REFERENCE_MARKER } from './entry-schema.js';
|
|
4
4
|
import { flattenGroupFields } from './utils/flatten-group-fields.js';
|
|
5
|
-
import { ensureSelectFieldsHaveOptions, ensureReferenceFieldsHaveScope, ensureNoFlattenedFieldNameCollisions, ensureNoGroupsInsideComplexFields, } from './config/validation.js';
|
|
5
|
+
import { ensureSelectFieldsHaveOptions, ensureReferenceFieldsHaveScope, ensureItemTitleFieldsExist, ensureNoFlattenedFieldNameCollisions, ensureNoGroupsInsideComplexFields, } from './config/validation.js';
|
|
6
6
|
/** Look up a field's type by dotted path (e.g., "meta.order").
|
|
7
7
|
* Groups are transparent — their children are searched at the same path level. */
|
|
8
8
|
function findFieldType(fields, dottedPath) {
|
|
@@ -35,10 +35,10 @@ function findFieldType(fields, dottedPath) {
|
|
|
35
35
|
* interface to maintain. Keyed any other way, the derived map is keyed by that
|
|
36
36
|
* string instead and will not plug into `TEntryTypes`.
|
|
37
37
|
*
|
|
38
|
-
* Rejected at call time: an empty registry; a schema that is not a non-empty
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
38
|
+
* Rejected at call time: an empty registry; a schema that is not a non-empty `EntrySchema`
|
|
39
|
+
* array; more than one `isTitle` per schema, or one on a non-string field or inside a list;
|
|
40
|
+
* more than one `isBody`, or one on a field that is not markdown/mdx or is named one of
|
|
41
|
+
* `RESOLVED_REFERENCE_KEYS`; a top-level or inline-group field named `unavailable`. This is also the one
|
|
42
42
|
* place the shared field-shape checks run: select fields must have options,
|
|
43
43
|
* reference fields must have `collections` or `entryTypes`, no inline groups
|
|
44
44
|
* inside object/block fields, no field-name collisions after group flattening.
|
|
@@ -92,8 +92,12 @@ export function createEntrySchemaRegistry(registry) {
|
|
|
92
92
|
if (reservedBodyField) {
|
|
93
93
|
throw new Error(`Entry schema registry entry "${key}": field "${reservedBodyField}" has isBody: true but "${reservedBodyField}" is reserved — reference resolution sets ${RESOLVED_REFERENCE_KEYS.map((k) => `"${k}"`).join(', ')} on a resolved reference, and a body field with one of those names would overwrite it. Rename the field (the body's field name is yours to choose; only these four are reserved).`);
|
|
94
94
|
}
|
|
95
|
+
if (flattenGroupFields(schema).some((f) => f.name === RESTRICTED_REFERENCE_MARKER)) {
|
|
96
|
+
throw new Error(`Entry schema registry entry "${key}": field "${RESTRICTED_REFERENCE_MARKER}" is reserved — a resolved reference carries "${RESTRICTED_REFERENCE_MARKER}: true" only when the reader may not read its target, so a field with that name cannot be delivered on a reference to this entry type. Rename the field.`);
|
|
97
|
+
}
|
|
95
98
|
ensureSelectFieldsHaveOptions(schema);
|
|
96
99
|
ensureReferenceFieldsHaveScope(schema);
|
|
100
|
+
ensureItemTitleFieldsExist(schema);
|
|
97
101
|
ensureNoGroupsInsideComplexFields(schema);
|
|
98
102
|
ensureNoFlattenedFieldNameCollisions(schema, `entry schema "${key}"`);
|
|
99
103
|
}
|
package/dist/entry-schema.d.ts
CHANGED
|
@@ -81,9 +81,6 @@ export interface ResolvedReferenceMeta {
|
|
|
81
81
|
* Assemble a resolved reference: the target's own data, then its body if the field asked to
|
|
82
82
|
* embed it, then the reserved metadata.
|
|
83
83
|
*
|
|
84
|
-
* Exists so the two places that construct one — the server resolver in content-store.ts and
|
|
85
|
-
* the editor's live-preview endpoint in api/resolve-references.ts — cannot drift.
|
|
86
|
-
*
|
|
87
84
|
* The ordering is the contract, not a detail. Metadata LAST means a target that models `id` as
|
|
88
85
|
* a content field cannot shadow the real content ID, which the write boundary recovers from
|
|
89
86
|
* `value.id` (`referenceValueId`) — a shadowed id makes a re-save persist the wrong value and
|
|
@@ -95,6 +92,30 @@ export declare function buildResolvedReference(data: Record<string, unknown>, me
|
|
|
95
92
|
fieldName: string;
|
|
96
93
|
value: string | undefined;
|
|
97
94
|
}): Record<string, unknown>;
|
|
95
|
+
/**
|
|
96
|
+
* The key that marks a `RestrictedReference`. Reserved as an entry field name, at the top level
|
|
97
|
+
* of any schema, because any schema can be a reference target.
|
|
98
|
+
*/
|
|
99
|
+
export declare const RESTRICTED_REFERENCE_MARKER = "unavailable";
|
|
100
|
+
/**
|
|
101
|
+
* What a reference resolves to when the reader may not read its target: enough to render a
|
|
102
|
+
* link (a title and the URL, which a denied reader can follow to a sign-in page) and nothing
|
|
103
|
+
* else of the target's data.
|
|
104
|
+
*
|
|
105
|
+
* `unavailable: true` is the marker a renderer branches on, so a denied target never arrives
|
|
106
|
+
* looking like a full one with its fields `undefined`; `reason` says why, leaving room for
|
|
107
|
+
* other reasons a target cannot be shown. A reference the reader may see in full never carries
|
|
108
|
+
* `unavailable`. `id` is always present, so a save of the referring entry writes the reference
|
|
109
|
+
* back unchanged (`normalizeReferenceValues` recovers it from `value.id`).
|
|
110
|
+
*/
|
|
111
|
+
export type RestrictedReference = Simplify<ResolvedReferenceMeta & {
|
|
112
|
+
/** The target's display title, by `resolveEntryTitle`'s fallback chain. */
|
|
113
|
+
title: string;
|
|
114
|
+
unavailable: true;
|
|
115
|
+
reason: 'restricted';
|
|
116
|
+
}>;
|
|
117
|
+
/** Assemble a {@link RestrictedReference}. Takes no target data, so none can leak into it. */
|
|
118
|
+
export declare function buildRestrictedReference(meta: ResolvedReferenceMeta, title: string): RestrictedReference;
|
|
98
119
|
/**
|
|
99
120
|
* Recursively flatten inline groups (type: 'group') out of a field tuple: they contribute
|
|
100
121
|
* no key to the content shape, so their children merge into the parent level and
|
|
@@ -205,7 +226,9 @@ type FieldValue<F extends InferableField> = F extends {
|
|
|
205
226
|
} ? ScalarValue<F, SelectValue<F>> : F extends {
|
|
206
227
|
type: 'reference';
|
|
207
228
|
resolvedSchema: infer S;
|
|
208
|
-
} ? ScalarValue<F, (InferContentShape<Extract<S, readonly InferableField[]>> & ResolvedReferenceMeta
|
|
229
|
+
} ? ScalarValue<F, (InferContentShape<Extract<S, readonly InferableField[]>> & ResolvedReferenceMeta & {
|
|
230
|
+
unavailable?: undefined;
|
|
231
|
+
}) | RestrictedReference | null> : F extends {
|
|
209
232
|
type: 'reference';
|
|
210
233
|
} ? ScalarValue<F, string | null> : F extends {
|
|
211
234
|
type: 'image';
|
package/dist/entry-schema.js
CHANGED
|
@@ -14,9 +14,6 @@ export const RESOLVED_REFERENCE_KEYS = ['id', 'slug', 'collection', 'urlPath'];
|
|
|
14
14
|
* Assemble a resolved reference: the target's own data, then its body if the field asked to
|
|
15
15
|
* embed it, then the reserved metadata.
|
|
16
16
|
*
|
|
17
|
-
* Exists so the two places that construct one — the server resolver in content-store.ts and
|
|
18
|
-
* the editor's live-preview endpoint in api/resolve-references.ts — cannot drift.
|
|
19
|
-
*
|
|
20
17
|
* The ordering is the contract, not a detail. Metadata LAST means a target that models `id` as
|
|
21
18
|
* a content field cannot shadow the real content ID, which the write boundary recovers from
|
|
22
19
|
* `value.id` (`referenceValueId`) — a shadowed id makes a re-save persist the wrong value and
|
|
@@ -35,8 +32,28 @@ export function buildResolvedReference(data, meta, body) {
|
|
|
35
32
|
resolved.slug = meta.slug;
|
|
36
33
|
resolved.collection = meta.collection;
|
|
37
34
|
resolved.urlPath = meta.urlPath;
|
|
35
|
+
// Backstop for the registry's rejection of a field with this name (entry-schema-registry.ts):
|
|
36
|
+
// a full reference carrying the marker would read as one the reader may not see.
|
|
37
|
+
delete resolved[RESTRICTED_REFERENCE_MARKER];
|
|
38
38
|
return resolved;
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* The key that marks a `RestrictedReference`. Reserved as an entry field name, at the top level
|
|
42
|
+
* of any schema, because any schema can be a reference target.
|
|
43
|
+
*/
|
|
44
|
+
export const RESTRICTED_REFERENCE_MARKER = 'unavailable';
|
|
45
|
+
/** Assemble a {@link RestrictedReference}. Takes no target data, so none can leak into it. */
|
|
46
|
+
export function buildRestrictedReference(meta, title) {
|
|
47
|
+
return {
|
|
48
|
+
id: meta.id,
|
|
49
|
+
slug: meta.slug,
|
|
50
|
+
collection: meta.collection,
|
|
51
|
+
urlPath: meta.urlPath,
|
|
52
|
+
title,
|
|
53
|
+
unavailable: true,
|
|
54
|
+
reason: 'restricted',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
40
57
|
/** Define an entry schema's field array with literal inference, without sprinkling `as const`. */
|
|
41
58
|
export const defineEntrySchema = (fields) => fields;
|
|
42
59
|
/**
|