@persona/layout-editor-remote-module 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -0
- package/dist/bridge-frame.d.mts +4 -1
- package/dist/bridge-frame.mjs +58 -14
- package/dist/bridge-host.d.mts +6 -2
- package/dist/bridge-host.mjs +65 -10
- package/dist/config-form.d.mts +23 -0
- package/dist/config-form.mjs +79 -0
- package/dist/{protocol-D_pdVZjs.mjs → protocol-BfjTaW1S.mjs} +7 -1
- package/dist/{protocol-Dqnl8JTs.d.mts → protocol-ClWDYwW2.d.mts} +2 -9
- package/dist/types-CBSiNsDJ.d.mts +17 -0
- package/package.json +8 -1
- package/skills/getting-started/SKILL.md +19 -15
package/README.md
CHANGED
|
@@ -71,12 +71,24 @@ import { RemoteModuleFrame } from '@persona/layout-editor-remote-module/bridge-h
|
|
|
71
71
|
data={moduleData}
|
|
72
72
|
context={moduleContext}
|
|
73
73
|
dynamicHeight
|
|
74
|
+
loadingFallback={<div>Loading module…</div>}
|
|
74
75
|
resolveDataPath={resolveDataPath}
|
|
75
76
|
/>;
|
|
76
77
|
```
|
|
77
78
|
|
|
78
79
|
The required `sandbox` value is chosen by the host.
|
|
79
80
|
|
|
81
|
+
`loadingFallback` is drawn over the iframe until the module has rendered. `LayoutModuleBridge`
|
|
82
|
+
advertises the `loaded` message automatically after its first render and waits for the first height
|
|
83
|
+
measurement before sending it when height observation is enabled. A module that needs a loading
|
|
84
|
+
state should render it on its first render; `LayoutModuleBridge`'s `fallback` is hidden under the
|
|
85
|
+
host overlay until then and remains available for render errors.
|
|
86
|
+
|
|
87
|
+
- `createModuleBridge` and `useRemoteModuleBridge` keep legacy behavior by default; direct bridge
|
|
88
|
+
users opt in with `signalsLoaded: true` and call `bridge.signalLoaded()` after rendering.
|
|
89
|
+
- Frames without the `loaded` capability are treated as legacy: `ready`, another bridge message, or
|
|
90
|
+
a silent-frame timeout after iframe load dismisses the overlay.
|
|
91
|
+
|
|
80
92
|
By default, the bridge accepts host origins matching `*.withpersona.com`,
|
|
81
93
|
`*.withpersona-staging.com`, `localhost`, and `127.0.0.1`. Add development or preview origins with
|
|
82
94
|
`allowedHostOrigins`, optionally composing the exported `personaDashboardHostOrigins` helper:
|
|
@@ -168,3 +180,24 @@ const { modules } = parseRemoteModuleManifest(await response.json());
|
|
|
168
180
|
It throws `RemoteModuleManifestParseError` (with `.issues` and a prettified `.message`) on a
|
|
169
181
|
malformed document; `safeParseRemoteModuleManifest` returns `{ success, data | error }` instead of
|
|
170
182
|
throwing.
|
|
183
|
+
|
|
184
|
+
### Rendering a config panel
|
|
185
|
+
|
|
186
|
+
A host stores a placed module's config under a `config` property, so its RJSF form edits
|
|
187
|
+
`{ config }`. The `./config-form` entry adapts a manifest to that shape, with no React or RJSF
|
|
188
|
+
dependency:
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
import {
|
|
192
|
+
nestRemoteConfigSchema,
|
|
193
|
+
nestRemoteUiConfigSchema,
|
|
194
|
+
remoteConfigDefaults,
|
|
195
|
+
} from '@persona/layout-editor-remote-module/config-form';
|
|
196
|
+
|
|
197
|
+
const schema = nestRemoteConfigSchema(module.configSchema); // `$ref`s rewritten to match
|
|
198
|
+
const uiSchema = nestRemoteUiConfigSchema(module.uiConfigSchema);
|
|
199
|
+
const initialConfig = remoteConfigDefaults(module.configSchema); // top-level `default`s
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Render the form with `schema`, `uiSchema` and `formData: { config }`, and read the edited config
|
|
203
|
+
back from `formData.config`.
|
package/dist/bridge-frame.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { t as Json } from "./types-CBSiNsDJ.mjs";
|
|
2
|
+
import { t as RemoteModuleContext } from "./protocol-ClWDYwW2.mjs";
|
|
2
3
|
import { ReactNode } from "react";
|
|
3
4
|
|
|
4
5
|
//#region src/bridge/origin.d.ts
|
|
@@ -12,6 +13,7 @@ type ModuleBridgeInitPayload = {
|
|
|
12
13
|
type CreateModuleBridgeOptions = {
|
|
13
14
|
allowedHostOrigins?: (string | RegExp)[] | ((origin: string) => boolean);
|
|
14
15
|
resolveTimeoutMs?: number;
|
|
16
|
+
signalsLoaded?: boolean;
|
|
15
17
|
};
|
|
16
18
|
type ModuleBridge = {
|
|
17
19
|
ready: () => Promise<ModuleBridgeInitPayload>;
|
|
@@ -21,6 +23,7 @@ type ModuleBridge = {
|
|
|
21
23
|
reportRenderError: (message: string) => void;
|
|
22
24
|
resolveDataPath: (dataPath: string, variableMap?: Record<string, string | number>) => Promise<Json>;
|
|
23
25
|
observeHeight: (element: Element) => () => void;
|
|
26
|
+
signalLoaded: () => void;
|
|
24
27
|
destroy: () => void;
|
|
25
28
|
};
|
|
26
29
|
//#endregion
|
package/dist/bridge-frame.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as resolveAllowedHostOrigins, n as SOURCE, o as candidateParentOrigin, r as parseHostToModuleMessage, s as personaDashboardHostOrigins, t as PROTOCOL_VERSION } from "./protocol-
|
|
1
|
+
import { c as resolveAllowedHostOrigins, n as SOURCE, o as candidateParentOrigin, r as parseHostToModuleMessage, s as personaDashboardHostOrigins, t as PROTOCOL_VERSION } from "./protocol-BfjTaW1S.mjs";
|
|
2
2
|
import { Component, useEffect, useRef, useState } from "react";
|
|
3
3
|
import { jsx } from "react/jsx-runtime";
|
|
4
4
|
|
|
@@ -70,7 +70,7 @@ const measureDocumentHeight = (element) => {
|
|
|
70
70
|
}
|
|
71
71
|
return Math.ceil(rect.top + window.scrollY + rect.height + trailing);
|
|
72
72
|
};
|
|
73
|
-
function createModuleBridge({ allowedHostOrigins = personaDashboardHostOrigins, resolveTimeoutMs = 3e4 } = {}) {
|
|
73
|
+
function createModuleBridge({ allowedHostOrigins = personaDashboardHostOrigins, resolveTimeoutMs = 3e4, signalsLoaded = false } = {}) {
|
|
74
74
|
const isEmbedded = typeof window !== "undefined" && window.parent !== window;
|
|
75
75
|
const allowedOrigin = resolveAllowedHostOrigins(allowedHostOrigins);
|
|
76
76
|
if (!isEmbedded) {
|
|
@@ -88,6 +88,7 @@ function createModuleBridge({ allowedHostOrigins = personaDashboardHostOrigins,
|
|
|
88
88
|
throw notEmbeddedError;
|
|
89
89
|
},
|
|
90
90
|
observeHeight: () => () => {},
|
|
91
|
+
signalLoaded: () => {},
|
|
91
92
|
destroy: () => {}
|
|
92
93
|
};
|
|
93
94
|
}
|
|
@@ -96,8 +97,12 @@ function createModuleBridge({ allowedHostOrigins = personaDashboardHostOrigins,
|
|
|
96
97
|
let data = null;
|
|
97
98
|
let context = null;
|
|
98
99
|
let readyResolved = false;
|
|
100
|
+
let initialReadyPosted = false;
|
|
101
|
+
let loadedRequested = false;
|
|
102
|
+
let loadedPosted = false;
|
|
99
103
|
const listeners = /* @__PURE__ */ new Set();
|
|
100
104
|
const observers = /* @__PURE__ */ new Set();
|
|
105
|
+
const pendingHeightObservers = /* @__PURE__ */ new Set();
|
|
101
106
|
const pendingRequests = createPendingRequests();
|
|
102
107
|
const queuedRequests = /* @__PURE__ */ new Map();
|
|
103
108
|
let resolveReady;
|
|
@@ -107,7 +112,18 @@ function createModuleBridge({ allowedHostOrigins = personaDashboardHostOrigins,
|
|
|
107
112
|
rejectReady = reject;
|
|
108
113
|
});
|
|
109
114
|
const post = (message) => {
|
|
110
|
-
if (
|
|
115
|
+
if (!destroyed && hostOrigin) {
|
|
116
|
+
window.parent.postMessage(message, hostOrigin);
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
120
|
+
};
|
|
121
|
+
const flushLoaded = () => {
|
|
122
|
+
if (!destroyed && loadedRequested && !loadedPosted && hostOrigin && pendingHeightObservers.size === 0) loadedPosted = post({
|
|
123
|
+
source: SOURCE,
|
|
124
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
125
|
+
type: "loaded"
|
|
126
|
+
});
|
|
111
127
|
};
|
|
112
128
|
const flushQueuedRequests = () => {
|
|
113
129
|
if (!hostOrigin) return;
|
|
@@ -127,7 +143,6 @@ function createModuleBridge({ allowedHostOrigins = personaDashboardHostOrigins,
|
|
|
127
143
|
if (!allowedOrigin(origin)) return false;
|
|
128
144
|
if (hostOrigin !== void 0 && hostOrigin !== origin) return false;
|
|
129
145
|
hostOrigin = origin;
|
|
130
|
-
flushQueuedRequests();
|
|
131
146
|
return true;
|
|
132
147
|
};
|
|
133
148
|
const notify = () => {
|
|
@@ -137,6 +152,17 @@ function createModuleBridge({ allowedHostOrigins = personaDashboardHostOrigins,
|
|
|
137
152
|
if (destroyed || event.source !== window.parent) return;
|
|
138
153
|
const message = parseHostToModuleMessage(event.data);
|
|
139
154
|
if (!message || !pinHostOrigin(event.origin)) return;
|
|
155
|
+
if (message.type === "init" && signalsLoaded && !initialReadyPosted) {
|
|
156
|
+
initialReadyPosted = true;
|
|
157
|
+
post({
|
|
158
|
+
source: SOURCE,
|
|
159
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
160
|
+
type: "ready",
|
|
161
|
+
capabilities: { loaded: true }
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
flushQueuedRequests();
|
|
165
|
+
flushLoaded();
|
|
140
166
|
switch (message.type) {
|
|
141
167
|
case "init":
|
|
142
168
|
data = message.data;
|
|
@@ -166,11 +192,15 @@ function createModuleBridge({ allowedHostOrigins = personaDashboardHostOrigins,
|
|
|
166
192
|
};
|
|
167
193
|
window.addEventListener("message", onMessage);
|
|
168
194
|
const parentOrigin = candidateParentOrigin();
|
|
169
|
-
if (parentOrigin && allowedOrigin(parentOrigin))
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
195
|
+
if (parentOrigin && allowedOrigin(parentOrigin)) {
|
|
196
|
+
window.parent.postMessage({
|
|
197
|
+
source: SOURCE,
|
|
198
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
199
|
+
type: "ready",
|
|
200
|
+
...signalsLoaded ? { capabilities: { loaded: true } } : {}
|
|
201
|
+
}, parentOrigin);
|
|
202
|
+
initialReadyPosted = true;
|
|
203
|
+
}
|
|
174
204
|
return {
|
|
175
205
|
ready: () => readyPromise,
|
|
176
206
|
getData: () => data,
|
|
@@ -202,17 +232,19 @@ function createModuleBridge({ allowedHostOrigins = personaDashboardHostOrigins,
|
|
|
202
232
|
let lastHeight;
|
|
203
233
|
let frame;
|
|
204
234
|
let disconnected = false;
|
|
235
|
+
let observer;
|
|
205
236
|
const postHeight = (height) => {
|
|
206
237
|
if (disconnected || height === lastHeight) return;
|
|
207
|
-
|
|
208
|
-
post({
|
|
238
|
+
if (!post({
|
|
209
239
|
source: SOURCE,
|
|
210
240
|
protocolVersion: PROTOCOL_VERSION,
|
|
211
241
|
type: "set-height",
|
|
212
242
|
height
|
|
213
|
-
});
|
|
243
|
+
})) return;
|
|
244
|
+
lastHeight = height;
|
|
245
|
+
if (pendingHeightObservers.delete(observer)) flushLoaded();
|
|
214
246
|
};
|
|
215
|
-
|
|
247
|
+
observer = new ResizeObserver(() => {
|
|
216
248
|
if (frame !== void 0) return;
|
|
217
249
|
const callback = () => {
|
|
218
250
|
frame = void 0;
|
|
@@ -221,6 +253,7 @@ function createModuleBridge({ allowedHostOrigins = personaDashboardHostOrigins,
|
|
|
221
253
|
frame = typeof window.requestAnimationFrame === "function" ? window.requestAnimationFrame(callback) : window.setTimeout(callback, 0);
|
|
222
254
|
});
|
|
223
255
|
observers.add(observer);
|
|
256
|
+
pendingHeightObservers.add(observer);
|
|
224
257
|
observer.observe(element);
|
|
225
258
|
const body = element.ownerDocument?.body;
|
|
226
259
|
if (body && body !== element) observer.observe(body);
|
|
@@ -232,14 +265,21 @@ function createModuleBridge({ allowedHostOrigins = personaDashboardHostOrigins,
|
|
|
232
265
|
}
|
|
233
266
|
observer.disconnect();
|
|
234
267
|
observers.delete(observer);
|
|
268
|
+
if (pendingHeightObservers.delete(observer)) flushLoaded();
|
|
235
269
|
};
|
|
236
270
|
},
|
|
271
|
+
signalLoaded() {
|
|
272
|
+
if (destroyed || loadedRequested) return;
|
|
273
|
+
loadedRequested = true;
|
|
274
|
+
flushLoaded();
|
|
275
|
+
},
|
|
237
276
|
destroy() {
|
|
238
277
|
if (destroyed) return;
|
|
239
278
|
destroyed = true;
|
|
240
279
|
window.removeEventListener("message", onMessage);
|
|
241
280
|
for (const observer of observers) observer.disconnect();
|
|
242
281
|
observers.clear();
|
|
282
|
+
pendingHeightObservers.clear();
|
|
243
283
|
queuedRequests.clear();
|
|
244
284
|
const error = new BridgeDestroyedError();
|
|
245
285
|
pendingRequests.rejectAll(error);
|
|
@@ -320,7 +360,8 @@ var BridgeErrorBoundary = class extends Component {
|
|
|
320
360
|
function LayoutModuleBridge({ allowedHostOrigins, resolveTimeoutMs, observeHeight = true, fallback = null, children }) {
|
|
321
361
|
const { bridge, context, data, ready, resolveDataPath } = useRemoteModuleBridge({
|
|
322
362
|
allowedHostOrigins,
|
|
323
|
-
resolveTimeoutMs
|
|
363
|
+
resolveTimeoutMs,
|
|
364
|
+
signalsLoaded: true
|
|
324
365
|
});
|
|
325
366
|
const rootRef = useRef(null);
|
|
326
367
|
useEffect(() => {
|
|
@@ -338,6 +379,9 @@ function LayoutModuleBridge({ allowedHostOrigins, resolveTimeoutMs, observeHeigh
|
|
|
338
379
|
observeHeight,
|
|
339
380
|
ready
|
|
340
381
|
]);
|
|
382
|
+
useEffect(() => {
|
|
383
|
+
if (ready && bridge) bridge.signalLoaded();
|
|
384
|
+
}, [bridge, ready]);
|
|
341
385
|
if (!ready || !bridge) return fallback;
|
|
342
386
|
return /* @__PURE__ */ jsx(BridgeErrorBoundary, {
|
|
343
387
|
bridge,
|
package/dist/bridge-host.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { t as Json } from "./types-CBSiNsDJ.mjs";
|
|
2
|
+
import { t as RemoteModuleContext } from "./protocol-ClWDYwW2.mjs";
|
|
2
3
|
import { CSSProperties, ReactNode, Ref } from "react";
|
|
3
4
|
|
|
4
5
|
//#region src/bridge/host/buildModuleSrc.d.ts
|
|
@@ -36,6 +37,8 @@ type RemoteModuleFrameProps = {
|
|
|
36
37
|
context: RemoteModuleContext;
|
|
37
38
|
resolveDataPath: ResolveDataPath;
|
|
38
39
|
onRenderError?: (message: string) => void;
|
|
40
|
+
/** Rendered over the iframe until the module reports that it has loaded. */
|
|
41
|
+
loadingFallback?: ReactNode;
|
|
39
42
|
};
|
|
40
43
|
declare function RemoteModuleFrame({
|
|
41
44
|
documentUrl,
|
|
@@ -58,7 +61,8 @@ declare function RemoteModuleFrame({
|
|
|
58
61
|
data,
|
|
59
62
|
context,
|
|
60
63
|
resolveDataPath,
|
|
61
|
-
onRenderError
|
|
64
|
+
onRenderError,
|
|
65
|
+
loadingFallback
|
|
62
66
|
}: RemoteModuleFrameProps): ReactNode;
|
|
63
67
|
//#endregion
|
|
64
68
|
export { type Json, type RemoteModuleContext, RemoteModuleFrame, type RemoteModuleFrameProps, type ResolveDataPath };
|
package/dist/bridge-host.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as assertNotSelfOrigin, i as parseModuleToHostMessage, n as SOURCE, t as PROTOCOL_VERSION } from "./protocol-
|
|
1
|
+
import { a as assertNotSelfOrigin, i as parseModuleToHostMessage, n as SOURCE, t as PROTOCOL_VERSION } from "./protocol-BfjTaW1S.mjs";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
-
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
4
|
|
|
5
5
|
//#region src/bridge/host/buildModuleSrc.ts
|
|
6
6
|
const reservedParams = new Set([
|
|
@@ -24,11 +24,15 @@ function buildModuleSrc(documentUrl, options = {}) {
|
|
|
24
24
|
|
|
25
25
|
//#endregion
|
|
26
26
|
//#region src/bridge/host/createHostBridge.ts
|
|
27
|
-
function createHostBridge({ iframe, moduleOrigin, getData, getContext, resolveDataPath, onHeight, onRenderError, initRetryMs = 250, initRetryTimeoutMs = 1e4 }) {
|
|
27
|
+
function createHostBridge({ iframe, moduleOrigin, getData, getContext, resolveDataPath, onHeight, onRenderError, onLoaded, initRetryMs = 250, initRetryTimeoutMs = 1e4 }) {
|
|
28
28
|
assertNotSelfOrigin(moduleOrigin);
|
|
29
29
|
if (moduleOrigin === "*") throw new Error("Remote module origin must be explicit");
|
|
30
30
|
let destroyed = false;
|
|
31
31
|
let initialized = false;
|
|
32
|
+
let capable;
|
|
33
|
+
let loaded = false;
|
|
34
|
+
let fallbackTimer;
|
|
35
|
+
let fallbackTimerStarted = false;
|
|
32
36
|
let initRetryTimer;
|
|
33
37
|
const clearInitRetry = () => {
|
|
34
38
|
if (initRetryTimer !== void 0) {
|
|
@@ -36,6 +40,18 @@ function createHostBridge({ iframe, moduleOrigin, getData, getContext, resolveDa
|
|
|
36
40
|
initRetryTimer = void 0;
|
|
37
41
|
}
|
|
38
42
|
};
|
|
43
|
+
const clearFallbackTimer = () => {
|
|
44
|
+
if (fallbackTimer !== void 0) {
|
|
45
|
+
clearTimeout(fallbackTimer);
|
|
46
|
+
fallbackTimer = void 0;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const markLoaded = () => {
|
|
50
|
+
if (loaded) return;
|
|
51
|
+
loaded = true;
|
|
52
|
+
clearFallbackTimer();
|
|
53
|
+
onLoaded?.();
|
|
54
|
+
};
|
|
39
55
|
const post = (message) => {
|
|
40
56
|
if (!destroyed) iframe.contentWindow?.postMessage(message, moduleOrigin);
|
|
41
57
|
};
|
|
@@ -72,17 +88,38 @@ function createHostBridge({ iframe, moduleOrigin, getData, getContext, resolveDa
|
|
|
72
88
|
});
|
|
73
89
|
};
|
|
74
90
|
const onLoad = () => {
|
|
75
|
-
if (!destroyed)
|
|
91
|
+
if (!destroyed) {
|
|
92
|
+
if (!fallbackTimerStarted) {
|
|
93
|
+
fallbackTimerStarted = true;
|
|
94
|
+
fallbackTimer = setTimeout(() => {
|
|
95
|
+
fallbackTimer = void 0;
|
|
96
|
+
if (capable === void 0) markLoaded();
|
|
97
|
+
}, Math.max(initRetryTimeoutMs, 0) + 500);
|
|
98
|
+
}
|
|
99
|
+
startInitRetry();
|
|
100
|
+
}
|
|
76
101
|
};
|
|
77
102
|
const onMessage = (event) => {
|
|
78
103
|
if (destroyed || event.origin !== moduleOrigin || event.source !== iframe.contentWindow) return;
|
|
79
104
|
const message = parseModuleToHostMessage(event.data);
|
|
80
105
|
if (!message) return;
|
|
81
106
|
clearInitRetry();
|
|
107
|
+
if (message.type === "ready") {
|
|
108
|
+
if (capable === void 0) {
|
|
109
|
+
capable = message.capabilities?.loaded === true;
|
|
110
|
+
if (!capable) markLoaded();
|
|
111
|
+
}
|
|
112
|
+
} else if (message.type !== "loaded" && message.type !== "render-error" && capable === void 0) {
|
|
113
|
+
capable = false;
|
|
114
|
+
markLoaded();
|
|
115
|
+
}
|
|
82
116
|
switch (message.type) {
|
|
83
117
|
case "ready":
|
|
84
118
|
sendInit();
|
|
85
119
|
break;
|
|
120
|
+
case "loaded":
|
|
121
|
+
markLoaded();
|
|
122
|
+
break;
|
|
86
123
|
case "request-data":
|
|
87
124
|
sendData(getData(), message.requestId);
|
|
88
125
|
break;
|
|
@@ -90,6 +127,7 @@ function createHostBridge({ iframe, moduleOrigin, getData, getContext, resolveDa
|
|
|
90
127
|
onHeight?.(message.height);
|
|
91
128
|
break;
|
|
92
129
|
case "render-error":
|
|
130
|
+
markLoaded();
|
|
93
131
|
onRenderError?.(message.message);
|
|
94
132
|
break;
|
|
95
133
|
case "resolve-data-path":
|
|
@@ -131,6 +169,7 @@ function createHostBridge({ iframe, moduleOrigin, getData, getContext, resolveDa
|
|
|
131
169
|
if (destroyed) return;
|
|
132
170
|
destroyed = true;
|
|
133
171
|
clearInitRetry();
|
|
172
|
+
clearFallbackTimer();
|
|
134
173
|
window.removeEventListener("message", onMessage);
|
|
135
174
|
iframe.removeEventListener("load", onLoad);
|
|
136
175
|
}
|
|
@@ -145,7 +184,7 @@ function withDynamicHeight(context, dynamicHeight) {
|
|
|
145
184
|
dynamicHeight
|
|
146
185
|
};
|
|
147
186
|
}
|
|
148
|
-
function RemoteModuleFrame({ documentUrl, moduleOrigin, accessToken, environmentId, locale, extraParams, title, sandbox, referrerPolicy = "no-referrer", initRetryMs, initRetryTimeoutMs, dynamicHeight = false, heightOffset = 0, minHeight, className, style, iframeRef: externalIframeRef, data, context, resolveDataPath, onRenderError }) {
|
|
187
|
+
function RemoteModuleFrame({ documentUrl, moduleOrigin, accessToken, environmentId, locale, extraParams, title, sandbox, referrerPolicy = "no-referrer", initRetryMs, initRetryTimeoutMs, dynamicHeight = false, heightOffset = 0, minHeight, className, style, iframeRef: externalIframeRef, data, context, resolveDataPath, onRenderError, loadingFallback }) {
|
|
149
188
|
const iframeRef = useRef(null);
|
|
150
189
|
const dataRef = useRef(data);
|
|
151
190
|
const contextRef = useRef(withDynamicHeight(context, dynamicHeight));
|
|
@@ -154,6 +193,7 @@ function RemoteModuleFrame({ documentUrl, moduleOrigin, accessToken, environment
|
|
|
154
193
|
const bridgeRef = useRef(void 0);
|
|
155
194
|
const externalCleanupRef = useRef(void 0);
|
|
156
195
|
const [height, setHeight] = useState();
|
|
196
|
+
const [loadedSrc, setLoadedSrc] = useState();
|
|
157
197
|
const setRefs = useCallback((node) => {
|
|
158
198
|
iframeRef.current = node;
|
|
159
199
|
if (typeof externalIframeRef === "function") if (node) {
|
|
@@ -184,10 +224,10 @@ function RemoteModuleFrame({ documentUrl, moduleOrigin, accessToken, environment
|
|
|
184
224
|
locale
|
|
185
225
|
]);
|
|
186
226
|
useEffect(() => {
|
|
187
|
-
const iframe = iframeRef.current;
|
|
188
|
-
if (!iframe) return;
|
|
227
|
+
const iframe$1 = iframeRef.current;
|
|
228
|
+
if (!iframe$1) return;
|
|
189
229
|
const bridge = createHostBridge({
|
|
190
|
-
iframe,
|
|
230
|
+
iframe: iframe$1,
|
|
191
231
|
moduleOrigin,
|
|
192
232
|
getData: () => dataRef.current,
|
|
193
233
|
getContext: () => contextRef.current,
|
|
@@ -197,7 +237,8 @@ function RemoteModuleFrame({ documentUrl, moduleOrigin, accessToken, environment
|
|
|
197
237
|
onHeight: (nextHeight) => {
|
|
198
238
|
setHeight(nextHeight);
|
|
199
239
|
},
|
|
200
|
-
onRenderError: (message) => onRenderErrorRef.current?.(message)
|
|
240
|
+
onRenderError: (message) => onRenderErrorRef.current?.(message),
|
|
241
|
+
onLoaded: () => setLoadedSrc(src)
|
|
201
242
|
});
|
|
202
243
|
bridgeRef.current = bridge;
|
|
203
244
|
return () => {
|
|
@@ -217,7 +258,7 @@ function RemoteModuleFrame({ documentUrl, moduleOrigin, accessToken, environment
|
|
|
217
258
|
bridgeRef.current?.pushContext(hostContext);
|
|
218
259
|
}, [hostContext]);
|
|
219
260
|
const computedHeight = dynamicHeight && height !== void 0 ? `${Math.max(height + heightOffset, minHeight ?? 0)}px` : minHeight === void 0 ? "100%" : `${minHeight}px`;
|
|
220
|
-
|
|
261
|
+
const iframe = /* @__PURE__ */ jsx("iframe", {
|
|
221
262
|
ref: setRefs,
|
|
222
263
|
src,
|
|
223
264
|
title,
|
|
@@ -232,6 +273,20 @@ function RemoteModuleFrame({ documentUrl, moduleOrigin, accessToken, environment
|
|
|
232
273
|
border: "none"
|
|
233
274
|
}
|
|
234
275
|
}, src);
|
|
276
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
277
|
+
style: {
|
|
278
|
+
position: "relative",
|
|
279
|
+
width: "100%",
|
|
280
|
+
height: computedHeight === "100%" ? "100%" : void 0
|
|
281
|
+
},
|
|
282
|
+
children: [iframe, loadingFallback !== void 0 && !(loadedSrc === src) && /* @__PURE__ */ jsx("div", {
|
|
283
|
+
style: {
|
|
284
|
+
position: "absolute",
|
|
285
|
+
inset: 0
|
|
286
|
+
},
|
|
287
|
+
children: loadingFallback
|
|
288
|
+
})]
|
|
289
|
+
});
|
|
235
290
|
}
|
|
236
291
|
|
|
237
292
|
//#endregion
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { n as JsonSchemaObject, t as Json } from "./types-CBSiNsDJ.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/config-form.d.ts
|
|
4
|
+
|
|
5
|
+
/** JSON pointer, in the nested schema, of the module's own config schema. */
|
|
6
|
+
declare const REMOTE_CONFIG_POINTER_PREFIX = "#/properties/config";
|
|
7
|
+
/**
|
|
8
|
+
* Nest a manifest `configSchema` under a top-level `config` property, rewriting `$ref`s so they
|
|
9
|
+
* still resolve. Drops `$schema` and `$id`; keeps `$defs` / `definitions` at the root.
|
|
10
|
+
*/
|
|
11
|
+
declare function nestRemoteConfigSchema(configSchema: JsonSchemaObject): JsonSchemaObject;
|
|
12
|
+
/**
|
|
13
|
+
* Nest a manifest `uiConfigSchema` to match {@link nestRemoteConfigSchema}, hiding the wrapper
|
|
14
|
+
* object's title so the module's fields render as if top level.
|
|
15
|
+
*/
|
|
16
|
+
declare function nestRemoteUiConfigSchema(uiConfigSchema: JsonSchemaObject | undefined): JsonSchemaObject;
|
|
17
|
+
/**
|
|
18
|
+
* A config holding each top-level `configSchema` property's `default`. Properties without one are
|
|
19
|
+
* left unset; defaults are cloned so the result can be mutated freely.
|
|
20
|
+
*/
|
|
21
|
+
declare function remoteConfigDefaults(configSchema: JsonSchemaObject | undefined): Record<string, Json>;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { REMOTE_CONFIG_POINTER_PREFIX, nestRemoteConfigSchema, nestRemoteUiConfigSchema, remoteConfigDefaults };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
//#region src/config-form.ts
|
|
2
|
+
/** JSON pointer, in the nested schema, of the module's own config schema. */
|
|
3
|
+
const REMOTE_CONFIG_POINTER_PREFIX = "#/properties/config";
|
|
4
|
+
/** Keywords whose values are instance data, never schemas, so a `$ref` key inside them is data. */
|
|
5
|
+
const LITERAL_KEYWORDS = new Set([
|
|
6
|
+
"const",
|
|
7
|
+
"default",
|
|
8
|
+
"enum",
|
|
9
|
+
"examples"
|
|
10
|
+
]);
|
|
11
|
+
/** Keywords whose values map arbitrary names to schemas, so a name like `default` is a schema. */
|
|
12
|
+
const SCHEMA_MAP_KEYWORDS = new Set([
|
|
13
|
+
"$defs",
|
|
14
|
+
"definitions",
|
|
15
|
+
"dependentSchemas",
|
|
16
|
+
"patternProperties",
|
|
17
|
+
"properties"
|
|
18
|
+
]);
|
|
19
|
+
function rewriteSchemaMap(value, schemaId) {
|
|
20
|
+
if (value == null || typeof value !== "object" || Array.isArray(value)) return value;
|
|
21
|
+
return Object.fromEntries(Object.entries(value).map(([name, schema]) => [name, rewriteReferences(schema, schemaId)]));
|
|
22
|
+
}
|
|
23
|
+
function rewriteReferences(value, schemaId) {
|
|
24
|
+
if (Array.isArray(value)) return value.map((item) => rewriteReferences(item, schemaId));
|
|
25
|
+
if (value == null || typeof value !== "object") return value;
|
|
26
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => {
|
|
27
|
+
if (key === "$ref" && typeof child === "string") return [key, rewriteReference(child, schemaId)];
|
|
28
|
+
if (LITERAL_KEYWORDS.has(key)) return [key, child];
|
|
29
|
+
if (SCHEMA_MAP_KEYWORDS.has(key)) return [key, rewriteSchemaMap(child, schemaId)];
|
|
30
|
+
return [key, rewriteReferences(child, schemaId)];
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
function rewriteReference(reference, schemaId) {
|
|
34
|
+
if (schemaId != null) {
|
|
35
|
+
if (reference === schemaId) return REMOTE_CONFIG_POINTER_PREFIX;
|
|
36
|
+
if (reference.startsWith(`${schemaId}#`)) return rewriteReference(reference.slice(schemaId.length));
|
|
37
|
+
}
|
|
38
|
+
if (reference === "#") return REMOTE_CONFIG_POINTER_PREFIX;
|
|
39
|
+
if (reference.startsWith("#/$defs/") || reference.startsWith("#/definitions/")) return reference;
|
|
40
|
+
if (reference.startsWith("#/")) return `${REMOTE_CONFIG_POINTER_PREFIX}${reference.slice(1)}`;
|
|
41
|
+
return reference;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Nest a manifest `configSchema` under a top-level `config` property, rewriting `$ref`s so they
|
|
45
|
+
* still resolve. Drops `$schema` and `$id`; keeps `$defs` / `definitions` at the root.
|
|
46
|
+
*/
|
|
47
|
+
function nestRemoteConfigSchema(configSchema) {
|
|
48
|
+
const { $defs, definitions, $id, ...rest } = configSchema;
|
|
49
|
+
const body = Object.fromEntries(Object.entries(rest).filter(([key]) => key !== "$schema"));
|
|
50
|
+
const schemaId = typeof $id === "string" ? $id : void 0;
|
|
51
|
+
return {
|
|
52
|
+
type: "object",
|
|
53
|
+
properties: { config: rewriteReferences(body, schemaId) },
|
|
54
|
+
...$defs != null && { $defs: rewriteSchemaMap($defs, schemaId) },
|
|
55
|
+
...definitions != null && { definitions: rewriteSchemaMap(definitions, schemaId) }
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Nest a manifest `uiConfigSchema` to match {@link nestRemoteConfigSchema}, hiding the wrapper
|
|
60
|
+
* object's title so the module's fields render as if top level.
|
|
61
|
+
*/
|
|
62
|
+
function nestRemoteUiConfigSchema(uiConfigSchema) {
|
|
63
|
+
return { config: {
|
|
64
|
+
...uiConfigSchema,
|
|
65
|
+
"ui:title": ""
|
|
66
|
+
} };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* A config holding each top-level `configSchema` property's `default`. Properties without one are
|
|
70
|
+
* left unset; defaults are cloned so the result can be mutated freely.
|
|
71
|
+
*/
|
|
72
|
+
function remoteConfigDefaults(configSchema) {
|
|
73
|
+
const properties = configSchema?.properties;
|
|
74
|
+
if (properties == null || typeof properties !== "object" || Array.isArray(properties)) return {};
|
|
75
|
+
return Object.fromEntries(Object.entries(properties).filter(([, propertySchema]) => typeof propertySchema === "object" && propertySchema != null && !Array.isArray(propertySchema) && "default" in propertySchema).map(([key, propertySchema]) => [key, structuredClone(propertySchema.default)]));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
//#endregion
|
|
79
|
+
export { REMOTE_CONFIG_POINTER_PREFIX, nestRemoteConfigSchema, nestRemoteUiConfigSchema, remoteConfigDefaults };
|
|
@@ -117,7 +117,12 @@ const requestIdSchema = z.string().min(1);
|
|
|
117
117
|
const variableMapSchema = z.record(z.string(), z.union([z.string(), z.number()]));
|
|
118
118
|
const readySchema = z.object({
|
|
119
119
|
...envelopeSchema,
|
|
120
|
-
type: z.literal("ready")
|
|
120
|
+
type: z.literal("ready"),
|
|
121
|
+
capabilities: z.object({ loaded: z.boolean().optional() }).optional()
|
|
122
|
+
});
|
|
123
|
+
const loadedSchema = z.object({
|
|
124
|
+
...envelopeSchema,
|
|
125
|
+
type: z.literal("loaded")
|
|
121
126
|
});
|
|
122
127
|
const requestDataSchema = z.object({
|
|
123
128
|
...envelopeSchema,
|
|
@@ -161,6 +166,7 @@ const remoteModuleContextSchema = z.object({
|
|
|
161
166
|
});
|
|
162
167
|
const moduleToHostMessageSchema = z.discriminatedUnion("type", [
|
|
163
168
|
readySchema,
|
|
169
|
+
loadedSchema,
|
|
164
170
|
requestDataSchema,
|
|
165
171
|
setHeightSchema,
|
|
166
172
|
renderErrorSchema,
|
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
-
//#region src/types.d.ts
|
|
4
|
-
|
|
5
|
-
type Json = boolean | JsonObject | JsonValue[] | null | number | string;
|
|
6
|
-
type JsonObject = {
|
|
7
|
-
[key: string]: Json;
|
|
8
|
-
};
|
|
9
|
-
type JsonValue = Json;
|
|
10
|
-
//#endregion
|
|
11
3
|
//#region src/bridge/protocol.d.ts
|
|
4
|
+
|
|
12
5
|
/**
|
|
13
6
|
* Runtime facts about where the module is rendered, owned by the host and not
|
|
14
7
|
* derivable by the module itself. Distinct from `data`, which is the module's
|
|
@@ -24,4 +17,4 @@ declare const remoteModuleContextSchema: z.ZodObject<{
|
|
|
24
17
|
}, z.core.$strip>;
|
|
25
18
|
type RemoteModuleContext = z.infer<typeof remoteModuleContextSchema>;
|
|
26
19
|
//#endregion
|
|
27
|
-
export {
|
|
20
|
+
export { RemoteModuleContext as t };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
|
|
5
|
+
type Json = boolean | JsonObject | JsonValue[] | null | number | string;
|
|
6
|
+
/**
|
|
7
|
+
* Loose JSON Schema object. Deliberately not a full JSON Schema type system —
|
|
8
|
+
* this package only needs to carry what `z.toJSONSchema()` emits for consumers
|
|
9
|
+
* to validate or render.
|
|
10
|
+
*/
|
|
11
|
+
type JsonSchemaObject = Record<string, unknown>;
|
|
12
|
+
type JsonObject = {
|
|
13
|
+
[key: string]: Json;
|
|
14
|
+
};
|
|
15
|
+
type JsonValue = Json;
|
|
16
|
+
//#endregion
|
|
17
|
+
export { JsonSchemaObject as n, Json as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@persona/layout-editor-remote-module",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Shared manifest format, Vite plugin, and host/iframe bridge for remotely-defined dashboard layout modules in Persona dashboard apps.",
|
|
5
5
|
"license": "SEE LICENSE",
|
|
6
6
|
"type": "module",
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
],
|
|
16
16
|
"bridge-frame": [
|
|
17
17
|
"dist/bridge-frame.d.mts"
|
|
18
|
+
],
|
|
19
|
+
"config-form": [
|
|
20
|
+
"dist/config-form.d.mts"
|
|
18
21
|
]
|
|
19
22
|
}
|
|
20
23
|
},
|
|
@@ -111,6 +114,10 @@
|
|
|
111
114
|
"types": "./dist/bridge-host.d.mts",
|
|
112
115
|
"import": "./dist/bridge-host.mjs"
|
|
113
116
|
},
|
|
117
|
+
"./config-form": {
|
|
118
|
+
"types": "./dist/config-form.d.mts",
|
|
119
|
+
"import": "./dist/config-form.mjs"
|
|
120
|
+
},
|
|
114
121
|
"./bridge-frame": {
|
|
115
122
|
"types": "./dist/bridge-frame.d.mts",
|
|
116
123
|
"import": "./dist/bridge-frame.mjs"
|
|
@@ -6,11 +6,11 @@ description: >
|
|
|
6
6
|
document entry, add a zod configSchema and an RJSF uiConfigSchema (labels, field order,
|
|
7
7
|
DataSourceField pickers), and talk to the host from the iframe with LayoutModuleBridge (layout
|
|
8
8
|
data, host context such as layoutType / layoutObjectId / isReadOnly / dynamicHeight,
|
|
9
|
-
resolveDataPath, height). Activate when adding a layout module, integrating this
|
|
10
|
-
first time, wiring a module document to the dashboard host, or debugging a
|
|
11
|
-
build or validate (duplicate key, missing defaultSize entry, render path
|
|
12
|
-
never receives data from the host,
|
|
13
|
-
dynamic height.
|
|
9
|
+
resolveDataPath, height, loaded signal). Activate when adding a layout module, integrating this
|
|
10
|
+
package for the first time, wiring a module document to the dashboard host, or debugging a
|
|
11
|
+
manifest that fails to build or validate (duplicate key, missing defaultSize entry, render path
|
|
12
|
+
not found), a module that never receives data from the host, a host loading card that never
|
|
13
|
+
clears, or an iframe whose height collapses or keeps growing under dynamic height.
|
|
14
14
|
metadata:
|
|
15
15
|
type: core
|
|
16
16
|
library: '@persona/layout-editor-remote-module'
|
|
@@ -266,7 +266,7 @@ import { LayoutModuleBridge } from '@persona/layout-editor-remote-module/bridge-
|
|
|
266
266
|
|
|
267
267
|
export function App() {
|
|
268
268
|
return (
|
|
269
|
-
<LayoutModuleBridge
|
|
269
|
+
<LayoutModuleBridge>
|
|
270
270
|
{({ data, context, resolveDataPath }) => (
|
|
271
271
|
<RiskSummary data={data} context={context} resolveDataPath={resolveDataPath} />
|
|
272
272
|
)}
|
|
@@ -276,7 +276,9 @@ export function App() {
|
|
|
276
276
|
```
|
|
277
277
|
|
|
278
278
|
- `children` is only invoked once the host has sent the initial layout data — it never sees
|
|
279
|
-
`data: undefined`. Until then `fallback` (default `null`) is rendered
|
|
279
|
+
`data: undefined`. Until then `fallback` (default `null`) is rendered, hidden under the host's
|
|
280
|
+
loading card. After the first render the bridge posts `loaded` and the host removes the card, so
|
|
281
|
+
render any data-loading state from `children`.
|
|
280
282
|
- `data` is the module's persisted config as JSON, and re-renders whenever the host pushes an
|
|
281
283
|
update.
|
|
282
284
|
- `context` (`RemoteModuleContext`) is where the module is rendered — host-owned facts the module
|
|
@@ -291,7 +293,8 @@ export function App() {
|
|
|
291
293
|
borders, so the host can size the iframe (`observeHeight={false}` to opt out). The host uses the
|
|
292
294
|
reported height only when [`context.dynamicHeight`](#host-context) is true; otherwise, the iframe
|
|
293
295
|
keeps its grid size.
|
|
294
|
-
- A render error thrown inside `children` is caught, reported to the host, and `fallback` is shown
|
|
296
|
+
- A render error thrown inside `children` is caught, reported to the host, and `fallback` is shown
|
|
297
|
+
after the host loading overlay is dismissed.
|
|
295
298
|
- Mount exactly one `LayoutModuleBridge` per document.
|
|
296
299
|
|
|
297
300
|
Props: `fallback?: ReactNode`, `observeHeight?: boolean` (default `true`),
|
|
@@ -373,17 +376,18 @@ from there.
|
|
|
373
376
|
|
|
374
377
|
Source: `src/bridge/iframe/LayoutModuleBridge.tsx`.
|
|
375
378
|
|
|
376
|
-
### [MEDIUM]
|
|
379
|
+
### [MEDIUM] Host loading card stays visible forever when embedded
|
|
377
380
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
+
The host removes its loading card once `LayoutModuleBridge` has rendered `children`, so a card that
|
|
382
|
+
never clears usually means the handshake didn't complete. Check, in order: the document is really
|
|
383
|
+
cross-origin-embedded by a Persona host (the default `allowedHostOrigins` predicate rejects unknown
|
|
384
|
+
parents — pass your own only for a non-Persona dev host); the frame's
|
|
381
385
|
`@persona/layout-editor-remote-module` is 0.5.3 or later (a frame on 0.5.2 or older rejects an
|
|
382
386
|
`init` whose context has keys it doesn't know, e.g. `dynamicHeight`, sent by hosts on 0.8.0+, so
|
|
383
387
|
`children` never runs); and the host is rendering it through `RemoteModuleFrame` (bare `<iframe>`s
|
|
384
|
-
never send `init`).
|
|
385
|
-
|
|
386
|
-
|
|
388
|
+
never send `init`). For a direct `createModuleBridge` integration, also confirm that a bridge
|
|
389
|
+
created with `signalsLoaded: true` calls `signalLoaded()`. Opening the document directly in a tab is
|
|
390
|
+
_not_ embedded — `data` and `context` resolve to `null` and `children` renders.
|
|
387
391
|
|
|
388
392
|
Source: `src/bridge/iframe/createModuleBridge.ts`, `src/bridge/origin.ts`, `src/bridge/protocol.ts`.
|
|
389
393
|
|