improved a lot of things

This commit is contained in:
SinachPat
2026-04-30 16:47:26 +01:00
parent 0c3ac48a3b
commit 42ba668399
11 changed files with 650 additions and 149 deletions
+3
View File
@@ -0,0 +1,3 @@
# Allow esbuild to run its install script (downloads the native binary)
build-scripts-allowed=esbuild
+2
View File
@@ -9,6 +9,8 @@
"test": "vitest run --coverage",
"build": "pnpm --filter @originmain/app run build",
"dev": "pnpm --filter @originmain/app run dev",
"cli:build": "pnpm --filter @originmain/cli run build",
"cli:dev": "node packages/cli/dist/cli.js dev",
"migration:dry-run": "supabase db diff --schema public"
},
"devDependencies": {
@@ -43,10 +43,17 @@ export function LiveArtboard({
onComponentSelected,
style,
}: LiveArtboardProps) {
const iframeRef = useRef<HTMLIFrameElement>(null);
const iframeRef = useRef<HTMLIFrameElement>(null);
// Track whether the iframe has sent READY so we don't send messages too early.
const isReadyRef = useRef(false);
// Reset ready state whenever src changes. Without this, isReadyRef stays
// true from the previous page, causing design-token / selection effects to
// fire against a half-loaded iframe between navigation and the new READY.
useEffect(() => {
isReadyRef.current = false;
}, [src]);
// ── Send a typed message to the iframe ───────────────────────────────────
const sendMessage = useCallback(
(type: Parameters<typeof createHostEnvelope>[1]['type'], payload?: Record<string, unknown>) => {
@@ -85,6 +92,10 @@ export function LiveArtboard({
case 'COMPONENT_SELECTED':
onComponentSelected?.(msg.nodeId);
break;
case 'COMPONENT_DESELECTED':
// Renderer clicked empty space — clear the host-side selection.
onComponentSelected?.('');
break;
}
}
@@ -107,7 +107,9 @@ export const TOUR_STEPS: TourStep[] = [
</>
),
codeBlock: `# Terminal 2 — keep your dev server running in Terminal 1
npx @originmain/cli dev --target http://localhost:3000
# From the monorepo root:
pnpm cli:build # one-time build (skip if already built)
pnpm cli:dev --target http://localhost:3000
# ✓ Proxy listening on http://localhost:4170
# Paste http://localhost:4170 into the artboard URL input`,
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env node
// ── CLI bundle script ─────────────────────────────────────────────────────────
// Produces two self-contained ESM bundles under dist/:
//
// dist/cli.js — the `originmain` binary (has shebang, chmod +x)
// dist/index.js — programmatic API: startProxy(), injectFiberHook()
//
// Run: node build.mjs (or via "pnpm build")
import { build } from 'esbuild';
import { readFileSync, writeFileSync, chmodSync } from 'node:fs';
// Node.js built-in module names (without and with the node: prefix).
// Both forms must be listed so esbuild leaves them as-is whether the
// source imports 'http' or 'node:http'.
const NODE_BUILTINS = [
'node:*',
'assert', 'buffer', 'child_process', 'cluster', 'console', 'constants',
'crypto', 'dgram', 'dns', 'domain', 'events', 'fs', 'http', 'http2',
'https', 'module', 'net', 'os', 'path', 'perf_hooks', 'process',
'punycode', 'querystring', 'readline', 'repl', 'stream', 'string_decoder',
'sys', 'timers', 'tls', 'trace_events', 'tty', 'url', 'util', 'v8',
'vm', 'worker_threads', 'zlib',
];
const SHARED_OPTIONS = {
bundle: true,
platform: 'node',
format: 'esm',
target: 'node22',
external: NODE_BUILTINS,
logLevel: 'info',
};
// ── Build both entry points in parallel ──────────────────────────────────────
await Promise.all([
build({ ...SHARED_OPTIONS, entryPoints: ['src/cli.ts'], outfile: 'dist/cli.js' }),
build({ ...SHARED_OPTIONS, entryPoints: ['src/index.ts'], outfile: 'dist/index.js' }),
]);
// ── Add shebang + executable bit to the CLI binary ───────────────────────────
const SHEBANG = '#!/usr/bin/env node\n';
const cliContent = readFileSync('dist/cli.js', 'utf-8');
if (!cliContent.startsWith('#!')) {
writeFileSync('dist/cli.js', SHEBANG + cliContent, 'utf-8');
}
chmodSync('dist/cli.js', 0o755);
console.log(' ✓ dist/cli.js (binary)');
console.log(' ✓ dist/index.js (programmatic API)');
+13 -5
View File
@@ -8,17 +8,25 @@
"originmain": "./dist/cli.js"
},
"exports": {
".": "./src/index.ts"
".": {
"import": "./dist/index.js",
"default": "./dist/index.js"
}
},
"files": [
"dist/cli.js",
"dist/index.js",
"README.md"
],
"scripts": {
"typecheck": "tsc --noEmit",
"build": "tsc -p tsconfig.build.json"
},
"dependencies": {
"@originmain/renderer": "workspace:*"
"build": "node build.mjs"
},
"dependencies": {},
"devDependencies": {
"@originmain/renderer": "workspace:*",
"@types/node": "^22.0.0",
"esbuild": "^0.28.0",
"typescript": "^5.5.0"
},
"engines": {
+2 -1
View File
@@ -47,7 +47,8 @@ function main(): void {
if (values.help || !command) {
printUsage();
process.exit(command ? 0 : 1);
// --help is a success; missing command is a usage error.
process.exit(values.help ? 0 : 1);
}
if (command !== 'dev') {
+30 -10
View File
@@ -15,31 +15,51 @@ function getScriptTag(): string {
return cachedScriptTag;
}
/**
* Strip inline Content-Security-Policy meta tags from HTML.
*
* The proxy already removes the CSP response header, but some frameworks
* (e.g. Next.js with a custom _document) also embed CSP inside a meta tag.
* A meta CSP applies to the entire document — including scripts parsed before
* it — so our injected hook can be silently blocked even though it runs first.
* Removing these tags lets the hook execute freely inside the sandboxed iframe.
*/
function stripMetaCsp(html: string): string {
return html.replace(
/<meta[^>]+http-equiv\s*=\s*["']?\s*content-security-policy\s*["']?[^>]*\/?>/gi,
'',
);
}
/**
* Inject the fiber hook script into an HTML string.
*
* Injection strategy (in order of preference):
* 1. After `<head...>` — standard position for early scripts
* 2. After `<html...>` — fallback if <head> is missing
* 3. Prepend to document — final fallback
* Steps:
* 1. Strip any inline Content-Security-Policy meta tags that could block the
* injected script (the CSP response header is stripped by the proxy itself).
* 2. Insert the hook script immediately after the opening <head> tag
* (preferred), after <html> (fallback), or prepend to the document (final
* fallback). Placing it first ensures __REACT_DEVTOOLS_GLOBAL_HOOK__ is
* registered before React's module body runs.
*/
export function injectFiberHook(html: string): string {
const tag = getScriptTag();
const tag = getScriptTag();
const cleaned = stripMetaCsp(html);
// Try after <head>
const headMatch = /<head[^>]*>/i.exec(html);
const headMatch = /<head[^>]*>/i.exec(cleaned);
if (headMatch) {
const insertAt = headMatch.index + headMatch[0].length;
return html.slice(0, insertAt) + tag + html.slice(insertAt);
return cleaned.slice(0, insertAt) + tag + cleaned.slice(insertAt);
}
// Try after <html>
const htmlMatch = /<html[^>]*>/i.exec(html);
const htmlMatch = /<html[^>]*>/i.exec(cleaned);
if (htmlMatch) {
const insertAt = htmlMatch.index + htmlMatch[0].length;
return html.slice(0, insertAt) + tag + html.slice(insertAt);
return cleaned.slice(0, insertAt) + tag + cleaned.slice(insertAt);
}
// Final fallback: prepend
return tag + html;
return tag + cleaned;
}
+154 -64
View File
@@ -1,17 +1,20 @@
// ── Reverse Proxy ────────────────────────────────────────────────────────────
// HTTP reverse proxy that:
// 1. Forwards all requests to the target dev server
// HTTP(S) reverse proxy that:
// 1. Forwards all requests to the target dev server (http or https)
// 2. Strips X-Frame-Options and CSP frame-ancestors from responses
// 3. Injects the Originmain fiber hook into HTML responses
// 4. Passes WebSocket upgrades through for HMR
//
// Uses only Node.js built-ins — no external dependencies.
import { createServer, request as httpRequest } from 'node:http';
import { connect as netConnect } from 'node:net';
import type { IncomingMessage, ServerResponse } from 'node:http';
import type { Socket } from 'node:net';
import { injectFiberHook } from './inject.js';
import { createServer } from 'node:http';
import { request as httpRequest } from 'node:http';
import { request as httpsRequest } from 'node:https';
import { connect as netConnect } from 'node:net';
import type { IncomingMessage, ServerResponse,
RequestOptions, ClientRequest } from 'node:http';
import type { Socket } from 'node:net';
import { injectFiberHook } from './inject.js';
export interface ProxyOptions {
/** Target dev server URL, e.g. "http://localhost:3000" */
@@ -27,88 +30,141 @@ const STRIP_RESPONSE_HEADERS = new Set([
'content-security-policy-report-only',
]);
/** CORS headers added to every response for cross-origin API compatibility. */
/** CORS headers appended to every response so the canvas can reach the app. */
const CORS_HEADERS: Record<string, string> = {
'access-control-allow-origin': '*',
'access-control-allow-methods': '*',
'access-control-allow-headers': '*',
'access-control-allow-origin': '*',
'access-control-allow-methods': '*',
'access-control-allow-headers': '*',
'access-control-allow-credentials': 'true',
};
// ── Outgoing request helper ────────────────────────────────────────────────
// Chooses http / https based on the target protocol.
// rejectUnauthorized is disabled for HTTPS because dev / staging servers
// frequently use self-signed certificates.
function doRequest(
isHttps: boolean,
options: RequestOptions,
cb: (res: IncomingMessage) => void,
): ClientRequest {
if (isHttps) {
return httpsRequest({ ...options, rejectUnauthorized: false }, cb);
}
return httpRequest(options, cb);
}
/**
* Start the reverse proxy server.
* Returns a cleanup function that shuts down the server.
* Returns an object with a `close()` method for graceful shutdown.
*/
export function startProxy(opts: ProxyOptions): { close: () => void } {
const targetUrl = new URL(opts.target);
const targetUrl = new URL(opts.target);
const isHttps = targetUrl.protocol === 'https:';
const targetHost = targetUrl.hostname;
const targetPort = parseInt(targetUrl.port || '80', 10);
// Default port: 443 for HTTPS, 80 for HTTP — matches browser behaviour.
const targetPort = parseInt(targetUrl.port || (isHttps ? '443' : '80'), 10);
const server = createServer((clientReq: IncomingMessage, clientRes: ServerResponse) => {
// ── Build the outgoing request to the target ─────────────────────────
// ── Build the outgoing request headers ────────────────────────────────
// Clone headers, stripping Accept-Encoding so the target sends
// uncompressed HTML (avoids needing to decompress before injection).
const outHeaders: Record<string, string | string[]> = {};
for (const [key, val] of Object.entries(clientReq.headers)) {
if (key.toLowerCase() === 'accept-encoding') continue;
if (key.toLowerCase() === 'host') {
const lower = key.toLowerCase();
// Strip Accept-Encoding — we replace it with 'identity' below so the
// target always returns uncompressed HTML that we can decode and inject.
if (lower === 'accept-encoding') continue;
// Rewrite Host to point at the actual target, not the proxy.
if (lower === 'host') {
outHeaders[key] = `${targetHost}:${targetPort}`;
continue;
}
if (val !== undefined) {
outHeaders[key] = val;
}
if (val !== undefined) outHeaders[key] = val as string | string[];
}
const proxyReq = httpRequest(
// Explicitly ask for uncompressed content.
outHeaders['accept-encoding'] = 'identity';
const method = clientReq.method ?? 'GET';
const isHeadReq = method === 'HEAD';
const proxyReq = doRequest(
isHttps,
{
hostname: targetHost,
port: targetPort,
path: clientReq.url ?? '/',
method: clientReq.method,
headers: outHeaders,
port: targetPort,
path: clientReq.url ?? '/',
method,
headers: outHeaders,
},
(proxyRes) => {
// ── Process response headers ──────────────────────────────────
(proxyRes: IncomingMessage) => {
// ── Build clean response headers ──────────────────────────────────
const resHeaders: Record<string, string | string[]> = {};
for (const [key, val] of Object.entries(proxyRes.headers)) {
if (STRIP_RESPONSE_HEADERS.has(key.toLowerCase())) continue;
if (val !== undefined) {
resHeaders[key] = val;
}
if (val !== undefined) resHeaders[key] = val as string | string[];
}
// Add CORS headers
// Append CORS.
for (const [key, val] of Object.entries(CORS_HEADERS)) {
resHeaders[key] = val;
}
// ── Determine if this is an HTML response ──────────────────────
// ── Detect HTML responses ─────────────────────────────────────────
const contentType = (proxyRes.headers['content-type'] ?? '').toLowerCase();
const isHtml = contentType.includes('text/html');
const isHtml = contentType.includes('text/html');
if (!isHtml) {
// Non-HTML: stream through unchanged (headers already stripped)
// HEAD and non-HTML responses: stream (or drain) through unchanged.
// HEAD: RFC 7231 §4.3.2 — response MUST NOT include a body.
// We must drain the response to free the socket, but send no body.
// Non-HTML: no injection needed, stream straight through.
if (isHeadReq || !isHtml) {
clientRes.writeHead(proxyRes.statusCode ?? 200, resHeaders);
proxyRes.pipe(clientRes);
proxyRes.on('error', (err: Error) => {
console.error(`[originmain proxy] Response stream error: ${err.message}`);
clientRes.destroy();
});
if (isHeadReq) {
// Drain + end without sending body.
proxyRes.resume();
proxyRes.on('end', () => clientRes.end());
} else {
proxyRes.pipe(clientRes);
}
return;
}
// HTML: buffer the full body, inject the script, then send.
const chunks: Buffer[] = [];
proxyRes.on('data', (chunk: Buffer) => chunks.push(chunk));
proxyRes.on('end', () => {
const rawHtml = Buffer.concat(chunks).toString('utf-8');
const injectedHtml = injectFiberHook(rawHtml);
const body = Buffer.from(injectedHtml, 'utf-8');
// HTML GET/POST: buffer, inject the fiber hook script, then send.
// Strip Content-Encoding — we decode to UTF-8, so the encoding header
// would be incorrect after injection.
delete resHeaders['content-encoding'];
// Update Content-Length to match the injected body
const chunks: Buffer[] = [];
proxyRes.on('data', (chunk: Buffer) => chunks.push(chunk));
proxyRes.on('error', (err: Error) => {
console.error(`[originmain proxy] HTML response stream error: ${err.message}`);
if (!clientRes.headersSent) {
clientRes.writeHead(502, { 'content-type': 'text/plain' });
}
clientRes.destroy();
});
proxyRes.on('end', () => {
const rawHtml = Buffer.concat(chunks).toString('utf-8');
const injectedHtml = injectFiberHook(rawHtml);
const body = Buffer.from(injectedHtml, 'utf-8');
// Correct Content-Length and drop Transfer-Encoding: chunked.
resHeaders['content-length'] = String(body.byteLength);
// Remove Transfer-Encoding: chunked since we send the full body
delete resHeaders['transfer-encoding'];
clientRes.writeHead(proxyRes.statusCode ?? 200, resHeaders);
@@ -117,51 +173,82 @@ export function startProxy(opts: ProxyOptions): { close: () => void } {
},
);
proxyReq.on('error', (err) => {
// ── Handle target-unreachable errors ──────────────────────────────────
proxyReq.on('error', (err: Error) => {
console.error(`[originmain proxy] Target request failed: ${err.message}`);
if (!clientRes.headersSent) {
clientRes.writeHead(502, { 'content-type': 'text/plain' });
clientRes.end(
`Originmain proxy: could not reach target at ${opts.target}\n${err.message}`,
);
} else {
// Headers already on the wire — tear down the connection cleanly.
clientRes.destroy();
}
clientRes.end(`Originmain proxy: could not reach target at ${opts.target}\n${err.message}`);
});
// Pipe the client request body to the target
// ── Handle client disconnects ─────────────────────────────────────────
clientReq.on('error', (err: Error) => {
console.error(`[originmain proxy] Client request error: ${err.message}`);
proxyReq.destroy();
});
// Pipe the request body (relevant for POST / PUT / PATCH).
clientReq.pipe(proxyReq);
});
// ── WebSocket upgrade passthrough (for HMR) ─────────────────────────────
// ── Friendly error for port conflicts ─────────────────────────────────────
server.on('error', (err: NodeJS.ErrnoException) => {
if (err.code === 'EADDRINUSE') {
console.error(`\n \x1b[31mError:\x1b[0m Port ${opts.port} is already in use.`);
console.error(
` Try a different port: originmain dev --target ${opts.target} --port ${opts.port + 1}\n`,
);
process.exit(1);
}
throw err;
});
// ── WebSocket upgrade passthrough (for HMR) ───────────────────────────────
server.on('upgrade', (req: IncomingMessage, clientSocket: Socket, head: Buffer) => {
// Open a TCP connection to the target and forward the HTTP upgrade
const targetSocket = netConnect(targetPort, targetHost, () => {
// Reconstruct the raw HTTP upgrade request
const reqLine = `${req.method} ${req.url} HTTP/${req.httpVersion}\r\n`;
const headers = Object.entries(req.headers)
// Reconstruct the raw HTTP upgrade request.
const reqLine = `${req.method ?? 'GET'} ${req.url ?? '/'} HTTP/${req.httpVersion}\r\n`;
const hostHdr = `host: ${targetHost}:${targetPort}`;
const otherHdr = Object.entries(req.headers)
.filter(([key]) => key.toLowerCase() !== 'host')
.map(([key, val]) => `${key}: ${Array.isArray(val) ? val.join(', ') : val ?? ''}`)
.map(([key, val]) => `${key}: ${Array.isArray(val) ? val.join(', ') : (val ?? '')}`)
.join('\r\n');
const hostHeader = `host: ${targetHost}:${targetPort}`;
targetSocket.write(`${reqLine}${hostHeader}\r\n${headers}\r\n\r\n`);
if (head.length > 0) {
targetSocket.write(head);
}
targetSocket.write(`${reqLine}${hostHdr}\r\n${otherHdr}\r\n\r\n`);
if (head.length > 0) targetSocket.write(head);
// Pipe bidirectionally
// Bidirectional pipe.
targetSocket.pipe(clientSocket);
clientSocket.pipe(targetSocket);
});
targetSocket.on('error', (err) => {
// Ensure both sockets are torn down when either side closes.
targetSocket.on('close', () => clientSocket.destroy());
clientSocket.on('close', () => targetSocket.destroy());
targetSocket.on('error', (err: Error) => {
console.error(`[originmain proxy] WebSocket proxy error: ${err.message}`);
clientSocket.destroy();
});
clientSocket.on('error', () => {
clientSocket.on('error', (err: Error) => {
console.error(`[originmain proxy] WebSocket client error: ${err.message}`);
targetSocket.destroy();
});
});
// ── Start listening ───────────────────────────────────────────────────────
server.listen(opts.port, () => {
const proxyUrl = `http://localhost:${opts.port}`;
console.log('');
@@ -176,6 +263,9 @@ export function startProxy(opts: ProxyOptions): { close: () => void } {
console.log(' \x1b[2mFiber hook injection ........ active\x1b[0m');
console.log(' \x1b[2mX-Frame-Options stripping ... active\x1b[0m');
console.log(' \x1b[2mWebSocket passthrough ....... active\x1b[0m');
if (isHttps) {
console.log(' \x1b[2mHTTPS → HTTP bridge ......... active\x1b[0m');
}
console.log('');
});
+107 -67
View File
@@ -133,10 +133,10 @@ export function buildProxyFiberHookScript(): string {
var HOST_SOURCE = ${JSON.stringify(HOST_SOURCE)};
// ── Runtime state ─────────────────────────────────────────────────────────
var currentTree = null; // latest serialized FiberNode tree
var nodeMap = {}; // nodeId → { domRect } (flat for O(1) lookup)
var selectedNodeId = null; // currently highlighted component
var highlightEl = null; // the blue-ring DOM overlay element
var nodeMap = {}; // nodeId → { domRect, fiber }
var fiberMap = new WeakMap(); // fiber → nodeId (reverse lookup for click)
var selectedNodeId = null; // currently highlighted component
var highlightEl = null; // the blue-ring DOM overlay element
// ── postMessage helper ────────────────────────────────────────────────────
function post(msg) {
@@ -165,11 +165,15 @@ export function buildProxyFiberHookScript(): string {
catch (e) { /* don't break existing DevTools */ }
}
try {
// Reset maps before each walk so stale entries from the previous tree
// don't accumulate. fiberMap is a WeakMap so it self-cleans, but we
// rebuild nodeMap from scratch on every commit.
nodeMap = {};
fiberMap = new WeakMap();
var tree = serializeFiber(root.current, '');
currentTree = tree;
rebuildNodeMap(tree);
post({ type: 'FIBER_TREE_UPDATE', root: tree });
// Re-sync the highlight ring after each React commit (position may shift).
// Re-sync the highlight ring after each React commit (layout may shift).
if (selectedNodeId) updateHighlight();
} catch (err) {
post({ type: 'ERROR', message: String(err) });
@@ -178,22 +182,24 @@ export function buildProxyFiberHookScript(): string {
// ── Fiber serialization (stable path-based IDs) ───────────────────────────
// ID format: "ComponentName:siblingIndex/ChildName:siblingIndex/..."
// Stable across re-renders provided the tree structure doesn't change.
//
// IMPORTANT: unnamed fibers (Fragment, Context.Provider, React.memo wrappers)
// are transparent — their children are collected directly into their parent's
// children array. Returning only the first named child (the old approach)
// caused entire subtrees to vanish from the tree.
function serializeFiber(fiber, parentId) {
if (!fiber) return null;
var name = getDisplayName(fiber);
if (!name) {
// Unnamed fiber (Fragment, Context, Provider) — skip this level,
// but keep walking children so named descendants are not lost.
var c = fiber.child;
while (c) {
var s = serializeFiber(c, parentId);
if (s) return s;
c = c.sibling;
}
return null;
// Unnamed root fiber (HostRoot) — skip to first named child.
// collectChildren handles the Fragment case recursively.
var children = [];
collectChildren(fiber, parentId, children);
if (children.length === 1) return children[0];
if (children.length === 0) return null;
// Multiple named children at root — wrap in a synthetic root node.
return { id: '__root__', name: '__root__', props: {}, children: children };
}
var nodeId = (parentId ? parentId + '/' : '') + name + ':' + String(fiber.index);
@@ -201,13 +207,31 @@ export function buildProxyFiberHookScript(): string {
var node = { id: nodeId, name: name, props: serializeProps(fiber.memoizedProps), children: [] };
if (rect) node.domRect = rect;
// Register in both maps for O(1) lookup.
nodeMap[nodeId] = { domRect: rect || null, fiber: fiber };
fiberMap.set(fiber, nodeId);
collectChildren(fiber, nodeId, node.children);
return node;
}
// Collect all NAMED descendants of fiber.child into out[], transparently
// flattening unnamed intermediates (Fragments, Providers, wrappers).
function collectChildren(fiber, parentId, out) {
var child = fiber.child;
while (child) {
var serialized = serializeFiber(child, nodeId);
if (serialized) node.children.push(serialized);
var name = getDisplayName(child);
if (name) {
var serialized = serializeFiber(child, parentId);
if (serialized) out.push(serialized);
} else {
// Unnamed (Fragment / Context / Provider / forwardRef wrapper etc.):
// flatten its children directly into our level — they share parentId.
fiberMap.set(child, null); // mark as non-selectable
collectChildren(child, parentId, out);
}
child = child.sibling;
}
return node;
}
function getDisplayName(fiber) {
@@ -242,23 +266,20 @@ export function buildProxyFiberHookScript(): string {
return out;
}
// ── Node map: flat O(1) lookup by stable ID ───────────────────────────────
function rebuildNodeMap(node) {
nodeMap = {};
fillMap(node);
}
function fillMap(node) {
if (!node) return;
nodeMap[node.id] = { domRect: node.domRect || null };
var children = node.children;
for (var i = 0; i < children.length; i++) fillMap(children[i]);
}
// ── Highlight overlay (blue ring inside the iframe) ───────────────────────
// updateHighlight re-measures from the live fiber stateNode so the ring
// stays accurate even after scroll (between React commits).
function updateHighlight() {
var info = nodeMap[selectedNodeId];
if (info && info.domRect) renderHighlight(info.domRect);
else removeHighlight();
var info = selectedNodeId ? nodeMap[selectedNodeId] : null;
if (!info) { removeHighlight(); return; }
// Re-measure from the live DOM element for scroll accuracy.
var rect = info.fiber ? getDomRect(info.fiber) : info.domRect;
if (rect && rect.width > 0 && rect.height > 0) {
renderHighlight(rect);
} else {
removeHighlight();
}
}
function renderHighlight(rect) {
@@ -266,8 +287,6 @@ export function buildProxyFiberHookScript(): string {
highlightEl = document.createElement('div');
highlightEl.id = '__om_sel__';
highlightEl.setAttribute('aria-hidden', 'true');
// CSS kept inline so no stylesheet dependency. Transition animates when
// the selected component moves (e.g. during a re-render or scroll).
highlightEl.style.cssText = [
'position:fixed', 'pointer-events:none', 'z-index:2147483647',
'box-shadow:0 0 0 2px #3385FF',
@@ -290,42 +309,63 @@ export function buildProxyFiberHookScript(): string {
}
}
// ── Click-to-select (capturing phase) ────────────────────────────────────
// Finds the deepest named fiber node at the click point and reports it back.
// In normal canvas usage the SelectionOverlay sits on top of the iframe and
// this listener fires when the overlay is bypassed (e.g. direct preview mode).
document.addEventListener('click', function(event) {
var node = findDeepestAt(currentTree, event.clientX, event.clientY);
if (node && node.domRect) {
post({ type: 'COMPONENT_SELECTED', nodeId: node.id, rect: node.domRect });
}
// Re-measure on scroll so the ring follows the element without needing
// a React commit (which only fires on state/prop changes).
window.addEventListener('scroll', function() {
if (selectedNodeId) updateHighlight();
}, true);
function findDeepestAt(node, x, y) {
if (!node) return null;
var r = node.domRect;
var hit = r && x >= r.x && x <= r.x + r.width && y >= r.y && y <= r.y + r.height;
// ── Click-to-select (capturing phase) ────────────────────────────────────
// Uses document.elementFromPoint to get the live DOM element at the click
// position (accurate even after scroll), then walks the React fiber tree
// upward via __reactFiber$ keys to find the nearest tracked component.
// Falls back to a tree-based hit test if the fiber key is not present
// (e.g. in some SSR-hydrated contexts).
if (hit) {
// Matched — recurse children for a deeper (more specific) match.
var children = node.children;
for (var i = 0; i < children.length; i++) {
var deeper = findDeepestAt(children[i], x, y);
if (deeper) return deeper;
}
return node;
}
// No hit on this node — still check children for overflow:visible cases.
var children = node.children;
for (var i = 0; i < children.length; i++) {
var found = findDeepestAt(children[i], x, y);
if (found) return found;
function getFiberKey(el) {
var keys = Object.keys(el);
for (var i = 0; i < keys.length; i++) {
if (keys[i].indexOf('__reactFiber$') === 0) return keys[i];
}
return null;
}
document.addEventListener('click', function(event) {
// Ignore clicks on our own highlight overlay.
if (event.target === highlightEl) return;
var el = document.elementFromPoint(event.clientX, event.clientY);
// Walk the DOM upward, trying to find a tracked React fiber at each level.
var current = el;
while (current && current !== document.documentElement) {
var fiberKey = current ? getFiberKey(current) : null;
if (fiberKey) {
// Walk the fiber's return (parent) chain to find the nearest node we track.
var fiber = current[fiberKey];
while (fiber) {
var nodeId = fiberMap.get(fiber);
if (nodeId) {
// Re-measure from the live element for accurate post-scroll rect.
var liveEl = fiber.stateNode;
var rect = (liveEl && typeof liveEl.getBoundingClientRect === 'function')
? (function(r) { return { x: r.x, y: r.y, width: r.width, height: r.height }; })(liveEl.getBoundingClientRect())
: null;
if (rect) {
post({ type: 'COMPONENT_SELECTED', nodeId: nodeId, rect: rect });
return;
}
}
fiber = fiber.return;
}
}
current = current.parentElement;
}
// Nothing found — clear the selection.
post({ type: 'COMPONENT_DESELECTED' });
}, true);
// ── Host → Renderer message handler ──────────────────────────────────────
window.addEventListener('message', function(event) {
var data = event.data;
+271
View File
@@ -159,6 +159,9 @@ importers:
'@types/node':
specifier: ^22.0.0
version: 22.19.17
esbuild:
specifier: ^0.28.0
version: 0.28.0
typescript:
specifier: ^5.5.0
version: 5.9.3
@@ -395,138 +398,294 @@ packages:
cpu: [ppc64]
os: [aix]
'@esbuild/aix-ppc64@0.28.0':
resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.21.5':
resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==}
engines: {node: '>=12'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm64@0.28.0':
resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.21.5':
resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==}
engines: {node: '>=12'}
cpu: [arm]
os: [android]
'@esbuild/android-arm@0.28.0':
resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.21.5':
resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==}
engines: {node: '>=12'}
cpu: [x64]
os: [android]
'@esbuild/android-x64@0.28.0':
resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.21.5':
resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==}
engines: {node: '>=12'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-arm64@0.28.0':
resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.21.5':
resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==}
engines: {node: '>=12'}
cpu: [x64]
os: [darwin]
'@esbuild/darwin-x64@0.28.0':
resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.21.5':
resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==}
engines: {node: '>=12'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-arm64@0.28.0':
resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.21.5':
resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [freebsd]
'@esbuild/freebsd-x64@0.28.0':
resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.21.5':
resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==}
engines: {node: '>=12'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm64@0.28.0':
resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.21.5':
resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==}
engines: {node: '>=12'}
cpu: [arm]
os: [linux]
'@esbuild/linux-arm@0.28.0':
resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.21.5':
resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==}
engines: {node: '>=12'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-ia32@0.28.0':
resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.21.5':
resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==}
engines: {node: '>=12'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-loong64@0.28.0':
resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.21.5':
resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==}
engines: {node: '>=12'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-mips64el@0.28.0':
resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.21.5':
resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==}
engines: {node: '>=12'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-ppc64@0.28.0':
resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.21.5':
resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==}
engines: {node: '>=12'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-riscv64@0.28.0':
resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.21.5':
resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==}
engines: {node: '>=12'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-s390x@0.28.0':
resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.21.5':
resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [linux]
'@esbuild/linux-x64@0.28.0':
resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.28.0':
resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.21.5':
resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==}
engines: {node: '>=12'}
cpu: [x64]
os: [netbsd]
'@esbuild/netbsd-x64@0.28.0':
resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.28.0':
resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.21.5':
resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==}
engines: {node: '>=12'}
cpu: [x64]
os: [openbsd]
'@esbuild/openbsd-x64@0.28.0':
resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openharmony-arm64@0.28.0':
resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.21.5':
resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==}
engines: {node: '>=12'}
cpu: [x64]
os: [sunos]
'@esbuild/sunos-x64@0.28.0':
resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.21.5':
resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==}
engines: {node: '>=12'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-arm64@0.28.0':
resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.21.5':
resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==}
engines: {node: '>=12'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-ia32@0.28.0':
resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.21.5':
resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==}
engines: {node: '>=12'}
cpu: [x64]
os: [win32]
'@esbuild/win32-x64@0.28.0':
resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@eslint-community/eslint-utils@4.9.1':
resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -1904,6 +2063,11 @@ packages:
engines: {node: '>=12'}
hasBin: true
esbuild@0.28.0:
resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==}
engines: {node: '>=18'}
hasBin: true
escape-string-regexp@4.0.0:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
@@ -2719,72 +2883,150 @@ snapshots:
'@esbuild/aix-ppc64@0.21.5':
optional: true
'@esbuild/aix-ppc64@0.28.0':
optional: true
'@esbuild/android-arm64@0.21.5':
optional: true
'@esbuild/android-arm64@0.28.0':
optional: true
'@esbuild/android-arm@0.21.5':
optional: true
'@esbuild/android-arm@0.28.0':
optional: true
'@esbuild/android-x64@0.21.5':
optional: true
'@esbuild/android-x64@0.28.0':
optional: true
'@esbuild/darwin-arm64@0.21.5':
optional: true
'@esbuild/darwin-arm64@0.28.0':
optional: true
'@esbuild/darwin-x64@0.21.5':
optional: true
'@esbuild/darwin-x64@0.28.0':
optional: true
'@esbuild/freebsd-arm64@0.21.5':
optional: true
'@esbuild/freebsd-arm64@0.28.0':
optional: true
'@esbuild/freebsd-x64@0.21.5':
optional: true
'@esbuild/freebsd-x64@0.28.0':
optional: true
'@esbuild/linux-arm64@0.21.5':
optional: true
'@esbuild/linux-arm64@0.28.0':
optional: true
'@esbuild/linux-arm@0.21.5':
optional: true
'@esbuild/linux-arm@0.28.0':
optional: true
'@esbuild/linux-ia32@0.21.5':
optional: true
'@esbuild/linux-ia32@0.28.0':
optional: true
'@esbuild/linux-loong64@0.21.5':
optional: true
'@esbuild/linux-loong64@0.28.0':
optional: true
'@esbuild/linux-mips64el@0.21.5':
optional: true
'@esbuild/linux-mips64el@0.28.0':
optional: true
'@esbuild/linux-ppc64@0.21.5':
optional: true
'@esbuild/linux-ppc64@0.28.0':
optional: true
'@esbuild/linux-riscv64@0.21.5':
optional: true
'@esbuild/linux-riscv64@0.28.0':
optional: true
'@esbuild/linux-s390x@0.21.5':
optional: true
'@esbuild/linux-s390x@0.28.0':
optional: true
'@esbuild/linux-x64@0.21.5':
optional: true
'@esbuild/linux-x64@0.28.0':
optional: true
'@esbuild/netbsd-arm64@0.28.0':
optional: true
'@esbuild/netbsd-x64@0.21.5':
optional: true
'@esbuild/netbsd-x64@0.28.0':
optional: true
'@esbuild/openbsd-arm64@0.28.0':
optional: true
'@esbuild/openbsd-x64@0.21.5':
optional: true
'@esbuild/openbsd-x64@0.28.0':
optional: true
'@esbuild/openharmony-arm64@0.28.0':
optional: true
'@esbuild/sunos-x64@0.21.5':
optional: true
'@esbuild/sunos-x64@0.28.0':
optional: true
'@esbuild/win32-arm64@0.21.5':
optional: true
'@esbuild/win32-arm64@0.28.0':
optional: true
'@esbuild/win32-ia32@0.21.5':
optional: true
'@esbuild/win32-ia32@0.28.0':
optional: true
'@esbuild/win32-x64@0.21.5':
optional: true
'@esbuild/win32-x64@0.28.0':
optional: true
'@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)':
dependencies:
eslint: 9.39.4
@@ -4730,6 +4972,35 @@ snapshots:
'@esbuild/win32-ia32': 0.21.5
'@esbuild/win32-x64': 0.21.5
esbuild@0.28.0:
optionalDependencies:
'@esbuild/aix-ppc64': 0.28.0
'@esbuild/android-arm': 0.28.0
'@esbuild/android-arm64': 0.28.0
'@esbuild/android-x64': 0.28.0
'@esbuild/darwin-arm64': 0.28.0
'@esbuild/darwin-x64': 0.28.0
'@esbuild/freebsd-arm64': 0.28.0
'@esbuild/freebsd-x64': 0.28.0
'@esbuild/linux-arm': 0.28.0
'@esbuild/linux-arm64': 0.28.0
'@esbuild/linux-ia32': 0.28.0
'@esbuild/linux-loong64': 0.28.0
'@esbuild/linux-mips64el': 0.28.0
'@esbuild/linux-ppc64': 0.28.0
'@esbuild/linux-riscv64': 0.28.0
'@esbuild/linux-s390x': 0.28.0
'@esbuild/linux-x64': 0.28.0
'@esbuild/netbsd-arm64': 0.28.0
'@esbuild/netbsd-x64': 0.28.0
'@esbuild/openbsd-arm64': 0.28.0
'@esbuild/openbsd-x64': 0.28.0
'@esbuild/openharmony-arm64': 0.28.0
'@esbuild/sunos-x64': 0.28.0
'@esbuild/win32-arm64': 0.28.0
'@esbuild/win32-ia32': 0.28.0
'@esbuild/win32-x64': 0.28.0
escape-string-regexp@4.0.0: {}
eslint-scope@8.4.0: