@contentful/experiences-sdk-core 0.6.1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,96 @@
1
+ ## 0.7.1 (2026-08-07)
2
+
3
+ ### 🚀 Features
4
+
5
+ - server-side design pre-resolution against a fallback viewport [AIS-386] ([#119](https://github.com/contentful/experiences/pull/119))
6
+
7
+ ## 0.7.0 (2026-08-07)
8
+
9
+ ### 🩹 Fixes
10
+
11
+ - ⚠️ bump @contentful/experience-delivery to 1.0.0-dev.6 + migrate to renamed API [AIS-339] ([#120](https://github.com/contentful/experiences/pull/120))
12
+
13
+ ### ⚠️ Breaking Changes
14
+
15
+ - bump @contentful/experience-delivery to 1.0.0-dev.6 + migrate to renamed API [AIS-339] ([#120](https://github.com/contentful/experiences/pull/120))
16
+ the public API uses the component / experienceTemplate vocabulary.
17
+ - `registration.componentTypeId` -> `registration.componentId`
18
+ - `plan.template` -> `plan.experienceTemplate`
19
+ - `PortableTemplate` -> `PortableExperienceTemplate` (`templateId` ->
20
+ `experienceTemplateId`)
21
+ - `Config.templates` -> `Config.experienceTemplates`; `Templates` ->
22
+ `ExperienceTemplates`
23
+ - `defineTemplate` -> `defineExperienceTemplate`; `TemplateConfig` ->
24
+ `ExperienceTemplateConfig`; `TemplateRegistration` ->
25
+ `ExperienceTemplateRegistration`; `normalizeTemplateRegistration` ->
26
+ `normalizeExperienceTemplateRegistration`
27
+ - `useContentfulTemplate()` / `getContentfulTemplate()` ->
28
+ `useContentfulExperienceTemplate()` / `getContentfulExperienceTemplate()`;
29
+ `ContentfulTemplate` -> `ContentfulExperienceTemplate`
30
+ - `MissingComponentProps.componentTypeId` -> `componentId`
31
+ - Core payload types: `ComponentTypeNode`/`ComponentTypeRef` ->
32
+ `ComponentNode`/`ComponentRef`; `TemplateNode`/`TemplateRef` ->
33
+ `ExperienceTemplateNode`/`ExperienceTemplateRef`
34
+ `packages/core` stays zero-dep, so its payload types are hand-mirrored from the
35
+ delivery package rather than imported; each carries a doc comment naming its
36
+ upstream counterpart, and `client:typecheck` catches drift when the delivery SDK
37
+ regenerates. Documented in AGENTS.md.
38
+ `examples/scripts/` is intentionally unchanged: it targets the management SDK
39
+ (`contentful-management`), which has its own entity-type surface, and adopting it
40
+ would require changing that dependency. The seed script therefore still
41
+ provisions the shapes that SDK produces.
42
+ Verified: build, typecheck, and 157 tests pass across all five packages.
43
+ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
44
+ * docs: describe current state only in comments and docs
45
+ Drop ticket ids, tracker links, and before/after narrative from doc comments and
46
+ prose so they explain what the code does now rather than how it got here.
47
+ - `alpha-feature.ts`: state what the header selects and why it is required,
48
+ without the two-shapes-in-transition framing or the removal timeline.
49
+ - `core/src/types.ts`: keep each type's upstream counterpart in
50
+ `@contentful/experience-delivery` (load-bearing for maintenance) but drop the
51
+ "formerly known as" and old-link-type asides.
52
+ - README: replace the rename migration table with a description of the
53
+ alpha-feature header and when a caller needs to send it.
54
+ - AGENTS.md: reframe the rationale entries that opened on prior designs to state
55
+ the current design and its reason.
56
+ - Drop stale option-shape asides ("was nested under ...") and a test name that
57
+ described superseded behaviour.
58
+ Generated CHANGELOG.md files are untouched — they are release history.
59
+ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
60
+ * feat!: migrate bootstrap fixture script to component/experienceTemplate entities
61
+ contentful-management@12.14.0 exposes the renamed ExO entities (Component,
62
+ ExperienceTemplate) alongside the deprecated ComponentType/Template ones. Adopt
63
+ the renamed shapes throughout examples/scripts so a freshly bootstrapped demo
64
+ Experience uses the same entity vocabulary the delivery-consuming packages now
65
+ read.
66
+ - Bump contentful-management to ^12.14.0 (existing dependency, version only —
67
+ no new packages).
68
+ - fixture/component-types.ts -> fixture/components.ts: ComponentTypeFixture ->
69
+ ComponentFixture.
70
+ - fixture/templates.ts -> fixture/experience-templates.ts:
71
+ TemplateFixture -> ExperienceTemplateFixture, TemplateTreeNode ->
72
+ ExperienceTemplateTreeNode.
73
+ - fixture/types.ts, fixture/experience.ts: ExperienceFixture.templateId ->
74
+ experienceTemplateId; node nodeType 'InlineFragment' ->
75
+ 'InlineExperienceFragment' with componentTypeId -> componentId, matching
76
+ contentful-management's InlineExperienceFragmentNode.
77
+ - fixture/data-assemblies.ts: dataAssemblyComponentTypeLinks ->
78
+ dataAssemblyComponentLinks (componentTypeId -> componentId).
79
+ - bootstrap-example.ts: cma.componentType -> cma.component,
80
+ cma.template -> cma.experienceTemplate; sys.type 'ComponentType' ->
81
+ 'Component', 'Template' -> 'ExperienceTemplate'; resource-link types
82
+ Contentful:ComponentType -> Contentful:Component and Contentful:Template ->
83
+ Contentful:ExperienceTemplate; URN segments components/ and
84
+ experienceTemplates/; seedComponentType -> seedComponent, seedTemplate ->
85
+ seedExperienceTemplate, linkDataAssembliesToComponentTypes ->
86
+ linkDataAssembliesToComponents.
87
+ The metadata.annotations.Template composed/coded-implementation marker is
88
+ unrelated to this rename (identical shape in 12.10.0 and 12.14.0) and is left
89
+ untouched.
90
+ Verified: examples/scripts typechecks clean; the main workspace's build,
91
+ typecheck, and 157 tests are unaffected.
92
+ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
93
+
1
94
  ## 0.6.1 (2026-08-06)
2
95
 
3
96
  This was a version bump only for core to align it with other projects, there were no code changes.
package/README.md CHANGED
@@ -6,7 +6,7 @@ Runtime-neutral primitives shared across all framework adapters.
6
6
 
7
7
  ## What lives here
8
8
 
9
- - **Types** — `PortableRenderPlan`, `PortableRenderNode`, `PortableTemplate`, `ExperiencePayload`, `ExperienceNode`, the discriminated `DesignPropValue` union (`ManualDesignValue` / `DesignToken` / `ValuesByViewport`), `ViewportDef`, `ExperienceContext`, `ResolveContext`.
9
+ - **Types** — `PortableRenderPlan`, `PortableRenderNode`, `PortableExperienceTemplate`, `ExperiencePayload`, `ExperienceNode`, the discriminated `DesignPropValue` union (`ManualDesignValue` / `DesignToken` / `ValuesByViewport`), `ViewportDef`, `ExperienceContext`, `ResolveContext`.
10
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.
11
11
 
12
12
  ## Why a separate package?
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
- export { ComponentTypeNode, ComponentTypeRef, DesignPropValue, DesignToken, ExperienceContext, ExperienceNode, ExperiencePayload, ExperienceSys, ManualDesignValue, PortableRegistration, PortableRenderNode, PortableRenderPlan, PortableTemplate, ResolveContext, ResolveToken, TemplateNode, TemplateRef, ValuesByViewport, ViewportDef } from './types.js';
1
+ export { ComponentNode, ComponentRef, DesignPropValue, DesignToken, ExperienceContext, ExperienceNode, ExperiencePayload, ExperienceSys, ExperienceTemplateNode, ExperienceTemplateRef, ManualDesignValue, PortableExperienceTemplate, 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
+ export { applyTokenResolver, getValueForViewport, getViewportIndex, resolveDesignProperties } from './viewport.js';
package/dist/index.js CHANGED
@@ -1,8 +1,18 @@
1
1
  export * from "./types";
2
2
  import { resolveExperience } from "./resolve-experience";
3
3
  import { createDebugLogger } from "./debug-logger";
4
+ import {
5
+ applyTokenResolver,
6
+ getValueForViewport,
7
+ getViewportIndex,
8
+ resolveDesignProperties
9
+ } from "./viewport";
4
10
  export {
11
+ applyTokenResolver,
5
12
  createDebugLogger,
13
+ getValueForViewport,
14
+ getViewportIndex,
15
+ resolveDesignProperties,
6
16
  resolveExperience
7
17
  };
8
18
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './types';\nexport { resolveExperience } from './resolve-experience';\nexport type { ResolverConfig, ResolveExperienceOptions } from './resolve-experience';\nexport { createDebugLogger } from './debug-logger';\nexport type { DebugLogger } from './debug-logger';\n"],"mappings":"AAAA,cAAc;AACd,SAAS,yBAAyB;AAElC,SAAS,yBAAyB;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './types';\nexport { resolveExperience } from './resolve-experience';\nexport type { ResolverConfig, ResolveExperienceOptions } from './resolve-experience';\nexport { createDebugLogger } from './debug-logger';\nexport type { DebugLogger } from './debug-logger';\nexport {\n applyTokenResolver,\n getValueForViewport,\n getViewportIndex,\n resolveDesignProperties,\n} from './viewport';\n"],"mappings":"AAAA,cAAc;AACd,SAAS,yBAAyB;AAElC,SAAS,yBAAyB;AAElC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;","names":[]}
@@ -1,4 +1,4 @@
1
- import { ExperiencePayload, PortableRenderPlan } from './types.js';
1
+ import { ResolveToken, ExperiencePayload, PortableRenderPlan } from './types.js';
2
2
 
3
3
  /**
4
4
  * Structural type the resolver walker depends on. Matches the React /
@@ -13,13 +13,23 @@ import { ExperiencePayload, PortableRenderPlan } from './types.js';
13
13
  */
14
14
  interface ResolverConfig {
15
15
  components: Record<string, unknown>;
16
- templates?: Record<string, unknown>;
16
+ experienceTemplates?: Record<string, unknown>;
17
+ /**
18
+ * Resolves `DesignToken` design properties to runtime values. Mirrors the
19
+ * adapter `Config.resolveToken`, so server and client agree without the
20
+ * caller re-supplying it. Used during server-side pre-resolution.
21
+ */
22
+ resolveToken?: ResolveToken;
23
+ /**
24
+ * Default fallback viewport for server-side design pre-resolution. When unset
25
+ * (and not overridden by `initialViewportId`), defaults to viewport[0].
26
+ */
27
+ fallbackViewportId?: string;
17
28
  }
18
29
  interface ResolveExperienceOptions {
19
30
  /**
20
31
  * Arbitrary per-render metadata exposed to every resolver as
21
- * `ctx.experience.metadata`. Flattened to a top-level option (was nested
22
- * under `experience`). Defaults to `{}`.
32
+ * `ctx.experience.metadata`. Defaults to `{}`.
23
33
  */
24
34
  metadata?: Record<string, unknown>;
25
35
  /**
@@ -28,6 +38,13 @@ interface ResolveExperienceOptions {
28
38
  * resolver context as `ctx.experience.debug`. Defaults to `false`.
29
39
  */
30
40
  debug?: boolean;
41
+ /**
42
+ * Per-request override for the design pre-resolution fallback viewport. Wins
43
+ * over `config.fallbackViewportId` — pass a request-time value (e.g. a
44
+ * User-Agent-detected viewport) so SSR targets the device's expected
45
+ * viewport. Defaults to viewport[0] when unset or unknown.
46
+ */
47
+ initialViewportId?: string;
31
48
  }
32
49
  /**
33
50
  * Turns an Experience payload (XDA response shape) into a PortableRenderPlan
@@ -1,4 +1,5 @@
1
1
  import { createDebugLogger } from "./debug-logger";
2
+ import { applyTokenResolver, getViewportIndex, resolveDesignProperties } from "./viewport";
2
3
  function getResolver(entry) {
3
4
  if (typeof entry !== "object" || entry === null) return void 0;
4
5
  const candidate = entry.resolveData;
@@ -9,29 +10,29 @@ const DEFAULT_EXPERIENCE = {
9
10
  metadata: {},
10
11
  viewports: []
11
12
  };
12
- function isComponentTypeNode(node) {
13
- return "componentType" in node;
13
+ function isComponentNode(node) {
14
+ return "component" in node;
14
15
  }
15
16
  function extractIdFromUrn(urn) {
16
17
  const segments = urn.split("/").filter((s) => s.length > 0);
17
18
  return segments[segments.length - 1] ?? urn;
18
19
  }
19
20
  function buildNode(node, config, nodeRefs) {
20
- if (!isComponentTypeNode(node)) {
21
+ if (!isComponentNode(node)) {
21
22
  if (typeof console !== "undefined") {
22
23
  console.warn(
23
- "[@contentful/experiences-sdk-core] Skipping Template-variant node \u2014 Templates are not supported in v1."
24
+ "[@contentful/experiences-sdk-core] Skipping Experience-Template-variant node \u2014 Experience Templates are not supported as nodes in v1."
24
25
  );
25
26
  }
26
27
  return null;
27
28
  }
28
- const componentTypeId = extractIdFromUrn(node.componentType.sys.urn);
29
+ const componentId = extractIdFromUrn(node.component.sys.urn);
29
30
  const slots = {};
30
31
  if (node.slots) {
31
32
  for (const [slotName, children] of Object.entries(node.slots)) {
32
33
  if (!Array.isArray(children)) {
33
34
  throw new TypeError(
34
- `Slot "${slotName}" on component "${componentTypeId}" must be an array of nodes.`
35
+ `Slot "${slotName}" on component "${componentId}" must be an array of nodes.`
35
36
  );
36
37
  }
37
38
  const built2 = [];
@@ -44,19 +45,47 @@ function buildNode(node, config, nodeRefs) {
44
45
  }
45
46
  }
46
47
  const built = {
47
- registration: { componentTypeId },
48
+ registration: { componentId },
48
49
  props: {
49
50
  content: { ...node.contentProperties ?? {} },
50
- design: { ...node.designProperties ?? {} }
51
+ // Resolved flat values are written by the pre-resolution pass below.
52
+ design: {},
53
+ designRaw: { ...node.designProperties ?? {} }
51
54
  },
52
55
  slots
53
56
  };
54
57
  if (node.id) built.nodeId = node.id;
55
- if (getResolver(config.components[componentTypeId])) {
58
+ if (getResolver(config.components[componentId])) {
56
59
  nodeRefs.push(built);
57
60
  }
58
61
  return built;
59
62
  }
63
+ function preResolveDesignProperties(design, viewports, fallbackViewportIndex, resolveToken) {
64
+ const cascaded = resolveDesignProperties(design, viewports, fallbackViewportIndex);
65
+ return applyTokenResolver(cascaded, resolveToken);
66
+ }
67
+ function warnUnresolvedTokens(label, unresolved, log) {
68
+ if (!unresolved.length || typeof console === "undefined") return;
69
+ console.warn(
70
+ `[@contentful/experiences] resolveToken returned undefined for token id(s) on "${label}": ${unresolved.join(", ")}. Resolved design (getDesignValues()) will omit those keys.`
71
+ );
72
+ log.log(`unresolved token id(s) on "${label}": ${unresolved.join(", ")}`);
73
+ }
74
+ function preResolveNodeTree(node, viewports, fallbackViewportIndex, resolveToken, log) {
75
+ const { props, unresolved } = preResolveDesignProperties(
76
+ node.props.designRaw,
77
+ viewports,
78
+ fallbackViewportIndex,
79
+ resolveToken
80
+ );
81
+ node.props.design = props;
82
+ warnUnresolvedTokens(node.registration.componentId, unresolved, log);
83
+ for (const children of Object.values(node.slots)) {
84
+ for (const child of children) {
85
+ preResolveNodeTree(child, viewports, fallbackViewportIndex, resolveToken, log);
86
+ }
87
+ }
88
+ }
60
89
  async function resolveExperience(payload, config, options = {}) {
61
90
  const log = createDebugLogger(options.debug, "core");
62
91
  log.lazy("resolveExperience called with payload", () => payload);
@@ -67,12 +96,12 @@ async function resolveExperience(payload, config, options = {}) {
67
96
  if (built !== null) nodes.push(built);
68
97
  }
69
98
  log.log(`built ${nodes.length} top-level node(s); ${nodeRefs.length} declare resolveData`);
70
- const templateUrn = payload.sys?.template?.sys.urn;
71
- let template;
72
- if (typeof templateUrn === "string" && templateUrn.length > 0) {
73
- template = {
74
- templateId: extractIdFromUrn(templateUrn),
75
- props: { content: {}, design: {} }
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: {} }
76
105
  };
77
106
  }
78
107
  const experience = {
@@ -85,11 +114,11 @@ async function resolveExperience(payload, config, options = {}) {
85
114
  };
86
115
  const tasks = [];
87
116
  for (const node of nodeRefs) {
88
- const resolver = getResolver(config.components[node.registration.componentTypeId]);
117
+ const resolver = getResolver(config.components[node.registration.componentId]);
89
118
  if (!resolver) continue;
90
119
  const ctx = {
91
120
  content: node.props.content,
92
- design: node.props.design,
121
+ design: node.props.designRaw,
93
122
  experience
94
123
  };
95
124
  tasks.push(
@@ -98,15 +127,17 @@ async function resolveExperience(payload, config, options = {}) {
98
127
  })
99
128
  );
100
129
  }
101
- if (template) {
102
- const tplResolver = getResolver(config.templates?.[template.templateId]);
130
+ if (experienceTemplate) {
131
+ const tplResolver = getResolver(
132
+ config.experienceTemplates?.[experienceTemplate.experienceTemplateId]
133
+ );
103
134
  if (tplResolver) {
104
135
  const ctx = {
105
- content: template.props.content,
106
- design: template.props.design,
136
+ content: experienceTemplate.props.content,
137
+ design: experienceTemplate.props.designRaw,
107
138
  experience
108
139
  };
109
- const tpl = template;
140
+ const tpl = experienceTemplate;
110
141
  tasks.push(
111
142
  Promise.resolve(tplResolver(ctx)).then((resolved) => {
112
143
  tpl.props.resolved = resolved;
@@ -117,10 +148,31 @@ async function resolveExperience(payload, config, options = {}) {
117
148
  if (tasks.length > 0) {
118
149
  await log.time(`${tasks.length} resolveData hook(s)`, () => Promise.all(tasks));
119
150
  }
151
+ const fallbackViewportId = options.initialViewportId ?? config.fallbackViewportId;
152
+ const fallbackViewportIndex = getViewportIndex(payload.viewports, fallbackViewportId);
153
+ for (const node of nodes) {
154
+ preResolveNodeTree(node, payload.viewports, fallbackViewportIndex, config.resolveToken, log);
155
+ }
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
+ log.log(`pre-resolved design against fallback viewport index ${fallbackViewportIndex}`);
120
171
  return {
121
172
  viewports: payload.viewports,
122
173
  nodes,
123
- ...template ? { template } : {}
174
+ ...experienceTemplate ? { experienceTemplate } : {},
175
+ fallbackViewportIndex
124
176
  };
125
177
  }
126
178
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/resolve-experience.ts"],"sourcesContent":["/*\n * Single async entry that turns an XDA Experience payload into a\n * runtime-neutral PortableRenderPlan ready to render.\n *\n * v1 behavior:\n * - Walk the payload's nodes recursively. Each ComponentType node becomes\n * a PortableRenderNode with `registration.componentTypeId` extracted\n * from `componentType.sys.urn` (last slash-segment).\n * - Split content + design properties onto `node.props.{content,design}`.\n * Design-prop envelopes (DesignToken / ManualDesignValue / ValuesByViewport)\n * are preserved on the IR; the design package unwraps them at render time.\n * - Template-variant nodes are skipped with a console.warn — out of v1 scope.\n * - For every component whose registration declares `resolveData`, run the\n * resolver (sync or async) in parallel with peers, and attach the result\n * to `node.props.resolved`.\n * - Unknown component-type-id is a render-time concern (handled by the\n * framework adapter via `renderUnknown`); the IR still emits the node.\n */\n\nimport { createDebugLogger } from './debug-logger';\nimport type {\n ComponentTypeNode,\n DesignPropValue,\n ExperienceContext,\n ExperienceNode,\n ExperiencePayload,\n PortableRenderNode,\n PortableRenderPlan,\n PortableTemplate,\n ResolveContext,\n} from './types';\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 templates?: Record<string, unknown>;\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`. Flattened to a top-level option (was nested\n * under `experience`). 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\nconst DEFAULT_EXPERIENCE: ExperienceContext = {\n debug: false,\n metadata: {},\n viewports: [],\n};\n\nfunction isComponentTypeNode(node: ExperienceNode): node is ComponentTypeNode {\n return 'componentType' in node;\n}\n\n/**\n * Extract the flat id (componentType or template) from its `ResourceLink`\n * URN. Real URN shapes:\n * crn:contentful:::experience:spaces/$self/environments/$self/componentTypes/<id>\n * crn:contentful:::experience:spaces/$self/environments/$self/templates/<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 (!isComponentTypeNode(node)) {\n if (typeof console !== 'undefined') {\n console.warn(\n '[@contentful/experiences-sdk-core] Skipping Template-variant node — Templates are not supported in v1.'\n );\n }\n return null;\n }\n\n const componentTypeId = extractIdFromUrn(node.componentType.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 \"${componentTypeId}\" 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: { componentTypeId },\n props: {\n content: { ...(node.contentProperties ?? {}) },\n design: { ...(node.designProperties ?? {}) } as Record<string, DesignPropValue>,\n },\n slots,\n };\n if (node.id) built.nodeId = node.id;\n if (getResolver(config.components[componentTypeId])) {\n nodeRefs.push(built);\n }\n return built;\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 template stub if the payload carries one. XDA\n // payloads don't yet emit template-level content/design properties, so\n // the IR carries empty bags.\n const templateUrn = payload.sys?.template?.sys.urn;\n let template: PortableTemplate | undefined;\n if (typeof templateUrn === 'string' && templateUrn.length > 0) {\n template = {\n templateId: extractIdFromUrn(templateUrn),\n props: { content: {}, design: {} },\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.componentTypeId]);\n if (!resolver) continue;\n const ctx: ResolveContext = {\n content: node.props.content,\n design: node.props.design,\n experience,\n };\n tasks.push(\n Promise.resolve(resolver(ctx)).then((resolved) => {\n node.props.resolved = resolved;\n })\n );\n }\n\n if (template) {\n const tplResolver = getResolver(config.templates?.[template.templateId]);\n if (tplResolver) {\n const ctx: ResolveContext = {\n content: template.props.content,\n design: template.props.design,\n experience,\n };\n const tpl = template;\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 return {\n viewports: payload.viewports,\n nodes,\n ...(template ? { template } : {}),\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 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"]}
package/dist/types.d.ts CHANGED
@@ -12,10 +12,9 @@
12
12
  * `debug` is the single observability switch. When on it: emits verbose logs
13
13
  * from `resolveExperience` and `fetchExperience`; renders the visible
14
14
  * missing-component box (see the adapters' `MissingComponent`); and turns the
15
- * default `renderUnknown` fallback into the richer debug component. It replaced
16
- * the old `isPreview` render flag — a single boolean that threads through both
17
- * fetch and render so a customer can't enable one half and be confused by the
18
- * other.
15
+ * default `renderUnknown` fallback into the richer debug component. One boolean
16
+ * threads through both fetch and render, so a customer can't enable one half
17
+ * and be confused by the other.
19
18
  */
20
19
  interface ExperienceContext {
21
20
  debug: boolean;
@@ -39,7 +38,7 @@ interface ViewportDef {
39
38
  /**
40
39
  * Discriminated design-property value as it arrives from XDA. v1 accepts:
41
40
  * - ManualDesignValue: an explicit scalar (no viewport involved).
42
- * - ValuesByViewport: a viewport-keyed bag where each entry is itself a
41
+ * - ValuesByViewport: a viewport-keyed map where each entry is itself a
43
42
  * ManualDesignValue or DesignToken.
44
43
  * - DesignToken: a token reference, passed through to customer components
45
44
  * as-is for v1. Resolution lands in the future tokens package.
@@ -54,7 +53,7 @@ interface DesignToken {
54
53
  value: string;
55
54
  }
56
55
  /**
57
- * Turns a `DesignToken` envelope into a runtime value. `ref.value` is the
56
+ * Turns a `DesignToken` into a runtime value. `ref.value` is the
58
57
  * customer-defined token id; returning `undefined` means "not resolvable" and
59
58
  * the adapter drops the key (with a warning). Sync only — it runs at render time.
60
59
  */
@@ -64,43 +63,52 @@ interface ValuesByViewport {
64
63
  values: Record<string, ManualDesignValue | DesignToken>;
65
64
  }
66
65
  /**
67
- * Resource-link reference to a registered Component Type. The `urn` carries
68
- * the type id; the build-plan extracts the id by taking the segment after
66
+ * Resource-link reference to a registered Component. The `urn` carries
67
+ * the component id; the build-plan extracts the id by taking the segment after
69
68
  * the last slash.
69
+ *
70
+ * Mirrors `ComponentLink` from `@contentful/experience-delivery`.
70
71
  */
71
- interface ComponentTypeRef {
72
+ interface ComponentRef {
72
73
  sys: {
73
74
  type: 'ResourceLink';
74
- linkType: 'Contentful:ComponentType';
75
+ linkType: 'Contentful:Component';
75
76
  urn: string;
76
77
  };
77
78
  }
78
79
  /**
79
- * Resource-link reference to a Template. Templates are out of v1 scope and
80
- * are skipped at plan-build time with a diagnostic.
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.
83
+ *
84
+ * Mirrors `ExperienceTemplateLink` from `@contentful/experience-delivery`.
81
85
  */
82
- interface TemplateRef {
86
+ interface ExperienceTemplateRef {
83
87
  sys: {
84
88
  type: 'ResourceLink';
85
- linkType: 'Contentful:Template';
89
+ linkType: 'Contentful:ExperienceTemplate';
86
90
  urn: string;
87
91
  };
88
92
  }
89
93
  /**
90
- * One node from `GetExperienceViewResponse.nodes` (or any `slots[name]`).
91
- * Discriminated by which of `componentType` / `template` is present.
94
+ * One node from `HydratedExperienceView.nodes` (or any `slots[name]`).
95
+ * Discriminated by which of `component` / `experienceTemplate` is present.
96
+ *
97
+ * Mirrors `RenamedHydratedTreeNode` from `@contentful/experience-delivery`.
92
98
  */
93
- type ExperienceNode = ComponentTypeNode | TemplateNode;
94
- interface ComponentTypeNode {
95
- componentType: ComponentTypeRef;
99
+ type ExperienceNode = ComponentNode | ExperienceTemplateNode;
100
+ /** Mirrors `RenamedComponentTreeNode` from `@contentful/experience-delivery`. */
101
+ interface ComponentNode {
102
+ component: ComponentRef;
96
103
  id?: string;
97
104
  contentProperties?: Record<string, unknown>;
98
105
  designProperties?: Record<string, DesignPropValue>;
99
106
  slots?: Record<string, ExperienceNode[]>;
100
107
  contentBindings?: string;
101
108
  }
102
- interface TemplateNode {
103
- template: TemplateRef;
109
+ /** Mirrors `RenamedTemplateTreeNode` from `@contentful/experience-delivery`. */
110
+ interface ExperienceTemplateNode {
111
+ experienceTemplate: ExperienceTemplateRef;
104
112
  id?: string;
105
113
  contentProperties?: Record<string, unknown>;
106
114
  designProperties?: Record<string, DesignPropValue>;
@@ -111,22 +119,32 @@ interface TemplateNode {
111
119
  * Top-level `sys` block on an Experience payload. The bits the SDK actually
112
120
  * reads are typed; everything else is left loose because the upstream
113
121
  * type carries dozens of editor/audit fields the renderer doesn't care about.
122
+ *
123
+ * Mirrors the parts of `RenamedDeliveryExperienceSys` the renderer reads.
114
124
  */
115
125
  interface ExperienceSys {
116
126
  /**
117
- * Optional page-level template reference. When present, the renderer wraps
127
+ * Page-level Experience Template reference. When present, the renderer wraps
118
128
  * the experience nodes with the matching template registered in the
119
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.
120
135
  */
121
- template?: TemplateRef;
136
+ experienceTemplate?: ExperienceTemplateRef;
122
137
  [key: string]: unknown;
123
138
  }
124
139
  /**
125
140
  * Top-level Experience payload as returned by the Experience Delivery API
126
- * (`GetExperienceViewResponse` from `@contentful/experience-delivery`).
141
+ * (`HydratedExperienceView` from `@contentful/experience-delivery`).
127
142
  *
128
143
  * Structurally compatible with the upstream type — no normalization step
129
- * required when consuming a delivery-client response.
144
+ * required when consuming a delivery-client response. The delivery API returns
145
+ * this shape when the request carries the
146
+ * `x-contentful-enable-alpha-feature: new-exo-entity-types` header, which
147
+ * `@contentful/experiences-client` sends on every request.
130
148
  */
131
149
  interface ExperiencePayload {
132
150
  viewports: ViewportDef[];
@@ -137,7 +155,7 @@ interface ExperiencePayload {
137
155
  }
138
156
  /**
139
157
  * Per-node context handed to a component's `resolveData` resolver. Carries
140
- * the raw content + design props from the payload (design envelopes are NOT
158
+ * the raw content + design props from the payload (design properties are NOT
141
159
  * pre-resolved against a viewport — viewport resolution stays a render-time
142
160
  * concern so client viewport changes don't re-trigger async resolvers).
143
161
  */
@@ -149,24 +167,29 @@ interface ResolveContext {
149
167
  /**
150
168
  * Registration metadata for a single instance — the SDK's interpreted
151
169
  * pointer to the customer's component implementation. Today carries only
152
- * the resolved component-type id; capabilities (state requirements,
170
+ * the resolved component id; capabilities (state requirements,
153
171
  * supported events, lifecycle hints, fallback ids) land here when needed.
154
172
  */
155
173
  interface PortableRegistration {
156
- componentTypeId: string;
174
+ componentId: string;
157
175
  }
158
176
  /**
159
177
  * The IR — one node per component instance. The seam that lets non-React
160
178
  * adapters (Angular, SwiftUI, Compose) consume the same interpretation.
161
179
  *
162
- * Design props preserve the discriminated envelope as they arrived. Adapters
180
+ * Design props preserve the discriminated value shape as they arrived. Adapters
163
181
  * unwrap to plain scalars at render time, given an active viewport.
164
- * (DesignToken envelopes pass through unwrapped — customer components decide
182
+ * (DesignToken values pass through unwrapped — customer components decide
165
183
  * how to resolve them in v1.)
166
184
  *
167
185
  * `props.resolved` is populated by `resolveExperience` from any
168
- * customer-supplied `resolveData` resolver and merged into the final prop bag
186
+ * customer-supplied `resolveData` resolver and merged into the final props
169
187
  * after content + design but before slot props.
188
+ *
189
+ * `props.design` is the server pre-resolution of design against the plan's
190
+ * fallback viewport; the raw per-viewport form stays on `props.designRaw` so
191
+ * the client can re-resolve when the active viewport differs. See the fields
192
+ * below.
170
193
  */
171
194
  interface PortableRenderNode {
172
195
  /**
@@ -178,27 +201,34 @@ interface PortableRenderNode {
178
201
  registration: PortableRegistration;
179
202
  props: {
180
203
  content: Record<string, unknown>;
181
- design: Record<string, DesignPropValue>;
204
+ /** Flat, viewport-cascaded, token-resolved design values (server-side). */
205
+ design: Record<string, unknown>;
182
206
  resolved?: Record<string, unknown>;
207
+ /** Raw per-viewport design, for client re-resolution on viewport change. */
208
+ designRaw: Record<string, DesignPropValue>;
183
209
  };
184
210
  slots: Record<string, PortableRenderNode[]>;
185
211
  }
186
212
  /**
187
- * Interpreted page-level template — the optional wrapper around the
188
- * experience tree. `templateId` is extracted from
189
- * `payload.sys.template.sys.urn` (last slash-segment).
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).
190
216
  *
191
- * Templates carry the same prop-resolution shape as components: content +
192
- * design envelopes plus an optional `resolved` bag from a `resolveData` hook.
193
- * v1 payloads from XDA don't carry template-level content/design properties
194
- * yet, but the IR makes room for them so the API doesn't need to break later.
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.
195
222
  */
196
- interface PortableTemplate {
197
- templateId: string;
223
+ interface PortableExperienceTemplate {
224
+ experienceTemplateId: string;
198
225
  props: {
199
226
  content: Record<string, unknown>;
200
- design: Record<string, DesignPropValue>;
227
+ /** Same as `PortableRenderNode.props.design`. */
228
+ design: Record<string, unknown>;
201
229
  resolved?: Record<string, unknown>;
230
+ /** Same as `PortableRenderNode.props.designRaw`. */
231
+ designRaw: Record<string, DesignPropValue>;
202
232
  };
203
233
  }
204
234
  /**
@@ -206,14 +236,20 @@ interface PortableTemplate {
206
236
  *
207
237
  * Top-level is `nodes: PortableRenderNode[]` (array, not single root) to
208
238
  * match the actual XDA payload shape. Renderers iterate top-level nodes
209
- * and recurse into `node.slots`. When `template` is present, the renderer
210
- * wraps the nodes with the matching template config; otherwise nodes
211
- * render at the top level.
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.
212
242
  */
213
243
  interface PortableRenderPlan {
214
244
  viewports: ViewportDef[];
215
245
  nodes: PortableRenderNode[];
216
- template?: PortableTemplate;
246
+ experienceTemplate?: PortableExperienceTemplate;
247
+ /**
248
+ * Viewport index the server pre-resolved design against (viewport[0] by
249
+ * default). Adapters use `props.design` as-is when their active viewport
250
+ * matches this, and recompute from `props.designRaw` otherwise.
251
+ */
252
+ fallbackViewportIndex: number;
217
253
  }
218
254
 
219
- export type { ComponentTypeNode, ComponentTypeRef, DesignPropValue, DesignToken, ExperienceContext, ExperienceNode, ExperiencePayload, ExperienceSys, ManualDesignValue, PortableRegistration, PortableRenderNode, PortableRenderPlan, PortableTemplate, ResolveContext, ResolveToken, TemplateNode, TemplateRef, ValuesByViewport, ViewportDef };
255
+ export type { ComponentNode, ComponentRef, DesignPropValue, DesignToken, ExperienceContext, ExperienceNode, ExperiencePayload, ExperienceSys, ExperienceTemplateNode, ExperienceTemplateRef, ManualDesignValue, PortableExperienceTemplate, PortableRegistration, PortableRenderNode, PortableRenderPlan, ResolveContext, ResolveToken, ValuesByViewport, ViewportDef };
@@ -0,0 +1,20 @@
1
+ import { DesignToken, ResolveToken, DesignPropValue, ViewportDef } from './types.js';
2
+
3
+ /** Viewport id → index. Returns 0 (the wildcard viewport) when unknown. */
4
+ declare function getViewportIndex(viewports: ViewportDef[], viewportId?: string): number;
5
+ /** Resolve one design property to its render-time value (cascade + unwrap). */
6
+ declare function getValueForViewport(prop: DesignPropValue | undefined, viewports: ViewportDef[], activeViewportIndex: number): string | number | boolean | DesignToken | undefined;
7
+ /** Resolve every design property on a node into a flat record keyed by name. */
8
+ declare function resolveDesignProperties(designProperties: Record<string, DesignPropValue> | undefined, viewports: ViewportDef[], activeViewportIndex: number): Record<string, string | number | boolean | DesignToken>;
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).
14
+ */
15
+ declare function applyTokenResolver(props: Record<string, string | number | boolean | DesignToken>, resolveToken?: ResolveToken): {
16
+ props: Record<string, unknown>;
17
+ unresolved: string[];
18
+ };
19
+
20
+ export { applyTokenResolver, getValueForViewport, getViewportIndex, resolveDesignProperties };
@@ -0,0 +1,71 @@
1
+ function getViewportIndex(viewports, viewportId) {
2
+ if (!viewportId) return 0;
3
+ const index = viewports.findIndex((v) => v.id === viewportId);
4
+ return index === -1 ? 0 : index;
5
+ }
6
+ function unwrapInner(inner) {
7
+ if (!inner) return void 0;
8
+ if (inner.type === "ManualDesignValue") return inner.value;
9
+ return inner;
10
+ }
11
+ function resolveValuesByViewport(valuesByViewport, viewports, activeViewportIndex) {
12
+ for (let i = activeViewportIndex; i >= 0; i--) {
13
+ const viewport = viewports[i];
14
+ if (!viewport) continue;
15
+ const candidate = valuesByViewport.values[viewport.id];
16
+ if (candidate !== void 0 && candidate !== null) return candidate;
17
+ }
18
+ return void 0;
19
+ }
20
+ function getValueForViewport(prop, viewports, activeViewportIndex) {
21
+ if (!prop) return void 0;
22
+ if (prop.type === "ManualDesignValue") return prop.value;
23
+ if (prop.type === "DesignToken") return prop;
24
+ return unwrapInner(resolveValuesByViewport(prop, viewports, activeViewportIndex));
25
+ }
26
+ function resolveDesignProperties(designProperties, viewports, activeViewportIndex) {
27
+ const out = {};
28
+ if (!designProperties) return out;
29
+ for (const [key, prop] of Object.entries(designProperties)) {
30
+ const value = getValueForViewport(prop, viewports, activeViewportIndex);
31
+ if (value !== void 0) out[key] = value;
32
+ }
33
+ return out;
34
+ }
35
+ let warnedMissingResolver = false;
36
+ 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
+ const out = {};
50
+ const unresolved = [];
51
+ for (const [key, value] of Object.entries(props)) {
52
+ if (typeof value === "object" && value !== null && value.type === "DesignToken") {
53
+ const resolved = resolveToken(value);
54
+ if (resolved === void 0) {
55
+ unresolved.push(value.value);
56
+ continue;
57
+ }
58
+ out[key] = resolved;
59
+ continue;
60
+ }
61
+ out[key] = value;
62
+ }
63
+ return { props: out, unresolved };
64
+ }
65
+ export {
66
+ applyTokenResolver,
67
+ getValueForViewport,
68
+ getViewportIndex,
69
+ resolveDesignProperties
70
+ };
71
+ //# sourceMappingURL=viewport.js.map
@@ -0,0 +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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experiences-sdk-core",
3
- "version": "0.6.1",
3
+ "version": "0.7.1",
4
4
  "description": "Runtime-neutral types + experience resolution for Contentful Experiences",
5
5
  "license": "MIT",
6
6
  "type": "module",