@contentful/experiences-sdk-core 0.7.3 → 0.7.5

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,15 @@
1
+ ## 0.7.5 (2026-08-12)
2
+
3
+ ### 🩹 Fixes
4
+
5
+ - **core,adapters:** render Experience Templates as ordinary nodes [AIS-413] ([#133](https://github.com/contentful/experiences/pull/133))
6
+
7
+ ## 0.7.4 (2026-08-11)
8
+
9
+ ### 🚀 Features
10
+
11
+ - stop sending the alpha-feature header manually ([#131](https://github.com/contentful/experiences/pull/131))
12
+
1
13
  ## 0.7.3 (2026-08-11)
2
14
 
3
15
  This was a version bump only for core to align it with other projects, there were no code changes.
package/README.md CHANGED
@@ -6,8 +6,8 @@ Runtime-neutral primitives shared across all framework adapters.
6
6
 
7
7
  ## What lives here
8
8
 
9
- - **Types** — `PortableRenderPlan`, `PortableRenderNode`, `PortableExperienceTemplate`, `ExperiencePayload`, `ExperienceNode`, the discriminated `DesignPropValue` union (`ManualDesignValue` / `DesignToken` / `ValuesByViewport`), `ViewportDef`, `ExperienceContext`, `ResolveContext`.
10
- - **`resolveExperience(payload, config, opts)`** — single async entry that walks an XDA payload, classifies content vs. design properties, captures slots, runs any component-declared `resolveData` hooks in parallel, and emits a runtime-neutral `PortableRenderPlan` ready for any framework adapter to render.
9
+ - **Types** — `PortableRenderPlan`, `PortableRenderNode`, `PortableRegistration`, `ExperiencePayload`, `ExperienceNode`, the discriminated `DesignPropValue` union (`ManualDesignValue` / `DesignToken` / `ValuesByViewport`), `ViewportDef`, `ExperienceContext`, `ResolveContext`.
10
+ - **`resolveExperience(payload, config, opts)`** — single async entry that walks an XDA payload, classifies content vs. design properties, captures slots, runs any component-declared `resolveData` hooks in parallel, and emits a runtime-neutral `PortableRenderPlan` ready for any framework adapter to render. Every node in the payload becomes a `PortableRenderNode`; `registration.kind` records whether the adapter should resolve its id against `config.components` or `config.experienceTemplates`. A coded Experience Template is an ordinary node — `payload.sys.experienceTemplate` is never read.
11
11
 
12
12
  ## Why a separate package?
13
13
 
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { ComponentNode, ComponentRef, DesignPropValue, DesignToken, ExperienceContext, ExperienceNode, ExperiencePayload, ExperienceSys, ExperienceTemplateNode, ExperienceTemplateRef, ManualDesignValue, PortableExperienceTemplate, PortableRegistration, PortableRenderNode, PortableRenderPlan, ResolveContext, ResolveToken, ValuesByViewport, ViewportDef } from './types.js';
1
+ export { ComponentNode, ComponentRef, DesignPropValue, DesignToken, ExperienceContext, ExperienceNode, ExperiencePayload, ExperienceSys, ExperienceTemplateNode, ExperienceTemplateRef, ManualDesignValue, PortableRegistration, PortableRenderNode, PortableRenderPlan, ResolveContext, ResolveToken, ValuesByViewport, ViewportDef } 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, getValueForViewport, getViewportIndex, resolveDesignProperties } from './viewport.js';
@@ -10,42 +10,73 @@ const DEFAULT_EXPERIENCE = {
10
10
  metadata: {},
11
11
  viewports: []
12
12
  };
13
- function isComponentNode(node) {
14
- return "component" in node;
13
+ function lookupEntry(config, registration) {
14
+ return registration.kind === "experienceTemplate" ? config.experienceTemplates?.[registration.id] : config.components[registration.id];
15
15
  }
16
16
  function extractIdFromUrn(urn) {
17
17
  const segments = urn.split("/").filter((s) => s.length > 0);
18
18
  return segments[segments.length - 1] ?? urn;
19
19
  }
20
- function buildNode(node, config, nodeRefs) {
21
- if (!isComponentNode(node)) {
22
- if (typeof console !== "undefined") {
23
- console.warn(
24
- "[@contentful/experiences-sdk-core] Skipping Experience-Template-variant node \u2014 Experience Templates are not supported as nodes in v1."
25
- );
20
+ function readNodeRef(node) {
21
+ const isExperienceTemplate = "experienceTemplate" in node;
22
+ const ref = isExperienceTemplate ? node.experienceTemplate : "component" in node ? node.component : void 0;
23
+ const urn = ref?.sys?.urn;
24
+ if (typeof urn !== "string" || urn.length === 0) return null;
25
+ return { kind: isExperienceTemplate ? "experienceTemplate" : "component", urn };
26
+ }
27
+ function countPayloadNodes(node) {
28
+ const slots = node.slots;
29
+ let total = 1;
30
+ if (slots && typeof slots === "object") {
31
+ for (const children of Object.values(slots)) {
32
+ if (!Array.isArray(children)) continue;
33
+ for (const child of children) total += countPayloadNodes(child);
26
34
  }
35
+ }
36
+ return total;
37
+ }
38
+ function warnUnrenderableNode(node, log) {
39
+ const id = node.id;
40
+ const label = typeof id === "string" ? ` "${id}"` : "";
41
+ const keys = Object.keys(node);
42
+ const descendants = countPayloadNodes(node) - 1;
43
+ const message = `Skipping unidentifiable node${label}: expected a \`component\` or \`experienceTemplate\` ResourceLink carrying a urn, got keys [${keys.join(", ")}]. Dropping it and ${descendants} descendant node(s). A payload node kind this SDK does not know is usually a version skew \u2014 upgrading @contentful/experiences-sdk-core may be all that is needed.`;
44
+ if (typeof console !== "undefined") {
45
+ console.warn(`[@contentful/experiences] ${message}`);
46
+ }
47
+ log.log(message);
48
+ }
49
+ function buildNodes(nodes, config, nodeRefs, log) {
50
+ const built = [];
51
+ for (const node of nodes) {
52
+ const one = buildNode(node, config, nodeRefs, log);
53
+ if (one !== null) built.push(one);
54
+ }
55
+ return built;
56
+ }
57
+ function buildNode(node, config, nodeRefs, log) {
58
+ const ref = readNodeRef(node);
59
+ if (ref === null) {
60
+ warnUnrenderableNode(node, log);
27
61
  return null;
28
62
  }
29
- const componentId = extractIdFromUrn(node.component.sys.urn);
63
+ const registration = {
64
+ kind: ref.kind,
65
+ id: extractIdFromUrn(ref.urn)
66
+ };
30
67
  const slots = {};
31
68
  if (node.slots) {
32
69
  for (const [slotName, children] of Object.entries(node.slots)) {
33
70
  if (!Array.isArray(children)) {
34
71
  throw new TypeError(
35
- `Slot "${slotName}" on component "${componentId}" must be an array of nodes.`
72
+ `Slot "${slotName}" on ${registration.kind} "${registration.id}" must be an array of nodes.`
36
73
  );
37
74
  }
38
- const built2 = [];
39
- for (const child of children) {
40
- const childNode = buildNode(child, config, nodeRefs);
41
- if (childNode === null) continue;
42
- built2.push(childNode);
43
- }
44
- slots[slotName] = built2;
75
+ slots[slotName] = buildNodes(children, config, nodeRefs, log);
45
76
  }
46
77
  }
47
78
  const built = {
48
- registration: { componentId },
79
+ registration,
49
80
  props: {
50
81
  content: { ...node.contentProperties ?? {} },
51
82
  // Resolved flat values are written by the pre-resolution pass below.
@@ -55,7 +86,7 @@ function buildNode(node, config, nodeRefs) {
55
86
  slots
56
87
  };
57
88
  if (node.id) built.nodeId = node.id;
58
- if (getResolver(config.components[componentId])) {
89
+ if (getResolver(lookupEntry(config, registration))) {
59
90
  nodeRefs.push(built);
60
91
  }
61
92
  return built;
@@ -79,7 +110,7 @@ function preResolveNodeTree(node, viewports, fallbackViewportIndex, resolveToken
79
110
  resolveToken
80
111
  );
81
112
  node.props.design = props;
82
- warnUnresolvedTokens(node.registration.componentId, unresolved, log);
113
+ warnUnresolvedTokens(`${node.registration.kind}:${node.registration.id}`, unresolved, log);
83
114
  for (const children of Object.values(node.slots)) {
84
115
  for (const child of children) {
85
116
  preResolveNodeTree(child, viewports, fallbackViewportIndex, resolveToken, log);
@@ -90,20 +121,8 @@ async function resolveExperience(payload, config, options = {}) {
90
121
  const log = createDebugLogger(options.debug, "core");
91
122
  log.lazy("resolveExperience called with payload", () => payload);
92
123
  const nodeRefs = [];
93
- const nodes = [];
94
- for (const node of payload.nodes) {
95
- const built = buildNode(node, config, nodeRefs);
96
- if (built !== null) nodes.push(built);
97
- }
124
+ const nodes = buildNodes(payload.nodes, config, nodeRefs, log);
98
125
  log.log(`built ${nodes.length} top-level node(s); ${nodeRefs.length} declare resolveData`);
99
- const experienceTemplateUrn = payload.sys?.experienceTemplate?.sys.urn;
100
- let experienceTemplate;
101
- if (typeof experienceTemplateUrn === "string" && experienceTemplateUrn.length > 0) {
102
- experienceTemplate = {
103
- experienceTemplateId: extractIdFromUrn(experienceTemplateUrn),
104
- props: { content: {}, design: {}, designRaw: {} }
105
- };
106
- }
107
126
  const experience = {
108
127
  debug: options.debug ?? DEFAULT_EXPERIENCE.debug,
109
128
  metadata: {
@@ -114,7 +133,7 @@ async function resolveExperience(payload, config, options = {}) {
114
133
  };
115
134
  const tasks = [];
116
135
  for (const node of nodeRefs) {
117
- const resolver = getResolver(config.components[node.registration.componentId]);
136
+ const resolver = getResolver(lookupEntry(config, node.registration));
118
137
  if (!resolver) continue;
119
138
  const ctx = {
120
139
  content: node.props.content,
@@ -127,24 +146,6 @@ async function resolveExperience(payload, config, options = {}) {
127
146
  })
128
147
  );
129
148
  }
130
- if (experienceTemplate) {
131
- const tplResolver = getResolver(
132
- config.experienceTemplates?.[experienceTemplate.experienceTemplateId]
133
- );
134
- if (tplResolver) {
135
- const ctx = {
136
- content: experienceTemplate.props.content,
137
- design: experienceTemplate.props.designRaw,
138
- experience
139
- };
140
- const tpl = experienceTemplate;
141
- tasks.push(
142
- Promise.resolve(tplResolver(ctx)).then((resolved) => {
143
- tpl.props.resolved = resolved;
144
- })
145
- );
146
- }
147
- }
148
149
  if (tasks.length > 0) {
149
150
  await log.time(`${tasks.length} resolveData hook(s)`, () => Promise.all(tasks));
150
151
  }
@@ -153,25 +154,10 @@ async function resolveExperience(payload, config, options = {}) {
153
154
  for (const node of nodes) {
154
155
  preResolveNodeTree(node, payload.viewports, fallbackViewportIndex, config.resolveToken, log);
155
156
  }
156
- if (experienceTemplate) {
157
- const { props, unresolved } = preResolveDesignProperties(
158
- experienceTemplate.props.designRaw,
159
- payload.viewports,
160
- fallbackViewportIndex,
161
- config.resolveToken
162
- );
163
- experienceTemplate.props.design = props;
164
- warnUnresolvedTokens(
165
- `experienceTemplate:${experienceTemplate.experienceTemplateId}`,
166
- unresolved,
167
- log
168
- );
169
- }
170
157
  log.log(`pre-resolved design against fallback viewport index ${fallbackViewportIndex}`);
171
158
  return {
172
159
  viewports: payload.viewports,
173
160
  nodes,
174
- ...experienceTemplate ? { experienceTemplate } : {},
175
161
  fallbackViewportIndex
176
162
  };
177
163
  }
@@ -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 ComponentNode,\n DesignPropValue,\n ExperienceContext,\n ExperienceNode,\n ExperiencePayload,\n PortableExperienceTemplate,\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\nfunction isComponentNode(node: ExperienceNode): node is ComponentNode {\n return 'component' in node;\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 * 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 */\nfunction buildNode(\n node: ExperienceNode,\n config: ResolverConfig,\n nodeRefs: PortableRenderNode[]\n): PortableRenderNode | null {\n if (!isComponentNode(node)) {\n if (typeof console !== 'undefined') {\n console.warn(\n '[@contentful/experiences-sdk-core] Skipping Experience-Template-variant node — Experience Templates are not supported as nodes in v1.'\n );\n }\n return null;\n }\n\n const componentId = extractIdFromUrn(node.component.sys.urn);\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 component \"${componentId}\" must be an array of nodes.`\n );\n }\n const built: PortableRenderNode[] = [];\n for (const child of children) {\n const childNode = buildNode(child, config, nodeRefs);\n if (childNode === null) continue;\n built.push(childNode);\n }\n slots[slotName] = built;\n }\n }\n\n const built: PortableRenderNode = {\n registration: { componentId },\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(config.components[componentId])) {\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.componentId, 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[] = [];\n for (const node of payload.nodes) {\n const built = buildNode(node, config, nodeRefs);\n if (built !== null) nodes.push(built);\n }\n log.log(`built ${nodes.length} top-level node(s); ${nodeRefs.length} declare resolveData`);\n\n // Build the page-level Experience Template stub if the payload carries one.\n // XDA payloads don't yet emit template-level content/design properties, so\n // the IR carries empty bags.\n const experienceTemplateUrn = payload.sys?.experienceTemplate?.sys.urn;\n let experienceTemplate: PortableExperienceTemplate | undefined;\n if (typeof experienceTemplateUrn === 'string' && experienceTemplateUrn.length > 0) {\n experienceTemplate = {\n experienceTemplateId: extractIdFromUrn(experienceTemplateUrn),\n props: { content: {}, design: {}, designRaw: {} },\n };\n }\n\n // Pass 2: run resolveData hooks for components AND the template 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(config.components[node.registration.componentId]);\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 if (experienceTemplate) {\n const tplResolver = getResolver(\n config.experienceTemplates?.[experienceTemplate.experienceTemplateId]\n );\n if (tplResolver) {\n const ctx: ResolveContext = {\n content: experienceTemplate.props.content,\n design: experienceTemplate.props.designRaw,\n experience,\n };\n const tpl = experienceTemplate;\n tasks.push(\n Promise.resolve(tplResolver(ctx)).then((resolved) => {\n tpl.props.resolved = resolved;\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(payload.viewports, fallbackViewportId);\n for (const node of nodes) {\n preResolveNodeTree(node, payload.viewports, fallbackViewportIndex, config.resolveToken, log);\n }\n if (experienceTemplate) {\n const { props, unresolved } = preResolveDesignProperties(\n experienceTemplate.props.designRaw,\n payload.viewports,\n fallbackViewportIndex,\n config.resolveToken\n );\n experienceTemplate.props.design = props;\n warnUnresolvedTokens(\n `experienceTemplate:${experienceTemplate.experienceTemplateId}`,\n unresolved,\n log\n );\n }\n log.log(`pre-resolved design against fallback viewport index ${fallbackViewportIndex}`);\n\n return {\n viewports: payload.viewports,\n nodes,\n ...(experienceTemplate ? { experienceTemplate } : {}),\n fallbackViewportIndex,\n };\n}\n"],"mappings":"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","names":["built"]}
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 * prop bags, 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":"AAOA,SAAS,yBAA2C;AAapD,SAAS,oBAAoB,kBAAkB,+BAA+B;AA6B9E,SAAS,YACP,OAGY;AACZ,MAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AACxD,QAAM,YAAa,MAAoC;AACvD,SAAO,OAAO,cAAc,aACvB,YAGD;AACN;AAuBA,MAAM,qBAAwC;AAAA,EAC5C,OAAO;AAAA,EACP,UAAU,CAAC;AAAA,EACX,WAAW,CAAC;AACd;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;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,KAAwB;AAC1E,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;AACjB;AAMA,SAAS,WACP,OACA,QACA,UACA,KACsB;AACtB,QAAM,QAA8B,CAAC;AACrC,aAAW,QAAQ,OAAO;AACxB,UAAM,MAAM,UAAU,MAAM,QAAQ,UAAU,GAAG;AACjD,QAAI,QAAQ,KAAM,OAAM,KAAK,GAAG;AAAA,EAClC;AACA,SAAO;AACT;AAWA,SAAS,UACP,MACA,QACA,UACA,KAC2B;AAC3B,QAAM,MAAM,YAAY,IAAI;AAC5B,MAAI,QAAQ,MAAM;AAChB,yBAAqB,MAAM,GAAG;AAC9B,WAAO;AAAA,EACT;AACA,QAAM,eAAqC;AAAA,IACzC,MAAM,IAAI;AAAA,IACV,IAAI,iBAAiB,IAAI,GAAG;AAAA,EAC9B;AAEA,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,IAAI;AAAA,UACR,SAAS,QAAQ,QAAQ,aAAa,IAAI,KAAK,aAAa,EAAE;AAAA,QAChE;AAAA,MACF;AACA,YAAM,QAAQ,IAAI,WAAW,UAAU,QAAQ,UAAU,GAAG;AAAA,IAC9D;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;AAIA,SAAS,2BACP,QACA,WACA,uBACA,cAC0D;AAC1D,QAAM,WAAW,wBAAwB,QAAQ,WAAW,qBAAqB;AACjF,SAAO,mBAAmB,UAAU,YAAY;AAClD;AAGA,SAAS,qBAAqB,OAAe,YAAsB,KAAwB;AACzF,MAAI,CAAC,WAAW,UAAU,OAAO,YAAY,YAAa;AAC1D,UAAQ;AAAA,IACN,iFAAiF,KAAK,MAAM,WAAW,KAAK,IAAI,CAAC;AAAA,EACnH;AACA,MAAI,IAAI,8BAA8B,KAAK,MAAM,WAAW,KAAK,IAAI,CAAC,EAAE;AAC1E;AAGA,SAAS,mBACP,MACA,WACA,uBACA,cACA,KACM;AACN,QAAM,EAAE,OAAO,WAAW,IAAI;AAAA,IAC5B,KAAK,MAAM;AAAA,IACX;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,OAAK,MAAM,SAAS;AACpB,uBAAqB,GAAG,KAAK,aAAa,IAAI,IAAI,KAAK,aAAa,EAAE,IAAI,YAAY,GAAG;AACzF,aAAW,YAAY,OAAO,OAAO,KAAK,KAAK,GAAG;AAChD,eAAW,SAAS,UAAU;AAC5B,yBAAmB,OAAO,WAAW,uBAAuB,cAAc,GAAG;AAAA,IAC/E;AAAA,EACF;AACF;AAYA,eAAsB,kBACpB,SACA,QACA,UAAoC,CAAC,GACR;AAC7B,QAAM,MAAM,kBAAkB,QAAQ,OAAO,MAAM;AACnD,MAAI,KAAK,yCAAyC,MAAM,OAAO;AAI/D,QAAM,WAAiC,CAAC;AACxC,QAAM,QAA8B,WAAW,QAAQ,OAAO,QAAQ,UAAU,GAAG;AACnF,MAAI,IAAI,SAAS,MAAM,MAAM,uBAAuB,SAAS,MAAM,sBAAsB;AAKzF,QAAM,aAAgC;AAAA,IACpC,OAAO,QAAQ,SAAS,mBAAmB;AAAA,IAC3C,UAAU;AAAA,MACR,GAAG,mBAAmB;AAAA,MACtB,GAAI,QAAQ,YAAY,CAAC;AAAA,IAC3B;AAAA,IACA,WAAW,QAAQ;AAAA,EACrB;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,MACJ,QAAQ,QAAQ,SAAS,GAAG,CAAC,EAAE,KAAK,CAAC,aAAa;AAChD,aAAK,MAAM,WAAW;AAAA,MACxB,CAAC;AAAA,IACH;AAAA,EACF;AAIA,MAAI,MAAM,SAAS,GAAG;AACpB,UAAM,IAAI,KAAK,GAAG,MAAM,MAAM,wBAAwB,MAAM,QAAQ,IAAI,KAAK,CAAC;AAAA,EAChF;AAKA,QAAM,qBAAqB,QAAQ,qBAAqB,OAAO;AAC/D,QAAM,wBAAwB,iBAAiB,QAAQ,WAAW,kBAAkB;AACpF,aAAW,QAAQ,OAAO;AACxB,uBAAmB,MAAM,QAAQ,WAAW,uBAAuB,OAAO,cAAc,GAAG;AAAA,EAC7F;AACA,MAAI,IAAI,uDAAuD,qBAAqB,EAAE;AAEtF,SAAO;AAAA,IACL,WAAW,QAAQ;AAAA,IACnB;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
package/dist/types.d.ts CHANGED
@@ -77,9 +77,9 @@ interface ComponentRef {
77
77
  };
78
78
  }
79
79
  /**
80
- * Resource-link reference to an Experience Template. Experience Templates
81
- * are out of v1 scope as *nodes* and are skipped at plan-build time with a
82
- * diagnostic; the page-level reference on `sys` is honored.
80
+ * Resource-link reference to an Experience Template. The `urn` carries the
81
+ * template id, extracted the same way as a component id (segment after the
82
+ * last slash).
83
83
  *
84
84
  * Mirrors `ExperienceTemplateLink` from `@contentful/experience-delivery`.
85
85
  */
@@ -124,14 +124,13 @@ interface ExperienceTemplateNode {
124
124
  */
125
125
  interface ExperienceSys {
126
126
  /**
127
- * Page-level Experience Template reference. When present, the renderer wraps
128
- * the experience nodes with the matching template registered in the
129
- * customer's Config. When absent, nodes render at the top level.
130
- *
131
- * Optional here while the delivery type declares it required — a required
132
- * field satisfies an optional one, and keeping it optional lets
133
- * `resolveExperience` accept hand-authored payloads for Experiences that
134
- * carry no template.
127
+ * Editorial link to the Experience Template this Experience was authored
128
+ * from. The renderer does NOT read this — it is present on every Experience
129
+ * (both coded and composite templates), so it carries no signal about
130
+ * whether a template should wrap anything. Rendering is driven entirely by
131
+ * `nodes`: an `ExperienceTemplateNode` there means "render this coded
132
+ * template"; its absence means the template was composite and the nodes are
133
+ * plain components. Typed here only so payloads round-trip.
135
134
  */
136
135
  experienceTemplate?: ExperienceTemplateRef;
137
136
  [key: string]: unknown;
@@ -144,7 +143,7 @@ interface ExperienceSys {
144
143
  * required when consuming a delivery-client response. The delivery API returns
145
144
  * this shape when the request carries the
146
145
  * `x-contentful-enable-alpha-feature: new-exo-entity-types` header, which
147
- * `@contentful/experiences-client` sends on every request.
146
+ * `@contentful/experience-delivery` sends on every request.
148
147
  */
149
148
  interface ExperiencePayload {
150
149
  viewports: ViewportDef[];
@@ -166,16 +165,24 @@ interface ResolveContext {
166
165
  }
167
166
  /**
168
167
  * Registration metadata for a single instance — the SDK's interpreted
169
- * pointer to the customer's component implementation. Today carries only
170
- * the resolved component id; capabilities (state requirements,
171
- * supported events, lifecycle hints, fallback ids) land here when needed.
168
+ * pointer to the customer's implementation. Carries the resolved id plus the
169
+ * registry that id belongs to; capabilities (state requirements, supported
170
+ * events, lifecycle hints, fallback ids) land here when needed.
171
+ *
172
+ * `kind` tells the adapter which registry to look `id` up in:
173
+ * `'component'` → `Config.components`, `'experienceTemplate'` →
174
+ * `Config.experienceTemplates`. Everything else about a node is identical
175
+ * across the two kinds — a coded Experience Template is just a node whose
176
+ * implementation lives in the other registry.
172
177
  */
173
178
  interface PortableRegistration {
174
- componentId: string;
179
+ kind: 'component' | 'experienceTemplate';
180
+ id: string;
175
181
  }
176
182
  /**
177
- * The IR — one node per component instance. The seam that lets non-React
178
- * adapters (Angular, SwiftUI, Compose) consume the same interpretation.
183
+ * The IR — one node per component or Experience Template instance. The seam
184
+ * that lets non-React adapters (Angular, SwiftUI, Compose) consume the same
185
+ * interpretation.
179
186
  *
180
187
  * Design props preserve the discriminated value shape as they arrived. Adapters
181
188
  * unwrap to plain scalars at render time, given an active viewport.
@@ -207,43 +214,26 @@ interface PortableRenderNode {
207
214
  /** Raw per-viewport design, for client re-resolution on viewport change. */
208
215
  designRaw: Record<string, DesignPropValue>;
209
216
  };
217
+ /**
218
+ * Slot children keyed by slot name, pre-built in payload order. Adapters
219
+ * pass each entry to the customer's implementation as a prop of the same
220
+ * name — a slot named `content` becomes a `content` prop. `children` is not
221
+ * special; it is simply the conventional default slot name.
222
+ */
210
223
  slots: Record<string, PortableRenderNode[]>;
211
224
  }
212
- /**
213
- * Interpreted page-level Experience Template — the optional wrapper around the
214
- * experience tree. `experienceTemplateId` is extracted from
215
- * `payload.sys.experienceTemplate.sys.urn` (last slash-segment).
216
- *
217
- * Experience Templates carry the same prop-resolution shape as components:
218
- * content + design properties plus an optional `resolved` map from a
219
- * `resolveData` hook. v1 payloads from XDA don't carry template-level
220
- * content/design properties yet, but the IR makes room for them so the API
221
- * doesn't need to break later.
222
- */
223
- interface PortableExperienceTemplate {
224
- experienceTemplateId: string;
225
- props: {
226
- content: Record<string, unknown>;
227
- /** Same as `PortableRenderNode.props.design`. */
228
- design: Record<string, unknown>;
229
- resolved?: Record<string, unknown>;
230
- /** Same as `PortableRenderNode.props.designRaw`. */
231
- designRaw: Record<string, DesignPropValue>;
232
- };
233
- }
234
225
  /**
235
226
  * The interpreted experience tree.
236
227
  *
237
228
  * Top-level is `nodes: PortableRenderNode[]` (array, not single root) to
238
- * match the actual XDA payload shape. Renderers iterate top-level nodes
239
- * and recurse into `node.slots`. When `experienceTemplate` is present, the
240
- * renderer wraps the nodes with the matching Experience Template config;
241
- * otherwise nodes render at the top level.
229
+ * match the actual XDA payload shape. Renderers iterate top-level nodes and
230
+ * recurse into `node.slots`. A coded Experience Template shows up as a
231
+ * top-level node with `registration.kind === 'experienceTemplate'`, so there
232
+ * is no plan-level template concept — see `PortableRegistration`.
242
233
  */
243
234
  interface PortableRenderPlan {
244
235
  viewports: ViewportDef[];
245
236
  nodes: PortableRenderNode[];
246
- experienceTemplate?: PortableExperienceTemplate;
247
237
  /**
248
238
  * Viewport index the server pre-resolved design against (viewport[0] by
249
239
  * default). Adapters use `props.design` as-is when their active viewport
@@ -252,4 +242,4 @@ interface PortableRenderPlan {
252
242
  fallbackViewportIndex: number;
253
243
  }
254
244
 
255
- export type { ComponentNode, ComponentRef, DesignPropValue, DesignToken, ExperienceContext, ExperienceNode, ExperiencePayload, ExperienceSys, ExperienceTemplateNode, ExperienceTemplateRef, ManualDesignValue, PortableExperienceTemplate, PortableRegistration, PortableRenderNode, PortableRenderPlan, ResolveContext, ResolveToken, ValuesByViewport, ViewportDef };
245
+ export type { ComponentNode, ComponentRef, DesignPropValue, DesignToken, ExperienceContext, ExperienceNode, ExperiencePayload, ExperienceSys, ExperienceTemplateNode, ExperienceTemplateRef, ManualDesignValue, PortableRegistration, PortableRenderNode, PortableRenderPlan, ResolveContext, ResolveToken, ValuesByViewport, ViewportDef };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experiences-sdk-core",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
4
4
  "description": "Runtime-neutral types + experience resolution for Contentful Experiences",
5
5
  "license": "MIT",
6
6
  "type": "module",