made tiny updates
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
// ── Universal DOM Inspector Script ───────────────────────────────────────────
|
||||
// Generates a self-contained IIFE that can be injected into ANY HTML page
|
||||
// (React, Vue, static HTML — anything) via the CLI proxy or the live-sdk.
|
||||
//
|
||||
// What it does:
|
||||
// • Intercepts clicks → sends COMPONENT_SELECTED + ELEMENT_STYLES
|
||||
// • Responds to PATCH_ELEMENT_STYLE / REMOVE_ELEMENT / REQUEST_ELEMENT_STYLES
|
||||
// • Discovers routes from <a> links → sends ROUTES_DISCOVERED
|
||||
// • Applies SET_DESIGN_TOKENS as CSS custom properties on :root
|
||||
// • Sends READY once the DOM is interactive
|
||||
//
|
||||
// No React dependency. Works for any HTML page loaded in an Originmain iframe.
|
||||
|
||||
import { RENDERER_SOURCE, HOST_SOURCE } from './protocol.js';
|
||||
|
||||
/** Returns the injectable DOM inspector script as a string (IIFE). */
|
||||
export function buildDomInspectorScript(): string {
|
||||
return `(function(){
|
||||
/* ── Guard ─────────────────────────────────── */
|
||||
var PREFIX='om:',SRC=${JSON.stringify(RENDERER_SOURCE)},HOST=${JSON.stringify(HOST_SOURCE)};
|
||||
if(window.parent===window)return;
|
||||
var aid='';
|
||||
try{var n=window.name;if(typeof n==='string'&&n.indexOf(PREFIX)===0)aid=n.slice(PREFIX.length);}catch(_){}
|
||||
if(!aid)return;
|
||||
|
||||
/* ── postMessage ────────────────────────────── */
|
||||
function post(m){try{window.parent.postMessage({source:SRC,artboardId:aid,message:m},'*');}catch(_){}}
|
||||
|
||||
/* ── Element IDs ────────────────────────────── */
|
||||
var seq=0;
|
||||
function assignId(el){var cur=el.getAttribute('data-om');if(cur)return cur;var id=(++seq).toString(36);try{el.setAttribute('data-om',id);}catch(_){}return id;}
|
||||
function byId(id){return document.querySelector('[data-om="'+id+'"]');}
|
||||
|
||||
/* ── Component name (React-aware) ───────────── */
|
||||
function nameOf(el){
|
||||
var ks=Object.keys(el);
|
||||
for(var i=0;i<ks.length;i++){
|
||||
if(ks[i].indexOf('__reactFiber')!==0)continue;
|
||||
var f=el[ks[i]];
|
||||
while(f){var t=f.type;if(typeof t==='function'){var nm=t.displayName||t.name;if(nm&&!/^[a-z]/.test(nm))return nm;}f=f.return;}
|
||||
}
|
||||
var tag=(el.tagName||'el').toLowerCase();
|
||||
var id2=el.id?'#'+el.id:'';
|
||||
var cls='';
|
||||
if(el.className&&typeof el.className==='string'){var fc=el.className.trim().split(/\\s+/)[0];if(fc)cls='.'+fc;}
|
||||
return tag+(id2||cls);
|
||||
}
|
||||
|
||||
/* ── CSS capture ────────────────────────────── */
|
||||
var PROPS=['width','height','display','position','top','left','right','bottom',
|
||||
'padding-top','padding-right','padding-bottom','padding-left',
|
||||
'margin-top','margin-right','margin-bottom','margin-left',
|
||||
'background-color','color','font-size','font-weight','font-family',
|
||||
'line-height','letter-spacing','text-align','border-radius',
|
||||
'border-width','border-color','border-style','box-shadow','opacity',
|
||||
'flex-direction','align-items','justify-content','gap','overflow','z-index'];
|
||||
function capStyles(el){var cs=window.getComputedStyle(el),out={};for(var i=0;i<PROPS.length;i++){var v=cs.getPropertyValue(PROPS[i]);if(v)out[PROPS[i]]=v;}return out;}
|
||||
function capRect(el){var r=el.getBoundingClientRect();return{x:r.left,y:r.top,width:r.width,height:r.height};}
|
||||
|
||||
/* ── Selection highlight ────────────────────── */
|
||||
var selEl=null;
|
||||
function ensureCss(){
|
||||
if(document.getElementById('om-css'))return;
|
||||
var s=document.createElement('style');s.id='om-css';
|
||||
s.textContent='[data-om-s]{outline:2px solid rgba(51,133,255,0.85)!important;outline-offset:1px!important;}';
|
||||
(document.head||document.documentElement).appendChild(s);
|
||||
}
|
||||
function selectEl(el){if(selEl)selEl.removeAttribute('data-om-s');selEl=el;if(el)el.setAttribute('data-om-s','');}
|
||||
|
||||
/* ── Click handler ──────────────────────────── */
|
||||
document.addEventListener('click',function(e){
|
||||
e.preventDefault();e.stopPropagation();
|
||||
var el=e.target;
|
||||
if(!el||el===document.documentElement||el===document.body){selectEl(null);post({type:'COMPONENT_DESELECTED'});return;}
|
||||
var id=assignId(el);
|
||||
selectEl(el);
|
||||
post({type:'COMPONENT_SELECTED',nodeId:id,nodeName:nameOf(el),rect:capRect(el)});
|
||||
post({type:'ELEMENT_STYLES',nodeId:id,styles:capStyles(el)});
|
||||
},true);
|
||||
|
||||
/* ── Host messages ──────────────────────────── */
|
||||
window.addEventListener('message',function(e){
|
||||
var d=e.data;
|
||||
if(!d||d.source!==HOST||d.artboardId!==aid)return;
|
||||
var m=d.message||{};var el;
|
||||
if(m.type==='PATCH_ELEMENT_STYLE'){el=byId(m.nodeId);if(el)el.style.setProperty(m.property,m.value);}
|
||||
else if(m.type==='REMOVE_ELEMENT'){el=byId(m.nodeId);if(el)el.style.display='none';}
|
||||
else if(m.type==='REQUEST_ELEMENT_STYLES'){el=byId(m.nodeId);if(el)post({type:'ELEMENT_STYLES',nodeId:m.nodeId,styles:capStyles(el)});}
|
||||
else if(m.type==='SELECT_COMPONENT'){el=byId(m.nodeId);selectEl(el||null);}
|
||||
else if(m.type==='DESELECT'){selectEl(null);}
|
||||
else if(m.type==='SET_DESIGN_TOKENS'){var tks=m.tokens||{},ks=Object.keys(tks);for(var i=0;i<ks.length;i++)document.documentElement.style.setProperty(ks[i],tks[ks[i]]);}
|
||||
else if(m.type==='NAVIGATE'){if(m.path)window.location.pathname=m.path;}
|
||||
});
|
||||
|
||||
/* ── Route discovery ────────────────────────── */
|
||||
function discoverRoutes(){
|
||||
var seen=Object.create(null),out=[];
|
||||
var as=document.querySelectorAll('a[href]');
|
||||
for(var i=0;i<as.length;i++){
|
||||
try{
|
||||
var url=new URL(as[i].href,window.location.href);
|
||||
if(url.origin!==window.location.origin)continue;
|
||||
var p=url.pathname;if(seen[p])continue;seen[p]=true;
|
||||
var label=(as[i].textContent||'').trim().slice(0,40)||p;
|
||||
out.push({path:p,label:label});
|
||||
}catch(_){}
|
||||
}
|
||||
if(out.length)post({type:'ROUTES_DISCOVERED',routes:out});
|
||||
}
|
||||
|
||||
/* ── Init ───────────────────────────────────── */
|
||||
function init(){ensureCss();post({type:'READY'});discoverRoutes();}
|
||||
if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',init);}else{init();}
|
||||
})();`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use buildDomInspectorScript() instead.
|
||||
* Kept for backward compatibility — now returns the DOM inspector, not the fiber hook.
|
||||
*/
|
||||
export function buildProxyFiberHookScript(): string {
|
||||
return buildDomInspectorScript();
|
||||
}
|
||||
@@ -478,10 +478,13 @@ export function buildProxyFiberHookScript(): string {
|
||||
}
|
||||
|
||||
// ── Route discovery ───────────────────────────────────────────────────────
|
||||
// Scans <a href> links and the fiber nodeMap for same-origin routes, then
|
||||
// posts ROUTES_DISCOVERED so the host canvas can auto-create artboards for
|
||||
// every page. Called once 800 ms after READY (giving React time to paint)
|
||||
// and again on every SPA popstate so navigation is reflected in the canvas.
|
||||
// Mines routes from four sources (in priority order):
|
||||
// 1. window.__NEXT_DATA__ (Next.js Pages Router build manifest)
|
||||
// 2. window.__next_router_basepath (App Router)
|
||||
// 3. <a href> same-origin anchor links in the rendered DOM
|
||||
// 4. Fiber nodeMap — Link/NavLink/NextLink component props
|
||||
// Called 800 ms after READY and on every SPA popstate navigation.
|
||||
|
||||
function humanLabel(path) {
|
||||
var seg = path.replace(/\/+$/, '').split('/').filter(function(s) { return s.length > 0; });
|
||||
if (seg.length === 0) return 'Home';
|
||||
@@ -494,24 +497,58 @@ export function buildProxyFiberHookScript(): string {
|
||||
var routes = [];
|
||||
|
||||
function addRoute(path, hint) {
|
||||
if (!path || seen[path]) return;
|
||||
if (path.charAt(0) === '#') return;
|
||||
seen[path] = true;
|
||||
var label = (hint && typeof hint === 'string' && hint.trim().slice(0, 50)) || humanLabel(path);
|
||||
routes.push({ path: path, label: label });
|
||||
if (!path || typeof path !== 'string') return;
|
||||
// Skip hash fragments, external links that slipped through, and dynamic segments
|
||||
if (path.charAt(0) !== '/') return;
|
||||
// Normalise: strip trailing slash except for root
|
||||
var norm = path.length > 1 ? path.replace(/\\/+$/, '') : '/';
|
||||
if (seen[norm]) return;
|
||||
seen[norm] = true;
|
||||
var label = (hint && typeof hint === 'string' && hint.trim().slice(0, 50)) || humanLabel(norm);
|
||||
routes.push({ path: norm, label: label });
|
||||
}
|
||||
|
||||
// ① Current page is always included
|
||||
addRoute(window.location.pathname, document.title || undefined);
|
||||
|
||||
// ② Next.js Pages Router — __NEXT_DATA__ contains the full page list in build id manifest
|
||||
try {
|
||||
var nextData = window.__NEXT_DATA__;
|
||||
if (nextData && nextData.buildId) {
|
||||
// Fetch the pages manifest — available at /_next/static/{buildId}/_buildManifest.js
|
||||
var manifestUrl = '/_next/static/' + nextData.buildId + '/_buildManifest.js';
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', manifestUrl, false); // sync — runs at init time before user interaction
|
||||
xhr.send();
|
||||
if (xhr.status === 200) {
|
||||
// Manifest exposes self.__BUILD_MANIFEST = { sortedPages: [...] }
|
||||
var match = xhr.responseText.match(/sortedPages\\s*:\\s*(\\[[^\\]]+\\])/);
|
||||
if (match) {
|
||||
try {
|
||||
var pages = JSON.parse(match[1]);
|
||||
pages.forEach(function(p) {
|
||||
// Skip catch-all and dynamic segments for now; static routes only
|
||||
if (p.indexOf('[') === -1) addRoute(p);
|
||||
});
|
||||
} catch (e) { /* parse failed */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) { /* Next.js not present or manifest unavailable */ }
|
||||
|
||||
// ③ <a href> same-origin links from the rendered DOM
|
||||
var anchors = document.querySelectorAll('a[href]');
|
||||
for (var i = 0; i < anchors.length; i++) {
|
||||
try {
|
||||
var url = new URL(anchors[i].href, window.location.href);
|
||||
if (url.origin !== window.location.origin) continue;
|
||||
addRoute(url.pathname, anchors[i].textContent || undefined);
|
||||
// Skip hash-only links
|
||||
if (!url.pathname || url.hash && !url.pathname) continue;
|
||||
addRoute(url.pathname, (anchors[i].textContent || '').trim() || undefined);
|
||||
} catch (e) { /* skip malformed hrefs */ }
|
||||
}
|
||||
|
||||
// ④ Fiber nodeMap — Link/NavLink props carry href/to
|
||||
Object.keys(nodeMap).forEach(function(nid) {
|
||||
var entry = nodeMap[nid];
|
||||
var fiber = entry && entry.fiber;
|
||||
@@ -519,7 +556,11 @@ export function buildProxyFiberHookScript(): string {
|
||||
if (name && /^(Link|NavLink|NextLink|RouterLink|a)$/.test(name)) {
|
||||
var props = fiber.memoizedProps;
|
||||
var href = props && (props.href || props.to);
|
||||
if (typeof href === 'string' && href.charAt(0) === '/') addRoute(href);
|
||||
if (typeof href === 'string') addRoute(href);
|
||||
// Next.js <Link href={{ pathname }}>
|
||||
if (href && typeof href === 'object' && typeof href.pathname === 'string') {
|
||||
addRoute(href.pathname);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -529,6 +570,7 @@ export function buildProxyFiberHookScript(): string {
|
||||
// ── Ready signal ──────────────────────────────────────────────────────────
|
||||
post({ type: 'READY' });
|
||||
setTimeout(discoverRoutes, 800);
|
||||
// Re-discover on SPA navigation (Next.js App Router fires popstate on push)
|
||||
window.addEventListener('popstate', function() { setTimeout(discoverRoutes, 100); });
|
||||
})();`;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,11 @@ export type {
|
||||
RendererEnvelope,
|
||||
} from './protocol.js';
|
||||
|
||||
// React fiber hook — hooks into __REACT_DEVTOOLS_GLOBAL_HOOK__ to intercept
|
||||
// React commits. Works only in React apps loaded in an Originmain iframe.
|
||||
export { buildFiberHookScript, buildProxyFiberHookScript } from './fiber-hook.js';
|
||||
// Universal DOM inspector — fallback for static HTML pages (React not required)
|
||||
export { buildDomInspectorScript } from './dom-inspector.js';
|
||||
|
||||
export {
|
||||
createRendererHostConfig,
|
||||
|
||||
@@ -48,7 +48,7 @@ export interface HostEnvelope {
|
||||
export type RendererMessage =
|
||||
| { type: 'READY' }
|
||||
| { type: 'FIBER_TREE_UPDATE'; root: FiberNode }
|
||||
| { type: 'COMPONENT_SELECTED'; nodeId: string; rect: DOMRectLike }
|
||||
| { type: 'COMPONENT_SELECTED'; nodeId: string; nodeName?: string; rect: DOMRectLike }
|
||||
| { type: 'COMPONENT_DESELECTED' }
|
||||
| { type: 'ERROR'; message: string }
|
||||
/** Response to REQUEST_ELEMENT_STYLES — computed CSS properties for the node. */
|
||||
|
||||
Reference in New Issue
Block a user