@contentful/experiences-sdk-core 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/README.md +20 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/resolve-experience.d.ts +37 -0
- package/dist/resolve-experience.js +124 -0
- package/dist/resolve-experience.js.map +1 -0
- package/dist/types.d.ts +211 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/package.json +32 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
## 0.5.2 (2026-07-20)
|
|
2
|
+
|
|
3
|
+
### 🩹 Fixes
|
|
4
|
+
|
|
5
|
+
- rename experiences-core to experiences-sdk-core [AIS-305] ([#76](https://github.com/contentful/experiences/pull/76))
|
|
6
|
+
|
|
7
|
+
## 0.5.1 (2026-07-17)
|
|
8
|
+
|
|
9
|
+
### 🚀 Features
|
|
10
|
+
|
|
11
|
+
- design-token resolution via resolveToken + useDesignValues [AIS-149] ([#53](https://github.com/contentful/experiences/pull/53))
|
|
12
|
+
|
|
13
|
+
## 0.5.0 (2026-07-17)
|
|
14
|
+
|
|
15
|
+
This was a version bump only for core to align it with other projects, there were no code changes.
|
|
16
|
+
|
|
17
|
+
## 0.4.0 (2026-07-08)
|
|
18
|
+
|
|
19
|
+
### 🚀 Features
|
|
20
|
+
|
|
21
|
+
- idiomatic adapters — bare components + context hooks ([0365bfb](https://github.com/contentful/experiences/commit/0365bfb))
|
|
22
|
+
|
|
23
|
+
## 0.3.0 (2026-06-24)
|
|
24
|
+
|
|
25
|
+
### 🚀 Features
|
|
26
|
+
|
|
27
|
+
- more-robust examples + simple/advanced README split + contentful prop ([#18](https://github.com/contentful/experiences/pull/18))
|
|
28
|
+
|
|
29
|
+
## 0.2.0 (2026-06-24)
|
|
30
|
+
|
|
31
|
+
This was a version bump only for core to align it with other projects, there were no code changes.
|
|
32
|
+
|
|
33
|
+
## 0.1.0 (2026-06-23)
|
|
34
|
+
|
|
35
|
+
### 🚀 Features
|
|
36
|
+
|
|
37
|
+
- initial Experiences SDK monorepo with React adapter [] ([#16](https://github.com/contentful/experiences/pull/16))
|
package/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# @contentful/experiences-sdk-core
|
|
2
|
+
|
|
3
|
+
> ⚠️ **Internal package.** Customers do not install this directly. The framework adapter (e.g. [`@contentful/experiences-react`](../adapter-react/)) re-exports everything customers need.
|
|
4
|
+
|
|
5
|
+
Runtime-neutral primitives shared across all framework adapters.
|
|
6
|
+
|
|
7
|
+
## What lives here
|
|
8
|
+
|
|
9
|
+
- **Types** — `PortableRenderPlan`, `PortableRenderNode`, `PortableTemplate`, `ExperiencePayload`, `ExperienceNode`, the discriminated `DesignPropValue` union (`ManualDesignValue` / `DesignToken` / `ValuesByViewport`), `ViewportDef`, `ExperienceContext`, `ResolveContext`.
|
|
10
|
+
- **`resolveExperience(payload, config, opts)`** — single async entry that walks an XDA payload, classifies content vs. design properties, captures slots, runs any component-declared `resolveData` hooks in parallel, and emits a runtime-neutral `PortableRenderPlan` ready for any framework adapter to render.
|
|
11
|
+
|
|
12
|
+
## Why a separate package?
|
|
13
|
+
|
|
14
|
+
Future Angular, Svelte, Vue, SwiftUI, and Compose adapters consume the same plan. Sharing core means each adapter has zero plan-building or prop-classification logic to duplicate — the seam is the `PortableRenderPlan` contract.
|
|
15
|
+
|
|
16
|
+
See [`../../AGENTS.md`](../../AGENTS.md) for the full architecture and design decisions.
|
|
17
|
+
|
|
18
|
+
## License
|
|
19
|
+
|
|
20
|
+
MIT. See the repository [`LICENSE`](../../LICENSE) and [`NOTICE`](../../NOTICE) for full attribution.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { ComponentTypeNode, ComponentTypeRef, DesignPropValue, DesignToken, ExperienceContext, ExperienceNode, ExperiencePayload, ExperienceSys, ManualDesignValue, PortableRegistration, PortableRenderNode, PortableRenderPlan, PortableTemplate, ResolveContext, ResolveToken, TemplateNode, TemplateRef, ValuesByViewport, ViewportDef } from './types.js';
|
|
2
|
+
export { ResolveExperienceOptions, ResolverConfig, resolveExperience } from './resolve-experience.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './types';\nexport { resolveExperience } from './resolve-experience';\nexport type { ResolverConfig, ResolveExperienceOptions } from './resolve-experience';\n"],"mappings":"AAAA,cAAc;AACd,SAAS,yBAAyB;","names":[]}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ExperienceContext, ExperiencePayload, PortableRenderPlan } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Structural type the resolver walker depends on. Matches the React /
|
|
5
|
+
* Svelte adapter `Config` shape but doesn't require importing them —
|
|
6
|
+
* render-core stays decoupled from any framework.
|
|
7
|
+
*
|
|
8
|
+
* Registry values are typed as `unknown` because each adapter accepts
|
|
9
|
+
* either a bare framework component (function / Svelte class / etc.) OR
|
|
10
|
+
* a config-object shape with `{ component, defaults?, resolveData? }`.
|
|
11
|
+
* The resolver only cares about `resolveData`; it duck-types each entry
|
|
12
|
+
* at runtime and ignores anything without it.
|
|
13
|
+
*/
|
|
14
|
+
interface ResolverConfig {
|
|
15
|
+
components: Record<string, unknown>;
|
|
16
|
+
templates?: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
interface ResolveExperienceOptions {
|
|
19
|
+
/**
|
|
20
|
+
* Per-render runtime context exposed to every resolver as `ctx.experience`.
|
|
21
|
+
* Defaults to `{ isPreview: false, metadata: {} }`.
|
|
22
|
+
*/
|
|
23
|
+
experience?: Partial<ExperienceContext>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Turns an Experience payload (XDA response shape) into a PortableRenderPlan
|
|
27
|
+
* ready to hand to a renderer. Walks the tree, classifies props, captures
|
|
28
|
+
* slots, and runs any component-declared `resolveData` hooks (sync or async)
|
|
29
|
+
* in parallel.
|
|
30
|
+
*
|
|
31
|
+
* Implementation note: the function is always async — even when no component
|
|
32
|
+
* declares a resolver, the cost is one microtask. Customers get a single
|
|
33
|
+
* uniform call site.
|
|
34
|
+
*/
|
|
35
|
+
declare function resolveExperience(payload: ExperiencePayload, config: ResolverConfig, options?: ResolveExperienceOptions): Promise<PortableRenderPlan>;
|
|
36
|
+
|
|
37
|
+
export { type ResolveExperienceOptions, type ResolverConfig, resolveExperience };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
function getResolver(entry) {
|
|
2
|
+
if (typeof entry !== "object" || entry === null) return void 0;
|
|
3
|
+
const candidate = entry.resolveData;
|
|
4
|
+
return typeof candidate === "function" ? candidate : void 0;
|
|
5
|
+
}
|
|
6
|
+
const DEFAULT_EXPERIENCE = {
|
|
7
|
+
isPreview: false,
|
|
8
|
+
metadata: {},
|
|
9
|
+
viewports: []
|
|
10
|
+
};
|
|
11
|
+
function isComponentTypeNode(node) {
|
|
12
|
+
return "componentType" in node;
|
|
13
|
+
}
|
|
14
|
+
function extractIdFromUrn(urn) {
|
|
15
|
+
const segments = urn.split("/").filter((s) => s.length > 0);
|
|
16
|
+
return segments[segments.length - 1] ?? urn;
|
|
17
|
+
}
|
|
18
|
+
function buildNode(node, config, nodeRefs) {
|
|
19
|
+
if (!isComponentTypeNode(node)) {
|
|
20
|
+
if (typeof console !== "undefined") {
|
|
21
|
+
console.warn(
|
|
22
|
+
"[@contentful/experiences-sdk-core] Skipping Template-variant node \u2014 Templates are not supported in v1."
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const componentTypeId = extractIdFromUrn(node.componentType.sys.urn);
|
|
28
|
+
const slots = {};
|
|
29
|
+
if (node.slots) {
|
|
30
|
+
for (const [slotName, children] of Object.entries(node.slots)) {
|
|
31
|
+
if (!Array.isArray(children)) {
|
|
32
|
+
throw new TypeError(
|
|
33
|
+
`Slot "${slotName}" on component "${componentTypeId}" must be an array of nodes.`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
const built2 = [];
|
|
37
|
+
for (const child of children) {
|
|
38
|
+
const childNode = buildNode(child, config, nodeRefs);
|
|
39
|
+
if (childNode === null) continue;
|
|
40
|
+
built2.push(childNode);
|
|
41
|
+
}
|
|
42
|
+
slots[slotName] = built2;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const built = {
|
|
46
|
+
registration: { componentTypeId },
|
|
47
|
+
props: {
|
|
48
|
+
content: { ...node.contentProperties ?? {} },
|
|
49
|
+
design: { ...node.designProperties ?? {} }
|
|
50
|
+
},
|
|
51
|
+
slots
|
|
52
|
+
};
|
|
53
|
+
if (node.id) built.nodeId = node.id;
|
|
54
|
+
if (getResolver(config.components[componentTypeId])) {
|
|
55
|
+
nodeRefs.push(built);
|
|
56
|
+
}
|
|
57
|
+
return built;
|
|
58
|
+
}
|
|
59
|
+
async function resolveExperience(payload, config, options = {}) {
|
|
60
|
+
const nodeRefs = [];
|
|
61
|
+
const nodes = [];
|
|
62
|
+
for (const node of payload.nodes) {
|
|
63
|
+
const built = buildNode(node, config, nodeRefs);
|
|
64
|
+
if (built !== null) nodes.push(built);
|
|
65
|
+
}
|
|
66
|
+
const templateUrn = payload.sys?.template?.sys.urn;
|
|
67
|
+
let template;
|
|
68
|
+
if (typeof templateUrn === "string" && templateUrn.length > 0) {
|
|
69
|
+
template = {
|
|
70
|
+
templateId: extractIdFromUrn(templateUrn),
|
|
71
|
+
props: { content: {}, design: {} }
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const experience = {
|
|
75
|
+
...DEFAULT_EXPERIENCE,
|
|
76
|
+
...options.experience,
|
|
77
|
+
metadata: {
|
|
78
|
+
...DEFAULT_EXPERIENCE.metadata,
|
|
79
|
+
...options.experience?.metadata ?? {}
|
|
80
|
+
},
|
|
81
|
+
viewports: payload.viewports
|
|
82
|
+
};
|
|
83
|
+
const tasks = [];
|
|
84
|
+
for (const node of nodeRefs) {
|
|
85
|
+
const resolver = getResolver(config.components[node.registration.componentTypeId]);
|
|
86
|
+
if (!resolver) continue;
|
|
87
|
+
const ctx = {
|
|
88
|
+
content: node.props.content,
|
|
89
|
+
design: node.props.design,
|
|
90
|
+
experience
|
|
91
|
+
};
|
|
92
|
+
tasks.push(
|
|
93
|
+
Promise.resolve(resolver(ctx)).then((resolved) => {
|
|
94
|
+
node.props.resolved = resolved;
|
|
95
|
+
})
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
if (template) {
|
|
99
|
+
const tplResolver = getResolver(config.templates?.[template.templateId]);
|
|
100
|
+
if (tplResolver) {
|
|
101
|
+
const ctx = {
|
|
102
|
+
content: template.props.content,
|
|
103
|
+
design: template.props.design,
|
|
104
|
+
experience
|
|
105
|
+
};
|
|
106
|
+
const tpl = template;
|
|
107
|
+
tasks.push(
|
|
108
|
+
Promise.resolve(tplResolver(ctx)).then((resolved) => {
|
|
109
|
+
tpl.props.resolved = resolved;
|
|
110
|
+
})
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (tasks.length > 0) await Promise.all(tasks);
|
|
115
|
+
return {
|
|
116
|
+
viewports: payload.viewports,
|
|
117
|
+
nodes,
|
|
118
|
+
...template ? { template } : {}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
export {
|
|
122
|
+
resolveExperience
|
|
123
|
+
};
|
|
124
|
+
//# sourceMappingURL=resolve-experience.js.map
|
|
@@ -0,0 +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 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 * Per-render runtime context exposed to every resolver as `ctx.experience`.\n * Defaults to `{ isPreview: false, metadata: {} }`.\n */\n experience?: Partial<ExperienceContext>;\n}\n\nconst DEFAULT_EXPERIENCE: ExperienceContext = {\n isPreview: 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 // 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\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 — caller-supplied\n // options.experience.viewports is ignored (the list is fact, not opinion).\n const experience: ExperienceContext = {\n ...DEFAULT_EXPERIENCE,\n ...options.experience,\n metadata: {\n ...DEFAULT_EXPERIENCE.metadata,\n ...(options.experience?.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 if (tasks.length > 0) await Promise.all(tasks);\n\n return {\n viewports: payload.viewports,\n nodes,\n ...(template ? { template } : {}),\n };\n}\n"],"mappings":"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","names":["built"]}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-render runtime context attached to every customer component as the
|
|
3
|
+
* `experience` prop, and passed to every `resolveData` hook as `ctx.experience`.
|
|
4
|
+
* The conventional, single injection point — kept small at v1.
|
|
5
|
+
*
|
|
6
|
+
* `viewports` (the *list*) is here so customer resolvers can inspect what
|
|
7
|
+
* viewports an Experience declares (e.g. "is there a mobile viewport?").
|
|
8
|
+
* The *active* viewport is render-time only and lives on the framework
|
|
9
|
+
* adapter's RenderContext — exposing it here would mean async resolvers
|
|
10
|
+
* re-fire on every viewport change, which would be a footgun.
|
|
11
|
+
*/
|
|
12
|
+
interface ExperienceContext {
|
|
13
|
+
isPreview: boolean;
|
|
14
|
+
metadata: Record<string, unknown>;
|
|
15
|
+
viewports: ViewportDef[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* One viewport definition from a delivered Experience. The `query` is the
|
|
19
|
+
* Contentful media-query DSL ("*" | "<992px" | ">1200px"), not raw CSS.
|
|
20
|
+
*
|
|
21
|
+
* The first viewport in the list is conventionally the wildcard ("*") that
|
|
22
|
+
* always matches. The viewport order encodes the cascade direction —
|
|
23
|
+
* desktop-first (descending) or mobile-first (ascending).
|
|
24
|
+
*/
|
|
25
|
+
interface ViewportDef {
|
|
26
|
+
id: string;
|
|
27
|
+
query: string;
|
|
28
|
+
displayName: string;
|
|
29
|
+
previewSize: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Discriminated design-property value as it arrives from XDA. v1 accepts:
|
|
33
|
+
* - ManualDesignValue: an explicit scalar (no viewport involved).
|
|
34
|
+
* - ValuesByViewport: a viewport-keyed bag where each entry is itself a
|
|
35
|
+
* ManualDesignValue or DesignToken.
|
|
36
|
+
* - DesignToken: a token reference, passed through to customer components
|
|
37
|
+
* as-is for v1. Resolution lands in the future tokens package.
|
|
38
|
+
*/
|
|
39
|
+
type DesignPropValue = ManualDesignValue | DesignToken | ValuesByViewport;
|
|
40
|
+
interface ManualDesignValue {
|
|
41
|
+
type: 'ManualDesignValue';
|
|
42
|
+
value: string | number | boolean;
|
|
43
|
+
}
|
|
44
|
+
interface DesignToken {
|
|
45
|
+
type: 'DesignToken';
|
|
46
|
+
value: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Turns a `DesignToken` envelope into a runtime value. `ref.value` is the
|
|
50
|
+
* customer-defined token id; returning `undefined` means "not resolvable" and
|
|
51
|
+
* the adapter drops the key (with a warning). Sync only — it runs at render time.
|
|
52
|
+
*/
|
|
53
|
+
type ResolveToken = (ref: DesignToken) => unknown;
|
|
54
|
+
interface ValuesByViewport {
|
|
55
|
+
type: 'ValuesByViewport';
|
|
56
|
+
values: Record<string, ManualDesignValue | DesignToken>;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Resource-link reference to a registered Component Type. The `urn` carries
|
|
60
|
+
* the type id; the build-plan extracts the id by taking the segment after
|
|
61
|
+
* the last slash.
|
|
62
|
+
*/
|
|
63
|
+
interface ComponentTypeRef {
|
|
64
|
+
sys: {
|
|
65
|
+
type: 'ResourceLink';
|
|
66
|
+
linkType: 'Contentful:ComponentType';
|
|
67
|
+
urn: string;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Resource-link reference to a Template. Templates are out of v1 scope and
|
|
72
|
+
* are skipped at plan-build time with a diagnostic.
|
|
73
|
+
*/
|
|
74
|
+
interface TemplateRef {
|
|
75
|
+
sys: {
|
|
76
|
+
type: 'ResourceLink';
|
|
77
|
+
linkType: 'Contentful:Template';
|
|
78
|
+
urn: string;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* One node from `GetExperienceViewResponse.nodes` (or any `slots[name]`).
|
|
83
|
+
* Discriminated by which of `componentType` / `template` is present.
|
|
84
|
+
*/
|
|
85
|
+
type ExperienceNode = ComponentTypeNode | TemplateNode;
|
|
86
|
+
interface ComponentTypeNode {
|
|
87
|
+
componentType: ComponentTypeRef;
|
|
88
|
+
id?: string;
|
|
89
|
+
contentProperties?: Record<string, unknown>;
|
|
90
|
+
designProperties?: Record<string, DesignPropValue>;
|
|
91
|
+
slots?: Record<string, ExperienceNode[]>;
|
|
92
|
+
contentBindings?: string;
|
|
93
|
+
}
|
|
94
|
+
interface TemplateNode {
|
|
95
|
+
template: TemplateRef;
|
|
96
|
+
id?: string;
|
|
97
|
+
contentProperties?: Record<string, unknown>;
|
|
98
|
+
designProperties?: Record<string, DesignPropValue>;
|
|
99
|
+
slots?: Record<string, ExperienceNode[]>;
|
|
100
|
+
contentBindings?: string;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Top-level `sys` block on an Experience payload. The bits the SDK actually
|
|
104
|
+
* reads are typed; everything else is left loose because the upstream
|
|
105
|
+
* type carries dozens of editor/audit fields the renderer doesn't care about.
|
|
106
|
+
*/
|
|
107
|
+
interface ExperienceSys {
|
|
108
|
+
/**
|
|
109
|
+
* Optional page-level template reference. When present, the renderer wraps
|
|
110
|
+
* the experience nodes with the matching template registered in the
|
|
111
|
+
* customer's Config. When absent, nodes render at the top level.
|
|
112
|
+
*/
|
|
113
|
+
template?: TemplateRef;
|
|
114
|
+
[key: string]: unknown;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Top-level Experience payload as returned by the Experience Delivery API
|
|
118
|
+
* (`GetExperienceViewResponse` from `@contentful/experience-delivery`).
|
|
119
|
+
*
|
|
120
|
+
* Structurally compatible with the upstream type — no normalization step
|
|
121
|
+
* required when consuming a delivery-client response.
|
|
122
|
+
*/
|
|
123
|
+
interface ExperiencePayload {
|
|
124
|
+
viewports: ViewportDef[];
|
|
125
|
+
nodes: ExperienceNode[];
|
|
126
|
+
errors?: unknown[];
|
|
127
|
+
extensions?: unknown;
|
|
128
|
+
sys?: ExperienceSys;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Per-node context handed to a component's `resolveData` resolver. Carries
|
|
132
|
+
* the raw content + design props from the payload (design envelopes are NOT
|
|
133
|
+
* pre-resolved against a viewport — viewport resolution stays a render-time
|
|
134
|
+
* concern so client viewport changes don't re-trigger async resolvers).
|
|
135
|
+
*/
|
|
136
|
+
interface ResolveContext {
|
|
137
|
+
content: Record<string, unknown>;
|
|
138
|
+
design: Record<string, DesignPropValue>;
|
|
139
|
+
experience: ExperienceContext;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Registration metadata for a single instance — the SDK's interpreted
|
|
143
|
+
* pointer to the customer's component implementation. Today carries only
|
|
144
|
+
* the resolved component-type id; capabilities (state requirements,
|
|
145
|
+
* supported events, lifecycle hints, fallback ids) land here when needed.
|
|
146
|
+
*/
|
|
147
|
+
interface PortableRegistration {
|
|
148
|
+
componentTypeId: string;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* The IR — one node per component instance. The seam that lets non-React
|
|
152
|
+
* adapters (Angular, SwiftUI, Compose) consume the same interpretation.
|
|
153
|
+
*
|
|
154
|
+
* Design props preserve the discriminated envelope as they arrived. Adapters
|
|
155
|
+
* unwrap to plain scalars at render time, given an active viewport.
|
|
156
|
+
* (DesignToken envelopes pass through unwrapped — customer components decide
|
|
157
|
+
* how to resolve them in v1.)
|
|
158
|
+
*
|
|
159
|
+
* `props.resolved` is populated by `resolveExperience` from any
|
|
160
|
+
* customer-supplied `resolveData` resolver and merged into the final prop bag
|
|
161
|
+
* after content + design but before slot props.
|
|
162
|
+
*/
|
|
163
|
+
interface PortableRenderNode {
|
|
164
|
+
/**
|
|
165
|
+
* Optional. Passed through from the XDA payload's `id` field when the
|
|
166
|
+
* editor supplies one. The SDK does NOT auto-generate ids; adapters fall
|
|
167
|
+
* back to the array index for React keys / debug labels when absent.
|
|
168
|
+
*/
|
|
169
|
+
nodeId?: string;
|
|
170
|
+
registration: PortableRegistration;
|
|
171
|
+
props: {
|
|
172
|
+
content: Record<string, unknown>;
|
|
173
|
+
design: Record<string, DesignPropValue>;
|
|
174
|
+
resolved?: Record<string, unknown>;
|
|
175
|
+
};
|
|
176
|
+
slots: Record<string, PortableRenderNode[]>;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Interpreted page-level template — the optional wrapper around the
|
|
180
|
+
* experience tree. `templateId` is extracted from
|
|
181
|
+
* `payload.sys.template.sys.urn` (last slash-segment).
|
|
182
|
+
*
|
|
183
|
+
* Templates carry the same prop-resolution shape as components: content +
|
|
184
|
+
* design envelopes plus an optional `resolved` bag from a `resolveData` hook.
|
|
185
|
+
* v1 payloads from XDA don't carry template-level content/design properties
|
|
186
|
+
* yet, but the IR makes room for them so the API doesn't need to break later.
|
|
187
|
+
*/
|
|
188
|
+
interface PortableTemplate {
|
|
189
|
+
templateId: string;
|
|
190
|
+
props: {
|
|
191
|
+
content: Record<string, unknown>;
|
|
192
|
+
design: Record<string, DesignPropValue>;
|
|
193
|
+
resolved?: Record<string, unknown>;
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* The interpreted experience tree.
|
|
198
|
+
*
|
|
199
|
+
* Top-level is `nodes: PortableRenderNode[]` (array, not single root) to
|
|
200
|
+
* match the actual XDA payload shape. Renderers iterate top-level nodes
|
|
201
|
+
* and recurse into `node.slots`. When `template` is present, the renderer
|
|
202
|
+
* wraps the nodes with the matching template config; otherwise nodes
|
|
203
|
+
* render at the top level.
|
|
204
|
+
*/
|
|
205
|
+
interface PortableRenderPlan {
|
|
206
|
+
viewports: ViewportDef[];
|
|
207
|
+
nodes: PortableRenderNode[];
|
|
208
|
+
template?: PortableTemplate;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export type { ComponentTypeNode, ComponentTypeRef, DesignPropValue, DesignToken, ExperienceContext, ExperienceNode, ExperiencePayload, ExperienceSys, ManualDesignValue, PortableRegistration, PortableRenderNode, PortableRenderPlan, PortableTemplate, ResolveContext, ResolveToken, TemplateNode, TemplateRef, ValuesByViewport, ViewportDef };
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@contentful/experiences-sdk-core",
|
|
3
|
+
"version": "0.5.2",
|
|
4
|
+
"description": "Runtime-neutral types + experience resolution for Contentful Experiences",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"sideEffects": false,
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md",
|
|
22
|
+
"CHANGELOG.md"
|
|
23
|
+
],
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/contentful/experiences.git",
|
|
30
|
+
"directory": "packages/core"
|
|
31
|
+
}
|
|
32
|
+
}
|