@contentful/experiences-sdk-core 0.6.1 → 0.7.0
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 +87 -0
- package/README.md +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/resolve-experience.d.ts +2 -3
- package/dist/resolve-experience.js +22 -20
- package/dist/resolve-experience.js.map +1 -1
- package/dist/types.d.ts +58 -39
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,90 @@
|
|
|
1
|
+
## 0.7.0 (2026-08-07)
|
|
2
|
+
|
|
3
|
+
### 🩹 Fixes
|
|
4
|
+
|
|
5
|
+
- ⚠️ bump @contentful/experience-delivery to 1.0.0-dev.6 + migrate to renamed API [AIS-339] ([#120](https://github.com/contentful/experiences/pull/120))
|
|
6
|
+
|
|
7
|
+
### ⚠️ Breaking Changes
|
|
8
|
+
|
|
9
|
+
- bump @contentful/experience-delivery to 1.0.0-dev.6 + migrate to renamed API [AIS-339] ([#120](https://github.com/contentful/experiences/pull/120))
|
|
10
|
+
the public API uses the component / experienceTemplate vocabulary.
|
|
11
|
+
- `registration.componentTypeId` -> `registration.componentId`
|
|
12
|
+
- `plan.template` -> `plan.experienceTemplate`
|
|
13
|
+
- `PortableTemplate` -> `PortableExperienceTemplate` (`templateId` ->
|
|
14
|
+
`experienceTemplateId`)
|
|
15
|
+
- `Config.templates` -> `Config.experienceTemplates`; `Templates` ->
|
|
16
|
+
`ExperienceTemplates`
|
|
17
|
+
- `defineTemplate` -> `defineExperienceTemplate`; `TemplateConfig` ->
|
|
18
|
+
`ExperienceTemplateConfig`; `TemplateRegistration` ->
|
|
19
|
+
`ExperienceTemplateRegistration`; `normalizeTemplateRegistration` ->
|
|
20
|
+
`normalizeExperienceTemplateRegistration`
|
|
21
|
+
- `useContentfulTemplate()` / `getContentfulTemplate()` ->
|
|
22
|
+
`useContentfulExperienceTemplate()` / `getContentfulExperienceTemplate()`;
|
|
23
|
+
`ContentfulTemplate` -> `ContentfulExperienceTemplate`
|
|
24
|
+
- `MissingComponentProps.componentTypeId` -> `componentId`
|
|
25
|
+
- Core payload types: `ComponentTypeNode`/`ComponentTypeRef` ->
|
|
26
|
+
`ComponentNode`/`ComponentRef`; `TemplateNode`/`TemplateRef` ->
|
|
27
|
+
`ExperienceTemplateNode`/`ExperienceTemplateRef`
|
|
28
|
+
`packages/core` stays zero-dep, so its payload types are hand-mirrored from the
|
|
29
|
+
delivery package rather than imported; each carries a doc comment naming its
|
|
30
|
+
upstream counterpart, and `client:typecheck` catches drift when the delivery SDK
|
|
31
|
+
regenerates. Documented in AGENTS.md.
|
|
32
|
+
`examples/scripts/` is intentionally unchanged: it targets the management SDK
|
|
33
|
+
(`contentful-management`), which has its own entity-type surface, and adopting it
|
|
34
|
+
would require changing that dependency. The seed script therefore still
|
|
35
|
+
provisions the shapes that SDK produces.
|
|
36
|
+
Verified: build, typecheck, and 157 tests pass across all five packages.
|
|
37
|
+
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
38
|
+
* docs: describe current state only in comments and docs
|
|
39
|
+
Drop ticket ids, tracker links, and before/after narrative from doc comments and
|
|
40
|
+
prose so they explain what the code does now rather than how it got here.
|
|
41
|
+
- `alpha-feature.ts`: state what the header selects and why it is required,
|
|
42
|
+
without the two-shapes-in-transition framing or the removal timeline.
|
|
43
|
+
- `core/src/types.ts`: keep each type's upstream counterpart in
|
|
44
|
+
`@contentful/experience-delivery` (load-bearing for maintenance) but drop the
|
|
45
|
+
"formerly known as" and old-link-type asides.
|
|
46
|
+
- README: replace the rename migration table with a description of the
|
|
47
|
+
alpha-feature header and when a caller needs to send it.
|
|
48
|
+
- AGENTS.md: reframe the rationale entries that opened on prior designs to state
|
|
49
|
+
the current design and its reason.
|
|
50
|
+
- Drop stale option-shape asides ("was nested under ...") and a test name that
|
|
51
|
+
described superseded behaviour.
|
|
52
|
+
Generated CHANGELOG.md files are untouched — they are release history.
|
|
53
|
+
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
54
|
+
* feat!: migrate bootstrap fixture script to component/experienceTemplate entities
|
|
55
|
+
contentful-management@12.14.0 exposes the renamed ExO entities (Component,
|
|
56
|
+
ExperienceTemplate) alongside the deprecated ComponentType/Template ones. Adopt
|
|
57
|
+
the renamed shapes throughout examples/scripts so a freshly bootstrapped demo
|
|
58
|
+
Experience uses the same entity vocabulary the delivery-consuming packages now
|
|
59
|
+
read.
|
|
60
|
+
- Bump contentful-management to ^12.14.0 (existing dependency, version only —
|
|
61
|
+
no new packages).
|
|
62
|
+
- fixture/component-types.ts -> fixture/components.ts: ComponentTypeFixture ->
|
|
63
|
+
ComponentFixture.
|
|
64
|
+
- fixture/templates.ts -> fixture/experience-templates.ts:
|
|
65
|
+
TemplateFixture -> ExperienceTemplateFixture, TemplateTreeNode ->
|
|
66
|
+
ExperienceTemplateTreeNode.
|
|
67
|
+
- fixture/types.ts, fixture/experience.ts: ExperienceFixture.templateId ->
|
|
68
|
+
experienceTemplateId; node nodeType 'InlineFragment' ->
|
|
69
|
+
'InlineExperienceFragment' with componentTypeId -> componentId, matching
|
|
70
|
+
contentful-management's InlineExperienceFragmentNode.
|
|
71
|
+
- fixture/data-assemblies.ts: dataAssemblyComponentTypeLinks ->
|
|
72
|
+
dataAssemblyComponentLinks (componentTypeId -> componentId).
|
|
73
|
+
- bootstrap-example.ts: cma.componentType -> cma.component,
|
|
74
|
+
cma.template -> cma.experienceTemplate; sys.type 'ComponentType' ->
|
|
75
|
+
'Component', 'Template' -> 'ExperienceTemplate'; resource-link types
|
|
76
|
+
Contentful:ComponentType -> Contentful:Component and Contentful:Template ->
|
|
77
|
+
Contentful:ExperienceTemplate; URN segments components/ and
|
|
78
|
+
experienceTemplates/; seedComponentType -> seedComponent, seedTemplate ->
|
|
79
|
+
seedExperienceTemplate, linkDataAssembliesToComponentTypes ->
|
|
80
|
+
linkDataAssembliesToComponents.
|
|
81
|
+
The metadata.annotations.Template composed/coded-implementation marker is
|
|
82
|
+
unrelated to this rename (identical shape in 12.10.0 and 12.14.0) and is left
|
|
83
|
+
untouched.
|
|
84
|
+
Verified: examples/scripts typechecks clean; the main workspace's build,
|
|
85
|
+
typecheck, and 157 tests are unaffected.
|
|
86
|
+
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
87
|
+
|
|
1
88
|
## 0.6.1 (2026-08-06)
|
|
2
89
|
|
|
3
90
|
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`, `
|
|
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,3 @@
|
|
|
1
|
-
export {
|
|
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';
|
|
@@ -13,13 +13,12 @@ import { ExperiencePayload, PortableRenderPlan } from './types.js';
|
|
|
13
13
|
*/
|
|
14
14
|
interface ResolverConfig {
|
|
15
15
|
components: Record<string, unknown>;
|
|
16
|
-
|
|
16
|
+
experienceTemplates?: Record<string, unknown>;
|
|
17
17
|
}
|
|
18
18
|
interface ResolveExperienceOptions {
|
|
19
19
|
/**
|
|
20
20
|
* Arbitrary per-render metadata exposed to every resolver as
|
|
21
|
-
* `ctx.experience.metadata`.
|
|
22
|
-
* under `experience`). Defaults to `{}`.
|
|
21
|
+
* `ctx.experience.metadata`. Defaults to `{}`.
|
|
23
22
|
*/
|
|
24
23
|
metadata?: Record<string, unknown>;
|
|
25
24
|
/**
|
|
@@ -9,29 +9,29 @@ const DEFAULT_EXPERIENCE = {
|
|
|
9
9
|
metadata: {},
|
|
10
10
|
viewports: []
|
|
11
11
|
};
|
|
12
|
-
function
|
|
13
|
-
return "
|
|
12
|
+
function isComponentNode(node) {
|
|
13
|
+
return "component" in node;
|
|
14
14
|
}
|
|
15
15
|
function extractIdFromUrn(urn) {
|
|
16
16
|
const segments = urn.split("/").filter((s) => s.length > 0);
|
|
17
17
|
return segments[segments.length - 1] ?? urn;
|
|
18
18
|
}
|
|
19
19
|
function buildNode(node, config, nodeRefs) {
|
|
20
|
-
if (!
|
|
20
|
+
if (!isComponentNode(node)) {
|
|
21
21
|
if (typeof console !== "undefined") {
|
|
22
22
|
console.warn(
|
|
23
|
-
"[@contentful/experiences-sdk-core] Skipping Template-variant node \u2014 Templates are not supported in v1."
|
|
23
|
+
"[@contentful/experiences-sdk-core] Skipping Experience-Template-variant node \u2014 Experience Templates are not supported as nodes in v1."
|
|
24
24
|
);
|
|
25
25
|
}
|
|
26
26
|
return null;
|
|
27
27
|
}
|
|
28
|
-
const
|
|
28
|
+
const componentId = extractIdFromUrn(node.component.sys.urn);
|
|
29
29
|
const slots = {};
|
|
30
30
|
if (node.slots) {
|
|
31
31
|
for (const [slotName, children] of Object.entries(node.slots)) {
|
|
32
32
|
if (!Array.isArray(children)) {
|
|
33
33
|
throw new TypeError(
|
|
34
|
-
`Slot "${slotName}" on component "${
|
|
34
|
+
`Slot "${slotName}" on component "${componentId}" must be an array of nodes.`
|
|
35
35
|
);
|
|
36
36
|
}
|
|
37
37
|
const built2 = [];
|
|
@@ -44,7 +44,7 @@ function buildNode(node, config, nodeRefs) {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
const built = {
|
|
47
|
-
registration: {
|
|
47
|
+
registration: { componentId },
|
|
48
48
|
props: {
|
|
49
49
|
content: { ...node.contentProperties ?? {} },
|
|
50
50
|
design: { ...node.designProperties ?? {} }
|
|
@@ -52,7 +52,7 @@ function buildNode(node, config, nodeRefs) {
|
|
|
52
52
|
slots
|
|
53
53
|
};
|
|
54
54
|
if (node.id) built.nodeId = node.id;
|
|
55
|
-
if (getResolver(config.components[
|
|
55
|
+
if (getResolver(config.components[componentId])) {
|
|
56
56
|
nodeRefs.push(built);
|
|
57
57
|
}
|
|
58
58
|
return built;
|
|
@@ -67,11 +67,11 @@ async function resolveExperience(payload, config, options = {}) {
|
|
|
67
67
|
if (built !== null) nodes.push(built);
|
|
68
68
|
}
|
|
69
69
|
log.log(`built ${nodes.length} top-level node(s); ${nodeRefs.length} declare resolveData`);
|
|
70
|
-
const
|
|
71
|
-
let
|
|
72
|
-
if (typeof
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
const experienceTemplateUrn = payload.sys?.experienceTemplate?.sys.urn;
|
|
71
|
+
let experienceTemplate;
|
|
72
|
+
if (typeof experienceTemplateUrn === "string" && experienceTemplateUrn.length > 0) {
|
|
73
|
+
experienceTemplate = {
|
|
74
|
+
experienceTemplateId: extractIdFromUrn(experienceTemplateUrn),
|
|
75
75
|
props: { content: {}, design: {} }
|
|
76
76
|
};
|
|
77
77
|
}
|
|
@@ -85,7 +85,7 @@ async function resolveExperience(payload, config, options = {}) {
|
|
|
85
85
|
};
|
|
86
86
|
const tasks = [];
|
|
87
87
|
for (const node of nodeRefs) {
|
|
88
|
-
const resolver = getResolver(config.components[node.registration.
|
|
88
|
+
const resolver = getResolver(config.components[node.registration.componentId]);
|
|
89
89
|
if (!resolver) continue;
|
|
90
90
|
const ctx = {
|
|
91
91
|
content: node.props.content,
|
|
@@ -98,15 +98,17 @@ async function resolveExperience(payload, config, options = {}) {
|
|
|
98
98
|
})
|
|
99
99
|
);
|
|
100
100
|
}
|
|
101
|
-
if (
|
|
102
|
-
const tplResolver = getResolver(
|
|
101
|
+
if (experienceTemplate) {
|
|
102
|
+
const tplResolver = getResolver(
|
|
103
|
+
config.experienceTemplates?.[experienceTemplate.experienceTemplateId]
|
|
104
|
+
);
|
|
103
105
|
if (tplResolver) {
|
|
104
106
|
const ctx = {
|
|
105
|
-
content:
|
|
106
|
-
design:
|
|
107
|
+
content: experienceTemplate.props.content,
|
|
108
|
+
design: experienceTemplate.props.design,
|
|
107
109
|
experience
|
|
108
110
|
};
|
|
109
|
-
const tpl =
|
|
111
|
+
const tpl = experienceTemplate;
|
|
110
112
|
tasks.push(
|
|
111
113
|
Promise.resolve(tplResolver(ctx)).then((resolved) => {
|
|
112
114
|
tpl.props.resolved = resolved;
|
|
@@ -120,7 +122,7 @@ async function resolveExperience(payload, config, options = {}) {
|
|
|
120
122
|
return {
|
|
121
123
|
viewports: payload.viewports,
|
|
122
124
|
nodes,
|
|
123
|
-
...
|
|
125
|
+
...experienceTemplate ? { experienceTemplate } : {}
|
|
124
126
|
};
|
|
125
127
|
}
|
|
126
128
|
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 * 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 Component node becomes\n * a PortableRenderNode with `registration.componentId` extracted\n * from `component.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 * - Experience-Template-variant nodes are skipped with a console.warn — out\n * 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 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 ComponentNode,\n DesignPropValue,\n ExperienceContext,\n ExperienceNode,\n ExperiencePayload,\n PortableExperienceTemplate,\n PortableRenderNode,\n PortableRenderPlan,\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 experienceTemplates?: 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`. 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 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 design: { ...(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/**\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: {} },\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.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 (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.design,\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 return {\n viewports: payload.viewports,\n nodes,\n ...(experienceTemplate ? { experienceTemplate } : {}),\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.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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;
|
|
@@ -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
|
|
68
|
-
* the
|
|
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
|
|
72
|
+
interface ComponentRef {
|
|
72
73
|
sys: {
|
|
73
74
|
type: 'ResourceLink';
|
|
74
|
-
linkType: 'Contentful:
|
|
75
|
+
linkType: 'Contentful:Component';
|
|
75
76
|
urn: string;
|
|
76
77
|
};
|
|
77
78
|
}
|
|
78
79
|
/**
|
|
79
|
-
* Resource-link reference to
|
|
80
|
-
* are skipped at plan-build time with a
|
|
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
|
|
86
|
+
interface ExperienceTemplateRef {
|
|
83
87
|
sys: {
|
|
84
88
|
type: 'ResourceLink';
|
|
85
|
-
linkType: 'Contentful:
|
|
89
|
+
linkType: 'Contentful:ExperienceTemplate';
|
|
86
90
|
urn: string;
|
|
87
91
|
};
|
|
88
92
|
}
|
|
89
93
|
/**
|
|
90
|
-
* One node from `
|
|
91
|
-
* Discriminated by which of `
|
|
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 =
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
* (`
|
|
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[];
|
|
@@ -149,11 +167,11 @@ 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
|
|
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
|
-
|
|
174
|
+
componentId: string;
|
|
157
175
|
}
|
|
158
176
|
/**
|
|
159
177
|
* The IR — one node per component instance. The seam that lets non-React
|
|
@@ -184,17 +202,18 @@ interface PortableRenderNode {
|
|
|
184
202
|
slots: Record<string, PortableRenderNode[]>;
|
|
185
203
|
}
|
|
186
204
|
/**
|
|
187
|
-
* Interpreted page-level
|
|
188
|
-
* experience tree. `
|
|
189
|
-
* `payload.sys.
|
|
205
|
+
* Interpreted page-level Experience Template — the optional wrapper around the
|
|
206
|
+
* experience tree. `experienceTemplateId` is extracted from
|
|
207
|
+
* `payload.sys.experienceTemplate.sys.urn` (last slash-segment).
|
|
190
208
|
*
|
|
191
|
-
* Templates carry the same prop-resolution shape as components:
|
|
192
|
-
* design envelopes plus an optional `resolved` bag from a
|
|
193
|
-
* v1 payloads from XDA don't carry template-level
|
|
194
|
-
* yet, but the IR makes room for them so the API
|
|
209
|
+
* Experience Templates carry the same prop-resolution shape as components:
|
|
210
|
+
* content + design envelopes plus an optional `resolved` bag from a
|
|
211
|
+
* `resolveData` hook. v1 payloads from XDA don't carry template-level
|
|
212
|
+
* content/design properties yet, but the IR makes room for them so the API
|
|
213
|
+
* doesn't need to break later.
|
|
195
214
|
*/
|
|
196
|
-
interface
|
|
197
|
-
|
|
215
|
+
interface PortableExperienceTemplate {
|
|
216
|
+
experienceTemplateId: string;
|
|
198
217
|
props: {
|
|
199
218
|
content: Record<string, unknown>;
|
|
200
219
|
design: Record<string, DesignPropValue>;
|
|
@@ -206,14 +225,14 @@ interface PortableTemplate {
|
|
|
206
225
|
*
|
|
207
226
|
* Top-level is `nodes: PortableRenderNode[]` (array, not single root) to
|
|
208
227
|
* match the actual XDA payload shape. Renderers iterate top-level nodes
|
|
209
|
-
* and recurse into `node.slots`. When `
|
|
210
|
-
* wraps the nodes with the matching
|
|
211
|
-
* render at the top level.
|
|
228
|
+
* and recurse into `node.slots`. When `experienceTemplate` is present, the
|
|
229
|
+
* renderer wraps the nodes with the matching Experience Template config;
|
|
230
|
+
* otherwise nodes render at the top level.
|
|
212
231
|
*/
|
|
213
232
|
interface PortableRenderPlan {
|
|
214
233
|
viewports: ViewportDef[];
|
|
215
234
|
nodes: PortableRenderNode[];
|
|
216
|
-
|
|
235
|
+
experienceTemplate?: PortableExperienceTemplate;
|
|
217
236
|
}
|
|
218
237
|
|
|
219
|
-
export type {
|
|
238
|
+
export type { ComponentNode, ComponentRef, DesignPropValue, DesignToken, ExperienceContext, ExperienceNode, ExperiencePayload, ExperienceSys, ExperienceTemplateNode, ExperienceTemplateRef, ManualDesignValue, PortableExperienceTemplate, PortableRegistration, PortableRenderNode, PortableRenderPlan, ResolveContext, ResolveToken, ValuesByViewport, ViewportDef };
|