@contentful/experiences-react 0.14.6 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## 0.15.0 (2026-10-27)
2
+
3
+ ### 🧱 Updated Dependencies
4
+
5
+ - Updated runtime to 0.3.0
6
+
7
+ ## 0.14.7 (2026-10-26)
8
+
9
+ ### 🚀 Features
10
+
11
+ - **react:** add ./custom-renderer export for host-rendered nodes [AIS-723] ([#260](https://github.com/contentful/experiences/pull/260))
12
+
1
13
  ## 0.14.6 (2026-10-26)
2
14
 
3
15
  ### 🚀 Features
package/README.md CHANGED
@@ -114,6 +114,17 @@ ClientExperienceRenderer; // Client renderer, alias: ExperienceRenderer
114
114
  MissingComponent; // Default fallback for unregistered component types
115
115
  ```
116
116
 
117
+ ### Custom renderer (`@contentful/experiences-react/custom-renderer`)
118
+
119
+ For hosts that render a plan themselves instead of using `ServerExperienceRenderer`. `resolveNodeProps` returns each node's component and merged props, `ExperienceNode` renders it the way the SDK renderers do, and `createExperienceNode` renders a node's serializable `snapshot` where components can't be passed as props. `ServerExperienceRenderer` and `ClientExperienceRenderer` render every node through the same `resolveNodeProps` + `ExperienceNode`.
120
+
121
+ ```ts
122
+ resolveNodeProps(node, config); // ResolvedNode: 'resolved' (component, props, design, contentful, snapshot) | 'unregistered'
123
+ ExperienceNode; // Renders one resolved node or snapshot exactly as the SDK renderers do
124
+ createExperienceNode(components, experienceTemplates?); // ExperienceNode with its registry bound, for snapshots
125
+ type ResolvedNode, NodeSnapshot, ExperienceNodeProps;
126
+ ```
127
+
117
128
  ### Styling + runtime context (hooks)
118
129
 
119
130
  ```ts
@@ -0,0 +1,8 @@
1
+ export { NodeSnapshot, ResolvedNode, resolveNodeProps } from './resolve-node-props.js';
2
+ export { ExperienceNode, ExperienceNodeProps, createExperienceNode } from './experience-node.js';
3
+ import 'react';
4
+ import '@contentful/experiences-sdk-core';
5
+ import './types.js';
6
+ import './nodes-renderer.js';
7
+ import './component-error.js';
8
+ import './missing-component.js';
@@ -0,0 +1,8 @@
1
+ import { resolveNodeProps } from "./resolve-node-props.js";
2
+ import { createExperienceNode, ExperienceNode } from "./experience-node.js";
3
+ export {
4
+ ExperienceNode,
5
+ createExperienceNode,
6
+ resolveNodeProps
7
+ };
8
+ //# sourceMappingURL=custom-renderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/custom-renderer.ts"],"sourcesContent":["/*\n * `@contentful/experiences-react/custom-renderer`: the building blocks of the\n * SDK renderers, for hosts that render a plan themselves.\n */\n\nexport { resolveNodeProps } from './resolve-node-props.js';\nexport type { NodeSnapshot, ResolvedNode } from './resolve-node-props.js';\n\nexport { createExperienceNode, ExperienceNode } from './experience-node.js';\nexport type { ExperienceNodeProps } from './experience-node.js';\n"],"mappings":"AAKA,SAAS,wBAAwB;AAGjC,SAAS,sBAAsB,sBAAsB;","names":[]}
@@ -0,0 +1,32 @@
1
+ import { ResolvedComponentNode, ResolvedExperienceTemplateNode, NodeSnapshot } from './resolve-node-props.js';
2
+ import { ReactNode } from 'react';
3
+ import { RenderError } from './nodes-renderer.js';
4
+ import { Components, ExperienceTemplates } from './types.js';
5
+ import { RenderContext } from '@contentful/experiences-sdk-core';
6
+ import './component-error.js';
7
+ import './missing-component.js';
8
+
9
+ /** Props of `ExperienceNode` and of the component `createExperienceNode` returns. */
10
+ interface BoundExperienceNodeProps {
11
+ node: ResolvedComponentNode | ResolvedExperienceTemplateNode | NodeSnapshot;
12
+ /** Publishes `useExperience()`. Needed outside an SDK renderer; context doesn't cross serialization boundaries. */
13
+ experience?: RenderContext;
14
+ /** Fallback rendered when the component throws. Defaults to `ComponentError`. */
15
+ renderError?: RenderError;
16
+ /** Slot props by name. Extra props are treated as slots too; `slots` wins on a clash. */
17
+ slots?: Record<string, unknown>;
18
+ [slot: string]: unknown;
19
+ }
20
+ interface ExperienceNodeProps extends BoundExperienceNodeProps {
21
+ /** Where a snapshot `node`'s component is looked up. */
22
+ components?: Components;
23
+ experienceTemplates?: ExperienceTemplates;
24
+ }
25
+ declare function ExperienceNode({ node, components, experienceTemplates, experience, renderError, slots, ...slotProps }: ExperienceNodeProps): ReactNode;
26
+ /**
27
+ * `ExperienceNode` with its registries bound, for rendering snapshots where
28
+ * components can't be passed as props. Only the bound components are bundled.
29
+ */
30
+ declare function createExperienceNode(components: Components, experienceTemplates?: ExperienceTemplates): (props: BoundExperienceNodeProps) => ReactNode;
31
+
32
+ export { type BoundExperienceNodeProps, ExperienceNode, type ExperienceNodeProps, createExperienceNode };
@@ -0,0 +1,68 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { createElement } from "react";
3
+ import { ComponentError } from "./component-error.js";
4
+ import { ComponentErrorBoundary } from "./component-error-boundary.js";
5
+ import {
6
+ ContentfulComponentProvider,
7
+ ContentfulExperienceTemplateProvider,
8
+ ExperienceProvider,
9
+ ResolvedDesignProvider
10
+ } from "./context.js";
11
+ import {
12
+ normalizeComponentRegistration,
13
+ normalizeExperienceTemplateRegistration
14
+ } from "./types.js";
15
+ function ExperienceNode({
16
+ node,
17
+ components,
18
+ experienceTemplates,
19
+ experience,
20
+ renderError = ComponentError,
21
+ slots,
22
+ ...slotProps
23
+ }) {
24
+ const component = "component" in node ? node.component : lookupComponent(node, components, experienceTemplates);
25
+ const element = /* @__PURE__ */ jsx(ResolvedDesignProvider, { value: node.design, children: /* @__PURE__ */ jsx(
26
+ ComponentErrorBoundary,
27
+ {
28
+ fallback: createElement(renderError, { componentId: node.id, nodeId: node.nodeId }),
29
+ componentId: node.id,
30
+ kind: node.kind,
31
+ nodeId: node.nodeId,
32
+ children: createElement(component, { ...node.props, ...slotProps, ...slots })
33
+ }
34
+ ) });
35
+ const scoped = node.kind === "experienceTemplate" ? /* @__PURE__ */ jsx(ContentfulExperienceTemplateProvider, { value: node.contentful, children: element }) : /* @__PURE__ */ jsx(ContentfulComponentProvider, { value: node.contentful, children: element });
36
+ return experience ? /* @__PURE__ */ jsx(ExperienceProvider, { value: experience, children: scoped }) : scoped;
37
+ }
38
+ function createExperienceNode(components, experienceTemplates) {
39
+ function BoundExperienceNode(props) {
40
+ return /* @__PURE__ */ jsx(
41
+ ExperienceNode,
42
+ {
43
+ ...props,
44
+ components,
45
+ experienceTemplates
46
+ }
47
+ );
48
+ }
49
+ return BoundExperienceNode;
50
+ }
51
+ function lookupComponent(node, components, experienceTemplates) {
52
+ if (node.kind === "experienceTemplate") {
53
+ const entry2 = experienceTemplates?.[node.id];
54
+ if (!entry2) throw new Error(missingMessage(node, "experienceTemplates"));
55
+ return normalizeExperienceTemplateRegistration(entry2).component;
56
+ }
57
+ const entry = components?.[node.id];
58
+ if (!entry) throw new Error(missingMessage(node, "components"));
59
+ return normalizeComponentRegistration(entry).component;
60
+ }
61
+ function missingMessage(node, registry) {
62
+ return `[@contentful/experiences-react] ExperienceNode received a snapshot of ${node.kind} "${node.id}"${node.nodeId ? ` (node "${node.nodeId}")` : ""} but "${node.id}" is not in its ${registry}. Add it to the map passed to createExperienceNode().`;
63
+ }
64
+ export {
65
+ ExperienceNode,
66
+ createExperienceNode
67
+ };
68
+ //# sourceMappingURL=experience-node.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/experience-node.tsx"],"sourcesContent":["/*\n * Renders one node the way the SDK renderers do (they render through this too):\n * context providers, error boundary, then the component with props and slots.\n * Directive-free so a Server Component can pass it a resolved node.\n */\n\nimport { createElement, type ReactNode } from 'react';\n\nimport { ComponentError } from './component-error.js';\n// A class component, so it lives in its own `'use client'` file.\nimport { ComponentErrorBoundary } from './component-error-boundary.js';\nimport {\n ContentfulComponentProvider,\n ContentfulExperienceTemplateProvider,\n ExperienceProvider,\n ResolvedDesignProvider,\n} from './context.js';\nimport type { RenderError } from './nodes-renderer.js';\nimport type {\n NodeSnapshot,\n ResolvedComponentNode,\n ResolvedExperienceTemplateNode,\n} from './resolve-node-props.js';\nimport {\n normalizeComponentRegistration,\n normalizeExperienceTemplateRegistration,\n type Components,\n type ExperienceTemplates,\n type RenderContext,\n} from './types.js';\n\n/** Props of `ExperienceNode` and of the component `createExperienceNode` returns. */\nexport interface BoundExperienceNodeProps {\n node: ResolvedComponentNode | ResolvedExperienceTemplateNode | NodeSnapshot;\n /** Publishes `useExperience()`. Needed outside an SDK renderer; context doesn't cross serialization boundaries. */\n experience?: RenderContext;\n /** Fallback rendered when the component throws. Defaults to `ComponentError`. */\n renderError?: RenderError;\n /** Slot props by name. Extra props are treated as slots too; `slots` wins on a clash. */\n slots?: Record<string, unknown>;\n [slot: string]: unknown;\n}\n\nexport interface ExperienceNodeProps extends BoundExperienceNodeProps {\n /** Where a snapshot `node`'s component is looked up. */\n components?: Components;\n experienceTemplates?: ExperienceTemplates;\n}\n\nexport function ExperienceNode({\n node,\n components,\n experienceTemplates,\n experience,\n renderError = ComponentError,\n slots,\n ...slotProps\n}: ExperienceNodeProps): ReactNode {\n const component =\n 'component' in node ? node.component : lookupComponent(node, components, experienceTemplates);\n\n const element = (\n <ResolvedDesignProvider value={node.design}>\n <ComponentErrorBoundary\n fallback={createElement(renderError, { componentId: node.id, nodeId: node.nodeId })}\n componentId={node.id}\n kind={node.kind}\n nodeId={node.nodeId}\n >\n {createElement(component, { ...node.props, ...slotProps, ...slots })}\n </ComponentErrorBoundary>\n </ResolvedDesignProvider>\n );\n\n const scoped =\n node.kind === 'experienceTemplate' ? (\n <ContentfulExperienceTemplateProvider value={node.contentful}>\n {element}\n </ContentfulExperienceTemplateProvider>\n ) : (\n <ContentfulComponentProvider value={node.contentful}>{element}</ContentfulComponentProvider>\n );\n\n return experience ? <ExperienceProvider value={experience}>{scoped}</ExperienceProvider> : scoped;\n}\n\n/**\n * `ExperienceNode` with its registries bound, for rendering snapshots where\n * components can't be passed as props. Only the bound components are bundled.\n */\nexport function createExperienceNode(\n components: Components,\n experienceTemplates?: ExperienceTemplates\n): (props: BoundExperienceNodeProps) => ReactNode {\n function BoundExperienceNode(props: BoundExperienceNodeProps): ReactNode {\n return (\n <ExperienceNode\n {...props}\n components={components}\n experienceTemplates={experienceTemplates}\n />\n );\n }\n return BoundExperienceNode;\n}\n\nfunction lookupComponent(\n node: NodeSnapshot,\n components: Components | undefined,\n experienceTemplates: ExperienceTemplates | undefined\n) {\n // A missing entry is a wiring mistake, not missing content, so fail loudly.\n if (node.kind === 'experienceTemplate') {\n const entry = experienceTemplates?.[node.id];\n if (!entry) throw new Error(missingMessage(node, 'experienceTemplates'));\n return normalizeExperienceTemplateRegistration(entry).component;\n }\n const entry = components?.[node.id];\n if (!entry) throw new Error(missingMessage(node, 'components'));\n return normalizeComponentRegistration(entry).component;\n}\n\nfunction missingMessage(node: NodeSnapshot, registry: string): string {\n return (\n `[@contentful/experiences-react] ExperienceNode received a snapshot of ${node.kind} \"${node.id}\"` +\n `${node.nodeId ? ` (node \"${node.nodeId}\")` : ''} but \"${node.id}\" is not in its ${registry}. ` +\n `Add it to the map passed to createExperienceNode().`\n );\n}\n"],"mappings":"AA+DM;AAzDN,SAAS,qBAAqC;AAE9C,SAAS,sBAAsB;AAE/B,SAAS,8BAA8B;AACvC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAOP;AAAA,EACE;AAAA,EACA;AAAA,OAIK;AAoBA,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA,GAAG;AACL,GAAmC;AACjC,QAAM,YACJ,eAAe,OAAO,KAAK,YAAY,gBAAgB,MAAM,YAAY,mBAAmB;AAE9F,QAAM,UACJ,oBAAC,0BAAuB,OAAO,KAAK,QAClC;AAAA,IAAC;AAAA;AAAA,MACC,UAAU,cAAc,aAAa,EAAE,aAAa,KAAK,IAAI,QAAQ,KAAK,OAAO,CAAC;AAAA,MAClF,aAAa,KAAK;AAAA,MAClB,MAAM,KAAK;AAAA,MACX,QAAQ,KAAK;AAAA,MAEZ,wBAAc,WAAW,EAAE,GAAG,KAAK,OAAO,GAAG,WAAW,GAAG,MAAM,CAAC;AAAA;AAAA,EACrE,GACF;AAGF,QAAM,SACJ,KAAK,SAAS,uBACZ,oBAAC,wCAAqC,OAAO,KAAK,YAC/C,mBACH,IAEA,oBAAC,+BAA4B,OAAO,KAAK,YAAa,mBAAQ;AAGlE,SAAO,aAAa,oBAAC,sBAAmB,OAAO,YAAa,kBAAO,IAAwB;AAC7F;AAMO,SAAS,qBACd,YACA,qBACgD;AAChD,WAAS,oBAAoB,OAA4C;AACvE,WACE;AAAA,MAAC;AAAA;AAAA,QACE,GAAG;AAAA,QACJ;AAAA,QACA;AAAA;AAAA,IACF;AAAA,EAEJ;AACA,SAAO;AACT;AAEA,SAAS,gBACP,MACA,YACA,qBACA;AAEA,MAAI,KAAK,SAAS,sBAAsB;AACtC,UAAMA,SAAQ,sBAAsB,KAAK,EAAE;AAC3C,QAAI,CAACA,OAAO,OAAM,IAAI,MAAM,eAAe,MAAM,qBAAqB,CAAC;AACvE,WAAO,wCAAwCA,MAAK,EAAE;AAAA,EACxD;AACA,QAAM,QAAQ,aAAa,KAAK,EAAE;AAClC,MAAI,CAAC,MAAO,OAAM,IAAI,MAAM,eAAe,MAAM,YAAY,CAAC;AAC9D,SAAO,+BAA+B,KAAK,EAAE;AAC/C;AAEA,SAAS,eAAe,MAAoB,UAA0B;AACpE,SACE,yEAAyE,KAAK,IAAI,KAAK,KAAK,EAAE,IAC3F,KAAK,SAAS,WAAW,KAAK,MAAM,OAAO,EAAE,SAAS,KAAK,EAAE,mBAAmB,QAAQ;AAG/F;","names":["entry"]}
@@ -1,15 +1,7 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import { Fragment, createElement } from "react";
3
- import { ComponentErrorBoundary } from "./component-error-boundary.js";
4
- import {
5
- ContentfulComponentProvider,
6
- ContentfulExperienceTemplateProvider,
7
- ResolvedDesignProvider
8
- } from "./context.js";
9
- import {
10
- normalizeComponentRegistration,
11
- normalizeExperienceTemplateRegistration
12
- } from "./types.js";
3
+ import { ExperienceNode } from "./experience-node.js";
4
+ import { resolveNodeProps } from "./resolve-node-props.js";
13
5
  function NodesRenderer({
14
6
  nodes,
15
7
  config,
@@ -62,8 +54,8 @@ function NodeRenderer({
62
54
  child.nodeId ?? index
63
55
  ));
64
56
  }
65
- const entry = isExperienceTemplate ? config.experienceTemplates?.[id] : config.components[id];
66
- if (!entry) {
57
+ const resolved = resolveNodeProps(node, config);
58
+ if (resolved.status === "unregistered") {
67
59
  if (isExperienceTemplate) {
68
60
  const message = `No experience template registered for id "${id}". Rendering its slot children without the experience template wrapper.`;
69
61
  if (typeof console !== "undefined") {
@@ -79,45 +71,7 @@ function NodeRenderer({
79
71
  );
80
72
  return createElement(renderUnknown, { componentId: id, nodeId: node.nodeId });
81
73
  }
82
- const registrationConfig = isExperienceTemplate ? normalizeExperienceTemplateRegistration(entry) : normalizeComponentRegistration(entry);
83
- const tokenResolvedDesign = node.props.design;
84
- const composed = {
85
- ...registrationConfig.defaults,
86
- ...tokenResolvedDesign,
87
- ...node.props.content,
88
- ...node.props.resolved,
89
- ...slotProps
90
- };
91
- const element = /* @__PURE__ */ jsx(ResolvedDesignProvider, { value: tokenResolvedDesign, children: /* @__PURE__ */ jsx(
92
- ComponentErrorBoundary,
93
- {
94
- fallback: createElement(renderError, { componentId: id, nodeId: node.nodeId }),
95
- componentId: id,
96
- kind,
97
- nodeId: node.nodeId,
98
- children: createElement(registrationConfig.component, composed)
99
- }
100
- ) });
101
- if (isExperienceTemplate) {
102
- const contentful2 = {
103
- experienceTemplateId: id,
104
- nodeId: node.nodeId,
105
- content: node.props.content,
106
- design: node.props.designRaw,
107
- resolved: node.props.resolved,
108
- attribution: node.attribution
109
- };
110
- return /* @__PURE__ */ jsx(ContentfulExperienceTemplateProvider, { value: contentful2, children: element });
111
- }
112
- const contentful = {
113
- componentId: id,
114
- nodeId: node.nodeId,
115
- content: node.props.content,
116
- design: node.props.designRaw,
117
- resolved: node.props.resolved,
118
- attribution: node.attribution
119
- };
120
- return /* @__PURE__ */ jsx(ContentfulComponentProvider, { value: contentful, children: element });
74
+ return /* @__PURE__ */ jsx(ExperienceNode, { node: resolved, renderError, slots: slotProps });
121
75
  }
122
76
  export {
123
77
  NodesRenderer
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/nodes-renderer.tsx"],"sourcesContent":["/*\n * Recursive renderer over PortableRenderNodes. Resolved design values auto-fill\n * matching props (below content/resolveData) and are also available via\n * `useDesignValues()`.\n */\n\nimport { Fragment, createElement, type ReactNode } from 'react';\n\nimport type { PortableRenderNode } from '@contentful/experiences-sdk-core';\n\nimport { ComponentErrorBoundary } from './component-error-boundary.js';\nimport type { ComponentErrorProps } from './component-error.js';\nimport {\n ContentfulComponentProvider,\n ContentfulExperienceTemplateProvider,\n ResolvedDesignProvider,\n} from './context.js';\nimport type { MissingComponentProps } from './missing-component.js';\nimport {\n normalizeComponentRegistration,\n normalizeExperienceTemplateRegistration,\n type ContentfulComponent,\n type ContentfulExperienceTemplate,\n type Config,\n} from './types.js';\n\nexport type RenderUnknown = (props: MissingComponentProps) => ReactNode;\nexport type RenderError = (props: ComponentErrorProps) => ReactNode;\n\n/**\n * Reports one render-time diagnostic (unregistered id, a component that\n * threw). `ServerExperienceRenderer` passes a closure that pushes onto a\n * plain array (SSR is synchronous top-down, so the array is fully populated\n * by the time `<DebugExperience>` reads it — see the element-order note\n * there). `ClientExperienceRenderer` passes a `setState` updater instead, so\n * `<DebugExperience>` re-renders reactively when a later interaction throws.\n */\nexport type DiagnosticReporter = (error: Error) => void;\n\nexport interface NodesRendererProps {\n nodes: PortableRenderNode[];\n config: Config;\n renderUnknown: RenderUnknown;\n renderError: RenderError;\n onDiagnostic: DiagnosticReporter;\n}\n\nexport function NodesRenderer({\n nodes,\n config,\n renderUnknown,\n renderError,\n onDiagnostic,\n}: NodesRendererProps): ReactNode {\n if (!nodes.length) return null;\n return (\n <Fragment>\n {nodes.map((node, index) => (\n <NodeRenderer\n key={node.nodeId ?? index}\n node={node}\n config={config}\n renderUnknown={renderUnknown}\n renderError={renderError}\n onDiagnostic={onDiagnostic}\n />\n ))}\n </Fragment>\n );\n}\n\ninterface NodeRendererProps {\n node: PortableRenderNode;\n config: Config;\n renderUnknown: RenderUnknown;\n renderError: RenderError;\n onDiagnostic: DiagnosticReporter;\n}\n\nfunction NodeRenderer({\n node,\n config,\n renderUnknown,\n renderError,\n onDiagnostic,\n}: NodeRendererProps): ReactNode {\n const { kind, id } = node.registration;\n const isExperienceTemplate = kind === 'experienceTemplate';\n\n // Diagnostics are reported straight to `onDiagnostic`, synchronously during\n // render. Deliberate: this component is shared between SSR and CSR, and\n // `useEffect` never runs during `renderToStaticMarkup`/streaming SSR — an\n // effect-deferred report would silently vanish on the server.\n //\n // Dedup lives in `onDiagnostic` (see both renderers), not here. An ancestor\n // re-render re-executes this whole function, so an unregistered id or\n // malformed slot would otherwise be re-reported every time — the collectors\n // drop a message they already hold. Keeping the guard there rather than in a\n // `useRef` is what lets this module stay hook-free, and therefore usable from\n // a React Server Component: `ServerExperienceRenderer` renders it without a\n // `'use client'` boundary, which is what keeps `config` (component\n // references, unserializable) from having to cross one.\n\n // Pre-render each slot's children as an *array* of ReactNodes (one keyed\n // element per child), not a single wrapping node. A component can drop the\n // array straight into JSX for the common \"just render them\" case (React\n // renders keyed arrays), or map/filter/wrap the children individually.\n //\n // Slot name → prop name, for both node kinds: a coded Experience Template\n // declaring a `content` slot receives a `content` prop. `children` is not a\n // special case, just the conventional default slot name.\n const slotProps: Record<string, ReactNode[]> = {};\n for (const [slotName, children] of Object.entries(node.slots)) {\n // Defensive: `node.slots[x]` is typed as an array, but a hand-built\n // PortableRenderPlan (a supported path — customers can construct one\n // directly instead of going through `resolveExperience`) is not\n // type-checked at runtime. Warn + drop rather than letting `.map` throw.\n if (!Array.isArray(children)) {\n const message =\n `Slot \"${slotName}\" on ${kind} \"${id}\"${node.nodeId ? ` (node \"${node.nodeId}\")` : ''} ` +\n `is not an array of nodes; rendering it as empty instead of throwing.`;\n if (typeof console !== 'undefined') {\n console.warn(`[@contentful/experiences-react] ${message}`);\n }\n onDiagnostic(new Error(message));\n slotProps[slotName] = [];\n continue;\n }\n slotProps[slotName] = children.map((child, index) => (\n <NodeRenderer\n key={child.nodeId ?? index}\n node={child}\n config={config}\n renderUnknown={renderUnknown}\n renderError={renderError}\n onDiagnostic={onDiagnostic}\n />\n ));\n }\n\n const entry = isExperienceTemplate ? config.experienceTemplates?.[id] : config.components[id];\n if (!entry) {\n // An unregistered Experience Template would blank the page if we swapped\n // it for the missing-component box, so warn and render its slot children\n // unwrapped — the content survives, the diagnostic names what's missing.\n if (isExperienceTemplate) {\n const message = `No experience template registered for id \"${id}\". Rendering its slot children without the experience template wrapper.`;\n if (typeof console !== 'undefined') {\n console.warn(`[@contentful/experiences-react] ${message}`);\n }\n onDiagnostic(new Error(message));\n return <Fragment>{Object.values(slotProps).flat()}</Fragment>;\n }\n // `MissingComponent` (the default `renderUnknown`) does its own\n // console.warn; a custom override may not, so the diagnostic is recorded\n // here regardless of which fallback ends up rendering.\n onDiagnostic(\n new Error(\n `No component registered for id \"${id}\"${node.nodeId ? ` (nodeId: ${node.nodeId})` : ''}.`\n )\n );\n return createElement(renderUnknown, { componentId: id, nodeId: node.nodeId });\n }\n const registrationConfig = isExperienceTemplate\n ? normalizeExperienceTemplateRegistration(entry)\n : normalizeComponentRegistration(entry);\n\n const tokenResolvedDesign = node.props.design;\n\n // Merge precedence (last wins): defaults < design < content < resolveData < slots.\n const composed = {\n ...registrationConfig.defaults,\n ...tokenResolvedDesign,\n ...node.props.content,\n ...node.props.resolved,\n ...slotProps,\n };\n\n const element = (\n <ResolvedDesignProvider value={tokenResolvedDesign}>\n <ComponentErrorBoundary\n fallback={createElement(renderError, { componentId: id, nodeId: node.nodeId })}\n componentId={id}\n kind={kind}\n nodeId={node.nodeId}\n >\n {createElement(registrationConfig.component, composed)}\n </ComponentErrorBoundary>\n </ResolvedDesignProvider>\n );\n\n if (isExperienceTemplate) {\n const contentful: ContentfulExperienceTemplate = {\n experienceTemplateId: id,\n nodeId: node.nodeId,\n content: node.props.content,\n design: node.props.designRaw,\n resolved: node.props.resolved,\n attribution: node.attribution,\n };\n return (\n <ContentfulExperienceTemplateProvider value={contentful}>\n {element}\n </ContentfulExperienceTemplateProvider>\n );\n }\n\n const contentful: ContentfulComponent = {\n componentId: id,\n nodeId: node.nodeId,\n content: node.props.content,\n design: node.props.designRaw,\n resolved: node.props.resolved,\n attribution: node.attribution,\n };\n return <ContentfulComponentProvider value={contentful}>{element}</ContentfulComponentProvider>;\n}\n\n// `ComponentErrorBoundary` lives in its own `'use client'` file — see\n// component-error-boundary.tsx for why (it's a class component, which\n// Next.js's RSC bundler rejects from a directive-free file).\n"],"mappings":"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","names":["contentful"]}
1
+ {"version":3,"sources":["../src/nodes-renderer.tsx"],"sourcesContent":["/*\n * Recursive renderer over PortableRenderNodes. Resolved design values auto-fill\n * matching props (below content/resolveData) and are also available via\n * `useDesignValues()`.\n */\n\nimport { Fragment, createElement, type ReactNode } from 'react';\n\nimport type { PortableRenderNode } from '@contentful/experiences-sdk-core';\n\nimport type { ComponentErrorProps } from './component-error.js';\nimport { ExperienceNode } from './experience-node.js';\nimport type { MissingComponentProps } from './missing-component.js';\nimport { resolveNodeProps } from './resolve-node-props.js';\nimport type { Config } from './types.js';\n\nexport type RenderUnknown = (props: MissingComponentProps) => ReactNode;\nexport type RenderError = (props: ComponentErrorProps) => ReactNode;\n\n/**\n * Reports one render-time diagnostic (unregistered id, a component that\n * threw). `ServerExperienceRenderer` passes a closure that pushes onto a\n * plain array (SSR is synchronous top-down, so the array is fully populated\n * by the time `<DebugExperience>` reads it — see the element-order note\n * there). `ClientExperienceRenderer` passes a `setState` updater instead, so\n * `<DebugExperience>` re-renders reactively when a later interaction throws.\n */\nexport type DiagnosticReporter = (error: Error) => void;\n\nexport interface NodesRendererProps {\n nodes: PortableRenderNode[];\n config: Config;\n renderUnknown: RenderUnknown;\n renderError: RenderError;\n onDiagnostic: DiagnosticReporter;\n}\n\nexport function NodesRenderer({\n nodes,\n config,\n renderUnknown,\n renderError,\n onDiagnostic,\n}: NodesRendererProps): ReactNode {\n if (!nodes.length) return null;\n return (\n <Fragment>\n {nodes.map((node, index) => (\n <NodeRenderer\n key={node.nodeId ?? index}\n node={node}\n config={config}\n renderUnknown={renderUnknown}\n renderError={renderError}\n onDiagnostic={onDiagnostic}\n />\n ))}\n </Fragment>\n );\n}\n\ninterface NodeRendererProps {\n node: PortableRenderNode;\n config: Config;\n renderUnknown: RenderUnknown;\n renderError: RenderError;\n onDiagnostic: DiagnosticReporter;\n}\n\nfunction NodeRenderer({\n node,\n config,\n renderUnknown,\n renderError,\n onDiagnostic,\n}: NodeRendererProps): ReactNode {\n const { kind, id } = node.registration;\n const isExperienceTemplate = kind === 'experienceTemplate';\n\n // Diagnostics are reported straight to `onDiagnostic`, synchronously during\n // render. Deliberate: this component is shared between SSR and CSR, and\n // `useEffect` never runs during `renderToStaticMarkup`/streaming SSR — an\n // effect-deferred report would silently vanish on the server.\n //\n // Dedup lives in `onDiagnostic` (see both renderers), not here. An ancestor\n // re-render re-executes this whole function, so an unregistered id or\n // malformed slot would otherwise be re-reported every time — the collectors\n // drop a message they already hold. Keeping the guard there rather than in a\n // `useRef` is what lets this module stay hook-free, and therefore usable from\n // a React Server Component: `ServerExperienceRenderer` renders it without a\n // `'use client'` boundary, which is what keeps `config` (component\n // references, unserializable) from having to cross one.\n\n // Pre-render each slot's children as an *array* of ReactNodes (one keyed\n // element per child), not a single wrapping node. A component can drop the\n // array straight into JSX for the common \"just render them\" case (React\n // renders keyed arrays), or map/filter/wrap the children individually.\n //\n // Slot name → prop name, for both node kinds: a coded Experience Template\n // declaring a `content` slot receives a `content` prop. `children` is not a\n // special case, just the conventional default slot name.\n const slotProps: Record<string, ReactNode[]> = {};\n for (const [slotName, children] of Object.entries(node.slots)) {\n // Defensive: `node.slots[x]` is typed as an array, but a hand-built\n // PortableRenderPlan (a supported path — customers can construct one\n // directly instead of going through `resolveExperience`) is not\n // type-checked at runtime. Warn + drop rather than letting `.map` throw.\n if (!Array.isArray(children)) {\n const message =\n `Slot \"${slotName}\" on ${kind} \"${id}\"${node.nodeId ? ` (node \"${node.nodeId}\")` : ''} ` +\n `is not an array of nodes; rendering it as empty instead of throwing.`;\n if (typeof console !== 'undefined') {\n console.warn(`[@contentful/experiences-react] ${message}`);\n }\n onDiagnostic(new Error(message));\n slotProps[slotName] = [];\n continue;\n }\n slotProps[slotName] = children.map((child, index) => (\n <NodeRenderer\n key={child.nodeId ?? index}\n node={child}\n config={config}\n renderUnknown={renderUnknown}\n renderError={renderError}\n onDiagnostic={onDiagnostic}\n />\n ));\n }\n\n const resolved = resolveNodeProps(node, config);\n if (resolved.status === 'unregistered') {\n // An unregistered Experience Template would blank the page if we swapped\n // it for the missing-component box, so warn and render its slot children\n // unwrapped — the content survives, the diagnostic names what's missing.\n if (isExperienceTemplate) {\n const message = `No experience template registered for id \"${id}\". Rendering its slot children without the experience template wrapper.`;\n if (typeof console !== 'undefined') {\n console.warn(`[@contentful/experiences-react] ${message}`);\n }\n onDiagnostic(new Error(message));\n return <Fragment>{Object.values(slotProps).flat()}</Fragment>;\n }\n // `MissingComponent` (the default `renderUnknown`) does its own\n // console.warn; a custom override may not, so the diagnostic is recorded\n // here regardless of which fallback ends up rendering.\n onDiagnostic(\n new Error(\n `No component registered for id \"${id}\"${node.nodeId ? ` (nodeId: ${node.nodeId})` : ''}.`\n )\n );\n return createElement(renderUnknown, { componentId: id, nodeId: node.nodeId });\n }\n // Slots go through the explicit `slots` prop so a slot can't collide with one\n // of ExperienceNode's own prop names.\n return <ExperienceNode node={resolved} renderError={renderError} slots={slotProps} />;\n}\n"],"mappings":"AAgDQ;AA1CR,SAAS,UAAU,qBAAqC;AAKxD,SAAS,sBAAsB;AAE/B,SAAS,wBAAwB;AAwB1B,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAkC;AAChC,MAAI,CAAC,MAAM,OAAQ,QAAO;AAC1B,SACE,oBAAC,YACE,gBAAM,IAAI,CAAC,MAAM,UAChB;AAAA,IAAC;AAAA;AAAA,MAEC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA;AAAA,IALK,KAAK,UAAU;AAAA,EAMtB,CACD,GACH;AAEJ;AAUA,SAAS,aAAa;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAiC;AAC/B,QAAM,EAAE,MAAM,GAAG,IAAI,KAAK;AAC1B,QAAM,uBAAuB,SAAS;AAwBtC,QAAM,YAAyC,CAAC;AAChD,aAAW,CAAC,UAAU,QAAQ,KAAK,OAAO,QAAQ,KAAK,KAAK,GAAG;AAK7D,QAAI,CAAC,MAAM,QAAQ,QAAQ,GAAG;AAC5B,YAAM,UACJ,SAAS,QAAQ,QAAQ,IAAI,KAAK,EAAE,IAAI,KAAK,SAAS,WAAW,KAAK,MAAM,OAAO,EAAE;AAEvF,UAAI,OAAO,YAAY,aAAa;AAClC,gBAAQ,KAAK,mCAAmC,OAAO,EAAE;AAAA,MAC3D;AACA,mBAAa,IAAI,MAAM,OAAO,CAAC;AAC/B,gBAAU,QAAQ,IAAI,CAAC;AACvB;AAAA,IACF;AACA,cAAU,QAAQ,IAAI,SAAS,IAAI,CAAC,OAAO,UACzC;AAAA,MAAC;AAAA;AAAA,QAEC,MAAM;AAAA,QACN;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA;AAAA,MALK,MAAM,UAAU;AAAA,IAMvB,CACD;AAAA,EACH;AAEA,QAAM,WAAW,iBAAiB,MAAM,MAAM;AAC9C,MAAI,SAAS,WAAW,gBAAgB;AAItC,QAAI,sBAAsB;AACxB,YAAM,UAAU,6CAA6C,EAAE;AAC/D,UAAI,OAAO,YAAY,aAAa;AAClC,gBAAQ,KAAK,mCAAmC,OAAO,EAAE;AAAA,MAC3D;AACA,mBAAa,IAAI,MAAM,OAAO,CAAC;AAC/B,aAAO,oBAAC,YAAU,iBAAO,OAAO,SAAS,EAAE,KAAK,GAAE;AAAA,IACpD;AAIA;AAAA,MACE,IAAI;AAAA,QACF,mCAAmC,EAAE,IAAI,KAAK,SAAS,aAAa,KAAK,MAAM,MAAM,EAAE;AAAA,MACzF;AAAA,IACF;AACA,WAAO,cAAc,eAAe,EAAE,aAAa,IAAI,QAAQ,KAAK,OAAO,CAAC;AAAA,EAC9E;AAGA,SAAO,oBAAC,kBAAe,MAAM,UAAU,aAA0B,OAAO,WAAW;AACrF;","names":[]}
@@ -0,0 +1,45 @@
1
+ import { ComponentType } from 'react';
2
+ import { PortableRenderNode } from '@contentful/experiences-sdk-core';
3
+ import { ContentfulComponent, ContentfulExperienceTemplate, Config } from './types.js';
4
+
5
+ interface NodeSnapshotBase {
6
+ id: string;
7
+ nodeId?: string;
8
+ /** Merged props (defaults < design < content < resolveData), without slots. */
9
+ props: Record<string, unknown>;
10
+ /** Token-resolved design values. */
11
+ design: Record<string, unknown>;
12
+ }
13
+ /**
14
+ * A resolved node as plain data, without its component. Serializable as long
15
+ * as the `resolveData` output it carries is.
16
+ */
17
+ interface ComponentNodeSnapshot extends NodeSnapshotBase {
18
+ kind: 'component';
19
+ contentful: ContentfulComponent;
20
+ }
21
+ interface ExperienceTemplateNodeSnapshot extends NodeSnapshotBase {
22
+ kind: 'experienceTemplate';
23
+ contentful: ContentfulExperienceTemplate;
24
+ }
25
+ type NodeSnapshot = ComponentNodeSnapshot | ExperienceTemplateNodeSnapshot;
26
+ interface ResolvedNodeExtras<S extends NodeSnapshot> {
27
+ status: 'resolved';
28
+ component: ComponentType<any>;
29
+ /** This node as plain data, for passing across a serialization boundary. */
30
+ snapshot: S;
31
+ }
32
+ type ResolvedComponentNode = ComponentNodeSnapshot & ResolvedNodeExtras<ComponentNodeSnapshot>;
33
+ type ResolvedExperienceTemplateNode = ExperienceTemplateNodeSnapshot & ResolvedNodeExtras<ExperienceTemplateNodeSnapshot>;
34
+ /** A node whose id has no registration. Returned, not thrown, so the caller picks the fallback. */
35
+ interface UnregisteredNode {
36
+ status: 'unregistered';
37
+ kind: 'component' | 'experienceTemplate';
38
+ id: string;
39
+ nodeId?: string;
40
+ }
41
+ type ResolvedNode = ResolvedComponentNode | ResolvedExperienceTemplateNode | UnregisteredNode;
42
+ /** Look a node up in `config.components` or `config.experienceTemplates` and merge its props. */
43
+ declare function resolveNodeProps(node: PortableRenderNode, config: Config): ResolvedNode;
44
+
45
+ export { type ComponentNodeSnapshot, type ExperienceTemplateNodeSnapshot, type NodeSnapshot, type ResolvedComponentNode, type ResolvedExperienceTemplateNode, type ResolvedNode, type UnregisteredNode, resolveNodeProps };
@@ -0,0 +1,60 @@
1
+ import {
2
+ normalizeComponentRegistration,
3
+ normalizeExperienceTemplateRegistration
4
+ } from "./types.js";
5
+ function resolveNodeProps(node, config) {
6
+ const { kind, id } = node.registration;
7
+ const { nodeId } = node;
8
+ if (kind === "experienceTemplate") {
9
+ const entry2 = config.experienceTemplates?.[id];
10
+ if (!entry2) return { status: "unregistered", kind, id, nodeId };
11
+ const registration2 = normalizeExperienceTemplateRegistration(entry2);
12
+ const snapshot2 = {
13
+ kind,
14
+ id,
15
+ nodeId,
16
+ props: mergeProps(registration2.defaults, node),
17
+ design: node.props.design,
18
+ contentful: {
19
+ experienceTemplateId: id,
20
+ nodeId,
21
+ content: node.props.content,
22
+ design: node.props.designRaw,
23
+ resolved: node.props.resolved,
24
+ attribution: node.attribution
25
+ }
26
+ };
27
+ return { ...snapshot2, status: "resolved", component: registration2.component, snapshot: snapshot2 };
28
+ }
29
+ const entry = config.components[id];
30
+ if (!entry) return { status: "unregistered", kind, id, nodeId };
31
+ const registration = normalizeComponentRegistration(entry);
32
+ const snapshot = {
33
+ kind,
34
+ id,
35
+ nodeId,
36
+ props: mergeProps(registration.defaults, node),
37
+ design: node.props.design,
38
+ contentful: {
39
+ componentId: id,
40
+ nodeId,
41
+ content: node.props.content,
42
+ design: node.props.designRaw,
43
+ resolved: node.props.resolved,
44
+ attribution: node.attribution
45
+ }
46
+ };
47
+ return { ...snapshot, status: "resolved", component: registration.component, snapshot };
48
+ }
49
+ function mergeProps(defaults, node) {
50
+ return {
51
+ ...defaults,
52
+ ...node.props.design,
53
+ ...node.props.content,
54
+ ...node.props.resolved
55
+ };
56
+ }
57
+ export {
58
+ resolveNodeProps
59
+ };
60
+ //# sourceMappingURL=resolve-node-props.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/resolve-node-props.ts"],"sourcesContent":["/*\n * Registry lookup and prop merge for one plan node, shared by the SDK renderers\n * and custom renderers. Hook- and directive-free so any server module can call it.\n */\n\nimport type { ComponentType } from 'react';\n\nimport type { PortableRenderNode } from '@contentful/experiences-sdk-core';\n\nimport {\n normalizeComponentRegistration,\n normalizeExperienceTemplateRegistration,\n type Config,\n type ContentfulComponent,\n type ContentfulExperienceTemplate,\n} from './types.js';\n\ninterface NodeSnapshotBase {\n id: string;\n nodeId?: string;\n /** Merged props (defaults < design < content < resolveData), without slots. */\n props: Record<string, unknown>;\n /** Token-resolved design values. */\n design: Record<string, unknown>;\n}\n\n/**\n * A resolved node as plain data, without its component. Serializable as long\n * as the `resolveData` output it carries is.\n */\nexport interface ComponentNodeSnapshot extends NodeSnapshotBase {\n kind: 'component';\n contentful: ContentfulComponent;\n}\n\nexport interface ExperienceTemplateNodeSnapshot extends NodeSnapshotBase {\n kind: 'experienceTemplate';\n contentful: ContentfulExperienceTemplate;\n}\n\nexport type NodeSnapshot = ComponentNodeSnapshot | ExperienceTemplateNodeSnapshot;\n\ninterface ResolvedNodeExtras<S extends NodeSnapshot> {\n status: 'resolved';\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- the registry erases per-entry prop types; see `Components`.\n component: ComponentType<any>;\n /** This node as plain data, for passing across a serialization boundary. */\n snapshot: S;\n}\n\nexport type ResolvedComponentNode = ComponentNodeSnapshot &\n ResolvedNodeExtras<ComponentNodeSnapshot>;\n\nexport type ResolvedExperienceTemplateNode = ExperienceTemplateNodeSnapshot &\n ResolvedNodeExtras<ExperienceTemplateNodeSnapshot>;\n\n/** A node whose id has no registration. Returned, not thrown, so the caller picks the fallback. */\nexport interface UnregisteredNode {\n status: 'unregistered';\n kind: 'component' | 'experienceTemplate';\n id: string;\n nodeId?: string;\n}\n\nexport type ResolvedNode =\n ResolvedComponentNode | ResolvedExperienceTemplateNode | UnregisteredNode;\n\n/** Look a node up in `config.components` or `config.experienceTemplates` and merge its props. */\nexport function resolveNodeProps(node: PortableRenderNode, config: Config): ResolvedNode {\n const { kind, id } = node.registration;\n const { nodeId } = node;\n\n if (kind === 'experienceTemplate') {\n const entry = config.experienceTemplates?.[id];\n if (!entry) return { status: 'unregistered', kind, id, nodeId };\n const registration = normalizeExperienceTemplateRegistration(entry);\n const snapshot: ExperienceTemplateNodeSnapshot = {\n kind,\n id,\n nodeId,\n props: mergeProps(registration.defaults, node),\n design: node.props.design,\n contentful: {\n experienceTemplateId: id,\n nodeId,\n content: node.props.content,\n design: node.props.designRaw,\n resolved: node.props.resolved,\n attribution: node.attribution,\n },\n };\n return { ...snapshot, status: 'resolved', component: registration.component, snapshot };\n }\n\n const entry = config.components[id];\n if (!entry) return { status: 'unregistered', kind, id, nodeId };\n const registration = normalizeComponentRegistration(entry);\n const snapshot: ComponentNodeSnapshot = {\n kind,\n id,\n nodeId,\n props: mergeProps(registration.defaults, node),\n design: node.props.design,\n contentful: {\n componentId: id,\n nodeId,\n content: node.props.content,\n design: node.props.designRaw,\n resolved: node.props.resolved,\n attribution: node.attribution,\n },\n };\n return { ...snapshot, status: 'resolved', component: registration.component, snapshot };\n}\n\n// Merge precedence (last wins): defaults < design < content < resolveData.\nfunction mergeProps(\n defaults: object | undefined,\n node: PortableRenderNode\n): Record<string, unknown> {\n return {\n ...defaults,\n ...node.props.design,\n ...node.props.content,\n ...node.props.resolved,\n };\n}\n"],"mappings":"AASA;AAAA,EACE;AAAA,EACA;AAAA,OAIK;AAqDA,SAAS,iBAAiB,MAA0B,QAA8B;AACvF,QAAM,EAAE,MAAM,GAAG,IAAI,KAAK;AAC1B,QAAM,EAAE,OAAO,IAAI;AAEnB,MAAI,SAAS,sBAAsB;AACjC,UAAMA,SAAQ,OAAO,sBAAsB,EAAE;AAC7C,QAAI,CAACA,OAAO,QAAO,EAAE,QAAQ,gBAAgB,MAAM,IAAI,OAAO;AAC9D,UAAMC,gBAAe,wCAAwCD,MAAK;AAClE,UAAME,YAA2C;AAAA,MAC/C;AAAA,MACA;AAAA,MACA;AAAA,MACA,OAAO,WAAWD,cAAa,UAAU,IAAI;AAAA,MAC7C,QAAQ,KAAK,MAAM;AAAA,MACnB,YAAY;AAAA,QACV,sBAAsB;AAAA,QACtB;AAAA,QACA,SAAS,KAAK,MAAM;AAAA,QACpB,QAAQ,KAAK,MAAM;AAAA,QACnB,UAAU,KAAK,MAAM;AAAA,QACrB,aAAa,KAAK;AAAA,MACpB;AAAA,IACF;AACA,WAAO,EAAE,GAAGC,WAAU,QAAQ,YAAY,WAAWD,cAAa,WAAW,UAAAC,UAAS;AAAA,EACxF;AAEA,QAAM,QAAQ,OAAO,WAAW,EAAE;AAClC,MAAI,CAAC,MAAO,QAAO,EAAE,QAAQ,gBAAgB,MAAM,IAAI,OAAO;AAC9D,QAAM,eAAe,+BAA+B,KAAK;AACzD,QAAM,WAAkC;AAAA,IACtC;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO,WAAW,aAAa,UAAU,IAAI;AAAA,IAC7C,QAAQ,KAAK,MAAM;AAAA,IACnB,YAAY;AAAA,MACV,aAAa;AAAA,MACb;AAAA,MACA,SAAS,KAAK,MAAM;AAAA,MACpB,QAAQ,KAAK,MAAM;AAAA,MACnB,UAAU,KAAK,MAAM;AAAA,MACrB,aAAa,KAAK;AAAA,IACpB;AAAA,EACF;AACA,SAAO,EAAE,GAAG,UAAU,QAAQ,YAAY,WAAW,aAAa,WAAW,SAAS;AACxF;AAGA,SAAS,WACP,UACA,MACyB;AACzB,SAAO;AAAA,IACL,GAAG;AAAA,IACH,GAAG,KAAK,MAAM;AAAA,IACd,GAAG,KAAK,MAAM;AAAA,IACd,GAAG,KAAK,MAAM;AAAA,EAChB;AACF;","names":["entry","registration","snapshot"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experiences-react",
3
- "version": "0.14.6",
3
+ "version": "0.15.0",
4
4
  "description": "React adapter for Contentful Experiences — Server and Client renderers over a PortableRenderPlan",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -14,6 +14,12 @@
14
14
  "default": "./dist/index.js"
15
15
  }
16
16
  },
17
+ "./custom-renderer": {
18
+ "import": {
19
+ "types": "./dist/custom-renderer.d.ts",
20
+ "default": "./dist/custom-renderer.js"
21
+ }
22
+ },
17
23
  "./package.json": "./package.json"
18
24
  },
19
25
  "files": [
@@ -31,7 +37,7 @@
31
37
  },
32
38
  "dependencies": {
33
39
  "@contentful/experiences-client": "0.10.6",
34
- "@contentful/experiences-runtime": "0.2.6",
40
+ "@contentful/experiences-runtime": "0.3.0",
35
41
  "@contentful/experiences-sdk-core": "0.13.5",
36
42
  "@contentful/experiences-design": "0.13.4",
37
43
  "@contentful/experiences-live-preview": "0.7.6"