@contentful/experiences-sdk-core 0.10.1 → 0.12.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 +138 -0
- package/README.md +3 -3
- package/dist/design-properties.d.ts +21 -0
- package/dist/design-properties.js +31 -0
- package/dist/design-properties.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -8
- package/dist/index.js.map +1 -1
- package/dist/resolve-experience.d.ts +0 -12
- package/dist/resolve-experience.js +12 -32
- package/dist/resolve-experience.js.map +1 -1
- package/dist/types.d.ts +16 -102
- package/package.json +1 -1
- package/dist/viewport.d.ts +0 -44
- package/dist/viewport.js +0 -59
- package/dist/viewport.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,141 @@
|
|
|
1
|
+
## 0.12.0 (2026-10-08)
|
|
2
|
+
|
|
3
|
+
### 🚀 Features
|
|
4
|
+
|
|
5
|
+
- ⚠️ remove sdk viewport surface [SPA-5275] ([#220](https://github.com/contentful/experiences/pull/220))
|
|
6
|
+
|
|
7
|
+
### ⚠️ Breaking Changes
|
|
8
|
+
|
|
9
|
+
- remove sdk viewport surface [SPA-5275] ([#220](https://github.com/contentful/experiences/pull/220))
|
|
10
|
+
The SDK no longer exposes viewport definitions, viewport-aware design-value resolution, or active-viewport hooks. Use CSS media queries or application-owned matchMedia integration for responsive behavior. Delivered design property values are now flat and require no migration."
|
|
11
|
+
M AGENTS.md
|
|
12
|
+
M ARCHITECTURE.md
|
|
13
|
+
M CONTRIBUTING.md
|
|
14
|
+
M README.md
|
|
15
|
+
M catalog-info.yaml
|
|
16
|
+
M examples/angular/README.md
|
|
17
|
+
M examples/angular/src/app/components/heading.component.ts
|
|
18
|
+
D examples/angular/src/app/lib/detect-viewport.ts
|
|
19
|
+
M examples/angular/src/app/lib/experience-config.ts
|
|
20
|
+
M examples/angular/src/app/lib/experience-route-data.ts
|
|
21
|
+
M examples/angular/src/app/pages/experience-page.component.ts
|
|
22
|
+
M examples/angular/src/server.ts
|
|
23
|
+
M examples/nextjs/README.md
|
|
24
|
+
M examples/nextjs/app/[slug]/page.tsx
|
|
25
|
+
M examples/nextjs/components/Heading.tsx
|
|
26
|
+
M examples/nextjs/components/LivePreviewExperience.tsx
|
|
27
|
+
M examples/nextjs/components/Section.tsx
|
|
28
|
+
D examples/nextjs/lib/detect-viewport.ts
|
|
29
|
+
M examples/nextjs/lib/experience-config.tsx
|
|
30
|
+
M examples/scripts/bootstrap-example.ts
|
|
31
|
+
M examples/scripts/fixture/experience.ts
|
|
32
|
+
M examples/scripts/fixture/types.ts
|
|
33
|
+
M examples/sveltekit/README.md
|
|
34
|
+
M examples/sveltekit/src/lib/components/Heading.svelte
|
|
35
|
+
D examples/sveltekit/src/lib/detect-viewport.ts
|
|
36
|
+
M examples/sveltekit/src/lib/experience-config.ts
|
|
37
|
+
M examples/sveltekit/src/routes/[slug]/+page.server.ts
|
|
38
|
+
M examples/sveltekit/src/routes/[slug]/+page.svelte
|
|
39
|
+
M examples/sveltekit/vite.config.ts
|
|
40
|
+
M package-lock.json
|
|
41
|
+
M packages/adapter-angular/README.md
|
|
42
|
+
M packages/adapter-angular/src/client-experience-renderer.component.ts
|
|
43
|
+
M packages/adapter-angular/src/context.ts
|
|
44
|
+
M packages/adapter-angular/src/debug-experience.test.ts
|
|
45
|
+
M packages/adapter-angular/src/debug-panel-coverage.ssr.test.ts
|
|
46
|
+
M packages/adapter-angular/src/debug-panel-coverage.test.ts
|
|
47
|
+
M packages/adapter-angular/src/dom-parity.test.ts
|
|
48
|
+
M packages/adapter-angular/src/experience-defaults.ts
|
|
49
|
+
M packages/adapter-angular/src/experience-scope.ts
|
|
50
|
+
M packages/adapter-angular/src/index.ts
|
|
51
|
+
D packages/adapter-angular/src/inject-active-viewport.ts
|
|
52
|
+
M packages/adapter-angular/src/inject-design-values.ts
|
|
53
|
+
M packages/adapter-angular/src/inject-experience-plan.ts
|
|
54
|
+
M packages/adapter-angular/src/live-preview-experience.ssr.test.ts
|
|
55
|
+
M packages/adapter-angular/src/live-preview-experience.test.ts
|
|
56
|
+
M packages/adapter-angular/src/node-render-engine.test.ts
|
|
57
|
+
M packages/adapter-angular/src/node-render-engine.ts
|
|
58
|
+
M packages/adapter-angular/src/node-scopes.ts
|
|
59
|
+
M packages/adapter-angular/src/nodes-renderer.ssr.test.ts
|
|
60
|
+
M packages/adapter-angular/src/server-experience-renderer.component.ts
|
|
61
|
+
M packages/adapter-angular/src/server-renderer.test.ts
|
|
62
|
+
M packages/adapter-angular/src/test-fixtures/render-harness.ts
|
|
63
|
+
M packages/adapter-angular/src/types.ts
|
|
64
|
+
M packages/adapter-react/README.md
|
|
65
|
+
M packages/adapter-react/src/client-renderer.tsx
|
|
66
|
+
M packages/adapter-react/src/context.tsx
|
|
67
|
+
M packages/adapter-react/src/debug-experience.test.tsx
|
|
68
|
+
M packages/adapter-react/src/debug-experience.tsx
|
|
69
|
+
M packages/adapter-react/src/debug-panel-coverage.test.tsx
|
|
70
|
+
M packages/adapter-react/src/index.ts
|
|
71
|
+
M packages/adapter-react/src/nodes-renderer.ssr.test.tsx
|
|
72
|
+
M packages/adapter-react/src/nodes-renderer.test.tsx
|
|
73
|
+
M packages/adapter-react/src/nodes-renderer.tsx
|
|
74
|
+
M packages/adapter-react/src/server-renderer.test.tsx
|
|
75
|
+
M packages/adapter-react/src/server-renderer.tsx
|
|
76
|
+
M packages/adapter-react/src/types.ts
|
|
77
|
+
D packages/adapter-react/src/use-active-viewport.ts
|
|
78
|
+
M packages/adapter-react/src/use-design-values.tsx
|
|
79
|
+
M packages/adapter-react/src/use-experience-plan.test.tsx
|
|
80
|
+
M packages/adapter-react/src/use-experience-plan.ts
|
|
81
|
+
M packages/adapter-react/src/use-live-preview-experience.test.tsx
|
|
82
|
+
M packages/adapter-react/src/use-live-preview.test.tsx
|
|
83
|
+
M packages/adapter-svelte/README.md
|
|
84
|
+
M packages/adapter-svelte/src/ClientExperienceRenderer.svelte
|
|
85
|
+
M packages/adapter-svelte/src/DebugExperience.svelte
|
|
86
|
+
M packages/adapter-svelte/src/DebugExperience.test.ts
|
|
87
|
+
M packages/adapter-svelte/src/NodeRenderer.svelte
|
|
88
|
+
M packages/adapter-svelte/src/NodeRenderer.test.ts
|
|
89
|
+
M packages/adapter-svelte/src/NodesRenderer.svelte
|
|
90
|
+
M packages/adapter-svelte/src/ServerExperienceRenderer.svelte
|
|
91
|
+
M packages/adapter-svelte/src/component-props.ts
|
|
92
|
+
M packages/adapter-svelte/src/context.ts
|
|
93
|
+
M packages/adapter-svelte/src/debug-panel-coverage.ssr.test.ts
|
|
94
|
+
M packages/adapter-svelte/src/debug-panel-coverage.test.ts
|
|
95
|
+
M packages/adapter-svelte/src/get-design-values.ts
|
|
96
|
+
M packages/adapter-svelte/src/index.ts
|
|
97
|
+
M packages/adapter-svelte/src/nodes-renderer.ssr.test.ts
|
|
98
|
+
M packages/adapter-svelte/src/server-renderer.test.ts
|
|
99
|
+
M packages/adapter-svelte/src/types.ts
|
|
100
|
+
D packages/adapter-svelte/src/use-active-viewport.svelte.ts
|
|
101
|
+
M packages/adapter-svelte/src/use-experience-plan.test.ts
|
|
102
|
+
M packages/adapter-svelte/src/use-live-preview-experience.test.ts
|
|
103
|
+
M packages/adapter-svelte/src/use-live-preview.test.ts
|
|
104
|
+
M packages/client/src/contentful-experiences.test.ts
|
|
105
|
+
M packages/client/src/contentful-experiences.ts
|
|
106
|
+
M packages/client/src/fetch-experience.test.ts
|
|
107
|
+
M packages/client/src/fetch-experience.ts
|
|
108
|
+
M packages/core/README.md
|
|
109
|
+
A packages/core/src/design-properties.test.ts
|
|
110
|
+
A packages/core/src/design-properties.ts
|
|
111
|
+
M packages/core/src/index.ts
|
|
112
|
+
M packages/core/src/resolve-experience.test.ts
|
|
113
|
+
M packages/core/src/resolve-experience.ts
|
|
114
|
+
M packages/core/src/types.ts
|
|
115
|
+
D packages/core/src/viewport.test.ts
|
|
116
|
+
D packages/core/src/viewport.ts
|
|
117
|
+
M packages/design/README.md
|
|
118
|
+
M packages/design/package.json
|
|
119
|
+
M packages/design/src/index.ts
|
|
120
|
+
D packages/design/src/media-query-matchers.ts
|
|
121
|
+
D packages/design/src/select-resolved-design.ts
|
|
122
|
+
D packages/design/src/viewport.test.ts
|
|
123
|
+
D packages/design/src/viewport.ts
|
|
124
|
+
M packages/live-preview/README.md
|
|
125
|
+
M packages/live-preview/src/experience-payload.ts
|
|
126
|
+
M packages/live-preview/src/fetch-preview-session.test.ts
|
|
127
|
+
M packages/live-preview/src/fetch-preview-session.ts
|
|
128
|
+
M packages/live-preview/src/live-preview-client.test.ts
|
|
129
|
+
M packages/node/README.md
|
|
130
|
+
M packages/node/src/contentful-experiences.test.ts
|
|
131
|
+
M packages/node/src/contentful-experiences.ts
|
|
132
|
+
M packages/web/README.md
|
|
133
|
+
M packages/web/src/contentful-experiences.test.ts
|
|
134
|
+
|
|
135
|
+
## 0.11.0 (2026-10-06)
|
|
136
|
+
|
|
137
|
+
This was a version bump only for core to align it with other projects, there were no code changes.
|
|
138
|
+
|
|
1
139
|
## 0.10.1 (2026-10-01)
|
|
2
140
|
|
|
3
141
|
### 🚀 Features
|
package/README.md
CHANGED
|
@@ -7,9 +7,9 @@ adapters. This package has no runtime dependencies.
|
|
|
7
7
|
|
|
8
8
|
## What lives here
|
|
9
9
|
|
|
10
|
-
- **Types** — `PortableRenderPlan`, `PortableRenderNode`, `PortableRegistration`, `ExperiencePayload`, `ExperienceNode`, the discriminated `DesignPropValue` union (`ManualDesignValue` / `DesignToken`
|
|
11
|
-
- **`resolveExperience(payload, config, opts)`** — single async entry that walks an XDA payload, classifies content vs. design properties, captures slots, runs any component-declared `resolveData` hooks in parallel, and emits a runtime-neutral `PortableRenderPlan` ready for any framework adapter to render. Every node in the payload becomes a `PortableRenderNode`; `registration.kind` records whether the adapter should resolve its id against `config.components` or `config.experienceTemplates`. A coded Experience Template is an ordinary node — `payload.sys.experienceTemplate` is never read.
|
|
12
|
-
- **Diagnostics and design support** — `createDebugLogger`,
|
|
10
|
+
- **Types** — `PortableRenderPlan`, `PortableRenderNode`, `PortableRegistration`, `ExperiencePayload`, `ExperienceNode`, the discriminated `DesignPropValue` union (`ManualDesignValue` / `DesignToken`), `ExperienceContext`, `ResolveContext`.
|
|
11
|
+
- **`resolveExperience(payload, config, opts)`** — single async entry that walks an XDA payload, classifies content vs. design properties, resolves design values and tokens, captures slots, runs any component-declared `resolveData` hooks in parallel, and emits a runtime-neutral `PortableRenderPlan` ready for any framework adapter to render. Every node in the payload becomes a `PortableRenderNode`; `registration.kind` records whether the adapter should resolve its id against `config.components` or `config.experienceTemplates`. A coded Experience Template is an ordinary node — `payload.sys.experienceTemplate` is never read.
|
|
12
|
+
- **Diagnostics and design support** — `createDebugLogger`, `resolveDesignProperties`, and `applyTokenResolver`.
|
|
13
13
|
|
|
14
14
|
Delivery transport and event construction do not belong here. They are owned by
|
|
15
15
|
Client, keeping this package usable in any runtime without pulling in a delivery
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { DesignToken, ResolveToken, DesignPropValue } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve every design property on a node into a record keyed by name.
|
|
5
|
+
*/
|
|
6
|
+
declare function resolveDesignProperties(designProperties: Record<string, DesignPropValue> | undefined): Record<string, string | number | boolean | DesignToken>;
|
|
7
|
+
/**
|
|
8
|
+
* Resolve `DesignToken` values via `resolveToken`; scalars pass through.
|
|
9
|
+
* Keys that don't resolve — no `resolveToken` configured, or the resolver
|
|
10
|
+
* returns `undefined` for that particular token — pass through as the raw
|
|
11
|
+
* `DesignToken` and their id is collected in `unresolved` for the caller to
|
|
12
|
+
* warn/diagnose (see `warnUnresolvedTokens` in `resolve-experience.ts`, which
|
|
13
|
+
* already dedupes per `resolveExperience()` call, so this function carries
|
|
14
|
+
* no warn-once state of its own).
|
|
15
|
+
*/
|
|
16
|
+
declare function applyTokenResolver(props: Record<string, string | number | boolean | DesignToken>, resolveToken?: ResolveToken): {
|
|
17
|
+
props: Record<string, unknown>;
|
|
18
|
+
unresolved: string[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export { applyTokenResolver, resolveDesignProperties };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
function resolveDesignProperties(designProperties) {
|
|
2
|
+
const out = {};
|
|
3
|
+
if (!designProperties) return out;
|
|
4
|
+
for (const [key, prop] of Object.entries(designProperties)) {
|
|
5
|
+
out[key] = prop.type === "ManualDesignValue" ? prop.value : prop;
|
|
6
|
+
}
|
|
7
|
+
return out;
|
|
8
|
+
}
|
|
9
|
+
function applyTokenResolver(props, resolveToken) {
|
|
10
|
+
const out = {};
|
|
11
|
+
const unresolved = [];
|
|
12
|
+
for (const [key, value] of Object.entries(props)) {
|
|
13
|
+
if (typeof value === "object" && value !== null && value.type === "DesignToken") {
|
|
14
|
+
const resolved = resolveToken?.(value);
|
|
15
|
+
if (resolved === void 0) {
|
|
16
|
+
out[key] = value;
|
|
17
|
+
unresolved.push(value.value);
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
out[key] = resolved;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
out[key] = value;
|
|
24
|
+
}
|
|
25
|
+
return { props: out, unresolved };
|
|
26
|
+
}
|
|
27
|
+
export {
|
|
28
|
+
applyTokenResolver,
|
|
29
|
+
resolveDesignProperties
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=design-properties.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/design-properties.ts"],"sourcesContent":["/* Design-property and token resolution for the core render-plan builder. */\n\nimport type { DesignPropValue, DesignToken, ResolveToken } from './types.js';\n\n/**\n * Resolve every design property on a node into a record keyed by name.\n */\nexport function resolveDesignProperties(\n designProperties: Record<string, DesignPropValue> | undefined\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 out[key] = prop.type === 'ManualDesignValue' ? prop.value : prop;\n }\n return out;\n}\n\n/**\n * Resolve `DesignToken` values via `resolveToken`; scalars pass through.\n * Keys that don't resolve — no `resolveToken` configured, or the resolver\n * returns `undefined` for that particular token — pass through as the raw\n * `DesignToken` and their id is collected in `unresolved` for the caller to\n * warn/diagnose (see `warnUnresolvedTokens` in `resolve-experience.ts`, which\n * already dedupes per `resolveExperience()` call, so this function carries\n * no warn-once state of its own).\n */\nexport function applyTokenResolver(\n props: Record<string, string | number | boolean | DesignToken>,\n resolveToken?: ResolveToken\n): { props: Record<string, unknown>; unresolved: string[] } {\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 out[key] = value;\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":"AAOO,SAAS,wBACd,kBACyD;AACzD,QAAM,MAA+D,CAAC;AACtE,MAAI,CAAC,iBAAkB,QAAO;AAC9B,aAAW,CAAC,KAAK,IAAI,KAAK,OAAO,QAAQ,gBAAgB,GAAG;AAC1D,QAAI,GAAG,IAAI,KAAK,SAAS,sBAAsB,KAAK,QAAQ;AAAA,EAC9D;AACA,SAAO;AACT;AAWO,SAAS,mBACd,OACA,cAC0D;AAC1D,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,eAAe,KAAK;AACrC,UAAI,aAAa,QAAW;AAC1B,YAAI,GAAG,IAAI;AACX,mBAAW,KAAK,MAAM,KAAK;AAC3B;AAAA,MACF;AACA,UAAI,GAAG,IAAI;AACX;AAAA,IACF;AACA,QAAI,GAAG,IAAI;AAAA,EACb;AACA,SAAO,EAAE,OAAO,KAAK,WAAW;AAClC;","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { ComponentNode, ComponentRef, DesignPropValue, DesignToken, ExperienceContext, ExperienceExtensions, ExperienceNode, ExperiencePayload, ExperienceSourceMap, ExperienceSys, ExperienceTemplateNode, ExperienceTemplateRef, ManualDesignValue, PortableRegistration, PortableRenderNode, PortableRenderPlan, RenderContext, ResolveContext, ResolveToken
|
|
1
|
+
export { ComponentNode, ComponentRef, DesignPropValue, DesignToken, ExperienceContext, ExperienceExtensions, ExperienceNode, ExperiencePayload, ExperienceSourceMap, ExperienceSys, ExperienceTemplateNode, ExperienceTemplateRef, ManualDesignValue, PortableRegistration, PortableRenderNode, PortableRenderPlan, RenderContext, ResolveContext, ResolveToken } 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,
|
|
4
|
+
export { applyTokenResolver, resolveDesignProperties } from './design-properties.js';
|
package/dist/index.js
CHANGED
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
export * from "./types.js";
|
|
2
2
|
import { resolveExperience } from "./resolve-experience.js";
|
|
3
3
|
import { createDebugLogger } from "./debug-logger.js";
|
|
4
|
-
import {
|
|
5
|
-
applyTokenResolver,
|
|
6
|
-
getValueForViewport,
|
|
7
|
-
getViewportIndex,
|
|
8
|
-
resolveDesignProperties
|
|
9
|
-
} from "./viewport.js";
|
|
4
|
+
import { applyTokenResolver, resolveDesignProperties } from "./design-properties.js";
|
|
10
5
|
export {
|
|
11
6
|
applyTokenResolver,
|
|
12
7
|
createDebugLogger,
|
|
13
|
-
getValueForViewport,
|
|
14
|
-
getViewportIndex,
|
|
15
8
|
resolveDesignProperties,
|
|
16
9
|
resolveExperience
|
|
17
10
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './types.js';\nexport { resolveExperience } from './resolve-experience.js';\nexport type { ResolverConfig, ResolveExperienceOptions } from './resolve-experience.js';\nexport { createDebugLogger } from './debug-logger.js';\nexport type { DebugLogger } from './debug-logger.js';\nexport {
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './types.js';\nexport { resolveExperience } from './resolve-experience.js';\nexport type { ResolverConfig, ResolveExperienceOptions } from './resolve-experience.js';\nexport { createDebugLogger } from './debug-logger.js';\nexport type { DebugLogger } from './debug-logger.js';\nexport { applyTokenResolver, resolveDesignProperties } from './design-properties.js';\n"],"mappings":"AAAA,cAAc;AACd,SAAS,yBAAyB;AAElC,SAAS,yBAAyB;AAElC,SAAS,oBAAoB,+BAA+B;","names":[]}
|
|
@@ -20,11 +20,6 @@ interface ResolverConfig {
|
|
|
20
20
|
* caller re-supplying it. Used during server-side pre-resolution.
|
|
21
21
|
*/
|
|
22
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;
|
|
28
23
|
}
|
|
29
24
|
interface ResolveExperienceOptions {
|
|
30
25
|
/**
|
|
@@ -38,13 +33,6 @@ interface ResolveExperienceOptions {
|
|
|
38
33
|
* resolver context as `ctx.experience.debug`. Defaults to `false`.
|
|
39
34
|
*/
|
|
40
35
|
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;
|
|
48
36
|
/** Carried onto the plan as-is. Omit for no source map. */
|
|
49
37
|
sourceMap?: ExperienceSourceMap;
|
|
50
38
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createDebugLogger } from "./debug-logger.js";
|
|
2
|
-
import { applyTokenResolver,
|
|
2
|
+
import { applyTokenResolver, resolveDesignProperties } from "./design-properties.js";
|
|
3
3
|
function getResolver(entry) {
|
|
4
4
|
if (typeof entry !== "object" || entry === null) return void 0;
|
|
5
5
|
const candidate = entry.resolveData;
|
|
@@ -7,8 +7,7 @@ function getResolver(entry) {
|
|
|
7
7
|
}
|
|
8
8
|
const DEFAULT_EXPERIENCE = {
|
|
9
9
|
debug: false,
|
|
10
|
-
metadata: {}
|
|
11
|
-
viewports: []
|
|
10
|
+
metadata: {}
|
|
12
11
|
};
|
|
13
12
|
function lookupEntry(config, registration) {
|
|
14
13
|
return registration.kind === "experienceTemplate" ? config.experienceTemplates?.[registration.id] : config.components[registration.id];
|
|
@@ -96,7 +95,7 @@ function buildNode(node, config, nodeRefs, log, diagnostics) {
|
|
|
96
95
|
registration,
|
|
97
96
|
props: {
|
|
98
97
|
content: { ...node.contentProperties ?? {} },
|
|
99
|
-
// Resolved
|
|
98
|
+
// Resolved design values are written by the pre-resolution pass below.
|
|
100
99
|
design: {},
|
|
101
100
|
designRaw: { ...node.designProperties ?? {} }
|
|
102
101
|
},
|
|
@@ -108,9 +107,8 @@ function buildNode(node, config, nodeRefs, log, diagnostics) {
|
|
|
108
107
|
}
|
|
109
108
|
return built;
|
|
110
109
|
}
|
|
111
|
-
function preResolveDesignProperties(design,
|
|
112
|
-
|
|
113
|
-
return applyTokenResolver(cascaded, resolveToken);
|
|
110
|
+
function preResolveDesignProperties(design, resolveToken) {
|
|
111
|
+
return applyTokenResolver(resolveDesignProperties(design), resolveToken);
|
|
114
112
|
}
|
|
115
113
|
function warnUnresolvedTokens(label, unresolved, log, diagnostics) {
|
|
116
114
|
if (!unresolved.length) return;
|
|
@@ -127,13 +125,8 @@ function warnUnresolvedTokens(label, unresolved, log, diagnostics) {
|
|
|
127
125
|
);
|
|
128
126
|
}
|
|
129
127
|
}
|
|
130
|
-
function preResolveNodeTree(node,
|
|
131
|
-
const { props, unresolved } = preResolveDesignProperties(
|
|
132
|
-
node.props.designRaw,
|
|
133
|
-
viewports,
|
|
134
|
-
fallbackViewportIndex,
|
|
135
|
-
resolveToken
|
|
136
|
-
);
|
|
128
|
+
function preResolveNodeTree(node, resolveToken, log, diagnostics) {
|
|
129
|
+
const { props, unresolved } = preResolveDesignProperties(node.props.designRaw, resolveToken);
|
|
137
130
|
node.props.design = props;
|
|
138
131
|
warnUnresolvedTokens(
|
|
139
132
|
`${node.registration.kind}:${node.registration.id}`,
|
|
@@ -143,7 +136,7 @@ function preResolveNodeTree(node, viewports, fallbackViewportIndex, resolveToken
|
|
|
143
136
|
);
|
|
144
137
|
for (const children of Object.values(node.slots)) {
|
|
145
138
|
for (const child of children) {
|
|
146
|
-
preResolveNodeTree(child,
|
|
139
|
+
preResolveNodeTree(child, resolveToken, log, diagnostics);
|
|
147
140
|
}
|
|
148
141
|
}
|
|
149
142
|
}
|
|
@@ -153,7 +146,7 @@ async function resolveExperience(payload, config, options = {}) {
|
|
|
153
146
|
const diagnostics = [];
|
|
154
147
|
const isPlainPayload = payload !== null && typeof payload === "object";
|
|
155
148
|
if (!isPlainPayload) {
|
|
156
|
-
const message = `Experience payload is ${payload === null ? "null" : typeof payload}, not an object; treating it as an empty experience (no nodes
|
|
149
|
+
const message = `Experience payload is ${payload === null ? "null" : typeof payload}, not an object; treating it as an empty experience (no nodes) instead of throwing. This usually means the fetch or transform pipeline handed resolveExperience a missing or malformed payload \u2014 check what produced it.`;
|
|
157
150
|
if (typeof console !== "undefined") {
|
|
158
151
|
console.warn(`[@contentful/experiences] ${message}`);
|
|
159
152
|
}
|
|
@@ -161,7 +154,6 @@ async function resolveExperience(payload, config, options = {}) {
|
|
|
161
154
|
diagnostics.push(new Error(message));
|
|
162
155
|
}
|
|
163
156
|
const payloadNodes = isPlainPayload ? ensureArray(payload.nodes, "nodes", log, diagnostics) : [];
|
|
164
|
-
const viewports = isPlainPayload && payload.viewports !== void 0 ? ensureArray(payload.viewports, "viewports", log, diagnostics) : [];
|
|
165
157
|
const nodeRefs = [];
|
|
166
158
|
const nodes = buildNodes(payloadNodes, config, nodeRefs, log, diagnostics);
|
|
167
159
|
log.log(`built ${nodes.length} top-level node(s); ${nodeRefs.length} declare resolveData`);
|
|
@@ -170,8 +162,7 @@ async function resolveExperience(payload, config, options = {}) {
|
|
|
170
162
|
metadata: {
|
|
171
163
|
...DEFAULT_EXPERIENCE.metadata,
|
|
172
164
|
...options.metadata ?? {}
|
|
173
|
-
}
|
|
174
|
-
viewports
|
|
165
|
+
}
|
|
175
166
|
};
|
|
176
167
|
const tasks = [];
|
|
177
168
|
for (const node of nodeRefs) {
|
|
@@ -209,23 +200,12 @@ async function resolveExperience(payload, config, options = {}) {
|
|
|
209
200
|
if (tasks.length > 0) {
|
|
210
201
|
await log.time(`${tasks.length} resolveData hook(s)`, () => Promise.all(tasks));
|
|
211
202
|
}
|
|
212
|
-
const fallbackViewportId = options.initialViewportId ?? config.fallbackViewportId;
|
|
213
|
-
const fallbackViewportIndex = getViewportIndex(viewports, fallbackViewportId);
|
|
214
203
|
for (const node of nodes) {
|
|
215
|
-
preResolveNodeTree(
|
|
216
|
-
node,
|
|
217
|
-
viewports,
|
|
218
|
-
fallbackViewportIndex,
|
|
219
|
-
config.resolveToken,
|
|
220
|
-
log,
|
|
221
|
-
diagnostics
|
|
222
|
-
);
|
|
204
|
+
preResolveNodeTree(node, config.resolveToken, log, diagnostics);
|
|
223
205
|
}
|
|
224
|
-
log.log(
|
|
206
|
+
log.log("resolved design properties");
|
|
225
207
|
const plan = {
|
|
226
|
-
viewports,
|
|
227
208
|
nodes,
|
|
228
|
-
fallbackViewportIndex,
|
|
229
209
|
diagnostics,
|
|
230
210
|
metadata: experience.metadata,
|
|
231
211
|
debug: experience.debug
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/resolve-experience.ts"],"sourcesContent":["/*\n * Turns an XDA Experience payload into a runtime-neutral PortableRenderPlan.\n * Walks nodes recursively, splits content + design props, runs any registered\n * `resolveData` hooks in parallel, and pre-resolves design against a fallback\n * viewport (see `resolveExperience` below).\n */\n\nimport { createDebugLogger, type DebugLogger } from './debug-logger.js';\nimport type {\n DesignPropValue,\n ExperienceContext,\n ExperienceSourceMap,\n ExperienceNode,\n ExperiencePayload,\n PortableRegistration,\n PortableRenderNode,\n PortableRenderPlan,\n ResolveContext,\n ResolveToken,\n ViewportDef,\n} from './types.js';\nimport { applyTokenResolver, getViewportIndex, resolveDesignProperties } from './viewport.js';\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 /** Carried onto the plan as-is. Omit for no source map. */\n sourceMap?: ExperienceSourceMap;\n}\n\nconst DEFAULT_EXPERIENCE: ExperienceContext = {\n debug: false,\n metadata: {},\n viewports: [],\n};\n\n/**\n * Registry lookup for a built node. A node's `kind` decides which half of the\n * customer Config owns its implementation — components and Experience\n * Templates are otherwise interchangeable at every other step.\n */\nfunction lookupEntry(config: ResolverConfig, registration: PortableRegistration): unknown {\n return registration.kind === 'experienceTemplate'\n ? config.experienceTemplates?.[registration.id]\n : config.components[registration.id];\n}\n\n/**\n * Extract the flat id (component or experienceTemplate) from its\n * `ResourceLink` URN. Real URN shapes:\n * crn:contentful:::experience:spaces/$self/environments/$self/components/<id>\n * crn:contentful:::experience:spaces/$self/environments/$self/experienceTemplates/<id>\n *\n * The id is the final path segment. We split on `/` and take the last\n * non-empty piece so this also tolerates trailing slashes or alternative\n * prefix shapes.\n */\nfunction extractIdFromUrn(urn: string): string {\n const segments = urn.split('/').filter((s) => s.length > 0);\n return segments[segments.length - 1] ?? urn;\n}\n\n/**\n * Read the ResourceLink a node points at. Which key is present is the only\n * difference between the two node variants: `experienceTemplate` means the\n * implementation lives in the customer's `experienceTemplates` registry,\n * `component` in `components`. Both carry ids in the same URN shape, the same\n * props shape, and the same slots — so everything downstream is kind-agnostic.\n *\n * `ExperienceNode` is a closed union, so a typed caller cannot produce anything\n * else. Payloads, however, are untrusted JSON at runtime, and the realistic way\n * a third shape arrives is a node kind *newer than this SDK*. Returns `null`\n * for anything unidentifiable rather than letting the ref access throw — see\n * `buildNode`.\n */\nfunction readNodeRef(\n node: ExperienceNode\n): { kind: PortableRegistration['kind']; urn: string } | null {\n const isExperienceTemplate = 'experienceTemplate' in node;\n const ref = isExperienceTemplate\n ? node.experienceTemplate\n : 'component' in node\n ? node.component\n : undefined;\n // Cast because the runtime value may violate the declared type: a node can\n // carry `component: {}`, which satisfies `'component' in node` but has no urn.\n const urn = (ref as { sys?: { urn?: unknown } } | undefined)?.sys?.urn;\n if (typeof urn !== 'string' || urn.length === 0) return null;\n return { kind: isExperienceTemplate ? 'experienceTemplate' : 'component', urn };\n}\n\n/**\n * Guards a top-level payload field that must be an array. A malformed or\n * partial payload (bad JSON, a hand-authored payload with the wrong shape)\n * should degrade to an empty experience rather than throwing and failing the\n * whole render — same \"dropping beats throwing\" rationale as\n * `warnUnrenderableNode` below, just one level up the tree.\n */\nfunction ensureArray<T>(\n value: unknown,\n field: string,\n log: DebugLogger,\n diagnostics: Error[]\n): T[] {\n if (Array.isArray(value)) return value as T[];\n const message =\n `Experience payload's \"${field}\" is not an array (got ${value === null ? 'null' : typeof value}); ` +\n `treating it as empty instead of throwing. This usually means a malformed or partial payload — ` +\n `check what produced it.`;\n if (typeof console !== 'undefined') {\n console.warn(`[@contentful/experiences] ${message}`);\n }\n log.log(message);\n diagnostics.push(new Error(message));\n return [];\n}\n\n/** Total node count in a payload subtree, the node itself included. */\nfunction countPayloadNodes(node: ExperienceNode): number {\n const slots = (node as { slots?: Record<string, unknown> }).slots;\n let total = 1;\n if (slots && typeof slots === 'object') {\n for (const children of Object.values(slots)) {\n if (!Array.isArray(children)) continue;\n for (const child of children) total += countPayloadNodes(child as ExperienceNode);\n }\n }\n return total;\n}\n\n/**\n * The one case where a node is dropped. AIS-413 was the opposite failure —\n * a node kind the SDK recognized and could have rendered, skipped behind a\n * vague warning, taking its whole subtree with it — so the bar here is that\n * nothing vanishes without a diagnostic naming what was lost.\n *\n * Dropping beats throwing: a `resolveExperience` rejection fails the entire\n * experience, so one unrecognized node in a sidebar would take down every page\n * containing it, for a payload the customer does not control and cannot fix.\n */\nfunction warnUnrenderableNode(node: ExperienceNode, log: DebugLogger, diagnostics: Error[]): void {\n const id = (node as { id?: unknown }).id;\n const label = typeof id === 'string' ? ` \"${id}\"` : '';\n const keys = Object.keys(node);\n const descendants = countPayloadNodes(node) - 1;\n const message =\n `Skipping unidentifiable node${label}: expected a \\`component\\` or \\`experienceTemplate\\` ` +\n `ResourceLink carrying a urn, got keys [${keys.join(', ')}]. Dropping it and ${descendants} ` +\n `descendant node(s). A payload node kind this SDK does not know is usually a version skew — ` +\n `upgrading @contentful/experiences-sdk-core may be all that is needed.`;\n if (typeof console !== 'undefined') {\n console.warn(`[@contentful/experiences] ${message}`);\n }\n log.log(message);\n diagnostics.push(new Error(message));\n}\n\n/**\n * Walk a sibling list into IR nodes, dropping any node `buildNode` cannot\n * identify. Used for both the top-level list and every slot.\n */\nfunction buildNodes(\n nodes: ExperienceNode[],\n config: ResolverConfig,\n nodeRefs: PortableRenderNode[],\n log: DebugLogger,\n diagnostics: Error[]\n): PortableRenderNode[] {\n const built: PortableRenderNode[] = [];\n for (const node of nodes) {\n const one = buildNode(node, config, nodeRefs, log, diagnostics);\n if (one !== null) built.push(one);\n }\n return built;\n}\n\n/**\n * Recursively turn a payload node into an IR node. The collected `nodeRefs`\n * array is for the resolver pass — every built node with a registered\n * resolver gets a reference appended so we can run them in parallel without\n * walking the tree twice.\n *\n * Returns `null` only for a node whose ResourceLink cannot be read, which\n * `warnUnrenderableNode` has already reported. Callers go through `buildNodes`.\n */\nfunction buildNode(\n node: ExperienceNode,\n config: ResolverConfig,\n nodeRefs: PortableRenderNode[],\n log: DebugLogger,\n diagnostics: Error[]\n): PortableRenderNode | null {\n const ref = readNodeRef(node);\n if (ref === null) {\n warnUnrenderableNode(node, log, diagnostics);\n return null;\n }\n const registration: PortableRegistration = {\n kind: ref.kind,\n id: extractIdFromUrn(ref.urn),\n };\n const nodeId = typeof node.id === 'string' ? node.id : undefined;\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 const message =\n `Slot \"${slotName}\" on ${registration.kind} \"${registration.id}\"` +\n `${nodeId ? ` (node \"${nodeId}\")` : ''} is not an array of nodes (got ` +\n `${children === null ? 'null' : typeof children}); treating it as empty instead of ` +\n `failing the whole experience. A hand-built payload with the wrong slot shape is the ` +\n `usual cause.`;\n if (typeof console !== 'undefined') {\n console.warn(`[@contentful/experiences] ${message}`);\n }\n log.log(message);\n diagnostics.push(new Error(message));\n slots[slotName] = [];\n continue;\n }\n slots[slotName] = buildNodes(children, config, nodeRefs, log, diagnostics);\n }\n }\n\n const built: PortableRenderNode = {\n registration,\n props: {\n content: { ...(node.contentProperties ?? {}) },\n // Resolved flat values are written by the pre-resolution pass below.\n design: {},\n designRaw: { ...(node.designProperties ?? {}) } as Record<string, DesignPropValue>,\n },\n slots,\n };\n if (node.id) built.nodeId = node.id;\n if (getResolver(lookupEntry(config, registration))) {\n nodeRefs.push(built);\n }\n return built;\n}\n\n// Cascade a node's raw design to the fallback viewport and resolve tokens.\n// Returns the flat resolved map plus any token ids left unresolved (dropped).\nfunction preResolveDesignProperties(\n design: Record<string, DesignPropValue>,\n viewports: ViewportDef[],\n fallbackViewportIndex: number,\n resolveToken: ResolveToken | undefined\n): { props: Record<string, unknown>; unresolved: string[] } {\n const cascaded = resolveDesignProperties(design, viewports, fallbackViewportIndex);\n return applyTokenResolver(cascaded, resolveToken);\n}\n\n// Warn when resolveToken left tokens unresolved, so pass-through keys are diagnosable.\nfunction warnUnresolvedTokens(\n label: string,\n unresolved: string[],\n log: DebugLogger,\n diagnostics: Error[]\n): void {\n if (!unresolved.length) return;\n const message = `resolveToken returned undefined for token id(s) on \"${label}\": ${unresolved.join(', ')}. Resolved design (getDesignValues()) will carry the raw, unresolved DesignToken for those keys.`;\n if (typeof console !== 'undefined') {\n console.warn(`[@contentful/experiences] ${message}`);\n }\n log.log(`unresolved token id(s) on \"${label}\": ${unresolved.join(', ')}`);\n for (const tokenId of unresolved) {\n diagnostics.push(\n new Error(\n `Design token \"${tokenId}\" on ${label} has no \\`resolveToken\\` mapping; the raw token reaches the component unresolved.`\n )\n );\n }\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 diagnostics: Error[]\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(\n `${node.registration.kind}:${node.registration.id}`,\n unresolved,\n log,\n diagnostics\n );\n for (const children of Object.values(node.slots)) {\n for (const child of children) {\n preResolveNodeTree(child, viewports, fallbackViewportIndex, resolveToken, log, diagnostics);\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 const diagnostics: Error[] = [];\n\n // A payload that isn't an object at all (missing fetch result, a raw\n // `null`/`undefined` handed in by a broken transform step) can't be\n // guarded field-by-field via `ensureArray` below — that only guards once\n // we're already inside a `payload.nodes` access. Guard the whole payload\n // first so we degrade to an empty experience instead of throwing a raw\n // TypeError, and report exactly one diagnostic rather than double-counting\n // once we also run `nodes`/`viewports` through `ensureArray`.\n const isPlainPayload = payload !== null && typeof payload === 'object';\n if (!isPlainPayload) {\n const message =\n `Experience payload is ${payload === null ? 'null' : typeof payload}, not an object; ` +\n `treating it as an empty experience (no nodes, no viewports) instead of throwing. This ` +\n `usually means the fetch or transform pipeline handed resolveExperience a missing or ` +\n `malformed payload — check what produced it.`;\n if (typeof console !== 'undefined') {\n console.warn(`[@contentful/experiences] ${message}`);\n }\n log.log(message);\n diagnostics.push(new Error(message));\n }\n const payloadNodes = isPlainPayload\n ? ensureArray<ExperienceNode>(payload.nodes, 'nodes', log, diagnostics)\n : [];\n // `viewports` is being removed from the API, so its absence is expected, not\n // malformed — only warn via `ensureArray` when the field is present but not\n // an array. Otherwise default to `[]` silently.\n const viewports =\n isPlainPayload && payload.viewports !== undefined\n ? ensureArray<ViewportDef>(payload.viewports, 'viewports', log, diagnostics)\n : [];\n\n // Pass 1: walk the payload into the IR. Collect refs to nodes that need\n // resolveData so pass 2 can run them in parallel without re-walking.\n const nodeRefs: PortableRenderNode[] = [];\n const nodes: PortableRenderNode[] = buildNodes(payloadNodes, config, nodeRefs, log, diagnostics);\n log.log(`built ${nodes.length} top-level node(s); ${nodeRefs.length} declare resolveData`);\n\n // Pass 2: run every node's resolveData hook in parallel.\n // `viewports` is always sourced from the payload — the viewport list is fact,\n // not opinion, so it can't be overridden by the caller.\n const experience: ExperienceContext = {\n debug: options.debug ?? DEFAULT_EXPERIENCE.debug,\n metadata: {\n ...DEFAULT_EXPERIENCE.metadata,\n ...(options.metadata ?? {}),\n },\n viewports,\n };\n\n const tasks: Array<Promise<void>> = [];\n\n for (const node of nodeRefs) {\n const resolver = getResolver(lookupEntry(config, node.registration));\n if (!resolver) continue;\n const ctx: ResolveContext = {\n content: node.props.content,\n design: node.props.designRaw,\n experience,\n };\n tasks.push(\n // Deferred via `.then(() => resolver(ctx))` rather than\n // `Promise.resolve(resolver(ctx))`: the latter calls `resolver(ctx)`\n // eagerly as an argument, so a resolver that throws *synchronously*\n // escapes immediately instead of becoming a rejection. The `onFailure`\n // handler below never rethrows, so one broken resolver can't fail the\n // `Promise.all` for every other node.\n Promise.resolve()\n .then(() => resolver(ctx))\n .then(\n (resolved) => {\n node.props.resolved = resolved;\n },\n (err: unknown) => {\n const label = `${node.registration.kind}:${node.registration.id}`;\n const reason = err instanceof Error ? err.message : String(err);\n const message =\n `resolveData for ${label}${node.nodeId ? ` (node \"${node.nodeId}\")` : ''} threw or ` +\n `rejected: ${reason}. Rendering the node without resolved data instead of failing ` +\n `the whole experience.`;\n if (typeof console !== 'undefined') {\n console.warn(`[@contentful/experiences] ${message}`);\n }\n log.log(message);\n diagnostics.push(new Error(message, { cause: err instanceof Error ? err : undefined }));\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(viewports, fallbackViewportId);\n for (const node of nodes) {\n preResolveNodeTree(\n node,\n viewports,\n fallbackViewportIndex,\n config.resolveToken,\n log,\n diagnostics\n );\n }\n log.log(`pre-resolved design against fallback viewport index ${fallbackViewportIndex}`);\n\n // Reuse `experience.metadata` rather than re-merging, so resolvers and the\n // renderer read the same object. `viewports` is the guarded local, not\n // `payload.viewports` — a malformed payload degrades to an empty list.\n const plan: PortableRenderPlan = {\n viewports,\n nodes,\n fallbackViewportIndex,\n diagnostics,\n metadata: experience.metadata,\n debug: experience.debug,\n };\n if (options.sourceMap !== undefined) plan.sourceMap = options.sourceMap;\n const profileId = isPlainPayload && payload.extensions?.personalization?.profile?.id;\n if (profileId) {\n plan.personalization = { profileId };\n }\n return plan;\n}\n"],"mappings":"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","names":[]}
|
|
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 resolves design values for the renderer.\n */\n\nimport { createDebugLogger, type DebugLogger } from './debug-logger.js';\nimport type {\n DesignPropValue,\n ExperienceContext,\n ExperienceSourceMap,\n ExperienceNode,\n ExperiencePayload,\n PortableRegistration,\n PortableRenderNode,\n PortableRenderPlan,\n ResolveContext,\n ResolveToken,\n} from './types.js';\nimport { applyTokenResolver, resolveDesignProperties } from './design-properties.js';\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\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 /** Carried onto the plan as-is. Omit for no source map. */\n sourceMap?: ExperienceSourceMap;\n}\n\nconst DEFAULT_EXPERIENCE: ExperienceContext = {\n debug: false,\n metadata: {},\n};\n\n/**\n * Registry lookup for a built node. A node's `kind` decides which half of the\n * customer Config owns its implementation — components and Experience\n * Templates are otherwise interchangeable at every other step.\n */\nfunction lookupEntry(config: ResolverConfig, registration: PortableRegistration): unknown {\n return registration.kind === 'experienceTemplate'\n ? config.experienceTemplates?.[registration.id]\n : config.components[registration.id];\n}\n\n/**\n * Extract the id (component or experienceTemplate) from its\n * `ResourceLink` URN. Real URN shapes:\n * crn:contentful:::experience:spaces/$self/environments/$self/components/<id>\n * crn:contentful:::experience:spaces/$self/environments/$self/experienceTemplates/<id>\n *\n * The id is the final path segment. We split on `/` and take the last\n * non-empty piece so this also tolerates trailing slashes or alternative\n * prefix shapes.\n */\nfunction extractIdFromUrn(urn: string): string {\n const segments = urn.split('/').filter((s) => s.length > 0);\n return segments[segments.length - 1] ?? urn;\n}\n\n/**\n * Read the ResourceLink a node points at. Which key is present is the only\n * difference between the two node variants: `experienceTemplate` means the\n * implementation lives in the customer's `experienceTemplates` registry,\n * `component` in `components`. Both carry ids in the same URN shape, the same\n * props shape, and the same slots — so everything downstream is kind-agnostic.\n *\n * `ExperienceNode` is a closed union, so a typed caller cannot produce anything\n * else. Payloads, however, are untrusted JSON at runtime, and the realistic way\n * a third shape arrives is a node kind *newer than this SDK*. Returns `null`\n * for anything unidentifiable rather than letting the ref access throw — see\n * `buildNode`.\n */\nfunction readNodeRef(\n node: ExperienceNode\n): { kind: PortableRegistration['kind']; urn: string } | null {\n const isExperienceTemplate = 'experienceTemplate' in node;\n const ref = isExperienceTemplate\n ? node.experienceTemplate\n : 'component' in node\n ? node.component\n : undefined;\n // Cast because the runtime value may violate the declared type: a node can\n // carry `component: {}`, which satisfies `'component' in node` but has no urn.\n const urn = (ref as { sys?: { urn?: unknown } } | undefined)?.sys?.urn;\n if (typeof urn !== 'string' || urn.length === 0) return null;\n return { kind: isExperienceTemplate ? 'experienceTemplate' : 'component', urn };\n}\n\n/**\n * Guards a top-level payload field that must be an array. A malformed or\n * partial payload (bad JSON, a hand-authored payload with the wrong shape)\n * should degrade to an empty experience rather than throwing and failing the\n * whole render — same \"dropping beats throwing\" rationale as\n * `warnUnrenderableNode` below, just one level up the tree.\n */\nfunction ensureArray<T>(\n value: unknown,\n field: string,\n log: DebugLogger,\n diagnostics: Error[]\n): T[] {\n if (Array.isArray(value)) return value as T[];\n const message =\n `Experience payload's \"${field}\" is not an array (got ${value === null ? 'null' : typeof value}); ` +\n `treating it as empty instead of throwing. This usually means a malformed or partial payload — ` +\n `check what produced it.`;\n if (typeof console !== 'undefined') {\n console.warn(`[@contentful/experiences] ${message}`);\n }\n log.log(message);\n diagnostics.push(new Error(message));\n return [];\n}\n\n/** Total node count in a payload subtree, the node itself included. */\nfunction countPayloadNodes(node: ExperienceNode): number {\n const slots = (node as { slots?: Record<string, unknown> }).slots;\n let total = 1;\n if (slots && typeof slots === 'object') {\n for (const children of Object.values(slots)) {\n if (!Array.isArray(children)) continue;\n for (const child of children) total += countPayloadNodes(child as ExperienceNode);\n }\n }\n return total;\n}\n\n/**\n * The one case where a node is dropped. AIS-413 was the opposite failure —\n * a node kind the SDK recognized and could have rendered, skipped behind a\n * vague warning, taking its whole subtree with it — so the bar here is that\n * nothing vanishes without a diagnostic naming what was lost.\n *\n * Dropping beats throwing: a `resolveExperience` rejection fails the entire\n * experience, so one unrecognized node in a sidebar would take down every page\n * containing it, for a payload the customer does not control and cannot fix.\n */\nfunction warnUnrenderableNode(node: ExperienceNode, log: DebugLogger, diagnostics: Error[]): void {\n const id = (node as { id?: unknown }).id;\n const label = typeof id === 'string' ? ` \"${id}\"` : '';\n const keys = Object.keys(node);\n const descendants = countPayloadNodes(node) - 1;\n const message =\n `Skipping unidentifiable node${label}: expected a \\`component\\` or \\`experienceTemplate\\` ` +\n `ResourceLink carrying a urn, got keys [${keys.join(', ')}]. Dropping it and ${descendants} ` +\n `descendant node(s). A payload node kind this SDK does not know is usually a version skew — ` +\n `upgrading @contentful/experiences-sdk-core may be all that is needed.`;\n if (typeof console !== 'undefined') {\n console.warn(`[@contentful/experiences] ${message}`);\n }\n log.log(message);\n diagnostics.push(new Error(message));\n}\n\n/**\n * Walk a sibling list into IR nodes, dropping any node `buildNode` cannot\n * identify. Used for both the top-level list and every slot.\n */\nfunction buildNodes(\n nodes: ExperienceNode[],\n config: ResolverConfig,\n nodeRefs: PortableRenderNode[],\n log: DebugLogger,\n diagnostics: Error[]\n): PortableRenderNode[] {\n const built: PortableRenderNode[] = [];\n for (const node of nodes) {\n const one = buildNode(node, config, nodeRefs, log, diagnostics);\n if (one !== null) built.push(one);\n }\n return built;\n}\n\n/**\n * Recursively turn a payload node into an IR node. The collected `nodeRefs`\n * array is for the resolver pass — every built node with a registered\n * resolver gets a reference appended so we can run them in parallel without\n * walking the tree twice.\n *\n * Returns `null` only for a node whose ResourceLink cannot be read, which\n * `warnUnrenderableNode` has already reported. Callers go through `buildNodes`.\n */\nfunction buildNode(\n node: ExperienceNode,\n config: ResolverConfig,\n nodeRefs: PortableRenderNode[],\n log: DebugLogger,\n diagnostics: Error[]\n): PortableRenderNode | null {\n const ref = readNodeRef(node);\n if (ref === null) {\n warnUnrenderableNode(node, log, diagnostics);\n return null;\n }\n const registration: PortableRegistration = {\n kind: ref.kind,\n id: extractIdFromUrn(ref.urn),\n };\n const nodeId = typeof node.id === 'string' ? node.id : undefined;\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 const message =\n `Slot \"${slotName}\" on ${registration.kind} \"${registration.id}\"` +\n `${nodeId ? ` (node \"${nodeId}\")` : ''} is not an array of nodes (got ` +\n `${children === null ? 'null' : typeof children}); treating it as empty instead of ` +\n `failing the whole experience. A hand-built payload with the wrong slot shape is the ` +\n `usual cause.`;\n if (typeof console !== 'undefined') {\n console.warn(`[@contentful/experiences] ${message}`);\n }\n log.log(message);\n diagnostics.push(new Error(message));\n slots[slotName] = [];\n continue;\n }\n slots[slotName] = buildNodes(children, config, nodeRefs, log, diagnostics);\n }\n }\n\n const built: PortableRenderNode = {\n registration,\n props: {\n content: { ...(node.contentProperties ?? {}) },\n // Resolved design values are written by the pre-resolution pass below.\n design: {},\n designRaw: { ...(node.designProperties ?? {}) } as Record<string, DesignPropValue>,\n },\n slots,\n };\n if (node.id) built.nodeId = node.id;\n if (getResolver(lookupEntry(config, registration))) {\n nodeRefs.push(built);\n }\n return built;\n}\n\n// Resolve a node's raw design values and tokens.\nfunction preResolveDesignProperties(\n design: Record<string, DesignPropValue>,\n resolveToken: ResolveToken | undefined\n): { props: Record<string, unknown>; unresolved: string[] } {\n return applyTokenResolver(resolveDesignProperties(design), resolveToken);\n}\n\n// Warn when resolveToken left tokens unresolved, so pass-through keys are diagnosable.\nfunction warnUnresolvedTokens(\n label: string,\n unresolved: string[],\n log: DebugLogger,\n diagnostics: Error[]\n): void {\n if (!unresolved.length) return;\n const message = `resolveToken returned undefined for token id(s) on \"${label}\": ${unresolved.join(', ')}. Resolved design (getDesignValues()) will carry the raw, unresolved DesignToken for those keys.`;\n if (typeof console !== 'undefined') {\n console.warn(`[@contentful/experiences] ${message}`);\n }\n log.log(`unresolved token id(s) on \"${label}\": ${unresolved.join(', ')}`);\n for (const tokenId of unresolved) {\n diagnostics.push(\n new Error(\n `Design token \"${tokenId}\" on ${label} has no \\`resolveToken\\` mapping; the raw token reaches the component unresolved.`\n )\n );\n }\n}\n\n// Depth-first token resolution for a node and its slot children.\nfunction preResolveNodeTree(\n node: PortableRenderNode,\n resolveToken: ResolveToken | undefined,\n log: DebugLogger,\n diagnostics: Error[]\n): void {\n const { props, unresolved } = preResolveDesignProperties(node.props.designRaw, resolveToken);\n node.props.design = props;\n warnUnresolvedTokens(\n `${node.registration.kind}:${node.registration.id}`,\n unresolved,\n log,\n diagnostics\n );\n for (const children of Object.values(node.slots)) {\n for (const child of children) {\n preResolveNodeTree(child, resolveToken, log, diagnostics);\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 const diagnostics: Error[] = [];\n\n // A payload that isn't an object at all (missing fetch result, a raw\n // `null`/`undefined` handed in by a broken transform step) can't be\n // guarded field-by-field via `ensureArray` below — that only guards once\n // we're already inside a `payload.nodes` access. Guard the whole payload\n // first so we degrade to an empty experience instead of throwing a raw\n // TypeError, and report exactly one diagnostic rather than double-counting\n // once we also run `nodes` through `ensureArray`.\n const isPlainPayload = payload !== null && typeof payload === 'object';\n if (!isPlainPayload) {\n const message =\n `Experience payload is ${payload === null ? 'null' : typeof payload}, not an object; ` +\n `treating it as an empty experience (no nodes) instead of throwing. This ` +\n `usually means the fetch or transform pipeline handed resolveExperience a missing or ` +\n `malformed payload — check what produced it.`;\n if (typeof console !== 'undefined') {\n console.warn(`[@contentful/experiences] ${message}`);\n }\n log.log(message);\n diagnostics.push(new Error(message));\n }\n const payloadNodes = isPlainPayload\n ? ensureArray<ExperienceNode>(payload.nodes, 'nodes', log, diagnostics)\n : [];\n // Pass 1: walk the payload into the IR. Collect refs to nodes that need\n // resolveData so pass 2 can run them in parallel without re-walking.\n const nodeRefs: PortableRenderNode[] = [];\n const nodes: PortableRenderNode[] = buildNodes(payloadNodes, config, nodeRefs, log, diagnostics);\n log.log(`built ${nodes.length} top-level node(s); ${nodeRefs.length} declare resolveData`);\n\n // Pass 2: run every node's resolveData hook in parallel.\n const experience: ExperienceContext = {\n debug: options.debug ?? DEFAULT_EXPERIENCE.debug,\n metadata: {\n ...DEFAULT_EXPERIENCE.metadata,\n ...(options.metadata ?? {}),\n },\n };\n\n const tasks: Array<Promise<void>> = [];\n\n for (const node of nodeRefs) {\n const resolver = getResolver(lookupEntry(config, node.registration));\n if (!resolver) continue;\n const ctx: ResolveContext = {\n content: node.props.content,\n design: node.props.designRaw,\n experience,\n };\n tasks.push(\n // Deferred via `.then(() => resolver(ctx))` rather than\n // `Promise.resolve(resolver(ctx))`: the latter calls `resolver(ctx)`\n // eagerly as an argument, so a resolver that throws *synchronously*\n // escapes immediately instead of becoming a rejection. The `onFailure`\n // handler below never rethrows, so one broken resolver can't fail the\n // `Promise.all` for every other node.\n Promise.resolve()\n .then(() => resolver(ctx))\n .then(\n (resolved) => {\n node.props.resolved = resolved;\n },\n (err: unknown) => {\n const label = `${node.registration.kind}:${node.registration.id}`;\n const reason = err instanceof Error ? err.message : String(err);\n const message =\n `resolveData for ${label}${node.nodeId ? ` (node \"${node.nodeId}\")` : ''} threw or ` +\n `rejected: ${reason}. Rendering the node without resolved data instead of failing ` +\n `the whole experience.`;\n if (typeof console !== 'undefined') {\n console.warn(`[@contentful/experiences] ${message}`);\n }\n log.log(message);\n diagnostics.push(new Error(message, { cause: err instanceof Error ? err : undefined }));\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 for (const node of nodes) {\n preResolveNodeTree(node, config.resolveToken, log, diagnostics);\n }\n log.log('resolved design properties');\n\n // Reuse `experience.metadata` rather than re-merging, so resolvers and the\n // renderer read the same object.\n const plan: PortableRenderPlan = {\n nodes,\n diagnostics,\n metadata: experience.metadata,\n debug: experience.debug,\n };\n if (options.sourceMap !== undefined) plan.sourceMap = options.sourceMap;\n const profileId = isPlainPayload && payload.extensions?.personalization?.profile?.id;\n if (profileId) {\n plan.personalization = { profileId };\n }\n return plan;\n}\n"],"mappings":"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","names":[]}
|
package/dist/types.d.ts
CHANGED
|
@@ -4,12 +4,6 @@
|
|
|
4
4
|
* every `resolveData` hook as `ctx.experience`. Never spread onto component
|
|
5
5
|
* props — components receive only the props they declare.
|
|
6
6
|
*
|
|
7
|
-
* `viewports` (the *list*) is here so customer resolvers can inspect what
|
|
8
|
-
* viewports an Experience declares (e.g. "is there a mobile viewport?").
|
|
9
|
-
* The *active* viewport is render-time only and lives on the framework
|
|
10
|
-
* adapter's RenderContext — exposing it here would mean async resolvers
|
|
11
|
-
* re-fire on every viewport change, which would be a footgun.
|
|
12
|
-
*
|
|
13
7
|
* `debug` is the single observability switch. When on it: emits verbose logs
|
|
14
8
|
* from `resolveExperience` and `fetchExperience`; renders the visible
|
|
15
9
|
* missing-component box (see the adapters' `MissingComponent`); and turns the
|
|
@@ -27,63 +21,15 @@ interface ExperienceContext {
|
|
|
27
21
|
* crosses server/client boundaries.
|
|
28
22
|
*/
|
|
29
23
|
metadata: Record<string, unknown>;
|
|
30
|
-
/**
|
|
31
|
-
* @deprecated Viewports are being removed from the Experiences APIs. Design
|
|
32
|
-
* property values arrive flat. Will not be removed before 2026-10-06.
|
|
33
|
-
*/
|
|
34
|
-
viewports: ViewportDef[];
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* `ExperienceContext` plus the viewport state that only exists at render time.
|
|
38
|
-
*
|
|
39
|
-
* Declared here, not per-adapter: it is plain data, so every adapter re-exports
|
|
40
|
-
* this same type and a field added here reaches all three at once.
|
|
41
|
-
*/
|
|
42
|
-
interface RenderContext extends ExperienceContext {
|
|
43
|
-
/**
|
|
44
|
-
* @deprecated Viewport state is being removed from the Experiences APIs.
|
|
45
|
-
* Will not be removed before 2026-10-06.
|
|
46
|
-
*/
|
|
47
|
-
activeViewport: ViewportDef;
|
|
48
|
-
/**
|
|
49
|
-
* @deprecated Viewport state is being removed from the Experiences APIs.
|
|
50
|
-
* Will not be removed before 2026-10-06.
|
|
51
|
-
*/
|
|
52
|
-
activeViewportIndex: number;
|
|
53
|
-
/**
|
|
54
|
-
* @deprecated Viewport state is being removed from the Experiences APIs.
|
|
55
|
-
* Will not be removed before 2026-10-06.
|
|
56
|
-
*/
|
|
57
|
-
fallbackViewportIndex: number;
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* One viewport definition from a delivered Experience. The `query` is the
|
|
61
|
-
* Contentful media-query DSL ("*" | "<992px" | ">1200px"), not raw CSS.
|
|
62
|
-
*
|
|
63
|
-
* The first viewport in the list is conventionally the wildcard ("*") that
|
|
64
|
-
* always matches. The viewport order encodes the cascade direction —
|
|
65
|
-
* desktop-first (descending) or mobile-first (ascending).
|
|
66
|
-
*/
|
|
67
|
-
/**
|
|
68
|
-
* @deprecated Viewports are being removed from the Experiences APIs. No
|
|
69
|
-
* customer action is needed: design property values arrive flat. Will not be
|
|
70
|
-
* removed before 2026-10-06.
|
|
71
|
-
*/
|
|
72
|
-
interface ViewportDef {
|
|
73
|
-
id: string;
|
|
74
|
-
query: string;
|
|
75
|
-
displayName: string;
|
|
76
|
-
previewSize: string;
|
|
77
24
|
}
|
|
25
|
+
/** Render-time context is the same plain data supplied to `resolveData`. */
|
|
26
|
+
type RenderContext = ExperienceContext;
|
|
78
27
|
/**
|
|
79
28
|
* Discriminated design-property value as it arrives from XDA. v1 accepts:
|
|
80
|
-
* - ManualDesignValue: an explicit scalar
|
|
81
|
-
* -
|
|
82
|
-
* ManualDesignValue or DesignToken.
|
|
83
|
-
* - DesignToken: a token reference, passed through to customer components
|
|
84
|
-
* as-is for v1. Resolution lands in the future tokens package.
|
|
29
|
+
* - ManualDesignValue: an explicit scalar.
|
|
30
|
+
* - DesignToken: a token reference resolved while building the render plan.
|
|
85
31
|
*/
|
|
86
|
-
type DesignPropValue = ManualDesignValue | DesignToken
|
|
32
|
+
type DesignPropValue = ManualDesignValue | DesignToken;
|
|
87
33
|
interface ManualDesignValue {
|
|
88
34
|
type: 'ManualDesignValue';
|
|
89
35
|
value: string | number | boolean;
|
|
@@ -94,19 +40,10 @@ interface DesignToken {
|
|
|
94
40
|
}
|
|
95
41
|
/**
|
|
96
42
|
* Turns a `DesignToken` into a runtime value. `ref.value` is the
|
|
97
|
-
* customer-defined token id; returning `undefined`
|
|
98
|
-
*
|
|
43
|
+
* customer-defined token id; returning `undefined` retains the raw token and
|
|
44
|
+
* records a diagnostic. Sync only — it runs while building the render plan.
|
|
99
45
|
*/
|
|
100
46
|
type ResolveToken = (ref: DesignToken) => unknown;
|
|
101
|
-
/**
|
|
102
|
-
* @deprecated Viewports are being removed from the Experiences APIs. No
|
|
103
|
-
* customer action is needed: design property values arrive flat. Will not be
|
|
104
|
-
* removed before 2026-10-06.
|
|
105
|
-
*/
|
|
106
|
-
interface ValuesByViewport {
|
|
107
|
-
type: 'ValuesByViewport';
|
|
108
|
-
values: Record<string, ManualDesignValue | DesignToken>;
|
|
109
|
-
}
|
|
110
47
|
/**
|
|
111
48
|
* Resource-link reference to a registered Component. The `urn` carries
|
|
112
49
|
* the component id; the build-plan extracts the id by taking the segment after
|
|
@@ -226,11 +163,6 @@ interface ExperienceExtensions {
|
|
|
226
163
|
* `@contentful/experience-delivery` sends on every request.
|
|
227
164
|
*/
|
|
228
165
|
interface ExperiencePayload {
|
|
229
|
-
/**
|
|
230
|
-
* @deprecated Absent once the API stops sending `viewports`. `resolveExperience`
|
|
231
|
-
* defaults to `[]` when this is missing. Will not be removed before 2026-10-06.
|
|
232
|
-
*/
|
|
233
|
-
viewports?: ViewportDef[];
|
|
234
166
|
nodes: ExperienceNode[];
|
|
235
167
|
errors?: unknown[];
|
|
236
168
|
extensions?: ExperienceExtensions;
|
|
@@ -238,9 +170,7 @@ interface ExperiencePayload {
|
|
|
238
170
|
}
|
|
239
171
|
/**
|
|
240
172
|
* Per-node context handed to a component's `resolveData` resolver. Carries
|
|
241
|
-
* the raw content + design props from the payload
|
|
242
|
-
* pre-resolved against a viewport — viewport resolution stays a render-time
|
|
243
|
-
* concern so client viewport changes don't re-trigger async resolvers).
|
|
173
|
+
* the raw content + design props from the payload.
|
|
244
174
|
*/
|
|
245
175
|
interface ResolveContext {
|
|
246
176
|
content: Record<string, unknown>;
|
|
@@ -268,19 +198,16 @@ interface PortableRegistration {
|
|
|
268
198
|
* that lets non-React adapters (Angular, SwiftUI, Compose) consume the same
|
|
269
199
|
* interpretation.
|
|
270
200
|
*
|
|
271
|
-
* Design props preserve the discriminated value shape as they arrived.
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
* how to resolve them in v1.)
|
|
201
|
+
* Design props preserve the discriminated value shape as they arrived. Token
|
|
202
|
+
* resolution happens when the plan is built, before any framework renderer
|
|
203
|
+
* reads the values.
|
|
275
204
|
*
|
|
276
205
|
* `props.resolved` is populated by `resolveExperience` from any
|
|
277
206
|
* customer-supplied `resolveData` resolver and merged into the final props
|
|
278
207
|
* after content + design but before slot props.
|
|
279
208
|
*
|
|
280
|
-
* `props.design`
|
|
281
|
-
*
|
|
282
|
-
* the client can re-resolve when the active viewport differs. See the fields
|
|
283
|
-
* below.
|
|
209
|
+
* `props.design` contains values ready for the renderer; `props.designRaw`
|
|
210
|
+
* preserves the source values for `resolveData` and payload context helpers.
|
|
284
211
|
*/
|
|
285
212
|
interface PortableRenderNode {
|
|
286
213
|
/**
|
|
@@ -292,13 +219,10 @@ interface PortableRenderNode {
|
|
|
292
219
|
registration: PortableRegistration;
|
|
293
220
|
props: {
|
|
294
221
|
content: Record<string, unknown>;
|
|
295
|
-
/**
|
|
222
|
+
/** Token-resolved design values. */
|
|
296
223
|
design: Record<string, unknown>;
|
|
297
224
|
resolved?: Record<string, unknown>;
|
|
298
|
-
/**
|
|
299
|
-
* @deprecated Design property values are becoming flat. Will not be
|
|
300
|
-
* removed before 2026-10-06.
|
|
301
|
-
*/
|
|
225
|
+
/** Design-property envelopes as delivered by XDA. */
|
|
302
226
|
designRaw: Record<string, DesignPropValue>;
|
|
303
227
|
};
|
|
304
228
|
/**
|
|
@@ -319,17 +243,7 @@ interface PortableRenderNode {
|
|
|
319
243
|
* is no plan-level template concept — see `PortableRegistration`.
|
|
320
244
|
*/
|
|
321
245
|
interface PortableRenderPlan {
|
|
322
|
-
/**
|
|
323
|
-
* @deprecated Viewports are being removed from the Experiences APIs. Design
|
|
324
|
-
* property values arrive flat. Will not be removed before 2026-10-06.
|
|
325
|
-
*/
|
|
326
|
-
viewports: ViewportDef[];
|
|
327
246
|
nodes: PortableRenderNode[];
|
|
328
|
-
/**
|
|
329
|
-
* @deprecated Viewport state is being removed from the Experiences APIs.
|
|
330
|
-
* Will not be removed before 2026-10-06.
|
|
331
|
-
*/
|
|
332
|
-
fallbackViewportIndex: number;
|
|
333
247
|
/**
|
|
334
248
|
* The `metadata` the resolve step ran with, so the renderer does not need it
|
|
335
249
|
* passed again. The renderer's `metadata` prop merges over this. `{}` when
|
|
@@ -363,4 +277,4 @@ interface PortableRenderPlan {
|
|
|
363
277
|
diagnostics: Error[];
|
|
364
278
|
}
|
|
365
279
|
|
|
366
|
-
export type { ComponentNode, ComponentRef, DesignPropValue, DesignToken, ExperienceContext, ExperienceExtensions, ExperienceNode, ExperiencePayload, ExperienceSourceMap, ExperienceSys, ExperienceTemplateNode, ExperienceTemplateRef, ManualDesignValue, PortableRegistration, PortableRenderNode, PortableRenderPlan, RenderContext, ResolveContext, ResolveToken
|
|
280
|
+
export type { ComponentNode, ComponentRef, DesignPropValue, DesignToken, ExperienceContext, ExperienceExtensions, ExperienceNode, ExperiencePayload, ExperienceSourceMap, ExperienceSys, ExperienceTemplateNode, ExperienceTemplateRef, ManualDesignValue, PortableRegistration, PortableRenderNode, PortableRenderPlan, RenderContext, ResolveContext, ResolveToken };
|
package/package.json
CHANGED
package/dist/viewport.d.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { DesignToken, ResolveToken, DesignPropValue, ViewportDef } from './types.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Viewport id → index. Returns 0 (the wildcard viewport) when unknown.
|
|
5
|
-
*
|
|
6
|
-
* @deprecated Viewports are being removed from the Experiences APIs. No
|
|
7
|
-
* customer action is needed: design property values arrive flat, so this
|
|
8
|
-
* resolution step is skipped automatically. Will not be removed before
|
|
9
|
-
* 2026-10-06.
|
|
10
|
-
*/
|
|
11
|
-
declare function getViewportIndex(viewports: ViewportDef[], viewportId?: string): number;
|
|
12
|
-
/**
|
|
13
|
-
* Resolve one design property to its render-time value (cascade + unwrap).
|
|
14
|
-
*
|
|
15
|
-
* @deprecated Viewports are being removed from the Experiences APIs. No
|
|
16
|
-
* customer action is needed: flat values already return early here, so this
|
|
17
|
-
* cascade step is skipped automatically. Will not be removed before
|
|
18
|
-
* 2026-10-06.
|
|
19
|
-
*/
|
|
20
|
-
declare function getValueForViewport(prop: DesignPropValue | undefined, viewports: ViewportDef[], activeViewportIndex: number): string | number | boolean | DesignToken | undefined;
|
|
21
|
-
/**
|
|
22
|
-
* Resolve every design property on a node into a flat record keyed by name.
|
|
23
|
-
*
|
|
24
|
-
* NOTE: once viewports are removed from the Experiences APIs, every design
|
|
25
|
-
* property value arrives already flat, so the `viewports` and
|
|
26
|
-
* `activeViewportIndex` parameters will be dropped. Will not happen before
|
|
27
|
-
* 2026-10-06.
|
|
28
|
-
*/
|
|
29
|
-
declare function resolveDesignProperties(designProperties: Record<string, DesignPropValue> | undefined, viewports: ViewportDef[], activeViewportIndex: number): Record<string, string | number | boolean | DesignToken>;
|
|
30
|
-
/**
|
|
31
|
-
* Resolve `DesignToken` values via `resolveToken`; scalars pass through.
|
|
32
|
-
* Keys that don't resolve — no `resolveToken` configured, or the resolver
|
|
33
|
-
* returns `undefined` for that particular token — pass through as the raw
|
|
34
|
-
* `DesignToken` and their id is collected in `unresolved` for the caller to
|
|
35
|
-
* warn/diagnose (see `warnUnresolvedTokens` in `resolve-experience.ts`, which
|
|
36
|
-
* already dedupes per `resolveExperience()` call, so this function carries
|
|
37
|
-
* no warn-once state of its own).
|
|
38
|
-
*/
|
|
39
|
-
declare function applyTokenResolver(props: Record<string, string | number | boolean | DesignToken>, resolveToken?: ResolveToken): {
|
|
40
|
-
props: Record<string, unknown>;
|
|
41
|
-
unresolved: string[];
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
export { applyTokenResolver, getValueForViewport, getViewportIndex, resolveDesignProperties };
|
package/dist/viewport.js
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
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
|
-
function applyTokenResolver(props, resolveToken) {
|
|
36
|
-
const out = {};
|
|
37
|
-
const unresolved = [];
|
|
38
|
-
for (const [key, value] of Object.entries(props)) {
|
|
39
|
-
if (typeof value === "object" && value !== null && value.type === "DesignToken") {
|
|
40
|
-
const resolved = resolveToken?.(value);
|
|
41
|
-
if (resolved === void 0) {
|
|
42
|
-
out[key] = value;
|
|
43
|
-
unresolved.push(value.value);
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
out[key] = resolved;
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
out[key] = value;
|
|
50
|
-
}
|
|
51
|
-
return { props: out, unresolved };
|
|
52
|
-
}
|
|
53
|
-
export {
|
|
54
|
-
applyTokenResolver,
|
|
55
|
-
getValueForViewport,
|
|
56
|
-
getViewportIndex,
|
|
57
|
-
resolveDesignProperties
|
|
58
|
-
};
|
|
59
|
-
//# sourceMappingURL=viewport.js.map
|
package/dist/viewport.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
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.js';\n\n/**\n * Viewport id → index. Returns 0 (the wildcard viewport) when unknown.\n *\n * @deprecated Viewports are being removed from the Experiences APIs. No\n * customer action is needed: design property values arrive flat, so this\n * resolution step is skipped automatically. Will not be removed before\n * 2026-10-06.\n */\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/**\n * Resolve one design property to its render-time value (cascade + unwrap).\n *\n * @deprecated Viewports are being removed from the Experiences APIs. No\n * customer action is needed: flat values already return early here, so this\n * cascade step is skipped automatically. Will not be removed before\n * 2026-10-06.\n */\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/**\n * Resolve every design property on a node into a flat record keyed by name.\n *\n * NOTE: once viewports are removed from the Experiences APIs, every design\n * property value arrives already flat, so the `viewports` and\n * `activeViewportIndex` parameters will be dropped. Will not happen before\n * 2026-10-06.\n */\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\n/**\n * Resolve `DesignToken` values via `resolveToken`; scalars pass through.\n * Keys that don't resolve — no `resolveToken` configured, or the resolver\n * returns `undefined` for that particular token — pass through as the raw\n * `DesignToken` and their id is collected in `unresolved` for the caller to\n * warn/diagnose (see `warnUnresolvedTokens` in `resolve-experience.ts`, which\n * already dedupes per `resolveExperience()` call, so this function carries\n * no warn-once state of its own).\n */\nexport function applyTokenResolver(\n props: Record<string, string | number | boolean | DesignToken>,\n resolveToken?: ResolveToken\n): { props: Record<string, unknown>; unresolved: string[] } {\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 out[key] = value;\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":"AA4BO,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;AAUO,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;AAUO,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;AAWO,SAAS,mBACd,OACA,cAC0D;AAC1D,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,eAAe,KAAK;AACrC,UAAI,aAAa,QAAW;AAC1B,YAAI,GAAG,IAAI;AACX,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":[]}
|