@contentful/experiences-svelte 0.5.5 → 0.5.6
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 +12 -0
- package/README.md +8 -6
- package/dist/ClientExperienceRenderer.svelte +6 -11
- package/dist/ClientExperienceRenderer.svelte.d.ts.map +1 -1
- package/dist/DebugExperience.svelte +9 -2
- package/dist/DebugExperience.svelte.d.ts.map +1 -1
- package/dist/DebugExperience.test.js +22 -9
- package/dist/NodeRenderer.svelte +72 -18
- package/dist/NodeRenderer.svelte.d.ts +6 -1
- package/dist/NodeRenderer.svelte.d.ts.map +1 -1
- package/dist/NodesRenderer.svelte +40 -17
- package/dist/NodesRenderer.svelte.d.ts.map +1 -1
- package/dist/ServerExperienceRenderer.svelte +6 -11
- package/dist/ServerExperienceRenderer.svelte.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/nodes-renderer.ssr.test.d.ts +2 -0
- package/dist/nodes-renderer.ssr.test.d.ts.map +1 -0
- package/dist/nodes-renderer.ssr.test.js +126 -0
- package/dist/server-renderer.test.js +100 -44
- package/dist/test-fixtures/ExperienceTemplateFixture.svelte +8 -3
- package/dist/test-fixtures/ExperienceTemplateFixture.svelte.d.ts +1 -1
- package/dist/test-fixtures/ExperienceTemplateFixture.svelte.d.ts.map +1 -1
- package/dist/types.d.ts +28 -10
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -5
- package/dist/WrapWithExperienceTemplate.svelte +0 -90
- package/dist/WrapWithExperienceTemplate.svelte.d.ts +0 -13
- package/dist/WrapWithExperienceTemplate.svelte.d.ts.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## 0.5.6 (2026-08-12)
|
|
2
|
+
|
|
3
|
+
### 🩹 Fixes
|
|
4
|
+
|
|
5
|
+
- **core,adapters:** render Experience Templates as ordinary nodes [AIS-413] ([#133](https://github.com/contentful/experiences/pull/133))
|
|
6
|
+
|
|
7
|
+
### 🧱 Updated Dependencies
|
|
8
|
+
|
|
9
|
+
- Updated client to 0.3.5
|
|
10
|
+
- Updated design to 0.7.5
|
|
11
|
+
- Updated core to 0.7.5
|
|
12
|
+
|
|
1
13
|
## 0.5.5 (2026-08-11)
|
|
2
14
|
|
|
3
15
|
### 🚀 Features
|
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ The public API mirrors `@contentful/experiences-react` 1:1 in shape; only the re
|
|
|
20
20
|
|
|
21
21
|
```ts
|
|
22
22
|
defineComponent<Props>(config); // Type-narrowing identity for component-type configs
|
|
23
|
-
defineExperienceTemplate<Props>(config); // Same shape, for
|
|
23
|
+
defineExperienceTemplate<Props>(config); // Same shape, for coded Experience Template configs
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
### Resolver
|
|
@@ -35,7 +35,7 @@ resolveExperience(payload, config, opts?); // Async; walks payload, runs resolve
|
|
|
35
35
|
ServerExperienceRenderer; // SSR-safe; active viewport seeded from initialViewportId
|
|
36
36
|
ClientExperienceRenderer; // Subscribes to window.matchMedia via runes
|
|
37
37
|
MissingComponent; // Default fallback for unregistered component types
|
|
38
|
-
NodesRenderer; // Exposed so you can render
|
|
38
|
+
NodesRenderer; // Exposed so you can re-render a slot's raw nodes yourself (see Slot children)
|
|
39
39
|
useActiveViewport; // Rune-backed reactive object; you'll rarely need it directly
|
|
40
40
|
```
|
|
41
41
|
|
|
@@ -46,7 +46,7 @@ getDesignValues<T>(); // Optional helper: the same resolved design record that a
|
|
|
46
46
|
toCss(design, options?); // Turns a design record into a plain style object, keeping only real CSS keys
|
|
47
47
|
getExperience(); // RenderContext: debug, metadata, viewports, activeViewport
|
|
48
48
|
getContentfulComponent(); // Raw payload for the enclosing node (or undefined)
|
|
49
|
-
getContentfulExperienceTemplate(); // Same, for
|
|
49
|
+
getContentfulExperienceTemplate(); // Same, for an enclosing coded Experience Template node
|
|
50
50
|
type ToCssOptions;
|
|
51
51
|
```
|
|
52
52
|
|
|
@@ -61,7 +61,7 @@ type ComponentConfig, ExperienceTemplateConfig,
|
|
|
61
61
|
type ContentfulComponent, ContentfulExperienceTemplate,
|
|
62
62
|
type RenderContext, ResolveToken,
|
|
63
63
|
type ExperiencePayload, ExperienceNode, ComponentNode, ExperienceTemplateNode,
|
|
64
|
-
type PortableRenderPlan, PortableRenderNode,
|
|
64
|
+
type PortableRenderPlan, PortableRenderNode, PortableRegistration,
|
|
65
65
|
type DesignPropValue, ManualDesignValue, DesignToken, ValuesByViewport,
|
|
66
66
|
type ViewportDef, ExperienceContext, ResolveContext,
|
|
67
67
|
type ResolverConfig, ResolveExperienceOptions
|
|
@@ -129,7 +129,7 @@ export const experienceConfig: Config = { components, resolveToken };
|
|
|
129
129
|
|
|
130
130
|
### Slot children
|
|
131
131
|
|
|
132
|
-
|
|
132
|
+
Every slot arrives as a prop named after the slot, holding an **array of Snippets** (`Snippet[]`), one per child — not a single wrapping Snippet. For the common "just render them" case, iterate the array with `{#each}` and `{@render}`; to wrap, reorder, or drop children individually, do it inside that loop.
|
|
133
133
|
|
|
134
134
|
```svelte
|
|
135
135
|
<!-- Section.svelte -->
|
|
@@ -156,7 +156,9 @@ A component's default slot children arrive as an **array of Snippets** (`Snippet
|
|
|
156
156
|
-->
|
|
157
157
|
```
|
|
158
158
|
|
|
159
|
-
|
|
159
|
+
`children` is not special — it is simply the conventional name for the default slot. **Every** slot in the payload becomes a same-named `Snippet[]` prop, so a component with a `header` slot just declares `header?: Snippet[]` and renders it the same way. This applies identically to coded Experience Templates: a template with a `content` slot receives a `content` prop.
|
|
160
|
+
|
|
161
|
+
If you'd rather render a slot's raw nodes yourself, they are still on the payload at `getContentfulComponent().slots` (a `Record<string, PortableRenderNode[]>`) — hand them to `NodesRenderer`.
|
|
160
162
|
|
|
161
163
|
For the full getting-started walkthrough, the merge-precedence rules, viewport handling, and design rationale, see the [root README](../../README.md) and [`AGENTS.md`](../../AGENTS.md).
|
|
162
164
|
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
import DebugExperience from './DebugExperience.svelte';
|
|
15
15
|
import MissingComponent from './MissingComponent.svelte';
|
|
16
16
|
import NodesRenderer from './NodesRenderer.svelte';
|
|
17
|
-
import WrapWithExperienceTemplate from './WrapWithExperienceTemplate.svelte';
|
|
18
17
|
import type { ClientExperienceRendererProps } from './component-props.js';
|
|
19
18
|
import { setExperience } from './context.js';
|
|
20
19
|
import type { RenderContext } from './types.js';
|
|
@@ -75,14 +74,10 @@
|
|
|
75
74
|
{#if debug}
|
|
76
75
|
<DebugExperience {experience} />
|
|
77
76
|
{/if}
|
|
78
|
-
<
|
|
79
|
-
{
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
{renderUnknown}
|
|
85
|
-
/>
|
|
86
|
-
{/snippet}
|
|
87
|
-
</WrapWithExperienceTemplate>
|
|
77
|
+
<NodesRenderer
|
|
78
|
+
nodes={experience.nodes}
|
|
79
|
+
{config}
|
|
80
|
+
experience={liveContext}
|
|
81
|
+
{renderUnknown}
|
|
82
|
+
/>
|
|
88
83
|
{/if}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ClientExperienceRenderer.svelte.d.ts","sourceRoot":"","sources":["../src/ClientExperienceRenderer.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ClientExperienceRenderer.svelte.d.ts","sourceRoot":"","sources":["../src/ClientExperienceRenderer.svelte.ts"],"names":[],"mappings":"AAqFA,QAAA,MAAM,wBAAwB,kGAAwC,CAAC;AACvE,KAAK,wBAAwB,GAAG,UAAU,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAC5E,eAAe,wBAAwB,CAAC"}
|
|
@@ -17,10 +17,17 @@
|
|
|
17
17
|
let { experience, defaultOpen = false }: DebugExperienceProps = $props();
|
|
18
18
|
|
|
19
19
|
const nodeCount = $derived(experience.nodes.length);
|
|
20
|
+
// Coded Experience Templates are ordinary top-level nodes, so name them from
|
|
21
|
+
// the node list — a composite Experience simply has none to report.
|
|
22
|
+
const templateIds = $derived(
|
|
23
|
+
experience.nodes
|
|
24
|
+
.filter((node) => node.registration.kind === 'experienceTemplate')
|
|
25
|
+
.map((node) => node.registration.id)
|
|
26
|
+
);
|
|
20
27
|
const summary = $derived(
|
|
21
28
|
`Experience debug — ${nodeCount} top-level node${nodeCount === 1 ? '' : 's'}${
|
|
22
|
-
|
|
23
|
-
? `, experience template: ${
|
|
29
|
+
templateIds.length
|
|
30
|
+
? `, experience template${templateIds.length === 1 ? '' : 's'}: ${templateIds.join(', ')}`
|
|
24
31
|
: ''
|
|
25
32
|
}`
|
|
26
33
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DebugExperience.svelte.d.ts","sourceRoot":"","sources":["../src/DebugExperience.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"DebugExperience.svelte.d.ts","sourceRoot":"","sources":["../src/DebugExperience.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AA4DjE,QAAA,MAAM,eAAe,0DAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
|
|
@@ -2,13 +2,20 @@ import { render } from '@testing-library/svelte';
|
|
|
2
2
|
import { describe, expect, it } from 'vitest';
|
|
3
3
|
import DebugExperience from './DebugExperience.svelte';
|
|
4
4
|
const emptyPlan = { nodes: [], viewports: [], fallbackViewportIndex: 0 };
|
|
5
|
-
function node(
|
|
5
|
+
function node(id, content = {}) {
|
|
6
6
|
return {
|
|
7
|
-
registration: {
|
|
7
|
+
registration: { kind: 'component', id },
|
|
8
8
|
props: { content, design: {}, designRaw: {} },
|
|
9
9
|
slots: {},
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
|
+
function templateNode(id) {
|
|
13
|
+
return {
|
|
14
|
+
registration: { kind: 'experienceTemplate', id },
|
|
15
|
+
props: { content: {}, design: {}, designRaw: {} },
|
|
16
|
+
slots: {},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
12
19
|
describe('DebugExperience.svelte', () => {
|
|
13
20
|
it('renders a collapsible details panel with the debug marker, collapsed by default', () => {
|
|
14
21
|
const { container } = render(DebugExperience, { props: { experience: emptyPlan } });
|
|
@@ -33,19 +40,25 @@ describe('DebugExperience.svelte', () => {
|
|
|
33
40
|
const { container } = render(DebugExperience, { props: { experience: one } });
|
|
34
41
|
expect(container.innerHTML).toContain('Experience debug — 1 top-level node');
|
|
35
42
|
});
|
|
36
|
-
it('names
|
|
43
|
+
it('names experienceTemplate nodes in the summary', () => {
|
|
37
44
|
const plan = {
|
|
38
45
|
viewports: [],
|
|
39
|
-
nodes: [],
|
|
40
|
-
experienceTemplate: {
|
|
41
|
-
experienceTemplateId: 'page',
|
|
42
|
-
props: { content: {}, design: {}, designRaw: {} },
|
|
43
|
-
},
|
|
46
|
+
nodes: [templateNode('page')],
|
|
44
47
|
fallbackViewportIndex: 0,
|
|
45
48
|
};
|
|
46
49
|
const { container } = render(DebugExperience, { props: { experience: plan } });
|
|
47
50
|
expect(container.innerHTML).toContain('experience template: page');
|
|
48
51
|
});
|
|
52
|
+
it('omits the experience-template summary for a composite experience', () => {
|
|
53
|
+
const plan = {
|
|
54
|
+
viewports: [],
|
|
55
|
+
nodes: [node('button'), node('text')],
|
|
56
|
+
fallbackViewportIndex: 0,
|
|
57
|
+
};
|
|
58
|
+
const { container } = render(DebugExperience, { props: { experience: plan } });
|
|
59
|
+
expect(container.innerHTML).toContain('Experience debug — 2 top-level nodes');
|
|
60
|
+
expect(container.innerHTML).not.toContain('experience template');
|
|
61
|
+
});
|
|
49
62
|
it('dumps the plan as pretty JSON', () => {
|
|
50
63
|
const plan = {
|
|
51
64
|
viewports: [],
|
|
@@ -53,7 +66,7 @@ describe('DebugExperience.svelte', () => {
|
|
|
53
66
|
fallbackViewportIndex: 0,
|
|
54
67
|
};
|
|
55
68
|
const { container } = render(DebugExperience, { props: { experience: plan } });
|
|
56
|
-
expect(container.innerHTML).toContain('
|
|
69
|
+
expect(container.innerHTML).toContain('registration');
|
|
57
70
|
expect(container.innerHTML).toContain('button');
|
|
58
71
|
expect(container.innerHTML).toContain('Go');
|
|
59
72
|
});
|
package/dist/NodeRenderer.svelte
CHANGED
|
@@ -2,18 +2,29 @@
|
|
|
2
2
|
* Single-node renderer. Splits out of NodesRenderer so the per-node
|
|
3
3
|
* `setContext` for the contentful payload happens during *that node's*
|
|
4
4
|
* component init (Svelte's setContext can only be called at top level).
|
|
5
|
+
*
|
|
6
|
+
* A node's `registration.kind` picks which half of the customer Config owns
|
|
7
|
+
* its implementation — `experienceTemplates` for a coded Experience Template,
|
|
8
|
+
* `components` otherwise. Everything else (slot props, merge precedence,
|
|
9
|
+
* design resolution) is identical across the two kinds.
|
|
5
10
|
-->
|
|
6
11
|
<script lang="ts">
|
|
7
12
|
import type { Snippet } from 'svelte';
|
|
8
13
|
|
|
9
14
|
import type { PortableRenderNode } from '@contentful/experiences-sdk-core';
|
|
10
15
|
|
|
11
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
setContentfulComponent,
|
|
18
|
+
setContentfulExperienceTemplate,
|
|
19
|
+
setResolvedDesign,
|
|
20
|
+
} from './context.js';
|
|
12
21
|
import { selectResolvedDesign } from './design-utils.js';
|
|
13
22
|
import type { RenderUnknown } from './component-props.js';
|
|
14
23
|
import {
|
|
15
24
|
normalizeComponentRegistration,
|
|
25
|
+
normalizeExperienceTemplateRegistration,
|
|
16
26
|
type ContentfulComponent,
|
|
27
|
+
type ContentfulExperienceTemplate,
|
|
17
28
|
type Config,
|
|
18
29
|
type RenderContext,
|
|
19
30
|
} from './types.js';
|
|
@@ -23,23 +34,49 @@
|
|
|
23
34
|
config: Config;
|
|
24
35
|
experience: RenderContext;
|
|
25
36
|
renderUnknown: RenderUnknown;
|
|
26
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Pre-rendered slot children keyed by slot name — one zero-arg Snippet per
|
|
39
|
+
* child node. Spread onto the customer component as named props, so a slot
|
|
40
|
+
* named `content` arrives as a `content` prop. `children` is not special.
|
|
41
|
+
*/
|
|
42
|
+
slotSnippets: Record<string, Snippet[]>;
|
|
27
43
|
}
|
|
28
44
|
|
|
29
|
-
let { node, config, experience, renderUnknown,
|
|
45
|
+
let { node, config, experience, renderUnknown, slotSnippets }: NodeRendererProps = $props();
|
|
46
|
+
|
|
47
|
+
const { kind, id } = node.registration;
|
|
48
|
+
const isExperienceTemplate = kind === 'experienceTemplate';
|
|
30
49
|
|
|
31
|
-
const entry = $derived(
|
|
32
|
-
|
|
50
|
+
const entry = $derived(
|
|
51
|
+
isExperienceTemplate ? config.experienceTemplates?.[id] : config.components[id]
|
|
52
|
+
);
|
|
53
|
+
const componentConfig = $derived.by(() => {
|
|
54
|
+
if (!entry) return null;
|
|
55
|
+
return isExperienceTemplate
|
|
56
|
+
? normalizeExperienceTemplateRegistration(entry)
|
|
57
|
+
: normalizeComponentRegistration(entry);
|
|
58
|
+
});
|
|
33
59
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
60
|
+
if (isExperienceTemplate) {
|
|
61
|
+
const contentful: ContentfulExperienceTemplate = {
|
|
62
|
+
experienceTemplateId: id,
|
|
63
|
+
nodeId: node.nodeId,
|
|
64
|
+
content: node.props.content,
|
|
65
|
+
design: node.props.designRaw,
|
|
66
|
+
resolved: node.props.resolved,
|
|
67
|
+
};
|
|
68
|
+
setContentfulExperienceTemplate(contentful);
|
|
69
|
+
} else {
|
|
70
|
+
const contentful: ContentfulComponent = {
|
|
71
|
+
componentId: id,
|
|
72
|
+
nodeId: node.nodeId,
|
|
73
|
+
content: node.props.content,
|
|
74
|
+
design: node.props.designRaw,
|
|
75
|
+
resolved: node.props.resolved,
|
|
76
|
+
slots: node.slots,
|
|
77
|
+
};
|
|
78
|
+
setContentfulComponent(contentful);
|
|
79
|
+
}
|
|
43
80
|
|
|
44
81
|
const tokenResolvedDesign = $derived.by(() => {
|
|
45
82
|
const { props, unresolved } = selectResolvedDesign(
|
|
@@ -51,7 +88,7 @@
|
|
|
51
88
|
);
|
|
52
89
|
if (unresolved.length && typeof console !== 'undefined') {
|
|
53
90
|
console.warn(
|
|
54
|
-
`[@contentful/experiences-svelte] resolveToken returned undefined for token id(s) on "${
|
|
91
|
+
`[@contentful/experiences-svelte] resolveToken returned undefined for token id(s) on ${kind} "${id}": ${unresolved.join(', ')}. getDesignValues() will omit those keys.`
|
|
55
92
|
);
|
|
56
93
|
}
|
|
57
94
|
return props;
|
|
@@ -59,7 +96,7 @@
|
|
|
59
96
|
|
|
60
97
|
setResolvedDesign(() => tokenResolvedDesign);
|
|
61
98
|
|
|
62
|
-
// Merge precedence (last wins): defaults < design < content < resolveData <
|
|
99
|
+
// Merge precedence (last wins): defaults < design < content < resolveData < slots.
|
|
63
100
|
const composed = $derived.by(() => {
|
|
64
101
|
if (!componentConfig) return null;
|
|
65
102
|
return {
|
|
@@ -67,18 +104,35 @@
|
|
|
67
104
|
...tokenResolvedDesign,
|
|
68
105
|
...node.props.content,
|
|
69
106
|
...node.props.resolved,
|
|
70
|
-
|
|
107
|
+
...slotSnippets,
|
|
71
108
|
};
|
|
72
109
|
});
|
|
110
|
+
|
|
111
|
+
// An unregistered Experience Template would blank the page if we swapped it
|
|
112
|
+
// for the missing-component box, so warn and render its slot children
|
|
113
|
+
// unwrapped — the content survives, the diagnostic names what's missing.
|
|
114
|
+
const orphanedSnippets = $derived.by(() => {
|
|
115
|
+
if (componentConfig || !isExperienceTemplate) return null;
|
|
116
|
+
if (typeof console !== 'undefined') {
|
|
117
|
+
console.warn(
|
|
118
|
+
`[@contentful/experiences-svelte] No experience template registered for id "${id}". Rendering its slot children without the experience template wrapper.`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return Object.values(slotSnippets).flat();
|
|
122
|
+
});
|
|
73
123
|
</script>
|
|
74
124
|
|
|
75
125
|
{#if componentConfig && composed}
|
|
76
126
|
{@const Cmp = componentConfig.component}
|
|
77
127
|
<Cmp {...composed} />
|
|
128
|
+
{:else if orphanedSnippets}
|
|
129
|
+
{#each orphanedSnippets as childSnippet, index (index)}
|
|
130
|
+
{@render childSnippet()}
|
|
131
|
+
{/each}
|
|
78
132
|
{:else}
|
|
79
133
|
{@const Unknown = renderUnknown}
|
|
80
134
|
<Unknown
|
|
81
|
-
componentId={
|
|
135
|
+
componentId={id}
|
|
82
136
|
nodeId={node.nodeId}
|
|
83
137
|
/>
|
|
84
138
|
{/if}
|
|
@@ -7,7 +7,12 @@ interface NodeRendererProps {
|
|
|
7
7
|
config: Config;
|
|
8
8
|
experience: RenderContext;
|
|
9
9
|
renderUnknown: RenderUnknown;
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Pre-rendered slot children keyed by slot name — one zero-arg Snippet per
|
|
12
|
+
* child node. Spread onto the customer component as named props, so a slot
|
|
13
|
+
* named `content` arrives as a `content` prop. `children` is not special.
|
|
14
|
+
*/
|
|
15
|
+
slotSnippets: Record<string, Snippet[]>;
|
|
11
16
|
}
|
|
12
17
|
declare const NodeRenderer: import("svelte").Component<NodeRendererProps, {}, "">;
|
|
13
18
|
type NodeRenderer = ReturnType<typeof NodeRenderer>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NodeRenderer.svelte.d.ts","sourceRoot":"","sources":["../src/NodeRenderer.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;
|
|
1
|
+
{"version":3,"file":"NodeRenderer.svelte.d.ts","sourceRoot":"","sources":["../src/NodeRenderer.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAQ3E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAKH,KAAK,MAAM,EACX,KAAK,aAAa,EACnB,MAAM,YAAY,CAAC;AAGpB,UAAU,iBAAiB;IACzB,IAAI,EAAE,kBAAkB,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,aAAa,CAAC;IAC1B,aAAa,EAAE,aAAa,CAAC;IAC7B;;;;OAIG;IACH,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;CACzC;AA2GH,QAAA,MAAM,YAAY,uDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
* Recursive renderer over PortableRenderNodes. Customer components receive
|
|
3
|
-
* the merged content / design / resolveData prop bag plus
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* the merged content / design / resolveData prop bag plus one prop per slot,
|
|
4
|
+
* each an ARRAY of Snippets — one zero-arg Snippet per pre-rendered child
|
|
5
|
+
* node. A slot named `content` arrives as `content?: Snippet[]`; the
|
|
6
|
+
* conventional default slot arrives as `children?: Snippet[]`. A component
|
|
6
7
|
* renders them all with an each-block + @render, or maps/filters/wraps the
|
|
7
|
-
* children individually. The
|
|
8
|
+
* children individually. The arrays mirror the React adapter's ReactNode[]
|
|
8
9
|
* slot contract; see the README for the consuming-a-slot example.
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* This applies to every node kind — a coded Experience Template declaring a
|
|
12
|
+
* `content` slot gets a `content` prop exactly as a component would. The raw
|
|
13
|
+
* per-slot payload nodes are also reachable via
|
|
14
|
+
* `getContentfulComponent().slots` for callers that want to render them
|
|
15
|
+
* themselves with the exported NodesRenderer.
|
|
15
16
|
*
|
|
16
17
|
* The Experience runtime context and the raw Contentful payload are read
|
|
17
18
|
* via `getExperience` / `getContentfulComponent`.
|
|
@@ -38,20 +39,42 @@
|
|
|
38
39
|
let { nodes, config, experience, renderUnknown }: NodesRendererProps = $props();
|
|
39
40
|
|
|
40
41
|
// Curry the parameterized `renderNode` snippet into a zero-arg Snippet bound
|
|
41
|
-
// to one node
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
42
|
+
// to one node: forward the anchor (the renderer, under SSR) and supply the
|
|
43
|
+
// node through a getter. This is the one spot that leans on Svelte's snippet
|
|
44
|
+
// calling convention; pinned by the slot-array tests in
|
|
45
|
+
// server-renderer.test.ts and nodes-renderer.ssr.test.ts.
|
|
45
46
|
function toChildSnippet(node: PortableRenderNode): Snippet {
|
|
46
47
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- programmatic snippet construction; see note above.
|
|
47
48
|
return ((anchor: unknown) => (renderNode as any)(anchor, () => node)) as Snippet;
|
|
48
49
|
}
|
|
50
|
+
|
|
51
|
+
// Svelte's two compiler outputs disagree on how snippet arguments arrive: the
|
|
52
|
+
// client build passes each one as a getter, the server build passes it by
|
|
53
|
+
// value. Compiler-generated `{@render}` call sites match whichever build they
|
|
54
|
+
// are in, but `toChildSnippet` writes its call by hand, so the snippet body
|
|
55
|
+
// accepts either shape and normalizes here.
|
|
56
|
+
type NodeArg = PortableRenderNode | (() => PortableRenderNode);
|
|
57
|
+
function unwrapNode(node: NodeArg): PortableRenderNode {
|
|
58
|
+
return typeof node === 'function' ? node() : node;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Build one Snippet[] per slot so every slot becomes a named prop. Snippets
|
|
62
|
+
// themselves are compile-time entities, but the *prop bag* is plain runtime
|
|
63
|
+
// data — so an unbounded set of payload-driven slot names is fine as long as
|
|
64
|
+
// each snippet comes from `toChildSnippet`.
|
|
65
|
+
function toSlotSnippets(node: PortableRenderNode): Record<string, Snippet[]> {
|
|
66
|
+
const slotSnippets: Record<string, Snippet[]> = {};
|
|
67
|
+
for (const [slotName, children] of Object.entries(node.slots)) {
|
|
68
|
+
slotSnippets[slotName] = children.map(toChildSnippet);
|
|
69
|
+
}
|
|
70
|
+
return slotSnippets;
|
|
71
|
+
}
|
|
49
72
|
</script>
|
|
50
73
|
|
|
51
|
-
{#snippet renderNode(
|
|
52
|
-
{@const
|
|
53
|
-
{@const
|
|
54
|
-
<NodeRenderer {node} {config} {experience} {renderUnknown} {
|
|
74
|
+
{#snippet renderNode(nodeArg: NodeArg)}
|
|
75
|
+
{@const node = unwrapNode(nodeArg)}
|
|
76
|
+
{@const slotSnippets = toSlotSnippets(node)}
|
|
77
|
+
<NodeRenderer {node} {config} {experience} {renderUnknown} {slotSnippets} />
|
|
55
78
|
{/snippet}
|
|
56
79
|
|
|
57
80
|
{#each nodes as node, index (node.nodeId ?? index)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NodesRenderer.svelte.d.ts","sourceRoot":"","sources":["../src/NodesRenderer.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAG3E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAGtD,UAAU,kBAAkB;IAC1B,KAAK,EAAE,kBAAkB,EAAE,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,aAAa,CAAC;IAC1B,aAAa,EAAE,aAAa,CAAC;CAC9B;
|
|
1
|
+
{"version":3,"file":"NodesRenderer.svelte.d.ts","sourceRoot":"","sources":["../src/NodesRenderer.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAG3E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAGtD,UAAU,kBAAkB;IAC1B,KAAK,EAAE,kBAAkB,EAAE,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,aAAa,CAAC;IAC1B,aAAa,EAAE,aAAa,CAAC;CAC9B;AA4DH,QAAA,MAAM,aAAa,wDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
import DebugExperience from './DebugExperience.svelte';
|
|
15
15
|
import MissingComponent from './MissingComponent.svelte';
|
|
16
16
|
import NodesRenderer from './NodesRenderer.svelte';
|
|
17
|
-
import WrapWithExperienceTemplate from './WrapWithExperienceTemplate.svelte';
|
|
18
17
|
import type { ServerExperienceRendererProps } from './component-props.js';
|
|
19
18
|
import { setExperience } from './context.js';
|
|
20
19
|
import type { RenderContext } from './types.js';
|
|
@@ -63,14 +62,10 @@
|
|
|
63
62
|
{#if debug}
|
|
64
63
|
<DebugExperience {experience} />
|
|
65
64
|
{/if}
|
|
66
|
-
<
|
|
67
|
-
{
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
{renderUnknown}
|
|
73
|
-
/>
|
|
74
|
-
{/snippet}
|
|
75
|
-
</WrapWithExperienceTemplate>
|
|
65
|
+
<NodesRenderer
|
|
66
|
+
nodes={experience.nodes}
|
|
67
|
+
{config}
|
|
68
|
+
experience={renderContext}
|
|
69
|
+
{renderUnknown}
|
|
70
|
+
/>
|
|
76
71
|
{/if}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ServerExperienceRenderer.svelte.d.ts","sourceRoot":"","sources":["../src/ServerExperienceRenderer.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ServerExperienceRenderer.svelte.d.ts","sourceRoot":"","sources":["../src/ServerExperienceRenderer.svelte.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,sBAAsB,CAAC;AAiE1E,QAAA,MAAM,wBAAwB,mEAAwC,CAAC;AACvE,KAAK,wBAAwB,GAAG,UAAU,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAC5E,eAAe,wBAAwB,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export { defineComponent, defineExperienceTemplate, normalizeComponentRegistrati
|
|
|
15
15
|
export type { ComponentConfig, Components, Config, ContentfulComponent, ContentfulExperienceTemplate, ExperienceTemplateConfig, ExperienceTemplateRegistration, ExperienceTemplates, Registration, RenderContext, ResolveToken, } from './types.js';
|
|
16
16
|
export { resolveExperience } from '@contentful/experiences-sdk-core';
|
|
17
17
|
export type { ResolverConfig, ResolveExperienceOptions } from '@contentful/experiences-sdk-core';
|
|
18
|
-
export type { ComponentNode, ComponentRef, DesignPropValue, DesignToken, ExperienceContext, ExperienceNode, ExperiencePayload, ExperienceSys, ExperienceTemplateNode, ExperienceTemplateRef, ManualDesignValue,
|
|
18
|
+
export type { ComponentNode, ComponentRef, DesignPropValue, DesignToken, ExperienceContext, ExperienceNode, ExperiencePayload, ExperienceSys, ExperienceTemplateNode, ExperienceTemplateRef, ManualDesignValue, PortableRegistration, PortableRenderNode, PortableRenderPlan, ResolveContext, ValuesByViewport, ViewportDef, } from '@contentful/experiences-sdk-core';
|
|
19
19
|
export { CSS_PROPERTIES, getValueForViewport, getViewportIndex, isCssProperty, resolveDesignProperties, toCssKey, toCssMediaQuery, } from '@contentful/experiences-design';
|
|
20
20
|
export { ContentfulViewDelivery, ContentfulViewDeliveryClient, DELIVERY_HOST, NotFoundError, PREVIEW_HOST, createClient, fetchExperience, } from '@contentful/experiences-client';
|
|
21
21
|
export type { ClientOptions, CreateClientOptions, ExperienceOptions, ResolveOptions, } from '@contentful/experiences-client';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,OAAO,IAAI,wBAAwB,EAAE,MAAM,mCAAmC,CAAC;AACxF,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AAClF,OAAO,EAAE,OAAO,IAAI,wBAAwB,EAAE,MAAM,mCAAmC,CAAC;AACxF,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,OAAO,IAAI,wBAAwB,EAAE,MAAM,mCAAmC,CAAC;AACxF,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AAClF,OAAO,EAAE,OAAO,IAAI,wBAAwB,EAAE,MAAM,mCAAmC,CAAC;AACxF,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAKtE,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAElE,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACpE,YAAY,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAG/E,OAAO,EACL,aAAa,EACb,sBAAsB,EACtB,+BAA+B,GAChC,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC1C,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAItD,YAAY,EACV,6BAA6B,EAC7B,6BAA6B,IAAI,uBAAuB,EACxD,oBAAoB,EACpB,qBAAqB,EACrB,aAAa,EACb,6BAA6B,GAC9B,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EACL,eAAe,EACf,wBAAwB,EACxB,8BAA8B,EAC9B,uCAAuC,GACxC,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,eAAe,EACf,UAAU,EACV,MAAM,EACN,mBAAmB,EACnB,4BAA4B,EAC5B,wBAAwB,EACxB,8BAA8B,EAC9B,mBAAmB,EACnB,YAAY,EACZ,aAAa,EACb,YAAY,GACb,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AACrE,YAAY,EAAE,cAAc,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAGjG,YAAY,EACV,aAAa,EACb,YAAY,EACZ,eAAe,EACf,WAAW,EACX,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,aAAa,EACb,sBAAsB,EACtB,qBAAqB,EACrB,iBAAiB,EACjB,oBAAoB,EACpB,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,WAAW,GACZ,MAAM,kCAAkC,CAAC;AAG1C,OAAO,EACL,cAAc,EACd,mBAAmB,EACnB,gBAAgB,EAChB,aAAa,EACb,uBAAuB,EACvB,QAAQ,EACR,eAAe,GAChB,MAAM,gCAAgC,CAAC;AAGxC,OAAO,EACL,sBAAsB,EACtB,4BAA4B,EAC5B,aAAa,EACb,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,eAAe,GAChB,MAAM,gCAAgC,CAAC;AACxC,YAAY,EACV,aAAa,EACb,mBAAmB,EACnB,iBAAiB,EACjB,cAAc,GACf,MAAM,gCAAgC,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,8 @@ export { default as ExperienceRenderer } from './ClientExperienceRenderer.svelte
|
|
|
17
17
|
export { default as ServerExperienceRenderer } from './ServerExperienceRenderer.svelte';
|
|
18
18
|
export { default as MissingComponent } from './MissingComponent.svelte';
|
|
19
19
|
export { default as DebugExperience } from './DebugExperience.svelte';
|
|
20
|
-
//
|
|
20
|
+
// Every slot already arrives as a same-named `Snippet[]` prop; this is exposed so
|
|
21
|
+
// advanced customers can render a slot's raw nodes themselves instead:
|
|
21
22
|
// e.g. `<NodesRenderer nodes={contentful.slots.header as PortableRenderNode[]} ... />`.
|
|
22
23
|
export { default as NodesRenderer } from './NodesRenderer.svelte';
|
|
23
24
|
export { useActiveViewport } from './use-active-viewport.svelte.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nodes-renderer.ssr.test.d.ts","sourceRoot":"","sources":["../src/nodes-renderer.ssr.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* True SSR coverage: renders through `svelte/server` so Svelte's *server*
|
|
3
|
+
* compiler output is exercised. Every other test in this package renders via
|
|
4
|
+
* `@testing-library/svelte`, i.e. the client build.
|
|
5
|
+
*
|
|
6
|
+
* The two builds differ in how a compiled snippet receives its arguments — by
|
|
7
|
+
* getter on the client, by value on the server — and `NodesRenderer` builds one
|
|
8
|
+
* snippet call by hand (`toChildSnippet`). These tests pin that nested nodes
|
|
9
|
+
* render under SSR for both node kinds; without the normalization in
|
|
10
|
+
* `NodesRenderer`, every child node throws.
|
|
11
|
+
*
|
|
12
|
+
* Runs under vitest.ssr.config.ts (environment: node).
|
|
13
|
+
*/
|
|
14
|
+
import { describe, expect, it } from 'vitest';
|
|
15
|
+
import { render } from 'svelte/server';
|
|
16
|
+
import { resolveExperience } from '@contentful/experiences-sdk-core';
|
|
17
|
+
import ServerExperienceRenderer from './ServerExperienceRenderer.svelte';
|
|
18
|
+
import ButtonFixture from './test-fixtures/ButtonFixture.svelte';
|
|
19
|
+
import ContainerFixture from './test-fixtures/ContainerFixture.svelte';
|
|
20
|
+
import ExperienceTemplateFixture from './test-fixtures/ExperienceTemplateFixture.svelte';
|
|
21
|
+
const VIEWPORTS = [{ id: 'desktop', query: '*', displayName: 'Desktop', previewSize: '100%' }];
|
|
22
|
+
const componentNode = (typeId, rest = {}) => ({
|
|
23
|
+
component: {
|
|
24
|
+
sys: {
|
|
25
|
+
type: 'ResourceLink',
|
|
26
|
+
linkType: 'Contentful:Component',
|
|
27
|
+
urn: `crn:contentful:::experience:spaces/$self/environments/$self/components/${typeId}`,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
...rest,
|
|
31
|
+
});
|
|
32
|
+
const experienceTemplateNode = (typeId, rest = {}) => ({
|
|
33
|
+
experienceTemplate: {
|
|
34
|
+
sys: {
|
|
35
|
+
type: 'ResourceLink',
|
|
36
|
+
linkType: 'Contentful:ExperienceTemplate',
|
|
37
|
+
urn: `crn:contentful:::experience:spaces/$self/environments/$self/experienceTemplates/${typeId}`,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
...rest,
|
|
41
|
+
});
|
|
42
|
+
const button = (label) => componentNode('contentful-button', {
|
|
43
|
+
id: `btn-${label}`,
|
|
44
|
+
contentProperties: { label },
|
|
45
|
+
designProperties: {},
|
|
46
|
+
slots: {},
|
|
47
|
+
});
|
|
48
|
+
const config = {
|
|
49
|
+
components: {
|
|
50
|
+
'contentful-container': ContainerFixture,
|
|
51
|
+
'contentful-button': ButtonFixture,
|
|
52
|
+
},
|
|
53
|
+
experienceTemplates: { page: ExperienceTemplateFixture },
|
|
54
|
+
};
|
|
55
|
+
const renderPlan = async (payload) => {
|
|
56
|
+
const plan = await resolveExperience(payload, config);
|
|
57
|
+
return render(ServerExperienceRenderer, {
|
|
58
|
+
props: { experience: plan, config },
|
|
59
|
+
}).body;
|
|
60
|
+
};
|
|
61
|
+
describe('NodesRenderer — SSR (server-compiled output)', () => {
|
|
62
|
+
it('renders a component node’s slot children', async () => {
|
|
63
|
+
const html = await renderPlan({
|
|
64
|
+
viewports: VIEWPORTS,
|
|
65
|
+
nodes: [
|
|
66
|
+
componentNode('contentful-container', {
|
|
67
|
+
id: 'container',
|
|
68
|
+
contentProperties: {},
|
|
69
|
+
designProperties: {},
|
|
70
|
+
slots: { children: [button('Get started')] },
|
|
71
|
+
}),
|
|
72
|
+
],
|
|
73
|
+
});
|
|
74
|
+
expect(html).toContain('Get started');
|
|
75
|
+
expect(html.indexOf('<div')).toBeLessThan(html.indexOf('Get started'));
|
|
76
|
+
});
|
|
77
|
+
it('renders a coded Experience Template node’s named `content` slot', async () => {
|
|
78
|
+
const html = await renderPlan({
|
|
79
|
+
viewports: VIEWPORTS,
|
|
80
|
+
nodes: [
|
|
81
|
+
experienceTemplateNode('page', {
|
|
82
|
+
id: 'tpl',
|
|
83
|
+
contentProperties: { title: 'Coded page' },
|
|
84
|
+
designProperties: {},
|
|
85
|
+
slots: { content: [button('Read the blog')] },
|
|
86
|
+
}),
|
|
87
|
+
],
|
|
88
|
+
});
|
|
89
|
+
expect(html).toContain('data-experience-template="page"');
|
|
90
|
+
expect(html).toContain('data-title="Coded page"');
|
|
91
|
+
expect(html).toContain('Read the blog');
|
|
92
|
+
// The button is inside the template chrome, not a sibling of it.
|
|
93
|
+
expect(html.split('<main')[1]).toContain('Read the blog');
|
|
94
|
+
});
|
|
95
|
+
it('renders deeply nested slot children', async () => {
|
|
96
|
+
const html = await renderPlan({
|
|
97
|
+
viewports: VIEWPORTS,
|
|
98
|
+
nodes: [
|
|
99
|
+
experienceTemplateNode('page', {
|
|
100
|
+
id: 'tpl',
|
|
101
|
+
contentProperties: {},
|
|
102
|
+
designProperties: {},
|
|
103
|
+
slots: {
|
|
104
|
+
content: [
|
|
105
|
+
componentNode('contentful-container', {
|
|
106
|
+
id: 'container',
|
|
107
|
+
contentProperties: {},
|
|
108
|
+
designProperties: {},
|
|
109
|
+
slots: { children: [button('Deep')] },
|
|
110
|
+
}),
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
}),
|
|
114
|
+
],
|
|
115
|
+
});
|
|
116
|
+
expect(html).toContain('Deep');
|
|
117
|
+
});
|
|
118
|
+
it('renders a composite experience unwrapped', async () => {
|
|
119
|
+
const html = await renderPlan({
|
|
120
|
+
viewports: VIEWPORTS,
|
|
121
|
+
nodes: [button('Alone')],
|
|
122
|
+
});
|
|
123
|
+
expect(html).toContain('Alone');
|
|
124
|
+
expect(html).not.toContain('<main');
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -35,6 +35,38 @@ function componentNode(typeId, rest = {}) {
|
|
|
35
35
|
...rest,
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* A coded Experience Template is an ordinary top-level node — the only
|
|
40
|
+
* difference from `componentNode` is which registry its id resolves against.
|
|
41
|
+
*/
|
|
42
|
+
function experienceTemplateNode(typeId, rest = {}) {
|
|
43
|
+
return {
|
|
44
|
+
experienceTemplate: {
|
|
45
|
+
sys: {
|
|
46
|
+
type: 'ResourceLink',
|
|
47
|
+
linkType: 'Contentful:ExperienceTemplate',
|
|
48
|
+
urn: `crn:contentful:::experience:spaces/$self/environments/$self/experienceTemplates/${typeId}`,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
...rest,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* `sys.experienceTemplate` rides along on every Experience — coded and
|
|
56
|
+
* composite alike — so it must never influence rendering. Tests attach it to
|
|
57
|
+
* prove it is ignored.
|
|
58
|
+
*/
|
|
59
|
+
function sysWithExperienceTemplate(typeId) {
|
|
60
|
+
return {
|
|
61
|
+
experienceTemplate: {
|
|
62
|
+
sys: {
|
|
63
|
+
type: 'ResourceLink',
|
|
64
|
+
linkType: 'Contentful:ExperienceTemplate',
|
|
65
|
+
urn: `crn:contentful:::experience:spaces/$self/environments/$self/experienceTemplates/${typeId}`,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
38
70
|
beforeEach(() => {
|
|
39
71
|
captureSink.splice(0);
|
|
40
72
|
});
|
|
@@ -158,13 +190,13 @@ describe('ServerExperienceRenderer', () => {
|
|
|
158
190
|
nodes: [
|
|
159
191
|
{
|
|
160
192
|
nodeId: 'root',
|
|
161
|
-
registration: {
|
|
193
|
+
registration: { kind: 'component', id: 'contentful-container' },
|
|
162
194
|
props: { content: {}, design: {}, designRaw: {} },
|
|
163
195
|
slots: {
|
|
164
196
|
children: [
|
|
165
197
|
{
|
|
166
198
|
nodeId: 'ghost',
|
|
167
|
-
registration: {
|
|
199
|
+
registration: { kind: 'component', id: 'NotRegistered' },
|
|
168
200
|
props: { content: {}, design: {}, designRaw: {} },
|
|
169
201
|
slots: {},
|
|
170
202
|
},
|
|
@@ -247,7 +279,7 @@ describe('ServerExperienceRenderer', () => {
|
|
|
247
279
|
nodes: [
|
|
248
280
|
{
|
|
249
281
|
nodeId: 'r',
|
|
250
|
-
registration: {
|
|
282
|
+
registration: { kind: 'component', id: 'item' },
|
|
251
283
|
props: {
|
|
252
284
|
content: { value: 'fromContent' },
|
|
253
285
|
design: {},
|
|
@@ -263,25 +295,26 @@ describe('ServerExperienceRenderer', () => {
|
|
|
263
295
|
});
|
|
264
296
|
expect(container.innerHTML).toContain('data-value="fromResolveData"');
|
|
265
297
|
});
|
|
266
|
-
it('
|
|
298
|
+
it('renders an experienceTemplate node from the experienceTemplates registry', async () => {
|
|
267
299
|
const tplConfig = {
|
|
268
300
|
components: { item: PrecedenceFixture },
|
|
269
301
|
experienceTemplates: {
|
|
270
302
|
page: { component: ExperienceTemplateFixture, defaults: { title: 'Default Title' } },
|
|
271
303
|
},
|
|
272
304
|
};
|
|
305
|
+
// The payload's `content` slot must arrive as a named `content` Snippet[]
|
|
306
|
+
// prop — the fixture renders it, so the child only appears if that holds.
|
|
273
307
|
const tplPayload = {
|
|
274
|
-
sys:
|
|
275
|
-
experienceTemplate: {
|
|
276
|
-
sys: {
|
|
277
|
-
type: 'ResourceLink',
|
|
278
|
-
linkType: 'Contentful:ExperienceTemplate',
|
|
279
|
-
urn: 'crn:contentful:::experience:spaces/$self/environments/$self/experienceTemplates/page',
|
|
280
|
-
},
|
|
281
|
-
},
|
|
282
|
-
},
|
|
308
|
+
sys: sysWithExperienceTemplate('page'),
|
|
283
309
|
viewports: VIEWPORTS,
|
|
284
|
-
nodes: [
|
|
310
|
+
nodes: [
|
|
311
|
+
experienceTemplateNode('page', {
|
|
312
|
+
id: 'tpl',
|
|
313
|
+
slots: {
|
|
314
|
+
content: [componentNode('item', { id: 'i', contentProperties: { value: 'inside' } })],
|
|
315
|
+
},
|
|
316
|
+
}),
|
|
317
|
+
],
|
|
285
318
|
};
|
|
286
319
|
const plan = await resolveExperience(tplPayload, tplConfig);
|
|
287
320
|
const { container } = render(ServerExperienceRenderer, {
|
|
@@ -291,26 +324,52 @@ describe('ServerExperienceRenderer', () => {
|
|
|
291
324
|
expect(container.innerHTML).toContain('data-title="Default Title"');
|
|
292
325
|
expect(container.innerHTML).toContain('inside');
|
|
293
326
|
});
|
|
294
|
-
it('renders
|
|
327
|
+
it('renders a composite experience unwrapped — sys.experienceTemplate is ignored', async () => {
|
|
328
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
329
|
+
// `hero` is registered as a template, but the nodes are plain components,
|
|
330
|
+
// so nothing may wrap them.
|
|
331
|
+
const cfg = {
|
|
332
|
+
components: { item: PrecedenceFixture },
|
|
333
|
+
experienceTemplates: { hero: ExperienceTemplateFixture },
|
|
334
|
+
};
|
|
335
|
+
const plan = await resolveExperience({
|
|
336
|
+
sys: sysWithExperienceTemplate('hero'),
|
|
337
|
+
viewports: VIEWPORTS,
|
|
338
|
+
nodes: [
|
|
339
|
+
componentNode('item', { id: 'a', contentProperties: { value: 'one' } }),
|
|
340
|
+
componentNode('item', { id: 'b', contentProperties: { value: 'two' } }),
|
|
341
|
+
],
|
|
342
|
+
}, cfg);
|
|
343
|
+
const { container } = render(ServerExperienceRenderer, {
|
|
344
|
+
props: { experience: plan, config: cfg },
|
|
345
|
+
});
|
|
346
|
+
expect(container.innerHTML).toContain('data-value="one"');
|
|
347
|
+
expect(container.innerHTML).toContain('data-value="two"');
|
|
348
|
+
expect(container.innerHTML).not.toContain('data-experience-template');
|
|
349
|
+
expect(warn).not.toHaveBeenCalled();
|
|
350
|
+
warn.mockRestore();
|
|
351
|
+
});
|
|
352
|
+
it('renders slot children unwrapped + warns when the experienceTemplate is not registered', async () => {
|
|
295
353
|
const warn = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
296
354
|
const cfg = { components: { item: PrecedenceFixture } };
|
|
297
355
|
const tplPayload = {
|
|
298
|
-
sys: {
|
|
299
|
-
experienceTemplate: {
|
|
300
|
-
sys: {
|
|
301
|
-
type: 'ResourceLink',
|
|
302
|
-
linkType: 'Contentful:ExperienceTemplate',
|
|
303
|
-
urn: 'crn:contentful:::experience:spaces/$self/environments/$self/experienceTemplates/missing-experienceTemplate',
|
|
304
|
-
},
|
|
305
|
-
},
|
|
306
|
-
},
|
|
307
356
|
viewports: VIEWPORTS,
|
|
308
|
-
nodes: [
|
|
357
|
+
nodes: [
|
|
358
|
+
experienceTemplateNode('missing-experienceTemplate', {
|
|
359
|
+
id: 'tpl',
|
|
360
|
+
slots: {
|
|
361
|
+
content: [
|
|
362
|
+
componentNode('item', { id: 'i', contentProperties: { value: 'unwrapped' } }),
|
|
363
|
+
],
|
|
364
|
+
},
|
|
365
|
+
}),
|
|
366
|
+
],
|
|
309
367
|
};
|
|
310
368
|
const plan = await resolveExperience(tplPayload, cfg);
|
|
311
369
|
const { container } = render(ServerExperienceRenderer, {
|
|
312
370
|
props: { experience: plan, config: cfg },
|
|
313
371
|
});
|
|
372
|
+
// The subtree survives — an unregistered template must not blank the page.
|
|
314
373
|
expect(container.innerHTML).toContain('data-value="unwrapped"');
|
|
315
374
|
expect(container.innerHTML).not.toContain('data-experience-template');
|
|
316
375
|
expect(warn).toHaveBeenCalledWith(expect.stringContaining('missing-experienceTemplate'));
|
|
@@ -407,8 +466,9 @@ describe('ServerExperienceRenderer — bare-component registrations', () => {
|
|
|
407
466
|
expect(keys).toContain('text');
|
|
408
467
|
expect(keys).not.toContain('experience');
|
|
409
468
|
expect(keys).not.toContain('contentful');
|
|
410
|
-
//
|
|
411
|
-
|
|
469
|
+
// Slot props exist only for slots the payload actually carries — `children`
|
|
470
|
+
// is just a slot name, so a node without slots gets no `children` prop.
|
|
471
|
+
expect(keys).not.toContain('children');
|
|
412
472
|
});
|
|
413
473
|
});
|
|
414
474
|
describe('ServerExperienceRenderer — getContentfulComponent()', () => {
|
|
@@ -518,32 +578,28 @@ describe('ServerExperienceRenderer — resolveToken', () => {
|
|
|
518
578
|
// unchanged, so the customer can inspect it or resolve it themselves.
|
|
519
579
|
expect(container.innerHTML).toContain('data-bg="[object Object]"');
|
|
520
580
|
});
|
|
521
|
-
it('runs on
|
|
581
|
+
it('runs on an experienceTemplate node design props too', async () => {
|
|
522
582
|
const tplConfig = {
|
|
523
583
|
components: { item: PrecedenceFixture },
|
|
524
584
|
experienceTemplates: { page: ExperienceTemplateFixture },
|
|
525
585
|
resolveToken: (ref) => (ref.value === 'brand/canvas' ? '#111827' : undefined),
|
|
526
586
|
};
|
|
527
587
|
const tplPayload = {
|
|
528
|
-
sys:
|
|
529
|
-
experienceTemplate: {
|
|
530
|
-
sys: {
|
|
531
|
-
type: 'ResourceLink',
|
|
532
|
-
linkType: 'Contentful:ExperienceTemplate',
|
|
533
|
-
urn: 'crn:contentful:::experience:spaces/$self/environments/$self/experienceTemplates/page',
|
|
534
|
-
},
|
|
535
|
-
},
|
|
536
|
-
},
|
|
588
|
+
sys: sysWithExperienceTemplate('page'),
|
|
537
589
|
viewports: VIEWPORTS,
|
|
538
|
-
nodes: [
|
|
590
|
+
nodes: [
|
|
591
|
+
experienceTemplateNode('page', {
|
|
592
|
+
id: 'tpl',
|
|
593
|
+
designProperties: { cfBackground: dt('brand/canvas') },
|
|
594
|
+
slots: {
|
|
595
|
+
content: [componentNode('item', { id: 'i', contentProperties: { value: 'ok' } })],
|
|
596
|
+
},
|
|
597
|
+
}),
|
|
598
|
+
],
|
|
539
599
|
};
|
|
540
600
|
const plan = await resolveExperience(tplPayload, tplConfig);
|
|
541
|
-
//
|
|
542
|
-
//
|
|
543
|
-
// experienceTemplate path. Render at a non-fallback viewport (mobile ≠
|
|
544
|
-
// fallback index 0) so the adapter recomputes from the seeded raw design
|
|
545
|
-
// and runs resolveToken.
|
|
546
|
-
plan.experienceTemplate.props.designRaw = { cfBackground: dt('brand/canvas') };
|
|
601
|
+
// Render at a non-fallback viewport (mobile ≠ fallback index 0) so the
|
|
602
|
+
// adapter recomputes from the raw design and runs resolveToken itself.
|
|
547
603
|
const { container } = render(ServerExperienceRenderer, {
|
|
548
604
|
props: { experience: plan, config: tplConfig, initialViewportId: 'mobile' },
|
|
549
605
|
});
|
|
@@ -3,16 +3,21 @@
|
|
|
3
3
|
|
|
4
4
|
import { getDesignValues } from '../get-design-values.js';
|
|
5
5
|
|
|
6
|
+
// A coded Experience Template's slots arrive as named `Snippet[]` props, so
|
|
7
|
+
// the payload's `content` slot lands on a `content` prop — no `children`
|
|
8
|
+
// special case.
|
|
6
9
|
let {
|
|
7
10
|
title,
|
|
8
|
-
|
|
11
|
+
content,
|
|
9
12
|
}: {
|
|
10
13
|
title?: string;
|
|
11
|
-
|
|
14
|
+
content?: Snippet[];
|
|
12
15
|
} = $props();
|
|
13
16
|
const design = $derived(getDesignValues());
|
|
14
17
|
</script>
|
|
15
18
|
|
|
16
19
|
<main data-experience-template="page" data-title={title} data-bg={design.cfBackground as string}>
|
|
17
|
-
{
|
|
20
|
+
{#each content ?? [] as childSnippet, index (index)}
|
|
21
|
+
{@render childSnippet()}
|
|
22
|
+
{/each}
|
|
18
23
|
</main>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
type $$ComponentProps = {
|
|
3
3
|
title?: string;
|
|
4
|
-
|
|
4
|
+
content?: Snippet[];
|
|
5
5
|
};
|
|
6
6
|
declare const ExperienceTemplateFixture: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
7
|
type ExperienceTemplateFixture = ReturnType<typeof ExperienceTemplateFixture>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExperienceTemplateFixture.svelte.d.ts","sourceRoot":"","sources":["../../src/test-fixtures/ExperienceTemplateFixture.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAIrC,KAAK,gBAAgB,GAAI;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,
|
|
1
|
+
{"version":3,"file":"ExperienceTemplateFixture.svelte.d.ts","sourceRoot":"","sources":["../../src/test-fixtures/ExperienceTemplateFixture.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAIrC,KAAK,gBAAgB,GAAI;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC;CACrB,CAAC;AAwBJ,QAAA,MAAM,yBAAyB,sDAAwC,CAAC;AACxE,KAAK,yBAAyB,GAAG,UAAU,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAC9E,eAAe,yBAAyB,CAAC"}
|
package/dist/types.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export type { ResolveContext, ResolveToken };
|
|
|
7
7
|
* node. Useful for custom design-property resolution outside the SDK's
|
|
8
8
|
* cascade, branching by `componentId` in a generic wrapper, keying
|
|
9
9
|
* analytics on `nodeId`, rendering a raw-payload panel in preview, or
|
|
10
|
-
* rendering
|
|
10
|
+
* re-rendering a slot's nodes yourself through the exported `<NodesRenderer />`.
|
|
11
11
|
*
|
|
12
12
|
* Design properties stay in their **raw discriminated form** here (the same
|
|
13
13
|
* shape `ctx.design` carries inside `resolveData`). The viewport-cascaded,
|
|
@@ -20,20 +20,21 @@ export interface ContentfulComponent {
|
|
|
20
20
|
design: Record<string, DesignPropValue>;
|
|
21
21
|
resolved?: Record<string, unknown>;
|
|
22
22
|
/**
|
|
23
|
-
* Raw per-slot node arrays from the payload.
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* `<NodesRenderer nodes={...} />`.
|
|
23
|
+
* Raw per-slot node arrays from the payload. Every slot is also rendered
|
|
24
|
+
* automatically and passed as a same-named `Snippet[]` prop (`children` is
|
|
25
|
+
* not special); these raw nodes are here for callers that want to render a
|
|
26
|
+
* slot themselves with `<NodesRenderer nodes={...} />`.
|
|
27
27
|
*/
|
|
28
28
|
slots: Record<string, unknown>;
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
|
-
* Same shape as `ContentfulComponent`, but for
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* Same shape as `ContentfulComponent`, but for an Experience Template node.
|
|
32
|
+
* Exposed via `getContentfulExperienceTemplate()` inside an Experience
|
|
33
|
+
* Template's component tree.
|
|
34
34
|
*/
|
|
35
35
|
export interface ContentfulExperienceTemplate {
|
|
36
36
|
experienceTemplateId: string;
|
|
37
|
+
nodeId?: string;
|
|
37
38
|
content: Record<string, unknown>;
|
|
38
39
|
design: Record<string, DesignPropValue>;
|
|
39
40
|
resolved?: Record<string, unknown>;
|
|
@@ -56,8 +57,8 @@ export interface RenderContext extends ExperienceContext {
|
|
|
56
57
|
}
|
|
57
58
|
/**
|
|
58
59
|
* Customer-supplied configuration for a single component type. The `component`
|
|
59
|
-
* receives the merged props (design + content + resolveData + `
|
|
60
|
-
*
|
|
60
|
+
* receives the merged props (design + content + resolveData + one `Snippet[]`
|
|
61
|
+
* prop per slot); resolved design values auto-fill matching props and are also
|
|
61
62
|
* readable via `getDesignValues()`. Runtime context and raw payload come from
|
|
62
63
|
* `getExperience()` / `getContentfulComponent()`.
|
|
63
64
|
*/
|
|
@@ -76,6 +77,13 @@ export interface ComponentConfig<Props extends object = Record<string, unknown>>
|
|
|
76
77
|
* card: defineComponent<CardProps>({ component: Card, resolveData: ... }),
|
|
77
78
|
*/
|
|
78
79
|
export type Registration<Props extends object = Record<string, unknown>> = Component<any> | ComponentConfig<Props>;
|
|
80
|
+
/**
|
|
81
|
+
* Customer-supplied configuration for a coded Experience Template. Identical in
|
|
82
|
+
* shape and behavior to `ComponentConfig` — an Experience Template is just a
|
|
83
|
+
* node whose implementation lives in the `experienceTemplates` registry. Its
|
|
84
|
+
* slots arrive as named `Snippet[]` props (a `content` slot becomes a `content`
|
|
85
|
+
* prop), so there is no `children` special case.
|
|
86
|
+
*/
|
|
79
87
|
export interface ExperienceTemplateConfig<Props extends object = Record<string, unknown>> {
|
|
80
88
|
defaults?: Partial<Props>;
|
|
81
89
|
resolveData?: (ctx: ResolveContext) => Partial<Props> | Promise<Partial<Props>>;
|
|
@@ -84,7 +92,17 @@ export interface ExperienceTemplateConfig<Props extends object = Record<string,
|
|
|
84
92
|
export type ExperienceTemplateRegistration<Props extends object = Record<string, unknown>> = Component<any> | ExperienceTemplateConfig<Props>;
|
|
85
93
|
export declare function defineExperienceTemplate<Props extends object = Record<string, unknown>>(config: ExperienceTemplateConfig<Props>): ExperienceTemplateConfig<Props>;
|
|
86
94
|
export declare function defineComponent<Props extends object = Record<string, unknown>>(config: ComponentConfig<Props>): ComponentConfig<Props>;
|
|
95
|
+
/**
|
|
96
|
+
* Component registry — keyed by `componentId` (last slash-segment of
|
|
97
|
+
* `component.sys.urn`).
|
|
98
|
+
*/
|
|
87
99
|
export type Components = Record<string, Registration<any>>;
|
|
100
|
+
/**
|
|
101
|
+
* Experience Template registry — keyed by `experienceTemplateId` (last
|
|
102
|
+
* slash-segment of an Experience Template node's own
|
|
103
|
+
* `experienceTemplate.sys.urn`). Not read from `payload.sys`; templates are
|
|
104
|
+
* ordinary nodes.
|
|
105
|
+
*/
|
|
88
106
|
export type ExperienceTemplates = Record<string, ExperienceTemplateRegistration<any>>;
|
|
89
107
|
export interface Config {
|
|
90
108
|
components: Components;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAExC,OAAO,KAAK,EACV,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,YAAY,EACZ,WAAW,EACZ,MAAM,kCAAkC,CAAC;AAE1C,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC;AAE7C;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC;;;;;OAKG;IACH,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED;;;;GAIG;AACH,MAAM,WAAW,4BAA4B;IAC3C,oBAAoB,EAAE,MAAM,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAc,SAAQ,iBAAiB;IACtD,cAAc,EAAE,WAAW,CAAC;IAC5B,mBAAmB,EAAE,MAAM,CAAC;IAC5B;;;;;OAKG;IACH,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC7E,QAAQ,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IAC1B,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;IAEhF,SAAS,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC;CAC3B;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,YAAY,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAErE,SAAS,CAAC,GAAG,CAAC,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;AAE1C;;;;;;GAMG;AACH,MAAM,WAAW,wBAAwB,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACtF,QAAQ,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IAC1B,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;IAEhF,SAAS,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC;CAC3B;AAED,MAAM,MAAM,8BAA8B,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAEvF,SAAS,CAAC,GAAG,CAAC,GAAG,wBAAwB,CAAC,KAAK,CAAC,CAAC;AAEnD,wBAAgB,wBAAwB,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACrF,MAAM,EAAE,wBAAwB,CAAC,KAAK,CAAC,GACtC,wBAAwB,CAAC,KAAK,CAAC,CAEjC;AAED,wBAAgB,eAAe,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5E,MAAM,EAAE,eAAe,CAAC,KAAK,CAAC,GAC7B,eAAe,CAAC,KAAK,CAAC,CAExB;AAED;;;GAGG;AAEH,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC;AAE3D;;;;;GAKG;AAEH,MAAM,MAAM,mBAAmB,GAAG,MAAM,CAAC,MAAM,EAAE,8BAA8B,CAAC,GAAG,CAAC,CAAC,CAAC;AAEtF,MAAM,WAAW,MAAM;IACrB,UAAU,EAAE,UAAU,CAAC;IACvB,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;IAC1C;;;;OAIG;IACH,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B;AAED;;;;;GAKG;AACH,wBAAgB,8BAA8B,CAAC,CAAC,SAAS,MAAM,EAC7D,GAAG,EAAE,YAAY,CAAC,CAAC,CAAC,GACnB,eAAe,CAAC,CAAC,CAAC,CAMpB;AAED,wBAAgB,uCAAuC,CAAC,CAAC,SAAS,MAAM,EACtE,GAAG,EAAE,8BAA8B,CAAC,CAAC,CAAC,GACrC,wBAAwB,CAAC,CAAC,CAAC,CAM7B"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contentful/experiences-svelte",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.6",
|
|
4
4
|
"description": "Svelte adapter for Contentful Experiences — Server and Client renderers over a PortableRenderPlan",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -31,13 +31,13 @@
|
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
33
|
"build": "svelte-package -i src -o dist && publint",
|
|
34
|
-
"test": "vitest run",
|
|
34
|
+
"test": "vitest run && vitest run -c vitest.ssr.config.ts",
|
|
35
35
|
"check": "svelte-check --tsconfig ./tsconfig.json"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@contentful/experiences-client": "0.3.
|
|
39
|
-
"@contentful/experiences-sdk-core": "0.7.
|
|
40
|
-
"@contentful/experiences-design": "0.7.
|
|
38
|
+
"@contentful/experiences-client": "0.3.5",
|
|
39
|
+
"@contentful/experiences-sdk-core": "0.7.5",
|
|
40
|
+
"@contentful/experiences-design": "0.7.5"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"svelte": "^5.55.7"
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
* Wraps the rendered Experience nodes with the page-level Experience Template when the
|
|
3
|
-
* plan carries one and the customer registered an Experience Template config under that
|
|
4
|
-
* id. When the Experience Template is referenced but unregistered, warns once and
|
|
5
|
-
* renders children unwrapped — graceful degradation matches the
|
|
6
|
-
* unknown-component fallback story.
|
|
7
|
-
-->
|
|
8
|
-
<script lang="ts">
|
|
9
|
-
import type { Snippet } from 'svelte';
|
|
10
|
-
|
|
11
|
-
import type { PortableExperienceTemplate } from '@contentful/experiences-sdk-core';
|
|
12
|
-
|
|
13
|
-
import { setContentfulExperienceTemplate, setResolvedDesign } from './context.js';
|
|
14
|
-
import { selectResolvedDesign } from './design-utils.js';
|
|
15
|
-
import {
|
|
16
|
-
normalizeExperienceTemplateRegistration,
|
|
17
|
-
type ContentfulExperienceTemplate,
|
|
18
|
-
type Config,
|
|
19
|
-
type RenderContext,
|
|
20
|
-
} from './types.js';
|
|
21
|
-
|
|
22
|
-
interface WrapWithExperienceTemplateProps {
|
|
23
|
-
experienceTemplate: PortableExperienceTemplate | undefined;
|
|
24
|
-
config: Config;
|
|
25
|
-
experience: RenderContext;
|
|
26
|
-
children: Snippet;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
let { experienceTemplate, config, experience, children }: WrapWithExperienceTemplateProps = $props();
|
|
30
|
-
|
|
31
|
-
// setContext must run during synchronous component init — call once with
|
|
32
|
-
// the initial value (Experience Templates do not toggle on/off mid-mount).
|
|
33
|
-
if (experienceTemplate) {
|
|
34
|
-
setContentfulExperienceTemplate({
|
|
35
|
-
experienceTemplateId: experienceTemplate.experienceTemplateId,
|
|
36
|
-
content: experienceTemplate.props.content,
|
|
37
|
-
design: experienceTemplate.props.designRaw,
|
|
38
|
-
resolved: experienceTemplate.props.resolved,
|
|
39
|
-
} satisfies ContentfulExperienceTemplate);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const entry = $derived(experienceTemplate ? config.experienceTemplates?.[experienceTemplate.experienceTemplateId] : undefined);
|
|
43
|
-
const experienceTemplateConfig = $derived(entry ? normalizeExperienceTemplateRegistration(entry) : null);
|
|
44
|
-
|
|
45
|
-
const tokenResolvedDesign = $derived.by(() => {
|
|
46
|
-
if (!experienceTemplate) return {};
|
|
47
|
-
const { props, unresolved } = selectResolvedDesign(
|
|
48
|
-
experienceTemplate.props,
|
|
49
|
-
experience.viewports,
|
|
50
|
-
experience.activeViewportIndex,
|
|
51
|
-
experience.fallbackViewportIndex,
|
|
52
|
-
config.resolveToken
|
|
53
|
-
);
|
|
54
|
-
if (unresolved.length && typeof console !== 'undefined') {
|
|
55
|
-
console.warn(
|
|
56
|
-
`[@contentful/experiences-svelte] resolveToken returned undefined for token id(s) on experience template "${experienceTemplate.experienceTemplateId}": ${unresolved.join(', ')}. getDesignValues() will omit those keys.`
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
return props;
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
// A getter (not a snapshot) so getDesignValues() stays reactive.
|
|
63
|
-
setResolvedDesign(() => tokenResolvedDesign);
|
|
64
|
-
|
|
65
|
-
// Same precedence as component nodes.
|
|
66
|
-
const composed = $derived.by(() => {
|
|
67
|
-
if (!experienceTemplate || !experienceTemplateConfig) return null;
|
|
68
|
-
return {
|
|
69
|
-
...experienceTemplateConfig.defaults,
|
|
70
|
-
...tokenResolvedDesign,
|
|
71
|
-
...experienceTemplate.props.content,
|
|
72
|
-
...experienceTemplate.props.resolved,
|
|
73
|
-
};
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
$effect(() => {
|
|
77
|
-
if (experienceTemplate && !experienceTemplateConfig && typeof console !== 'undefined') {
|
|
78
|
-
console.warn(
|
|
79
|
-
`[@contentful/experiences-svelte] No experience template registered for id "${experienceTemplate.experienceTemplateId}". Rendering nodes without the experience template wrapper.`
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
</script>
|
|
84
|
-
|
|
85
|
-
{#if experienceTemplate && experienceTemplateConfig && composed}
|
|
86
|
-
{@const Tpl = experienceTemplateConfig.component}
|
|
87
|
-
<Tpl {...composed} {children} />
|
|
88
|
-
{:else}
|
|
89
|
-
{@render children()}
|
|
90
|
-
{/if}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { Snippet } from 'svelte';
|
|
2
|
-
import type { PortableExperienceTemplate } from '@contentful/experiences-sdk-core';
|
|
3
|
-
import { type Config, type RenderContext } from './types.js';
|
|
4
|
-
interface WrapWithExperienceTemplateProps {
|
|
5
|
-
experienceTemplate: PortableExperienceTemplate | undefined;
|
|
6
|
-
config: Config;
|
|
7
|
-
experience: RenderContext;
|
|
8
|
-
children: Snippet;
|
|
9
|
-
}
|
|
10
|
-
declare const WrapWithExperienceTemplate: import("svelte").Component<WrapWithExperienceTemplateProps, {}, "">;
|
|
11
|
-
type WrapWithExperienceTemplate = ReturnType<typeof WrapWithExperienceTemplate>;
|
|
12
|
-
export default WrapWithExperienceTemplate;
|
|
13
|
-
//# sourceMappingURL=WrapWithExperienceTemplate.svelte.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"WrapWithExperienceTemplate.svelte.d.ts","sourceRoot":"","sources":["../src/WrapWithExperienceTemplate.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,kCAAkC,CAAC;AAInF,OAAO,EAGH,KAAK,MAAM,EACX,KAAK,aAAa,EACnB,MAAM,YAAY,CAAC;AAGpB,UAAU,+BAA+B;IACvC,kBAAkB,EAAE,0BAA0B,GAAG,SAAS,CAAC;IAC3D,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,aAAa,CAAC;IAC1B,QAAQ,EAAE,OAAO,CAAC;CACnB;AA6EH,QAAA,MAAM,0BAA0B,qEAAwC,CAAC;AACzE,KAAK,0BAA0B,GAAG,UAAU,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAChF,eAAe,0BAA0B,CAAC"}
|