@persona/layout-editor-remote-module 0.0.0-bootstrap.0 → 0.5.1
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/README.md +164 -0
- package/dist/bridge-frame.d.mts +48 -0
- package/dist/bridge-frame.mjs +350 -0
- package/dist/bridge-host.d.mts +61 -0
- package/dist/bridge-host.mjs +217 -0
- package/dist/define-C-3P4CFz.d.mts +223 -0
- package/dist/index.d.mts +98 -0
- package/dist/index.mjs +58 -0
- package/dist/protocol-Dz3ZX59L.mjs +207 -0
- package/dist/protocol-edoRdXpF.d.mts +30 -0
- package/dist/schema-Bc3ZwB8y.mjs +103 -0
- package/dist/vite-plugin.d.mts +43 -0
- package/dist/vite-plugin.mjs +434 -0
- package/package.json +117 -4
- package/skills/getting-started/SKILL.md +374 -0
package/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# @persona/layout-editor-remote-module
|
|
2
|
+
|
|
3
|
+
Shared manifest format and Vite plugin for remotely-defined dashboard layout modules — used by
|
|
4
|
+
Persona dashboard apps to declare a layout module directly in a typical app's `vite.config.ts`,
|
|
5
|
+
without editing the host.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
yarn add @persona/layout-editor-remote-module
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`vite`, `esbuild`, and `zod` are peer dependencies. The plugin's dev-mode bundling uses `esbuild`
|
|
14
|
+
directly, and declaring it as a peer means it resolves to the same copy `vite` already pulls in
|
|
15
|
+
rather than a second version. With Yarn's `node-modules` linker the hoisted transitive copy usually
|
|
16
|
+
satisfies it; add `esbuild` to your own `devDependencies` if your package manager warns about an
|
|
17
|
+
unmet peer.
|
|
18
|
+
|
|
19
|
+
The optional `react` and `react-dom` peer dependencies are required only when using the React bridge
|
|
20
|
+
components.
|
|
21
|
+
|
|
22
|
+
The package is published to the public npm registry.
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
// vite.config.ts
|
|
28
|
+
import {
|
|
29
|
+
defineLayoutModule,
|
|
30
|
+
personaLayoutModules,
|
|
31
|
+
} from '@persona/layout-editor-remote-module/vite-plugin';
|
|
32
|
+
|
|
33
|
+
export default defineConfig({
|
|
34
|
+
plugins: [
|
|
35
|
+
react(),
|
|
36
|
+
personaLayoutModules({
|
|
37
|
+
modules: [
|
|
38
|
+
defineLayoutModule({
|
|
39
|
+
key: 'risk_summary',
|
|
40
|
+
displayName: 'Risk Summary',
|
|
41
|
+
descriptionShort: 'Show the account risk score',
|
|
42
|
+
allowAddingToLayout: true,
|
|
43
|
+
layouts: ['case'],
|
|
44
|
+
defaultSize: { case: { width: 4, height: 20 } },
|
|
45
|
+
// `.tsx`/`.js` — a `code` delivery, imported and rendered directly
|
|
46
|
+
// in the host's own React tree.
|
|
47
|
+
render: { entry: './src/layout-modules/risk-summary/render.tsx' },
|
|
48
|
+
}),
|
|
49
|
+
],
|
|
50
|
+
}),
|
|
51
|
+
],
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
An empty or omitted `layouts` makes the module available in every layout. When `layouts` lists
|
|
56
|
+
specific layouts, `defaultSize` must provide an entry for each one.
|
|
57
|
+
|
|
58
|
+
An `.html` entry (rather than `.tsx`/`.js`) is a `document` delivery instead — the host iframes it
|
|
59
|
+
rather than importing anything. See `LayoutModuleEntry`'s doc comment in `src/define.ts` for the
|
|
60
|
+
full contract, including the optional `path` field for a client-routed SPA.
|
|
61
|
+
|
|
62
|
+
## Communicating with the host
|
|
63
|
+
|
|
64
|
+
Document-delivered modules can use the browser bridge to receive layout data, report their height,
|
|
65
|
+
and resolve data paths through the host. The host renders the document with `RemoteModuleFrame`:
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
import { RemoteModuleFrame } from '@persona/layout-editor-remote-module/bridge-host';
|
|
69
|
+
|
|
70
|
+
<RemoteModuleFrame
|
|
71
|
+
documentUrl={registration.document}
|
|
72
|
+
moduleOrigin={registration.origin}
|
|
73
|
+
title='Risk summary'
|
|
74
|
+
sandbox='allow-scripts allow-same-origin'
|
|
75
|
+
data={moduleData}
|
|
76
|
+
context={moduleContext}
|
|
77
|
+
dynamicHeight
|
|
78
|
+
resolveDataPath={resolveDataPath}
|
|
79
|
+
/>;
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The required `sandbox` value is chosen by the host.
|
|
83
|
+
|
|
84
|
+
By default, the bridge accepts host origins matching `*.withpersona.com`,
|
|
85
|
+
`*.withpersona-staging.com`, `localhost`, and `127.0.0.1`. Add development or preview origins with
|
|
86
|
+
`allowedHostOrigins`, optionally composing the exported `personaDashboardHostOrigins` helper:
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
import {
|
|
90
|
+
LayoutModuleBridge,
|
|
91
|
+
personaDashboardHostOrigins,
|
|
92
|
+
} from '@persona/layout-editor-remote-module/bridge-frame';
|
|
93
|
+
|
|
94
|
+
<LayoutModuleBridge
|
|
95
|
+
allowedHostOrigins={(origin) => personaDashboardHostOrigins(origin) || myDevHost(origin)}
|
|
96
|
+
>
|
|
97
|
+
{({ data }) => <RiskSummary data={data} />}
|
|
98
|
+
</LayoutModuleBridge>;
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The document entry uses `LayoutModuleBridge` as a render-prop component:
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
import { LayoutModuleBridge } from '@persona/layout-editor-remote-module/bridge-frame';
|
|
105
|
+
|
|
106
|
+
<LayoutModuleBridge>
|
|
107
|
+
{({ data, context, resolveDataPath }) => (
|
|
108
|
+
<RiskSummary data={data} context={context} resolveDataPath={resolveDataPath} />
|
|
109
|
+
)}
|
|
110
|
+
</LayoutModuleBridge>;
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Alongside `data` (the module's persisted config), the host sends a typed `context` payload
|
|
114
|
+
(`RemoteModuleContext`) describing where the module is rendered — facts the host owns and the module
|
|
115
|
+
cannot derive itself:
|
|
116
|
+
|
|
117
|
+
- `layoutType` — which layout kind the module is placed on (`account`, `case`, or `transaction`)
|
|
118
|
+
- `layoutObjectId` — the ID of the object the layout is rendered for (e.g. the viewed case's ID on a
|
|
119
|
+
`case` layout), or `null` when there is none
|
|
120
|
+
- `isReadOnly` — the host object is locked for edits
|
|
121
|
+
- `isInEditor` — the module is rendered inside the layout editor rather than on a live record
|
|
122
|
+
- `isUsingStubData` — the host is feeding placeholder/stub data instead of a real record
|
|
123
|
+
|
|
124
|
+
`context` is sent with `init` and pushed again whenever it changes, just like `data`; inside the
|
|
125
|
+
iframe it arrives through `ready()`, `getContext()`, and the `context` render prop (all
|
|
126
|
+
`RemoteModuleContext | null` until init).
|
|
127
|
+
|
|
128
|
+
The host still appends `access_token`, `environment_id`, and `locale` as query parameters. The app
|
|
129
|
+
hosting the module consumes and strips those parameters itself, so by the time `LayoutModuleBridge`
|
|
130
|
+
runs they have already been handled; the bridge neither reads nor strips them. Keep the host
|
|
131
|
+
iframe's `referrerpolicy` at its default `no-referrer` (or use `strict-origin`) so the URL
|
|
132
|
+
parameters cannot leak through subsequent requests.
|
|
133
|
+
|
|
134
|
+
The bridge's security model is intentionally narrow:
|
|
135
|
+
|
|
136
|
+
- The host sends only to the exact module origin and accepts messages only from that origin and
|
|
137
|
+
iframe window.
|
|
138
|
+
- The iframe accepts only messages from its parent window and an allowed host origin, then pins the
|
|
139
|
+
first trusted origin for the lifetime of the bridge.
|
|
140
|
+
- The iframe derives the ready target from `ancestorOrigins` or the referrer, and the host retries
|
|
141
|
+
`init` until the frame answers.
|
|
142
|
+
- Messages use a strict, versioned protocol. Credentials remain in the iframe URL and are never
|
|
143
|
+
handled through bridge messages.
|
|
144
|
+
- `extraParams` rejects reserved names and credential-like keys.
|
|
145
|
+
|
|
146
|
+
### Consuming a manifest
|
|
147
|
+
|
|
148
|
+
The plugin emits `{ modules: RemoteModuleRegistrationManifestV1[] }` at `manifestPath`. A host
|
|
149
|
+
fetching that document should run it through `parseRemoteModuleManifest` — it validates the
|
|
150
|
+
envelope, every module, and key uniqueness, and returns the typed document:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import {
|
|
154
|
+
parseRemoteModuleManifest,
|
|
155
|
+
RemoteModuleManifestParseError,
|
|
156
|
+
} from '@persona/layout-editor-remote-module';
|
|
157
|
+
|
|
158
|
+
const response = await fetch(`${appOrigin}/_layout_modules_manifest.json`);
|
|
159
|
+
const { modules } = parseRemoteModuleManifest(await response.json());
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
It throws `RemoteModuleManifestParseError` (with `.issues` and a prettified `.message`) on a
|
|
163
|
+
malformed document; `safeParseRemoteModuleManifest` returns `{ success, data | error }` instead of
|
|
164
|
+
throwing.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { n as Json, t as RemoteModuleContext } from "./protocol-edoRdXpF.mjs";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/bridge/origin.d.ts
|
|
5
|
+
declare function personaDashboardHostOrigins(origin: string): boolean;
|
|
6
|
+
//#endregion
|
|
7
|
+
//#region src/bridge/iframe/createModuleBridge.d.ts
|
|
8
|
+
type ModuleBridgeInitPayload = {
|
|
9
|
+
data: Json;
|
|
10
|
+
context: RemoteModuleContext | null;
|
|
11
|
+
};
|
|
12
|
+
type CreateModuleBridgeOptions = {
|
|
13
|
+
allowedHostOrigins?: (string | RegExp)[] | ((origin: string) => boolean);
|
|
14
|
+
resolveTimeoutMs?: number;
|
|
15
|
+
};
|
|
16
|
+
type ModuleBridge = {
|
|
17
|
+
ready: () => Promise<ModuleBridgeInitPayload>;
|
|
18
|
+
getData: () => Json;
|
|
19
|
+
getContext: () => RemoteModuleContext | null;
|
|
20
|
+
subscribe: (listener: () => void) => () => void;
|
|
21
|
+
reportRenderError: (message: string) => void;
|
|
22
|
+
resolveDataPath: (dataPath: string, variableMap?: Record<string, string | number>) => Promise<Json>;
|
|
23
|
+
observeHeight: (element: Element) => () => void;
|
|
24
|
+
destroy: () => void;
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/bridge/iframe/LayoutModuleBridge.d.ts
|
|
28
|
+
type LayoutModuleBridgeRenderProps = {
|
|
29
|
+
data: ReturnType<ModuleBridge['getData']>;
|
|
30
|
+
context: RemoteModuleContext | null;
|
|
31
|
+
resolveDataPath: ModuleBridge['resolveDataPath'];
|
|
32
|
+
};
|
|
33
|
+
type LayoutModuleBridgeProps = {
|
|
34
|
+
allowedHostOrigins?: CreateModuleBridgeOptions['allowedHostOrigins'];
|
|
35
|
+
resolveTimeoutMs?: number;
|
|
36
|
+
observeHeight?: boolean;
|
|
37
|
+
fallback?: ReactNode;
|
|
38
|
+
children: (props: LayoutModuleBridgeRenderProps) => ReactNode;
|
|
39
|
+
};
|
|
40
|
+
declare function LayoutModuleBridge({
|
|
41
|
+
allowedHostOrigins,
|
|
42
|
+
resolveTimeoutMs,
|
|
43
|
+
observeHeight,
|
|
44
|
+
fallback,
|
|
45
|
+
children
|
|
46
|
+
}: LayoutModuleBridgeProps): ReactNode;
|
|
47
|
+
//#endregion
|
|
48
|
+
export { type Json, LayoutModuleBridge, type LayoutModuleBridgeProps, type LayoutModuleBridgeRenderProps, type RemoteModuleContext, personaDashboardHostOrigins };
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { c as resolveAllowedHostOrigins, n as SOURCE, o as candidateParentOrigin, r as parseHostToModuleMessage, s as personaDashboardHostOrigins, t as PROTOCOL_VERSION } from "./protocol-Dz3ZX59L.mjs";
|
|
2
|
+
import { Component, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
|
|
5
|
+
//#region src/bridge/rpc.ts
|
|
6
|
+
var BridgeDestroyedError = class extends Error {
|
|
7
|
+
constructor(message = "Bridge has been destroyed") {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "BridgeDestroyedError";
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var BridgeTimeoutError = class extends Error {
|
|
13
|
+
constructor(message = "Bridge request timed out") {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "BridgeTimeoutError";
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
function createRequestId() {
|
|
19
|
+
const cryptoApi = globalThis.crypto;
|
|
20
|
+
if (cryptoApi?.randomUUID) return cryptoApi.randomUUID();
|
|
21
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
22
|
+
}
|
|
23
|
+
function createPendingRequests() {
|
|
24
|
+
const pending = /* @__PURE__ */ new Map();
|
|
25
|
+
return {
|
|
26
|
+
add(timeoutMs) {
|
|
27
|
+
const requestId = createRequestId();
|
|
28
|
+
return {
|
|
29
|
+
requestId,
|
|
30
|
+
promise: new Promise((resolve, reject) => {
|
|
31
|
+
const timer = setTimeout(() => {
|
|
32
|
+
pending.delete(requestId);
|
|
33
|
+
reject(new BridgeTimeoutError());
|
|
34
|
+
}, timeoutMs);
|
|
35
|
+
pending.set(requestId, {
|
|
36
|
+
resolve,
|
|
37
|
+
reject,
|
|
38
|
+
timer
|
|
39
|
+
});
|
|
40
|
+
})
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
settle(requestId, settlement) {
|
|
44
|
+
const request = pending.get(requestId);
|
|
45
|
+
if (!request) return false;
|
|
46
|
+
pending.delete(requestId);
|
|
47
|
+
clearTimeout(request.timer);
|
|
48
|
+
if ("error" in settlement) request.reject(settlement.error);
|
|
49
|
+
else request.resolve(settlement.result);
|
|
50
|
+
return true;
|
|
51
|
+
},
|
|
52
|
+
rejectAll(error) {
|
|
53
|
+
for (const [requestId, request] of pending) {
|
|
54
|
+
pending.delete(requestId);
|
|
55
|
+
clearTimeout(request.timer);
|
|
56
|
+
request.reject(error);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region src/bridge/iframe/createModuleBridge.ts
|
|
64
|
+
function createModuleBridge({ allowedHostOrigins = personaDashboardHostOrigins, resolveTimeoutMs = 3e4 } = {}) {
|
|
65
|
+
const isEmbedded = typeof window !== "undefined" && window.parent !== window;
|
|
66
|
+
const allowedOrigin = resolveAllowedHostOrigins(allowedHostOrigins);
|
|
67
|
+
if (!isEmbedded) {
|
|
68
|
+
const notEmbeddedError = new BridgeDestroyedError("Bridge is not embedded");
|
|
69
|
+
return {
|
|
70
|
+
ready: async () => ({
|
|
71
|
+
data: null,
|
|
72
|
+
context: null
|
|
73
|
+
}),
|
|
74
|
+
getData: () => null,
|
|
75
|
+
getContext: () => null,
|
|
76
|
+
subscribe: () => () => {},
|
|
77
|
+
reportRenderError: () => {},
|
|
78
|
+
resolveDataPath: async () => {
|
|
79
|
+
throw notEmbeddedError;
|
|
80
|
+
},
|
|
81
|
+
observeHeight: () => () => {},
|
|
82
|
+
destroy: () => {}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
let destroyed = false;
|
|
86
|
+
let hostOrigin;
|
|
87
|
+
let data = null;
|
|
88
|
+
let context = null;
|
|
89
|
+
let readyResolved = false;
|
|
90
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
91
|
+
const observers = /* @__PURE__ */ new Set();
|
|
92
|
+
const pendingRequests = createPendingRequests();
|
|
93
|
+
const queuedRequests = /* @__PURE__ */ new Map();
|
|
94
|
+
let resolveReady;
|
|
95
|
+
let rejectReady;
|
|
96
|
+
const readyPromise = new Promise((resolve, reject) => {
|
|
97
|
+
resolveReady = resolve;
|
|
98
|
+
rejectReady = reject;
|
|
99
|
+
});
|
|
100
|
+
const post = (message) => {
|
|
101
|
+
if (hostOrigin) window.parent.postMessage(message, hostOrigin);
|
|
102
|
+
};
|
|
103
|
+
const flushQueuedRequests = () => {
|
|
104
|
+
if (!hostOrigin) return;
|
|
105
|
+
for (const [requestId, request] of queuedRequests) {
|
|
106
|
+
post({
|
|
107
|
+
source: SOURCE,
|
|
108
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
109
|
+
type: "resolve-data-path",
|
|
110
|
+
requestId,
|
|
111
|
+
dataPath: request.dataPath,
|
|
112
|
+
...request.variableMap === void 0 ? {} : { variableMap: request.variableMap }
|
|
113
|
+
});
|
|
114
|
+
queuedRequests.delete(requestId);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const pinHostOrigin = (origin) => {
|
|
118
|
+
if (!allowedOrigin(origin)) return false;
|
|
119
|
+
if (hostOrigin !== void 0 && hostOrigin !== origin) return false;
|
|
120
|
+
hostOrigin = origin;
|
|
121
|
+
flushQueuedRequests();
|
|
122
|
+
return true;
|
|
123
|
+
};
|
|
124
|
+
const notify = () => {
|
|
125
|
+
for (const listener of listeners) listener();
|
|
126
|
+
};
|
|
127
|
+
const onMessage = (event) => {
|
|
128
|
+
if (destroyed || event.source !== window.parent) return;
|
|
129
|
+
const message = parseHostToModuleMessage(event.data);
|
|
130
|
+
if (!message || !pinHostOrigin(event.origin)) return;
|
|
131
|
+
switch (message.type) {
|
|
132
|
+
case "init":
|
|
133
|
+
data = message.data;
|
|
134
|
+
context = message.context;
|
|
135
|
+
notify();
|
|
136
|
+
if (!readyResolved) {
|
|
137
|
+
readyResolved = true;
|
|
138
|
+
resolveReady({
|
|
139
|
+
data,
|
|
140
|
+
context
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
break;
|
|
144
|
+
case "data":
|
|
145
|
+
data = message.data;
|
|
146
|
+
notify();
|
|
147
|
+
break;
|
|
148
|
+
case "context":
|
|
149
|
+
context = message.context;
|
|
150
|
+
notify();
|
|
151
|
+
break;
|
|
152
|
+
case "data-path-result":
|
|
153
|
+
if (message.error !== void 0) pendingRequests.settle(message.requestId, { error: new Error(message.error) });
|
|
154
|
+
else pendingRequests.settle(message.requestId, { result: message.result ?? null });
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
window.addEventListener("message", onMessage);
|
|
159
|
+
const parentOrigin = candidateParentOrigin();
|
|
160
|
+
if (parentOrigin && allowedOrigin(parentOrigin)) window.parent.postMessage({
|
|
161
|
+
source: SOURCE,
|
|
162
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
163
|
+
type: "ready"
|
|
164
|
+
}, parentOrigin);
|
|
165
|
+
return {
|
|
166
|
+
ready: () => readyPromise,
|
|
167
|
+
getData: () => data,
|
|
168
|
+
getContext: () => context,
|
|
169
|
+
subscribe(listener) {
|
|
170
|
+
listeners.add(listener);
|
|
171
|
+
return () => listeners.delete(listener);
|
|
172
|
+
},
|
|
173
|
+
reportRenderError(message) {
|
|
174
|
+
post({
|
|
175
|
+
source: SOURCE,
|
|
176
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
177
|
+
type: "render-error",
|
|
178
|
+
message
|
|
179
|
+
});
|
|
180
|
+
},
|
|
181
|
+
resolveDataPath(dataPath, variableMap) {
|
|
182
|
+
if (destroyed) return Promise.reject(new BridgeDestroyedError());
|
|
183
|
+
const { requestId, promise } = pendingRequests.add(resolveTimeoutMs);
|
|
184
|
+
queuedRequests.set(requestId, {
|
|
185
|
+
dataPath,
|
|
186
|
+
variableMap
|
|
187
|
+
});
|
|
188
|
+
flushQueuedRequests();
|
|
189
|
+
return promise;
|
|
190
|
+
},
|
|
191
|
+
observeHeight(element) {
|
|
192
|
+
if (destroyed || typeof ResizeObserver === "undefined") return () => {};
|
|
193
|
+
let lastHeight;
|
|
194
|
+
let frame;
|
|
195
|
+
let disconnected = false;
|
|
196
|
+
const postHeight = (height) => {
|
|
197
|
+
if (disconnected || height === lastHeight) return;
|
|
198
|
+
lastHeight = height;
|
|
199
|
+
post({
|
|
200
|
+
source: SOURCE,
|
|
201
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
202
|
+
type: "set-height",
|
|
203
|
+
height
|
|
204
|
+
});
|
|
205
|
+
};
|
|
206
|
+
const observer = new ResizeObserver((entries) => {
|
|
207
|
+
const entry = entries[0];
|
|
208
|
+
if (!entry) return;
|
|
209
|
+
const borderBoxSize = entry.borderBoxSize;
|
|
210
|
+
const height = (Array.isArray(borderBoxSize) ? borderBoxSize[0]?.blockSize : void 0) ?? entry.contentRect.height;
|
|
211
|
+
if (frame !== void 0) return;
|
|
212
|
+
const callback = () => {
|
|
213
|
+
frame = void 0;
|
|
214
|
+
postHeight(height);
|
|
215
|
+
};
|
|
216
|
+
frame = typeof window.requestAnimationFrame === "function" ? window.requestAnimationFrame(callback) : window.setTimeout(callback, 0);
|
|
217
|
+
});
|
|
218
|
+
observers.add(observer);
|
|
219
|
+
observer.observe(element);
|
|
220
|
+
return () => {
|
|
221
|
+
disconnected = true;
|
|
222
|
+
if (frame !== void 0) {
|
|
223
|
+
window.cancelAnimationFrame?.(frame);
|
|
224
|
+
window.clearTimeout(frame);
|
|
225
|
+
}
|
|
226
|
+
observer.disconnect();
|
|
227
|
+
observers.delete(observer);
|
|
228
|
+
};
|
|
229
|
+
},
|
|
230
|
+
destroy() {
|
|
231
|
+
if (destroyed) return;
|
|
232
|
+
destroyed = true;
|
|
233
|
+
window.removeEventListener("message", onMessage);
|
|
234
|
+
for (const observer of observers) observer.disconnect();
|
|
235
|
+
observers.clear();
|
|
236
|
+
queuedRequests.clear();
|
|
237
|
+
const error = new BridgeDestroyedError();
|
|
238
|
+
pendingRequests.rejectAll(error);
|
|
239
|
+
if (!readyResolved) rejectReady(error);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
//#endregion
|
|
245
|
+
//#region src/bridge/iframe/useRemoteModuleBridge.ts
|
|
246
|
+
const rejectNotReady = async () => {
|
|
247
|
+
throw new BridgeDestroyedError("Bridge is not ready");
|
|
248
|
+
};
|
|
249
|
+
function useRemoteModuleBridge(options = {}) {
|
|
250
|
+
const optionsRef = useRef(options);
|
|
251
|
+
const [bridge, setBridge] = useState(null);
|
|
252
|
+
const [ready, setReady] = useState(false);
|
|
253
|
+
const [data, setData] = useState(null);
|
|
254
|
+
const [context, setContext] = useState(null);
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
let active = true;
|
|
257
|
+
const currentBridge = createModuleBridge(optionsRef.current);
|
|
258
|
+
setBridge(currentBridge);
|
|
259
|
+
setData(currentBridge.getData());
|
|
260
|
+
setContext(currentBridge.getContext());
|
|
261
|
+
const unsubscribe = currentBridge.subscribe(() => {
|
|
262
|
+
if (active) {
|
|
263
|
+
setData(currentBridge.getData());
|
|
264
|
+
setContext(currentBridge.getContext());
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
currentBridge.ready().then(({ data: initialData, context: initialContext }) => {
|
|
268
|
+
if (active) {
|
|
269
|
+
setData(initialData);
|
|
270
|
+
setContext(initialContext);
|
|
271
|
+
setReady(true);
|
|
272
|
+
}
|
|
273
|
+
}, () => {
|
|
274
|
+
if (active) setReady(false);
|
|
275
|
+
});
|
|
276
|
+
return () => {
|
|
277
|
+
active = false;
|
|
278
|
+
unsubscribe();
|
|
279
|
+
currentBridge.destroy();
|
|
280
|
+
setBridge(null);
|
|
281
|
+
setData(null);
|
|
282
|
+
setContext(null);
|
|
283
|
+
setReady(false);
|
|
284
|
+
};
|
|
285
|
+
}, []);
|
|
286
|
+
return {
|
|
287
|
+
bridge,
|
|
288
|
+
context,
|
|
289
|
+
data,
|
|
290
|
+
ready,
|
|
291
|
+
resolveDataPath: bridge?.resolveDataPath ?? rejectNotReady
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
//#endregion
|
|
296
|
+
//#region src/bridge/iframe/LayoutModuleBridge.tsx
|
|
297
|
+
let mountedBridgeCount = 0;
|
|
298
|
+
function BridgeErrorContent({ render }) {
|
|
299
|
+
return render();
|
|
300
|
+
}
|
|
301
|
+
var BridgeErrorBoundary = class extends Component {
|
|
302
|
+
state = { error: null };
|
|
303
|
+
static getDerivedStateFromError(error) {
|
|
304
|
+
return { error };
|
|
305
|
+
}
|
|
306
|
+
componentDidCatch(error, _errorInfo) {
|
|
307
|
+
this.props.bridge.reportRenderError(error.message);
|
|
308
|
+
}
|
|
309
|
+
render() {
|
|
310
|
+
return this.state.error ? this.props.fallback : /* @__PURE__ */ jsx(BridgeErrorContent, { render: this.props.render });
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
function LayoutModuleBridge({ allowedHostOrigins, resolveTimeoutMs, observeHeight = true, fallback = null, children }) {
|
|
314
|
+
const { bridge, context, data, ready, resolveDataPath } = useRemoteModuleBridge({
|
|
315
|
+
allowedHostOrigins,
|
|
316
|
+
resolveTimeoutMs
|
|
317
|
+
});
|
|
318
|
+
const rootRef = useRef(null);
|
|
319
|
+
useEffect(() => {
|
|
320
|
+
mountedBridgeCount += 1;
|
|
321
|
+
if (typeof process !== "undefined" && mountedBridgeCount > 1) console.warn("Multiple LayoutModuleBridge instances are mounted in the same document; this is unsupported.");
|
|
322
|
+
return () => {
|
|
323
|
+
mountedBridgeCount -= 1;
|
|
324
|
+
};
|
|
325
|
+
}, []);
|
|
326
|
+
useEffect(() => {
|
|
327
|
+
if (!ready || !observeHeight || !rootRef.current || !bridge) return;
|
|
328
|
+
return bridge.observeHeight(rootRef.current);
|
|
329
|
+
}, [
|
|
330
|
+
bridge,
|
|
331
|
+
observeHeight,
|
|
332
|
+
ready
|
|
333
|
+
]);
|
|
334
|
+
if (!ready || !bridge) return fallback;
|
|
335
|
+
return /* @__PURE__ */ jsx(BridgeErrorBoundary, {
|
|
336
|
+
bridge,
|
|
337
|
+
fallback,
|
|
338
|
+
render: () => /* @__PURE__ */ jsx("div", {
|
|
339
|
+
ref: rootRef,
|
|
340
|
+
children: children({
|
|
341
|
+
context,
|
|
342
|
+
data,
|
|
343
|
+
resolveDataPath
|
|
344
|
+
})
|
|
345
|
+
})
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
//#endregion
|
|
350
|
+
export { LayoutModuleBridge, personaDashboardHostOrigins };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { n as Json, t as RemoteModuleContext } from "./protocol-edoRdXpF.mjs";
|
|
2
|
+
import { CSSProperties, ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/bridge/host/buildModuleSrc.d.ts
|
|
5
|
+
type BuildModuleSrcOptions = {
|
|
6
|
+
accessToken?: string;
|
|
7
|
+
environmentId?: string;
|
|
8
|
+
locale?: string;
|
|
9
|
+
extraParams?: Record<string, string>;
|
|
10
|
+
};
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/bridge/host/createHostBridge.d.ts
|
|
13
|
+
type ResolveDataPath = (dataPath: string, variableMap?: Record<string, string | number>) => Promise<Json | undefined> | Json | undefined;
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/bridge/host/RemoteModuleFrame.d.ts
|
|
16
|
+
type RemoteModuleFrameProps = {
|
|
17
|
+
documentUrl: string;
|
|
18
|
+
moduleOrigin: string;
|
|
19
|
+
accessToken?: string;
|
|
20
|
+
environmentId?: string;
|
|
21
|
+
locale?: string;
|
|
22
|
+
extraParams?: BuildModuleSrcOptions['extraParams'];
|
|
23
|
+
title: string;
|
|
24
|
+
sandbox: string;
|
|
25
|
+
referrerPolicy?: ReferrerPolicy;
|
|
26
|
+
initRetryMs?: number;
|
|
27
|
+
initRetryTimeoutMs?: number;
|
|
28
|
+
dynamicHeight?: boolean;
|
|
29
|
+
heightOffset?: number;
|
|
30
|
+
minHeight?: number;
|
|
31
|
+
className?: string;
|
|
32
|
+
style?: CSSProperties;
|
|
33
|
+
data: Json;
|
|
34
|
+
context: RemoteModuleContext;
|
|
35
|
+
resolveDataPath: ResolveDataPath;
|
|
36
|
+
onRenderError?: (message: string) => void;
|
|
37
|
+
};
|
|
38
|
+
declare function RemoteModuleFrame({
|
|
39
|
+
documentUrl,
|
|
40
|
+
moduleOrigin,
|
|
41
|
+
accessToken,
|
|
42
|
+
environmentId,
|
|
43
|
+
locale,
|
|
44
|
+
extraParams,
|
|
45
|
+
title,
|
|
46
|
+
sandbox,
|
|
47
|
+
referrerPolicy,
|
|
48
|
+
initRetryMs,
|
|
49
|
+
initRetryTimeoutMs,
|
|
50
|
+
dynamicHeight,
|
|
51
|
+
heightOffset,
|
|
52
|
+
minHeight,
|
|
53
|
+
className,
|
|
54
|
+
style,
|
|
55
|
+
data,
|
|
56
|
+
context,
|
|
57
|
+
resolveDataPath,
|
|
58
|
+
onRenderError
|
|
59
|
+
}: RemoteModuleFrameProps): ReactNode;
|
|
60
|
+
//#endregion
|
|
61
|
+
export { type Json, type RemoteModuleContext, RemoteModuleFrame, type RemoteModuleFrameProps, type ResolveDataPath };
|