@createcms/core 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +169 -0
- package/dist/ab-edge/index.cjs +214 -0
- package/dist/ab-edge/index.d.cts +121 -0
- package/dist/ab-edge/index.d.ts +121 -0
- package/dist/ab-edge/index.js +205 -0
- package/dist/bin/createcms.js +3082 -0
- package/dist/db.cjs +496 -0
- package/dist/db.d.cts +128 -0
- package/dist/db.d.ts +128 -0
- package/dist/db.js +488 -0
- package/dist/index.cjs +13789 -0
- package/dist/index.d.cts +10277 -0
- package/dist/index.d.ts +10277 -0
- package/dist/index.js +13737 -0
- package/dist/nanoid.cjs +50 -0
- package/dist/nanoid.d.cts +29 -0
- package/dist/nanoid.d.ts +29 -0
- package/dist/nanoid.js +47 -0
- package/dist/next/index.cjs +60 -0
- package/dist/next/index.d.cts +141 -0
- package/dist/next/index.d.ts +141 -0
- package/dist/next/index.js +58 -0
- package/dist/next/middleware.cjs +113 -0
- package/dist/next/middleware.d.cts +77 -0
- package/dist/next/middleware.d.ts +77 -0
- package/dist/next/middleware.js +111 -0
- package/dist/plugins/ab-test/analytics/upstash.cjs +345 -0
- package/dist/plugins/ab-test/analytics/upstash.d.cts +193 -0
- package/dist/plugins/ab-test/analytics/upstash.d.ts +193 -0
- package/dist/plugins/ab-test/analytics/upstash.js +343 -0
- package/dist/plugins/ab-test/client.cjs +686 -0
- package/dist/plugins/ab-test/client.d.cts +233 -0
- package/dist/plugins/ab-test/client.d.ts +233 -0
- package/dist/plugins/ab-test/client.js +684 -0
- package/dist/plugins/ab-test/index.cjs +3400 -0
- package/dist/plugins/ab-test/index.d.cts +1131 -0
- package/dist/plugins/ab-test/index.d.ts +1131 -0
- package/dist/plugins/ab-test/index.js +3367 -0
- package/dist/plugins/client.cjs +20 -0
- package/dist/plugins/client.d.cts +3 -0
- package/dist/plugins/client.d.ts +3 -0
- package/dist/plugins/client.js +3 -0
- package/dist/plugins/consent/client.cjs +315 -0
- package/dist/plugins/consent/client.d.cts +145 -0
- package/dist/plugins/consent/client.d.ts +145 -0
- package/dist/plugins/consent/client.js +313 -0
- package/dist/plugins/consent/index.cjs +267 -0
- package/dist/plugins/consent/index.d.cts +618 -0
- package/dist/plugins/consent/index.d.ts +618 -0
- package/dist/plugins/consent/index.js +258 -0
- package/dist/plugins/i18n/index.cjs +2177 -0
- package/dist/plugins/i18n/index.d.cts +562 -0
- package/dist/plugins/i18n/index.d.ts +562 -0
- package/dist/plugins/i18n/index.js +2150 -0
- package/dist/plugins/media-optimize/index.cjs +315 -0
- package/dist/plugins/media-optimize/index.d.cts +144 -0
- package/dist/plugins/media-optimize/index.d.ts +144 -0
- package/dist/plugins/media-optimize/index.js +311 -0
- package/dist/plugins/multi-tenant/index.cjs +210 -0
- package/dist/plugins/multi-tenant/index.d.cts +431 -0
- package/dist/plugins/multi-tenant/index.d.ts +431 -0
- package/dist/plugins/multi-tenant/index.js +207 -0
- package/dist/plugins/server.cjs +24 -0
- package/dist/plugins/server.d.cts +3 -0
- package/dist/plugins/server.d.ts +3 -0
- package/dist/plugins/server.js +3 -0
- package/dist/react/blocks.cjs +233 -0
- package/dist/react/blocks.d.cts +320 -0
- package/dist/react/blocks.d.ts +320 -0
- package/dist/react/blocks.js +226 -0
- package/dist/react/index.cjs +901 -0
- package/dist/react/index.d.cts +992 -0
- package/dist/react/index.d.ts +992 -0
- package/dist/react/index.js +872 -0
- package/dist/react/tracking.cjs +243 -0
- package/dist/react/tracking.d.cts +364 -0
- package/dist/react/tracking.d.ts +364 -0
- package/dist/react/tracking.js +216 -0
- package/dist/react/variant.cjs +59 -0
- package/dist/react/variant.d.cts +26 -0
- package/dist/react/variant.d.ts +26 -0
- package/dist/react/variant.js +57 -0
- package/package.json +303 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
type BlockTreeNode = {
|
|
4
|
+
blockId: string;
|
|
5
|
+
type: string;
|
|
6
|
+
properties: Record<string, unknown>;
|
|
7
|
+
children: BlockTreeNode[];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* One published branch of a root, as a snapshot. Shared by the page-level
|
|
12
|
+
* variant shape and the block-level A/B `variants` (below) so the two can't
|
|
13
|
+
* drift. `properties` mirrors the (depth-1 typed) reference `properties`.
|
|
14
|
+
*/
|
|
15
|
+
type PublishedBranchSnapshot<TProps = Record<string, unknown>> = {
|
|
16
|
+
branchId: string;
|
|
17
|
+
isControl: boolean;
|
|
18
|
+
properties: TProps;
|
|
19
|
+
tree: BlockTreeNode;
|
|
20
|
+
};
|
|
21
|
+
type ResolvedReference<TProps = Record<string, unknown>> = {
|
|
22
|
+
rootId: string;
|
|
23
|
+
collection: string;
|
|
24
|
+
properties: TProps;
|
|
25
|
+
tree: BlockTreeNode;
|
|
26
|
+
/**
|
|
27
|
+
* Present only when this referenced root has a RUNNING A/B test (AB_FANOUT
|
|
28
|
+
* F2 server fan-out). The server stays a pure, cacheable function: top-level
|
|
29
|
+
* `tree`/`properties` are the CONTROL branch (a no-JS / AB-disabled client
|
|
30
|
+
* renders it as-is), and the client pre-render pass (F3) picks the visitor's
|
|
31
|
+
* variant from `variants` and swaps it in. `variants` includes the control.
|
|
32
|
+
* An OPTIONAL field (not a discriminated union) so `isResolvedReference` —
|
|
33
|
+
* which narrows on `tree`/`properties` — keeps matching.
|
|
34
|
+
*/
|
|
35
|
+
abTest?: {
|
|
36
|
+
testId: string;
|
|
37
|
+
trafficPercentage: number;
|
|
38
|
+
variants: PublishedBranchSnapshot<TProps>[];
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
type BlockTypes = {
|
|
42
|
+
string: string;
|
|
43
|
+
number: number;
|
|
44
|
+
boolean: boolean;
|
|
45
|
+
date: string;
|
|
46
|
+
richText: string;
|
|
47
|
+
image: string;
|
|
48
|
+
select: string;
|
|
49
|
+
reference: string;
|
|
50
|
+
};
|
|
51
|
+
/** Reference inference mode: `raw` (write input + getBlockTree editor read) keeps
|
|
52
|
+
* a `reference` as its stored rootId string; `resolved` (getPublishedContent)
|
|
53
|
+
* surfaces the inlined `ResolvedReference`. */
|
|
54
|
+
type RefMode = 'raw' | 'resolved';
|
|
55
|
+
type BlockPropertyType = keyof BlockTypes;
|
|
56
|
+
type SelectOption = {
|
|
57
|
+
readonly label: string;
|
|
58
|
+
readonly value: string;
|
|
59
|
+
};
|
|
60
|
+
type BlockPropertySpec<T extends BlockPropertyType> = {
|
|
61
|
+
type: T;
|
|
62
|
+
required?: boolean;
|
|
63
|
+
defaultValue?: BlockTypes[T];
|
|
64
|
+
label: string;
|
|
65
|
+
description?: string;
|
|
66
|
+
placeholder?: string;
|
|
67
|
+
} & (T extends 'select' ? {
|
|
68
|
+
options: readonly SelectOption[];
|
|
69
|
+
} : {}) & (T extends 'reference' ? {
|
|
70
|
+
collection: string;
|
|
71
|
+
} : {});
|
|
72
|
+
/** Discriminated union over all concrete block-property specs. */
|
|
73
|
+
type BlockProperty = {
|
|
74
|
+
[K in BlockPropertyType]: BlockPropertySpec<K>;
|
|
75
|
+
}[BlockPropertyType];
|
|
76
|
+
type Simplify<T> = {
|
|
77
|
+
[K in keyof T]: T[K];
|
|
78
|
+
};
|
|
79
|
+
/** Extracts the runtime value type for a block property.
|
|
80
|
+
* For `select` properties with options, returns the union of option values.
|
|
81
|
+
* A `reference` is a rootId string in `raw` mode (write input + editor read) and
|
|
82
|
+
* a `ResolvedReference` in `resolved` mode (published read).
|
|
83
|
+
* For all other types, returns the corresponding primitive type. */
|
|
84
|
+
type InferPropertyValue<T extends BlockProperty, M extends RefMode = 'raw', TCol extends Record<string, AnyCollectionDefinition> = {}> = T extends {
|
|
85
|
+
type: 'select';
|
|
86
|
+
options: readonly {
|
|
87
|
+
readonly value: infer V extends string;
|
|
88
|
+
}[];
|
|
89
|
+
} ? V : T extends {
|
|
90
|
+
type: 'reference';
|
|
91
|
+
collection: infer C extends string;
|
|
92
|
+
} ? M extends 'resolved' ? C extends keyof TCol ? ResolvedReference<NonNullable<InferBlockProperties<TCol[C]['root']['properties'], 'resolved'>>> : ResolvedReference : string : BlockTypes[T['type']];
|
|
93
|
+
type RequiredPart<T extends Record<string, BlockProperty>, M extends RefMode, TCol extends Record<string, AnyCollectionDefinition>> = {
|
|
94
|
+
[K in keyof T as T[K] extends {
|
|
95
|
+
required: true;
|
|
96
|
+
} ? K : never]: InferPropertyValue<T[K], M, TCol>;
|
|
97
|
+
};
|
|
98
|
+
type OptionalPart<T extends Record<string, BlockProperty>, M extends RefMode, TCol extends Record<string, AnyCollectionDefinition>> = {
|
|
99
|
+
[K in keyof T as T[K] extends {
|
|
100
|
+
required: true;
|
|
101
|
+
} ? never : K]?: InferPropertyValue<T[K], M, TCol>;
|
|
102
|
+
};
|
|
103
|
+
type HasRequiredKeys<T extends Record<string, BlockProperty>> = true extends {
|
|
104
|
+
[K in keyof T]: T[K] extends {
|
|
105
|
+
required: true;
|
|
106
|
+
} ? true : never;
|
|
107
|
+
}[keyof T] ? true : false;
|
|
108
|
+
/** Maps a properties record to its runtime value types, respecting `required`.
|
|
109
|
+
* When all properties are optional, the entire input becomes optional (| undefined). */
|
|
110
|
+
type InferBlockProperties<T extends Record<string, BlockProperty>, M extends RefMode = 'raw', TCol extends Record<string, AnyCollectionDefinition> = {}> = HasRequiredKeys<T> extends true ? Simplify<RequiredPart<T, M, TCol> & OptionalPart<T, M, TCol>> : Simplify<RequiredPart<T, M, TCol> & OptionalPart<T, M, TCol>> | undefined;
|
|
111
|
+
/** Scalar property subset usable as an event parameter (no references/media). */
|
|
112
|
+
type ScalarBlockProperty = Extract<BlockProperty, {
|
|
113
|
+
type: 'string' | 'number' | 'boolean' | 'select' | 'date';
|
|
114
|
+
}>;
|
|
115
|
+
/**
|
|
116
|
+
* Declares a meaningful event a functional block can emit (e.g. a form's
|
|
117
|
+
* `submitSuccess`). Living on the block DEFINITION makes it the single source of
|
|
118
|
+
* truth for the typed `fire(...)` union, the test-creation goal picker, and the
|
|
119
|
+
* analytics wire name. `name` overrides the GA4/dataLayer wire name (defaults to
|
|
120
|
+
* `cms_<blockType>_<eventKey>`, computed by the measurement layer). Whether an
|
|
121
|
+
* event counts as a conversion is decided per test in the UI, not here.
|
|
122
|
+
*/
|
|
123
|
+
type EventDeclaration = {
|
|
124
|
+
/** Analytics wire-name override (snake_case). Defaults to cms_<type>_<key>. */
|
|
125
|
+
name?: string;
|
|
126
|
+
/** Typed parameters carried with the event (scalar only). */
|
|
127
|
+
params?: Record<string, ScalarBlockProperty>;
|
|
128
|
+
/** Human label for the goal picker. */
|
|
129
|
+
label?: string;
|
|
130
|
+
};
|
|
131
|
+
type BlockDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>, TEvents extends Record<string, EventDeclaration> = Record<string, never>> = {
|
|
132
|
+
properties: TProps;
|
|
133
|
+
label: string;
|
|
134
|
+
description?: string;
|
|
135
|
+
previewImageUrl?: string;
|
|
136
|
+
/** Events this (functional) block can emit — see {@link EventDeclaration}. */
|
|
137
|
+
events?: TEvents;
|
|
138
|
+
} & ({
|
|
139
|
+
allowChildren?: false;
|
|
140
|
+
} | {
|
|
141
|
+
allowChildren: true;
|
|
142
|
+
allowedChildBlocks?: string[];
|
|
143
|
+
});
|
|
144
|
+
type AnyBlockDefinition = BlockDefinition<Record<string, BlockProperty>, Record<string, EventDeclaration>>;
|
|
145
|
+
type RootDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>> = {
|
|
146
|
+
properties: TProps;
|
|
147
|
+
};
|
|
148
|
+
type SlugConfig = {
|
|
149
|
+
enabled: false;
|
|
150
|
+
} | {
|
|
151
|
+
enabled: true;
|
|
152
|
+
root: string;
|
|
153
|
+
allowRoot?: boolean;
|
|
154
|
+
normalize?: boolean;
|
|
155
|
+
nested?: boolean;
|
|
156
|
+
};
|
|
157
|
+
type CollectionDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>, TBlocks extends Record<string, AnyBlockDefinition> = Record<string, AnyBlockDefinition>> = {
|
|
158
|
+
slug?: SlugConfig;
|
|
159
|
+
root: RootDefinition<TProps>;
|
|
160
|
+
blocks?: TBlocks;
|
|
161
|
+
label: string;
|
|
162
|
+
description?: string;
|
|
163
|
+
/**
|
|
164
|
+
* Marks this collection as one whose roots are meant to be EMBEDDED into other
|
|
165
|
+
* roots via a `reference` property (a "reusable block" library). Purely an
|
|
166
|
+
* ergonomic hint — it informs editor pickers and which endpoints to surface; it
|
|
167
|
+
* NEVER gates safety (the delete-in-use guard protects every referenced root
|
|
168
|
+
* regardless of this flag). Any collection can still be a reference target.
|
|
169
|
+
*/
|
|
170
|
+
reusableBlock?: boolean;
|
|
171
|
+
};
|
|
172
|
+
type AnyCollectionDefinition = CollectionDefinition<Record<string, BlockProperty>, Record<string, AnyBlockDefinition>>;
|
|
173
|
+
|
|
174
|
+
/** Props passed to each block component in the renderer map. The renderer
|
|
175
|
+
* consumes RESOLVED content (getPublishedContent), so `reference` properties
|
|
176
|
+
* surface as `ResolvedReference` objects — the `resolved` inference mode. */
|
|
177
|
+
type BlockComponentProps<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>> = {
|
|
178
|
+
properties: InferBlockProperties<TProps, 'resolved'>;
|
|
179
|
+
children: ReactNode;
|
|
180
|
+
blockId: string;
|
|
181
|
+
node: BlockTreeNode;
|
|
182
|
+
};
|
|
183
|
+
/** Shorthand to derive block component props from a collection definition. */
|
|
184
|
+
type BlockProps<TCollection extends {
|
|
185
|
+
blocks: Record<string, AnyBlockDefinition>;
|
|
186
|
+
}, TBlock extends keyof TCollection['blocks'] & string> = BlockComponentProps<TCollection['blocks'][TBlock]['properties']>;
|
|
187
|
+
type BlockComponentMap<TBlocks extends Record<string, AnyBlockDefinition>> = {
|
|
188
|
+
[K in keyof TBlocks & string]: (props: BlockComponentProps<TBlocks[K]['properties']>) => ReactNode;
|
|
189
|
+
};
|
|
190
|
+
declare function isResolvedReference(value: unknown): value is ResolvedReference;
|
|
191
|
+
/**
|
|
192
|
+
* Opaque handle returned by `createBlocksMap`. Pass it to `<BlocksRenderer>`.
|
|
193
|
+
* Carries the React component map AND the per-block-type event declarations
|
|
194
|
+
* (the runtime half of the M2a typed-events seam) so the renderer can tell a
|
|
195
|
+
* functional block (one that declared `events`) from a presentational one.
|
|
196
|
+
*/
|
|
197
|
+
type BlocksMap = {
|
|
198
|
+
readonly __brand: 'BlocksMap';
|
|
199
|
+
readonly _components: Record<string, (props: any) => ReactNode>;
|
|
200
|
+
readonly _events: Record<string, Record<string, EventDeclaration>>;
|
|
201
|
+
};
|
|
202
|
+
/**
|
|
203
|
+
* Extracts the per-block-type event declarations from a collection definition —
|
|
204
|
+
* the runtime half of the M2a typed-events seam. Only functional blocks (those
|
|
205
|
+
* that declared a non-empty `events`) get an entry; presentational blocks are
|
|
206
|
+
* omitted, so `type in blocksMap._events` is the runtime "is this block
|
|
207
|
+
* functional?" test the M3 BlockTracker keys on.
|
|
208
|
+
*/
|
|
209
|
+
declare function extractBlockEvents(blocks: Record<string, AnyBlockDefinition> | undefined): Record<string, Record<string, EventDeclaration>>;
|
|
210
|
+
/**
|
|
211
|
+
* Creates a type-safe block component map for a CMS collection. Pass the
|
|
212
|
+
* collection DEFINITION (the runtime object) as the single source of truth:
|
|
213
|
+
* the component-prop types are inferred from its blocks, and its `events`
|
|
214
|
+
* declarations are carried into the map for the M3 tracker.
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* ```tsx
|
|
218
|
+
* import { createBlocksMap } from '@createcms/core/react';
|
|
219
|
+
* import { pagesCollection } from '@/cms/collections/pages/definition';
|
|
220
|
+
*
|
|
221
|
+
* export const pageBlocks = createBlocksMap(pagesCollection, {
|
|
222
|
+
* headline: ({ properties }) => <h1>{properties.text}</h1>,
|
|
223
|
+
* hero: ({ properties, children }) => (
|
|
224
|
+
* <section>
|
|
225
|
+
* <h1>{properties.headline}</h1>
|
|
226
|
+
* {children}
|
|
227
|
+
* </section>
|
|
228
|
+
* ),
|
|
229
|
+
* });
|
|
230
|
+
* ```
|
|
231
|
+
*/
|
|
232
|
+
declare function createBlocksMap<TProps extends Record<string, BlockProperty>, TBlocks extends Record<string, AnyBlockDefinition>>(collection: CollectionDefinition<TProps, TBlocks>, components: BlockComponentMap<TBlocks>): BlocksMap;
|
|
233
|
+
/**
|
|
234
|
+
* Renders a `BlockTreeNode` tree using a block component map.
|
|
235
|
+
*
|
|
236
|
+
* Delegates to the reference-aware `renderContentNode`, which is a strict
|
|
237
|
+
* superset: for the reference-free trees that `getBlockTree` produces it
|
|
238
|
+
* behaves identically, and it additionally resolves inline references should a
|
|
239
|
+
* tree ever carry them.
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* ```tsx
|
|
243
|
+
* import { BlocksRenderer } from '@createcms/core/react';
|
|
244
|
+
* import { pageBlocks } from '@/lib/blocks/pages';
|
|
245
|
+
*
|
|
246
|
+
* export default async function Page() {
|
|
247
|
+
* const tree = await cms.api.pages.getBlockTree(...);
|
|
248
|
+
* return <BlocksRenderer blocks={pageBlocks} tree={tree} />;
|
|
249
|
+
* }
|
|
250
|
+
* ```
|
|
251
|
+
*/
|
|
252
|
+
declare function BlocksRenderer({ blocks, tree, }: {
|
|
253
|
+
blocks: BlocksMap;
|
|
254
|
+
tree: BlockTreeNode;
|
|
255
|
+
}): ReactNode;
|
|
256
|
+
/**
|
|
257
|
+
* Creates a type-safe block renderer component for a CMS collection.
|
|
258
|
+
* Convenience shorthand that combines `createBlocksMap` + `BlocksRenderer`.
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* ```tsx
|
|
262
|
+
* import { createBlocksRenderer } from '@createcms/core/react';
|
|
263
|
+
* import { pagesCollection } from '@/cms/collections/pages/definition';
|
|
264
|
+
*
|
|
265
|
+
* const PageBlocks = createBlocksRenderer(pagesCollection, {
|
|
266
|
+
* headline: ({ properties }) => <h1>{properties.text}</h1>,
|
|
267
|
+
* hero: ({ properties, children }) => (
|
|
268
|
+
* <section>
|
|
269
|
+
* <h1>{properties.headline}</h1>
|
|
270
|
+
* {children}
|
|
271
|
+
* </section>
|
|
272
|
+
* ),
|
|
273
|
+
* });
|
|
274
|
+
*
|
|
275
|
+
* // In a page component:
|
|
276
|
+
* <PageBlocks tree={tree} />
|
|
277
|
+
* ```
|
|
278
|
+
*/
|
|
279
|
+
declare function createBlocksRenderer<TProps extends Record<string, BlockProperty>, TBlocks extends Record<string, AnyBlockDefinition>>(collection: CollectionDefinition<TProps, TBlocks>, components: BlockComponentMap<TBlocks>): {
|
|
280
|
+
({ tree }: {
|
|
281
|
+
tree: BlockTreeNode;
|
|
282
|
+
}): ReactNode;
|
|
283
|
+
displayName: string;
|
|
284
|
+
};
|
|
285
|
+
/**
|
|
286
|
+
* Renders a block tree with automatic reference resolution.
|
|
287
|
+
*
|
|
288
|
+
* When a block has a `reference` property that was resolved by
|
|
289
|
+
* `getPublishedContent`, the referenced block tree is rendered inline
|
|
290
|
+
* using the same block components. Data-only references (collections
|
|
291
|
+
* without blocks) are available directly in `properties`.
|
|
292
|
+
*
|
|
293
|
+
* @example
|
|
294
|
+
* ```tsx
|
|
295
|
+
* import { createContentRenderer } from '@createcms/core/react';
|
|
296
|
+
* import { pagesCollection } from '@/cms/collections/pages/definition';
|
|
297
|
+
*
|
|
298
|
+
* const RenderPage = createContentRenderer(pagesCollection, {
|
|
299
|
+
* headline: ({ properties }) => <h1>{properties.text}</h1>,
|
|
300
|
+
* paragraph: ({ properties }) => <p>{properties.text}</p>,
|
|
301
|
+
* authorCard: ({ properties }) => (
|
|
302
|
+
* <div>{properties.author.properties.name}</div>
|
|
303
|
+
* ),
|
|
304
|
+
* // No component needed for blocks that just embed a referenced tree —
|
|
305
|
+
* // the referenced content renders inline automatically.
|
|
306
|
+
* });
|
|
307
|
+
*
|
|
308
|
+
* // Usage:
|
|
309
|
+
* <RenderPage tree={tree} />
|
|
310
|
+
* ```
|
|
311
|
+
*/
|
|
312
|
+
declare function createContentRenderer<TProps extends Record<string, BlockProperty>, TBlocks extends Record<string, AnyBlockDefinition>>(collection: CollectionDefinition<TProps, TBlocks>, components: Partial<BlockComponentMap<TBlocks>>): {
|
|
313
|
+
({ tree, }: {
|
|
314
|
+
tree: BlockTreeNode;
|
|
315
|
+
}): ReactNode;
|
|
316
|
+
displayName: string;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
export { BlocksRenderer, createBlocksMap, createBlocksRenderer, createContentRenderer, extractBlockEvents, isResolvedReference };
|
|
320
|
+
export type { BlockComponentProps, BlockProps, BlocksMap };
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
|
|
2
|
+
import { BlockTracker } from './tracking.js';
|
|
3
|
+
|
|
4
|
+
function isResolvedReference(value) {
|
|
5
|
+
return typeof value === 'object' && value !== null && 'rootId' in value && 'collection' in value && 'tree' in value && 'properties' in value;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Extracts the per-block-type event declarations from a collection definition —
|
|
9
|
+
* the runtime half of the M2a typed-events seam. Only functional blocks (those
|
|
10
|
+
* that declared a non-empty `events`) get an entry; presentational blocks are
|
|
11
|
+
* omitted, so `type in blocksMap._events` is the runtime "is this block
|
|
12
|
+
* functional?" test the M3 BlockTracker keys on.
|
|
13
|
+
*/ function extractBlockEvents(blocks) {
|
|
14
|
+
const out = {};
|
|
15
|
+
if (!blocks) return out;
|
|
16
|
+
for (const [type, def] of Object.entries(blocks)){
|
|
17
|
+
if (def.events && Object.keys(def.events).length > 0) {
|
|
18
|
+
// Shallow-copy so the map OWNS its event records (the `readonly` on
|
|
19
|
+
// BlocksMap._events is a real contract): never alias the live collection
|
|
20
|
+
// definition, so a consumer can't mutate it through the map.
|
|
21
|
+
out[type] = {
|
|
22
|
+
...def.events
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Creates a type-safe block component map for a CMS collection. Pass the
|
|
30
|
+
* collection DEFINITION (the runtime object) as the single source of truth:
|
|
31
|
+
* the component-prop types are inferred from its blocks, and its `events`
|
|
32
|
+
* declarations are carried into the map for the M3 tracker.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```tsx
|
|
36
|
+
* import { createBlocksMap } from '@createcms/core/react';
|
|
37
|
+
* import { pagesCollection } from '@/cms/collections/pages/definition';
|
|
38
|
+
*
|
|
39
|
+
* export const pageBlocks = createBlocksMap(pagesCollection, {
|
|
40
|
+
* headline: ({ properties }) => <h1>{properties.text}</h1>,
|
|
41
|
+
* hero: ({ properties, children }) => (
|
|
42
|
+
* <section>
|
|
43
|
+
* <h1>{properties.headline}</h1>
|
|
44
|
+
* {children}
|
|
45
|
+
* </section>
|
|
46
|
+
* ),
|
|
47
|
+
* });
|
|
48
|
+
* ```
|
|
49
|
+
*/ function createBlocksMap(collection, components) {
|
|
50
|
+
return {
|
|
51
|
+
__brand: 'BlocksMap',
|
|
52
|
+
_components: components,
|
|
53
|
+
_events: extractBlockEvents(collection.blocks)
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// ============================================================================
|
|
57
|
+
// BlocksRenderer
|
|
58
|
+
// ============================================================================
|
|
59
|
+
/**
|
|
60
|
+
* Renders a `BlockTreeNode` tree using a block component map.
|
|
61
|
+
*
|
|
62
|
+
* Delegates to the reference-aware `renderContentNode`, which is a strict
|
|
63
|
+
* superset: for the reference-free trees that `getBlockTree` produces it
|
|
64
|
+
* behaves identically, and it additionally resolves inline references should a
|
|
65
|
+
* tree ever carry them.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```tsx
|
|
69
|
+
* import { BlocksRenderer } from '@createcms/core/react';
|
|
70
|
+
* import { pageBlocks } from '@/lib/blocks/pages';
|
|
71
|
+
*
|
|
72
|
+
* export default async function Page() {
|
|
73
|
+
* const tree = await cms.api.pages.getBlockTree(...);
|
|
74
|
+
* return <BlocksRenderer blocks={pageBlocks} tree={tree} />;
|
|
75
|
+
* }
|
|
76
|
+
* ```
|
|
77
|
+
*/ function BlocksRenderer({ blocks, tree }) {
|
|
78
|
+
return renderContentNode(tree, blocks._components, blocks._events);
|
|
79
|
+
}
|
|
80
|
+
// ============================================================================
|
|
81
|
+
// createBlocksRenderer (convenience shorthand)
|
|
82
|
+
// ============================================================================
|
|
83
|
+
/**
|
|
84
|
+
* Creates a type-safe block renderer component for a CMS collection.
|
|
85
|
+
* Convenience shorthand that combines `createBlocksMap` + `BlocksRenderer`.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```tsx
|
|
89
|
+
* import { createBlocksRenderer } from '@createcms/core/react';
|
|
90
|
+
* import { pagesCollection } from '@/cms/collections/pages/definition';
|
|
91
|
+
*
|
|
92
|
+
* const PageBlocks = createBlocksRenderer(pagesCollection, {
|
|
93
|
+
* headline: ({ properties }) => <h1>{properties.text}</h1>,
|
|
94
|
+
* hero: ({ properties, children }) => (
|
|
95
|
+
* <section>
|
|
96
|
+
* <h1>{properties.headline}</h1>
|
|
97
|
+
* {children}
|
|
98
|
+
* </section>
|
|
99
|
+
* ),
|
|
100
|
+
* });
|
|
101
|
+
*
|
|
102
|
+
* // In a page component:
|
|
103
|
+
* <PageBlocks tree={tree} />
|
|
104
|
+
* ```
|
|
105
|
+
*/ function createBlocksRenderer(collection, components) {
|
|
106
|
+
const blocksMap = createBlocksMap(collection, components);
|
|
107
|
+
function Renderer({ tree }) {
|
|
108
|
+
return /*#__PURE__*/ jsx(BlocksRenderer, {
|
|
109
|
+
blocks: blocksMap,
|
|
110
|
+
tree: tree
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
Renderer.displayName = `BlocksRenderer(${collection.label})`;
|
|
114
|
+
return Renderer;
|
|
115
|
+
}
|
|
116
|
+
// ============================================================================
|
|
117
|
+
// ContentRenderer (reference-aware tree rendering)
|
|
118
|
+
// ============================================================================
|
|
119
|
+
function renderContentNode(node, components, events, fromReference = false) {
|
|
120
|
+
const renderedChildren = node.children.map((child)=>renderContentNode(child, components, events));
|
|
121
|
+
const childrenNode = renderedChildren.length > 0 ? /*#__PURE__*/ jsx(Fragment, {
|
|
122
|
+
children: renderedChildren
|
|
123
|
+
}) : null;
|
|
124
|
+
if (node.type === 'root') {
|
|
125
|
+
return /*#__PURE__*/ jsx(Fragment, {
|
|
126
|
+
children: childrenNode
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
const Component = components[node.type];
|
|
130
|
+
if (!Component) {
|
|
131
|
+
// No component mapped for this block type.
|
|
132
|
+
// Check if any property is a resolved reference with a tree —
|
|
133
|
+
// if so, render the referenced tree inline using the same components.
|
|
134
|
+
for (const value of Object.values(node.properties)){
|
|
135
|
+
if (isResolvedReference(value) && value.tree.children.length > 0) {
|
|
136
|
+
const refChildren = value.tree.children.map((child)=>renderContentNode(child, components, events));
|
|
137
|
+
return /*#__PURE__*/ jsx(Fragment, {
|
|
138
|
+
children: refChildren
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Blocks from referenced data-only collections won't have a mapped
|
|
143
|
+
// component — that's expected, not an error.
|
|
144
|
+
if (!fromReference && process.env.NODE_ENV !== 'production') {
|
|
145
|
+
console.warn(`[cms] No component mapped for block type "${node.type}"`);
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
// If the block has reference properties with block trees, render them
|
|
150
|
+
// and pass as children (appended after the block's own children). The same
|
|
151
|
+
// `events` map flows down so a functional block embedded via a reference
|
|
152
|
+
// still binds to the host page's ambient ab-context.
|
|
153
|
+
let refRendered = [];
|
|
154
|
+
for (const value of Object.values(node.properties)){
|
|
155
|
+
if (isResolvedReference(value) && value.tree.children.length > 0) {
|
|
156
|
+
for (const child of value.tree.children){
|
|
157
|
+
refRendered.push(renderContentNode(child, components, events, true));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const allChildren = renderedChildren.length > 0 || refRendered.length > 0 ? /*#__PURE__*/ jsxs(Fragment, {
|
|
162
|
+
children: [
|
|
163
|
+
renderedChildren,
|
|
164
|
+
refRendered
|
|
165
|
+
]
|
|
166
|
+
}) : null;
|
|
167
|
+
const element = /*#__PURE__*/ jsx(Component, {
|
|
168
|
+
properties: node.properties,
|
|
169
|
+
children: allChildren,
|
|
170
|
+
blockId: node.blockId,
|
|
171
|
+
node: node
|
|
172
|
+
}, node.blockId);
|
|
173
|
+
// M3c — a FUNCTIONAL block (declared `events`, carried in BlocksMap._events)
|
|
174
|
+
// is wrapped in the 'use client' <BlockTracker> so it can fire its declared
|
|
175
|
+
// events. children-as-props: `element` is server-rendered and just passed
|
|
176
|
+
// through, so presentational subtrees stay RSC. The dispatch + ab-context come
|
|
177
|
+
// from the consumer's <TrackingRuntimeProvider>, not from here.
|
|
178
|
+
if (node.type in events) {
|
|
179
|
+
const rawTrackingId = node.properties.trackingId;
|
|
180
|
+
return /*#__PURE__*/ jsx(BlockTracker, {
|
|
181
|
+
blockType: node.type,
|
|
182
|
+
blockId: node.blockId,
|
|
183
|
+
trackingId: typeof rawTrackingId === 'string' ? rawTrackingId : undefined,
|
|
184
|
+
events: events[node.type],
|
|
185
|
+
children: element
|
|
186
|
+
}, node.blockId);
|
|
187
|
+
}
|
|
188
|
+
return element;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Renders a block tree with automatic reference resolution.
|
|
192
|
+
*
|
|
193
|
+
* When a block has a `reference` property that was resolved by
|
|
194
|
+
* `getPublishedContent`, the referenced block tree is rendered inline
|
|
195
|
+
* using the same block components. Data-only references (collections
|
|
196
|
+
* without blocks) are available directly in `properties`.
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* ```tsx
|
|
200
|
+
* import { createContentRenderer } from '@createcms/core/react';
|
|
201
|
+
* import { pagesCollection } from '@/cms/collections/pages/definition';
|
|
202
|
+
*
|
|
203
|
+
* const RenderPage = createContentRenderer(pagesCollection, {
|
|
204
|
+
* headline: ({ properties }) => <h1>{properties.text}</h1>,
|
|
205
|
+
* paragraph: ({ properties }) => <p>{properties.text}</p>,
|
|
206
|
+
* authorCard: ({ properties }) => (
|
|
207
|
+
* <div>{properties.author.properties.name}</div>
|
|
208
|
+
* ),
|
|
209
|
+
* // No component needed for blocks that just embed a referenced tree —
|
|
210
|
+
* // the referenced content renders inline automatically.
|
|
211
|
+
* });
|
|
212
|
+
*
|
|
213
|
+
* // Usage:
|
|
214
|
+
* <RenderPage tree={tree} />
|
|
215
|
+
* ```
|
|
216
|
+
*/ function createContentRenderer(collection, components) {
|
|
217
|
+
const componentMap = components;
|
|
218
|
+
const events = extractBlockEvents(collection.blocks);
|
|
219
|
+
function ContentRendererComponent({ tree }) {
|
|
220
|
+
return renderContentNode(tree, componentMap, events);
|
|
221
|
+
}
|
|
222
|
+
ContentRendererComponent.displayName = `ContentRenderer(${collection.label})`;
|
|
223
|
+
return ContentRendererComponent;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export { BlocksRenderer, createBlocksMap, createBlocksRenderer, createContentRenderer, extractBlockEvents, isResolvedReference };
|