improved a lot of things
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
// ── Originmain Fiber Hook ────────────────────────────────────────────────────
|
||||
// This module installs a React DevTools–compatible global hook BEFORE React
|
||||
// evaluates its module body. React checks for __REACT_DEVTOOLS_GLOBAL_HOOK__
|
||||
// exactly once at import time; any later installation is too late.
|
||||
//
|
||||
// The hook only activates when the app is iframed by Originmain (detected by
|
||||
// the `om:` prefix in `window.name`, which LiveArtboard.tsx sets on the
|
||||
// <iframe> element). Outside an Originmain iframe the module is a no-op.
|
||||
//
|
||||
// This file is intentionally self-contained — no imports from other
|
||||
// @originmain/* packages — because it ships as a public npm package.
|
||||
|
||||
const RENDERER_SOURCE = 'originmain-renderer';
|
||||
const NAME_PREFIX = 'om:';
|
||||
|
||||
// ── Guard: only run inside an Originmain iframe ──────────────────────────────
|
||||
|
||||
function isOriginmainIframe(): boolean {
|
||||
try {
|
||||
return window.parent !== window
|
||||
&& typeof window.name === 'string'
|
||||
&& window.name.startsWith(NAME_PREFIX);
|
||||
} catch {
|
||||
// Accessing window.parent can throw in certain sandboxed contexts.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isOriginmainIframe()) {
|
||||
installFiberHook();
|
||||
}
|
||||
|
||||
// ── Core ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
function installFiberHook(): void {
|
||||
const artboardId = window.name.slice(NAME_PREFIX.length);
|
||||
|
||||
function post(msg: Record<string, unknown>): void {
|
||||
try {
|
||||
window.parent.postMessage(
|
||||
{ source: RENDERER_SOURCE, artboardId, message: msg },
|
||||
'*',
|
||||
);
|
||||
} catch {
|
||||
// Parent frame unreachable — silently ignore.
|
||||
}
|
||||
}
|
||||
|
||||
// Install or wrap the global hook. If React DevTools is already present,
|
||||
// we wrap its onCommitFiberRoot so both receive commits.
|
||||
type Hook = {
|
||||
renderers: Map<unknown, unknown>;
|
||||
supportsFiber: boolean;
|
||||
_isDisabled: boolean;
|
||||
onCommitFiberRoot?: (...args: unknown[]) => void;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const g = globalThis as any;
|
||||
let hook: Hook = g.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
||||
|
||||
if (!hook) {
|
||||
hook = { renderers: new Map(), supportsFiber: true, _isDisabled: false };
|
||||
g.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook;
|
||||
}
|
||||
|
||||
const originalOnCommit = hook.onCommitFiberRoot;
|
||||
|
||||
hook.onCommitFiberRoot = function onCommitFiberRoot(...args: unknown[]) {
|
||||
// Delegate to the previous handler (React DevTools) first.
|
||||
if (typeof originalOnCommit === 'function') {
|
||||
try { originalOnCommit.apply(this, args); }
|
||||
catch { /* don't break DevTools */ }
|
||||
}
|
||||
|
||||
try {
|
||||
// args[1] is the FiberRoot — { current: Fiber }
|
||||
const root = args[1] as { current: FiberLike } | undefined;
|
||||
if (!root?.current) return;
|
||||
const tree = serializeFiber(root.current);
|
||||
post({ type: 'FIBER_TREE_UPDATE', root: tree });
|
||||
} catch (err) {
|
||||
post({ type: 'ERROR', message: String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// Signal readiness.
|
||||
post({ type: 'READY' });
|
||||
}
|
||||
|
||||
// ── Fiber Serialization ──────────────────────────────────────────────────────
|
||||
|
||||
interface FiberLike {
|
||||
type: unknown;
|
||||
index: number;
|
||||
child: FiberLike | null;
|
||||
sibling: FiberLike | null;
|
||||
stateNode: unknown;
|
||||
memoizedProps: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
interface SerializedNode {
|
||||
id: string;
|
||||
name: string;
|
||||
props: Record<string, string | number | boolean | null>;
|
||||
children: SerializedNode[];
|
||||
domRect?: { x: number; y: number; width: number; height: number };
|
||||
}
|
||||
|
||||
function serializeFiber(fiber: FiberLike | null): SerializedNode | null {
|
||||
if (!fiber) return null;
|
||||
|
||||
const name = getDisplayName(fiber);
|
||||
if (!name) return serializeFiber(fiber.child);
|
||||
|
||||
const rect = getDomRect(fiber);
|
||||
const node: SerializedNode = {
|
||||
id: String(fiber.index || Math.random()),
|
||||
name,
|
||||
props: serializeProps(fiber.memoizedProps),
|
||||
children: [],
|
||||
};
|
||||
if (rect) {
|
||||
node.domRect = rect;
|
||||
}
|
||||
|
||||
let child = fiber.child;
|
||||
while (child) {
|
||||
const serialized = serializeFiber(child);
|
||||
if (serialized) node.children.push(serialized);
|
||||
child = child.sibling;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
function getDisplayName(fiber: FiberLike): string | null {
|
||||
const type = fiber.type;
|
||||
if (!type) return null;
|
||||
if (typeof type === 'string') return type;
|
||||
if (typeof type === 'function') {
|
||||
return (type as { displayName?: string; name?: string }).displayName
|
||||
?? (type as { name?: string }).name
|
||||
?? null;
|
||||
}
|
||||
if (typeof type === 'object' && type !== null && '$$typeof' in type) {
|
||||
return (type as { displayName?: string; name?: string }).displayName
|
||||
?? (type as { name?: string }).name
|
||||
?? null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getDomRect(fiber: FiberLike): { x: number; y: number; width: number; height: number } | null {
|
||||
try {
|
||||
const dom = fiber.stateNode;
|
||||
if (dom && typeof dom === 'object' && 'getBoundingClientRect' in dom) {
|
||||
const r = (dom as Element).getBoundingClientRect();
|
||||
return { x: r.x, y: r.y, width: r.width, height: r.height };
|
||||
}
|
||||
} catch { /* no DOM node */ }
|
||||
return null;
|
||||
}
|
||||
|
||||
function serializeProps(
|
||||
props: Record<string, unknown> | null,
|
||||
): Record<string, string | number | boolean | null> {
|
||||
if (!props || typeof props !== 'object') return {};
|
||||
const out: Record<string, string | number | boolean | null> = {};
|
||||
for (const key in props) {
|
||||
if (key === 'children') continue;
|
||||
const val = props[key];
|
||||
const t = typeof val;
|
||||
if (t === 'string' || t === 'number' || t === 'boolean' || val === null) {
|
||||
out[key] = val as string | number | boolean | null;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// @originmain/live — Originmain fiber hook for live component inspection.
|
||||
//
|
||||
// Usage:
|
||||
// import '@originmain/live'; // MUST be before any React import
|
||||
// import React from 'react';
|
||||
// ...
|
||||
//
|
||||
// This is a side-effect-only import. It installs __REACT_DEVTOOLS_GLOBAL_HOOK__
|
||||
// before React evaluates, enabling Originmain to inspect the component tree,
|
||||
// read props, and generate diffs. The hook only activates when the app is
|
||||
// rendered inside an Originmain artboard iframe — otherwise it is a complete
|
||||
// no-op with zero runtime cost.
|
||||
|
||||
import './hook.js';
|
||||
Reference in New Issue
Block a user