@contentful/experiences-sdk-core 0.7.10 → 0.7.11

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 CHANGED
@@ -1,3 +1,9 @@
1
+ ## 0.7.11 (2026-08-28)
2
+
3
+ ### 🚀 Features
4
+
5
+ - error handling and sensible fallbacks for non-happy-path rendering [AIS-364] ([#156](https://github.com/contentful/experiences/pull/156))
6
+
1
7
  ## 0.7.10 (2026-08-27)
2
8
 
3
9
  This was a version bump only for core to align it with other projects, there were no code changes.
@@ -24,6 +24,16 @@ function readNodeRef(node) {
24
24
  if (typeof urn !== "string" || urn.length === 0) return null;
25
25
  return { kind: isExperienceTemplate ? "experienceTemplate" : "component", urn };
26
26
  }
27
+ function ensureArray(value, field, log, diagnostics) {
28
+ if (Array.isArray(value)) return value;
29
+ const message = `Experience payload's "${field}" is not an array (got ${value === null ? "null" : typeof value}); treating it as empty instead of throwing. This usually means a malformed or partial payload \u2014 check what produced it.`;
30
+ if (typeof console !== "undefined") {
31
+ console.warn(`[@contentful/experiences] ${message}`);
32
+ }
33
+ log.log(message);
34
+ diagnostics.push(new Error(message));
35
+ return [];
36
+ }
27
37
  function countPayloadNodes(node) {
28
38
  const slots = node.slots;
29
39
  let total = 1;
@@ -35,7 +45,7 @@ function countPayloadNodes(node) {
35
45
  }
36
46
  return total;
37
47
  }
38
- function warnUnrenderableNode(node, log) {
48
+ function warnUnrenderableNode(node, log, diagnostics) {
39
49
  const id = node.id;
40
50
  const label = typeof id === "string" ? ` "${id}"` : "";
41
51
  const keys = Object.keys(node);
@@ -45,34 +55,41 @@ function warnUnrenderableNode(node, log) {
45
55
  console.warn(`[@contentful/experiences] ${message}`);
46
56
  }
47
57
  log.log(message);
58
+ diagnostics.push(new Error(message));
48
59
  }
49
- function buildNodes(nodes, config, nodeRefs, log) {
60
+ function buildNodes(nodes, config, nodeRefs, log, diagnostics) {
50
61
  const built = [];
51
62
  for (const node of nodes) {
52
- const one = buildNode(node, config, nodeRefs, log);
63
+ const one = buildNode(node, config, nodeRefs, log, diagnostics);
53
64
  if (one !== null) built.push(one);
54
65
  }
55
66
  return built;
56
67
  }
57
- function buildNode(node, config, nodeRefs, log) {
68
+ function buildNode(node, config, nodeRefs, log, diagnostics) {
58
69
  const ref = readNodeRef(node);
59
70
  if (ref === null) {
60
- warnUnrenderableNode(node, log);
71
+ warnUnrenderableNode(node, log, diagnostics);
61
72
  return null;
62
73
  }
63
74
  const registration = {
64
75
  kind: ref.kind,
65
76
  id: extractIdFromUrn(ref.urn)
66
77
  };
78
+ const nodeId = typeof node.id === "string" ? node.id : void 0;
67
79
  const slots = {};
68
80
  if (node.slots) {
69
81
  for (const [slotName, children] of Object.entries(node.slots)) {
70
82
  if (!Array.isArray(children)) {
71
- throw new TypeError(
72
- `Slot "${slotName}" on ${registration.kind} "${registration.id}" must be an array of nodes.`
73
- );
83
+ const message = `Slot "${slotName}" on ${registration.kind} "${registration.id}"${nodeId ? ` (node "${nodeId}")` : ""} is not an array of nodes (got ${children === null ? "null" : typeof children}); treating it as empty instead of failing the whole experience. A hand-built payload with the wrong slot shape is the usual cause.`;
84
+ if (typeof console !== "undefined") {
85
+ console.warn(`[@contentful/experiences] ${message}`);
86
+ }
87
+ log.log(message);
88
+ diagnostics.push(new Error(message));
89
+ slots[slotName] = [];
90
+ continue;
74
91
  }
75
- slots[slotName] = buildNodes(children, config, nodeRefs, log);
92
+ slots[slotName] = buildNodes(children, config, nodeRefs, log, diagnostics);
76
93
  }
77
94
  }
78
95
  const built = {
@@ -95,14 +112,22 @@ function preResolveDesignProperties(design, viewports, fallbackViewportIndex, re
95
112
  const cascaded = resolveDesignProperties(design, viewports, fallbackViewportIndex);
96
113
  return applyTokenResolver(cascaded, resolveToken);
97
114
  }
98
- function warnUnresolvedTokens(label, unresolved, log) {
99
- if (!unresolved.length || typeof console === "undefined") return;
100
- console.warn(
101
- `[@contentful/experiences] resolveToken returned undefined for token id(s) on "${label}": ${unresolved.join(", ")}. Resolved design (getDesignValues()) will omit those keys.`
102
- );
115
+ function warnUnresolvedTokens(label, unresolved, log, diagnostics) {
116
+ if (!unresolved.length) return;
117
+ 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.`;
118
+ if (typeof console !== "undefined") {
119
+ console.warn(`[@contentful/experiences] ${message}`);
120
+ }
103
121
  log.log(`unresolved token id(s) on "${label}": ${unresolved.join(", ")}`);
122
+ for (const tokenId of unresolved) {
123
+ diagnostics.push(
124
+ new Error(
125
+ `Design token "${tokenId}" on ${label} has no \`resolveToken\` mapping; the raw token reaches the component unresolved.`
126
+ )
127
+ );
128
+ }
104
129
  }
105
- function preResolveNodeTree(node, viewports, fallbackViewportIndex, resolveToken, log) {
130
+ function preResolveNodeTree(node, viewports, fallbackViewportIndex, resolveToken, log, diagnostics) {
106
131
  const { props, unresolved } = preResolveDesignProperties(
107
132
  node.props.designRaw,
108
133
  viewports,
@@ -110,18 +135,35 @@ function preResolveNodeTree(node, viewports, fallbackViewportIndex, resolveToken
110
135
  resolveToken
111
136
  );
112
137
  node.props.design = props;
113
- warnUnresolvedTokens(`${node.registration.kind}:${node.registration.id}`, unresolved, log);
138
+ warnUnresolvedTokens(
139
+ `${node.registration.kind}:${node.registration.id}`,
140
+ unresolved,
141
+ log,
142
+ diagnostics
143
+ );
114
144
  for (const children of Object.values(node.slots)) {
115
145
  for (const child of children) {
116
- preResolveNodeTree(child, viewports, fallbackViewportIndex, resolveToken, log);
146
+ preResolveNodeTree(child, viewports, fallbackViewportIndex, resolveToken, log, diagnostics);
117
147
  }
118
148
  }
119
149
  }
120
150
  async function resolveExperience(payload, config, options = {}) {
121
151
  const log = createDebugLogger(options.debug, "core");
122
152
  log.lazy("resolveExperience called with payload", () => payload);
153
+ const diagnostics = [];
154
+ const isPlainPayload = payload !== null && typeof payload === "object";
155
+ if (!isPlainPayload) {
156
+ const message = `Experience payload is ${payload === null ? "null" : typeof payload}, not an object; treating it as an empty experience (no nodes, no viewports) instead of throwing. This usually means the fetch or transform pipeline handed resolveExperience a missing or malformed payload \u2014 check what produced it.`;
157
+ if (typeof console !== "undefined") {
158
+ console.warn(`[@contentful/experiences] ${message}`);
159
+ }
160
+ log.log(message);
161
+ diagnostics.push(new Error(message));
162
+ }
163
+ const payloadNodes = isPlainPayload ? ensureArray(payload.nodes, "nodes", log, diagnostics) : [];
164
+ const viewports = isPlainPayload ? ensureArray(payload.viewports, "viewports", log, diagnostics) : [];
123
165
  const nodeRefs = [];
124
- const nodes = buildNodes(payload.nodes, config, nodeRefs, log);
166
+ const nodes = buildNodes(payloadNodes, config, nodeRefs, log, diagnostics);
125
167
  log.log(`built ${nodes.length} top-level node(s); ${nodeRefs.length} declare resolveData`);
126
168
  const experience = {
127
169
  debug: options.debug ?? DEFAULT_EXPERIENCE.debug,
@@ -129,7 +171,7 @@ async function resolveExperience(payload, config, options = {}) {
129
171
  ...DEFAULT_EXPERIENCE.metadata,
130
172
  ...options.metadata ?? {}
131
173
  },
132
- viewports: payload.viewports
174
+ viewports
133
175
  };
134
176
  const tasks = [];
135
177
  for (const node of nodeRefs) {
@@ -141,24 +183,50 @@ async function resolveExperience(payload, config, options = {}) {
141
183
  experience
142
184
  };
143
185
  tasks.push(
144
- Promise.resolve(resolver(ctx)).then((resolved) => {
145
- node.props.resolved = resolved;
146
- })
186
+ // Deferred via `.then(() => resolver(ctx))` rather than
187
+ // `Promise.resolve(resolver(ctx))`: the latter calls `resolver(ctx)`
188
+ // eagerly as an argument, so a resolver that throws *synchronously*
189
+ // escapes immediately instead of becoming a rejection. The `onFailure`
190
+ // handler below never rethrows, so one broken resolver can't fail the
191
+ // `Promise.all` for every other node.
192
+ Promise.resolve().then(() => resolver(ctx)).then(
193
+ (resolved) => {
194
+ node.props.resolved = resolved;
195
+ },
196
+ (err) => {
197
+ const label = `${node.registration.kind}:${node.registration.id}`;
198
+ const reason = err instanceof Error ? err.message : String(err);
199
+ const message = `resolveData for ${label}${node.nodeId ? ` (node "${node.nodeId}")` : ""} threw or rejected: ${reason}. Rendering the node without resolved data instead of failing the whole experience.`;
200
+ if (typeof console !== "undefined") {
201
+ console.warn(`[@contentful/experiences] ${message}`);
202
+ }
203
+ log.log(message);
204
+ diagnostics.push(new Error(message, { cause: err instanceof Error ? err : void 0 }));
205
+ }
206
+ )
147
207
  );
148
208
  }
149
209
  if (tasks.length > 0) {
150
210
  await log.time(`${tasks.length} resolveData hook(s)`, () => Promise.all(tasks));
151
211
  }
152
212
  const fallbackViewportId = options.initialViewportId ?? config.fallbackViewportId;
153
- const fallbackViewportIndex = getViewportIndex(payload.viewports, fallbackViewportId);
213
+ const fallbackViewportIndex = getViewportIndex(viewports, fallbackViewportId);
154
214
  for (const node of nodes) {
155
- preResolveNodeTree(node, payload.viewports, fallbackViewportIndex, config.resolveToken, log);
215
+ preResolveNodeTree(
216
+ node,
217
+ viewports,
218
+ fallbackViewportIndex,
219
+ config.resolveToken,
220
+ log,
221
+ diagnostics
222
+ );
156
223
  }
157
224
  log.log(`pre-resolved design against fallback viewport index ${fallbackViewportIndex}`);
158
225
  return {
159
- viewports: payload.viewports,
226
+ viewports,
160
227
  nodes,
161
- fallbackViewportIndex
228
+ fallbackViewportIndex,
229
+ diagnostics
162
230
  };
163
231
  }
164
232
  export {
@@ -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 pre-resolves design against a fallback\n * viewport (see `resolveExperience` below).\n */\n\nimport { createDebugLogger, type DebugLogger } from './debug-logger';\nimport type {\n DesignPropValue,\n ExperienceContext,\n ExperienceNode,\n ExperiencePayload,\n PortableRegistration,\n PortableRenderNode,\n PortableRenderPlan,\n ResolveContext,\n ResolveToken,\n ViewportDef,\n} from './types';\nimport { applyTokenResolver, getViewportIndex, resolveDesignProperties } from './viewport';\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 * Default fallback viewport for server-side design pre-resolution. When unset\n * (and not overridden by `initialViewportId`), defaults to viewport[0].\n */\n fallbackViewportId?: string;\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 * Per-request override for the design pre-resolution fallback viewport. Wins\n * over `config.fallbackViewportId` — pass a request-time value (e.g. a\n * User-Agent-detected viewport) so SSR targets the device's expected\n * viewport. Defaults to viewport[0] when unset or unknown.\n */\n initialViewportId?: string;\n}\n\nconst DEFAULT_EXPERIENCE: ExperienceContext = {\n debug: false,\n metadata: {},\n viewports: [],\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 flat 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/** 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): 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}\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): PortableRenderNode[] {\n const built: PortableRenderNode[] = [];\n for (const node of nodes) {\n const one = buildNode(node, config, nodeRefs, log);\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): PortableRenderNode | null {\n const ref = readNodeRef(node);\n if (ref === null) {\n warnUnrenderableNode(node, log);\n return null;\n }\n const registration: PortableRegistration = {\n kind: ref.kind,\n id: extractIdFromUrn(ref.urn),\n };\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 throw new TypeError(\n `Slot \"${slotName}\" on ${registration.kind} \"${registration.id}\" must be an array of nodes.`\n );\n }\n slots[slotName] = buildNodes(children, config, nodeRefs, log);\n }\n }\n\n const built: PortableRenderNode = {\n registration,\n props: {\n content: { ...(node.contentProperties ?? {}) },\n // Resolved flat 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// Cascade a node's raw design to the fallback viewport and resolve tokens.\n// Returns the flat resolved map plus any token ids left unresolved (dropped).\nfunction preResolveDesignProperties(\n design: Record<string, DesignPropValue>,\n viewports: ViewportDef[],\n fallbackViewportIndex: number,\n resolveToken: ResolveToken | undefined\n): { props: Record<string, unknown>; unresolved: string[] } {\n const cascaded = resolveDesignProperties(design, viewports, fallbackViewportIndex);\n return applyTokenResolver(cascaded, resolveToken);\n}\n\n// Warn when resolveToken left tokens unresolved, so dropped keys are diagnosable.\nfunction warnUnresolvedTokens(label: string, unresolved: string[], log: DebugLogger): void {\n if (!unresolved.length || typeof console === 'undefined') return;\n console.warn(\n `[@contentful/experiences] resolveToken returned undefined for token id(s) on \"${label}\": ${unresolved.join(', ')}. Resolved design (getDesignValues()) will omit those keys.`\n );\n log.log(`unresolved token id(s) on \"${label}\": ${unresolved.join(', ')}`);\n}\n\n// Depth-first pre-resolve for a node and its slot children.\nfunction preResolveNodeTree(\n node: PortableRenderNode,\n viewports: ViewportDef[],\n fallbackViewportIndex: number,\n resolveToken: ResolveToken | undefined,\n log: DebugLogger\n): void {\n const { props, unresolved } = preResolveDesignProperties(\n node.props.designRaw,\n viewports,\n fallbackViewportIndex,\n resolveToken\n );\n node.props.design = props;\n warnUnresolvedTokens(`${node.registration.kind}:${node.registration.id}`, unresolved, log);\n for (const children of Object.values(node.slots)) {\n for (const child of children) {\n preResolveNodeTree(child, viewports, fallbackViewportIndex, resolveToken, log);\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 // 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(payload.nodes, config, nodeRefs, log);\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 // `viewports` is always sourced from the payload — the viewport list is fact,\n // not opinion, so it can't be overridden by the caller.\n const experience: ExperienceContext = {\n debug: options.debug ?? DEFAULT_EXPERIENCE.debug,\n metadata: {\n ...DEFAULT_EXPERIENCE.metadata,\n ...(options.metadata ?? {}),\n },\n viewports: payload.viewports,\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 Promise.resolve(resolver(ctx)).then((resolved) => {\n node.props.resolved = resolved;\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 // Pre-resolve design against the fallback viewport so SSR paints correct\n // values on first render. Fallback is initialViewportId, else\n // config.fallbackViewportId, else viewport[0].\n const fallbackViewportId = options.initialViewportId ?? config.fallbackViewportId;\n const fallbackViewportIndex = getViewportIndex(payload.viewports, fallbackViewportId);\n for (const node of nodes) {\n preResolveNodeTree(node, payload.viewports, fallbackViewportIndex, config.resolveToken, log);\n }\n log.log(`pre-resolved design against fallback viewport index ${fallbackViewportIndex}`);\n\n return {\n viewports: payload.viewports,\n nodes,\n fallbackViewportIndex,\n };\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 pre-resolves design against a fallback\n * viewport (see `resolveExperience` below).\n */\n\nimport { createDebugLogger, type DebugLogger } from './debug-logger';\nimport type {\n DesignPropValue,\n ExperienceContext,\n ExperienceNode,\n ExperiencePayload,\n PortableRegistration,\n PortableRenderNode,\n PortableRenderPlan,\n ResolveContext,\n ResolveToken,\n ViewportDef,\n} from './types';\nimport { applyTokenResolver, getViewportIndex, resolveDesignProperties } from './viewport';\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 * Default fallback viewport for server-side design pre-resolution. When unset\n * (and not overridden by `initialViewportId`), defaults to viewport[0].\n */\n fallbackViewportId?: string;\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 * Per-request override for the design pre-resolution fallback viewport. Wins\n * over `config.fallbackViewportId` — pass a request-time value (e.g. a\n * User-Agent-detected viewport) so SSR targets the device's expected\n * viewport. Defaults to viewport[0] when unset or unknown.\n */\n initialViewportId?: string;\n}\n\nconst DEFAULT_EXPERIENCE: ExperienceContext = {\n debug: false,\n metadata: {},\n viewports: [],\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 flat 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 flat 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// Cascade a node's raw design to the fallback viewport and resolve tokens.\n// Returns the flat resolved map plus any token ids left unresolved (dropped).\nfunction preResolveDesignProperties(\n design: Record<string, DesignPropValue>,\n viewports: ViewportDef[],\n fallbackViewportIndex: number,\n resolveToken: ResolveToken | undefined\n): { props: Record<string, unknown>; unresolved: string[] } {\n const cascaded = resolveDesignProperties(design, viewports, fallbackViewportIndex);\n return applyTokenResolver(cascaded, 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 pre-resolve for a node and its slot children.\nfunction preResolveNodeTree(\n node: PortableRenderNode,\n viewports: ViewportDef[],\n fallbackViewportIndex: number,\n resolveToken: ResolveToken | undefined,\n log: DebugLogger,\n diagnostics: Error[]\n): void {\n const { props, unresolved } = preResolveDesignProperties(\n node.props.designRaw,\n viewports,\n fallbackViewportIndex,\n resolveToken\n );\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, viewports, fallbackViewportIndex, 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`/`viewports` 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, no viewports) 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 const viewports = isPlainPayload\n ? ensureArray<ViewportDef>(payload.viewports, 'viewports', log, diagnostics)\n : [];\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 // `viewports` is always sourced from the payload — the viewport list is fact,\n // not opinion, so it can't be overridden by the caller.\n const experience: ExperienceContext = {\n debug: options.debug ?? DEFAULT_EXPERIENCE.debug,\n metadata: {\n ...DEFAULT_EXPERIENCE.metadata,\n ...(options.metadata ?? {}),\n },\n viewports,\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 // Pre-resolve design against the fallback viewport so SSR paints correct\n // values on first render. Fallback is initialViewportId, else\n // config.fallbackViewportId, else viewport[0].\n const fallbackViewportId = options.initialViewportId ?? config.fallbackViewportId;\n const fallbackViewportIndex = getViewportIndex(viewports, fallbackViewportId);\n for (const node of nodes) {\n preResolveNodeTree(\n node,\n viewports,\n fallbackViewportIndex,\n config.resolveToken,\n log,\n diagnostics\n );\n }\n log.log(`pre-resolved design against fallback viewport index ${fallbackViewportIndex}`);\n\n return {\n viewports,\n nodes,\n fallbackViewportIndex,\n diagnostics,\n };\n}\n"],"mappings":"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","names":[]}
package/dist/types.d.ts CHANGED
@@ -240,6 +240,17 @@ interface PortableRenderPlan {
240
240
  * matches this, and recompute from `props.designRaw` otherwise.
241
241
  */
242
242
  fallbackViewportIndex: number;
243
+ /**
244
+ * Resolve-time diagnostics collected while building this plan — malformed
245
+ * payload/slot shapes, an unidentifiable node, a failing `resolveData`, an
246
+ * unresolved design token. Plain `Error`s: the message names the node/
247
+ * component involved, and `resolve-data-failed`'s entry sets `.cause` to
248
+ * the original thrown/rejected error so the real stack trace stays
249
+ * reachable. Render-time diagnostics (unregistered id,
250
+ * component-render-error) are NOT here — each adapter collects those per
251
+ * render and merges both lists for `<DebugExperience>`.
252
+ */
253
+ diagnostics: Error[];
243
254
  }
244
255
 
245
256
  export type { ComponentNode, ComponentRef, DesignPropValue, DesignToken, ExperienceContext, ExperienceNode, ExperiencePayload, ExperienceSys, ExperienceTemplateNode, ExperienceTemplateRef, ManualDesignValue, PortableRegistration, PortableRenderNode, PortableRenderPlan, ResolveContext, ResolveToken, ValuesByViewport, ViewportDef };
@@ -7,10 +7,13 @@ declare function getValueForViewport(prop: DesignPropValue | undefined, viewport
7
7
  /** Resolve every design property on a node into a flat record keyed by name. */
8
8
  declare function resolveDesignProperties(designProperties: Record<string, DesignPropValue> | undefined, viewports: ViewportDef[], activeViewportIndex: number): Record<string, string | number | boolean | DesignToken>;
9
9
  /**
10
- * Resolve `DesignToken` values via `resolveToken`; scalars pass through. Keys
11
- * that resolve to `undefined` are dropped and their token ids collected in
12
- * `unresolved` for a grouped warning. With no resolver but tokens present,
13
- * warns once (those keys would reach components as raw token objects).
10
+ * Resolve `DesignToken` values via `resolveToken`; scalars pass through.
11
+ * Keys that don't resolve — no `resolveToken` configured, or the resolver
12
+ * returns `undefined` for that particular token — pass through as the raw
13
+ * `DesignToken` and their id is collected in `unresolved` for the caller to
14
+ * warn/diagnose (see `warnUnresolvedTokens` in `resolve-experience.ts`, which
15
+ * already dedupes per `resolveExperience()` call, so this function carries
16
+ * no warn-once state of its own).
14
17
  */
15
18
  declare function applyTokenResolver(props: Record<string, string | number | boolean | DesignToken>, resolveToken?: ResolveToken): {
16
19
  props: Record<string, unknown>;
package/dist/viewport.js CHANGED
@@ -32,26 +32,14 @@ function resolveDesignProperties(designProperties, viewports, activeViewportInde
32
32
  }
33
33
  return out;
34
34
  }
35
- let warnedMissingResolver = false;
36
35
  function applyTokenResolver(props, resolveToken) {
37
- if (!resolveToken) {
38
- if (!warnedMissingResolver && typeof console !== "undefined") {
39
- const tokenKeys = Object.entries(props).filter(([, v]) => typeof v === "object" && v !== null && v.type === "DesignToken").map(([k]) => k);
40
- if (tokenKeys.length) {
41
- warnedMissingResolver = true;
42
- console.warn(
43
- `[@contentful/experiences] Design tokens are present but no \`resolveToken\` is configured on the Config; token-valued design props (${tokenKeys.join(", ")}) reach components unresolved. Add \`resolveToken\` to your Config to map token ids to values.`
44
- );
45
- }
46
- }
47
- return { props, unresolved: [] };
48
- }
49
36
  const out = {};
50
37
  const unresolved = [];
51
38
  for (const [key, value] of Object.entries(props)) {
52
39
  if (typeof value === "object" && value !== null && value.type === "DesignToken") {
53
- const resolved = resolveToken(value);
40
+ const resolved = resolveToken?.(value);
54
41
  if (resolved === void 0) {
42
+ out[key] = value;
55
43
  unresolved.push(value.value);
56
44
  continue;
57
45
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/viewport.ts"],"sourcesContent":["/*\n * Viewport math + design-property resolution. Lives in core (not design) so the\n * resolve pipeline can pre-resolve design props server-side; the design package\n * re-exports these, so its public API is unchanged.\n *\n * Viewport order encodes cascade direction (desktop-first descends by width,\n * mobile-first ascends). The \"active viewport\" is the last-matching media query;\n * `getValueForViewport` walks backwards from it to viewport[0] and returns the\n * first defined value, matching CSS cascade behavior.\n */\n\nimport type {\n DesignPropValue,\n DesignToken,\n ManualDesignValue,\n ResolveToken,\n ValuesByViewport,\n ViewportDef,\n} from './types';\n\n/** Viewport id → index. Returns 0 (the wildcard viewport) when unknown. */\nexport function getViewportIndex(viewports: ViewportDef[], viewportId?: string): number {\n if (!viewportId) return 0;\n const index = viewports.findIndex((v) => v.id === viewportId);\n return index === -1 ? 0 : index;\n}\n\n// ManualDesignValue → its scalar; DesignToken → passed through (resolved later).\nfunction unwrapInner(\n inner: ManualDesignValue | DesignToken | undefined\n): string | number | boolean | DesignToken | undefined {\n if (!inner) return undefined;\n if (inner.type === 'ManualDesignValue') return inner.value;\n return inner;\n}\n\n// Cascade-lookup: walk back from activeViewportIndex to viewport[0], first defined wins.\nfunction resolveValuesByViewport(\n valuesByViewport: ValuesByViewport,\n viewports: ViewportDef[],\n activeViewportIndex: number\n): ManualDesignValue | DesignToken | undefined {\n for (let i = activeViewportIndex; i >= 0; i--) {\n const viewport = viewports[i];\n if (!viewport) continue;\n const candidate = valuesByViewport.values[viewport.id];\n if (candidate !== undefined && candidate !== null) return candidate;\n }\n return undefined;\n}\n\n/** Resolve one design property to its render-time value (cascade + unwrap). */\nexport function getValueForViewport(\n prop: DesignPropValue | undefined,\n viewports: ViewportDef[],\n activeViewportIndex: number\n): string | number | boolean | DesignToken | undefined {\n if (!prop) return undefined;\n if (prop.type === 'ManualDesignValue') return prop.value;\n if (prop.type === 'DesignToken') return prop;\n return unwrapInner(resolveValuesByViewport(prop, viewports, activeViewportIndex));\n}\n\n/** Resolve every design property on a node into a flat record keyed by name. */\nexport function resolveDesignProperties(\n designProperties: Record<string, DesignPropValue> | undefined,\n viewports: ViewportDef[],\n activeViewportIndex: number\n): Record<string, string | number | boolean | DesignToken> {\n const out: Record<string, string | number | boolean | DesignToken> = {};\n if (!designProperties) return out;\n for (const [key, prop] of Object.entries(designProperties)) {\n const value = getValueForViewport(prop, viewports, activeViewportIndex);\n if (value !== undefined) out[key] = value;\n }\n return out;\n}\n\nlet warnedMissingResolver = false;\n\n/**\n * Resolve `DesignToken` values via `resolveToken`; scalars pass through. Keys\n * that resolve to `undefined` are dropped and their token ids collected in\n * `unresolved` for a grouped warning. With no resolver but tokens present,\n * warns once (those keys would reach components as raw token objects).\n */\nexport function applyTokenResolver(\n props: Record<string, string | number | boolean | DesignToken>,\n resolveToken?: ResolveToken\n): { props: Record<string, unknown>; unresolved: string[] } {\n if (!resolveToken) {\n if (!warnedMissingResolver && typeof console !== 'undefined') {\n const tokenKeys = Object.entries(props)\n .filter(([, v]) => typeof v === 'object' && v !== null && v.type === 'DesignToken')\n .map(([k]) => k);\n if (tokenKeys.length) {\n warnedMissingResolver = true;\n console.warn(\n `[@contentful/experiences] Design tokens are present but no \\`resolveToken\\` is configured on the Config; token-valued design props (${tokenKeys.join(', ')}) reach components unresolved. Add \\`resolveToken\\` to your Config to map token ids to values.`\n );\n }\n }\n return { props, unresolved: [] };\n }\n const out: Record<string, unknown> = {};\n const unresolved: string[] = [];\n for (const [key, value] of Object.entries(props)) {\n if (typeof value === 'object' && value !== null && value.type === 'DesignToken') {\n const resolved = resolveToken(value);\n if (resolved === undefined) {\n unresolved.push(value.value);\n continue;\n }\n out[key] = resolved;\n continue;\n }\n out[key] = value;\n }\n return { props: out, unresolved };\n}\n"],"mappings":"AAqBO,SAAS,iBAAiB,WAA0B,YAA6B;AACtF,MAAI,CAAC,WAAY,QAAO;AACxB,QAAM,QAAQ,UAAU,UAAU,CAAC,MAAM,EAAE,OAAO,UAAU;AAC5D,SAAO,UAAU,KAAK,IAAI;AAC5B;AAGA,SAAS,YACP,OACqD;AACrD,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI,MAAM,SAAS,oBAAqB,QAAO,MAAM;AACrD,SAAO;AACT;AAGA,SAAS,wBACP,kBACA,WACA,qBAC6C;AAC7C,WAAS,IAAI,qBAAqB,KAAK,GAAG,KAAK;AAC7C,UAAM,WAAW,UAAU,CAAC;AAC5B,QAAI,CAAC,SAAU;AACf,UAAM,YAAY,iBAAiB,OAAO,SAAS,EAAE;AACrD,QAAI,cAAc,UAAa,cAAc,KAAM,QAAO;AAAA,EAC5D;AACA,SAAO;AACT;AAGO,SAAS,oBACd,MACA,WACA,qBACqD;AACrD,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI,KAAK,SAAS,oBAAqB,QAAO,KAAK;AACnD,MAAI,KAAK,SAAS,cAAe,QAAO;AACxC,SAAO,YAAY,wBAAwB,MAAM,WAAW,mBAAmB,CAAC;AAClF;AAGO,SAAS,wBACd,kBACA,WACA,qBACyD;AACzD,QAAM,MAA+D,CAAC;AACtE,MAAI,CAAC,iBAAkB,QAAO;AAC9B,aAAW,CAAC,KAAK,IAAI,KAAK,OAAO,QAAQ,gBAAgB,GAAG;AAC1D,UAAM,QAAQ,oBAAoB,MAAM,WAAW,mBAAmB;AACtE,QAAI,UAAU,OAAW,KAAI,GAAG,IAAI;AAAA,EACtC;AACA,SAAO;AACT;AAEA,IAAI,wBAAwB;AAQrB,SAAS,mBACd,OACA,cAC0D;AAC1D,MAAI,CAAC,cAAc;AACjB,QAAI,CAAC,yBAAyB,OAAO,YAAY,aAAa;AAC5D,YAAM,YAAY,OAAO,QAAQ,KAAK,EACnC,OAAO,CAAC,CAAC,EAAE,CAAC,MAAM,OAAO,MAAM,YAAY,MAAM,QAAQ,EAAE,SAAS,aAAa,EACjF,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC;AACjB,UAAI,UAAU,QAAQ;AACpB,gCAAwB;AACxB,gBAAQ;AAAA,UACN,uIAAuI,UAAU,KAAK,IAAI,CAAC;AAAA,QAC7J;AAAA,MACF;AAAA,IACF;AACA,WAAO,EAAE,OAAO,YAAY,CAAC,EAAE;AAAA,EACjC;AACA,QAAM,MAA+B,CAAC;AACtC,QAAM,aAAuB,CAAC;AAC9B,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AAChD,QAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,SAAS,eAAe;AAC/E,YAAM,WAAW,aAAa,KAAK;AACnC,UAAI,aAAa,QAAW;AAC1B,mBAAW,KAAK,MAAM,KAAK;AAC3B;AAAA,MACF;AACA,UAAI,GAAG,IAAI;AACX;AAAA,IACF;AACA,QAAI,GAAG,IAAI;AAAA,EACb;AACA,SAAO,EAAE,OAAO,KAAK,WAAW;AAClC;","names":[]}
1
+ {"version":3,"sources":["../src/viewport.ts"],"sourcesContent":["/*\n * Viewport math + design-property resolution. Lives in core (not design) so the\n * resolve pipeline can pre-resolve design props server-side; the design package\n * re-exports these, so its public API is unchanged.\n *\n * Viewport order encodes cascade direction (desktop-first descends by width,\n * mobile-first ascends). The \"active viewport\" is the last-matching media query;\n * `getValueForViewport` walks backwards from it to viewport[0] and returns the\n * first defined value, matching CSS cascade behavior.\n */\n\nimport type {\n DesignPropValue,\n DesignToken,\n ManualDesignValue,\n ResolveToken,\n ValuesByViewport,\n ViewportDef,\n} from './types';\n\n/** Viewport id → index. Returns 0 (the wildcard viewport) when unknown. */\nexport function getViewportIndex(viewports: ViewportDef[], viewportId?: string): number {\n if (!viewportId) return 0;\n const index = viewports.findIndex((v) => v.id === viewportId);\n return index === -1 ? 0 : index;\n}\n\n// ManualDesignValue → its scalar; DesignToken → passed through (resolved later).\nfunction unwrapInner(\n inner: ManualDesignValue | DesignToken | undefined\n): string | number | boolean | DesignToken | undefined {\n if (!inner) return undefined;\n if (inner.type === 'ManualDesignValue') return inner.value;\n return inner;\n}\n\n// Cascade-lookup: walk back from activeViewportIndex to viewport[0], first defined wins.\nfunction resolveValuesByViewport(\n valuesByViewport: ValuesByViewport,\n viewports: ViewportDef[],\n activeViewportIndex: number\n): ManualDesignValue | DesignToken | undefined {\n for (let i = activeViewportIndex; i >= 0; i--) {\n const viewport = viewports[i];\n if (!viewport) continue;\n const candidate = valuesByViewport.values[viewport.id];\n if (candidate !== undefined && candidate !== null) return candidate;\n }\n return undefined;\n}\n\n/** Resolve one design property to its render-time value (cascade + unwrap). */\nexport function getValueForViewport(\n prop: DesignPropValue | undefined,\n viewports: ViewportDef[],\n activeViewportIndex: number\n): string | number | boolean | DesignToken | undefined {\n if (!prop) return undefined;\n if (prop.type === 'ManualDesignValue') return prop.value;\n if (prop.type === 'DesignToken') return prop;\n return unwrapInner(resolveValuesByViewport(prop, viewports, activeViewportIndex));\n}\n\n/** Resolve every design property on a node into a flat record keyed by name. */\nexport function resolveDesignProperties(\n designProperties: Record<string, DesignPropValue> | undefined,\n viewports: ViewportDef[],\n activeViewportIndex: number\n): Record<string, string | number | boolean | DesignToken> {\n const out: Record<string, string | number | boolean | DesignToken> = {};\n if (!designProperties) return out;\n for (const [key, prop] of Object.entries(designProperties)) {\n const value = getValueForViewport(prop, viewports, activeViewportIndex);\n if (value !== undefined) out[key] = value;\n }\n return out;\n}\n\n/**\n * Resolve `DesignToken` values via `resolveToken`; scalars pass through.\n * Keys that don't resolve — no `resolveToken` configured, or the resolver\n * returns `undefined` for that particular token — pass through as the raw\n * `DesignToken` and their id is collected in `unresolved` for the caller to\n * warn/diagnose (see `warnUnresolvedTokens` in `resolve-experience.ts`, which\n * already dedupes per `resolveExperience()` call, so this function carries\n * no warn-once state of its own).\n */\nexport function applyTokenResolver(\n props: Record<string, string | number | boolean | DesignToken>,\n resolveToken?: ResolveToken\n): { props: Record<string, unknown>; unresolved: string[] } {\n const out: Record<string, unknown> = {};\n const unresolved: string[] = [];\n for (const [key, value] of Object.entries(props)) {\n if (typeof value === 'object' && value !== null && value.type === 'DesignToken') {\n const resolved = resolveToken?.(value);\n if (resolved === undefined) {\n out[key] = value;\n unresolved.push(value.value);\n continue;\n }\n out[key] = resolved;\n continue;\n }\n out[key] = value;\n }\n return { props: out, unresolved };\n}\n"],"mappings":"AAqBO,SAAS,iBAAiB,WAA0B,YAA6B;AACtF,MAAI,CAAC,WAAY,QAAO;AACxB,QAAM,QAAQ,UAAU,UAAU,CAAC,MAAM,EAAE,OAAO,UAAU;AAC5D,SAAO,UAAU,KAAK,IAAI;AAC5B;AAGA,SAAS,YACP,OACqD;AACrD,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI,MAAM,SAAS,oBAAqB,QAAO,MAAM;AACrD,SAAO;AACT;AAGA,SAAS,wBACP,kBACA,WACA,qBAC6C;AAC7C,WAAS,IAAI,qBAAqB,KAAK,GAAG,KAAK;AAC7C,UAAM,WAAW,UAAU,CAAC;AAC5B,QAAI,CAAC,SAAU;AACf,UAAM,YAAY,iBAAiB,OAAO,SAAS,EAAE;AACrD,QAAI,cAAc,UAAa,cAAc,KAAM,QAAO;AAAA,EAC5D;AACA,SAAO;AACT;AAGO,SAAS,oBACd,MACA,WACA,qBACqD;AACrD,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI,KAAK,SAAS,oBAAqB,QAAO,KAAK;AACnD,MAAI,KAAK,SAAS,cAAe,QAAO;AACxC,SAAO,YAAY,wBAAwB,MAAM,WAAW,mBAAmB,CAAC;AAClF;AAGO,SAAS,wBACd,kBACA,WACA,qBACyD;AACzD,QAAM,MAA+D,CAAC;AACtE,MAAI,CAAC,iBAAkB,QAAO;AAC9B,aAAW,CAAC,KAAK,IAAI,KAAK,OAAO,QAAQ,gBAAgB,GAAG;AAC1D,UAAM,QAAQ,oBAAoB,MAAM,WAAW,mBAAmB;AACtE,QAAI,UAAU,OAAW,KAAI,GAAG,IAAI;AAAA,EACtC;AACA,SAAO;AACT;AAWO,SAAS,mBACd,OACA,cAC0D;AAC1D,QAAM,MAA+B,CAAC;AACtC,QAAM,aAAuB,CAAC;AAC9B,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AAChD,QAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,SAAS,eAAe;AAC/E,YAAM,WAAW,eAAe,KAAK;AACrC,UAAI,aAAa,QAAW;AAC1B,YAAI,GAAG,IAAI;AACX,mBAAW,KAAK,MAAM,KAAK;AAC3B;AAAA,MACF;AACA,UAAI,GAAG,IAAI;AACX;AAAA,IACF;AACA,QAAI,GAAG,IAAI;AAAA,EACb;AACA,SAAO,EAAE,OAAO,KAAK,WAAW;AAClC;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experiences-sdk-core",
3
- "version": "0.7.10",
3
+ "version": "0.7.11",
4
4
  "description": "Runtime-neutral types + experience resolution for Contentful Experiences",
5
5
  "license": "MIT",
6
6
  "type": "module",