@contentful/experiences-sdk-core 0.12.1 → 0.12.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/dist/attribution.d.ts +6 -0
- package/dist/attribution.js +230 -0
- package/dist/attribution.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/resolve-experience.d.ts +5 -1
- package/dist/resolve-experience.js +7 -1
- package/dist/resolve-experience.js.map +1 -1
- package/dist/tracking-attributes.d.ts +34 -0
- package/dist/tracking-attributes.js +12 -0
- package/dist/tracking-attributes.js.map +1 -0
- package/dist/types.d.ts +40 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,40 @@
|
|
|
1
|
+
## 0.12.3 (2026-10-14)
|
|
2
|
+
|
|
3
|
+
### 🚀 Features
|
|
4
|
+
|
|
5
|
+
- ⚠️ **web:** [NT-4312] track by scope occurrence instead of node id ([#225](https://github.com/contentful/experiences/pull/225))
|
|
6
|
+
- **core:** [NT-4312] resolve reportable scopes and bound entries from the source map ([#223](https://github.com/contentful/experiences/pull/223))
|
|
7
|
+
|
|
8
|
+
### ⚠️ Breaking Changes
|
|
9
|
+
|
|
10
|
+
- **web:** [NT-4312] track by scope occurrence instead of node id ([#225](https://github.com/contentful/experiences/pull/225))
|
|
11
|
+
`data-ctfl-node-id` is replaced by `data-ctfl-scopes`, a
|
|
12
|
+
space-separated list of scope occurrence keys, and `resolveAttribution` now
|
|
13
|
+
takes a key rather than a node id. `getTrackingAttributes` takes a node's
|
|
14
|
+
`attribution` instead of its `nodeId`.
|
|
15
|
+
A node id cannot say that one element sits in several scopes, or that several
|
|
16
|
+
root elements make up one Fragment occurrence. Keys resolve through
|
|
17
|
+
`plan.attribution.scopes`.
|
|
18
|
+
`getTrackingAttributes` and the attribute names are pure string functions, so
|
|
19
|
+
they move into core, and React, Svelte and Angular re-export them: components
|
|
20
|
+
stamp the attribute from `attribution` without installing `web`. The
|
|
21
|
+
`tracking-attributes` subpath re-exports them from core.
|
|
22
|
+
The detectors group elements by key and hand each group to one observer, so an
|
|
23
|
+
occurrence is one view or hover series: it is in view while any element has a
|
|
24
|
+
visible pixel, hovered until the last element is left, and its final event keeps
|
|
25
|
+
the attribution that qualified it. Clicks go to the innermost Fragment
|
|
26
|
+
occurrence of the nearest tracked ancestor, read from the attribute so the order
|
|
27
|
+
survives `refresh()`. Experiences remain view-only.
|
|
28
|
+
This diverges from the Optimization SDK, which tracks per element; the ticket's
|
|
29
|
+
grouping requirement forces it. The interaction-tracking ADR is removed.
|
|
30
|
+
Refs: NT-4312
|
|
31
|
+
|
|
32
|
+
## 0.12.2 (2026-10-12)
|
|
33
|
+
|
|
34
|
+
### 🚀 Features
|
|
35
|
+
|
|
36
|
+
- [NT-4312] keep a raw payload's source map and let callers override it ([#222](https://github.com/contentful/experiences/pull/222))
|
|
37
|
+
|
|
1
38
|
## 0.12.1 (2026-10-09)
|
|
2
39
|
|
|
3
40
|
This was a version bump only for core to align it with other projects, there were no code changes.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { DebugLogger } from './debug-logger.js';
|
|
2
|
+
import { ExperienceSourceMap, PortableRenderNode, PlanAttribution } from './types.js';
|
|
3
|
+
|
|
4
|
+
declare function resolveAttribution(sourceMap: ExperienceSourceMap | undefined, nodes: PortableRenderNode[], log: DebugLogger): PlanAttribution | undefined;
|
|
5
|
+
|
|
6
|
+
export { resolveAttribution };
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
const SUPPORTED_VERSION = 1;
|
|
2
|
+
const DEFINING_LAYER_KIND = {
|
|
3
|
+
Experience: "ExperienceTemplate",
|
|
4
|
+
Fragment: "Component"
|
|
5
|
+
};
|
|
6
|
+
function asRecord(value) {
|
|
7
|
+
return typeof value === "object" && value !== null ? value : void 0;
|
|
8
|
+
}
|
|
9
|
+
function asIndex(value) {
|
|
10
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : void 0;
|
|
11
|
+
}
|
|
12
|
+
function asString(value) {
|
|
13
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
14
|
+
}
|
|
15
|
+
function rowAt(rows, ref) {
|
|
16
|
+
const index = asIndex(ref);
|
|
17
|
+
return index === void 0 ? void 0 : asRecord(rows[index]);
|
|
18
|
+
}
|
|
19
|
+
function isPath(value) {
|
|
20
|
+
return Array.isArray(value) && value.every((s) => typeof s === "string" || Number.isInteger(s));
|
|
21
|
+
}
|
|
22
|
+
function parseLayer(value) {
|
|
23
|
+
const record = asRecord(value);
|
|
24
|
+
if (record === void 0) return void 0;
|
|
25
|
+
return { record, kind: asString(record.kind), id: asString(record.id) };
|
|
26
|
+
}
|
|
27
|
+
function scopeKind(layer) {
|
|
28
|
+
if (layer.kind === "Experience") return "Experience";
|
|
29
|
+
if (layer.kind === "ExperienceFragment") return "Fragment";
|
|
30
|
+
return void 0;
|
|
31
|
+
}
|
|
32
|
+
function createContext(sourceMap, log) {
|
|
33
|
+
const map = asRecord(sourceMap);
|
|
34
|
+
if (map?.version !== SUPPORTED_VERSION) {
|
|
35
|
+
log.log(`ignoring source map: unsupported version ${String(map?.version)}`);
|
|
36
|
+
return void 0;
|
|
37
|
+
}
|
|
38
|
+
const nodes = asRecord(map.nodes);
|
|
39
|
+
if (!Array.isArray(map.layers) || !Array.isArray(map.variants) || nodes === void 0) {
|
|
40
|
+
log.log("ignoring source map: missing layers, variants or nodes");
|
|
41
|
+
return void 0;
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
nodes,
|
|
45
|
+
layers: map.layers.map(parseLayer),
|
|
46
|
+
variants: map.variants,
|
|
47
|
+
entries: Array.isArray(map.entries) ? map.entries : [],
|
|
48
|
+
dataAssemblies: Array.isArray(map.dataAssemblies) ? map.dataAssemblies : [],
|
|
49
|
+
log,
|
|
50
|
+
scopes: {},
|
|
51
|
+
scopeEntries: /* @__PURE__ */ new Map(),
|
|
52
|
+
bindingEntries: /* @__PURE__ */ new Map()
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function reportableScopes(ctx, chain) {
|
|
56
|
+
const outerFirst = [...chain].reverse();
|
|
57
|
+
const scopes = [];
|
|
58
|
+
for (const [position, row] of outerFirst.entries()) {
|
|
59
|
+
const layer = ctx.layers[row];
|
|
60
|
+
const kind = layer === void 0 ? void 0 : scopeKind(layer);
|
|
61
|
+
if (layer?.id === void 0 || kind === void 0) continue;
|
|
62
|
+
if (scopes.some((scope) => scope.row === row)) continue;
|
|
63
|
+
const innerRow = outerFirst[position + 1];
|
|
64
|
+
const innerLayer = innerRow === void 0 ? void 0 : ctx.layers[innerRow];
|
|
65
|
+
const definesThisScope = innerLayer?.kind === DEFINING_LAYER_KIND[kind];
|
|
66
|
+
scopes.push({
|
|
67
|
+
row,
|
|
68
|
+
layer,
|
|
69
|
+
entityId: layer.id,
|
|
70
|
+
kind,
|
|
71
|
+
entityKindId: definesThisScope ? innerLayer?.id : void 0
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return scopes;
|
|
75
|
+
}
|
|
76
|
+
function occurrenceKey(row, listPath, runStart) {
|
|
77
|
+
return `s${row}@${listPath}#${runStart}`;
|
|
78
|
+
}
|
|
79
|
+
function readVariant(ctx, scope) {
|
|
80
|
+
const refs = scope.layer.record.variants;
|
|
81
|
+
if (!Array.isArray(refs) || refs.length === 0) return {};
|
|
82
|
+
if (refs.length > 1) {
|
|
83
|
+
ctx.log.log(`layer "${scope.entityId}" lists ${refs.length} variants; using the first`);
|
|
84
|
+
}
|
|
85
|
+
const variant = rowAt(ctx.variants, refs[0]);
|
|
86
|
+
const optimizationId = asString(variant?.optimizationId);
|
|
87
|
+
const variantId = asString(variant?.variantId);
|
|
88
|
+
const variantIndex = variant?.variantIndex;
|
|
89
|
+
const fields = {};
|
|
90
|
+
if (optimizationId !== void 0) fields.optimizationId = optimizationId;
|
|
91
|
+
if (variantId !== void 0) fields.variantId = variantId;
|
|
92
|
+
if (typeof variantIndex === "number") fields.variantIndex = variantIndex;
|
|
93
|
+
return fields;
|
|
94
|
+
}
|
|
95
|
+
function buildScope(ctx, scope, key, parentExperienceId) {
|
|
96
|
+
const attribution = {
|
|
97
|
+
key,
|
|
98
|
+
entityId: scope.entityId,
|
|
99
|
+
entityKind: scope.kind,
|
|
100
|
+
...readVariant(ctx, scope)
|
|
101
|
+
};
|
|
102
|
+
if (scope.entityKindId !== void 0) {
|
|
103
|
+
attribution.entityKindId = scope.entityKindId;
|
|
104
|
+
}
|
|
105
|
+
if (scope.kind === "Fragment" && parentExperienceId !== void 0) {
|
|
106
|
+
attribution.parentExperienceId = parentExperienceId;
|
|
107
|
+
}
|
|
108
|
+
return attribution;
|
|
109
|
+
}
|
|
110
|
+
function entriesBoundAt(ctx, assemblyRow, path) {
|
|
111
|
+
const cacheKey = `${assemblyRow}|${JSON.stringify(path)}`;
|
|
112
|
+
const cached = ctx.bindingEntries.get(cacheKey);
|
|
113
|
+
if (cached !== void 0) return cached;
|
|
114
|
+
const found = /* @__PURE__ */ new Set();
|
|
115
|
+
const visited = /* @__PURE__ */ new Set();
|
|
116
|
+
const followAssembly = (row, path2) => {
|
|
117
|
+
const visitKey = `${row}|${JSON.stringify(path2)}`;
|
|
118
|
+
const assembly = asRecord(ctx.dataAssemblies[row]);
|
|
119
|
+
if (assembly === void 0 || visited.has(visitKey)) return;
|
|
120
|
+
visited.add(visitKey);
|
|
121
|
+
walk(assembly.return, path2);
|
|
122
|
+
};
|
|
123
|
+
const walkLeaf = (leaf, path2) => {
|
|
124
|
+
if (leaf.type === "entry") {
|
|
125
|
+
const entryId = asString(rowAt(ctx.entries, leaf.entry)?.id);
|
|
126
|
+
if (entryId !== void 0) found.add(entryId);
|
|
127
|
+
} else if (leaf.type === "dataAssembly") {
|
|
128
|
+
const row = asIndex(leaf.dataAssembly);
|
|
129
|
+
const leafPath = isPath(leaf.path) ? leaf.path : [];
|
|
130
|
+
if (row !== void 0) followAssembly(row, [...leafPath, ...path2]);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
const walk = (value, path2) => {
|
|
134
|
+
const [head, ...rest] = path2;
|
|
135
|
+
if (Array.isArray(value)) {
|
|
136
|
+
if (head === void 0) value.forEach((child) => walk(child, []));
|
|
137
|
+
else if (typeof head === "number") walk(value[head], rest);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const record = asRecord(value);
|
|
141
|
+
if (record === void 0) return;
|
|
142
|
+
if (typeof record.type === "string") walkLeaf(record, path2);
|
|
143
|
+
else if (head === void 0) Object.values(record).forEach((child) => walk(child, []));
|
|
144
|
+
else walk(record[String(head)], rest);
|
|
145
|
+
};
|
|
146
|
+
followAssembly(assemblyRow, path);
|
|
147
|
+
const ids = [...found];
|
|
148
|
+
ctx.bindingEntries.set(cacheKey, ids);
|
|
149
|
+
return ids;
|
|
150
|
+
}
|
|
151
|
+
function readNodeEntries(ctx, entry) {
|
|
152
|
+
const properties = Array.isArray(entry.contentProperties) ? entry.contentProperties : [];
|
|
153
|
+
const ids = /* @__PURE__ */ new Set();
|
|
154
|
+
for (const property of properties) {
|
|
155
|
+
const item = asRecord(property);
|
|
156
|
+
const assemblyRow = asIndex(item?.dataAssembly);
|
|
157
|
+
const bindings = asRecord(item?.bindings);
|
|
158
|
+
if (item?.type !== "dataAssembly" || assemblyRow === void 0 || bindings === void 0) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
for (const path of Object.values(bindings).filter(isPath)) {
|
|
162
|
+
for (const id of entriesBoundAt(ctx, assemblyRow, path)) ids.add(id);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return [...ids];
|
|
166
|
+
}
|
|
167
|
+
function addScopeEntries(ctx, scope, entryIds) {
|
|
168
|
+
const owned = ctx.scopeEntries.get(scope) ?? /* @__PURE__ */ new Set();
|
|
169
|
+
for (const id of entryIds) owned.add(id);
|
|
170
|
+
ctx.scopeEntries.set(scope, owned);
|
|
171
|
+
}
|
|
172
|
+
function readChain(entry) {
|
|
173
|
+
const { layers } = entry;
|
|
174
|
+
return Array.isArray(layers) && layers.every(Number.isInteger) ? layers : [];
|
|
175
|
+
}
|
|
176
|
+
function attributeNode(ctx, node, parentKeys, previousKeys, listPath, index) {
|
|
177
|
+
const entry = node.nodeId === void 0 ? void 0 : asRecord(ctx.nodes[node.nodeId]);
|
|
178
|
+
if (entry === void 0) return void 0;
|
|
179
|
+
const scopes = reportableScopes(ctx, readChain(entry));
|
|
180
|
+
if (scopes.length === 0) return void 0;
|
|
181
|
+
const keys = /* @__PURE__ */ new Map();
|
|
182
|
+
const attribution = { scopes: [], roots: [] };
|
|
183
|
+
let parentExperienceId;
|
|
184
|
+
let nearest;
|
|
185
|
+
const previousScopes = previousKeys.entries();
|
|
186
|
+
let sameOccurrencePrefix = true;
|
|
187
|
+
for (const scope of scopes) {
|
|
188
|
+
const previous = previousScopes.next().value;
|
|
189
|
+
sameOccurrencePrefix &&= previous?.[0] === scope.row;
|
|
190
|
+
const isRoot = !parentKeys.has(scope.row);
|
|
191
|
+
const key = parentKeys.get(scope.row) ?? (sameOccurrencePrefix ? previous?.[1] : void 0) ?? occurrenceKey(scope.row, listPath, index);
|
|
192
|
+
sameOccurrencePrefix &&= previous?.[1] === key;
|
|
193
|
+
keys.set(scope.row, key);
|
|
194
|
+
nearest = ctx.scopes[key] ??= buildScope(ctx, scope, key, parentExperienceId);
|
|
195
|
+
attribution.scopes.push(nearest);
|
|
196
|
+
if (isRoot) attribution.roots.push(nearest);
|
|
197
|
+
if (scope.kind === "Experience") parentExperienceId = scope.entityId;
|
|
198
|
+
}
|
|
199
|
+
const entryIds = readNodeEntries(ctx, entry);
|
|
200
|
+
if (entryIds.length > 0 && nearest !== void 0) {
|
|
201
|
+
attribution.entryIds = entryIds;
|
|
202
|
+
addScopeEntries(ctx, nearest, entryIds);
|
|
203
|
+
}
|
|
204
|
+
node.attribution = attribution;
|
|
205
|
+
return keys;
|
|
206
|
+
}
|
|
207
|
+
function assignSiblings(ctx, siblings, parentKeys, listPath = "") {
|
|
208
|
+
let previousKeys = /* @__PURE__ */ new Map();
|
|
209
|
+
for (const [index, node] of siblings.entries()) {
|
|
210
|
+
const ownKeys = attributeNode(ctx, node, parentKeys, previousKeys, listPath, index);
|
|
211
|
+
previousKeys = ownKeys ?? /* @__PURE__ */ new Map();
|
|
212
|
+
for (const [slotName, children] of Object.entries(node.slots)) {
|
|
213
|
+
assignSiblings(ctx, children, ownKeys ?? parentKeys, `${listPath}/${index}.${slotName}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function resolveAttribution(sourceMap, nodes, log) {
|
|
218
|
+
if (sourceMap === void 0) return void 0;
|
|
219
|
+
const ctx = createContext(sourceMap, log);
|
|
220
|
+
if (ctx === void 0) return void 0;
|
|
221
|
+
assignSiblings(ctx, nodes, /* @__PURE__ */ new Map());
|
|
222
|
+
for (const [scope, ids] of ctx.scopeEntries) scope.entryIds = [...ids];
|
|
223
|
+
const scopes = Object.values(ctx.scopes);
|
|
224
|
+
for (const scope of scopes) Object.freeze(scope);
|
|
225
|
+
return scopes.length > 0 ? { scopes: ctx.scopes } : void 0;
|
|
226
|
+
}
|
|
227
|
+
export {
|
|
228
|
+
resolveAttribution
|
|
229
|
+
};
|
|
230
|
+
//# sourceMappingURL=attribution.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/attribution.ts"],"sourcesContent":["import type { DebugLogger } from './debug-logger.js';\nimport type {\n ExperienceSourceMap,\n NodeAttribution,\n PlanAttribution,\n PortableRenderNode,\n ScopeAttribution,\n} from './types.js';\n\nconst SUPPORTED_VERSION = 1;\n\ntype EntityKind = ScopeAttribution['entityKind'];\ntype VariantFields = Pick<ScopeAttribution, 'optimizationId' | 'variantId' | 'variantIndex'>;\ntype Rec = Record<string, unknown>;\ntype PathSegment = string | number;\n\nconst DEFINING_LAYER_KIND: Record<EntityKind, string> = {\n Experience: 'ExperienceTemplate',\n Fragment: 'Component',\n};\n\nfunction asRecord(value: unknown): Rec | undefined {\n return typeof value === 'object' && value !== null ? (value as Rec) : undefined;\n}\n\nfunction asIndex(value: unknown): number | undefined {\n return typeof value === 'number' && Number.isInteger(value) && value >= 0 ? value : undefined;\n}\n\nfunction asString(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n\nfunction rowAt(rows: unknown[], ref: unknown): Rec | undefined {\n const index = asIndex(ref);\n return index === undefined ? undefined : asRecord(rows[index]);\n}\n\nfunction isPath(value: unknown): value is PathSegment[] {\n return Array.isArray(value) && value.every((s) => typeof s === 'string' || Number.isInteger(s));\n}\n\ninterface Layer {\n record: Rec;\n kind: string | undefined;\n id: string | undefined;\n}\n\nfunction parseLayer(value: unknown): Layer | undefined {\n const record = asRecord(value);\n if (record === undefined) return undefined;\n return { record, kind: asString(record.kind), id: asString(record.id) };\n}\n\nfunction scopeKind(layer: Layer): EntityKind | undefined {\n if (layer.kind === 'Experience') return 'Experience';\n if (layer.kind === 'ExperienceFragment') return 'Fragment';\n return undefined;\n}\n\ninterface Context {\n nodes: Rec;\n layers: (Layer | undefined)[];\n variants: unknown[];\n entries: unknown[];\n dataAssemblies: unknown[];\n log: DebugLogger;\n scopes: Record<string, ScopeAttribution>;\n scopeEntries: Map<ScopeAttribution, Set<string>>;\n bindingEntries: Map<string, readonly string[]>;\n}\n\nfunction createContext(sourceMap: ExperienceSourceMap, log: DebugLogger): Context | undefined {\n const map = asRecord(sourceMap);\n if (map?.version !== SUPPORTED_VERSION) {\n log.log(`ignoring source map: unsupported version ${String(map?.version)}`);\n return undefined;\n }\n const nodes = asRecord(map.nodes);\n if (!Array.isArray(map.layers) || !Array.isArray(map.variants) || nodes === undefined) {\n log.log('ignoring source map: missing layers, variants or nodes');\n return undefined;\n }\n return {\n nodes,\n layers: map.layers.map(parseLayer),\n variants: map.variants,\n entries: Array.isArray(map.entries) ? map.entries : [],\n dataAssemblies: Array.isArray(map.dataAssemblies) ? map.dataAssemblies : [],\n log,\n scopes: {},\n scopeEntries: new Map(),\n bindingEntries: new Map(),\n };\n}\n\ninterface ChainScope {\n row: number;\n layer: Layer;\n entityId: string;\n kind: EntityKind;\n entityKindId: string | undefined;\n}\n\nfunction reportableScopes(ctx: Context, chain: number[]): ChainScope[] {\n const outerFirst = [...chain].reverse();\n const scopes: ChainScope[] = [];\n\n for (const [position, row] of outerFirst.entries()) {\n const layer = ctx.layers[row];\n const kind = layer === undefined ? undefined : scopeKind(layer);\n if (layer?.id === undefined || kind === undefined) continue;\n if (scopes.some((scope) => scope.row === row)) continue;\n\n const innerRow = outerFirst[position + 1];\n const innerLayer = innerRow === undefined ? undefined : ctx.layers[innerRow];\n const definesThisScope = innerLayer?.kind === DEFINING_LAYER_KIND[kind];\n\n scopes.push({\n row,\n layer,\n entityId: layer.id,\n kind,\n entityKindId: definesThisScope ? innerLayer?.id : undefined,\n });\n }\n return scopes;\n}\n\nfunction occurrenceKey(row: number, listPath: string, runStart: number): string {\n return `s${row}@${listPath}#${runStart}`;\n}\n\nfunction readVariant(ctx: Context, scope: ChainScope): VariantFields {\n const refs = scope.layer.record.variants;\n if (!Array.isArray(refs) || refs.length === 0) return {};\n if (refs.length > 1) {\n ctx.log.log(`layer \"${scope.entityId}\" lists ${refs.length} variants; using the first`);\n }\n\n const variant = rowAt(ctx.variants, refs[0]);\n const optimizationId = asString(variant?.optimizationId);\n const variantId = asString(variant?.variantId);\n const variantIndex = variant?.variantIndex;\n\n const fields: VariantFields = {};\n if (optimizationId !== undefined) fields.optimizationId = optimizationId;\n if (variantId !== undefined) fields.variantId = variantId;\n if (typeof variantIndex === 'number') fields.variantIndex = variantIndex;\n return fields;\n}\n\nfunction buildScope(\n ctx: Context,\n scope: ChainScope,\n key: string,\n parentExperienceId: string | undefined\n): ScopeAttribution {\n const attribution: ScopeAttribution = {\n key,\n entityId: scope.entityId,\n entityKind: scope.kind,\n ...readVariant(ctx, scope),\n };\n if (scope.entityKindId !== undefined) {\n attribution.entityKindId = scope.entityKindId;\n }\n if (scope.kind === 'Fragment' && parentExperienceId !== undefined) {\n attribution.parentExperienceId = parentExperienceId;\n }\n return attribution;\n}\n\nfunction entriesBoundAt(ctx: Context, assemblyRow: number, path: PathSegment[]): readonly string[] {\n const cacheKey = `${assemblyRow}|${JSON.stringify(path)}`;\n const cached = ctx.bindingEntries.get(cacheKey);\n if (cached !== undefined) return cached;\n\n const found = new Set<string>();\n const visited = new Set<string>();\n\n const followAssembly = (row: number, path: PathSegment[]): void => {\n const visitKey = `${row}|${JSON.stringify(path)}`;\n const assembly = asRecord(ctx.dataAssemblies[row]);\n if (assembly === undefined || visited.has(visitKey)) return;\n visited.add(visitKey);\n walk(assembly.return, path);\n };\n\n const walkLeaf = (leaf: Rec, path: PathSegment[]): void => {\n if (leaf.type === 'entry') {\n const entryId = asString(rowAt(ctx.entries, leaf.entry)?.id);\n if (entryId !== undefined) found.add(entryId);\n } else if (leaf.type === 'dataAssembly') {\n const row = asIndex(leaf.dataAssembly);\n const leafPath = isPath(leaf.path) ? leaf.path : [];\n if (row !== undefined) followAssembly(row, [...leafPath, ...path]);\n }\n };\n\n const walk = (value: unknown, path: PathSegment[]): void => {\n const [head, ...rest] = path;\n\n if (Array.isArray(value)) {\n if (head === undefined) value.forEach((child) => walk(child, []));\n else if (typeof head === 'number') walk(value[head], rest);\n return;\n }\n\n const record = asRecord(value);\n if (record === undefined) return;\n if (typeof record.type === 'string') walkLeaf(record, path);\n else if (head === undefined) Object.values(record).forEach((child) => walk(child, []));\n else walk(record[String(head)], rest);\n };\n\n followAssembly(assemblyRow, path);\n const ids = [...found];\n ctx.bindingEntries.set(cacheKey, ids);\n return ids;\n}\n\nfunction readNodeEntries(ctx: Context, entry: Rec): string[] {\n const properties: unknown[] = Array.isArray(entry.contentProperties)\n ? entry.contentProperties\n : [];\n const ids = new Set<string>();\n\n for (const property of properties) {\n const item = asRecord(property);\n const assemblyRow = asIndex(item?.dataAssembly);\n const bindings = asRecord(item?.bindings);\n if (item?.type !== 'dataAssembly' || assemblyRow === undefined || bindings === undefined) {\n continue;\n }\n for (const path of Object.values(bindings).filter(isPath)) {\n for (const id of entriesBoundAt(ctx, assemblyRow, path)) ids.add(id);\n }\n }\n return [...ids];\n}\n\nfunction addScopeEntries(ctx: Context, scope: ScopeAttribution, entryIds: string[]): void {\n const owned = ctx.scopeEntries.get(scope) ?? new Set<string>();\n for (const id of entryIds) owned.add(id);\n ctx.scopeEntries.set(scope, owned);\n}\n\nfunction readChain(entry: Rec): number[] {\n const { layers } = entry;\n return Array.isArray(layers) && layers.every(Number.isInteger) ? (layers as number[]) : [];\n}\n\nfunction attributeNode(\n ctx: Context,\n node: PortableRenderNode,\n parentKeys: Map<number, string>,\n previousKeys: Map<number, string>,\n listPath: string,\n index: number\n): Map<number, string> | undefined {\n const entry = node.nodeId === undefined ? undefined : asRecord(ctx.nodes[node.nodeId]);\n if (entry === undefined) return undefined;\n const scopes = reportableScopes(ctx, readChain(entry));\n if (scopes.length === 0) return undefined;\n\n const keys = new Map<number, string>();\n const attribution: NodeAttribution = { scopes: [], roots: [] };\n let parentExperienceId: string | undefined;\n let nearest: ScopeAttribution | undefined;\n\n // A sibling's key is only reusable while every enclosing scope matches as well.\n const previousScopes = previousKeys.entries();\n let sameOccurrencePrefix = true;\n\n for (const scope of scopes) {\n const previous = previousScopes.next().value;\n sameOccurrencePrefix &&= previous?.[0] === scope.row;\n\n const isRoot = !parentKeys.has(scope.row);\n const key: string =\n parentKeys.get(scope.row) ??\n (sameOccurrencePrefix ? previous?.[1] : undefined) ??\n occurrenceKey(scope.row, listPath, index);\n sameOccurrencePrefix &&= previous?.[1] === key;\n keys.set(scope.row, key);\n\n nearest = ctx.scopes[key] ??= buildScope(ctx, scope, key, parentExperienceId);\n attribution.scopes.push(nearest);\n if (isRoot) attribution.roots.push(nearest);\n if (scope.kind === 'Experience') parentExperienceId = scope.entityId;\n }\n\n const entryIds = readNodeEntries(ctx, entry);\n if (entryIds.length > 0 && nearest !== undefined) {\n attribution.entryIds = entryIds;\n addScopeEntries(ctx, nearest, entryIds);\n }\n node.attribution = attribution;\n return keys;\n}\n\nfunction assignSiblings(\n ctx: Context,\n siblings: PortableRenderNode[],\n parentKeys: Map<number, string>,\n listPath = ''\n): void {\n let previousKeys = new Map<number, string>();\n for (const [index, node] of siblings.entries()) {\n const ownKeys = attributeNode(ctx, node, parentKeys, previousKeys, listPath, index);\n previousKeys = ownKeys ?? new Map();\n\n for (const [slotName, children] of Object.entries(node.slots)) {\n assignSiblings(ctx, children, ownKeys ?? parentKeys, `${listPath}/${index}.${slotName}`);\n }\n }\n}\n\nexport function resolveAttribution(\n sourceMap: ExperienceSourceMap | undefined,\n nodes: PortableRenderNode[],\n log: DebugLogger\n): PlanAttribution | undefined {\n if (sourceMap === undefined) return undefined;\n const ctx = createContext(sourceMap, log);\n if (ctx === undefined) return undefined;\n\n assignSiblings(ctx, nodes, new Map());\n for (const [scope, ids] of ctx.scopeEntries) scope.entryIds = [...ids];\n\n const scopes = Object.values(ctx.scopes);\n for (const scope of scopes) Object.freeze(scope);\n return scopes.length > 0 ? { scopes: ctx.scopes } : undefined;\n}\n"],"mappings":"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","names":["path"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export { ComponentNode, ComponentRef, DesignPropValue, DesignToken, ExperienceContext, ExperienceExtensions, ExperienceNode, ExperiencePayload, ExperienceSourceMap, ExperienceSys, ExperienceTemplateNode, ExperienceTemplateRef, ManualDesignValue, PortableRegistration, PortableRenderNode, PortableRenderPlan, RenderContext, ResolveContext, ResolveToken } from './types.js';
|
|
1
|
+
export { ComponentNode, ComponentRef, DesignPropValue, DesignToken, ExperienceContext, ExperienceExtensions, ExperienceNode, ExperiencePayload, ExperienceSourceMap, ExperienceSys, ExperienceTemplateNode, ExperienceTemplateRef, ManualDesignValue, NodeAttribution, PlanAttribution, PortableRegistration, PortableRenderNode, PortableRenderPlan, RenderContext, ResolveContext, ResolveToken, ScopeAttribution } from './types.js';
|
|
2
2
|
export { ResolveExperienceOptions, ResolverConfig, resolveExperience } from './resolve-experience.js';
|
|
3
3
|
export { DebugLogger, createDebugLogger } from './debug-logger.js';
|
|
4
4
|
export { applyTokenResolver, resolveDesignProperties } from './design-properties.js';
|
|
5
|
+
export { TRACKING_CLICKABLE_ATTRIBUTE, TRACKING_SCOPES_ATTRIBUTE, TrackingAttributes, getTrackingAttributes } from './tracking-attributes.js';
|
package/dist/index.js
CHANGED
|
@@ -2,9 +2,17 @@ export * from "./types.js";
|
|
|
2
2
|
import { resolveExperience } from "./resolve-experience.js";
|
|
3
3
|
import { createDebugLogger } from "./debug-logger.js";
|
|
4
4
|
import { applyTokenResolver, resolveDesignProperties } from "./design-properties.js";
|
|
5
|
+
import {
|
|
6
|
+
getTrackingAttributes,
|
|
7
|
+
TRACKING_CLICKABLE_ATTRIBUTE,
|
|
8
|
+
TRACKING_SCOPES_ATTRIBUTE
|
|
9
|
+
} from "./tracking-attributes.js";
|
|
5
10
|
export {
|
|
11
|
+
TRACKING_CLICKABLE_ATTRIBUTE,
|
|
12
|
+
TRACKING_SCOPES_ATTRIBUTE,
|
|
6
13
|
applyTokenResolver,
|
|
7
14
|
createDebugLogger,
|
|
15
|
+
getTrackingAttributes,
|
|
8
16
|
resolveDesignProperties,
|
|
9
17
|
resolveExperience
|
|
10
18
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './types.js';\nexport { resolveExperience } from './resolve-experience.js';\nexport type { ResolverConfig, ResolveExperienceOptions } from './resolve-experience.js';\nexport { createDebugLogger } from './debug-logger.js';\nexport type { DebugLogger } from './debug-logger.js';\nexport { applyTokenResolver, resolveDesignProperties } from './design-properties.js';\n"],"mappings":"AAAA,cAAc;AACd,SAAS,yBAAyB;AAElC,SAAS,yBAAyB;AAElC,SAAS,oBAAoB,+BAA+B;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './types.js';\nexport { resolveExperience } from './resolve-experience.js';\nexport type { ResolverConfig, ResolveExperienceOptions } from './resolve-experience.js';\nexport { createDebugLogger } from './debug-logger.js';\nexport type { DebugLogger } from './debug-logger.js';\nexport { applyTokenResolver, resolveDesignProperties } from './design-properties.js';\nexport {\n getTrackingAttributes,\n TRACKING_CLICKABLE_ATTRIBUTE,\n TRACKING_SCOPES_ATTRIBUTE,\n} from './tracking-attributes.js';\nexport type { TrackingAttributes } from './tracking-attributes.js';\n"],"mappings":"AAAA,cAAc;AACd,SAAS,yBAAyB;AAElC,SAAS,yBAAyB;AAElC,SAAS,oBAAoB,+BAA+B;AAC5D;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;","names":[]}
|
|
@@ -33,7 +33,11 @@ interface ResolveExperienceOptions {
|
|
|
33
33
|
* resolver context as `ctx.experience.debug`. Defaults to `false`.
|
|
34
34
|
*/
|
|
35
35
|
debug?: boolean;
|
|
36
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Source map carried onto the plan. An explicit map wins over
|
|
38
|
+
* `payload.extensions.sourceMap`. Omit to use the payload's map, if it
|
|
39
|
+
* carries one.
|
|
40
|
+
*/
|
|
37
41
|
sourceMap?: ExperienceSourceMap;
|
|
38
42
|
}
|
|
39
43
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveAttribution } from "./attribution.js";
|
|
1
2
|
import { createDebugLogger } from "./debug-logger.js";
|
|
2
3
|
import { applyTokenResolver, resolveDesignProperties } from "./design-properties.js";
|
|
3
4
|
function getResolver(entry) {
|
|
@@ -210,7 +211,12 @@ async function resolveExperience(payload, config, options = {}) {
|
|
|
210
211
|
metadata: experience.metadata,
|
|
211
212
|
debug: experience.debug
|
|
212
213
|
};
|
|
213
|
-
|
|
214
|
+
const sourceMap = options.sourceMap ?? (isPlainPayload ? payload.extensions?.sourceMap : void 0);
|
|
215
|
+
if (sourceMap) {
|
|
216
|
+
plan.sourceMap = sourceMap;
|
|
217
|
+
const attribution = resolveAttribution(sourceMap, nodes, log);
|
|
218
|
+
if (attribution) plan.attribution = attribution;
|
|
219
|
+
}
|
|
214
220
|
const profileId = isPlainPayload && payload.extensions?.personalization?.profile?.id;
|
|
215
221
|
if (profileId) {
|
|
216
222
|
plan.personalization = { profileId };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/resolve-experience.ts"],"sourcesContent":["/*\n * Turns an XDA Experience payload into a runtime-neutral PortableRenderPlan.\n * Walks nodes recursively, splits content + design props, runs any registered\n * `resolveData` hooks in parallel, and resolves design values for the renderer.\n */\n\nimport { createDebugLogger, type DebugLogger } from './debug-logger.js';\nimport type {\n DesignPropValue,\n ExperienceContext,\n ExperienceSourceMap,\n ExperienceNode,\n ExperiencePayload,\n PortableRegistration,\n PortableRenderNode,\n PortableRenderPlan,\n ResolveContext,\n ResolveToken,\n} from './types.js';\nimport { applyTokenResolver, resolveDesignProperties } from './design-properties.js';\n\n/**\n * Structural type the resolver walker depends on. Matches the React /\n * Svelte adapter `Config` shape but doesn't require importing them —\n * render-core stays decoupled from any framework.\n *\n * Registry values are typed as `unknown` because each adapter accepts\n * either a bare framework component (function / Svelte class / etc.) OR\n * a config-object shape with `{ component, defaults?, resolveData? }`.\n * The resolver only cares about `resolveData`; it duck-types each entry\n * at runtime and ignores anything without it.\n */\nexport interface ResolverConfig {\n components: Record<string, unknown>;\n experienceTemplates?: Record<string, unknown>;\n /**\n * Resolves `DesignToken` design properties to runtime values. Mirrors the\n * adapter `Config.resolveToken`, so server and client agree without the\n * caller re-supplying it. Used during server-side pre-resolution.\n */\n resolveToken?: ResolveToken;\n}\n\nfunction getResolver(\n entry: unknown\n):\n | ((ctx: ResolveContext) => Record<string, unknown> | Promise<Record<string, unknown>>)\n | undefined {\n if (typeof entry !== 'object' || entry === null) return undefined;\n const candidate = (entry as { resolveData?: unknown }).resolveData;\n return typeof candidate === 'function'\n ? (candidate as (\n ctx: ResolveContext\n ) => Record<string, unknown> | Promise<Record<string, unknown>>)\n : undefined;\n}\n\nexport interface ResolveExperienceOptions {\n /**\n * Arbitrary per-render metadata exposed to every resolver as\n * `ctx.experience.metadata`. Defaults to `{}`.\n */\n metadata?: Record<string, unknown>;\n /**\n * Observability switch. When on, `resolveExperience` logs the resolution\n * steps and per-node `resolveData` fan-out timings. Threads through to the\n * resolver context as `ctx.experience.debug`. Defaults to `false`.\n */\n debug?: boolean;\n /** Carried onto the plan as-is. Omit for no source map. */\n sourceMap?: ExperienceSourceMap;\n}\n\nconst DEFAULT_EXPERIENCE: ExperienceContext = {\n debug: false,\n metadata: {},\n};\n\n/**\n * Registry lookup for a built node. A node's `kind` decides which half of the\n * customer Config owns its implementation — components and Experience\n * Templates are otherwise interchangeable at every other step.\n */\nfunction lookupEntry(config: ResolverConfig, registration: PortableRegistration): unknown {\n return registration.kind === 'experienceTemplate'\n ? config.experienceTemplates?.[registration.id]\n : config.components[registration.id];\n}\n\n/**\n * Extract the id (component or experienceTemplate) from its\n * `ResourceLink` URN. Real URN shapes:\n * crn:contentful:::experience:spaces/$self/environments/$self/components/<id>\n * crn:contentful:::experience:spaces/$self/environments/$self/experienceTemplates/<id>\n *\n * The id is the final path segment. We split on `/` and take the last\n * non-empty piece so this also tolerates trailing slashes or alternative\n * prefix shapes.\n */\nfunction extractIdFromUrn(urn: string): string {\n const segments = urn.split('/').filter((s) => s.length > 0);\n return segments[segments.length - 1] ?? urn;\n}\n\n/**\n * Read the ResourceLink a node points at. Which key is present is the only\n * difference between the two node variants: `experienceTemplate` means the\n * implementation lives in the customer's `experienceTemplates` registry,\n * `component` in `components`. Both carry ids in the same URN shape, the same\n * props shape, and the same slots — so everything downstream is kind-agnostic.\n *\n * `ExperienceNode` is a closed union, so a typed caller cannot produce anything\n * else. Payloads, however, are untrusted JSON at runtime, and the realistic way\n * a third shape arrives is a node kind *newer than this SDK*. Returns `null`\n * for anything unidentifiable rather than letting the ref access throw — see\n * `buildNode`.\n */\nfunction readNodeRef(\n node: ExperienceNode\n): { kind: PortableRegistration['kind']; urn: string } | null {\n const isExperienceTemplate = 'experienceTemplate' in node;\n const ref = isExperienceTemplate\n ? node.experienceTemplate\n : 'component' in node\n ? node.component\n : undefined;\n // Cast because the runtime value may violate the declared type: a node can\n // carry `component: {}`, which satisfies `'component' in node` but has no urn.\n const urn = (ref as { sys?: { urn?: unknown } } | undefined)?.sys?.urn;\n if (typeof urn !== 'string' || urn.length === 0) return null;\n return { kind: isExperienceTemplate ? 'experienceTemplate' : 'component', urn };\n}\n\n/**\n * Guards a top-level payload field that must be an array. A malformed or\n * partial payload (bad JSON, a hand-authored payload with the wrong shape)\n * should degrade to an empty experience rather than throwing and failing the\n * whole render — same \"dropping beats throwing\" rationale as\n * `warnUnrenderableNode` below, just one level up the tree.\n */\nfunction ensureArray<T>(\n value: unknown,\n field: string,\n log: DebugLogger,\n diagnostics: Error[]\n): T[] {\n if (Array.isArray(value)) return value as T[];\n const message =\n `Experience payload's \"${field}\" is not an array (got ${value === null ? 'null' : typeof value}); ` +\n `treating it as empty instead of throwing. This usually means a malformed or partial payload — ` +\n `check what produced it.`;\n if (typeof console !== 'undefined') {\n console.warn(`[@contentful/experiences] ${message}`);\n }\n log.log(message);\n diagnostics.push(new Error(message));\n return [];\n}\n\n/** Total node count in a payload subtree, the node itself included. */\nfunction countPayloadNodes(node: ExperienceNode): number {\n const slots = (node as { slots?: Record<string, unknown> }).slots;\n let total = 1;\n if (slots && typeof slots === 'object') {\n for (const children of Object.values(slots)) {\n if (!Array.isArray(children)) continue;\n for (const child of children) total += countPayloadNodes(child as ExperienceNode);\n }\n }\n return total;\n}\n\n/**\n * The one case where a node is dropped. AIS-413 was the opposite failure —\n * a node kind the SDK recognized and could have rendered, skipped behind a\n * vague warning, taking its whole subtree with it — so the bar here is that\n * nothing vanishes without a diagnostic naming what was lost.\n *\n * Dropping beats throwing: a `resolveExperience` rejection fails the entire\n * experience, so one unrecognized node in a sidebar would take down every page\n * containing it, for a payload the customer does not control and cannot fix.\n */\nfunction warnUnrenderableNode(node: ExperienceNode, log: DebugLogger, diagnostics: Error[]): void {\n const id = (node as { id?: unknown }).id;\n const label = typeof id === 'string' ? ` \"${id}\"` : '';\n const keys = Object.keys(node);\n const descendants = countPayloadNodes(node) - 1;\n const message =\n `Skipping unidentifiable node${label}: expected a \\`component\\` or \\`experienceTemplate\\` ` +\n `ResourceLink carrying a urn, got keys [${keys.join(', ')}]. Dropping it and ${descendants} ` +\n `descendant node(s). A payload node kind this SDK does not know is usually a version skew — ` +\n `upgrading @contentful/experiences-sdk-core may be all that is needed.`;\n if (typeof console !== 'undefined') {\n console.warn(`[@contentful/experiences] ${message}`);\n }\n log.log(message);\n diagnostics.push(new Error(message));\n}\n\n/**\n * Walk a sibling list into IR nodes, dropping any node `buildNode` cannot\n * identify. Used for both the top-level list and every slot.\n */\nfunction buildNodes(\n nodes: ExperienceNode[],\n config: ResolverConfig,\n nodeRefs: PortableRenderNode[],\n log: DebugLogger,\n diagnostics: Error[]\n): PortableRenderNode[] {\n const built: PortableRenderNode[] = [];\n for (const node of nodes) {\n const one = buildNode(node, config, nodeRefs, log, diagnostics);\n if (one !== null) built.push(one);\n }\n return built;\n}\n\n/**\n * Recursively turn a payload node into an IR node. The collected `nodeRefs`\n * array is for the resolver pass — every built node with a registered\n * resolver gets a reference appended so we can run them in parallel without\n * walking the tree twice.\n *\n * Returns `null` only for a node whose ResourceLink cannot be read, which\n * `warnUnrenderableNode` has already reported. Callers go through `buildNodes`.\n */\nfunction buildNode(\n node: ExperienceNode,\n config: ResolverConfig,\n nodeRefs: PortableRenderNode[],\n log: DebugLogger,\n diagnostics: Error[]\n): PortableRenderNode | null {\n const ref = readNodeRef(node);\n if (ref === null) {\n warnUnrenderableNode(node, log, diagnostics);\n return null;\n }\n const registration: PortableRegistration = {\n kind: ref.kind,\n id: extractIdFromUrn(ref.urn),\n };\n const nodeId = typeof node.id === 'string' ? node.id : undefined;\n\n const slots: Record<string, PortableRenderNode[]> = {};\n if (node.slots) {\n for (const [slotName, children] of Object.entries(node.slots)) {\n if (!Array.isArray(children)) {\n const message =\n `Slot \"${slotName}\" on ${registration.kind} \"${registration.id}\"` +\n `${nodeId ? ` (node \"${nodeId}\")` : ''} is not an array of nodes (got ` +\n `${children === null ? 'null' : typeof children}); treating it as empty instead of ` +\n `failing the whole experience. A hand-built payload with the wrong slot shape is the ` +\n `usual cause.`;\n if (typeof console !== 'undefined') {\n console.warn(`[@contentful/experiences] ${message}`);\n }\n log.log(message);\n diagnostics.push(new Error(message));\n slots[slotName] = [];\n continue;\n }\n slots[slotName] = buildNodes(children, config, nodeRefs, log, diagnostics);\n }\n }\n\n const built: PortableRenderNode = {\n registration,\n props: {\n content: { ...(node.contentProperties ?? {}) },\n // Resolved design values are written by the pre-resolution pass below.\n design: {},\n designRaw: { ...(node.designProperties ?? {}) } as Record<string, DesignPropValue>,\n },\n slots,\n };\n if (node.id) built.nodeId = node.id;\n if (getResolver(lookupEntry(config, registration))) {\n nodeRefs.push(built);\n }\n return built;\n}\n\n// Resolve a node's raw design values and tokens.\nfunction preResolveDesignProperties(\n design: Record<string, DesignPropValue>,\n resolveToken: ResolveToken | undefined\n): { props: Record<string, unknown>; unresolved: string[] } {\n return applyTokenResolver(resolveDesignProperties(design), resolveToken);\n}\n\n// Warn when resolveToken left tokens unresolved, so pass-through keys are diagnosable.\nfunction warnUnresolvedTokens(\n label: string,\n unresolved: string[],\n log: DebugLogger,\n diagnostics: Error[]\n): void {\n if (!unresolved.length) return;\n const message = `resolveToken returned undefined for token id(s) on \"${label}\": ${unresolved.join(', ')}. Resolved design (getDesignValues()) will carry the raw, unresolved DesignToken for those keys.`;\n if (typeof console !== 'undefined') {\n console.warn(`[@contentful/experiences] ${message}`);\n }\n log.log(`unresolved token id(s) on \"${label}\": ${unresolved.join(', ')}`);\n for (const tokenId of unresolved) {\n diagnostics.push(\n new Error(\n `Design token \"${tokenId}\" on ${label} has no \\`resolveToken\\` mapping; the raw token reaches the component unresolved.`\n )\n );\n }\n}\n\n// Depth-first token resolution for a node and its slot children.\nfunction preResolveNodeTree(\n node: PortableRenderNode,\n resolveToken: ResolveToken | undefined,\n log: DebugLogger,\n diagnostics: Error[]\n): void {\n const { props, unresolved } = preResolveDesignProperties(node.props.designRaw, resolveToken);\n node.props.design = props;\n warnUnresolvedTokens(\n `${node.registration.kind}:${node.registration.id}`,\n unresolved,\n log,\n diagnostics\n );\n for (const children of Object.values(node.slots)) {\n for (const child of children) {\n preResolveNodeTree(child, resolveToken, log, diagnostics);\n }\n }\n}\n\n/**\n * Turns an Experience payload (XDA response shape) into a PortableRenderPlan\n * ready to hand to a renderer. Walks the tree, classifies props, captures\n * slots, and runs any component-declared `resolveData` hooks (sync or async)\n * in parallel.\n *\n * Implementation note: the function is always async — even when no component\n * declares a resolver, the cost is one microtask. Customers get a single\n * uniform call site.\n */\nexport async function resolveExperience(\n payload: ExperiencePayload,\n config: ResolverConfig,\n options: ResolveExperienceOptions = {}\n): Promise<PortableRenderPlan> {\n const log = createDebugLogger(options.debug, 'core');\n log.lazy('resolveExperience called with payload', () => payload);\n\n const diagnostics: Error[] = [];\n\n // A payload that isn't an object at all (missing fetch result, a raw\n // `null`/`undefined` handed in by a broken transform step) can't be\n // guarded field-by-field via `ensureArray` below — that only guards once\n // we're already inside a `payload.nodes` access. Guard the whole payload\n // first so we degrade to an empty experience instead of throwing a raw\n // TypeError, and report exactly one diagnostic rather than double-counting\n // once we also run `nodes` through `ensureArray`.\n const isPlainPayload = payload !== null && typeof payload === 'object';\n if (!isPlainPayload) {\n const message =\n `Experience payload is ${payload === null ? 'null' : typeof payload}, not an object; ` +\n `treating it as an empty experience (no nodes) instead of throwing. This ` +\n `usually means the fetch or transform pipeline handed resolveExperience a missing or ` +\n `malformed payload — check what produced it.`;\n if (typeof console !== 'undefined') {\n console.warn(`[@contentful/experiences] ${message}`);\n }\n log.log(message);\n diagnostics.push(new Error(message));\n }\n const payloadNodes = isPlainPayload\n ? ensureArray<ExperienceNode>(payload.nodes, 'nodes', log, diagnostics)\n : [];\n // Pass 1: walk the payload into the IR. Collect refs to nodes that need\n // resolveData so pass 2 can run them in parallel without re-walking.\n const nodeRefs: PortableRenderNode[] = [];\n const nodes: PortableRenderNode[] = buildNodes(payloadNodes, config, nodeRefs, log, diagnostics);\n log.log(`built ${nodes.length} top-level node(s); ${nodeRefs.length} declare resolveData`);\n\n // Pass 2: run every node's resolveData hook in parallel.\n const experience: ExperienceContext = {\n debug: options.debug ?? DEFAULT_EXPERIENCE.debug,\n metadata: {\n ...DEFAULT_EXPERIENCE.metadata,\n ...(options.metadata ?? {}),\n },\n };\n\n const tasks: Array<Promise<void>> = [];\n\n for (const node of nodeRefs) {\n const resolver = getResolver(lookupEntry(config, node.registration));\n if (!resolver) continue;\n const ctx: ResolveContext = {\n content: node.props.content,\n design: node.props.designRaw,\n experience,\n };\n tasks.push(\n // Deferred via `.then(() => resolver(ctx))` rather than\n // `Promise.resolve(resolver(ctx))`: the latter calls `resolver(ctx)`\n // eagerly as an argument, so a resolver that throws *synchronously*\n // escapes immediately instead of becoming a rejection. The `onFailure`\n // handler below never rethrows, so one broken resolver can't fail the\n // `Promise.all` for every other node.\n Promise.resolve()\n .then(() => resolver(ctx))\n .then(\n (resolved) => {\n node.props.resolved = resolved;\n },\n (err: unknown) => {\n const label = `${node.registration.kind}:${node.registration.id}`;\n const reason = err instanceof Error ? err.message : String(err);\n const message =\n `resolveData for ${label}${node.nodeId ? ` (node \"${node.nodeId}\")` : ''} threw or ` +\n `rejected: ${reason}. Rendering the node without resolved data instead of failing ` +\n `the whole experience.`;\n if (typeof console !== 'undefined') {\n console.warn(`[@contentful/experiences] ${message}`);\n }\n log.log(message);\n diagnostics.push(new Error(message, { cause: err instanceof Error ? err : undefined }));\n }\n )\n );\n }\n\n // Time the fan-out as a whole rather than per-resolver — one aggregate line\n // keeps the timing signal without a line per node (which gets noisy fast).\n if (tasks.length > 0) {\n await log.time(`${tasks.length} resolveData hook(s)`, () => Promise.all(tasks));\n }\n\n for (const node of nodes) {\n preResolveNodeTree(node, config.resolveToken, log, diagnostics);\n }\n log.log('resolved design properties');\n\n // Reuse `experience.metadata` rather than re-merging, so resolvers and the\n // renderer read the same object.\n const plan: PortableRenderPlan = {\n nodes,\n diagnostics,\n metadata: experience.metadata,\n debug: experience.debug,\n };\n if (options.sourceMap !== undefined) plan.sourceMap = options.sourceMap;\n const profileId = isPlainPayload && payload.extensions?.personalization?.profile?.id;\n if (profileId) {\n plan.personalization = { profileId };\n }\n return plan;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/resolve-experience.ts"],"sourcesContent":["/*\n * Turns an XDA Experience payload into a runtime-neutral PortableRenderPlan.\n * Walks nodes recursively, splits content + design props, runs any registered\n * `resolveData` hooks in parallel, and resolves design values for the renderer.\n */\n\nimport { resolveAttribution } from './attribution.js';\nimport { createDebugLogger, type DebugLogger } from './debug-logger.js';\nimport type {\n DesignPropValue,\n ExperienceContext,\n ExperienceSourceMap,\n ExperienceNode,\n ExperiencePayload,\n PortableRegistration,\n PortableRenderNode,\n PortableRenderPlan,\n ResolveContext,\n ResolveToken,\n} from './types.js';\nimport { applyTokenResolver, resolveDesignProperties } from './design-properties.js';\n\n/**\n * Structural type the resolver walker depends on. Matches the React /\n * Svelte adapter `Config` shape but doesn't require importing them —\n * render-core stays decoupled from any framework.\n *\n * Registry values are typed as `unknown` because each adapter accepts\n * either a bare framework component (function / Svelte class / etc.) OR\n * a config-object shape with `{ component, defaults?, resolveData? }`.\n * The resolver only cares about `resolveData`; it duck-types each entry\n * at runtime and ignores anything without it.\n */\nexport interface ResolverConfig {\n components: Record<string, unknown>;\n experienceTemplates?: Record<string, unknown>;\n /**\n * Resolves `DesignToken` design properties to runtime values. Mirrors the\n * adapter `Config.resolveToken`, so server and client agree without the\n * caller re-supplying it. Used during server-side pre-resolution.\n */\n resolveToken?: ResolveToken;\n}\n\nfunction getResolver(\n entry: unknown\n):\n | ((ctx: ResolveContext) => Record<string, unknown> | Promise<Record<string, unknown>>)\n | undefined {\n if (typeof entry !== 'object' || entry === null) return undefined;\n const candidate = (entry as { resolveData?: unknown }).resolveData;\n return typeof candidate === 'function'\n ? (candidate as (\n ctx: ResolveContext\n ) => Record<string, unknown> | Promise<Record<string, unknown>>)\n : undefined;\n}\n\nexport interface ResolveExperienceOptions {\n /**\n * Arbitrary per-render metadata exposed to every resolver as\n * `ctx.experience.metadata`. Defaults to `{}`.\n */\n metadata?: Record<string, unknown>;\n /**\n * Observability switch. When on, `resolveExperience` logs the resolution\n * steps and per-node `resolveData` fan-out timings. Threads through to the\n * resolver context as `ctx.experience.debug`. Defaults to `false`.\n */\n debug?: boolean;\n /**\n * Source map carried onto the plan. An explicit map wins over\n * `payload.extensions.sourceMap`. Omit to use the payload's map, if it\n * carries one.\n */\n sourceMap?: ExperienceSourceMap;\n}\n\nconst DEFAULT_EXPERIENCE: ExperienceContext = {\n debug: false,\n metadata: {},\n};\n\n/**\n * Registry lookup for a built node. A node's `kind` decides which half of the\n * customer Config owns its implementation — components and Experience\n * Templates are otherwise interchangeable at every other step.\n */\nfunction lookupEntry(config: ResolverConfig, registration: PortableRegistration): unknown {\n return registration.kind === 'experienceTemplate'\n ? config.experienceTemplates?.[registration.id]\n : config.components[registration.id];\n}\n\n/**\n * Extract the id (component or experienceTemplate) from its\n * `ResourceLink` URN. Real URN shapes:\n * crn:contentful:::experience:spaces/$self/environments/$self/components/<id>\n * crn:contentful:::experience:spaces/$self/environments/$self/experienceTemplates/<id>\n *\n * The id is the final path segment. We split on `/` and take the last\n * non-empty piece so this also tolerates trailing slashes or alternative\n * prefix shapes.\n */\nfunction extractIdFromUrn(urn: string): string {\n const segments = urn.split('/').filter((s) => s.length > 0);\n return segments[segments.length - 1] ?? urn;\n}\n\n/**\n * Read the ResourceLink a node points at. Which key is present is the only\n * difference between the two node variants: `experienceTemplate` means the\n * implementation lives in the customer's `experienceTemplates` registry,\n * `component` in `components`. Both carry ids in the same URN shape, the same\n * props shape, and the same slots — so everything downstream is kind-agnostic.\n *\n * `ExperienceNode` is a closed union, so a typed caller cannot produce anything\n * else. Payloads, however, are untrusted JSON at runtime, and the realistic way\n * a third shape arrives is a node kind *newer than this SDK*. Returns `null`\n * for anything unidentifiable rather than letting the ref access throw — see\n * `buildNode`.\n */\nfunction readNodeRef(\n node: ExperienceNode\n): { kind: PortableRegistration['kind']; urn: string } | null {\n const isExperienceTemplate = 'experienceTemplate' in node;\n const ref = isExperienceTemplate\n ? node.experienceTemplate\n : 'component' in node\n ? node.component\n : undefined;\n // Cast because the runtime value may violate the declared type: a node can\n // carry `component: {}`, which satisfies `'component' in node` but has no urn.\n const urn = (ref as { sys?: { urn?: unknown } } | undefined)?.sys?.urn;\n if (typeof urn !== 'string' || urn.length === 0) return null;\n return { kind: isExperienceTemplate ? 'experienceTemplate' : 'component', urn };\n}\n\n/**\n * Guards a top-level payload field that must be an array. A malformed or\n * partial payload (bad JSON, a hand-authored payload with the wrong shape)\n * should degrade to an empty experience rather than throwing and failing the\n * whole render — same \"dropping beats throwing\" rationale as\n * `warnUnrenderableNode` below, just one level up the tree.\n */\nfunction ensureArray<T>(\n value: unknown,\n field: string,\n log: DebugLogger,\n diagnostics: Error[]\n): T[] {\n if (Array.isArray(value)) return value as T[];\n const message =\n `Experience payload's \"${field}\" is not an array (got ${value === null ? 'null' : typeof value}); ` +\n `treating it as empty instead of throwing. This usually means a malformed or partial payload — ` +\n `check what produced it.`;\n if (typeof console !== 'undefined') {\n console.warn(`[@contentful/experiences] ${message}`);\n }\n log.log(message);\n diagnostics.push(new Error(message));\n return [];\n}\n\n/** Total node count in a payload subtree, the node itself included. */\nfunction countPayloadNodes(node: ExperienceNode): number {\n const slots = (node as { slots?: Record<string, unknown> }).slots;\n let total = 1;\n if (slots && typeof slots === 'object') {\n for (const children of Object.values(slots)) {\n if (!Array.isArray(children)) continue;\n for (const child of children) total += countPayloadNodes(child as ExperienceNode);\n }\n }\n return total;\n}\n\n/**\n * The one case where a node is dropped. AIS-413 was the opposite failure —\n * a node kind the SDK recognized and could have rendered, skipped behind a\n * vague warning, taking its whole subtree with it — so the bar here is that\n * nothing vanishes without a diagnostic naming what was lost.\n *\n * Dropping beats throwing: a `resolveExperience` rejection fails the entire\n * experience, so one unrecognized node in a sidebar would take down every page\n * containing it, for a payload the customer does not control and cannot fix.\n */\nfunction warnUnrenderableNode(node: ExperienceNode, log: DebugLogger, diagnostics: Error[]): void {\n const id = (node as { id?: unknown }).id;\n const label = typeof id === 'string' ? ` \"${id}\"` : '';\n const keys = Object.keys(node);\n const descendants = countPayloadNodes(node) - 1;\n const message =\n `Skipping unidentifiable node${label}: expected a \\`component\\` or \\`experienceTemplate\\` ` +\n `ResourceLink carrying a urn, got keys [${keys.join(', ')}]. Dropping it and ${descendants} ` +\n `descendant node(s). A payload node kind this SDK does not know is usually a version skew — ` +\n `upgrading @contentful/experiences-sdk-core may be all that is needed.`;\n if (typeof console !== 'undefined') {\n console.warn(`[@contentful/experiences] ${message}`);\n }\n log.log(message);\n diagnostics.push(new Error(message));\n}\n\n/**\n * Walk a sibling list into IR nodes, dropping any node `buildNode` cannot\n * identify. Used for both the top-level list and every slot.\n */\nfunction buildNodes(\n nodes: ExperienceNode[],\n config: ResolverConfig,\n nodeRefs: PortableRenderNode[],\n log: DebugLogger,\n diagnostics: Error[]\n): PortableRenderNode[] {\n const built: PortableRenderNode[] = [];\n for (const node of nodes) {\n const one = buildNode(node, config, nodeRefs, log, diagnostics);\n if (one !== null) built.push(one);\n }\n return built;\n}\n\n/**\n * Recursively turn a payload node into an IR node. The collected `nodeRefs`\n * array is for the resolver pass — every built node with a registered\n * resolver gets a reference appended so we can run them in parallel without\n * walking the tree twice.\n *\n * Returns `null` only for a node whose ResourceLink cannot be read, which\n * `warnUnrenderableNode` has already reported. Callers go through `buildNodes`.\n */\nfunction buildNode(\n node: ExperienceNode,\n config: ResolverConfig,\n nodeRefs: PortableRenderNode[],\n log: DebugLogger,\n diagnostics: Error[]\n): PortableRenderNode | null {\n const ref = readNodeRef(node);\n if (ref === null) {\n warnUnrenderableNode(node, log, diagnostics);\n return null;\n }\n const registration: PortableRegistration = {\n kind: ref.kind,\n id: extractIdFromUrn(ref.urn),\n };\n const nodeId = typeof node.id === 'string' ? node.id : undefined;\n\n const slots: Record<string, PortableRenderNode[]> = {};\n if (node.slots) {\n for (const [slotName, children] of Object.entries(node.slots)) {\n if (!Array.isArray(children)) {\n const message =\n `Slot \"${slotName}\" on ${registration.kind} \"${registration.id}\"` +\n `${nodeId ? ` (node \"${nodeId}\")` : ''} is not an array of nodes (got ` +\n `${children === null ? 'null' : typeof children}); treating it as empty instead of ` +\n `failing the whole experience. A hand-built payload with the wrong slot shape is the ` +\n `usual cause.`;\n if (typeof console !== 'undefined') {\n console.warn(`[@contentful/experiences] ${message}`);\n }\n log.log(message);\n diagnostics.push(new Error(message));\n slots[slotName] = [];\n continue;\n }\n slots[slotName] = buildNodes(children, config, nodeRefs, log, diagnostics);\n }\n }\n\n const built: PortableRenderNode = {\n registration,\n props: {\n content: { ...(node.contentProperties ?? {}) },\n // Resolved design values are written by the pre-resolution pass below.\n design: {},\n designRaw: { ...(node.designProperties ?? {}) } as Record<string, DesignPropValue>,\n },\n slots,\n };\n if (node.id) built.nodeId = node.id;\n if (getResolver(lookupEntry(config, registration))) {\n nodeRefs.push(built);\n }\n return built;\n}\n\n// Resolve a node's raw design values and tokens.\nfunction preResolveDesignProperties(\n design: Record<string, DesignPropValue>,\n resolveToken: ResolveToken | undefined\n): { props: Record<string, unknown>; unresolved: string[] } {\n return applyTokenResolver(resolveDesignProperties(design), resolveToken);\n}\n\n// Warn when resolveToken left tokens unresolved, so pass-through keys are diagnosable.\nfunction warnUnresolvedTokens(\n label: string,\n unresolved: string[],\n log: DebugLogger,\n diagnostics: Error[]\n): void {\n if (!unresolved.length) return;\n const message = `resolveToken returned undefined for token id(s) on \"${label}\": ${unresolved.join(', ')}. Resolved design (getDesignValues()) will carry the raw, unresolved DesignToken for those keys.`;\n if (typeof console !== 'undefined') {\n console.warn(`[@contentful/experiences] ${message}`);\n }\n log.log(`unresolved token id(s) on \"${label}\": ${unresolved.join(', ')}`);\n for (const tokenId of unresolved) {\n diagnostics.push(\n new Error(\n `Design token \"${tokenId}\" on ${label} has no \\`resolveToken\\` mapping; the raw token reaches the component unresolved.`\n )\n );\n }\n}\n\n// Depth-first token resolution for a node and its slot children.\nfunction preResolveNodeTree(\n node: PortableRenderNode,\n resolveToken: ResolveToken | undefined,\n log: DebugLogger,\n diagnostics: Error[]\n): void {\n const { props, unresolved } = preResolveDesignProperties(node.props.designRaw, resolveToken);\n node.props.design = props;\n warnUnresolvedTokens(\n `${node.registration.kind}:${node.registration.id}`,\n unresolved,\n log,\n diagnostics\n );\n for (const children of Object.values(node.slots)) {\n for (const child of children) {\n preResolveNodeTree(child, resolveToken, log, diagnostics);\n }\n }\n}\n\n/**\n * Turns an Experience payload (XDA response shape) into a PortableRenderPlan\n * ready to hand to a renderer. Walks the tree, classifies props, captures\n * slots, and runs any component-declared `resolveData` hooks (sync or async)\n * in parallel.\n *\n * Implementation note: the function is always async — even when no component\n * declares a resolver, the cost is one microtask. Customers get a single\n * uniform call site.\n */\nexport async function resolveExperience(\n payload: ExperiencePayload,\n config: ResolverConfig,\n options: ResolveExperienceOptions = {}\n): Promise<PortableRenderPlan> {\n const log = createDebugLogger(options.debug, 'core');\n log.lazy('resolveExperience called with payload', () => payload);\n\n const diagnostics: Error[] = [];\n\n // A payload that isn't an object at all (missing fetch result, a raw\n // `null`/`undefined` handed in by a broken transform step) can't be\n // guarded field-by-field via `ensureArray` below — that only guards once\n // we're already inside a `payload.nodes` access. Guard the whole payload\n // first so we degrade to an empty experience instead of throwing a raw\n // TypeError, and report exactly one diagnostic rather than double-counting\n // once we also run `nodes` through `ensureArray`.\n const isPlainPayload = payload !== null && typeof payload === 'object';\n if (!isPlainPayload) {\n const message =\n `Experience payload is ${payload === null ? 'null' : typeof payload}, not an object; ` +\n `treating it as an empty experience (no nodes) instead of throwing. This ` +\n `usually means the fetch or transform pipeline handed resolveExperience a missing or ` +\n `malformed payload — check what produced it.`;\n if (typeof console !== 'undefined') {\n console.warn(`[@contentful/experiences] ${message}`);\n }\n log.log(message);\n diagnostics.push(new Error(message));\n }\n const payloadNodes = isPlainPayload\n ? ensureArray<ExperienceNode>(payload.nodes, 'nodes', log, diagnostics)\n : [];\n // Pass 1: walk the payload into the IR. Collect refs to nodes that need\n // resolveData so pass 2 can run them in parallel without re-walking.\n const nodeRefs: PortableRenderNode[] = [];\n const nodes: PortableRenderNode[] = buildNodes(payloadNodes, config, nodeRefs, log, diagnostics);\n log.log(`built ${nodes.length} top-level node(s); ${nodeRefs.length} declare resolveData`);\n\n // Pass 2: run every node's resolveData hook in parallel.\n const experience: ExperienceContext = {\n debug: options.debug ?? DEFAULT_EXPERIENCE.debug,\n metadata: {\n ...DEFAULT_EXPERIENCE.metadata,\n ...(options.metadata ?? {}),\n },\n };\n\n const tasks: Array<Promise<void>> = [];\n\n for (const node of nodeRefs) {\n const resolver = getResolver(lookupEntry(config, node.registration));\n if (!resolver) continue;\n const ctx: ResolveContext = {\n content: node.props.content,\n design: node.props.designRaw,\n experience,\n };\n tasks.push(\n // Deferred via `.then(() => resolver(ctx))` rather than\n // `Promise.resolve(resolver(ctx))`: the latter calls `resolver(ctx)`\n // eagerly as an argument, so a resolver that throws *synchronously*\n // escapes immediately instead of becoming a rejection. The `onFailure`\n // handler below never rethrows, so one broken resolver can't fail the\n // `Promise.all` for every other node.\n Promise.resolve()\n .then(() => resolver(ctx))\n .then(\n (resolved) => {\n node.props.resolved = resolved;\n },\n (err: unknown) => {\n const label = `${node.registration.kind}:${node.registration.id}`;\n const reason = err instanceof Error ? err.message : String(err);\n const message =\n `resolveData for ${label}${node.nodeId ? ` (node \"${node.nodeId}\")` : ''} threw or ` +\n `rejected: ${reason}. Rendering the node without resolved data instead of failing ` +\n `the whole experience.`;\n if (typeof console !== 'undefined') {\n console.warn(`[@contentful/experiences] ${message}`);\n }\n log.log(message);\n diagnostics.push(new Error(message, { cause: err instanceof Error ? err : undefined }));\n }\n )\n );\n }\n\n // Time the fan-out as a whole rather than per-resolver — one aggregate line\n // keeps the timing signal without a line per node (which gets noisy fast).\n if (tasks.length > 0) {\n await log.time(`${tasks.length} resolveData hook(s)`, () => Promise.all(tasks));\n }\n\n for (const node of nodes) {\n preResolveNodeTree(node, config.resolveToken, log, diagnostics);\n }\n log.log('resolved design properties');\n\n // Reuse `experience.metadata` rather than re-merging, so resolvers and the\n // renderer read the same object.\n const plan: PortableRenderPlan = {\n nodes,\n diagnostics,\n metadata: experience.metadata,\n debug: experience.debug,\n };\n const sourceMap =\n options.sourceMap ?? (isPlainPayload ? payload.extensions?.sourceMap : undefined);\n if (sourceMap) {\n plan.sourceMap = sourceMap;\n const attribution = resolveAttribution(sourceMap, nodes, log);\n if (attribution) plan.attribution = attribution;\n }\n const profileId = isPlainPayload && payload.extensions?.personalization?.profile?.id;\n if (profileId) {\n plan.personalization = { profileId };\n }\n return plan;\n}\n"],"mappings":"AAMA,SAAS,0BAA0B;AACnC,SAAS,yBAA2C;AAapD,SAAS,oBAAoB,+BAA+B;AAwB5D,SAAS,YACP,OAGY;AACZ,MAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AACxD,QAAM,YAAa,MAAoC;AACvD,SAAO,OAAO,cAAc,aACvB,YAGD;AACN;AAsBA,MAAM,qBAAwC;AAAA,EAC5C,OAAO;AAAA,EACP,UAAU,CAAC;AACb;AAOA,SAAS,YAAY,QAAwB,cAA6C;AACxF,SAAO,aAAa,SAAS,uBACzB,OAAO,sBAAsB,aAAa,EAAE,IAC5C,OAAO,WAAW,aAAa,EAAE;AACvC;AAYA,SAAS,iBAAiB,KAAqB;AAC7C,QAAM,WAAW,IAAI,MAAM,GAAG,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC;AAC1D,SAAO,SAAS,SAAS,SAAS,CAAC,KAAK;AAC1C;AAeA,SAAS,YACP,MAC4D;AAC5D,QAAM,uBAAuB,wBAAwB;AACrD,QAAM,MAAM,uBACR,KAAK,qBACL,eAAe,OACb,KAAK,YACL;AAGN,QAAM,MAAO,KAAiD,KAAK;AACnE,MAAI,OAAO,QAAQ,YAAY,IAAI,WAAW,EAAG,QAAO;AACxD,SAAO,EAAE,MAAM,uBAAuB,uBAAuB,aAAa,IAAI;AAChF;AASA,SAAS,YACP,OACA,OACA,KACA,aACK;AACL,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO;AACjC,QAAM,UACJ,yBAAyB,KAAK,0BAA0B,UAAU,OAAO,SAAS,OAAO,KAAK;AAGhG,MAAI,OAAO,YAAY,aAAa;AAClC,YAAQ,KAAK,6BAA6B,OAAO,EAAE;AAAA,EACrD;AACA,MAAI,IAAI,OAAO;AACf,cAAY,KAAK,IAAI,MAAM,OAAO,CAAC;AACnC,SAAO,CAAC;AACV;AAGA,SAAS,kBAAkB,MAA8B;AACvD,QAAM,QAAS,KAA6C;AAC5D,MAAI,QAAQ;AACZ,MAAI,SAAS,OAAO,UAAU,UAAU;AACtC,eAAW,YAAY,OAAO,OAAO,KAAK,GAAG;AAC3C,UAAI,CAAC,MAAM,QAAQ,QAAQ,EAAG;AAC9B,iBAAW,SAAS,SAAU,UAAS,kBAAkB,KAAuB;AAAA,IAClF;AAAA,EACF;AACA,SAAO;AACT;AAYA,SAAS,qBAAqB,MAAsB,KAAkB,aAA4B;AAChG,QAAM,KAAM,KAA0B;AACtC,QAAM,QAAQ,OAAO,OAAO,WAAW,KAAK,EAAE,MAAM;AACpD,QAAM,OAAO,OAAO,KAAK,IAAI;AAC7B,QAAM,cAAc,kBAAkB,IAAI,IAAI;AAC9C,QAAM,UACJ,+BAA+B,KAAK,+FACM,KAAK,KAAK,IAAI,CAAC,sBAAsB,WAAW;AAG5F,MAAI,OAAO,YAAY,aAAa;AAClC,YAAQ,KAAK,6BAA6B,OAAO,EAAE;AAAA,EACrD;AACA,MAAI,IAAI,OAAO;AACf,cAAY,KAAK,IAAI,MAAM,OAAO,CAAC;AACrC;AAMA,SAAS,WACP,OACA,QACA,UACA,KACA,aACsB;AACtB,QAAM,QAA8B,CAAC;AACrC,aAAW,QAAQ,OAAO;AACxB,UAAM,MAAM,UAAU,MAAM,QAAQ,UAAU,KAAK,WAAW;AAC9D,QAAI,QAAQ,KAAM,OAAM,KAAK,GAAG;AAAA,EAClC;AACA,SAAO;AACT;AAWA,SAAS,UACP,MACA,QACA,UACA,KACA,aAC2B;AAC3B,QAAM,MAAM,YAAY,IAAI;AAC5B,MAAI,QAAQ,MAAM;AAChB,yBAAqB,MAAM,KAAK,WAAW;AAC3C,WAAO;AAAA,EACT;AACA,QAAM,eAAqC;AAAA,IACzC,MAAM,IAAI;AAAA,IACV,IAAI,iBAAiB,IAAI,GAAG;AAAA,EAC9B;AACA,QAAM,SAAS,OAAO,KAAK,OAAO,WAAW,KAAK,KAAK;AAEvD,QAAM,QAA8C,CAAC;AACrD,MAAI,KAAK,OAAO;AACd,eAAW,CAAC,UAAU,QAAQ,KAAK,OAAO,QAAQ,KAAK,KAAK,GAAG;AAC7D,UAAI,CAAC,MAAM,QAAQ,QAAQ,GAAG;AAC5B,cAAM,UACJ,SAAS,QAAQ,QAAQ,aAAa,IAAI,KAAK,aAAa,EAAE,IAC3D,SAAS,WAAW,MAAM,OAAO,EAAE,kCACnC,aAAa,OAAO,SAAS,OAAO,QAAQ;AAGjD,YAAI,OAAO,YAAY,aAAa;AAClC,kBAAQ,KAAK,6BAA6B,OAAO,EAAE;AAAA,QACrD;AACA,YAAI,IAAI,OAAO;AACf,oBAAY,KAAK,IAAI,MAAM,OAAO,CAAC;AACnC,cAAM,QAAQ,IAAI,CAAC;AACnB;AAAA,MACF;AACA,YAAM,QAAQ,IAAI,WAAW,UAAU,QAAQ,UAAU,KAAK,WAAW;AAAA,IAC3E;AAAA,EACF;AAEA,QAAM,QAA4B;AAAA,IAChC;AAAA,IACA,OAAO;AAAA,MACL,SAAS,EAAE,GAAI,KAAK,qBAAqB,CAAC,EAAG;AAAA;AAAA,MAE7C,QAAQ,CAAC;AAAA,MACT,WAAW,EAAE,GAAI,KAAK,oBAAoB,CAAC,EAAG;AAAA,IAChD;AAAA,IACA;AAAA,EACF;AACA,MAAI,KAAK,GAAI,OAAM,SAAS,KAAK;AACjC,MAAI,YAAY,YAAY,QAAQ,YAAY,CAAC,GAAG;AAClD,aAAS,KAAK,KAAK;AAAA,EACrB;AACA,SAAO;AACT;AAGA,SAAS,2BACP,QACA,cAC0D;AAC1D,SAAO,mBAAmB,wBAAwB,MAAM,GAAG,YAAY;AACzE;AAGA,SAAS,qBACP,OACA,YACA,KACA,aACM;AACN,MAAI,CAAC,WAAW,OAAQ;AACxB,QAAM,UAAU,uDAAuD,KAAK,MAAM,WAAW,KAAK,IAAI,CAAC;AACvG,MAAI,OAAO,YAAY,aAAa;AAClC,YAAQ,KAAK,6BAA6B,OAAO,EAAE;AAAA,EACrD;AACA,MAAI,IAAI,8BAA8B,KAAK,MAAM,WAAW,KAAK,IAAI,CAAC,EAAE;AACxE,aAAW,WAAW,YAAY;AAChC,gBAAY;AAAA,MACV,IAAI;AAAA,QACF,iBAAiB,OAAO,QAAQ,KAAK;AAAA,MACvC;AAAA,IACF;AAAA,EACF;AACF;AAGA,SAAS,mBACP,MACA,cACA,KACA,aACM;AACN,QAAM,EAAE,OAAO,WAAW,IAAI,2BAA2B,KAAK,MAAM,WAAW,YAAY;AAC3F,OAAK,MAAM,SAAS;AACpB;AAAA,IACE,GAAG,KAAK,aAAa,IAAI,IAAI,KAAK,aAAa,EAAE;AAAA,IACjD;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,aAAW,YAAY,OAAO,OAAO,KAAK,KAAK,GAAG;AAChD,eAAW,SAAS,UAAU;AAC5B,yBAAmB,OAAO,cAAc,KAAK,WAAW;AAAA,IAC1D;AAAA,EACF;AACF;AAYA,eAAsB,kBACpB,SACA,QACA,UAAoC,CAAC,GACR;AAC7B,QAAM,MAAM,kBAAkB,QAAQ,OAAO,MAAM;AACnD,MAAI,KAAK,yCAAyC,MAAM,OAAO;AAE/D,QAAM,cAAuB,CAAC;AAS9B,QAAM,iBAAiB,YAAY,QAAQ,OAAO,YAAY;AAC9D,MAAI,CAAC,gBAAgB;AACnB,UAAM,UACJ,yBAAyB,YAAY,OAAO,SAAS,OAAO,OAAO;AAIrE,QAAI,OAAO,YAAY,aAAa;AAClC,cAAQ,KAAK,6BAA6B,OAAO,EAAE;AAAA,IACrD;AACA,QAAI,IAAI,OAAO;AACf,gBAAY,KAAK,IAAI,MAAM,OAAO,CAAC;AAAA,EACrC;AACA,QAAM,eAAe,iBACjB,YAA4B,QAAQ,OAAO,SAAS,KAAK,WAAW,IACpE,CAAC;AAGL,QAAM,WAAiC,CAAC;AACxC,QAAM,QAA8B,WAAW,cAAc,QAAQ,UAAU,KAAK,WAAW;AAC/F,MAAI,IAAI,SAAS,MAAM,MAAM,uBAAuB,SAAS,MAAM,sBAAsB;AAGzF,QAAM,aAAgC;AAAA,IACpC,OAAO,QAAQ,SAAS,mBAAmB;AAAA,IAC3C,UAAU;AAAA,MACR,GAAG,mBAAmB;AAAA,MACtB,GAAI,QAAQ,YAAY,CAAC;AAAA,IAC3B;AAAA,EACF;AAEA,QAAM,QAA8B,CAAC;AAErC,aAAW,QAAQ,UAAU;AAC3B,UAAM,WAAW,YAAY,YAAY,QAAQ,KAAK,YAAY,CAAC;AACnE,QAAI,CAAC,SAAU;AACf,UAAM,MAAsB;AAAA,MAC1B,SAAS,KAAK,MAAM;AAAA,MACpB,QAAQ,KAAK,MAAM;AAAA,MACnB;AAAA,IACF;AACA,UAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAOJ,QAAQ,QAAQ,EACb,KAAK,MAAM,SAAS,GAAG,CAAC,EACxB;AAAA,QACC,CAAC,aAAa;AACZ,eAAK,MAAM,WAAW;AAAA,QACxB;AAAA,QACA,CAAC,QAAiB;AAChB,gBAAM,QAAQ,GAAG,KAAK,aAAa,IAAI,IAAI,KAAK,aAAa,EAAE;AAC/D,gBAAM,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC9D,gBAAM,UACJ,mBAAmB,KAAK,GAAG,KAAK,SAAS,WAAW,KAAK,MAAM,OAAO,EAAE,uBAC3D,MAAM;AAErB,cAAI,OAAO,YAAY,aAAa;AAClC,oBAAQ,KAAK,6BAA6B,OAAO,EAAE;AAAA,UACrD;AACA,cAAI,IAAI,OAAO;AACf,sBAAY,KAAK,IAAI,MAAM,SAAS,EAAE,OAAO,eAAe,QAAQ,MAAM,OAAU,CAAC,CAAC;AAAA,QACxF;AAAA,MACF;AAAA,IACJ;AAAA,EACF;AAIA,MAAI,MAAM,SAAS,GAAG;AACpB,UAAM,IAAI,KAAK,GAAG,MAAM,MAAM,wBAAwB,MAAM,QAAQ,IAAI,KAAK,CAAC;AAAA,EAChF;AAEA,aAAW,QAAQ,OAAO;AACxB,uBAAmB,MAAM,OAAO,cAAc,KAAK,WAAW;AAAA,EAChE;AACA,MAAI,IAAI,4BAA4B;AAIpC,QAAM,OAA2B;AAAA,IAC/B;AAAA,IACA;AAAA,IACA,UAAU,WAAW;AAAA,IACrB,OAAO,WAAW;AAAA,EACpB;AACA,QAAM,YACJ,QAAQ,cAAc,iBAAiB,QAAQ,YAAY,YAAY;AACzE,MAAI,WAAW;AACb,SAAK,YAAY;AACjB,UAAM,cAAc,mBAAmB,WAAW,OAAO,GAAG;AAC5D,QAAI,YAAa,MAAK,cAAc;AAAA,EACtC;AACA,QAAM,YAAY,kBAAkB,QAAQ,YAAY,iBAAiB,SAAS;AAClF,MAAI,WAAW;AACb,SAAK,kBAAkB,EAAE,UAAU;AAAA,EACrC;AACA,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lists the occurrence keys of the scopes an element roots, space-separated.
|
|
3
|
+
* Put it on the outermost element a node renders. The keys are opaque: they
|
|
4
|
+
* resolve through `plan.attribution.scopes`, so no attribution lives in the DOM.
|
|
5
|
+
*/
|
|
6
|
+
declare const TRACKING_SCOPES_ATTRIBUTE = "data-ctfl-scopes";
|
|
7
|
+
/**
|
|
8
|
+
* Opts an element into click tracking when it is not otherwise interactive.
|
|
9
|
+
* Links, buttons, form controls, `[role="button"]`, `[role="link"]`, and
|
|
10
|
+
* elements with an `onclick` handler count already. Set it to `"true"`.
|
|
11
|
+
*/
|
|
12
|
+
declare const TRACKING_CLICKABLE_ATTRIBUTE = "data-ctfl-clickable";
|
|
13
|
+
interface TrackingAttributes {
|
|
14
|
+
[TRACKING_SCOPES_ATTRIBUTE]?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Attributes to spread onto a tracked node's outermost element, built from the
|
|
18
|
+
* `attribution` a component context accessor returns. Returns `{}` when the
|
|
19
|
+
* node roots no scope: nothing is rendered for it, so a node in the middle of a
|
|
20
|
+
* scope, or one without attribution, is left alone.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```tsx
|
|
24
|
+
* const contentful = useContentfulComponent();
|
|
25
|
+
* <section {...getTrackingAttributes(contentful?.attribution)}>…</section>
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
declare function getTrackingAttributes(attribution: {
|
|
29
|
+
roots: ReadonlyArray<{
|
|
30
|
+
key: string;
|
|
31
|
+
}>;
|
|
32
|
+
} | undefined): TrackingAttributes;
|
|
33
|
+
|
|
34
|
+
export { TRACKING_CLICKABLE_ATTRIBUTE, TRACKING_SCOPES_ATTRIBUTE, type TrackingAttributes, getTrackingAttributes };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const TRACKING_SCOPES_ATTRIBUTE = "data-ctfl-scopes";
|
|
2
|
+
const TRACKING_CLICKABLE_ATTRIBUTE = "data-ctfl-clickable";
|
|
3
|
+
function getTrackingAttributes(attribution) {
|
|
4
|
+
const keys = attribution?.roots.map((scope) => scope.key) ?? [];
|
|
5
|
+
return keys.length > 0 ? { [TRACKING_SCOPES_ATTRIBUTE]: keys.join(" ") } : {};
|
|
6
|
+
}
|
|
7
|
+
export {
|
|
8
|
+
TRACKING_CLICKABLE_ATTRIBUTE,
|
|
9
|
+
TRACKING_SCOPES_ATTRIBUTE,
|
|
10
|
+
getTrackingAttributes
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=tracking-attributes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tracking-attributes.ts"],"sourcesContent":["/**\n * Lists the occurrence keys of the scopes an element roots, space-separated.\n * Put it on the outermost element a node renders. The keys are opaque: they\n * resolve through `plan.attribution.scopes`, so no attribution lives in the DOM.\n */\nexport const TRACKING_SCOPES_ATTRIBUTE = 'data-ctfl-scopes';\n\n/**\n * Opts an element into click tracking when it is not otherwise interactive.\n * Links, buttons, form controls, `[role=\"button\"]`, `[role=\"link\"]`, and\n * elements with an `onclick` handler count already. Set it to `\"true\"`.\n */\nexport const TRACKING_CLICKABLE_ATTRIBUTE = 'data-ctfl-clickable';\n\nexport interface TrackingAttributes {\n [TRACKING_SCOPES_ATTRIBUTE]?: string;\n}\n\n/**\n * Attributes to spread onto a tracked node's outermost element, built from the\n * `attribution` a component context accessor returns. Returns `{}` when the\n * node roots no scope: nothing is rendered for it, so a node in the middle of a\n * scope, or one without attribution, is left alone.\n *\n * @example\n * ```tsx\n * const contentful = useContentfulComponent();\n * <section {...getTrackingAttributes(contentful?.attribution)}>…</section>\n * ```\n */\nexport function getTrackingAttributes(\n attribution: { roots: ReadonlyArray<{ key: string }> } | undefined\n): TrackingAttributes {\n const keys = attribution?.roots.map((scope) => scope.key) ?? [];\n return keys.length > 0 ? { [TRACKING_SCOPES_ATTRIBUTE]: keys.join(' ') } : {};\n}\n"],"mappings":"AAKO,MAAM,4BAA4B;AAOlC,MAAM,+BAA+B;AAkBrC,SAAS,sBACd,aACoB;AACpB,QAAM,OAAO,aAAa,MAAM,IAAI,CAAC,UAAU,MAAM,GAAG,KAAK,CAAC;AAC9D,SAAO,KAAK,SAAS,IAAI,EAAE,CAAC,yBAAyB,GAAG,KAAK,KAAK,GAAG,EAAE,IAAI,CAAC;AAC9E;","names":[]}
|
package/dist/types.d.ts
CHANGED
|
@@ -210,6 +210,11 @@ interface PortableRegistration {
|
|
|
210
210
|
* preserves the source values for `resolveData` and payload context helpers.
|
|
211
211
|
*/
|
|
212
212
|
interface PortableRenderNode {
|
|
213
|
+
/**
|
|
214
|
+
* Reportable scopes this node belongs to. Present only when the plan carries
|
|
215
|
+
* a usable source map and the node's id is in it.
|
|
216
|
+
*/
|
|
217
|
+
attribution?: NodeAttribution;
|
|
213
218
|
/**
|
|
214
219
|
* Optional. Passed through from the XDA payload's `id` field when the
|
|
215
220
|
* editor supplies one. The SDK does NOT auto-generate ids; adapters fall
|
|
@@ -233,6 +238,38 @@ interface PortableRenderNode {
|
|
|
233
238
|
*/
|
|
234
239
|
slots: Record<string, PortableRenderNode[]>;
|
|
235
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* One rendered occurrence of an Experience or persisted Fragment. Field names
|
|
243
|
+
* follow the ExO event shape, so it stays structurally assignable to the
|
|
244
|
+
* client's interaction-event builder args. Optional fields are omitted, never
|
|
245
|
+
* invented: the baseline variant carries no `variantId`, `variantIndex` or
|
|
246
|
+
* `optimizationId`.
|
|
247
|
+
*/
|
|
248
|
+
interface ScopeAttribution {
|
|
249
|
+
/** Opaque, plan-local occurrence key. Never parse it. */
|
|
250
|
+
key: string;
|
|
251
|
+
entityId: string;
|
|
252
|
+
entityKind: 'Experience' | 'Fragment';
|
|
253
|
+
entityKindId?: string;
|
|
254
|
+
optimizationId?: string;
|
|
255
|
+
variantId?: string;
|
|
256
|
+
variantIndex?: number;
|
|
257
|
+
parentExperienceId?: string;
|
|
258
|
+
entryIds?: string[];
|
|
259
|
+
}
|
|
260
|
+
/** A node's own view of the scopes it belongs to. Plain data, safe to serialize. */
|
|
261
|
+
interface NodeAttribution {
|
|
262
|
+
/** Every reportable scope the node belongs to, outer to inner. Shared with `plan.attribution.scopes` and frozen. */
|
|
263
|
+
scopes: ScopeAttribution[];
|
|
264
|
+
/** The scopes this node is a top-level node of, i.e. its parent is not in them. */
|
|
265
|
+
roots: ScopeAttribution[];
|
|
266
|
+
/** Entries this node's own content properties are bound to. */
|
|
267
|
+
entryIds?: string[];
|
|
268
|
+
}
|
|
269
|
+
/** Occurrence-keyed lookup of every reportable scope in a plan. */
|
|
270
|
+
interface PlanAttribution {
|
|
271
|
+
scopes: Record<string, ScopeAttribution>;
|
|
272
|
+
}
|
|
236
273
|
/**
|
|
237
274
|
* The interpreted experience tree.
|
|
238
275
|
*
|
|
@@ -254,6 +291,8 @@ interface PortableRenderPlan {
|
|
|
254
291
|
debug: boolean;
|
|
255
292
|
/** Present only when the fetch requested the `sourceMap` extension. */
|
|
256
293
|
sourceMap?: ExperienceSourceMap;
|
|
294
|
+
/** Reportable scopes resolved from `sourceMap`. Absent when there is no usable map. */
|
|
295
|
+
attribution?: PlanAttribution;
|
|
257
296
|
/** Present when XDA returned personalization state for this Experience. */
|
|
258
297
|
personalization?: {
|
|
259
298
|
/** Profile id to use for subsequent personalized requests. */
|
|
@@ -277,4 +316,4 @@ interface PortableRenderPlan {
|
|
|
277
316
|
diagnostics: Error[];
|
|
278
317
|
}
|
|
279
318
|
|
|
280
|
-
export type { ComponentNode, ComponentRef, DesignPropValue, DesignToken, ExperienceContext, ExperienceExtensions, ExperienceNode, ExperiencePayload, ExperienceSourceMap, ExperienceSys, ExperienceTemplateNode, ExperienceTemplateRef, ManualDesignValue, PortableRegistration, PortableRenderNode, PortableRenderPlan, RenderContext, ResolveContext, ResolveToken };
|
|
319
|
+
export type { ComponentNode, ComponentRef, DesignPropValue, DesignToken, ExperienceContext, ExperienceExtensions, ExperienceNode, ExperiencePayload, ExperienceSourceMap, ExperienceSys, ExperienceTemplateNode, ExperienceTemplateRef, ManualDesignValue, NodeAttribution, PlanAttribution, PortableRegistration, PortableRenderNode, PortableRenderPlan, RenderContext, ResolveContext, ResolveToken, ScopeAttribution };
|