@contentful/experiences-react 0.7.10 → 0.7.12
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 +20 -0
- package/dist/client-renderer.d.ts +5 -2
- package/dist/client-renderer.js +24 -6
- package/dist/client-renderer.js.map +1 -1
- package/dist/component-error-boundary.d.ts +65 -0
- package/dist/component-error-boundary.js +31 -0
- package/dist/component-error-boundary.js.map +1 -0
- package/dist/component-error.d.ts +12 -0
- package/dist/component-error.js +34 -0
- package/dist/component-error.js.map +1 -0
- package/dist/debug-experience.d.ts +10 -1
- package/dist/debug-experience.js +10 -3
- package/dist/debug-experience.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/nodes-renderer.d.ts +15 -2
- package/dist/nodes-renderer.js +47 -9
- package/dist/nodes-renderer.js.map +1 -1
- package/dist/server-renderer.d.ts +5 -2
- package/dist/server-renderer.js +19 -13
- package/dist/server-renderer.js.map +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
## 0.7.12 (2026-08-28)
|
|
2
|
+
|
|
3
|
+
### 🚀 Features
|
|
4
|
+
|
|
5
|
+
- error handling and sensible fallbacks for non-happy-path rendering [AIS-364] ([#156](https://github.com/contentful/experiences/pull/156))
|
|
6
|
+
|
|
7
|
+
### 🧱 Updated Dependencies
|
|
8
|
+
|
|
9
|
+
- Updated client to 0.3.11
|
|
10
|
+
- Updated design to 0.7.11
|
|
11
|
+
- Updated core to 0.7.11
|
|
12
|
+
|
|
13
|
+
## 0.7.11 (2026-08-27)
|
|
14
|
+
|
|
15
|
+
### 🧱 Updated Dependencies
|
|
16
|
+
|
|
17
|
+
- Updated client to 0.3.10
|
|
18
|
+
- Updated design to 0.7.10
|
|
19
|
+
- Updated core to 0.7.10
|
|
20
|
+
|
|
1
21
|
## 0.7.10 (2026-08-26)
|
|
2
22
|
|
|
3
23
|
### 🧱 Updated Dependencies
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
2
|
import { PortableRenderPlan } from '@contentful/experiences-sdk-core';
|
|
3
|
-
import { RenderUnknown } from './nodes-renderer.js';
|
|
3
|
+
import { RenderUnknown, RenderError } from './nodes-renderer.js';
|
|
4
4
|
import { Config } from './types.js';
|
|
5
|
+
import './component-error.js';
|
|
5
6
|
import './missing-component.js';
|
|
6
7
|
|
|
7
8
|
interface ClientExperienceRendererProps {
|
|
@@ -21,7 +22,9 @@ interface ClientExperienceRendererProps {
|
|
|
21
22
|
*/
|
|
22
23
|
debug?: boolean;
|
|
23
24
|
renderUnknown?: RenderUnknown;
|
|
25
|
+
/** Override the fallback rendered when a registered component throws. */
|
|
26
|
+
renderError?: RenderError;
|
|
24
27
|
}
|
|
25
|
-
declare function ClientExperienceRenderer({ experience, config, initialViewportId, metadata, debug, renderUnknown, }: ClientExperienceRendererProps): ReactNode;
|
|
28
|
+
declare function ClientExperienceRenderer({ experience, config, initialViewportId, metadata, debug, renderUnknown, renderError, }: ClientExperienceRendererProps): ReactNode;
|
|
26
29
|
|
|
27
30
|
export { ClientExperienceRenderer, type ClientExperienceRendererProps };
|
package/dist/client-renderer.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useState } from "react";
|
|
4
|
+
import { ComponentError } from "./component-error";
|
|
5
|
+
import { DiagnosticReporterContext } from "./component-error-boundary";
|
|
3
6
|
import { DebugExperience } from "./debug-experience";
|
|
4
7
|
import { ExperienceProvider } from "./context";
|
|
5
8
|
import { MissingComponent } from "./missing-component";
|
|
@@ -22,10 +25,17 @@ function ClientExperienceRenderer({
|
|
|
22
25
|
initialViewportId,
|
|
23
26
|
metadata,
|
|
24
27
|
debug = false,
|
|
25
|
-
renderUnknown = MissingComponent
|
|
28
|
+
renderUnknown = MissingComponent,
|
|
29
|
+
renderError = ComponentError
|
|
26
30
|
}) {
|
|
31
|
+
const [renderDiagnostics, setRenderDiagnostics] = useState([]);
|
|
32
|
+
const onDiagnostic = useCallback((error) => {
|
|
33
|
+
queueMicrotask(() => {
|
|
34
|
+
setRenderDiagnostics((prev) => [...prev, error]);
|
|
35
|
+
});
|
|
36
|
+
}, []);
|
|
37
|
+
const { activeViewportIndex } = useActiveViewport(experience?.viewports ?? [], initialViewportId);
|
|
27
38
|
if (!experience) return null;
|
|
28
|
-
const { activeViewportIndex } = useActiveViewport(experience.viewports, initialViewportId);
|
|
29
39
|
const contextViewports = experience.viewports.map((v) => ({ ...v }));
|
|
30
40
|
const activeViewport = { ...experience.viewports[activeViewportIndex] ?? FALLBACK_VIEWPORT };
|
|
31
41
|
const renderContext = {
|
|
@@ -37,8 +47,7 @@ function ClientExperienceRenderer({
|
|
|
37
47
|
activeViewportIndex
|
|
38
48
|
};
|
|
39
49
|
return /* @__PURE__ */ jsxs(ExperienceProvider, { value: renderContext, children: [
|
|
40
|
-
|
|
41
|
-
/* @__PURE__ */ jsx(
|
|
50
|
+
/* @__PURE__ */ jsx(DiagnosticReporterContext.Provider, { value: onDiagnostic, children: /* @__PURE__ */ jsx(
|
|
42
51
|
NodesRenderer,
|
|
43
52
|
{
|
|
44
53
|
nodes: experience.nodes,
|
|
@@ -46,9 +55,18 @@ function ClientExperienceRenderer({
|
|
|
46
55
|
viewports: experience.viewports,
|
|
47
56
|
activeViewportIndex,
|
|
48
57
|
fallbackViewportIndex: experience.fallbackViewportIndex,
|
|
49
|
-
renderUnknown
|
|
58
|
+
renderUnknown,
|
|
59
|
+
renderError,
|
|
60
|
+
onDiagnostic
|
|
50
61
|
}
|
|
51
|
-
)
|
|
62
|
+
) }),
|
|
63
|
+
debug ? /* @__PURE__ */ jsx(
|
|
64
|
+
DebugExperience,
|
|
65
|
+
{
|
|
66
|
+
experience,
|
|
67
|
+
errors: [...experience.diagnostics ?? [], ...renderDiagnostics]
|
|
68
|
+
}
|
|
69
|
+
) : null
|
|
52
70
|
] });
|
|
53
71
|
}
|
|
54
72
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client-renderer.tsx"],"sourcesContent":["/*\n * Client Experience renderer. First paint matches the server renderer; after\n * hydration `useActiveViewport` takes over via `matchMedia`. Server-safe: the\n * hook returns the seeded index and registers no listeners when there's no\n * window, so SSR output matches `<ServerExperienceRenderer>`.\n */\n\n'use client';\n\nimport type
|
|
1
|
+
{"version":3,"sources":["../src/client-renderer.tsx"],"sourcesContent":["/*\n * Client Experience renderer. First paint matches the server renderer; after\n * hydration `useActiveViewport` takes over via `matchMedia`. Server-safe: the\n * hook returns the seeded index and registers no listeners when there's no\n * window, so SSR output matches `<ServerExperienceRenderer>`.\n */\n\n'use client';\n\nimport { useCallback, useState, type ReactNode } from 'react';\n\nimport type {\n ExperienceContext,\n PortableRenderPlan,\n ViewportDef,\n} from '@contentful/experiences-sdk-core';\n\nimport { ComponentError } from './component-error';\nimport { DiagnosticReporterContext } from './component-error-boundary';\nimport { DebugExperience } from './debug-experience';\nimport { ExperienceProvider } from './context';\nimport { MissingComponent } from './missing-component';\nimport { NodesRenderer, type RenderError, type RenderUnknown } from './nodes-renderer';\nimport type { Config, RenderContext } from './types';\nimport { useActiveViewport } from './use-active-viewport';\n\nconst DEFAULT_CONTEXT: ExperienceContext = {\n debug: false,\n metadata: {},\n viewports: [],\n};\n\nconst FALLBACK_VIEWPORT: ViewportDef = {\n id: '_',\n query: '*',\n displayName: 'Default',\n previewSize: '100%',\n};\n\nexport interface ClientExperienceRendererProps {\n experience: PortableRenderPlan | null | undefined;\n config: Config;\n /** Initial viewport seed; should match what the server-rendered output used. */\n initialViewportId?: string;\n /**\n * Arbitrary per-render metadata, readable by descendants via\n * `useExperience()` and by resolvers via `ctx.experience.metadata`.\n */\n metadata?: Record<string, unknown>;\n /**\n * Observability switch. When on: renders the visible missing-component box,\n * turns the default `renderUnknown` fallback into the debug component, and\n * auto-mounts `<DebugExperience>` after the tree.\n */\n debug?: boolean;\n renderUnknown?: RenderUnknown;\n /** Override the fallback rendered when a registered component throws. */\n renderError?: RenderError;\n}\n\nexport function ClientExperienceRenderer({\n experience,\n config,\n initialViewportId,\n metadata,\n debug = false,\n renderUnknown = MissingComponent,\n renderError = ComponentError,\n}: ClientExperienceRendererProps): ReactNode {\n const [renderDiagnostics, setRenderDiagnostics] = useState<Error[]>([]);\n // A `setState` updater rather than the SSR renderer's plain-array push: this\n // tree stays mounted and interactive, so a component that throws well after\n // first paint (a later re-render, an event handler) must still make\n // `<DebugExperience>` re-render with the new diagnostic — a mutated array\n // wouldn't trigger that.\n //\n // `queueMicrotask` defers the actual `setState` call: most of these\n // diagnostics (component-not-registered, malformed-slot,\n // experience-template-not-registered) are reported synchronously from\n // *inside* NodeRenderer's own render body, i.e. while React is mid-render\n // of a descendant — calling this component's setState right there is\n // exactly React's \"Cannot update a component while rendering a different\n // component\" anti-pattern. Escaping to a microtask (same \"break the\n // synchronous call chain\" rationale as core's resolveData deferral)\n // schedules the update for after the current render/commit fully unwinds.\n // `component-render-error`, reported from `componentDidCatch`, is already\n // commit-phase-safe — deferring it too is harmless and keeps one code path.\n // This is client-only (NodeRenderer's own onDiagnostic call is unchanged\n // and stays synchronous — see nodes-renderer.tsx for why SSR needs that):\n // `ServerExperienceRenderer` threads a plain synchronous array-push\n // closure instead of this setState-backed one, so SSR is unaffected.\n const onDiagnostic = useCallback((error: Error) => {\n queueMicrotask(() => {\n setRenderDiagnostics((prev) => [...prev, error]);\n });\n }, []);\n\n const { activeViewportIndex } = useActiveViewport(experience?.viewports ?? [], initialViewportId);\n if (!experience) return null;\n // Copy so the context shares no object identity with the plan arrays — see\n // the note in `server-renderer.tsx`.\n const contextViewports = experience.viewports.map((v) => ({ ...v }));\n const activeViewport = { ...(experience.viewports[activeViewportIndex] ?? FALLBACK_VIEWPORT) };\n\n const renderContext: RenderContext = {\n ...DEFAULT_CONTEXT,\n debug,\n metadata: { ...DEFAULT_CONTEXT.metadata, ...(metadata ?? {}) },\n viewports: contextViewports,\n activeViewport,\n activeViewportIndex,\n };\n\n return (\n <ExperienceProvider value={renderContext}>\n {/*\n Established here, not in `ServerExperienceRenderer`: this Provider\n (and the closure it carries) never crosses a Server→Client boundary\n because everything from `ClientExperienceRenderer` down is already\n client-rendered. `ComponentErrorBoundary` reads it via `contextType`\n to report a render-time throw without receiving a callback prop —\n see component-error-boundary.tsx for why that specifically matters.\n */}\n <DiagnosticReporterContext.Provider value={onDiagnostic}>\n <NodesRenderer\n nodes={experience.nodes}\n config={config}\n viewports={experience.viewports}\n activeViewportIndex={activeViewportIndex}\n fallbackViewportIndex={experience.fallbackViewportIndex}\n renderUnknown={renderUnknown}\n renderError={renderError}\n onDiagnostic={onDiagnostic}\n />\n </DiagnosticReporterContext.Provider>\n {debug ? (\n <DebugExperience\n experience={experience}\n errors={[...(experience.diagnostics ?? []), ...renderDiagnostics]}\n />\n ) : null}\n </ExperienceProvider>\n );\n}\n"],"mappings":";AAkHI,SAUI,KAVJ;AAzGJ,SAAS,aAAa,gBAAgC;AAQtD,SAAS,sBAAsB;AAC/B,SAAS,iCAAiC;AAC1C,SAAS,uBAAuB;AAChC,SAAS,0BAA0B;AACnC,SAAS,wBAAwB;AACjC,SAAS,qBAA2D;AAEpE,SAAS,yBAAyB;AAElC,MAAM,kBAAqC;AAAA,EACzC,OAAO;AAAA,EACP,UAAU,CAAC;AAAA,EACX,WAAW,CAAC;AACd;AAEA,MAAM,oBAAiC;AAAA,EACrC,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,aAAa;AAAA,EACb,aAAa;AACf;AAuBO,SAAS,yBAAyB;AAAA,EACvC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR,gBAAgB;AAAA,EAChB,cAAc;AAChB,GAA6C;AAC3C,QAAM,CAAC,mBAAmB,oBAAoB,IAAI,SAAkB,CAAC,CAAC;AAsBtE,QAAM,eAAe,YAAY,CAAC,UAAiB;AACjD,mBAAe,MAAM;AACnB,2BAAqB,CAAC,SAAS,CAAC,GAAG,MAAM,KAAK,CAAC;AAAA,IACjD,CAAC;AAAA,EACH,GAAG,CAAC,CAAC;AAEL,QAAM,EAAE,oBAAoB,IAAI,kBAAkB,YAAY,aAAa,CAAC,GAAG,iBAAiB;AAChG,MAAI,CAAC,WAAY,QAAO;AAGxB,QAAM,mBAAmB,WAAW,UAAU,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE;AACnE,QAAM,iBAAiB,EAAE,GAAI,WAAW,UAAU,mBAAmB,KAAK,kBAAmB;AAE7F,QAAM,gBAA+B;AAAA,IACnC,GAAG;AAAA,IACH;AAAA,IACA,UAAU,EAAE,GAAG,gBAAgB,UAAU,GAAI,YAAY,CAAC,EAAG;AAAA,IAC7D,WAAW;AAAA,IACX;AAAA,IACA;AAAA,EACF;AAEA,SACE,qBAAC,sBAAmB,OAAO,eASzB;AAAA,wBAAC,0BAA0B,UAA1B,EAAmC,OAAO,cACzC;AAAA,MAAC;AAAA;AAAA,QACC,OAAO,WAAW;AAAA,QAClB;AAAA,QACA,WAAW,WAAW;AAAA,QACtB;AAAA,QACA,uBAAuB,WAAW;AAAA,QAClC;AAAA,QACA;AAAA,QACA;AAAA;AAAA,IACF,GACF;AAAA,IACC,QACC;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,QAAQ,CAAC,GAAI,WAAW,eAAe,CAAC,GAAI,GAAG,iBAAiB;AAAA;AAAA,IAClE,IACE;AAAA,KACN;AAEJ;","names":[]}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { Component, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
declare const DiagnosticReporterContext: react.Context<((error: Error) => void) | null>;
|
|
5
|
+
interface ComponentErrorBoundaryProps {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
fallback: ReactNode;
|
|
8
|
+
/** Plain, serializable identity — never a callback; see the file doc comment. */
|
|
9
|
+
componentId: string;
|
|
10
|
+
kind: string;
|
|
11
|
+
nodeId?: string;
|
|
12
|
+
}
|
|
13
|
+
interface ComponentErrorBoundaryState {
|
|
14
|
+
hasError: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Per-node isolation for a customer component that throws during render. A
|
|
18
|
+
* class component is the only mechanism React offers for this — there is no
|
|
19
|
+
* hook equivalent.
|
|
20
|
+
*
|
|
21
|
+
* `getDerivedStateFromError`/`componentDidCatch` do NOT catch at all during
|
|
22
|
+
* React's SSR — neither the legacy `renderToString`/`renderToStaticMarkup`
|
|
23
|
+
* NOR the modern streaming `renderToPipeableStream`/`renderToReadableStream`
|
|
24
|
+
* (the one Next.js's App Router actually uses) run the class-error-boundary
|
|
25
|
+
* machinery server-side. What the modern streaming renderer DOES support is
|
|
26
|
+
* Suspense-based recovery: if a `<Suspense>` boundary's content throws during
|
|
27
|
+
* the server walk, Fizz emits that Suspense's `fallback` as the initial HTML
|
|
28
|
+
* and defers real rendering of that subtree to the client after hydration —
|
|
29
|
+
* "switch to client rendering" in React's own error message. Wrapping
|
|
30
|
+
* `children` in an internal `<Suspense fallback={fallback}>` (same fallback
|
|
31
|
+
* either way) is what makes this component degrade gracefully server-side:
|
|
32
|
+
* Fizz isolates the failing node without our own `hasError` branch ever
|
|
33
|
+
* running server-side. Once the client hydrates and re-executes that
|
|
34
|
+
* Suspense boundary's content, if the throw repeats, the client fiber
|
|
35
|
+
* reconciler *does* run `getDerivedStateFromError` and re-renders with
|
|
36
|
+
* `hasError: true` — siblings render normally because Fizz isolates
|
|
37
|
+
* per-Suspense-boundary.
|
|
38
|
+
*
|
|
39
|
+
* Net effect: the diagnostic fires only client-side — after hydration for an
|
|
40
|
+
* SSR-rendered page, or immediately for a client-only render.
|
|
41
|
+
* `ServerExperienceRenderer`'s own SSR-time diagnostics list can therefore
|
|
42
|
+
* never contain a `component-render-error` entry; only
|
|
43
|
+
* `ClientExperienceRenderer`'s reactive collector can, which is exactly why
|
|
44
|
+
* that collector is `useState`-based rather than a plain array. See the
|
|
45
|
+
* README's error-handling section.
|
|
46
|
+
*
|
|
47
|
+
* The legacy synchronous `renderToString`/`renderToStaticMarkup` APIs also
|
|
48
|
+
* honor the Suspense `fallback` for a thrown Error, not just a thrown
|
|
49
|
+
* thenable — so a customer calling those directly still gets the graceful
|
|
50
|
+
* fallback and sibling isolation. The difference from the streaming renderer
|
|
51
|
+
* is that legacy output carries no hydration data and gets no client-side
|
|
52
|
+
* retry: the fallback markup is permanent, and — same as above — no
|
|
53
|
+
* diagnostic is ever recorded for that render, since our `hasError` branch
|
|
54
|
+
* still never runs server-side.
|
|
55
|
+
*/
|
|
56
|
+
declare class ComponentErrorBoundary extends Component<ComponentErrorBoundaryProps, ComponentErrorBoundaryState> {
|
|
57
|
+
static contextType: react.Context<((error: Error) => void) | null>;
|
|
58
|
+
context: React.ContextType<typeof DiagnosticReporterContext>;
|
|
59
|
+
state: ComponentErrorBoundaryState;
|
|
60
|
+
static getDerivedStateFromError(): ComponentErrorBoundaryState;
|
|
61
|
+
componentDidCatch(error: unknown): void;
|
|
62
|
+
render(): ReactNode;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export { ComponentErrorBoundary, DiagnosticReporterContext };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { Component, Suspense, createContext } from "react";
|
|
4
|
+
const DiagnosticReporterContext = createContext(null);
|
|
5
|
+
class ComponentErrorBoundary extends Component {
|
|
6
|
+
static contextType = DiagnosticReporterContext;
|
|
7
|
+
state = { hasError: false };
|
|
8
|
+
static getDerivedStateFromError() {
|
|
9
|
+
return { hasError: true };
|
|
10
|
+
}
|
|
11
|
+
componentDidCatch(error) {
|
|
12
|
+
const { componentId, kind, nodeId } = this.props;
|
|
13
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
14
|
+
const message = `Component "${componentId}" (${kind}${nodeId ? `, node "${nodeId}"` : ""}) threw while rendering: ${reason}. Rendering the error fallback instead of crashing the surrounding tree.`;
|
|
15
|
+
if (typeof console !== "undefined") {
|
|
16
|
+
console.warn(`[@contentful/experiences-react] ${message}`);
|
|
17
|
+
}
|
|
18
|
+
this.context?.(new Error(message, { cause: error instanceof Error ? error : void 0 }));
|
|
19
|
+
}
|
|
20
|
+
render() {
|
|
21
|
+
if (this.state.hasError) {
|
|
22
|
+
return this.props.fallback;
|
|
23
|
+
}
|
|
24
|
+
return /* @__PURE__ */ jsx(Suspense, { fallback: this.props.fallback, children: this.props.children });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export {
|
|
28
|
+
ComponentErrorBoundary,
|
|
29
|
+
DiagnosticReporterContext
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=component-error-boundary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/component-error-boundary.tsx"],"sourcesContent":["/*\n * Split into its own file, `'use client'`-marked, because it's a class\n * component: Next.js's RSC bundler statically rejects a class component\n * import from a file with no directive (\"You're importing a class component.\n * It only works in a Client Component...\"), even though the class itself\n * renders fine under every SSR path this SDK supports. `nodes-renderer.tsx`\n * stays directive-free so it can still be imported from `server-renderer.tsx`\n * (a genuine Server Component); Server Components importing Client\n * Components is the normal RSC pattern, so this split costs nothing.\n *\n * Reporting goes through `DiagnosticReporterContext`, not a prop, for a\n * second RSC reason beyond the class-component one above: `NodeRenderer`\n * runs as part of the SERVER render when reached from\n * `ServerExperienceRenderer` (a real Server Component with no directive), and\n * React refuses to serialize a plain closure passed as a Client Component\n * prop from there (\"Event handlers cannot be passed to Client Component\n * props\"). Only `ClientExperienceRenderer` establishes this context (with a\n * `Provider` element that, being entirely within its own already-client-\n * rendered tree, never crosses that boundary); `context` reads `null` under\n * `ServerExperienceRenderer`, which is fine because `componentDidCatch` never\n * runs there anyway (see the class doc comment).\n */\n'use client';\n\nimport { Component, Suspense, createContext, type ReactNode } from 'react';\n\nexport const DiagnosticReporterContext = createContext<((error: Error) => void) | null>(null);\n\ninterface ComponentErrorBoundaryProps {\n children: ReactNode;\n fallback: ReactNode;\n /** Plain, serializable identity — never a callback; see the file doc comment. */\n componentId: string;\n kind: string;\n nodeId?: string;\n}\n\ninterface ComponentErrorBoundaryState {\n hasError: boolean;\n}\n\n/**\n * Per-node isolation for a customer component that throws during render. A\n * class component is the only mechanism React offers for this — there is no\n * hook equivalent.\n *\n * `getDerivedStateFromError`/`componentDidCatch` do NOT catch at all during\n * React's SSR — neither the legacy `renderToString`/`renderToStaticMarkup`\n * NOR the modern streaming `renderToPipeableStream`/`renderToReadableStream`\n * (the one Next.js's App Router actually uses) run the class-error-boundary\n * machinery server-side. What the modern streaming renderer DOES support is\n * Suspense-based recovery: if a `<Suspense>` boundary's content throws during\n * the server walk, Fizz emits that Suspense's `fallback` as the initial HTML\n * and defers real rendering of that subtree to the client after hydration —\n * \"switch to client rendering\" in React's own error message. Wrapping\n * `children` in an internal `<Suspense fallback={fallback}>` (same fallback\n * either way) is what makes this component degrade gracefully server-side:\n * Fizz isolates the failing node without our own `hasError` branch ever\n * running server-side. Once the client hydrates and re-executes that\n * Suspense boundary's content, if the throw repeats, the client fiber\n * reconciler *does* run `getDerivedStateFromError` and re-renders with\n * `hasError: true` — siblings render normally because Fizz isolates\n * per-Suspense-boundary.\n *\n * Net effect: the diagnostic fires only client-side — after hydration for an\n * SSR-rendered page, or immediately for a client-only render.\n * `ServerExperienceRenderer`'s own SSR-time diagnostics list can therefore\n * never contain a `component-render-error` entry; only\n * `ClientExperienceRenderer`'s reactive collector can, which is exactly why\n * that collector is `useState`-based rather than a plain array. See the\n * README's error-handling section.\n *\n * The legacy synchronous `renderToString`/`renderToStaticMarkup` APIs also\n * honor the Suspense `fallback` for a thrown Error, not just a thrown\n * thenable — so a customer calling those directly still gets the graceful\n * fallback and sibling isolation. The difference from the streaming renderer\n * is that legacy output carries no hydration data and gets no client-side\n * retry: the fallback markup is permanent, and — same as above — no\n * diagnostic is ever recorded for that render, since our `hasError` branch\n * still never runs server-side.\n */\nexport class ComponentErrorBoundary extends Component<\n ComponentErrorBoundaryProps,\n ComponentErrorBoundaryState\n> {\n static override contextType = DiagnosticReporterContext;\n declare context: React.ContextType<typeof DiagnosticReporterContext>;\n\n override state: ComponentErrorBoundaryState = { hasError: false };\n\n static getDerivedStateFromError(): ComponentErrorBoundaryState {\n return { hasError: true };\n }\n\n override componentDidCatch(error: unknown): void {\n const { componentId, kind, nodeId } = this.props;\n const reason = error instanceof Error ? error.message : String(error);\n const message =\n `Component \"${componentId}\" (${kind}${nodeId ? `, node \"${nodeId}\"` : ''}) threw ` +\n `while rendering: ${reason}. Rendering the error fallback instead of crashing the ` +\n `surrounding tree.`;\n if (typeof console !== 'undefined') {\n console.warn(`[@contentful/experiences-react] ${message}`);\n }\n this.context?.(new Error(message, { cause: error instanceof Error ? error : undefined }));\n }\n\n override render(): ReactNode {\n if (this.state.hasError) {\n return this.props.fallback;\n }\n return <Suspense fallback={this.props.fallback}>{this.props.children}</Suspense>;\n }\n}\n"],"mappings":";AA+GW;AAvFX,SAAS,WAAW,UAAU,qBAAqC;AAE5D,MAAM,4BAA4B,cAA+C,IAAI;AAuDrF,MAAM,+BAA+B,UAG1C;AAAA,EACA,OAAgB,cAAc;AAAA,EAGrB,QAAqC,EAAE,UAAU,MAAM;AAAA,EAEhE,OAAO,2BAAwD;AAC7D,WAAO,EAAE,UAAU,KAAK;AAAA,EAC1B;AAAA,EAES,kBAAkB,OAAsB;AAC/C,UAAM,EAAE,aAAa,MAAM,OAAO,IAAI,KAAK;AAC3C,UAAM,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACpE,UAAM,UACJ,cAAc,WAAW,MAAM,IAAI,GAAG,SAAS,WAAW,MAAM,MAAM,EAAE,4BACpD,MAAM;AAE5B,QAAI,OAAO,YAAY,aAAa;AAClC,cAAQ,KAAK,mCAAmC,OAAO,EAAE;AAAA,IAC3D;AACA,SAAK,UAAU,IAAI,MAAM,SAAS,EAAE,OAAO,iBAAiB,QAAQ,QAAQ,OAAU,CAAC,CAAC;AAAA,EAC1F;AAAA,EAES,SAAoB;AAC3B,QAAI,KAAK,MAAM,UAAU;AACvB,aAAO,KAAK,MAAM;AAAA,IACpB;AACA,WAAO,oBAAC,YAAS,UAAU,KAAK,MAAM,UAAW,eAAK,MAAM,UAAS;AAAA,EACvE;AACF;","names":[]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
interface ComponentErrorProps {
|
|
4
|
+
componentId: string;
|
|
5
|
+
/** Optional — only present when the payload supplied an id for this node. */
|
|
6
|
+
nodeId?: string;
|
|
7
|
+
/** The caught error's message, when available. */
|
|
8
|
+
message?: string;
|
|
9
|
+
}
|
|
10
|
+
declare function ComponentError({ componentId, nodeId, message }: ComponentErrorProps): ReactNode;
|
|
11
|
+
|
|
12
|
+
export { ComponentError, type ComponentErrorProps };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useExperience } from "./context";
|
|
4
|
+
function ComponentError({ componentId, nodeId, message }) {
|
|
5
|
+
const { debug } = useExperience();
|
|
6
|
+
if (!debug) return null;
|
|
7
|
+
return /* @__PURE__ */ jsxs(
|
|
8
|
+
"div",
|
|
9
|
+
{
|
|
10
|
+
style: {
|
|
11
|
+
border: "2px solid #b91c1c",
|
|
12
|
+
padding: "1rem",
|
|
13
|
+
color: "#b91c1c",
|
|
14
|
+
background: "#fff",
|
|
15
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
16
|
+
fontSize: "0.8125rem"
|
|
17
|
+
},
|
|
18
|
+
"data-experiences-render-error": componentId,
|
|
19
|
+
children: [
|
|
20
|
+
/* @__PURE__ */ jsxs("strong", { children: [
|
|
21
|
+
"Component \u2018",
|
|
22
|
+
componentId,
|
|
23
|
+
"\u2019 threw while rendering"
|
|
24
|
+
] }),
|
|
25
|
+
/* @__PURE__ */ jsx("p", { style: { margin: "0.5rem 0" }, children: "This component is registered but threw during render. Rendering this fallback instead of crashing the surrounding tree." }),
|
|
26
|
+
/* @__PURE__ */ jsx("pre", { style: { margin: 0, whiteSpace: "pre-wrap" }, children: JSON.stringify({ componentId, nodeId: nodeId ?? null, message: message ?? null }, null, 2) })
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
ComponentError
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=component-error.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/component-error.tsx"],"sourcesContent":["/*\n * Default fallback rendered when a registered component throws while\n * rendering. Sibling of `MissingComponent` — same debug-gated behavior\n * (visible box in debug mode, silent null otherwise), visually distinct\n * (different color/label) so a customer can tell \"not registered\" apart from\n * \"registered but threw\" at a glance. The diagnostic + console.warn for this\n * failure mode are recorded once, at the error-boundary call site in\n * `nodes-renderer.tsx` — not here — so they still fire even when a customer\n * overrides `renderError` with their own fallback.\n */\n'use client';\n\nimport type { ReactNode } from 'react';\n\nimport { useExperience } from './context';\n\nexport interface ComponentErrorProps {\n componentId: string;\n /** Optional — only present when the payload supplied an id for this node. */\n nodeId?: string;\n /** The caught error's message, when available. */\n message?: string;\n}\n\nexport function ComponentError({ componentId, nodeId, message }: ComponentErrorProps): ReactNode {\n const { debug } = useExperience();\n if (!debug) return null;\n return (\n <div\n style={{\n border: '2px solid #b91c1c',\n padding: '1rem',\n color: '#b91c1c',\n background: '#fff',\n fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',\n fontSize: '0.8125rem',\n }}\n data-experiences-render-error={componentId}\n >\n <strong>Component ‘{componentId}’ threw while rendering</strong>\n <p style={{ margin: '0.5rem 0' }}>\n This component is registered but threw during render. Rendering this fallback instead of\n crashing the surrounding tree.\n </p>\n <pre style={{ margin: 0, whiteSpace: 'pre-wrap' }}>\n {JSON.stringify({ componentId, nodeId: nodeId ?? null, message: message ?? null }, null, 2)}\n </pre>\n </div>\n );\n}\n"],"mappings":";AAuCM,SACA,KADA;AAzBN,SAAS,qBAAqB;AAUvB,SAAS,eAAe,EAAE,aAAa,QAAQ,QAAQ,GAAmC;AAC/F,QAAM,EAAE,MAAM,IAAI,cAAc;AAChC,MAAI,CAAC,MAAO,QAAO;AACnB,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAO;AAAA,QACL,QAAQ;AAAA,QACR,SAAS;AAAA,QACT,OAAO;AAAA,QACP,YAAY;AAAA,QACZ,YAAY;AAAA,QACZ,UAAU;AAAA,MACZ;AAAA,MACA,iCAA+B;AAAA,MAE/B;AAAA,6BAAC,YAAO;AAAA;AAAA,UAAkB;AAAA,UAAY;AAAA,WAA6B;AAAA,QACnE,oBAAC,OAAE,OAAO,EAAE,QAAQ,WAAW,GAAG,qIAGlC;AAAA,QACA,oBAAC,SAAI,OAAO,EAAE,QAAQ,GAAG,YAAY,WAAW,GAC7C,eAAK,UAAU,EAAE,aAAa,QAAQ,UAAU,MAAM,SAAS,WAAW,KAAK,GAAG,MAAM,CAAC,GAC5F;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
@@ -6,7 +6,16 @@ interface DebugExperienceProps {
|
|
|
6
6
|
experience: PortableRenderPlan;
|
|
7
7
|
/** Start expanded. Defaults to collapsed to stay out of the way. */
|
|
8
8
|
defaultOpen?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Resolve-time + render-time diagnostics for this render, merged by the
|
|
11
|
+
* caller (`ServerExperienceRenderer` / `ClientExperienceRenderer`). Plain
|
|
12
|
+
* `Error`s — the two cases that wrap a real caught exception
|
|
13
|
+
* (`component-render-error`, `resolve-data-failed`) set `.cause` to the
|
|
14
|
+
* original error, inspectable via `console.error` or `error.cause.stack`.
|
|
15
|
+
* Defaults to `[]` for a manually-mounted `<DebugExperience>`.
|
|
16
|
+
*/
|
|
17
|
+
errors?: Error[];
|
|
9
18
|
}
|
|
10
|
-
declare function DebugExperience({ experience, defaultOpen, }: DebugExperienceProps): ReactNode;
|
|
19
|
+
declare function DebugExperience({ experience, defaultOpen, errors, }: DebugExperienceProps): ReactNode;
|
|
11
20
|
|
|
12
21
|
export { DebugExperience, type DebugExperienceProps };
|
package/dist/debug-experience.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
function DebugExperience({
|
|
3
3
|
experience,
|
|
4
|
-
defaultOpen = false
|
|
4
|
+
defaultOpen = false,
|
|
5
|
+
errors = []
|
|
5
6
|
}) {
|
|
6
7
|
const nodeCount = experience.nodes.length;
|
|
7
8
|
const templateIds = experience.nodes.filter((node) => node.registration.kind === "experienceTemplate").map((node) => node.registration.id);
|
|
8
9
|
const summary = `Experience debug \u2014 ${nodeCount} top-level node${nodeCount === 1 ? "" : "s"}${templateIds.length ? `, experience template${templateIds.length === 1 ? "" : "s"}: ${templateIds.join(", ")}` : ""}`;
|
|
10
|
+
const open = defaultOpen || errors.length > 0;
|
|
9
11
|
return /* @__PURE__ */ jsxs(
|
|
10
12
|
"details",
|
|
11
13
|
{
|
|
12
|
-
open
|
|
14
|
+
open,
|
|
13
15
|
"data-experiences-debug": true,
|
|
14
16
|
style: {
|
|
15
17
|
margin: "1rem 0",
|
|
@@ -34,6 +36,7 @@ function DebugExperience({
|
|
|
34
36
|
children: summary
|
|
35
37
|
}
|
|
36
38
|
),
|
|
39
|
+
errors.length > 0 ? /* @__PURE__ */ jsx(DiagnosticList, { errors }) : null,
|
|
37
40
|
/* @__PURE__ */ jsx(
|
|
38
41
|
"pre",
|
|
39
42
|
{
|
|
@@ -52,6 +55,9 @@ function DebugExperience({
|
|
|
52
55
|
}
|
|
53
56
|
);
|
|
54
57
|
}
|
|
58
|
+
function DiagnosticList({ errors }) {
|
|
59
|
+
return /* @__PURE__ */ jsx("ul", { "data-experiences-debug-errors": true, children: errors.map((error, index) => /* @__PURE__ */ jsx("li", { children: error.message }, index)) });
|
|
60
|
+
}
|
|
55
61
|
function safeStringify(value) {
|
|
56
62
|
const seen = /* @__PURE__ */ new WeakSet();
|
|
57
63
|
try {
|
|
@@ -69,7 +75,8 @@ function safeStringify(value) {
|
|
|
69
75
|
2
|
|
70
76
|
);
|
|
71
77
|
} catch (err) {
|
|
72
|
-
|
|
78
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
79
|
+
return `[DebugExperience: could not serialize plan \u2014 ${reason}]`;
|
|
73
80
|
}
|
|
74
81
|
}
|
|
75
82
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/debug-experience.tsx"],"sourcesContent":["/*\n * First-party debug panel. Renders the resolved Experience plan as pretty\n * JSON so a customer can see exactly what the SDK interpreted from the payload\n * — node tree, registrations, resolved props, viewports.\n *\n * Two ways to use it:\n * - Auto-mounted by the renderers when `debug` is on (above the tree).\n * - Mounted manually anywhere: `<DebugExperience experience={plan} />`.\n *\n * v1 is just the JSON dump wrapped in a native <details> so it collapses\n * without any client JS (safe in server components). Room to grow into a\n * node-tree explorer, viewport indicator, and resolveData timing panel — kept\n * deliberately small for now.\n *\n * No React hooks here on purpose: it must render in both server and client\n * renderer subtrees.\n */\n\nimport type { ReactNode } from 'react';\n\nimport type { PortableRenderPlan } from '@contentful/experiences-sdk-core';\n\nexport interface DebugExperienceProps {\n /** The resolved plan to inspect (what a renderer receives as `experience`). */\n experience: PortableRenderPlan;\n /** Start expanded. Defaults to collapsed to stay out of the way. */\n defaultOpen?: boolean;\n}\n\nexport function DebugExperience({\n experience,\n defaultOpen = false,\n}: DebugExperienceProps): ReactNode {\n const nodeCount = experience.nodes.length;\n // Coded Experience Templates are ordinary top-level nodes, so name them from\n // the node list — a composite Experience simply has none to report.\n const templateIds = experience.nodes\n .filter((node) => node.registration.kind === 'experienceTemplate')\n .map((node) => node.registration.id);\n const summary = `Experience debug — ${nodeCount} top-level node${nodeCount === 1 ? '' : 's'}${\n templateIds.length\n ? `, experience template${templateIds.length === 1 ? '' : 's'}: ${templateIds.join(', ')}`\n : ''\n }`;\n\n return (\n <details\n open={
|
|
1
|
+
{"version":3,"sources":["../src/debug-experience.tsx"],"sourcesContent":["/*\n * First-party debug panel. Renders the resolved Experience plan as pretty\n * JSON so a customer can see exactly what the SDK interpreted from the payload\n * — node tree, registrations, resolved props, viewports.\n *\n * Two ways to use it:\n * - Auto-mounted by the renderers when `debug` is on (above the tree).\n * - Mounted manually anywhere: `<DebugExperience experience={plan} />`.\n *\n * v1 is just the JSON dump wrapped in a native <details> so it collapses\n * without any client JS (safe in server components). Room to grow into a\n * node-tree explorer, viewport indicator, and resolveData timing panel — kept\n * deliberately small for now.\n *\n * No React hooks here on purpose: it must render in both server and client\n * renderer subtrees.\n */\n\nimport type { ReactNode } from 'react';\n\nimport type { PortableRenderPlan } from '@contentful/experiences-sdk-core';\n\nexport interface DebugExperienceProps {\n /** The resolved plan to inspect (what a renderer receives as `experience`). */\n experience: PortableRenderPlan;\n /** Start expanded. Defaults to collapsed to stay out of the way. */\n defaultOpen?: boolean;\n /**\n * Resolve-time + render-time diagnostics for this render, merged by the\n * caller (`ServerExperienceRenderer` / `ClientExperienceRenderer`). Plain\n * `Error`s — the two cases that wrap a real caught exception\n * (`component-render-error`, `resolve-data-failed`) set `.cause` to the\n * original error, inspectable via `console.error` or `error.cause.stack`.\n * Defaults to `[]` for a manually-mounted `<DebugExperience>`.\n */\n errors?: Error[];\n}\n\nexport function DebugExperience({\n experience,\n defaultOpen = false,\n errors = [],\n}: DebugExperienceProps): ReactNode {\n const nodeCount = experience.nodes.length;\n // Coded Experience Templates are ordinary top-level nodes, so name them from\n // the node list — a composite Experience simply has none to report.\n const templateIds = experience.nodes\n .filter((node) => node.registration.kind === 'experienceTemplate')\n .map((node) => node.registration.id);\n const summary = `Experience debug — ${nodeCount} top-level node${nodeCount === 1 ? '' : 's'}${\n templateIds.length\n ? `, experience template${templateIds.length === 1 ? '' : 's'}: ${templateIds.join(', ')}`\n : ''\n }`;\n\n // Auto-expand whenever there's something to see, even if the caller didn't\n // explicitly ask — a beta customer shouldn't have to know to click into a\n // collapsed panel to discover that something went wrong.\n const open = defaultOpen || errors.length > 0;\n\n return (\n <details\n open={open}\n data-experiences-debug\n style={{\n margin: '1rem 0',\n border: '1px solid #6b7280',\n borderRadius: '6px',\n background: '#0b1021',\n color: '#e2e8f0',\n fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',\n fontSize: '0.75rem',\n overflow: 'hidden',\n }}\n >\n <summary\n style={{\n cursor: 'pointer',\n padding: '0.5rem 0.75rem',\n background: '#111827',\n userSelect: 'none',\n }}\n >\n {summary}\n </summary>\n {errors.length > 0 ? <DiagnosticList errors={errors} /> : null}\n <pre\n style={{\n margin: 0,\n padding: '0.75rem',\n overflow: 'auto',\n maxHeight: '32rem',\n whiteSpace: 'pre-wrap',\n wordBreak: 'break-word',\n }}\n >\n {safeStringify(experience)}\n </pre>\n </details>\n );\n}\n\n// Deliberately unstyled for now — this just needs to make the data visible,\n// not console-only. Visual treatment (grouping, color, counts) can grow later.\nfunction DiagnosticList({ errors }: { errors: Error[] }): ReactNode {\n return (\n <ul data-experiences-debug-errors>\n {errors.map((error, index) => (\n <li key={index}>{error.message}</li>\n ))}\n </ul>\n );\n}\n\n/**\n * JSON.stringify with a circular-reference guard. A resolved plan is plain\n * data, but a customer's `resolveData` could stash a non-serializable value on\n * `props.resolved` — degrade to a placeholder rather than throwing inside a\n * debug panel.\n */\nfunction safeStringify(value: unknown): string {\n const seen = new WeakSet<object>();\n try {\n return JSON.stringify(\n value,\n (_key, val) => {\n if (typeof val === 'object' && val !== null) {\n if (seen.has(val)) return '[Circular]';\n seen.add(val);\n }\n if (typeof val === 'function') return `[Function ${val.name || 'anonymous'}]`;\n if (typeof val === 'undefined') return '[undefined]';\n return val;\n },\n 2\n );\n } catch (err) {\n // `err` isn't guaranteed to be an Error — a getter deep in customer data\n // (a resolveData result, resolved design values) can `throw null` or\n // `throw 'reason'` during JSON.stringify's replacer walk, and this\n // fallback's whole job is to never throw itself.\n const reason = err instanceof Error ? err.message : String(err);\n return `[DebugExperience: could not serialize plan — ${reason}]`;\n }\n}\n"],"mappings":"AA6DI,SAcE,KAdF;AAvBG,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA,cAAc;AAAA,EACd,SAAS,CAAC;AACZ,GAAoC;AAClC,QAAM,YAAY,WAAW,MAAM;AAGnC,QAAM,cAAc,WAAW,MAC5B,OAAO,CAAC,SAAS,KAAK,aAAa,SAAS,oBAAoB,EAChE,IAAI,CAAC,SAAS,KAAK,aAAa,EAAE;AACrC,QAAM,UAAU,2BAAsB,SAAS,kBAAkB,cAAc,IAAI,KAAK,GAAG,GACzF,YAAY,SACR,wBAAwB,YAAY,WAAW,IAAI,KAAK,GAAG,KAAK,YAAY,KAAK,IAAI,CAAC,KACtF,EACN;AAKA,QAAM,OAAO,eAAe,OAAO,SAAS;AAE5C,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,0BAAsB;AAAA,MACtB,OAAO;AAAA,QACL,QAAQ;AAAA,QACR,QAAQ;AAAA,QACR,cAAc;AAAA,QACd,YAAY;AAAA,QACZ,OAAO;AAAA,QACP,YAAY;AAAA,QACZ,UAAU;AAAA,QACV,UAAU;AAAA,MACZ;AAAA,MAEA;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,OAAO;AAAA,cACL,QAAQ;AAAA,cACR,SAAS;AAAA,cACT,YAAY;AAAA,cACZ,YAAY;AAAA,YACd;AAAA,YAEC;AAAA;AAAA,QACH;AAAA,QACC,OAAO,SAAS,IAAI,oBAAC,kBAAe,QAAgB,IAAK;AAAA,QAC1D;AAAA,UAAC;AAAA;AAAA,YACC,OAAO;AAAA,cACL,QAAQ;AAAA,cACR,SAAS;AAAA,cACT,UAAU;AAAA,cACV,WAAW;AAAA,cACX,YAAY;AAAA,cACZ,WAAW;AAAA,YACb;AAAA,YAEC,wBAAc,UAAU;AAAA;AAAA,QAC3B;AAAA;AAAA;AAAA,EACF;AAEJ;AAIA,SAAS,eAAe,EAAE,OAAO,GAAmC;AAClE,SACE,oBAAC,QAAG,iCAA6B,MAC9B,iBAAO,IAAI,CAAC,OAAO,UAClB,oBAAC,QAAgB,gBAAM,WAAd,KAAsB,CAChC,GACH;AAEJ;AAQA,SAAS,cAAc,OAAwB;AAC7C,QAAM,OAAO,oBAAI,QAAgB;AACjC,MAAI;AACF,WAAO,KAAK;AAAA,MACV;AAAA,MACA,CAAC,MAAM,QAAQ;AACb,YAAI,OAAO,QAAQ,YAAY,QAAQ,MAAM;AAC3C,cAAI,KAAK,IAAI,GAAG,EAAG,QAAO;AAC1B,eAAK,IAAI,GAAG;AAAA,QACd;AACA,YAAI,OAAO,QAAQ,WAAY,QAAO,aAAa,IAAI,QAAQ,WAAW;AAC1E,YAAI,OAAO,QAAQ,YAAa,QAAO;AACvC,eAAO;AAAA,MACT;AAAA,MACA;AAAA,IACF;AAAA,EACF,SAAS,KAAK;AAKZ,UAAM,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC9D,WAAO,qDAAgD,MAAM;AAAA,EAC/D;AACF;","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
export { ClientExperienceRenderer, ClientExperienceRendererProps, ClientExperienceRenderer as ExperienceRenderer, ClientExperienceRendererProps as ExperienceRendererProps } from './client-renderer.js';
|
|
2
2
|
export { ServerExperienceRenderer, ServerExperienceRendererProps } from './server-renderer.js';
|
|
3
3
|
export { MissingComponent, MissingComponentProps } from './missing-component.js';
|
|
4
|
+
export { ComponentError, ComponentErrorProps } from './component-error.js';
|
|
4
5
|
export { DebugExperience, DebugExperienceProps } from './debug-experience.js';
|
|
5
6
|
export { UseActiveViewportResult, useActiveViewport } from './use-active-viewport.js';
|
|
6
|
-
export { RenderUnknown } from './nodes-renderer.js';
|
|
7
|
+
export { DiagnosticReporter, RenderError, RenderUnknown } from './nodes-renderer.js';
|
|
7
8
|
export { useContentfulComponent, useContentfulExperienceTemplate, useExperience } from './context.js';
|
|
8
9
|
export { useDesignValues } from './use-design-values.js';
|
|
9
10
|
export { ToCssOptions, toCss } from './design-utils.js';
|
|
10
11
|
export { ComponentConfig, Components, Config, ContentfulComponent, ContentfulExperienceTemplate, ExperienceTemplateConfig, ExperienceTemplateRegistration, ExperienceTemplates, Registration, RenderContext, defineComponent, defineExperienceTemplate, normalizeComponentRegistration, normalizeExperienceTemplateRegistration } from './types.js';
|
|
11
12
|
export { ComponentNode, ComponentRef, DesignPropValue, DesignToken, ExperienceContext, ExperienceNode, ExperiencePayload, ExperienceSys, ExperienceTemplateNode, ExperienceTemplateRef, ManualDesignValue, PortableRegistration, PortableRenderNode, PortableRenderPlan, ResolveContext, ResolveExperienceOptions, ResolveToken, ResolverConfig, ValuesByViewport, ViewportDef, resolveExperience } from '@contentful/experiences-sdk-core';
|
|
12
13
|
export { CSS_PROPERTIES, getValueForViewport, getViewportIndex, isCssProperty, resolveDesignProperties, toCssKey, toCssMediaQuery } from '@contentful/experiences-design';
|
|
13
|
-
export { ClientOptions, ContentfulViewDelivery, ContentfulViewDeliveryClient, CreateClientOptions, DELIVERY_HOST, ExperienceOptions, NotFoundError, PREVIEW_HOST, ResolveOptions, createClient, fetchExperience } from '@contentful/experiences-client';
|
|
14
|
+
export { ClientOptions, ContentfulViewDelivery, ContentfulViewDeliveryClient, CreateClientOptions, DELIVERY_HOST, ExperienceFetchError, ExperienceOptions, NotFoundError, PREVIEW_HOST, ResolveOptions, createClient, fetchExperience } from '@contentful/experiences-client';
|
|
14
15
|
import 'react';
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
} from "./client-renderer";
|
|
5
5
|
import { ServerExperienceRenderer } from "./server-renderer";
|
|
6
6
|
import { MissingComponent } from "./missing-component";
|
|
7
|
+
import { ComponentError } from "./component-error";
|
|
7
8
|
import { DebugExperience } from "./debug-experience";
|
|
8
9
|
import { useActiveViewport } from "./use-active-viewport";
|
|
9
10
|
import { useExperience, useContentfulComponent, useContentfulExperienceTemplate } from "./context";
|
|
@@ -29,6 +30,7 @@ import {
|
|
|
29
30
|
ContentfulViewDelivery,
|
|
30
31
|
ContentfulViewDeliveryClient,
|
|
31
32
|
DELIVERY_HOST,
|
|
33
|
+
ExperienceFetchError,
|
|
32
34
|
NotFoundError,
|
|
33
35
|
PREVIEW_HOST,
|
|
34
36
|
createClient,
|
|
@@ -37,10 +39,12 @@ import {
|
|
|
37
39
|
export {
|
|
38
40
|
CSS_PROPERTIES,
|
|
39
41
|
ClientExperienceRenderer2 as ClientExperienceRenderer,
|
|
42
|
+
ComponentError,
|
|
40
43
|
ContentfulViewDelivery,
|
|
41
44
|
ContentfulViewDeliveryClient,
|
|
42
45
|
DELIVERY_HOST,
|
|
43
46
|
DebugExperience,
|
|
47
|
+
ExperienceFetchError,
|
|
44
48
|
ClientExperienceRenderer as ExperienceRenderer,
|
|
45
49
|
MissingComponent,
|
|
46
50
|
NotFoundError,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/*\n * Public API surface for `@contentful/experiences-react`.\n *\n * Customers add ONLY this package to their app's dependencies. Everything\n * needed to render an Experience — types, the resolver, viewport utilities,\n * the renderer components, the authoring helpers — is re-exported from here.\n * The internal `@contentful/experiences-sdk-core` and\n * `@contentful/experiences-design` packages are workspace-only implementation\n * details; they are not part of the public API.\n *\n * `ExperienceRenderer` is an alias for `ClientExperienceRenderer`; SSR\n * consumers explicitly import `ServerExperienceRenderer`.\n */\n\n// ─── Renderers ─────────────────────────────────────────────────────────────\nexport {\n ClientExperienceRenderer as ExperienceRenderer,\n ClientExperienceRenderer,\n} from './client-renderer';\nexport type { ClientExperienceRendererProps as ExperienceRendererProps } from './client-renderer';\nexport type { ClientExperienceRendererProps } from './client-renderer';\n\nexport { ServerExperienceRenderer } from './server-renderer';\nexport type { ServerExperienceRendererProps } from './server-renderer';\n\nexport { MissingComponent } from './missing-component';\nexport type { MissingComponentProps } from './missing-component';\n\nexport { DebugExperience } from './debug-experience';\nexport type { DebugExperienceProps } from './debug-experience';\n\nexport { useActiveViewport } from './use-active-viewport';\nexport type { UseActiveViewportResult } from './use-active-viewport';\n\nexport type { RenderUnknown } from './nodes-renderer';\n\n// ─── Runtime context hooks ───────────────────────────────────────────────\nexport { useExperience, useContentfulComponent, useContentfulExperienceTemplate } from './context';\nexport { useDesignValues } from './use-design-values';\nexport { toCss } from './design-utils';\nexport type { ToCssOptions } from './design-utils';\n\n// ─── Authoring helpers + Config types ─────────────────────────────────────\nexport {\n defineComponent,\n defineExperienceTemplate,\n normalizeComponentRegistration,\n normalizeExperienceTemplateRegistration,\n} from './types';\nexport type {\n ComponentConfig,\n Components,\n Config,\n ContentfulComponent,\n ContentfulExperienceTemplate,\n ExperienceTemplateConfig,\n ExperienceTemplateRegistration,\n ExperienceTemplates,\n Registration,\n RenderContext,\n ResolveToken,\n} from './types';\n\n// ─── Resolver (re-exported from render-core) ──────────────────────────────\nexport { resolveExperience } from '@contentful/experiences-sdk-core';\nexport type { ResolverConfig, ResolveExperienceOptions } from '@contentful/experiences-sdk-core';\n\n// ─── Core IR + payload types (re-exported from render-core) ───────────────\nexport type {\n ComponentNode,\n ComponentRef,\n DesignPropValue,\n DesignToken,\n ExperienceContext,\n ExperienceNode,\n ExperiencePayload,\n ExperienceSys,\n ExperienceTemplateNode,\n ExperienceTemplateRef,\n ManualDesignValue,\n PortableRegistration,\n PortableRenderNode,\n PortableRenderPlan,\n ResolveContext,\n ValuesByViewport,\n ViewportDef,\n} from '@contentful/experiences-sdk-core';\n\n// ─── Design utilities (re-exported from design) ───────────────────────────\nexport {\n CSS_PROPERTIES,\n getValueForViewport,\n getViewportIndex,\n isCssProperty,\n resolveDesignProperties,\n toCssKey,\n toCssMediaQuery,\n} from '@contentful/experiences-design';\n\n// ─── Delivery client + fetchExperience ────────────────────────────────────\nexport {\n ContentfulViewDelivery,\n ContentfulViewDeliveryClient,\n DELIVERY_HOST,\n NotFoundError,\n PREVIEW_HOST,\n createClient,\n fetchExperience,\n} from '@contentful/experiences-client';\nexport type {\n ClientOptions,\n CreateClientOptions,\n ExperienceOptions,\n ResolveOptions,\n} from '@contentful/experiences-client';\n"],"mappings":"AAeA;AAAA,EAC8B;AAAA,EAC5B,4BAAAA;AAAA,OACK;AAIP,SAAS,gCAAgC;AAGzC,SAAS,wBAAwB;AAGjC,SAAS,uBAAuB;AAGhC,SAAS,yBAAyB;AAMlC,SAAS,eAAe,wBAAwB,uCAAuC;AACvF,SAAS,uBAAuB;AAChC,SAAS,aAAa;AAItB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAgBP,SAAS,yBAAyB;AAyBlC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;","names":["ClientExperienceRenderer"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/*\n * Public API surface for `@contentful/experiences-react`.\n *\n * Customers add ONLY this package to their app's dependencies. Everything\n * needed to render an Experience — types, the resolver, viewport utilities,\n * the renderer components, the authoring helpers — is re-exported from here.\n * The internal `@contentful/experiences-sdk-core` and\n * `@contentful/experiences-design` packages are workspace-only implementation\n * details; they are not part of the public API.\n *\n * `ExperienceRenderer` is an alias for `ClientExperienceRenderer`; SSR\n * consumers explicitly import `ServerExperienceRenderer`.\n */\n\n// ─── Renderers ─────────────────────────────────────────────────────────────\nexport {\n ClientExperienceRenderer as ExperienceRenderer,\n ClientExperienceRenderer,\n} from './client-renderer';\nexport type { ClientExperienceRendererProps as ExperienceRendererProps } from './client-renderer';\nexport type { ClientExperienceRendererProps } from './client-renderer';\n\nexport { ServerExperienceRenderer } from './server-renderer';\nexport type { ServerExperienceRendererProps } from './server-renderer';\n\nexport { MissingComponent } from './missing-component';\nexport type { MissingComponentProps } from './missing-component';\n\nexport { ComponentError } from './component-error';\nexport type { ComponentErrorProps } from './component-error';\n\nexport { DebugExperience } from './debug-experience';\nexport type { DebugExperienceProps } from './debug-experience';\n\nexport { useActiveViewport } from './use-active-viewport';\nexport type { UseActiveViewportResult } from './use-active-viewport';\n\nexport type { DiagnosticReporter, RenderError, RenderUnknown } from './nodes-renderer';\n\n// ─── Runtime context hooks ───────────────────────────────────────────────\nexport { useExperience, useContentfulComponent, useContentfulExperienceTemplate } from './context';\nexport { useDesignValues } from './use-design-values';\nexport { toCss } from './design-utils';\nexport type { ToCssOptions } from './design-utils';\n\n// ─── Authoring helpers + Config types ─────────────────────────────────────\nexport {\n defineComponent,\n defineExperienceTemplate,\n normalizeComponentRegistration,\n normalizeExperienceTemplateRegistration,\n} from './types';\nexport type {\n ComponentConfig,\n Components,\n Config,\n ContentfulComponent,\n ContentfulExperienceTemplate,\n ExperienceTemplateConfig,\n ExperienceTemplateRegistration,\n ExperienceTemplates,\n Registration,\n RenderContext,\n ResolveToken,\n} from './types';\n\n// ─── Resolver (re-exported from render-core) ──────────────────────────────\nexport { resolveExperience } from '@contentful/experiences-sdk-core';\nexport type { ResolverConfig, ResolveExperienceOptions } from '@contentful/experiences-sdk-core';\n\n// ─── Core IR + payload types (re-exported from render-core) ───────────────\nexport type {\n ComponentNode,\n ComponentRef,\n DesignPropValue,\n DesignToken,\n ExperienceContext,\n ExperienceNode,\n ExperiencePayload,\n ExperienceSys,\n ExperienceTemplateNode,\n ExperienceTemplateRef,\n ManualDesignValue,\n PortableRegistration,\n PortableRenderNode,\n PortableRenderPlan,\n ResolveContext,\n ValuesByViewport,\n ViewportDef,\n} from '@contentful/experiences-sdk-core';\n\n// ─── Design utilities (re-exported from design) ───────────────────────────\nexport {\n CSS_PROPERTIES,\n getValueForViewport,\n getViewportIndex,\n isCssProperty,\n resolveDesignProperties,\n toCssKey,\n toCssMediaQuery,\n} from '@contentful/experiences-design';\n\n// ─── Delivery client + fetchExperience ────────────────────────────────────\nexport {\n ContentfulViewDelivery,\n ContentfulViewDeliveryClient,\n DELIVERY_HOST,\n ExperienceFetchError,\n NotFoundError,\n PREVIEW_HOST,\n createClient,\n fetchExperience,\n} from '@contentful/experiences-client';\nexport type {\n ClientOptions,\n CreateClientOptions,\n ExperienceOptions,\n ResolveOptions,\n} from '@contentful/experiences-client';\n"],"mappings":"AAeA;AAAA,EAC8B;AAAA,EAC5B,4BAAAA;AAAA,OACK;AAIP,SAAS,gCAAgC;AAGzC,SAAS,wBAAwB;AAGjC,SAAS,sBAAsB;AAG/B,SAAS,uBAAuB;AAGhC,SAAS,yBAAyB;AAMlC,SAAS,eAAe,wBAAwB,uCAAuC;AACvF,SAAS,uBAAuB;AAChC,SAAS,aAAa;AAItB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAgBP,SAAS,yBAAyB;AAyBlC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;","names":["ClientExperienceRenderer"]}
|
package/dist/nodes-renderer.d.ts
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
2
|
import { PortableRenderNode, ViewportDef } from '@contentful/experiences-sdk-core';
|
|
3
|
+
import { ComponentErrorProps } from './component-error.js';
|
|
3
4
|
import { MissingComponentProps } from './missing-component.js';
|
|
4
5
|
import { Config } from './types.js';
|
|
5
6
|
|
|
6
7
|
type RenderUnknown = (props: MissingComponentProps) => ReactNode;
|
|
8
|
+
type RenderError = (props: ComponentErrorProps) => ReactNode;
|
|
9
|
+
/**
|
|
10
|
+
* Reports one render-time diagnostic (unregistered id, a component that
|
|
11
|
+
* threw). `ServerExperienceRenderer` passes a closure that pushes onto a
|
|
12
|
+
* plain array (SSR is synchronous top-down, so the array is fully populated
|
|
13
|
+
* by the time `<DebugExperience>` reads it — see the element-order note
|
|
14
|
+
* there). `ClientExperienceRenderer` passes a `setState` updater instead, so
|
|
15
|
+
* `<DebugExperience>` re-renders reactively when a later interaction throws.
|
|
16
|
+
*/
|
|
17
|
+
type DiagnosticReporter = (error: Error) => void;
|
|
7
18
|
interface NodesRendererProps {
|
|
8
19
|
nodes: PortableRenderNode[];
|
|
9
20
|
config: Config;
|
|
@@ -12,7 +23,9 @@ interface NodesRendererProps {
|
|
|
12
23
|
/** Viewport index the server pre-resolved design against. */
|
|
13
24
|
fallbackViewportIndex: number;
|
|
14
25
|
renderUnknown: RenderUnknown;
|
|
26
|
+
renderError: RenderError;
|
|
27
|
+
onDiagnostic: DiagnosticReporter;
|
|
15
28
|
}
|
|
16
|
-
declare function NodesRenderer({ nodes, config, viewports, activeViewportIndex, fallbackViewportIndex, renderUnknown, }: NodesRendererProps): ReactNode;
|
|
29
|
+
declare function NodesRenderer({ nodes, config, viewports, activeViewportIndex, fallbackViewportIndex, renderUnknown, renderError, onDiagnostic, }: NodesRendererProps): ReactNode;
|
|
17
30
|
|
|
18
|
-
export { NodesRenderer, type NodesRendererProps, type RenderUnknown };
|
|
31
|
+
export { type DiagnosticReporter, NodesRenderer, type NodesRendererProps, type RenderError, type RenderUnknown };
|
package/dist/nodes-renderer.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
|
-
import { Fragment, createElement } from "react";
|
|
2
|
+
import { Fragment, createElement, useRef } from "react";
|
|
3
3
|
import { selectResolvedDesign } from "@contentful/experiences-design";
|
|
4
|
+
import { ComponentErrorBoundary } from "./component-error-boundary";
|
|
4
5
|
import {
|
|
5
6
|
ContentfulComponentProvider,
|
|
6
7
|
ContentfulExperienceTemplateProvider,
|
|
@@ -16,7 +17,9 @@ function NodesRenderer({
|
|
|
16
17
|
viewports,
|
|
17
18
|
activeViewportIndex,
|
|
18
19
|
fallbackViewportIndex,
|
|
19
|
-
renderUnknown
|
|
20
|
+
renderUnknown,
|
|
21
|
+
renderError,
|
|
22
|
+
onDiagnostic
|
|
20
23
|
}) {
|
|
21
24
|
if (!nodes.length) return null;
|
|
22
25
|
return /* @__PURE__ */ jsx(Fragment, { children: nodes.map((node, index) => /* @__PURE__ */ jsx(
|
|
@@ -27,7 +30,9 @@ function NodesRenderer({
|
|
|
27
30
|
viewports,
|
|
28
31
|
activeViewportIndex,
|
|
29
32
|
fallbackViewportIndex,
|
|
30
|
-
renderUnknown
|
|
33
|
+
renderUnknown,
|
|
34
|
+
renderError,
|
|
35
|
+
onDiagnostic
|
|
31
36
|
},
|
|
32
37
|
node.nodeId ?? index
|
|
33
38
|
)) });
|
|
@@ -38,12 +43,29 @@ function NodeRenderer({
|
|
|
38
43
|
viewports,
|
|
39
44
|
activeViewportIndex,
|
|
40
45
|
fallbackViewportIndex,
|
|
41
|
-
renderUnknown
|
|
46
|
+
renderUnknown,
|
|
47
|
+
renderError,
|
|
48
|
+
onDiagnostic
|
|
42
49
|
}) {
|
|
43
50
|
const { kind, id } = node.registration;
|
|
44
51
|
const isExperienceTemplate = kind === "experienceTemplate";
|
|
52
|
+
const reported = useRef(/* @__PURE__ */ new Set());
|
|
53
|
+
function reportOnce(error) {
|
|
54
|
+
if (reported.current.has(error.message)) return;
|
|
55
|
+
reported.current.add(error.message);
|
|
56
|
+
onDiagnostic(error);
|
|
57
|
+
}
|
|
45
58
|
const slotProps = {};
|
|
46
59
|
for (const [slotName, children] of Object.entries(node.slots)) {
|
|
60
|
+
if (!Array.isArray(children)) {
|
|
61
|
+
const message = `Slot "${slotName}" on ${kind} "${id}"${node.nodeId ? ` (node "${node.nodeId}")` : ""} is not an array of nodes; rendering it as empty instead of throwing.`;
|
|
62
|
+
if (typeof console !== "undefined") {
|
|
63
|
+
console.warn(`[@contentful/experiences-react] ${message}`);
|
|
64
|
+
}
|
|
65
|
+
reportOnce(new Error(message));
|
|
66
|
+
slotProps[slotName] = [];
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
47
69
|
slotProps[slotName] = children.map((child, index) => /* @__PURE__ */ jsx(
|
|
48
70
|
NodeRenderer,
|
|
49
71
|
{
|
|
@@ -52,7 +74,9 @@ function NodeRenderer({
|
|
|
52
74
|
viewports,
|
|
53
75
|
activeViewportIndex,
|
|
54
76
|
fallbackViewportIndex,
|
|
55
|
-
renderUnknown
|
|
77
|
+
renderUnknown,
|
|
78
|
+
renderError,
|
|
79
|
+
onDiagnostic
|
|
56
80
|
},
|
|
57
81
|
child.nodeId ?? index
|
|
58
82
|
));
|
|
@@ -60,13 +84,18 @@ function NodeRenderer({
|
|
|
60
84
|
const entry = isExperienceTemplate ? config.experienceTemplates?.[id] : config.components[id];
|
|
61
85
|
if (!entry) {
|
|
62
86
|
if (isExperienceTemplate) {
|
|
87
|
+
const message = `No experience template registered for id "${id}". Rendering its slot children without the experience template wrapper.`;
|
|
63
88
|
if (typeof console !== "undefined") {
|
|
64
|
-
console.warn(
|
|
65
|
-
`[@contentful/experiences-react] No experience template registered for id "${id}". Rendering its slot children without the experience template wrapper.`
|
|
66
|
-
);
|
|
89
|
+
console.warn(`[@contentful/experiences-react] ${message}`);
|
|
67
90
|
}
|
|
91
|
+
reportOnce(new Error(message));
|
|
68
92
|
return /* @__PURE__ */ jsx(Fragment, { children: Object.values(slotProps).flat() });
|
|
69
93
|
}
|
|
94
|
+
reportOnce(
|
|
95
|
+
new Error(
|
|
96
|
+
`No component registered for id "${id}"${node.nodeId ? ` (nodeId: ${node.nodeId})` : ""}.`
|
|
97
|
+
)
|
|
98
|
+
);
|
|
70
99
|
return createElement(renderUnknown, { componentId: id, nodeId: node.nodeId });
|
|
71
100
|
}
|
|
72
101
|
const registrationConfig = isExperienceTemplate ? normalizeExperienceTemplateRegistration(entry) : normalizeComponentRegistration(entry);
|
|
@@ -89,7 +118,16 @@ function NodeRenderer({
|
|
|
89
118
|
...node.props.resolved,
|
|
90
119
|
...slotProps
|
|
91
120
|
};
|
|
92
|
-
const element = /* @__PURE__ */ jsx(ResolvedDesignProvider, { value: tokenResolvedDesign, children:
|
|
121
|
+
const element = /* @__PURE__ */ jsx(ResolvedDesignProvider, { value: tokenResolvedDesign, children: /* @__PURE__ */ jsx(
|
|
122
|
+
ComponentErrorBoundary,
|
|
123
|
+
{
|
|
124
|
+
fallback: createElement(renderError, { componentId: id, nodeId: node.nodeId }),
|
|
125
|
+
componentId: id,
|
|
126
|
+
kind,
|
|
127
|
+
nodeId: node.nodeId,
|
|
128
|
+
children: createElement(registrationConfig.component, composed)
|
|
129
|
+
}
|
|
130
|
+
) });
|
|
93
131
|
if (isExperienceTemplate) {
|
|
94
132
|
const contentful2 = {
|
|
95
133
|
experienceTemplateId: id,
|
|
@@ -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, ViewportDef } from '@contentful/experiences-sdk-core';\nimport { selectResolvedDesign } from '@contentful/experiences-design';\n\nimport {\n ContentfulComponentProvider,\n ContentfulExperienceTemplateProvider,\n ResolvedDesignProvider,\n} from './context';\nimport type { MissingComponentProps } from './missing-component';\nimport {\n normalizeComponentRegistration,\n normalizeExperienceTemplateRegistration,\n type ContentfulComponent,\n type ContentfulExperienceTemplate,\n type Config,\n} from './types';\n\nexport type RenderUnknown = (props: MissingComponentProps) => ReactNode;\n\n// Internal renderers take `viewports` + `activeViewportIndex`, not the whole\n// RenderContext object — the context is published once via ExperienceProvider,\n// and re-threading it as an element prop makes React's RSC serializer back-patch\n// a shared reference into frozen props (\"Cannot assign to read only property\").\nexport interface NodesRendererProps {\n nodes: PortableRenderNode[];\n config: Config;\n viewports: ViewportDef[];\n activeViewportIndex: number;\n /** Viewport index the server pre-resolved design against. */\n fallbackViewportIndex: number;\n renderUnknown: RenderUnknown;\n}\n\nexport function NodesRenderer({\n nodes,\n config,\n viewports,\n activeViewportIndex,\n fallbackViewportIndex,\n renderUnknown,\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 viewports={viewports}\n activeViewportIndex={activeViewportIndex}\n fallbackViewportIndex={fallbackViewportIndex}\n renderUnknown={renderUnknown}\n />\n ))}\n </Fragment>\n );\n}\n\ninterface NodeRendererProps {\n node: PortableRenderNode;\n config: Config;\n viewports: ViewportDef[];\n activeViewportIndex: number;\n fallbackViewportIndex: number;\n renderUnknown: RenderUnknown;\n}\n\nfunction NodeRenderer({\n node,\n config,\n viewports,\n activeViewportIndex,\n fallbackViewportIndex,\n renderUnknown,\n}: NodeRendererProps): ReactNode {\n const { kind, id } = node.registration;\n const isExperienceTemplate = kind === 'experienceTemplate';\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 slotProps[slotName] = children.map((child, index) => (\n <NodeRenderer\n key={child.nodeId ?? index}\n node={child}\n config={config}\n viewports={viewports}\n activeViewportIndex={activeViewportIndex}\n fallbackViewportIndex={fallbackViewportIndex}\n renderUnknown={renderUnknown}\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 if (typeof console !== 'undefined') {\n console.warn(\n `[@contentful/experiences-react] No experience template registered for id \"${id}\". Rendering its slot children without the experience template wrapper.`\n );\n }\n return <Fragment>{Object.values(slotProps).flat()}</Fragment>;\n }\n return createElement(renderUnknown, { componentId: id, nodeId: node.nodeId });\n }\n const registrationConfig = isExperienceTemplate\n ? normalizeExperienceTemplateRegistration(entry)\n : normalizeComponentRegistration(entry);\n\n const { props: tokenResolvedDesign, unresolved } = selectResolvedDesign(\n node.props,\n viewports,\n activeViewportIndex,\n fallbackViewportIndex,\n config.resolveToken\n );\n if (unresolved.length && typeof console !== 'undefined') {\n console.warn(\n `[@contentful/experiences-react] resolveToken returned undefined for token id(s) on ${kind} \"${id}\": ${unresolved.join(', ')}. useDesignValues() will omit those keys.`\n );\n }\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 {createElement(registrationConfig.component, composed)}\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 };\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 };\n return <ContentfulComponentProvider value={contentful}>{element}</ContentfulComponentProvider>;\n}\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, useRef, type ReactNode } from 'react';\n\nimport type { PortableRenderNode, ViewportDef } from '@contentful/experiences-sdk-core';\nimport { selectResolvedDesign } from '@contentful/experiences-design';\n\nimport { ComponentErrorBoundary } from './component-error-boundary';\nimport type { ComponentErrorProps } from './component-error';\nimport {\n ContentfulComponentProvider,\n ContentfulExperienceTemplateProvider,\n ResolvedDesignProvider,\n} from './context';\nimport type { MissingComponentProps } from './missing-component';\nimport {\n normalizeComponentRegistration,\n normalizeExperienceTemplateRegistration,\n type ContentfulComponent,\n type ContentfulExperienceTemplate,\n type Config,\n} from './types';\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\n// Internal renderers take `viewports` + `activeViewportIndex`, not the whole\n// RenderContext object — the context is published once via ExperienceProvider,\n// and re-threading it as an element prop makes React's RSC serializer back-patch\n// a shared reference into frozen props (\"Cannot assign to read only property\").\nexport interface NodesRendererProps {\n nodes: PortableRenderNode[];\n config: Config;\n viewports: ViewportDef[];\n activeViewportIndex: number;\n /** Viewport index the server pre-resolved design against. */\n fallbackViewportIndex: number;\n renderUnknown: RenderUnknown;\n renderError: RenderError;\n onDiagnostic: DiagnosticReporter;\n}\n\nexport function NodesRenderer({\n nodes,\n config,\n viewports,\n activeViewportIndex,\n fallbackViewportIndex,\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 viewports={viewports}\n activeViewportIndex={activeViewportIndex}\n fallbackViewportIndex={fallbackViewportIndex}\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 viewports: ViewportDef[];\n activeViewportIndex: number;\n fallbackViewportIndex: number;\n renderUnknown: RenderUnknown;\n renderError: RenderError;\n onDiagnostic: DiagnosticReporter;\n}\n\nfunction NodeRenderer({\n node,\n config,\n viewports,\n activeViewportIndex,\n fallbackViewportIndex,\n renderUnknown,\n renderError,\n onDiagnostic,\n}: NodeRendererProps): ReactNode {\n const { kind, id } = node.registration;\n const isExperienceTemplate = kind === 'experienceTemplate';\n\n // Any ancestor re-render (e.g. a viewport change from useActiveViewport,\n // or an unrelated parent state update) re-executes this whole function, so\n // an unregistered id/malformed slot would otherwise re-report the same\n // diagnostic every time — matches Angular's `lastDiagnostics` guard in\n // NodeRenderEngine. Dedup by content, not just by call site, since a node\n // with several malformed slots reports one diagnostic per slot.\n //\n // Calling `onDiagnostic` directly here (rather than via an effect) is\n // 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. The\n // client-only \"don't setState while a different component is rendering\"\n // concern is handled where `onDiagnostic` is actually implemented\n // (see client-renderer.tsx), not here.\n const reported = useRef<Set<string>>(new Set());\n function reportOnce(error: Error): void {\n if (reported.current.has(error.message)) return;\n reported.current.add(error.message);\n onDiagnostic(error);\n }\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 reportOnce(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 viewports={viewports}\n activeViewportIndex={activeViewportIndex}\n fallbackViewportIndex={fallbackViewportIndex}\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 reportOnce(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 reportOnce(\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 { props: tokenResolvedDesign, unresolved } = selectResolvedDesign(\n node.props,\n viewports,\n activeViewportIndex,\n fallbackViewportIndex,\n config.resolveToken\n );\n if (unresolved.length && typeof console !== 'undefined') {\n console.warn(\n `[@contentful/experiences-react] resolveToken returned undefined for token id(s) on ${kind} \"${id}\": ${unresolved.join(', ')}. useDesignValues() will omit those keys.`\n );\n }\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 };\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 };\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,7 +1,8 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
2
|
import { PortableRenderPlan } from '@contentful/experiences-sdk-core';
|
|
3
|
-
import { RenderUnknown } from './nodes-renderer.js';
|
|
3
|
+
import { RenderUnknown, RenderError } from './nodes-renderer.js';
|
|
4
4
|
import { Config } from './types.js';
|
|
5
|
+
import './component-error.js';
|
|
5
6
|
import './missing-component.js';
|
|
6
7
|
|
|
7
8
|
interface ServerExperienceRendererProps {
|
|
@@ -23,7 +24,9 @@ interface ServerExperienceRendererProps {
|
|
|
23
24
|
debug?: boolean;
|
|
24
25
|
/** Override the fallback rendered for unregistered component types. */
|
|
25
26
|
renderUnknown?: RenderUnknown;
|
|
27
|
+
/** Override the fallback rendered when a registered component throws. */
|
|
28
|
+
renderError?: RenderError;
|
|
26
29
|
}
|
|
27
|
-
declare function ServerExperienceRenderer({ experience, config, initialViewportId, metadata, debug, renderUnknown, }: ServerExperienceRendererProps): ReactNode;
|
|
30
|
+
declare function ServerExperienceRenderer({ experience, config, initialViewportId, metadata, debug, renderUnknown, renderError, }: ServerExperienceRendererProps): ReactNode;
|
|
28
31
|
|
|
29
32
|
export { ServerExperienceRenderer, type ServerExperienceRendererProps };
|
package/dist/server-renderer.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { getViewportIndex } from "@contentful/experiences-design";
|
|
3
|
+
import { ComponentError } from "./component-error";
|
|
3
4
|
import { DebugExperience } from "./debug-experience";
|
|
4
5
|
import { ExperienceProvider } from "./context";
|
|
5
6
|
import { MissingComponent } from "./missing-component";
|
|
@@ -21,7 +22,8 @@ function ServerExperienceRenderer({
|
|
|
21
22
|
initialViewportId,
|
|
22
23
|
metadata,
|
|
23
24
|
debug = false,
|
|
24
|
-
renderUnknown = MissingComponent
|
|
25
|
+
renderUnknown = MissingComponent,
|
|
26
|
+
renderError = ComponentError
|
|
25
27
|
}) {
|
|
26
28
|
if (!experience) return null;
|
|
27
29
|
const activeViewportIndex = getViewportIndex(experience.viewports, initialViewportId);
|
|
@@ -35,19 +37,23 @@ function ServerExperienceRenderer({
|
|
|
35
37
|
activeViewport,
|
|
36
38
|
activeViewportIndex
|
|
37
39
|
};
|
|
40
|
+
const renderDiagnostics = [...experience.diagnostics ?? []];
|
|
41
|
+
const tree = /* @__PURE__ */ jsx(
|
|
42
|
+
NodesRenderer,
|
|
43
|
+
{
|
|
44
|
+
nodes: experience.nodes,
|
|
45
|
+
config,
|
|
46
|
+
viewports: experience.viewports,
|
|
47
|
+
activeViewportIndex,
|
|
48
|
+
fallbackViewportIndex: experience.fallbackViewportIndex,
|
|
49
|
+
renderUnknown,
|
|
50
|
+
renderError,
|
|
51
|
+
onDiagnostic: (diagnostic) => renderDiagnostics.push(diagnostic)
|
|
52
|
+
}
|
|
53
|
+
);
|
|
38
54
|
return /* @__PURE__ */ jsxs(ExperienceProvider, { value: renderContext, children: [
|
|
39
|
-
|
|
40
|
-
/* @__PURE__ */ jsx(
|
|
41
|
-
NodesRenderer,
|
|
42
|
-
{
|
|
43
|
-
nodes: experience.nodes,
|
|
44
|
-
config,
|
|
45
|
-
viewports: experience.viewports,
|
|
46
|
-
activeViewportIndex,
|
|
47
|
-
fallbackViewportIndex: experience.fallbackViewportIndex,
|
|
48
|
-
renderUnknown
|
|
49
|
-
}
|
|
50
|
-
)
|
|
55
|
+
tree,
|
|
56
|
+
debug ? /* @__PURE__ */ jsx(DebugExperience, { experience, errors: renderDiagnostics }) : null
|
|
51
57
|
] });
|
|
52
58
|
}
|
|
53
59
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server-renderer.tsx"],"sourcesContent":["/*\n * Server-safe Experience renderer. Resolves the active viewport once from\n * `initialViewportId` and renders without any reactive subscription.\n */\n\nimport type { ReactNode } from 'react';\n\nimport type {\n ExperienceContext,\n PortableRenderPlan,\n ViewportDef,\n} from '@contentful/experiences-sdk-core';\nimport { getViewportIndex } from '@contentful/experiences-design';\n\nimport { DebugExperience } from './debug-experience';\nimport { ExperienceProvider } from './context';\nimport { MissingComponent } from './missing-component';\nimport { NodesRenderer, type RenderUnknown } from './nodes-renderer';\nimport type { Config, RenderContext } from './types';\n\nconst DEFAULT_CONTEXT: ExperienceContext = {\n debug: false,\n metadata: {},\n viewports: [],\n};\n\n// Keeps `activeViewport` non-null when a payload declares no viewports.\nconst FALLBACK_VIEWPORT: ViewportDef = {\n id: '_',\n query: '*',\n displayName: 'Default',\n previewSize: '100%',\n};\n\nexport interface ServerExperienceRendererProps {\n experience: PortableRenderPlan | null | undefined;\n config: Config;\n /** Initial viewport seed (e.g. derived from User-Agent). Defaults to viewport[0]. */\n initialViewportId?: string;\n /**\n * Arbitrary per-render metadata, readable by descendants via\n * `useExperience()` and by resolvers via `ctx.experience.metadata`.\n */\n metadata?: Record<string, unknown>;\n /**\n * Observability switch. When on: renders the visible missing-component box,\n * turns the default `renderUnknown` fallback into the debug component, and\n * auto-mounts `<DebugExperience>` (the resolved-plan JSON panel) after the\n * tree. Pair with `debug` on `fetchExperience` for end-to-end logging.\n */\n debug?: boolean;\n /** Override the fallback rendered for unregistered component types. */\n renderUnknown?: RenderUnknown;\n}\n\nexport function ServerExperienceRenderer({\n experience,\n config,\n initialViewportId,\n metadata,\n debug = false,\n renderUnknown = MissingComponent,\n}: ServerExperienceRendererProps): ReactNode {\n if (!experience) return null;\n\n const activeViewportIndex = getViewportIndex(experience.viewports, initialViewportId);\n\n // Copy viewports/activeViewport so the context (serialized + frozen by RSC)\n // shares no object identity with the plan arrays the renderers read below —\n // a shared reference makes Flight back-patch into frozen props and throw.\n const contextViewports = experience.viewports.map((v) => ({ ...v }));\n const activeViewport = { ...(experience.viewports[activeViewportIndex] ?? FALLBACK_VIEWPORT) };\n\n const renderContext: RenderContext = {\n ...DEFAULT_CONTEXT,\n debug,\n metadata: { ...DEFAULT_CONTEXT.metadata, ...(metadata ?? {}) },\n viewports: contextViewports,\n activeViewport,\n activeViewportIndex,\n };\n\n
|
|
1
|
+
{"version":3,"sources":["../src/server-renderer.tsx"],"sourcesContent":["/*\n * Server-safe Experience renderer. Resolves the active viewport once from\n * `initialViewportId` and renders without any reactive subscription.\n */\n\nimport type { ReactNode } from 'react';\n\nimport type {\n ExperienceContext,\n PortableRenderPlan,\n ViewportDef,\n} from '@contentful/experiences-sdk-core';\nimport { getViewportIndex } from '@contentful/experiences-design';\n\nimport { ComponentError } from './component-error';\nimport { DebugExperience } from './debug-experience';\nimport { ExperienceProvider } from './context';\nimport { MissingComponent } from './missing-component';\nimport { NodesRenderer, type RenderError, type RenderUnknown } from './nodes-renderer';\nimport type { Config, RenderContext } from './types';\n\nconst DEFAULT_CONTEXT: ExperienceContext = {\n debug: false,\n metadata: {},\n viewports: [],\n};\n\n// Keeps `activeViewport` non-null when a payload declares no viewports.\nconst FALLBACK_VIEWPORT: ViewportDef = {\n id: '_',\n query: '*',\n displayName: 'Default',\n previewSize: '100%',\n};\n\nexport interface ServerExperienceRendererProps {\n experience: PortableRenderPlan | null | undefined;\n config: Config;\n /** Initial viewport seed (e.g. derived from User-Agent). Defaults to viewport[0]. */\n initialViewportId?: string;\n /**\n * Arbitrary per-render metadata, readable by descendants via\n * `useExperience()` and by resolvers via `ctx.experience.metadata`.\n */\n metadata?: Record<string, unknown>;\n /**\n * Observability switch. When on: renders the visible missing-component box,\n * turns the default `renderUnknown` fallback into the debug component, and\n * auto-mounts `<DebugExperience>` (the resolved-plan JSON panel) after the\n * tree. Pair with `debug` on `fetchExperience` for end-to-end logging.\n */\n debug?: boolean;\n /** Override the fallback rendered for unregistered component types. */\n renderUnknown?: RenderUnknown;\n /** Override the fallback rendered when a registered component throws. */\n renderError?: RenderError;\n}\n\nexport function ServerExperienceRenderer({\n experience,\n config,\n initialViewportId,\n metadata,\n debug = false,\n renderUnknown = MissingComponent,\n renderError = ComponentError,\n}: ServerExperienceRendererProps): ReactNode {\n if (!experience) return null;\n\n const activeViewportIndex = getViewportIndex(experience.viewports, initialViewportId);\n\n // Copy viewports/activeViewport so the context (serialized + frozen by RSC)\n // shares no object identity with the plan arrays the renderers read below —\n // a shared reference makes Flight back-patch into frozen props and throw.\n const contextViewports = experience.viewports.map((v) => ({ ...v }));\n const activeViewport = { ...(experience.viewports[activeViewportIndex] ?? FALLBACK_VIEWPORT) };\n\n const renderContext: RenderContext = {\n ...DEFAULT_CONTEXT,\n debug,\n metadata: { ...DEFAULT_CONTEXT.metadata, ...(metadata ?? {}) },\n viewports: contextViewports,\n activeViewport,\n activeViewportIndex,\n };\n\n // Render-time diagnostics (unregistered id, a component that threw),\n // collected into a plain array rather than React state: SSR is synchronous\n // top-down, so by the time `<DebugExperience>` renders — after the tree,\n // see the element-order note below — this array is already fully populated.\n // Seeded with the resolve-time diagnostics up front rather than spreading\n // them in later: `<DebugExperience>` below reads this array by reference\n // (`errors={renderDiagnostics}`), not a spread copy — a copy would be built\n // right here, before React ever renders `tree` and gets a chance to push\n // into it.\n const renderDiagnostics: Error[] = [...(experience.diagnostics ?? [])];\n\n const tree = (\n <NodesRenderer\n nodes={experience.nodes}\n config={config}\n viewports={experience.viewports}\n activeViewportIndex={activeViewportIndex}\n fallbackViewportIndex={experience.fallbackViewportIndex}\n renderUnknown={renderUnknown}\n renderError={renderError}\n onDiagnostic={(diagnostic) => renderDiagnostics.push(diagnostic)}\n />\n );\n\n // The node tree renders BEFORE `<DebugExperience>` — required, not\n // stylistic. React's render is synchronous top-down; if DebugExperience\n // rendered first (as it used to), it would read `renderDiagnostics` before\n // any descendant had a chance to push into it, and always see it empty.\n return (\n <ExperienceProvider value={renderContext}>\n {tree}\n {debug ? <DebugExperience experience={experience} errors={renderDiagnostics} /> : null}\n </ExperienceProvider>\n );\n}\n"],"mappings":"AAkGI,cAiBA,YAjBA;AAtFJ,SAAS,wBAAwB;AAEjC,SAAS,sBAAsB;AAC/B,SAAS,uBAAuB;AAChC,SAAS,0BAA0B;AACnC,SAAS,wBAAwB;AACjC,SAAS,qBAA2D;AAGpE,MAAM,kBAAqC;AAAA,EACzC,OAAO;AAAA,EACP,UAAU,CAAC;AAAA,EACX,WAAW,CAAC;AACd;AAGA,MAAM,oBAAiC;AAAA,EACrC,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,aAAa;AAAA,EACb,aAAa;AACf;AAyBO,SAAS,yBAAyB;AAAA,EACvC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR,gBAAgB;AAAA,EAChB,cAAc;AAChB,GAA6C;AAC3C,MAAI,CAAC,WAAY,QAAO;AAExB,QAAM,sBAAsB,iBAAiB,WAAW,WAAW,iBAAiB;AAKpF,QAAM,mBAAmB,WAAW,UAAU,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE;AACnE,QAAM,iBAAiB,EAAE,GAAI,WAAW,UAAU,mBAAmB,KAAK,kBAAmB;AAE7F,QAAM,gBAA+B;AAAA,IACnC,GAAG;AAAA,IACH;AAAA,IACA,UAAU,EAAE,GAAG,gBAAgB,UAAU,GAAI,YAAY,CAAC,EAAG;AAAA,IAC7D,WAAW;AAAA,IACX;AAAA,IACA;AAAA,EACF;AAWA,QAAM,oBAA6B,CAAC,GAAI,WAAW,eAAe,CAAC,CAAE;AAErE,QAAM,OACJ;AAAA,IAAC;AAAA;AAAA,MACC,OAAO,WAAW;AAAA,MAClB;AAAA,MACA,WAAW,WAAW;AAAA,MACtB;AAAA,MACA,uBAAuB,WAAW;AAAA,MAClC;AAAA,MACA;AAAA,MACA,cAAc,CAAC,eAAe,kBAAkB,KAAK,UAAU;AAAA;AAAA,EACjE;AAOF,SACE,qBAAC,sBAAmB,OAAO,eACxB;AAAA;AAAA,IACA,QAAQ,oBAAC,mBAAgB,YAAwB,QAAQ,mBAAmB,IAAK;AAAA,KACpF;AAEJ;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contentful/experiences-react",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.12",
|
|
4
4
|
"description": "React adapter for Contentful Experiences — Server and Client renderers over a PortableRenderPlan",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
"directory": "packages/adapter-react"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@contentful/experiences-client": "0.3.
|
|
34
|
-
"@contentful/experiences-sdk-core": "0.7.
|
|
35
|
-
"@contentful/experiences-design": "0.7.
|
|
33
|
+
"@contentful/experiences-client": "0.3.11",
|
|
34
|
+
"@contentful/experiences-sdk-core": "0.7.11",
|
|
35
|
+
"@contentful/experiences-design": "0.7.11"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"react": "^18.0.0 || ^19.0.0",
|