@contentful/experiences-sdk-core 0.7.0 → 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 +6 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/resolve-experience.d.ts +19 -1
- package/dist/resolve-experience.js +55 -5
- package/dist/resolve-experience.js.map +1 -1
- package/dist/types.d.ts +26 -9
- package/dist/viewport.d.ts +20 -0
- package/dist/viewport.js +71 -0
- package/dist/viewport.js.map +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
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 /
|
|
@@ -14,6 +14,17 @@ import { ExperiencePayload, PortableRenderPlan } from './types.js';
|
|
|
14
14
|
interface ResolverConfig {
|
|
15
15
|
components: Record<string, unknown>;
|
|
16
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
|
/**
|
|
@@ -27,6 +38,13 @@ interface ResolveExperienceOptions {
|
|
|
27
38
|
* resolver context as `ctx.experience.debug`. Defaults to `false`.
|
|
28
39
|
*/
|
|
29
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;
|
|
30
48
|
}
|
|
31
49
|
/**
|
|
32
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;
|
|
@@ -47,7 +48,9 @@ function buildNode(node, config, nodeRefs) {
|
|
|
47
48
|
registration: { componentId },
|
|
48
49
|
props: {
|
|
49
50
|
content: { ...node.contentProperties ?? {} },
|
|
50
|
-
|
|
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
|
};
|
|
@@ -57,6 +60,32 @@ function buildNode(node, config, nodeRefs) {
|
|
|
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);
|
|
@@ -72,7 +101,7 @@ async function resolveExperience(payload, config, options = {}) {
|
|
|
72
101
|
if (typeof experienceTemplateUrn === "string" && experienceTemplateUrn.length > 0) {
|
|
73
102
|
experienceTemplate = {
|
|
74
103
|
experienceTemplateId: extractIdFromUrn(experienceTemplateUrn),
|
|
75
|
-
props: { content: {}, design: {} }
|
|
104
|
+
props: { content: {}, design: {}, designRaw: {} }
|
|
76
105
|
};
|
|
77
106
|
}
|
|
78
107
|
const experience = {
|
|
@@ -89,7 +118,7 @@ async function resolveExperience(payload, config, options = {}) {
|
|
|
89
118
|
if (!resolver) continue;
|
|
90
119
|
const ctx = {
|
|
91
120
|
content: node.props.content,
|
|
92
|
-
design: node.props.
|
|
121
|
+
design: node.props.designRaw,
|
|
93
122
|
experience
|
|
94
123
|
};
|
|
95
124
|
tasks.push(
|
|
@@ -105,7 +134,7 @@ async function resolveExperience(payload, config, options = {}) {
|
|
|
105
134
|
if (tplResolver) {
|
|
106
135
|
const ctx = {
|
|
107
136
|
content: experienceTemplate.props.content,
|
|
108
|
-
design: experienceTemplate.props.
|
|
137
|
+
design: experienceTemplate.props.designRaw,
|
|
109
138
|
experience
|
|
110
139
|
};
|
|
111
140
|
const tpl = experienceTemplate;
|
|
@@ -119,10 +148,31 @@ async function resolveExperience(payload, config, options = {}) {
|
|
|
119
148
|
if (tasks.length > 0) {
|
|
120
149
|
await log.time(`${tasks.length} resolveData hook(s)`, () => Promise.all(tasks));
|
|
121
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}`);
|
|
122
171
|
return {
|
|
123
172
|
viewports: payload.viewports,
|
|
124
173
|
nodes,
|
|
125
|
-
...experienceTemplate ? { experienceTemplate } : {}
|
|
174
|
+
...experienceTemplate ? { experienceTemplate } : {},
|
|
175
|
+
fallbackViewportIndex
|
|
126
176
|
};
|
|
127
177
|
}
|
|
128
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 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"]}
|
|
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
|
@@ -38,7 +38,7 @@ interface ViewportDef {
|
|
|
38
38
|
/**
|
|
39
39
|
* Discriminated design-property value as it arrives from XDA. v1 accepts:
|
|
40
40
|
* - ManualDesignValue: an explicit scalar (no viewport involved).
|
|
41
|
-
* - ValuesByViewport: a viewport-keyed
|
|
41
|
+
* - ValuesByViewport: a viewport-keyed map where each entry is itself a
|
|
42
42
|
* ManualDesignValue or DesignToken.
|
|
43
43
|
* - DesignToken: a token reference, passed through to customer components
|
|
44
44
|
* as-is for v1. Resolution lands in the future tokens package.
|
|
@@ -53,7 +53,7 @@ interface DesignToken {
|
|
|
53
53
|
value: string;
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
|
-
* Turns a `DesignToken`
|
|
56
|
+
* Turns a `DesignToken` into a runtime value. `ref.value` is the
|
|
57
57
|
* customer-defined token id; returning `undefined` means "not resolvable" and
|
|
58
58
|
* the adapter drops the key (with a warning). Sync only — it runs at render time.
|
|
59
59
|
*/
|
|
@@ -155,7 +155,7 @@ interface ExperiencePayload {
|
|
|
155
155
|
}
|
|
156
156
|
/**
|
|
157
157
|
* Per-node context handed to a component's `resolveData` resolver. Carries
|
|
158
|
-
* the raw content + design props from the payload (design
|
|
158
|
+
* the raw content + design props from the payload (design properties are NOT
|
|
159
159
|
* pre-resolved against a viewport — viewport resolution stays a render-time
|
|
160
160
|
* concern so client viewport changes don't re-trigger async resolvers).
|
|
161
161
|
*/
|
|
@@ -177,14 +177,19 @@ interface PortableRegistration {
|
|
|
177
177
|
* The IR — one node per component instance. The seam that lets non-React
|
|
178
178
|
* adapters (Angular, SwiftUI, Compose) consume the same interpretation.
|
|
179
179
|
*
|
|
180
|
-
* Design props preserve the discriminated
|
|
180
|
+
* Design props preserve the discriminated value shape as they arrived. Adapters
|
|
181
181
|
* unwrap to plain scalars at render time, given an active viewport.
|
|
182
|
-
* (DesignToken
|
|
182
|
+
* (DesignToken values pass through unwrapped — customer components decide
|
|
183
183
|
* how to resolve them in v1.)
|
|
184
184
|
*
|
|
185
185
|
* `props.resolved` is populated by `resolveExperience` from any
|
|
186
|
-
* customer-supplied `resolveData` resolver and merged into the final
|
|
186
|
+
* customer-supplied `resolveData` resolver and merged into the final props
|
|
187
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.
|
|
188
193
|
*/
|
|
189
194
|
interface PortableRenderNode {
|
|
190
195
|
/**
|
|
@@ -196,8 +201,11 @@ interface PortableRenderNode {
|
|
|
196
201
|
registration: PortableRegistration;
|
|
197
202
|
props: {
|
|
198
203
|
content: Record<string, unknown>;
|
|
199
|
-
|
|
204
|
+
/** Flat, viewport-cascaded, token-resolved design values (server-side). */
|
|
205
|
+
design: Record<string, unknown>;
|
|
200
206
|
resolved?: Record<string, unknown>;
|
|
207
|
+
/** Raw per-viewport design, for client re-resolution on viewport change. */
|
|
208
|
+
designRaw: Record<string, DesignPropValue>;
|
|
201
209
|
};
|
|
202
210
|
slots: Record<string, PortableRenderNode[]>;
|
|
203
211
|
}
|
|
@@ -207,7 +215,7 @@ interface PortableRenderNode {
|
|
|
207
215
|
* `payload.sys.experienceTemplate.sys.urn` (last slash-segment).
|
|
208
216
|
*
|
|
209
217
|
* Experience Templates carry the same prop-resolution shape as components:
|
|
210
|
-
* content + design
|
|
218
|
+
* content + design properties plus an optional `resolved` map from a
|
|
211
219
|
* `resolveData` hook. v1 payloads from XDA don't carry template-level
|
|
212
220
|
* content/design properties yet, but the IR makes room for them so the API
|
|
213
221
|
* doesn't need to break later.
|
|
@@ -216,8 +224,11 @@ interface PortableExperienceTemplate {
|
|
|
216
224
|
experienceTemplateId: string;
|
|
217
225
|
props: {
|
|
218
226
|
content: Record<string, unknown>;
|
|
219
|
-
design
|
|
227
|
+
/** Same as `PortableRenderNode.props.design`. */
|
|
228
|
+
design: Record<string, unknown>;
|
|
220
229
|
resolved?: Record<string, unknown>;
|
|
230
|
+
/** Same as `PortableRenderNode.props.designRaw`. */
|
|
231
|
+
designRaw: Record<string, DesignPropValue>;
|
|
221
232
|
};
|
|
222
233
|
}
|
|
223
234
|
/**
|
|
@@ -233,6 +244,12 @@ interface PortableRenderPlan {
|
|
|
233
244
|
viewports: ViewportDef[];
|
|
234
245
|
nodes: PortableRenderNode[];
|
|
235
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;
|
|
236
253
|
}
|
|
237
254
|
|
|
238
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 };
|
package/dist/viewport.js
ADDED
|
@@ -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":[]}
|