@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,243 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
3
|
+
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
var React = require('react');
|
|
6
|
+
|
|
7
|
+
function _interopNamespace(e) {
|
|
8
|
+
if (e && e.__esModule) return e;
|
|
9
|
+
var n = Object.create(null);
|
|
10
|
+
if (e) {
|
|
11
|
+
Object.keys(e).forEach(function (k) {
|
|
12
|
+
if (k !== 'default') {
|
|
13
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
14
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
15
|
+
enumerable: true,
|
|
16
|
+
get: function () { return e[k]; }
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
n.default = e;
|
|
22
|
+
return n;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
var React__namespace = /*#__PURE__*/_interopNamespace(React);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Resolves the GA4/dataLayer wire name for a block event key: the author's
|
|
29
|
+
* `EventDeclaration.name` override, else the default `cms_<blockType>_<key>`
|
|
30
|
+
* (locked measurement decision #7). Pure + framework-free so BOTH the client
|
|
31
|
+
* tracker (react/tracking.tsx, where a fire happens) and the server goal-picker
|
|
32
|
+
* (ab-test listGoalEvents, which advertises the goal) resolve names identically
|
|
33
|
+
* — the stored event_type and the offered goal must be the same string.
|
|
34
|
+
*/ function resolveWireName(key, blockType, events) {
|
|
35
|
+
return events?.[key]?.name ?? `cms_${blockType}_${key}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const TrackingRuntimeContext = /*#__PURE__*/ React.createContext(null);
|
|
39
|
+
/**
|
|
40
|
+
* Mount once, high in the CLIENT tree, wrapping the rendered page. Supplies the
|
|
41
|
+
* dispatch + ambient ab-context every {@link BlockTracker} below it reads.
|
|
42
|
+
*/ function TrackingRuntimeProvider({ runtime, children }) {
|
|
43
|
+
return /*#__PURE__*/ jsxRuntime.jsx(TrackingRuntimeContext.Provider, {
|
|
44
|
+
value: runtime,
|
|
45
|
+
children: children
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const BlockTrackingContext = /*#__PURE__*/ React.createContext(null);
|
|
49
|
+
/**
|
|
50
|
+
* Scopes the per-block tracking identity for its children. Rendered BY
|
|
51
|
+
* renderContentNode around a functional block (children-as-props — the wrapped
|
|
52
|
+
* subtree is server-rendered and just passed through). Itself renders nothing but
|
|
53
|
+
* the context provider, so it is safe during SSR and never blocks paint.
|
|
54
|
+
*/ function BlockTracker({ blockType, blockId, trackingId, events, children }) {
|
|
55
|
+
const value = React.useMemo(()=>({
|
|
56
|
+
blockType,
|
|
57
|
+
blockId,
|
|
58
|
+
trackingId,
|
|
59
|
+
events
|
|
60
|
+
}), [
|
|
61
|
+
blockType,
|
|
62
|
+
blockId,
|
|
63
|
+
trackingId,
|
|
64
|
+
events
|
|
65
|
+
]);
|
|
66
|
+
return /*#__PURE__*/ jsxRuntime.jsx(BlockTrackingContext.Provider, {
|
|
67
|
+
value: value,
|
|
68
|
+
children: children
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Builds the {@link ClientCMSEvent} a fired block event dispatches. Pure +
|
|
73
|
+
* exported so it is unit-testable without a React renderer: stamps the ambient
|
|
74
|
+
* ab-context, maps the block identity to `source` (trackingId → handle, type →
|
|
75
|
+
* block type), and marks it anonymous (the consent-free aggregate path; the
|
|
76
|
+
* consent-gated legs live inside dispatch).
|
|
77
|
+
*/ function buildBlockEvent(key, params, runtime, block, interactionId) {
|
|
78
|
+
return {
|
|
79
|
+
// The typed API fires the event KEY; the wire (GA4/dataLayer + the stored
|
|
80
|
+
// event_type) carries the resolved wire name. Outside a BlockTracker
|
|
81
|
+
// (block === null) there is nothing to resolve against, so the key passes.
|
|
82
|
+
name: block ? resolveWireName(key, block.blockType, block.events) : key,
|
|
83
|
+
anonymous: true,
|
|
84
|
+
...runtime.ab ? {
|
|
85
|
+
ab: runtime.ab
|
|
86
|
+
} : {},
|
|
87
|
+
...block ? {
|
|
88
|
+
source: {
|
|
89
|
+
handle: block.trackingId,
|
|
90
|
+
type: block.blockType
|
|
91
|
+
}
|
|
92
|
+
} : {},
|
|
93
|
+
...interactionId ? {
|
|
94
|
+
interactionId
|
|
95
|
+
} : {},
|
|
96
|
+
...params ? {
|
|
97
|
+
params
|
|
98
|
+
} : {}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The UNTYPED core hook. Reads the runtime + the per-block identity from context
|
|
103
|
+
* and returns a `fire` that builds a {@link ClientCMSEvent} and dispatches it
|
|
104
|
+
* (anonymous aggregate by design — the consent-gated legs are inside dispatch).
|
|
105
|
+
* If no {@link TrackingRuntimeProvider} is mounted, `fire` is a dev-warned no-op
|
|
106
|
+
* (degrade-safe). Prefer the typed {@link createTrackedBlocks} facade.
|
|
107
|
+
*/ function useBlockTrackerRaw(expectedBlockType) {
|
|
108
|
+
const runtime = React.useContext(TrackingRuntimeContext);
|
|
109
|
+
const block = React.useContext(BlockTrackingContext);
|
|
110
|
+
const emit = React.useCallback((name, params, interactionId)=>{
|
|
111
|
+
if (!runtime) {
|
|
112
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
113
|
+
console.warn(`[cms] useBlockTracker fire("${name}") called with no <TrackingRuntimeProvider> mounted — event dropped.`);
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
118
|
+
// Fired outside any <BlockTracker> (e.g. a <TrackedForm> not inside a
|
|
119
|
+
// functional block): the event name is NOT wire-resolved and carries no
|
|
120
|
+
// source, so it won't match a picked goal. Loud, not silent.
|
|
121
|
+
if (!block) {
|
|
122
|
+
console.warn(`[cms] fire("${name}") called outside a <BlockTracker> — the event name is unresolved (no wire name) and has no source; render the firing component inside a functional block.`);
|
|
123
|
+
}
|
|
124
|
+
// The block key is a TYPE-only selector; the dispatched source is the
|
|
125
|
+
// ENCLOSING <BlockTracker>. Warn when they disagree (wrong-key call, or
|
|
126
|
+
// a call from outside the block's own subtree) so a mis-stamped event
|
|
127
|
+
// is loud, not silent.
|
|
128
|
+
if (block && expectedBlockType && block.blockType !== expectedBlockType) {
|
|
129
|
+
console.warn(`[cms] useTrackedBlock("${expectedBlockType}").fire is running inside a "${block.blockType}" block — the event is stamped with "${block.blockType}". Call it from the block's own component.`);
|
|
130
|
+
}
|
|
131
|
+
if (block?.events && !(name in block.events)) {
|
|
132
|
+
console.warn(`[cms] fire("${name}") is not a declared event of block "${block.blockType}".`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
runtime.dispatch(buildBlockEvent(name, params, runtime, block, interactionId));
|
|
136
|
+
}, [
|
|
137
|
+
runtime,
|
|
138
|
+
block,
|
|
139
|
+
expectedBlockType
|
|
140
|
+
]);
|
|
141
|
+
const fire = React.useCallback((name, params)=>emit(name, params, undefined), [
|
|
142
|
+
emit
|
|
143
|
+
]);
|
|
144
|
+
const fireInteraction = React.useCallback((name, interactionId, params)=>emit(name, params, interactionId), [
|
|
145
|
+
emit
|
|
146
|
+
]);
|
|
147
|
+
return {
|
|
148
|
+
fire,
|
|
149
|
+
fireInteraction
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Builds the per-collection typed tracking facade. Pass the collection
|
|
154
|
+
* DEFINITION (single source of truth, same object as `createBlocksMap`).
|
|
155
|
+
* `useTrackedBlock('signupForm').fire` is narrowed to that block's declared
|
|
156
|
+
* events — `fire('typo')`, a missing required param, or a wrong-typed param are
|
|
157
|
+
* all compile errors; a non-functional block key is rejected too.
|
|
158
|
+
*
|
|
159
|
+
* The block key is a TYPE-level selector: the dispatched source is always the
|
|
160
|
+
* ENCLOSING <BlockTracker> (set by the renderer from the rendered node), so call
|
|
161
|
+
* `useTrackedBlock('x')` from block x's own component. A mismatch dev-warns.
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* ```tsx
|
|
165
|
+
* // blocks/index.tsx
|
|
166
|
+
* export const trackedBlocks = createTrackedBlocks(pagesCollection);
|
|
167
|
+
*
|
|
168
|
+
* // signup-form.tsx ('use client')
|
|
169
|
+
* const { fire } = trackedBlocks.useTrackedBlock('signupForm');
|
|
170
|
+
* <form action={() => fire('submitSuccess', { plan: 'pro' })} />
|
|
171
|
+
* ```
|
|
172
|
+
*/ function createTrackedBlocks(_collection) {
|
|
173
|
+
function useTrackedBlock(block) {
|
|
174
|
+
// The block-key literal selects the event union at the TYPE level; at
|
|
175
|
+
// runtime the BlockTracker has already scoped identity, so the raw hook's
|
|
176
|
+
// fire forwards the (compile-checked) name + params unchanged. The cast is
|
|
177
|
+
// the single controlled erasure point (mirrors BlocksMap._components: any).
|
|
178
|
+
// `block` is passed for the dev-mismatch warning, not for dispatch.
|
|
179
|
+
return useBlockTrackerRaw(block);
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
useTrackedBlock
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Wraps a `<form>` so one submit becomes a funnel: it mints an `interactionId`,
|
|
187
|
+
* fires the `attempt` event when the submit STARTS, runs `action`, and fires the
|
|
188
|
+
* `success` event only if `action` resolves — both legs share the interactionId
|
|
189
|
+
* so `completion_rate` (= successes / attempts) can pair them. A throw from
|
|
190
|
+
* `action` leaves the attempt unmatched (a started-but-not-completed interaction).
|
|
191
|
+
*
|
|
192
|
+
* Must be rendered inside a functional block (the renderer's <BlockTracker>), so
|
|
193
|
+
* the events stamp that block's source. Uses React 19 `useActionState` — render
|
|
194
|
+
* it only on React 19 (the rest of this module works on 18). `attempt`/`success`
|
|
195
|
+
* are the block's declared event keys (a typo dev-warns via the raw tracker).
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* ```tsx
|
|
199
|
+
* <TrackedForm attempt="submitAttempt" success="submitSuccess" action={subscribe}>
|
|
200
|
+
* <input name="email" /> <button>Join</button>
|
|
201
|
+
* </TrackedForm>
|
|
202
|
+
* ```
|
|
203
|
+
*/ function TrackedForm({ attempt, success, action, children, ...formProps }) {
|
|
204
|
+
const { fireInteraction } = useBlockTrackerRaw();
|
|
205
|
+
const run = React.useCallback(async (_prev, formData)=>{
|
|
206
|
+
const interactionId = typeof crypto !== 'undefined' && 'randomUUID' in crypto ? crypto.randomUUID() : `ix_${Date.now()}_${Math.round(Math.random() * 1e9)}`;
|
|
207
|
+
fireInteraction(attempt, interactionId);
|
|
208
|
+
try {
|
|
209
|
+
await action(formData);
|
|
210
|
+
fireInteraction(success, interactionId);
|
|
211
|
+
return {
|
|
212
|
+
ok: true
|
|
213
|
+
};
|
|
214
|
+
} catch {
|
|
215
|
+
return {
|
|
216
|
+
ok: false
|
|
217
|
+
}; // attempt stays unmatched (failed interaction)
|
|
218
|
+
}
|
|
219
|
+
}, [
|
|
220
|
+
attempt,
|
|
221
|
+
success,
|
|
222
|
+
action,
|
|
223
|
+
fireInteraction
|
|
224
|
+
]);
|
|
225
|
+
// React 19. Namespaced so importing this module never breaks on React 18 —
|
|
226
|
+
// only rendering <TrackedForm> requires 19.
|
|
227
|
+
const [, formAction] = React__namespace.useActionState(run, {
|
|
228
|
+
ok: false
|
|
229
|
+
});
|
|
230
|
+
return /*#__PURE__*/ jsxRuntime.jsx("form", {
|
|
231
|
+
action: formAction,
|
|
232
|
+
...formProps,
|
|
233
|
+
children: children
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
exports.BlockTracker = BlockTracker;
|
|
238
|
+
exports.TrackedForm = TrackedForm;
|
|
239
|
+
exports.TrackingRuntimeProvider = TrackingRuntimeProvider;
|
|
240
|
+
exports.buildBlockEvent = buildBlockEvent;
|
|
241
|
+
exports.createTrackedBlocks = createTrackedBlocks;
|
|
242
|
+
exports.resolveWireName = resolveWireName;
|
|
243
|
+
exports.useBlockTrackerRaw = useBlockTrackerRaw;
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ReactNode, ComponentProps } from 'react';
|
|
3
|
+
import { ConsentState } from '../plugins/consent/index.cjs';
|
|
4
|
+
|
|
5
|
+
type BlockTreeNode = {
|
|
6
|
+
blockId: string;
|
|
7
|
+
type: string;
|
|
8
|
+
properties: Record<string, unknown>;
|
|
9
|
+
children: BlockTreeNode[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* One published branch of a root, as a snapshot. Shared by the page-level
|
|
14
|
+
* variant shape and the block-level A/B `variants` (below) so the two can't
|
|
15
|
+
* drift. `properties` mirrors the (depth-1 typed) reference `properties`.
|
|
16
|
+
*/
|
|
17
|
+
type PublishedBranchSnapshot<TProps = Record<string, unknown>> = {
|
|
18
|
+
branchId: string;
|
|
19
|
+
isControl: boolean;
|
|
20
|
+
properties: TProps;
|
|
21
|
+
tree: BlockTreeNode;
|
|
22
|
+
};
|
|
23
|
+
type ResolvedReference<TProps = Record<string, unknown>> = {
|
|
24
|
+
rootId: string;
|
|
25
|
+
collection: string;
|
|
26
|
+
properties: TProps;
|
|
27
|
+
tree: BlockTreeNode;
|
|
28
|
+
/**
|
|
29
|
+
* Present only when this referenced root has a RUNNING A/B test (AB_FANOUT
|
|
30
|
+
* F2 server fan-out). The server stays a pure, cacheable function: top-level
|
|
31
|
+
* `tree`/`properties` are the CONTROL branch (a no-JS / AB-disabled client
|
|
32
|
+
* renders it as-is), and the client pre-render pass (F3) picks the visitor's
|
|
33
|
+
* variant from `variants` and swaps it in. `variants` includes the control.
|
|
34
|
+
* An OPTIONAL field (not a discriminated union) so `isResolvedReference` —
|
|
35
|
+
* which narrows on `tree`/`properties` — keeps matching.
|
|
36
|
+
*/
|
|
37
|
+
abTest?: {
|
|
38
|
+
testId: string;
|
|
39
|
+
trafficPercentage: number;
|
|
40
|
+
variants: PublishedBranchSnapshot<TProps>[];
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
type BlockTypes = {
|
|
44
|
+
string: string;
|
|
45
|
+
number: number;
|
|
46
|
+
boolean: boolean;
|
|
47
|
+
date: string;
|
|
48
|
+
richText: string;
|
|
49
|
+
image: string;
|
|
50
|
+
select: string;
|
|
51
|
+
reference: string;
|
|
52
|
+
};
|
|
53
|
+
/** Reference inference mode: `raw` (write input + getBlockTree editor read) keeps
|
|
54
|
+
* a `reference` as its stored rootId string; `resolved` (getPublishedContent)
|
|
55
|
+
* surfaces the inlined `ResolvedReference`. */
|
|
56
|
+
type RefMode = 'raw' | 'resolved';
|
|
57
|
+
type BlockPropertyType = keyof BlockTypes;
|
|
58
|
+
type SelectOption = {
|
|
59
|
+
readonly label: string;
|
|
60
|
+
readonly value: string;
|
|
61
|
+
};
|
|
62
|
+
type BlockPropertySpec<T extends BlockPropertyType> = {
|
|
63
|
+
type: T;
|
|
64
|
+
required?: boolean;
|
|
65
|
+
defaultValue?: BlockTypes[T];
|
|
66
|
+
label: string;
|
|
67
|
+
description?: string;
|
|
68
|
+
placeholder?: string;
|
|
69
|
+
} & (T extends 'select' ? {
|
|
70
|
+
options: readonly SelectOption[];
|
|
71
|
+
} : {}) & (T extends 'reference' ? {
|
|
72
|
+
collection: string;
|
|
73
|
+
} : {});
|
|
74
|
+
/** Discriminated union over all concrete block-property specs. */
|
|
75
|
+
type BlockProperty = {
|
|
76
|
+
[K in BlockPropertyType]: BlockPropertySpec<K>;
|
|
77
|
+
}[BlockPropertyType];
|
|
78
|
+
type Simplify<T> = {
|
|
79
|
+
[K in keyof T]: T[K];
|
|
80
|
+
};
|
|
81
|
+
/** Extracts the runtime value type for a block property.
|
|
82
|
+
* For `select` properties with options, returns the union of option values.
|
|
83
|
+
* A `reference` is a rootId string in `raw` mode (write input + editor read) and
|
|
84
|
+
* a `ResolvedReference` in `resolved` mode (published read).
|
|
85
|
+
* For all other types, returns the corresponding primitive type. */
|
|
86
|
+
type InferPropertyValue<T extends BlockProperty, M extends RefMode = 'raw', TCol extends Record<string, AnyCollectionDefinition> = {}> = T extends {
|
|
87
|
+
type: 'select';
|
|
88
|
+
options: readonly {
|
|
89
|
+
readonly value: infer V extends string;
|
|
90
|
+
}[];
|
|
91
|
+
} ? V : T extends {
|
|
92
|
+
type: 'reference';
|
|
93
|
+
collection: infer C extends string;
|
|
94
|
+
} ? M extends 'resolved' ? C extends keyof TCol ? ResolvedReference<NonNullable<InferBlockProperties<TCol[C]['root']['properties'], 'resolved'>>> : ResolvedReference : string : BlockTypes[T['type']];
|
|
95
|
+
type RequiredPart<T extends Record<string, BlockProperty>, M extends RefMode, TCol extends Record<string, AnyCollectionDefinition>> = {
|
|
96
|
+
[K in keyof T as T[K] extends {
|
|
97
|
+
required: true;
|
|
98
|
+
} ? K : never]: InferPropertyValue<T[K], M, TCol>;
|
|
99
|
+
};
|
|
100
|
+
type OptionalPart<T extends Record<string, BlockProperty>, M extends RefMode, TCol extends Record<string, AnyCollectionDefinition>> = {
|
|
101
|
+
[K in keyof T as T[K] extends {
|
|
102
|
+
required: true;
|
|
103
|
+
} ? never : K]?: InferPropertyValue<T[K], M, TCol>;
|
|
104
|
+
};
|
|
105
|
+
type HasRequiredKeys<T extends Record<string, BlockProperty>> = true extends {
|
|
106
|
+
[K in keyof T]: T[K] extends {
|
|
107
|
+
required: true;
|
|
108
|
+
} ? true : never;
|
|
109
|
+
}[keyof T] ? true : false;
|
|
110
|
+
/** Maps a properties record to its runtime value types, respecting `required`.
|
|
111
|
+
* When all properties are optional, the entire input becomes optional (| undefined). */
|
|
112
|
+
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;
|
|
113
|
+
/** Scalar property subset usable as an event parameter (no references/media). */
|
|
114
|
+
type ScalarBlockProperty = Extract<BlockProperty, {
|
|
115
|
+
type: 'string' | 'number' | 'boolean' | 'select' | 'date';
|
|
116
|
+
}>;
|
|
117
|
+
/**
|
|
118
|
+
* Declares a meaningful event a functional block can emit (e.g. a form's
|
|
119
|
+
* `submitSuccess`). Living on the block DEFINITION makes it the single source of
|
|
120
|
+
* truth for the typed `fire(...)` union, the test-creation goal picker, and the
|
|
121
|
+
* analytics wire name. `name` overrides the GA4/dataLayer wire name (defaults to
|
|
122
|
+
* `cms_<blockType>_<eventKey>`, computed by the measurement layer). Whether an
|
|
123
|
+
* event counts as a conversion is decided per test in the UI, not here.
|
|
124
|
+
*/
|
|
125
|
+
type EventDeclaration = {
|
|
126
|
+
/** Analytics wire-name override (snake_case). Defaults to cms_<type>_<key>. */
|
|
127
|
+
name?: string;
|
|
128
|
+
/** Typed parameters carried with the event (scalar only). */
|
|
129
|
+
params?: Record<string, ScalarBlockProperty>;
|
|
130
|
+
/** Human label for the goal picker. */
|
|
131
|
+
label?: string;
|
|
132
|
+
};
|
|
133
|
+
/** Call-args tuple for `fire`: required iff the event declares a required param. */
|
|
134
|
+
type EventFireArgs<E extends EventDeclaration> = E extends {
|
|
135
|
+
params: infer P extends Record<string, BlockProperty>;
|
|
136
|
+
} ? HasRequiredKeys<P> extends true ? [params: InferBlockProperties<P>] : [params?: InferBlockProperties<P>] : [];
|
|
137
|
+
/** Event keys a block declares. */
|
|
138
|
+
type BlockEventNames<TEvents extends Record<string, EventDeclaration>> = keyof TEvents & string;
|
|
139
|
+
/**
|
|
140
|
+
* The typed `fire` signature derived from a block's event declarations — the
|
|
141
|
+
* runtime tracker (M3) implements this. `fire('unknown')`, a missing required
|
|
142
|
+
* param, and a wrong-typed param are all compile errors.
|
|
143
|
+
*/
|
|
144
|
+
type BlockEventFire<TEvents extends Record<string, EventDeclaration>> = <K extends BlockEventNames<TEvents>>(name: K, ...args: EventFireArgs<TEvents[K]>) => void;
|
|
145
|
+
type BlockDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>, TEvents extends Record<string, EventDeclaration> = Record<string, never>> = {
|
|
146
|
+
properties: TProps;
|
|
147
|
+
label: string;
|
|
148
|
+
description?: string;
|
|
149
|
+
previewImageUrl?: string;
|
|
150
|
+
/** Events this (functional) block can emit — see {@link EventDeclaration}. */
|
|
151
|
+
events?: TEvents;
|
|
152
|
+
} & ({
|
|
153
|
+
allowChildren?: false;
|
|
154
|
+
} | {
|
|
155
|
+
allowChildren: true;
|
|
156
|
+
allowedChildBlocks?: string[];
|
|
157
|
+
});
|
|
158
|
+
type AnyBlockDefinition = BlockDefinition<Record<string, BlockProperty>, Record<string, EventDeclaration>>;
|
|
159
|
+
type RootDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>> = {
|
|
160
|
+
properties: TProps;
|
|
161
|
+
};
|
|
162
|
+
type SlugConfig = {
|
|
163
|
+
enabled: false;
|
|
164
|
+
} | {
|
|
165
|
+
enabled: true;
|
|
166
|
+
root: string;
|
|
167
|
+
allowRoot?: boolean;
|
|
168
|
+
normalize?: boolean;
|
|
169
|
+
nested?: boolean;
|
|
170
|
+
};
|
|
171
|
+
type CollectionDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>, TBlocks extends Record<string, AnyBlockDefinition> = Record<string, AnyBlockDefinition>> = {
|
|
172
|
+
slug?: SlugConfig;
|
|
173
|
+
root: RootDefinition<TProps>;
|
|
174
|
+
blocks?: TBlocks;
|
|
175
|
+
label: string;
|
|
176
|
+
description?: string;
|
|
177
|
+
/**
|
|
178
|
+
* Marks this collection as one whose roots are meant to be EMBEDDED into other
|
|
179
|
+
* roots via a `reference` property (a "reusable block" library). Purely an
|
|
180
|
+
* ergonomic hint — it informs editor pickers and which endpoints to surface; it
|
|
181
|
+
* NEVER gates safety (the delete-in-use guard protects every referenced root
|
|
182
|
+
* regardless of this flag). Any collection can still be a reference target.
|
|
183
|
+
*/
|
|
184
|
+
reusableBlock?: boolean;
|
|
185
|
+
};
|
|
186
|
+
type AnyCollectionDefinition = CollectionDefinition<Record<string, BlockProperty>, Record<string, AnyBlockDefinition>>;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The client-side event a sink receives. Decoupled from the server `CMSEvent`
|
|
190
|
+
* (no `timestamp`/storage `id` — those are minted server-side): this is what the
|
|
191
|
+
* browser knows at fire time.
|
|
192
|
+
*/
|
|
193
|
+
type ClientCMSEvent = {
|
|
194
|
+
/** Canonical event name, e.g. `'impression' | 'conversion' | 'form_submit'`. */
|
|
195
|
+
name: string;
|
|
196
|
+
/**
|
|
197
|
+
* A/B attribution by the SERVER-served branch (Pattern A: the variant came
|
|
198
|
+
* from the URL). `branchId` is what the client has; the store leg resolves it
|
|
199
|
+
* to a `variantId` server-side. Absent for non-A/B analytics events.
|
|
200
|
+
*/
|
|
201
|
+
ab?: {
|
|
202
|
+
testId: string;
|
|
203
|
+
branchId?: string;
|
|
204
|
+
variantId?: string;
|
|
205
|
+
};
|
|
206
|
+
/** Identity — only on the consent-gated unique-visitor path; never anonymous. */
|
|
207
|
+
visitorId?: string;
|
|
208
|
+
/** True when no identifier is attached (the consent-free aggregate path). */
|
|
209
|
+
anonymous: boolean;
|
|
210
|
+
/** Originating functional block instance (the `trackingId` + block type). */
|
|
211
|
+
source?: {
|
|
212
|
+
handle?: string;
|
|
213
|
+
type?: string;
|
|
214
|
+
};
|
|
215
|
+
/** Funnel grouping id (M4): shared by the attempt + success legs of one interaction. */
|
|
216
|
+
interactionId?: string;
|
|
217
|
+
/** GA4 stitching ids (M5): set only when consent is granted; the store leg
|
|
218
|
+
* forwards them so the server-MP can attribute the hit. */
|
|
219
|
+
transport?: {
|
|
220
|
+
clientId?: string;
|
|
221
|
+
sessionId?: string;
|
|
222
|
+
engagementTimeMsec?: number;
|
|
223
|
+
};
|
|
224
|
+
/**
|
|
225
|
+
* Consent Mode v2 state at fire time (M5). Stamped ALONGSIDE `transport` (only
|
|
226
|
+
* when analytics_storage is granted), so the store leg can relay it to the
|
|
227
|
+
* server, where `buildGa4Payload` needs it to authorize the server-MP forward.
|
|
228
|
+
* Absent on the consent-free anonymous path — its absence is exactly what keeps
|
|
229
|
+
* the server's denied-consent guard from dropping the anonymous aggregate count.
|
|
230
|
+
*/
|
|
231
|
+
consent?: ConsentState;
|
|
232
|
+
/** Scalar event params (GA4 wire params). */
|
|
233
|
+
params?: Record<string, string | number | boolean>;
|
|
234
|
+
metadata?: Record<string, unknown>;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Resolves the GA4/dataLayer wire name for a block event key: the author's
|
|
239
|
+
* `EventDeclaration.name` override, else the default `cms_<blockType>_<key>`
|
|
240
|
+
* (locked measurement decision #7). Pure + framework-free so BOTH the client
|
|
241
|
+
* tracker (react/tracking.tsx, where a fire happens) and the server goal-picker
|
|
242
|
+
* (ab-test listGoalEvents, which advertises the goal) resolve names identically
|
|
243
|
+
* — the stored event_type and the offered goal must be the same string.
|
|
244
|
+
*/
|
|
245
|
+
declare function resolveWireName(key: string, blockType: string, events: Record<string, EventDeclaration> | undefined): string;
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* The tracking runtime the consumer supplies via {@link TrackingRuntimeProvider}.
|
|
249
|
+
* `dispatch` is the M3a sink fan-out (e.g. `cmsClient.abTest.dispatchEvent`); the
|
|
250
|
+
* package reaches it through context, never by importing the client instance.
|
|
251
|
+
* `ab` is the ambient, server-served A/B attribution for THIS page — single-valued
|
|
252
|
+
* per the XOR rule, so every block event on the page binds to the one running test.
|
|
253
|
+
*/
|
|
254
|
+
type TrackingRuntime = {
|
|
255
|
+
dispatch: (event: ClientCMSEvent) => void;
|
|
256
|
+
ab?: {
|
|
257
|
+
testId: string;
|
|
258
|
+
branchId: string;
|
|
259
|
+
};
|
|
260
|
+
};
|
|
261
|
+
/**
|
|
262
|
+
* Mount once, high in the CLIENT tree, wrapping the rendered page. Supplies the
|
|
263
|
+
* dispatch + ambient ab-context every {@link BlockTracker} below it reads.
|
|
264
|
+
*/
|
|
265
|
+
declare function TrackingRuntimeProvider({ runtime, children, }: {
|
|
266
|
+
runtime: TrackingRuntime;
|
|
267
|
+
children: ReactNode;
|
|
268
|
+
}): React.JSX.Element;
|
|
269
|
+
/** Per-block identity injected by the renderer from the RSC node (serializable). */
|
|
270
|
+
type BlockTrackingCtx = {
|
|
271
|
+
blockType: string;
|
|
272
|
+
blockId: string;
|
|
273
|
+
/** The block's authored, branch-stable goal anchor (`trackingId` property). */
|
|
274
|
+
trackingId?: string;
|
|
275
|
+
/** The block's declared events — used to resolve wire names + dev-validate. */
|
|
276
|
+
events?: Record<string, EventDeclaration>;
|
|
277
|
+
};
|
|
278
|
+
/**
|
|
279
|
+
* Scopes the per-block tracking identity for its children. Rendered BY
|
|
280
|
+
* renderContentNode around a functional block (children-as-props — the wrapped
|
|
281
|
+
* subtree is server-rendered and just passed through). Itself renders nothing but
|
|
282
|
+
* the context provider, so it is safe during SSR and never blocks paint.
|
|
283
|
+
*/
|
|
284
|
+
declare function BlockTracker({ blockType, blockId, trackingId, events, children, }: BlockTrackingCtx & {
|
|
285
|
+
children: ReactNode;
|
|
286
|
+
}): React.JSX.Element;
|
|
287
|
+
/**
|
|
288
|
+
* Builds the {@link ClientCMSEvent} a fired block event dispatches. Pure +
|
|
289
|
+
* exported so it is unit-testable without a React renderer: stamps the ambient
|
|
290
|
+
* ab-context, maps the block identity to `source` (trackingId → handle, type →
|
|
291
|
+
* block type), and marks it anonymous (the consent-free aggregate path; the
|
|
292
|
+
* consent-gated legs live inside dispatch).
|
|
293
|
+
*/
|
|
294
|
+
declare function buildBlockEvent(key: string, params: Record<string, string | number | boolean> | undefined, runtime: TrackingRuntime, block: BlockTrackingCtx | null, interactionId?: string): ClientCMSEvent;
|
|
295
|
+
/**
|
|
296
|
+
* The UNTYPED core hook. Reads the runtime + the per-block identity from context
|
|
297
|
+
* and returns a `fire` that builds a {@link ClientCMSEvent} and dispatches it
|
|
298
|
+
* (anonymous aggregate by design — the consent-gated legs are inside dispatch).
|
|
299
|
+
* If no {@link TrackingRuntimeProvider} is mounted, `fire` is a dev-warned no-op
|
|
300
|
+
* (degrade-safe). Prefer the typed {@link createTrackedBlocks} facade.
|
|
301
|
+
*/
|
|
302
|
+
declare function useBlockTrackerRaw(expectedBlockType?: string): {
|
|
303
|
+
fire: (name: string, params?: Record<string, string | number | boolean>) => void;
|
|
304
|
+
/** Like {@link fire} but stamps a funnel `interactionId` (M4 — <TrackedForm>). */
|
|
305
|
+
fireInteraction: (name: string, interactionId: string, params?: Record<string, string | number | boolean>) => void;
|
|
306
|
+
};
|
|
307
|
+
/** The functional blocks of a collection (those that declared a non-empty `events`). */
|
|
308
|
+
type FunctionalBlocks<TBlocks extends Record<string, AnyBlockDefinition>> = {
|
|
309
|
+
[K in keyof TBlocks as TBlocks[K]['events'] extends Record<string, EventDeclaration> ? [keyof TBlocks[K]['events']] extends [never] ? never : K : never]: NonNullable<TBlocks[K]['events']>;
|
|
310
|
+
};
|
|
311
|
+
/**
|
|
312
|
+
* Builds the per-collection typed tracking facade. Pass the collection
|
|
313
|
+
* DEFINITION (single source of truth, same object as `createBlocksMap`).
|
|
314
|
+
* `useTrackedBlock('signupForm').fire` is narrowed to that block's declared
|
|
315
|
+
* events — `fire('typo')`, a missing required param, or a wrong-typed param are
|
|
316
|
+
* all compile errors; a non-functional block key is rejected too.
|
|
317
|
+
*
|
|
318
|
+
* The block key is a TYPE-level selector: the dispatched source is always the
|
|
319
|
+
* ENCLOSING <BlockTracker> (set by the renderer from the rendered node), so call
|
|
320
|
+
* `useTrackedBlock('x')` from block x's own component. A mismatch dev-warns.
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* ```tsx
|
|
324
|
+
* // blocks/index.tsx
|
|
325
|
+
* export const trackedBlocks = createTrackedBlocks(pagesCollection);
|
|
326
|
+
*
|
|
327
|
+
* // signup-form.tsx ('use client')
|
|
328
|
+
* const { fire } = trackedBlocks.useTrackedBlock('signupForm');
|
|
329
|
+
* <form action={() => fire('submitSuccess', { plan: 'pro' })} />
|
|
330
|
+
* ```
|
|
331
|
+
*/
|
|
332
|
+
declare function createTrackedBlocks<TProps extends Record<string, BlockProperty>, TBlocks extends Record<string, AnyBlockDefinition>>(_collection: CollectionDefinition<TProps, TBlocks>): {
|
|
333
|
+
useTrackedBlock: <K extends keyof FunctionalBlocks<TBlocks> & string>(block: K) => {
|
|
334
|
+
fire: BlockEventFire<FunctionalBlocks<TBlocks>[K]>;
|
|
335
|
+
};
|
|
336
|
+
};
|
|
337
|
+
/**
|
|
338
|
+
* Wraps a `<form>` so one submit becomes a funnel: it mints an `interactionId`,
|
|
339
|
+
* fires the `attempt` event when the submit STARTS, runs `action`, and fires the
|
|
340
|
+
* `success` event only if `action` resolves — both legs share the interactionId
|
|
341
|
+
* so `completion_rate` (= successes / attempts) can pair them. A throw from
|
|
342
|
+
* `action` leaves the attempt unmatched (a started-but-not-completed interaction).
|
|
343
|
+
*
|
|
344
|
+
* Must be rendered inside a functional block (the renderer's <BlockTracker>), so
|
|
345
|
+
* the events stamp that block's source. Uses React 19 `useActionState` — render
|
|
346
|
+
* it only on React 19 (the rest of this module works on 18). `attempt`/`success`
|
|
347
|
+
* are the block's declared event keys (a typo dev-warns via the raw tracker).
|
|
348
|
+
*
|
|
349
|
+
* @example
|
|
350
|
+
* ```tsx
|
|
351
|
+
* <TrackedForm attempt="submitAttempt" success="submitSuccess" action={subscribe}>
|
|
352
|
+
* <input name="email" /> <button>Join</button>
|
|
353
|
+
* </TrackedForm>
|
|
354
|
+
* ```
|
|
355
|
+
*/
|
|
356
|
+
declare function TrackedForm({ attempt, success, action, children, ...formProps }: {
|
|
357
|
+
attempt: string;
|
|
358
|
+
success: string;
|
|
359
|
+
action: (formData: FormData) => void | Promise<void>;
|
|
360
|
+
children: ReactNode;
|
|
361
|
+
} & Omit<ComponentProps<'form'>, 'action' | 'children'>): React.JSX.Element;
|
|
362
|
+
|
|
363
|
+
export { BlockTracker, TrackedForm, TrackingRuntimeProvider, buildBlockEvent, createTrackedBlocks, resolveWireName, useBlockTrackerRaw };
|
|
364
|
+
export type { BlockTrackingCtx, TrackingRuntime };
|