@contentful/experiences-svelte 0.5.5 → 0.5.7

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +8 -6
  3. package/dist/ClientExperienceRenderer.svelte +6 -11
  4. package/dist/ClientExperienceRenderer.svelte.d.ts.map +1 -1
  5. package/dist/DebugExperience.svelte +9 -2
  6. package/dist/DebugExperience.svelte.d.ts.map +1 -1
  7. package/dist/DebugExperience.test.js +22 -9
  8. package/dist/NodeRenderer.svelte +73 -19
  9. package/dist/NodeRenderer.svelte.d.ts +6 -1
  10. package/dist/NodeRenderer.svelte.d.ts.map +1 -1
  11. package/dist/NodesRenderer.svelte +40 -17
  12. package/dist/NodesRenderer.svelte.d.ts.map +1 -1
  13. package/dist/ServerExperienceRenderer.svelte +6 -11
  14. package/dist/ServerExperienceRenderer.svelte.d.ts.map +1 -1
  15. package/dist/design-utils.d.ts +0 -8
  16. package/dist/design-utils.d.ts.map +1 -1
  17. package/dist/design-utils.js +1 -10
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +2 -1
  21. package/dist/nodes-renderer.ssr.test.d.ts +2 -0
  22. package/dist/nodes-renderer.ssr.test.d.ts.map +1 -0
  23. package/dist/nodes-renderer.ssr.test.js +126 -0
  24. package/dist/server-renderer.test.js +100 -44
  25. package/dist/test-fixtures/ExperienceTemplateFixture.svelte +8 -3
  26. package/dist/test-fixtures/ExperienceTemplateFixture.svelte.d.ts +1 -1
  27. package/dist/test-fixtures/ExperienceTemplateFixture.svelte.d.ts.map +1 -1
  28. package/dist/types.d.ts +28 -10
  29. package/dist/types.d.ts.map +1 -1
  30. package/dist/use-active-viewport.svelte.d.ts.map +1 -1
  31. package/dist/use-active-viewport.svelte.js +1 -22
  32. package/package.json +5 -5
  33. package/dist/WrapWithExperienceTemplate.svelte +0 -90
  34. package/dist/WrapWithExperienceTemplate.svelte.d.ts +0 -13
  35. package/dist/WrapWithExperienceTemplate.svelte.d.ts.map +0 -1
@@ -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: { componentId: 'contentful-container' },
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: { componentId: 'NotRegistered' },
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: { componentId: 'item' },
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('wraps rendered nodes with the registered experienceTemplate', async () => {
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: [componentNode('item', { id: 'i', contentProperties: { value: 'inside' } })],
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 nodes unwrapped + warns when the experienceTemplate is not registered', async () => {
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: [componentNode('item', { id: 'i', contentProperties: { value: 'unwrapped' } })],
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
- // Children is always injected as a Snippet by the renderer.
411
- expect(keys).toContain('children');
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 page-level experienceTemplate design props too', async () => {
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: [componentNode('item', { id: 'i', contentProperties: { value: 'ok' } })],
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
- // Experience Templates don't carry design props in the current XDA payload
542
- // shape; seed a raw token on the resolved plan so we exercise the
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
- children,
11
+ content,
9
12
  }: {
10
13
  title?: string;
11
- children: Snippet;
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
- {@render children()}
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
- children: Snippet;
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,QAAQ,EAAE,OAAO,CAAC;CACnB,CAAC;AAmBJ,QAAA,MAAM,yBAAyB,sDAAwC,CAAC;AACxE,KAAK,yBAAyB,GAAG,UAAU,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAC9E,eAAe,yBAAyB,CAAC"}
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 non-`children` slots through the exported `<NodesRenderer />`.
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. The default slot named
24
- * `children` is rendered automatically and passed as a Snippet prop;
25
- * additional slots (if any) are reachable here and can be rendered with
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 the page-level Experience
32
- * Template.
33
- * Exposed via `getContentfulExperienceTemplate()` inside an Experience Template's component tree.
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 + `children`
60
- * Snippet); resolved design values auto-fill matching props and are also
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;
@@ -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":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"use-active-viewport.svelte.d.ts","sourceRoot":"","sources":["../src/use-active-viewport.svelte.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAGpE,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;CACtC;AA4BD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,WAAW,EAAE,EACxB,iBAAiB,CAAC,EAAE,MAAM,GACzB,uBAAuB,CAiDzB"}
1
+ {"version":3,"file":"use-active-viewport.svelte.d.ts","sourceRoot":"","sources":["../src/use-active-viewport.svelte.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAGpE,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;CACtC;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,WAAW,EAAE,EACxB,iBAAiB,CAAC,EAAE,MAAM,GACzB,uBAAuB,CAiDzB"}
@@ -10,28 +10,7 @@
10
10
  * to seed the first render so SSR + client agree on viewport before
11
11
  * window.matchMedia takes over.
12
12
  */
13
- import { getViewportIndex, toCssMediaQuery } from '@contentful/experiences-design';
14
- function createMediaQueryMatchers(viewports) {
15
- const mediaQueryMatches = new Array(viewports.length).fill(false);
16
- // The first viewport is the wildcard "*" and always matches.
17
- mediaQueryMatches[0] = true;
18
- const mediaQueryMatchers = [];
19
- if (typeof window === 'undefined') {
20
- return [mediaQueryMatchers, mediaQueryMatches];
21
- }
22
- for (let index = 0; index < viewports.length; index++) {
23
- const viewport = viewports[index];
24
- if (!viewport)
25
- continue;
26
- const cssQuery = toCssMediaQuery(viewport);
27
- if (!cssQuery)
28
- continue;
29
- const matcher = window.matchMedia(cssQuery);
30
- mediaQueryMatches[index] = matcher.matches;
31
- mediaQueryMatchers.push({ index, signal: matcher });
32
- }
33
- return [mediaQueryMatchers, mediaQueryMatches];
34
- }
13
+ import { createMediaQueryMatchers, getViewportIndex } from '@contentful/experiences-design';
35
14
  /**
36
15
  * Returns a reactive object whose `activeViewportIndex` updates as the
37
16
  * window's matched media queries change. Mounts listeners via `$effect`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experiences-svelte",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
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.4",
39
- "@contentful/experiences-sdk-core": "0.7.4",
40
- "@contentful/experiences-design": "0.7.4"
38
+ "@contentful/experiences-client": "0.3.6",
39
+ "@contentful/experiences-sdk-core": "0.7.6",
40
+ "@contentful/experiences-design": "0.7.6"
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"}