@xeonr/renderer-sdk 1.0.4 → 1.1.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.
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Drop-in replacement for `react-dom/client` that auto-wraps the rendered
3
+ * tree in `<RendererErrorBoundary>`. Used by `@xeonr/renderer-plugin-vite`
4
+ * to rewrite renderer-archive build imports so the boundary is mandatory
5
+ * without renderer authors having to opt in.
6
+ *
7
+ * Renderer authors do NOT need to import this directly — the vite plugin
8
+ * substitutes `react-dom/client` specifiers in renderer source code at
9
+ * build time. If a renderer is built outside the plugin (e.g. a custom
10
+ * pipeline), authors can import from here explicitly instead.
11
+ *
12
+ * Mirrors React 18+ `Root` / `RootOptions` surface; passes everything
13
+ * through unchanged except for `render` and `hydrateRoot`'s initial
14
+ * children, which are wrapped in the boundary so a single render-time
15
+ * crash is forwarded to the host overlay rather than tearing down the
16
+ * iframe silently.
17
+ */
18
+ import { createRoot as origCreateRoot, hydrateRoot as origHydrateRoot, } from 'react-dom/client';
19
+ import { createElement } from 'react';
20
+ import { RendererErrorBoundary } from './RendererErrorBoundary.js';
21
+ function wrap(children) {
22
+ return createElement(RendererErrorBoundary, null, children);
23
+ }
24
+ export function createRoot(container, options) {
25
+ const root = origCreateRoot(container, options);
26
+ const originalRender = root.render.bind(root);
27
+ root.render = (children) => originalRender(wrap(children));
28
+ return root;
29
+ }
30
+ export function hydrateRoot(container, initialChildren, options) {
31
+ const root = origHydrateRoot(container, wrap(initialChildren), options);
32
+ const originalRender = root.render.bind(root);
33
+ root.render = (children) => originalRender(wrap(children));
34
+ return root;
35
+ }
36
+ //# sourceMappingURL=dom.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dom.js","sourceRoot":"","sources":["../../src/react/dom.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EACN,UAAU,IAAI,cAAc,EAC5B,WAAW,IAAI,eAAe,GAI9B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,aAAa,EAAkB,MAAM,OAAO,CAAC;AACtD,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAEnE,SAAS,IAAI,CAAC,QAAmB;IAChC,OAAO,aAAa,CAAC,qBAAqB,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;AAC7D,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,SAAqC,EAAE,OAAqB;IACtF,MAAM,IAAI,GAAG,cAAc,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IAChD,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC9C,IAAI,CAAC,MAAM,GAAG,CAAC,QAAmB,EAAE,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;IACtE,OAAO,IAAI,CAAC;AACb,CAAC;AAED,MAAM,UAAU,WAAW,CAC1B,SAA6B,EAC7B,eAA0B,EAC1B,OAA0B;IAE1B,MAAM,IAAI,GAAG,eAAe,CAAC,SAAS,EAAE,IAAI,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC,CAAC;IACxE,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC9C,IAAI,CAAC,MAAM,GAAG,CAAC,QAAmB,EAAE,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;IACtE,OAAO,IAAI,CAAC;AACb,CAAC"}
@@ -1,3 +1,5 @@
1
1
  export { useRendererClient } from './useRendererClient.js';
2
2
  export type { UseRendererClientOptions, UseRendererClientResult } from './useRendererClient.js';
3
+ export { RendererErrorBoundary } from './RendererErrorBoundary.js';
4
+ export type { RendererErrorBoundaryProps } from './RendererErrorBoundary.js';
3
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,YAAY,EAAE,wBAAwB,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,YAAY,EAAE,wBAAwB,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAChG,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACnE,YAAY,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAC"}
@@ -1,2 +1,3 @@
1
1
  export { useRendererClient } from './useRendererClient.js';
2
+ export { RendererErrorBoundary } from './RendererErrorBoundary.js';
2
3
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAE3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"useRendererClient.d.ts","sourceRoot":"","sources":["../../src/react/useRendererClient.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAe,MAAM,aAAa,CAAC;AAEjH,MAAM,WAAW,wBAAyB,SAAQ,qBAAqB;CAAG;AAE1E,MAAM,WAAW,uBAAuB;IACvC,kDAAkD;IAClD,SAAS,EAAE,OAAO,CAAC;IACnB,+CAA+C;IAC/C,KAAK,EAAE,aAAa,GAAG,IAAI,CAAC;IAC5B,2CAA2C;IAC3C,aAAa,EAAE,aAAa,GAAG,IAAI,CAAC;IACpC,4CAA4C;IAC5C,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,kDAAkD;IAClD,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,8BAA8B;IAC9B,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC;IACxB,wDAAwD;IACxD,MAAM,EAAE,cAAc,GAAG,IAAI,CAAC;IAC9B,yDAAyD;IACzD,UAAU,EAAE,WAAW,GAAG,QAAQ,GAAG,IAAI,CAAC;IAC1C,qCAAqC;IACrC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,0EAA0E;IAC1E,UAAU,EAAE,kBAAkB,CAAC;IAC/B,oGAAoG;IACpG,IAAI,EAAE,MAAM,CAAC;IAEb,kDAAkD;IAClD,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,gDAAgD;IAChD,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,4DAA4D;IAC5D,KAAK,EAAE,MAAM,IAAI,CAAC;IAElB,+DAA+D;IAC/D,MAAM,EAAE,cAAc,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,CAAC,EAAE,wBAAwB,GAAG,uBAAuB,CAgF7F"}
1
+ {"version":3,"file":"useRendererClient.d.ts","sourceRoot":"","sources":["../../src/react/useRendererClient.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAe,MAAM,aAAa,CAAC;AAEjH,MAAM,WAAW,wBAAyB,SAAQ,qBAAqB;CAAG;AAqB1E,MAAM,WAAW,uBAAuB;IACvC,kDAAkD;IAClD,SAAS,EAAE,OAAO,CAAC;IACnB,+CAA+C;IAC/C,KAAK,EAAE,aAAa,GAAG,IAAI,CAAC;IAC5B,2CAA2C;IAC3C,aAAa,EAAE,aAAa,GAAG,IAAI,CAAC;IACpC,4CAA4C;IAC5C,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,kDAAkD;IAClD,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,8BAA8B;IAC9B,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC;IACxB,wDAAwD;IACxD,MAAM,EAAE,cAAc,GAAG,IAAI,CAAC;IAC9B,yDAAyD;IACzD,UAAU,EAAE,WAAW,GAAG,QAAQ,GAAG,IAAI,CAAC;IAC1C,qCAAqC;IACrC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,0EAA0E;IAC1E,UAAU,EAAE,kBAAkB,CAAC;IAC/B,oGAAoG;IACpG,IAAI,EAAE,MAAM,CAAC;IAEb,kDAAkD;IAClD,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,gDAAgD;IAChD,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,4DAA4D;IAC5D,KAAK,EAAE,MAAM,IAAI,CAAC;IAElB,+DAA+D;IAC/D,MAAM,EAAE,cAAc,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,CAAC,EAAE,wBAAwB,GAAG,uBAAuB,CAgF7F"}
@@ -1,5 +1,24 @@
1
1
  import { useState, useRef, useEffect, useMemo } from 'react';
2
2
  import { RendererClient } from '../client.js';
3
+ /**
4
+ * Apply the host's theme to the renderer document. Two distinct effects:
5
+ *
6
+ * - `documentElement.dataset.theme` — surfaces the theme as a
7
+ * `[data-theme="dark"]` selector for renderer-authored CSS.
8
+ * - `documentElement.style.colorScheme` — tells the browser to render
9
+ * scrollbars, form controls, and (critically) the iframe's default
10
+ * canvas in the matching mode. Without this, a cross-origin iframe
11
+ * defaults to a white canvas regardless of what the host's
12
+ * `color-scheme` is — so `background: transparent` in the renderer
13
+ * reveals white, not the host's dark surface. This is the fix for
14
+ * "renderer shows light mode when host is dark".
15
+ */
16
+ function applyThemeToDocument(theme) {
17
+ if (typeof document === 'undefined')
18
+ return;
19
+ document.documentElement.dataset.theme = theme;
20
+ document.documentElement.style.colorScheme = theme;
21
+ }
3
22
  /**
4
23
  * React hook for building custom renderers.
5
24
  *
@@ -51,11 +70,11 @@ export function useRendererClient(options) {
51
70
  setEntrypoint(payload.entrypoint);
52
71
  setApiBaseUrl(payload.apiBaseUrl);
53
72
  setPath(client.getPath());
54
- document.documentElement.dataset.theme = payload.theme;
73
+ applyThemeToDocument(payload.theme);
55
74
  });
56
75
  const unsubTheme = client.onThemeChange((newTheme) => {
57
76
  setTheme(newTheme);
58
- document.documentElement.dataset.theme = newTheme;
77
+ applyThemeToDocument(newTheme);
59
78
  });
60
79
  const unsubToken = client.onTokenRefresh((newToken, newExpiresAt) => {
61
80
  setToken(newToken);
@@ -1 +1 @@
1
- {"version":3,"file":"useRendererClient.js","sourceRoot":"","sources":["../../src/react/useRendererClient.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useRendererClient.js","sourceRoot":"","sources":["../../src/react/useRendererClient.ts"],"names":[],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -11,6 +11,27 @@ export interface RendererConfig {
11
11
  allowForms?: boolean;
12
12
  allowDownloads?: boolean;
13
13
  };
14
+ /**
15
+ * Extra origins this renderer needs to reach via `fetch` / `XMLHttpRequest`
16
+ * / WebSocket. Surfaced to the browser as additional entries in the CSP
17
+ * `connect-src` directive. The upl.im API + auth hosts are always allowed
18
+ * by the renderer-proxy; only list extras here (e.g. a third-party API
19
+ * the renderer integrates with).
20
+ *
21
+ * Each entry is a fully-qualified origin like `https://api.example.com`
22
+ * or a wildcarded host like `https://*.example.com`. No paths, no
23
+ * trailing slashes. Origins are validated at deploy time.
24
+ */
25
+ connectHosts?: string[];
26
+ /**
27
+ * Short build identifier the build pipeline stamps into config.json
28
+ * (e.g. the renderer-plugin-vite hash of the output directory).
29
+ * Surfaced in crash telemetry as a separate dimension so we can
30
+ * distinguish "crashed in build A" from "crashed in build B" even
31
+ * when both builds share the same archive_upload_id (the developer
32
+ * republished without bumping the archive). Bounded to 64 chars.
33
+ */
34
+ buildHash?: string;
14
35
  }
15
36
  export type RendererPermission = 'createFolder' | 'openUpload';
16
37
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC9B,OAAO,EAAE,CAAC,CAAC;IACX,uDAAuD;IACvD,WAAW,CAAC,EAAE,kBAAkB,EAAE,CAAC;IACnC,OAAO,CAAC,EAAE;QACT,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB,UAAU,CAAC,EAAE,OAAO,CAAC;QACrB,cAAc,CAAC,EAAE,OAAO,CAAC;KACzB,CAAC;CACF;AAED,MAAM,MAAM,kBAAkB,GAC3B,cAAc,GACd,YAAY,CAAC;AAEhB;;;GAGG;AACH,MAAM,MAAM,aAAa,GACtB,mBAAmB,GACnB,mBAAmB,GACnB,mBAAmB,GACnB,wBAAwB,CAAC;AAE5B,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,QAAQ,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,QAAQ,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,QAAQ,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,wBAAwB;IACxC,IAAI,EAAE,cAAc,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACb;AAED;;;GAGG;AACH,MAAM,MAAM,aAAa,GACtB,iBAAiB,GACjB,iBAAiB,GACjB,eAAe,CAAC;AAEnB;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC3C,uBAAuB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACnE,sBAAsB,CAAC,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC;CAC5C;AAED,0DAA0D;AAC1D,MAAM,WAAW,WAAW;IAC3B,wBAAwB;IACxB,OAAO,EAAE,CAAC,CAAC;IACX,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,cAAc,EAAE,MAAM,CAAC;IACvB,0BAA0B;IAC1B,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC;IACxB,mCAAmC;IACnC,UAAU,EAAE,WAAW,GAAG,QAAQ,CAAC;IACnC,+CAA+C;IAC/C,KAAK,EAAE,aAAa,CAAC;IACrB,4DAA4D;IAC5D,aAAa,EAAE,aAAa,CAAC;IAC7B,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,wDAAwD;IACxD,MAAM,EAAE,cAAc,CAAC;IACvB,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC9B,OAAO,EAAE,CAAC,CAAC;IACX,uDAAuD;IACvD,WAAW,CAAC,EAAE,kBAAkB,EAAE,CAAC;IACnC,OAAO,CAAC,EAAE;QACT,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB,UAAU,CAAC,EAAE,OAAO,CAAC;QACrB,cAAc,CAAC,EAAE,OAAO,CAAC;KACzB,CAAC;IACF;;;;;;;;;;OAUG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,kBAAkB,GAC3B,cAAc,GACd,YAAY,CAAC;AAEhB;;;GAGG;AACH,MAAM,MAAM,aAAa,GACtB,mBAAmB,GACnB,mBAAmB,GACnB,mBAAmB,GACnB,wBAAwB,CAAC;AAE5B,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,QAAQ,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,QAAQ,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,QAAQ,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,wBAAwB;IACxC,IAAI,EAAE,cAAc,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACb;AAED;;;GAGG;AACH,MAAM,MAAM,aAAa,GACtB,iBAAiB,GACjB,iBAAiB,GACjB,eAAe,CAAC;AAEnB;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC3C,uBAAuB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACnE,sBAAsB,CAAC,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC;CAC5C;AAED,0DAA0D;AAC1D,MAAM,WAAW,WAAW;IAC3B,wBAAwB;IACxB,OAAO,EAAE,CAAC,CAAC;IACX,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,cAAc,EAAE,MAAM,CAAC;IACvB,0BAA0B;IAC1B,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC;IACxB,mCAAmC;IACnC,UAAU,EAAE,WAAW,GAAG,QAAQ,CAAC;IACnC,+CAA+C;IAC/C,KAAK,EAAE,aAAa,CAAC;IACrB,4DAA4D;IAC5D,aAAa,EAAE,aAAa,CAAC;IAC7B,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,wDAAwD;IACxD,MAAM,EAAE,cAAc,CAAC;IACvB,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xeonr/renderer-sdk",
3
- "version": "1.0.4",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "license": "ISC",
6
6
  "description": "SDK for building custom renderers for upl.im integrations",
@@ -15,6 +15,10 @@
15
15
  "default": "./dist/react/index.js",
16
16
  "types": "./dist/react/index.d.ts"
17
17
  },
18
+ "./react/dom": {
19
+ "default": "./dist/react/dom.js",
20
+ "types": "./dist/react/dom.d.ts"
21
+ },
18
22
  "./protocol": {
19
23
  "default": "./dist/protocol.js",
20
24
  "types": "./dist/protocol.d.ts"
@@ -25,16 +29,22 @@
25
29
  }
26
30
  },
27
31
  "peerDependencies": {
28
- "react": ">=18.0.0"
32
+ "react": ">=18.0.0",
33
+ "react-dom": ">=18.0.0"
29
34
  },
30
35
  "peerDependenciesMeta": {
31
36
  "react": {
32
37
  "optional": true
38
+ },
39
+ "react-dom": {
40
+ "optional": true
33
41
  }
34
42
  },
35
43
  "devDependencies": {
36
44
  "@types/react": "^19.0.0",
45
+ "@types/react-dom": "^19.0.0",
37
46
  "react": "^19.0.0",
47
+ "react-dom": "^19.0.0",
38
48
  "typescript": "^5.8.3"
39
49
  },
40
50
  "scripts": {
package/src/client.ts CHANGED
@@ -6,8 +6,10 @@ import type {
6
6
  HostGenerateTokenResultMessage,
7
7
  IframeHistoryPushMessage,
8
8
  IframeHistoryReplaceMessage,
9
+ IframeCrashMessage,
10
+ RendererCrashKind,
9
11
  } from './protocol.js';
10
- import { isHostMessage } from './protocol.js';
12
+ import { buildCrashMessage, isHostMessage, postCrashToHost } from './protocol.js';
11
13
  import type { InitPayload, RendererApiAdapter, RendererConfig, RendererScope, RenderingType } from './types.js';
12
14
 
13
15
  export interface RendererClientOptions {
@@ -61,7 +63,11 @@ export class RendererClient {
61
63
  private currentPath: string = '/';
62
64
 
63
65
  private listener: ((event: MessageEvent) => void) | null = null;
66
+ private errorListener: ((event: ErrorEvent) => void) | null = null;
67
+ private rejectionListener: ((event: PromiseRejectionEvent) => void) | null = null;
64
68
  private hashChangeListener: (() => void) | null = null;
69
+ private readyRetryInterval: ReturnType<typeof setInterval> | null = null;
70
+ private readyRetryTimeout: ReturnType<typeof setTimeout> | null = null;
65
71
  private historyPatched = false;
66
72
  private originalPushState: typeof history.pushState | null = null;
67
73
  private originalReplaceState: typeof history.replaceState | null = null;
@@ -243,10 +249,19 @@ export class RendererClient {
243
249
  /** Remove all listeners and stop the client. */
244
250
  destroy(): void {
245
251
  this.destroyed = true;
252
+ this.stopReadyRetry();
246
253
  if (this.listener) {
247
254
  window.removeEventListener('message', this.listener);
248
255
  this.listener = null;
249
256
  }
257
+ if (this.errorListener) {
258
+ window.removeEventListener('error', this.errorListener);
259
+ this.errorListener = null;
260
+ }
261
+ if (this.rejectionListener) {
262
+ window.removeEventListener('unhandledrejection', this.rejectionListener);
263
+ this.rejectionListener = null;
264
+ }
250
265
  if (this.hashChangeListener) {
251
266
  window.removeEventListener('hashchange', this.hashChangeListener);
252
267
  this.hashChangeListener = null;
@@ -276,13 +291,41 @@ export class RendererClient {
276
291
  if (this.targetOrigin !== '*' && event.origin !== this.targetOrigin) return;
277
292
  if (!isHostMessage(event.data)) return;
278
293
 
294
+ // Any valid host message implies the host's listener is attached and the
295
+ // handshake is underway, so stop pinging.
296
+ this.stopReadyRetry();
279
297
  this.handleMessage(event.data);
280
298
  };
281
299
 
282
300
  window.addEventListener('message', this.listener);
283
301
 
284
- // Tell the host we're ready
302
+ this.installCrashHooks();
303
+
304
+ // Tell the host we're ready. We retry on an interval because a fast iframe can
305
+ // post 'ready' before the host has attached its message listener — the first
306
+ // ping is lost, the host times out, and the user has to click retry. Pinging
307
+ // until the host responds closes that race without changing the protocol.
285
308
  this.postToHost({ type: 'uplim:ready' });
309
+ this.readyRetryInterval = setInterval(() => {
310
+ if (this.destroyed) return;
311
+ this.postToHost({ type: 'uplim:ready' });
312
+ }, 200);
313
+ // Bound the retries: if the host genuinely isn't there after 8s, stop pinging
314
+ // so we don't spam a misbehaving parent.
315
+ this.readyRetryTimeout = setTimeout(() => {
316
+ this.stopReadyRetry();
317
+ }, 8000);
318
+ }
319
+
320
+ private stopReadyRetry(): void {
321
+ if (this.readyRetryInterval) {
322
+ clearInterval(this.readyRetryInterval);
323
+ this.readyRetryInterval = null;
324
+ }
325
+ if (this.readyRetryTimeout) {
326
+ clearTimeout(this.readyRetryTimeout);
327
+ this.readyRetryTimeout = null;
328
+ }
286
329
  }
287
330
 
288
331
  private handleMessage(message: HostMessage): void {
@@ -423,4 +466,64 @@ export class RendererClient {
423
466
  if (this.destroyed) return;
424
467
  window.parent.postMessage(message, this.targetOrigin);
425
468
  }
469
+
470
+ /**
471
+ * Install window-level crash hooks the moment the client is constructed
472
+ * (i.e. as soon as a renderer touches the SDK). We deliberately do this
473
+ * here — not in a React-only entry point — so that even renderers that
474
+ * forget to wrap their root in `<RendererErrorBoundary>` still report
475
+ * async / global failures to the host.
476
+ *
477
+ * The two hooks cover the failure modes a React boundary can't see:
478
+ * - `window.error` — synchronous throws outside React's tree (raw
479
+ * <script> bugs, event-handler exceptions, image onerror handlers).
480
+ * - `unhandledrejection` — Promise chains with no `.catch()`, which
481
+ * are surprisingly common in fetch-heavy renderers.
482
+ *
483
+ * Both fire-and-forget via `postCrashToHost`; the host decides whether
484
+ * to render the overlay (a single render-time crash usually warrants
485
+ * it, a stray unhandled rejection may not).
486
+ */
487
+ private installCrashHooks(): void {
488
+ if (typeof window === 'undefined') return;
489
+
490
+ this.errorListener = (event: ErrorEvent) => {
491
+ if (this.destroyed) return;
492
+ // Prefer event.error for the real stack; fall back to a synthetic
493
+ // shape when the browser only gave us message/filename/lineno
494
+ // (cross-origin scripts strip event.error to null).
495
+ const thrown = event.error ?? {
496
+ name: 'ErrorEvent',
497
+ message: event.message || 'Uncaught error',
498
+ stack: event.filename
499
+ ? `at ${event.filename}:${event.lineno ?? '?'}:${event.colno ?? '?'}`
500
+ : '',
501
+ };
502
+ this.reportCrash('error', thrown);
503
+ };
504
+ window.addEventListener('error', this.errorListener);
505
+
506
+ this.rejectionListener = (event: PromiseRejectionEvent) => {
507
+ if (this.destroyed) return;
508
+ this.reportCrash('unhandled-rejection', event.reason);
509
+ };
510
+ window.addEventListener('unhandledrejection', this.rejectionListener);
511
+ }
512
+
513
+ /**
514
+ * Forward a crash to the host. Called by the global hooks and by
515
+ * `<RendererErrorBoundary>`; renderers may also call it explicitly if
516
+ * they catch something themselves and want it surfaced (e.g. data-load
517
+ * failure that wipes the UI even though no exception escaped).
518
+ *
519
+ * No-throw: telemetry must never make the underlying crash worse.
520
+ */
521
+ reportCrash(kind: RendererCrashKind, thrown: unknown): void {
522
+ try {
523
+ const msg: IframeCrashMessage = buildCrashMessage(kind, thrown);
524
+ postCrashToHost(msg, this.targetOrigin);
525
+ } catch {
526
+ // Swallowed on purpose — see above.
527
+ }
528
+ }
426
529
  }
package/src/index.ts CHANGED
@@ -20,6 +20,8 @@ export {
20
20
  isRendererMessage,
21
21
  isHostMessage,
22
22
  isIframeMessage,
23
+ buildCrashMessage,
24
+ postCrashToHost,
23
25
  } from './protocol.js';
24
26
 
25
27
  export type {
@@ -39,5 +41,7 @@ export type {
39
41
  IframeGenerateTokenMessage,
40
42
  IframeHistoryPushMessage,
41
43
  IframeHistoryReplaceMessage,
44
+ IframeCrashMessage,
45
+ RendererCrashKind,
42
46
  RendererMessage,
43
47
  } from './protocol.js';
package/src/protocol.ts CHANGED
@@ -104,6 +104,53 @@ export interface IframeHistoryReplaceMessage {
104
104
  path: string;
105
105
  }
106
106
 
107
+ /**
108
+ * Which SDK code path observed the failure. Maps directly to the host's
109
+ * `RendererCrashKind` enum so the wire schema and the telemetry shape
110
+ * stay in sync.
111
+ *
112
+ * - `render` — React tree threw during render/commit; caught by the
113
+ * SDK's mandatory ErrorBoundary at the renderer root. The UI is gone.
114
+ * - `error` — `window.onerror`: synchronous uncaught exception outside
115
+ * React (event handler, top-level script). UI may or may not still
116
+ * be visible.
117
+ * - `unhandled-rejection` — `window.onunhandledrejection`: a Promise
118
+ * rejected with no `.catch()`. Almost always async, often non-fatal,
119
+ * but a useful signal that something is wrong.
120
+ */
121
+ export type RendererCrashKind = 'render' | 'error' | 'unhandled-rejection';
122
+
123
+ /**
124
+ * Tells the host the renderer just hit an unhandled exception. The host
125
+ * decides what to render in response (typically an overlay with reload
126
+ * and fallback options); the renderer never paints its own crash screen
127
+ * because (a) styling would be inconsistent with the host UI and (b) a
128
+ * render-time crash may have left the renderer's CSS / DOM unusable.
129
+ *
130
+ * Fields are deliberately string-bounded on the proto side (see
131
+ * `ReportRendererCrashRequest`) — keep payloads under those limits
132
+ * before posting.
133
+ */
134
+ export interface IframeCrashMessage {
135
+ type: 'uplim:crash';
136
+ kind: RendererCrashKind;
137
+ /** Error.name (e.g. `TypeError`). Empty when the source threw a non-Error. */
138
+ name: string;
139
+ /** Error.message — user-facing description of the failure. */
140
+ message: string;
141
+ /** Stack trace if the browser exposed one. May be minified in prod builds. */
142
+ stack: string;
143
+ /** `window.location.href` at the time of the crash, for narrowing down the route. */
144
+ rendererLocation?: string;
145
+ /** Iframe-side wall-clock millis when the crash was observed. */
146
+ reportedAtMs: number;
147
+ // NOTE: things the parent can observe for itself (user agent, viewport,
148
+ // buildHash from the rendererConfig the host already holds) are
149
+ // captured host-side in useRendererBridge — we don't trust the iframe
150
+ // to populate them. The iframe only sends what *only* it can know:
151
+ // the crash facts.
152
+ }
153
+
107
154
  export type IframeMessage =
108
155
  | IframeReadyMessage
109
156
  | IframeOpenUploadMessage
@@ -112,7 +159,8 @@ export type IframeMessage =
112
159
  | IframeInitAckMessage
113
160
  | IframeGenerateTokenMessage
114
161
  | IframeHistoryPushMessage
115
- | IframeHistoryReplaceMessage;
162
+ | IframeHistoryReplaceMessage
163
+ | IframeCrashMessage;
116
164
 
117
165
  // ---------------------------------------------------------------------------
118
166
  // Union type for any message
@@ -138,6 +186,100 @@ export function isHostMessage(data: unknown): data is HostMessage {
138
186
  return t === 'uplim:init' || t === 'uplim:theme' || t === 'uplim:token' || t === 'uplim:generateTokenResult' || t === 'uplim:historyBack' || t === 'uplim:historyForward';
139
187
  }
140
188
 
189
+ // Proto-side limits (see ReportRendererCrashRequest) — clamp before sending so
190
+ // we don't get truncated mid-string by buf.validate and miss the crash entirely.
191
+ const CRASH_NAME_MAX = 256;
192
+ const CRASH_MESSAGE_MAX = 4096;
193
+ const CRASH_STACK_MAX = 32768;
194
+ const CRASH_LOCATION_MAX = 2048;
195
+
196
+ function clampString(value: string, max: number): string {
197
+ if (value.length <= max) return value;
198
+ // Reserve a marker so it's obvious in telemetry that we truncated.
199
+ const suffix = '…[truncated]';
200
+ return value.slice(0, max - suffix.length) + suffix;
201
+ }
202
+
203
+ /**
204
+ * Build an `IframeCrashMessage` from an unknown thrown value. Centralised so
205
+ * the React boundary and the global `error` / `unhandledrejection` hooks all
206
+ * produce identically-shaped payloads regardless of what JS threw at them
207
+ * (strings, objects, DOMExceptions, real Errors, …).
208
+ *
209
+ * Only includes what the iframe is the sole source-of-truth for —
210
+ * `name`, `message`, `stack`, `rendererLocation`, `kind`. UA, viewport,
211
+ * buildHash are deliberately omitted: the parent observes them directly
212
+ * (and trusting the iframe to self-report those opens a small but
213
+ * pointless spoof surface).
214
+ */
215
+ export function buildCrashMessage(
216
+ kind: RendererCrashKind,
217
+ thrown: unknown,
218
+ ): IframeCrashMessage {
219
+ let name = '';
220
+ let message = '';
221
+ let stack = '';
222
+ if (thrown instanceof Error) {
223
+ name = thrown.name || 'Error';
224
+ message = thrown.message || String(thrown);
225
+ stack = thrown.stack ?? '';
226
+ } else if (typeof thrown === 'string') {
227
+ name = 'StringError';
228
+ message = thrown;
229
+ } else if (thrown && typeof thrown === 'object') {
230
+ const obj = thrown as { name?: unknown; message?: unknown; stack?: unknown };
231
+ name = typeof obj.name === 'string' ? obj.name : 'UnknownError';
232
+ message = typeof obj.message === 'string' ? obj.message : safeStringify(thrown);
233
+ stack = typeof obj.stack === 'string' ? obj.stack : '';
234
+ } else {
235
+ name = 'UnknownError';
236
+ message = String(thrown);
237
+ }
238
+
239
+ let rendererLocation: string | undefined;
240
+ if (typeof window !== 'undefined' && window.location) {
241
+ rendererLocation = clampString(window.location.href, CRASH_LOCATION_MAX);
242
+ }
243
+
244
+ return {
245
+ type: 'uplim:crash',
246
+ kind,
247
+ name: clampString(name, CRASH_NAME_MAX),
248
+ message: clampString(message, CRASH_MESSAGE_MAX),
249
+ stack: clampString(stack, CRASH_STACK_MAX),
250
+ rendererLocation,
251
+ reportedAtMs: Date.now(),
252
+ };
253
+ }
254
+
255
+ function safeStringify(value: unknown): string {
256
+ try {
257
+ return JSON.stringify(value);
258
+ } catch {
259
+ return String(value);
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Fire-and-forget post of a crash message to the host. Used by both the SDK's
265
+ * global hooks and the React boundary. Falls back silently when called outside
266
+ * an iframe (e.g. SSR tests) so the SDK never throws on `window.parent` being
267
+ * `undefined`.
268
+ *
269
+ * `targetOrigin` defaults to `'*'` — at crash time we deliberately do not
270
+ * filter, because the renderer may have lost the host origin context (e.g.
271
+ * if the crash happened before `init` arrived).
272
+ */
273
+ export function postCrashToHost(message: IframeCrashMessage, targetOrigin: string = '*'): void {
274
+ if (typeof window === 'undefined' || !window.parent || window.parent === window) return;
275
+ try {
276
+ window.parent.postMessage(message, targetOrigin);
277
+ } catch {
278
+ // postMessage can throw on some browsers if the target origin is bad
279
+ // (cross-origin frame missing). Telemetry is best-effort.
280
+ }
281
+ }
282
+
141
283
  export function isIframeMessage(data: unknown): data is IframeMessage {
142
284
  if (!isRendererMessage(data)) return false;
143
285
  return !isHostMessage(data);
@@ -0,0 +1,95 @@
1
+ import { Component, type ErrorInfo, type ReactNode } from 'react';
2
+ import { buildCrashMessage, postCrashToHost } from '../protocol.js';
3
+
4
+ /**
5
+ * React error boundary that catches render-time exceptions and forwards
6
+ * them to the host via `uplim:crash`. **Renderers should wrap their root
7
+ * in this component** — the SDK's global window hooks catch async
8
+ * failures, but they cannot catch errors thrown during React render or
9
+ * commit (those abort the tree before bubbling to `window.error`).
10
+ *
11
+ * Deliberately renders **nothing** on crash. The host owns the overlay
12
+ * UI for two reasons:
13
+ * 1. Styling stays consistent with the dashboard chrome — a renderer's
14
+ * CSS may itself be the thing that broke, so painting an inline
15
+ * fallback inside the iframe risks an invisible / unreadable
16
+ * screen.
17
+ * 2. The host's overlay can offer recovery actions the renderer
18
+ * can't, like "open the built-in preview instead" (which involves
19
+ * tearing this iframe down and rendering something else).
20
+ *
21
+ * Renderers may pass a `fallback` if they want a minimal visible
22
+ * placeholder while the host's overlay paints (e.g. so users don't
23
+ * stare at a transparent iframe for a frame), but the host overlay
24
+ * remains the source of truth.
25
+ *
26
+ * Usage:
27
+ *
28
+ * ```tsx
29
+ * import { createRoot } from 'react-dom/client';
30
+ * import { RendererErrorBoundary } from '@xeonr/renderer-sdk/react';
31
+ *
32
+ * createRoot(document.getElementById('root')!).render(
33
+ * <RendererErrorBoundary>
34
+ * <App />
35
+ * </RendererErrorBoundary>
36
+ * );
37
+ * ```
38
+ */
39
+ export interface RendererErrorBoundaryProps {
40
+ children: ReactNode;
41
+ /** Optional inline fallback rendered after the crash is reported. Default: nothing. */
42
+ fallback?: ReactNode;
43
+ /**
44
+ * Optional targetOrigin override for the postMessage. Matches
45
+ * `RendererClientOptions.targetOrigin`. Defaults to `'*'` because at
46
+ * crash time we don't want to gate telemetry on origin matching.
47
+ */
48
+ targetOrigin?: string;
49
+ /** Optional hook called after the host has been notified — useful for sentry / console wiring. */
50
+ onCrash?: (error: unknown, info: ErrorInfo) => void;
51
+ }
52
+
53
+ interface State {
54
+ crashed: boolean;
55
+ }
56
+
57
+ export class RendererErrorBoundary extends Component<RendererErrorBoundaryProps, State> {
58
+ state: State = { crashed: false };
59
+
60
+ static getDerivedStateFromError(): State {
61
+ return { crashed: true };
62
+ }
63
+
64
+ componentDidCatch(error: unknown, info: ErrorInfo): void {
65
+ // Build the crash payload via the shared helper so global hooks and the
66
+ // boundary produce identical wire shapes.
67
+ const msg = buildCrashMessage('render', error);
68
+
69
+ // React stack is more useful than the JS stack for render-time bugs —
70
+ // it points at the component path, not just the throwing function. We
71
+ // append it so server-side logs preserve both.
72
+ if (info?.componentStack) {
73
+ const componentStack = `\n\nReact component stack:\n${info.componentStack}`;
74
+ // Respect the proto's stack limit (32768) — buildCrashMessage already
75
+ // clamped, but we have new content to add, so re-clamp the combined value.
76
+ const combined = (msg.stack ? `${msg.stack}\n` : '') + componentStack;
77
+ msg.stack = combined.length > 32768 ? combined.slice(0, 32768 - 12) + '…[truncated]' : combined;
78
+ }
79
+
80
+ postCrashToHost(msg, this.props.targetOrigin ?? '*');
81
+
82
+ try {
83
+ this.props.onCrash?.(error, info);
84
+ } catch {
85
+ // Best-effort: never let the consumer's handler resurface the crash.
86
+ }
87
+ }
88
+
89
+ render(): ReactNode {
90
+ if (this.state.crashed) {
91
+ return this.props.fallback ?? null;
92
+ }
93
+ return this.props.children;
94
+ }
95
+ }