improved a lot of things
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
import { RENDERER_SOURCE } from './protocol.js';
|
||||
import type { FiberNode, DOMRectLike } from './protocol.js';
|
||||
|
||||
// ── Fiber hook script ─────────────────────────────────────────────────────────
|
||||
// This script is injected into the sandboxed iframe before the remote app
|
||||
// initialises. It installs a React DevTools global hook so React reports every
|
||||
// commit. On each commit, we walk the Fiber tree, serialize it to FiberNode[],
|
||||
// and postMessage the result to the host.
|
||||
//
|
||||
// The script must be self-contained (no imports) because it runs in the iframe.
|
||||
// We generate it as a string via buildFiberHookScript() so it can be injected
|
||||
// via a <script> tag or a blob URL.
|
||||
|
||||
export function buildFiberHookScript(artboardId: string): string {
|
||||
// Inline the constants and logic — the iframe has no access to this module.
|
||||
return `(function(artboardId) {
|
||||
var SOURCE = ${JSON.stringify(RENDERER_SOURCE)};
|
||||
|
||||
// Install the React DevTools global hook BEFORE React loads.
|
||||
// React checks for this object at module evaluation time and registers itself.
|
||||
var hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
||||
if (!hook) {
|
||||
hook = { renderers: new Map(), _isDisabled: false };
|
||||
window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook;
|
||||
}
|
||||
|
||||
var originalOnCommitFiberRoot = hook.onCommitFiberRoot;
|
||||
|
||||
hook.onCommitFiberRoot = function(rendererId, root, priorityLevel, didError) {
|
||||
if (typeof originalOnCommitFiberRoot === 'function') {
|
||||
originalOnCommitFiberRoot.call(this, rendererId, root, priorityLevel, didError);
|
||||
}
|
||||
try {
|
||||
var fiberRoot = root.current;
|
||||
var tree = serializeFiber(fiberRoot);
|
||||
window.parent.postMessage(
|
||||
{ source: SOURCE, artboardId: artboardId, message: { type: 'FIBER_TREE_UPDATE', root: tree } },
|
||||
'*'
|
||||
);
|
||||
} catch (err) {
|
||||
window.parent.postMessage(
|
||||
{ source: SOURCE, artboardId: artboardId, message: { type: 'ERROR', message: String(err) } },
|
||||
'*'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function serializeFiber(fiber) {
|
||||
if (!fiber) return null;
|
||||
var name = getDisplayName(fiber);
|
||||
if (!name) return serializeFiber(fiber.child) || null;
|
||||
|
||||
var rect = getDomRect(fiber);
|
||||
var node = {
|
||||
id: String(fiber.index || Math.random()),
|
||||
name: name,
|
||||
props: serializeProps(fiber.memoizedProps),
|
||||
children: [],
|
||||
domRect: rect || undefined,
|
||||
};
|
||||
|
||||
var child = fiber.child;
|
||||
while (child) {
|
||||
var serialized = serializeFiber(child);
|
||||
if (serialized) node.children.push(serialized);
|
||||
child = child.sibling;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
function getDisplayName(fiber) {
|
||||
var type = fiber.type;
|
||||
if (!type) return null;
|
||||
if (typeof type === 'string') return type;
|
||||
if (typeof type === 'function') return type.displayName || type.name || null;
|
||||
if (type.$$typeof) return type.displayName || type.name || null;
|
||||
return null;
|
||||
}
|
||||
|
||||
function getDomRect(fiber) {
|
||||
try {
|
||||
var dom = fiber.stateNode;
|
||||
if (dom && dom.getBoundingClientRect) {
|
||||
var r = dom.getBoundingClientRect();
|
||||
return { x: r.x, y: r.y, width: r.width, height: r.height };
|
||||
}
|
||||
} catch (_) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
function serializeProps(props) {
|
||||
if (!props || typeof props !== 'object') return {};
|
||||
var out = {};
|
||||
for (var key in props) {
|
||||
if (key === 'children') continue;
|
||||
var val = props[key];
|
||||
var type = typeof val;
|
||||
if (type === 'string' || type === 'number' || type === 'boolean' || val === null) {
|
||||
out[key] = val;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Signal that the renderer iframe is ready.
|
||||
window.parent.postMessage(
|
||||
{ source: SOURCE, artboardId: artboardId, message: { type: 'READY' } },
|
||||
'*'
|
||||
);
|
||||
})(${JSON.stringify(artboardId)});`;
|
||||
}
|
||||
|
||||
// Re-export types for convenience
|
||||
export type { FiberNode, DOMRectLike };
|
||||
@@ -1 +1,26 @@
|
||||
export {};
|
||||
export {
|
||||
HOST_SOURCE,
|
||||
RENDERER_SOURCE,
|
||||
isHostEnvelope,
|
||||
isRendererEnvelope,
|
||||
createHostEnvelope,
|
||||
createRendererEnvelope,
|
||||
} from './protocol.js';
|
||||
|
||||
export type {
|
||||
FiberNode,
|
||||
DOMRectLike,
|
||||
HostMessage,
|
||||
HostEnvelope,
|
||||
RendererMessage,
|
||||
RendererEnvelope,
|
||||
} from './protocol.js';
|
||||
|
||||
export { buildFiberHookScript } from './fiber-hook.js';
|
||||
|
||||
export {
|
||||
createRendererHostConfig,
|
||||
createRemoteConfig,
|
||||
} from './module-federation.js';
|
||||
|
||||
export type { RemoteConfig, RendererHostOptions } from './module-federation.js';
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// ── Module Federation host configuration helper ───────────────────────────────
|
||||
// The Originmain renderer acts as the MF *host*. The connected application acts
|
||||
// as the *remote*, exposing its component routes via ModuleFederationPlugin.
|
||||
//
|
||||
// Usage in packages/app/next.config.ts (or a custom webpack.config.js):
|
||||
// import { createRendererHostConfig } from '@originmain/renderer';
|
||||
// const { ModuleFederationPlugin } = require('webpack').container;
|
||||
// new ModuleFederationPlugin(createRendererHostConfig({ remoteUrl }))
|
||||
|
||||
export interface RemoteConfig {
|
||||
/** Unique name for this remote (used as the JS namespace, e.g. "connected_app") */
|
||||
name: string;
|
||||
/** Public URL of the remote's remoteEntry.js, e.g. "http://localhost:3001/remoteEntry.js" */
|
||||
url: string;
|
||||
/** Component paths exposed by the remote, e.g. ["./Button", "./Card"] */
|
||||
exposes?: string[];
|
||||
}
|
||||
|
||||
export interface RendererHostOptions {
|
||||
/** All remote applications to wire up */
|
||||
remotes?: RemoteConfig[];
|
||||
}
|
||||
|
||||
/** Webpack ModuleFederationPlugin config for the Originmain renderer host. */
|
||||
export function createRendererHostConfig(opts: RendererHostOptions = {}) {
|
||||
const { remotes = [] } = opts;
|
||||
|
||||
const remotesMap: Record<string, string> = {};
|
||||
for (const r of remotes) {
|
||||
// Webpack MF syntax: "namespace@url"
|
||||
remotesMap[r.name] = `${r.name}@${r.url}`;
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'originmain_host',
|
||||
remotes: remotesMap,
|
||||
// React and ReactDOM must be singletons — a duplicate React instance causes
|
||||
// hooks to fail silently across the host/remote boundary.
|
||||
shared: {
|
||||
react: { singleton: true, requiredVersion: '>=19', eager: false },
|
||||
'react-dom': { singleton: true, requiredVersion: '>=19', eager: false },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Webpack ModuleFederationPlugin config scaffold for the *remote* (connected app). */
|
||||
export function createRemoteConfig(opts: {
|
||||
name: string;
|
||||
exposes: Record<string, string>;
|
||||
publicPath?: string;
|
||||
}) {
|
||||
return {
|
||||
name: opts.name,
|
||||
filename: 'remoteEntry.js',
|
||||
exposes: opts.exposes,
|
||||
publicPath: opts.publicPath ?? 'auto',
|
||||
shared: {
|
||||
react: { singleton: true, requiredVersion: '>=19' },
|
||||
'react-dom': { singleton: true, requiredVersion: '>=19' },
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// ── Source discriminants ──────────────────────────────────────────────────────
|
||||
// All postMessage envelopes carry a `source` field so the host and renderer can
|
||||
// ignore messages from unrelated parties (browser extensions, devtools, etc.).
|
||||
|
||||
export const HOST_SOURCE = 'originmain-host' as const;
|
||||
export const RENDERER_SOURCE = 'originmain-renderer' as const;
|
||||
|
||||
// ── Fiber node ────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface FiberNode {
|
||||
id: string;
|
||||
name: string;
|
||||
props: Record<string, unknown>;
|
||||
children: FiberNode[];
|
||||
domRect?: DOMRectLike;
|
||||
}
|
||||
|
||||
export interface DOMRectLike {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
// ── Host → Renderer messages ──────────────────────────────────────────────────
|
||||
|
||||
export type HostMessage =
|
||||
| { type: 'SET_DESIGN_TOKENS'; tokens: Record<string, string> }
|
||||
| { type: 'NAVIGATE'; path: string }
|
||||
| { type: 'SELECT_COMPONENT'; nodeId: string }
|
||||
| { type: 'DESELECT' }
|
||||
| { type: 'INJECT_FIBER_HOOK' };
|
||||
|
||||
export interface HostEnvelope {
|
||||
source: typeof HOST_SOURCE;
|
||||
artboardId: string;
|
||||
message: HostMessage;
|
||||
}
|
||||
|
||||
// ── Renderer → Host messages ──────────────────────────────────────────────────
|
||||
|
||||
export type RendererMessage =
|
||||
| { type: 'READY' }
|
||||
| { type: 'FIBER_TREE_UPDATE'; root: FiberNode }
|
||||
| { type: 'COMPONENT_SELECTED'; nodeId: string; rect: DOMRectLike }
|
||||
| { type: 'COMPONENT_DESELECTED' }
|
||||
| { type: 'ERROR'; message: string };
|
||||
|
||||
export interface RendererEnvelope {
|
||||
source: typeof RENDERER_SOURCE;
|
||||
artboardId: string;
|
||||
message: RendererMessage;
|
||||
}
|
||||
|
||||
// ── Type guards ───────────────────────────────────────────────────────────────
|
||||
|
||||
export function isHostEnvelope(data: unknown): data is HostEnvelope {
|
||||
return (
|
||||
typeof data === 'object' &&
|
||||
data !== null &&
|
||||
(data as HostEnvelope).source === HOST_SOURCE
|
||||
);
|
||||
}
|
||||
|
||||
export function isRendererEnvelope(data: unknown): data is RendererEnvelope {
|
||||
return (
|
||||
typeof data === 'object' &&
|
||||
data !== null &&
|
||||
(data as RendererEnvelope).source === RENDERER_SOURCE
|
||||
);
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export function createHostEnvelope(
|
||||
artboardId: string,
|
||||
message: HostMessage
|
||||
): HostEnvelope {
|
||||
return { source: HOST_SOURCE, artboardId, message };
|
||||
}
|
||||
|
||||
export function createRendererEnvelope(
|
||||
artboardId: string,
|
||||
message: RendererMessage
|
||||
): RendererEnvelope {
|
||||
return { source: RENDERER_SOURCE, artboardId, message };
|
||||
}
|
||||
Reference in New Issue
Block a user