@persona/layout-editor-remote-module
Version:
Shared manifest format, Vite plugin, and host/iframe bridge for remotely-defined dashboard layout modules in Persona dashboard apps.
294 lines (290 loc) • 8.87 kB
JavaScript
import { a as assertNotSelfOrigin, i as parseModuleToHostMessage, n as SOURCE, t as PROTOCOL_VERSION } from "./protocol-CdFmFd-i.mjs";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { jsx, jsxs } from "react/jsx-runtime";
//#region src/bridge/host/buildModuleSrc.ts
const reservedParams = new Set([
"access_token",
"environment_id",
"locale"
]);
const credentialParamPattern = /token|secret|password|authorization|api[_-]?key/i;
function buildModuleSrc(documentUrl, options = {}) {
const url = new URL(documentUrl, "http://remote-module.invalid");
for (const [key, value] of Object.entries(options.extraParams ?? {})) {
if (reservedParams.has(key) || credentialParamPattern.test(key)) throw new Error(`extraParams contains a reserved or credential-like key: ${key}`);
url.searchParams.set(key, value);
}
if (options.accessToken !== void 0) url.searchParams.set("access_token", options.accessToken);
if (options.environmentId !== void 0) url.searchParams.set("environment_id", options.environmentId);
if (options.locale !== void 0) url.searchParams.set("locale", options.locale);
if (/^[a-z][a-z\d+.-]*:\/\//i.test(documentUrl)) return url.toString();
return `${url.pathname}${url.search}${url.hash}`;
}
//#endregion
//#region src/bridge/host/createHostBridge.ts
function createHostBridge({ iframe, moduleOrigin, getData, getContext, resolveDataPath, onHeight, onRenderError, onLoaded, initRetryMs = 250, initRetryTimeoutMs = 1e4 }) {
assertNotSelfOrigin(moduleOrigin);
if (moduleOrigin === "*") throw new Error("Remote module origin must be explicit");
let destroyed = false;
let initialized = false;
let capable;
let loaded = false;
let fallbackTimer;
let fallbackTimerStarted = false;
let initRetryTimer;
const clearInitRetry = () => {
if (initRetryTimer !== void 0) {
clearInterval(initRetryTimer);
initRetryTimer = void 0;
}
};
const clearFallbackTimer = () => {
if (fallbackTimer !== void 0) {
clearTimeout(fallbackTimer);
fallbackTimer = void 0;
}
};
const markLoaded = () => {
if (loaded) return;
loaded = true;
clearFallbackTimer();
onLoaded?.();
};
const post = (message) => {
if (!destroyed) iframe.contentWindow?.postMessage(message, moduleOrigin);
};
const sendInit = () => {
initialized = true;
post({
source: SOURCE,
protocolVersion: PROTOCOL_VERSION,
type: "init",
data: getData(),
context: getContext()
});
};
const startInitRetry = () => {
clearInitRetry();
sendInit();
if (initRetryMs <= 0 || initRetryTimeoutMs <= 0) return;
const deadline = Date.now() + initRetryTimeoutMs;
initRetryTimer = setInterval(() => {
if (Date.now() >= deadline) {
clearInitRetry();
return;
}
sendInit();
}, initRetryMs);
};
const sendData = (data, requestId) => {
post({
source: SOURCE,
protocolVersion: PROTOCOL_VERSION,
type: "data",
...requestId === void 0 ? {} : { requestId },
data
});
};
const onLoad = () => {
if (!destroyed) {
if (!fallbackTimerStarted) {
fallbackTimerStarted = true;
fallbackTimer = setTimeout(() => {
fallbackTimer = void 0;
if (capable === void 0) markLoaded();
}, Math.max(initRetryTimeoutMs, 0) + 500);
}
startInitRetry();
}
};
const onMessage = (event) => {
if (destroyed || event.origin !== moduleOrigin || event.source !== iframe.contentWindow) return;
const message = parseModuleToHostMessage(event.data);
if (!message) return;
clearInitRetry();
if (message.type === "ready") {
if (capable === void 0) {
capable = message.capabilities?.loaded === true;
if (!capable) markLoaded();
}
} else if (message.type !== "loaded" && message.type !== "render-error" && capable === void 0) {
capable = false;
markLoaded();
}
switch (message.type) {
case "ready":
sendInit();
break;
case "loaded":
markLoaded();
break;
case "request-data":
sendData(getData(), message.requestId);
break;
case "set-height":
onHeight?.(message.height);
break;
case "render-error":
markLoaded();
onRenderError?.(message.message);
break;
case "resolve-data-path":
Promise.resolve().then(() => resolveDataPath(message.dataPath, message.variableMap)).then((result) => {
post({
source: SOURCE,
protocolVersion: PROTOCOL_VERSION,
type: "data-path-result",
requestId: message.requestId,
result: result ?? null
});
}, (error) => {
post({
source: SOURCE,
protocolVersion: PROTOCOL_VERSION,
type: "data-path-result",
requestId: message.requestId,
error: error instanceof Error ? error.message : String(error)
});
});
break;
}
};
window.addEventListener("message", onMessage);
iframe.addEventListener("load", onLoad);
return {
pushData(data) {
if (!destroyed && initialized) sendData(data);
},
pushContext(context) {
if (!destroyed && initialized) post({
source: SOURCE,
protocolVersion: PROTOCOL_VERSION,
type: "context",
context
});
},
destroy() {
if (destroyed) return;
destroyed = true;
clearInitRetry();
clearFallbackTimer();
window.removeEventListener("message", onMessage);
iframe.removeEventListener("load", onLoad);
}
};
}
//#endregion
//#region src/bridge/host/RemoteModuleFrame.tsx
function withDynamicHeight(context, dynamicHeight) {
return {
...context,
dynamicHeight
};
}
function RemoteModuleFrame({ documentUrl, moduleOrigin, accessToken, environmentId, locale, extraParams, title, sandbox, allow, referrerPolicy = "no-referrer", initRetryMs, initRetryTimeoutMs, dynamicHeight = false, heightOffset = 0, minHeight, className, style, iframeRef: externalIframeRef, data, context, resolveDataPath, onRenderError, loadingFallback }) {
const iframeRef = useRef(null);
const dataRef = useRef(data);
const contextRef = useRef(withDynamicHeight(context, dynamicHeight));
const onRenderErrorRef = useRef(onRenderError);
const resolveDataPathRef = useRef(resolveDataPath);
const bridgeRef = useRef(void 0);
const externalCleanupRef = useRef(void 0);
const [height, setHeight] = useState();
const [loadedSrc, setLoadedSrc] = useState();
const setRefs = useCallback((node) => {
iframeRef.current = node;
if (typeof externalIframeRef === "function") if (node) {
const cleanup = externalIframeRef(node);
externalCleanupRef.current = typeof cleanup === "function" ? cleanup : void 0;
} else if (externalCleanupRef.current) {
const cleanup = externalCleanupRef.current;
externalCleanupRef.current = void 0;
cleanup();
} else externalIframeRef(null);
else if (externalIframeRef) externalIframeRef.current = node;
}, [externalIframeRef]);
const hostContext = useMemo(() => withDynamicHeight(context, dynamicHeight), [context, dynamicHeight]);
dataRef.current = data;
contextRef.current = hostContext;
onRenderErrorRef.current = onRenderError;
resolveDataPathRef.current = resolveDataPath;
const src = useMemo(() => buildModuleSrc(documentUrl, {
accessToken,
environmentId,
locale,
extraParams
}), [
accessToken,
documentUrl,
environmentId,
extraParams,
locale
]);
useEffect(() => {
const iframe$1 = iframeRef.current;
if (!iframe$1) return;
const bridge = createHostBridge({
iframe: iframe$1,
moduleOrigin,
getData: () => dataRef.current,
getContext: () => contextRef.current,
resolveDataPath: (...args) => resolveDataPathRef.current(...args),
initRetryMs,
initRetryTimeoutMs,
onHeight: (nextHeight) => {
setHeight(nextHeight);
},
onRenderError: (message) => onRenderErrorRef.current?.(message),
onLoaded: () => setLoadedSrc(src)
});
bridgeRef.current = bridge;
return () => {
bridge.destroy();
bridgeRef.current = void 0;
};
}, [
initRetryMs,
initRetryTimeoutMs,
moduleOrigin,
src
]);
useEffect(() => {
bridgeRef.current?.pushData(data);
}, [data]);
useEffect(() => {
bridgeRef.current?.pushContext(hostContext);
}, [hostContext]);
const computedHeight = dynamicHeight && height !== void 0 ? `${Math.max(height + heightOffset, minHeight ?? 0)}px` : minHeight === void 0 ? "100%" : `${minHeight}px`;
const iframe = /* @__PURE__ */ jsx("iframe", {
ref: setRefs,
src,
title,
sandbox,
allow,
referrerPolicy,
className,
style: {
...style,
height: computedHeight,
display: style?.display ?? "block",
width: "100%",
border: "none"
}
}, src);
return /* @__PURE__ */ jsxs("div", {
style: {
position: "relative",
width: "100%",
height: computedHeight === "100%" ? "100%" : void 0
},
children: [iframe, loadingFallback !== void 0 && !(loadedSrc === src) && /* @__PURE__ */ jsx("div", {
style: {
position: "absolute",
inset: 0
},
children: loadingFallback
})]
});
}
//#endregion
export { RemoteModuleFrame };