updated stuff
This commit is contained in:
@@ -0,0 +1,478 @@
|
|||||||
|
# Originmain SDK Architecture
|
||||||
|
### From Proxy to SDK — System Design & Implementation Progress
|
||||||
|
|
||||||
|
> **Living document.** Updated every time a piece of this architecture is implemented.
|
||||||
|
> Supersedes `RENDERING-ARCHITECTURE.md` (proxy-era, now deprecated).
|
||||||
|
>
|
||||||
|
> Last updated: 2026-05-13
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Why We Moved Away From the Proxy](#1-why-we-moved-away-from-the-proxy)
|
||||||
|
2. [Target Architecture](#2-target-architecture)
|
||||||
|
3. [System Components](#3-system-components)
|
||||||
|
4. [Wire Protocol](#4-wire-protocol)
|
||||||
|
5. [Implementation Progress](#5-implementation-progress)
|
||||||
|
6. [Package Map](#6-package-map)
|
||||||
|
7. [Data Flow — Step by Step](#7-data-flow--step-by-step)
|
||||||
|
8. [Local Dev Problem & Tunnel Strategy](#8-local-dev-problem--tunnel-strategy)
|
||||||
|
9. [Edge Cases](#9-edge-cases)
|
||||||
|
10. [What Remains To Be Built](#10-what-remains-to-be-built)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Why We Moved Away From the Proxy
|
||||||
|
|
||||||
|
The original approach (`@originmain/cli`) ran a local HTTP reverse proxy that injected a
|
||||||
|
`<script>` tag into every HTML response. This failed in practice for four compounding reasons:
|
||||||
|
|
||||||
|
| Failure | Root Cause | Fatal? |
|
||||||
|
|---|---|---|
|
||||||
|
| Script never executed | React 19 `hydrateRoot(document, ...)` reconciles the entire `<head>` and removes any `<script>` tag not present in React's VDOM | ✅ Yes |
|
||||||
|
| `window.name` always empty | Chrome 88+ strips `window.name` on cross-origin iframe loads (Spectre mitigation) — the SDK used this as its artboard ID source | ✅ Yes |
|
||||||
|
| Inline script blocked | CSPs without `'unsafe-inline'` blocked the injected `<script>…</script>` content | ✅ Yes |
|
||||||
|
| Hook installed too late | `__REACT_DEVTOOLS_GLOBAL_HOOK__` must exist **before** React's module body evaluates. Injected scripts run after HTML parses — after React is already loaded | ✅ Yes |
|
||||||
|
|
||||||
|
**The only reliable fix**: the fiber hook must be installed by the app itself, not by an external injector. That means it ships as an npm package the developer imports **before React** in their app entry point.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Target Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ORIGINMAIN CLOUD (originmain.com) │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────┐ ┌──────────────────────────────────┐ │
|
||||||
|
│ │ Canvas (Next.js app) │◄───►│ WebSocket Bridge │ │
|
||||||
|
│ │ • Artboards / iframes │ │ /api/sdk/[projectId] │ │
|
||||||
|
│ │ • Inspector / Editor │ │ • auth via SDK token │ │
|
||||||
|
│ │ • File diff viewer │ │ • routes messages to user │ │
|
||||||
|
│ └─────────────────────────────┘ └──────────────────────────────────┘ │
|
||||||
|
│ ▲ │
|
||||||
|
└────────────────────────────────────────────────────────│───────────────────┘
|
||||||
|
│ WSS
|
||||||
|
│ (wss://originmain.com/api/sdk/[projectId])
|
||||||
|
┌────────────────────────────────────────────────────────│───────────────────┐
|
||||||
|
│ USER MACHINE (localhost / staging) │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌──────────────────────────┐ ┌────────────────────────────────────┐ │
|
||||||
|
│ │ Next.js dev server │◄───►│ @originmain/dev (SDK) │ │
|
||||||
|
│ │ (next dev / Vercel) │ │ • client runtime in browser │ │
|
||||||
|
│ │ │ │ • server runtime in Node │ │
|
||||||
|
│ │ │ │ • direct React fiber access │ │
|
||||||
|
│ │ Components.tsx ◄────────┼─────┤ • file-write capability │ │
|
||||||
|
│ │ │ │ • opens WSS to cloud canvas │ │
|
||||||
|
│ └──────────────────────────┘ └────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ next.config.js — wrapped with @originmain/next plugin │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key invariant**: the fiber hook lives *inside* the user's app bundle.
|
||||||
|
No proxy, no injection, no external script — the user's `import '@originmain/live'`
|
||||||
|
is what instruments React.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. System Components
|
||||||
|
|
||||||
|
### 3.1 `@originmain/live` (client runtime) — `packages/live-sdk`
|
||||||
|
|
||||||
|
The browser-side SDK. A side-effect import that:
|
||||||
|
|
||||||
|
- Installs `__REACT_DEVTOOLS_GLOBAL_HOOK__` **before React evaluates** (module-load time)
|
||||||
|
- Resolves the artboard ID from three sources in priority order:
|
||||||
|
1. URL fragment: `location.hash` contains `#__om_artboard=<id>` ← primary, cross-origin safe
|
||||||
|
2. `window.name`: starts with `om:` (same-origin iframes only)
|
||||||
|
3. postMessage handshake: sends `{ __om_init_request: true }` to parent, awaits reply
|
||||||
|
- Runs the full postMessage protocol (see §4) once the artboard ID is known
|
||||||
|
- Is a **complete no-op** outside an Originmain artboard iframe (zero runtime cost in production)
|
||||||
|
|
||||||
|
### 3.2 `@originmain/next` (build plugin) — `packages/next`
|
||||||
|
|
||||||
|
A `withOriginmain(nextConfig)` wrapper for `next.config.ts` that:
|
||||||
|
|
||||||
|
- Prepends `@originmain/live` to every client-side webpack entry point
|
||||||
|
- Ensures the fiber hook import runs **before any other module** in the bundle
|
||||||
|
- Skips the server-side bundle (fiber hook is browser-only)
|
||||||
|
- Is idempotent (safe to wrap twice)
|
||||||
|
- Delegates to any existing `webpack` customisation in the user's config
|
||||||
|
|
||||||
|
### 3.3 `@originmain/dev` (full SDK) — `packages/dev` ❌ NOT BUILT YET
|
||||||
|
|
||||||
|
The full SDK intended for local development. Will combine:
|
||||||
|
|
||||||
|
- Everything in `@originmain/live` (client runtime, fiber hook)
|
||||||
|
- A **server runtime** that runs inside the Next.js dev server process
|
||||||
|
- A **WebSocket client** that connects outbound to the cloud canvas bridge
|
||||||
|
- **File-write capability**: receives design panel edits from the canvas and applies them to source files (`.tsx`, `.ts`, CSS modules)
|
||||||
|
|
||||||
|
### 3.4 WebSocket Bridge — `packages/app/src/app/api/sdk/[projectId]/` ❌ NOT BUILT YET
|
||||||
|
|
||||||
|
A Next.js API route on the cloud canvas that:
|
||||||
|
|
||||||
|
- Accepts an inbound WSS connection from `@originmain/dev`
|
||||||
|
- Authenticates via a project-scoped SDK token (issued in project settings)
|
||||||
|
- Routes messages bidirectionally:
|
||||||
|
- SDK → Canvas: `FIBER_TREE_UPDATE`, `ELEMENT_STYLES`, `ROUTES_DISCOVERED`, etc.
|
||||||
|
- Canvas → SDK: `PATCH_ELEMENT_STYLE`, `REQUEST_ELEMENT_STYLES`, `CAPTURE_SNAPSHOT`, etc.
|
||||||
|
- Maintains one WebSocket connection per active project session
|
||||||
|
|
||||||
|
### 3.5 Canvas (Next.js app) — `packages/app`
|
||||||
|
|
||||||
|
Originmain's cloud editor. Current transport: **postMessage via iframe**.
|
||||||
|
Future transport: **WebSocket via bridge** (for `@originmain/dev` local dev).
|
||||||
|
|
||||||
|
Key components:
|
||||||
|
- `LiveArtboard.tsx` — manages the `<iframe>`, postMessage listener, message dispatch
|
||||||
|
- `DesignTab.tsx` — design property sections (Frame, Layout, Fill, Typography, etc.)
|
||||||
|
- `Inspector.tsx` — tab container (Design / Props / Code / Diff / Graph)
|
||||||
|
- `Artboard.tsx` — artboard frame, selection overlay, static-page detection
|
||||||
|
- `Canvas.tsx` — infinite canvas, zoom/pan, artboard layout, onboarding overlay
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Wire Protocol
|
||||||
|
|
||||||
|
Defined in `packages/renderer/src/protocol.ts`. Both the postMessage transport
|
||||||
|
(current) and the WebSocket transport (planned) use the same message shapes.
|
||||||
|
|
||||||
|
### 4.1 Canvas → App (Host → Renderer)
|
||||||
|
|
||||||
|
| Message | Payload | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `SET_DESIGN_TOKENS` | `{ tokens: Record<string, string> }` | Push CSS custom property values |
|
||||||
|
| `NAVIGATE` | `{ path: string }` | SPA navigation inside the iframe |
|
||||||
|
| `SELECT_COMPONENT` | `{ nodeId: string }` | Show highlight ring on component |
|
||||||
|
| `DESELECT` | — | Remove highlight ring |
|
||||||
|
| `REQUEST_ELEMENT_STYLES` | `{ nodeId: string }` | Ask for computed CSS properties |
|
||||||
|
| `PATCH_ELEMENT_STYLE` | `{ nodeId, property, value }` | Apply one inline CSS override |
|
||||||
|
| `PATCH_CHILDREN_STYLE` | `{ parentNodeId, selector, property, value }` | Patch CSS on all matching children |
|
||||||
|
| `REMOVE_ELEMENT` | `{ nodeId: string }` | Set `display:none` on element |
|
||||||
|
| `CAPTURE_THUMBNAIL` | — | Capture full-page JPEG via html2canvas |
|
||||||
|
| `CAPTURE_SNAPSHOT` | `{ nodeId: string }` | Capture selected element PNG |
|
||||||
|
| `CANCEL_SNAPSHOT` | — | Abort in-flight snapshot |
|
||||||
|
|
||||||
|
### 4.2 App → Canvas (Renderer → Host)
|
||||||
|
|
||||||
|
| Message | Payload | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `READY` | `{ rootFontSizePx?: number }` | Hook active, React detected |
|
||||||
|
| `FIBER_TREE_UPDATE` | `{ root: FiberNode }` | Full serialized component tree |
|
||||||
|
| `COMPONENT_SELECTED` | `{ nodeId, rect }` | User clicked a component |
|
||||||
|
| `COMPONENT_DESELECTED` | — | User clicked empty space |
|
||||||
|
| `ELEMENT_STYLES` | `{ nodeId, styles, hasDirectText, hasParagraphChildren }` | Computed CSS response |
|
||||||
|
| `ROUTES_DISCOVERED` | `{ routes: Array<{ path, label }> }` | App's navigation routes |
|
||||||
|
| `THUMBNAIL_READY` | `{ dataUrl: string \| null }` | JPEG data URL or null |
|
||||||
|
| `SNAPSHOT_READY` | `{ dataUrl: string \| null, nodeId: string }` | PNG data URL or null |
|
||||||
|
| `ERROR` | `{ message: string }` | Hook or serialization error |
|
||||||
|
|
||||||
|
### 4.3 Envelope Format (postMessage)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Canvas → App
|
||||||
|
{ source: 'originmain-host', artboardId: string, message: HostMessage }
|
||||||
|
|
||||||
|
// App → Canvas
|
||||||
|
{ source: 'originmain-renderer', artboardId: string, message: RendererMessage }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 Artboard ID Resolution (how the SDK finds its artboard)
|
||||||
|
|
||||||
|
The canvas appends the artboard ID to the iframe's `src` as a URL fragment:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// LiveArtboard.tsx
|
||||||
|
src={url + '#__om_artboard=' + encodeURIComponent(id)}
|
||||||
|
```
|
||||||
|
|
||||||
|
The SDK reads it in priority order:
|
||||||
|
```
|
||||||
|
1. location.hash → /__om_artboard=abc123/ (primary, cross-origin safe)
|
||||||
|
2. window.name → "om:abc123" (same-origin iframes)
|
||||||
|
3. postMessage handshake (async fallback, 10s timeout)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Implementation Progress
|
||||||
|
|
||||||
|
### ✅ Done
|
||||||
|
|
||||||
|
| Component | File(s) | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `@originmain/live` client runtime | `packages/live-sdk/src/hook.ts` | Full rewrite. URL fragment guard. Full protocol parity. html2canvas support. |
|
||||||
|
| `@originmain/live` build step | `packages/live-sdk/build.mjs` | esbuild → `dist/index.js` (9.4kb browser ESM bundle, minified). |
|
||||||
|
| `@originmain/next` build plugin | `packages/next/src/index.ts` | `withOriginmain()` — webpack entry prepend. Idempotent. |
|
||||||
|
| `@originmain/next` build step | `packages/next/build.mjs` | esbuild → ESM + CJS; tsc → `dist/index.d.ts` type declarations. |
|
||||||
|
| Root `sdk:build` script | `package.json` | `pnpm sdk:build` builds both SDK packages in order. |
|
||||||
|
| LiveArtboard postMessage handshake | `packages/app/src/components/canvas/LiveArtboard.tsx` | Responds to `__om_init_request` from SDK |
|
||||||
|
| LiveArtboard URL fragment injection | `packages/app/src/components/canvas/LiveArtboard.tsx` | Appends `#__om_artboard=<id>` to all iframe src URLs |
|
||||||
|
| Style refresh after design panel edit | `packages/app/src/components/canvas/LiveArtboard.tsx` | 120ms debounced `REQUEST_ELEMENT_STYLES` after queue drains |
|
||||||
|
| Design panel → history tracking | `packages/app/src/components/inspector/DesignTab.tsx` | `pushEdit()` on every `patch()` call — feeds Diff tab |
|
||||||
|
| Design panel → optimistic styles | `packages/app/src/components/inspector/DesignTab.tsx` | `setComponentStyles()` optimistic update — inputs reflect change immediately |
|
||||||
|
| Canvas onboarding UI | `packages/app/src/components/canvas/Canvas.tsx` | Updated from proxy instructions to SDK install instructions |
|
||||||
|
| Static page banner | `packages/app/src/components/canvas/Artboard.tsx` | Now says "add `import '@originmain/live'`" instead of confusing message |
|
||||||
|
| `tsconfig.base.json` path alias | `tsconfig.base.json` | `@originmain/next` added to paths |
|
||||||
|
|
||||||
|
### 🚧 In Progress / Partially Done
|
||||||
|
|
||||||
|
| Component | File(s) | Status | Blocker |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `@originmain/live` publishing | `packages/live-sdk/package.json` | Build complete. `"private": false`. **Not yet `npm publish`-ed** | Run `pnpm sdk:build && cd packages/live-sdk && npm publish` |
|
||||||
|
| `@originmain/next` publishing | `packages/next/package.json` | Build complete. `"private": false`. **Not yet `npm publish`-ed** | Same (depends on live being published first) |
|
||||||
|
| CLI proxy deprecation | `packages/cli/` | Still exists and still works | Can delete once SDK is published and users migrate |
|
||||||
|
|
||||||
|
### ❌ Not Built
|
||||||
|
|
||||||
|
| Component | Target File(s) | Priority | Depends On |
|
||||||
|
|---|---|---|---|
|
||||||
|
| WebSocket bridge API route | `packages/app/src/app/api/sdk/[projectId]/route.ts` | HIGH | — |
|
||||||
|
| `@originmain/dev` package | `packages/dev/` | HIGH | WebSocket bridge |
|
||||||
|
| SDK server runtime (Node.js) | `packages/dev/src/server.ts` | HIGH | WebSocket bridge |
|
||||||
|
| File-write capability | `packages/dev/src/file-writer.ts` | HIGH | Server runtime |
|
||||||
|
| SDK token issuance in canvas | `packages/app/src/app/api/sdk/token/route.ts` | HIGH | WebSocket bridge |
|
||||||
|
| Local dev tunnel (localhost → WSS) | TBD | MEDIUM | `@originmain/dev` |
|
||||||
|
| `IsolationFrame.tsx` SDK migration | `packages/app/src/components/canvas/IsolationFrame.tsx` | LOW | `@originmain/dev` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Package Map
|
||||||
|
|
||||||
|
```
|
||||||
|
packages/
|
||||||
|
├── app/ @originmain/app — Cloud canvas (Next.js, Vercel)
|
||||||
|
├── live-sdk/ @originmain/live — Client SDK: browser fiber hook ✅ DONE
|
||||||
|
├── next/ @originmain/next — Next.js build plugin ✅ DONE
|
||||||
|
├── dev/ @originmain/dev — Full SDK (client + server) ❌ NOT BUILT
|
||||||
|
├── renderer/ @originmain/renderer — Protocol types, message shapes
|
||||||
|
├── cli/ @originmain/cli — Legacy proxy (deprecated, not deleted yet)
|
||||||
|
├── diff-engine/ @originmain/diff-engine
|
||||||
|
├── design-language/ @originmain/design-language
|
||||||
|
├── agent-bridge/ @originmain/agent-bridge
|
||||||
|
├── ai-layer/ @originmain/ai-layer
|
||||||
|
├── platform/ @originmain/platform
|
||||||
|
├── multiplayer/ @originmain/multiplayer
|
||||||
|
├── origin-graph/ @originmain/origin-graph
|
||||||
|
├── ui/ @originmain/ui
|
||||||
|
└── e2e/ (test suite)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Data Flow — Step by Step
|
||||||
|
|
||||||
|
### 7.1 Current Flow (postMessage via iframe)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. User adds @originmain/live to their Next.js app
|
||||||
|
2. User wraps next.config.ts with withOriginmain()
|
||||||
|
3. User deploys to Vercel — @originmain/live is in the bundle
|
||||||
|
|
||||||
|
4. Canvas: user pastes Vercel URL → artboard created in DB
|
||||||
|
5. Canvas: LiveArtboard renders <iframe src="https://app.vercel.app/route#__om_artboard=abc123">
|
||||||
|
6. Browser: iframe loads app from Vercel
|
||||||
|
|
||||||
|
7. App bundle: @originmain/live runs (module-level side effect, before React)
|
||||||
|
→ installs __REACT_DEVTOOLS_GLOBAL_HOOK__
|
||||||
|
→ reads location.hash → finds __om_artboard=abc123
|
||||||
|
→ calls startMainLoop("abc123")
|
||||||
|
|
||||||
|
8. React evaluates → sees hook → registers onCommitFiberRoot
|
||||||
|
|
||||||
|
9. App renders → React commits
|
||||||
|
→ onCommitFiberRoot fires
|
||||||
|
→ SDK serializes fiber tree
|
||||||
|
→ postMessage({ source: 'originmain-renderer', artboardId: 'abc123',
|
||||||
|
message: { type: 'FIBER_TREE_UPDATE', root: ... } })
|
||||||
|
|
||||||
|
10. Canvas: LiveArtboard.handleMessage receives FIBER_TREE_UPDATE
|
||||||
|
→ setFiberRoot(artboardId, root) [Zustand]
|
||||||
|
→ SelectionOverlay renders component hit-test overlay
|
||||||
|
→ Inspector shows component tree (Graph tab)
|
||||||
|
|
||||||
|
11. User clicks a component in the iframe
|
||||||
|
→ SDK: click handler → getFiberKey(el) → walk fiber.return chain
|
||||||
|
→ postMessage COMPONENT_SELECTED { nodeId, rect }
|
||||||
|
|
||||||
|
12. Canvas: receives COMPONENT_SELECTED
|
||||||
|
→ selectComponent(nodeId) [Zustand]
|
||||||
|
→ LiveArtboard sends REQUEST_ELEMENT_STYLES { nodeId }
|
||||||
|
|
||||||
|
13. SDK: respondWithStyles() → getComputedStyle(el) → all CSS properties
|
||||||
|
→ postMessage ELEMENT_STYLES { nodeId, styles, hasDirectText, hasParagraphChildren }
|
||||||
|
|
||||||
|
14. Canvas: setComponentStyles(styles) [Zustand]
|
||||||
|
→ DesignTab re-renders with actual computed values
|
||||||
|
→ Frame/Layout/Fill/Typography sections show live data
|
||||||
|
|
||||||
|
15. User edits width in Frame section: 200px → 250px
|
||||||
|
[DesignTab.patch('width', '250px')]
|
||||||
|
→ patchStyleEdit(artboardId, nodeId, 'width', '250px') → styleEditQueue
|
||||||
|
→ setComponentStyles({ ...styles, width: '250px' }) → optimistic panel update
|
||||||
|
→ pushEdit({ key: 'width', before: '200px', after: '250px', ... }) → history
|
||||||
|
|
||||||
|
16. LiveArtboard: styleEditQueue effect drains
|
||||||
|
→ postMessage PATCH_ELEMENT_STYLE { nodeId, property: 'width', value: '250px' }
|
||||||
|
→ starts 120ms debounce timer
|
||||||
|
|
||||||
|
17. SDK: patchElementStyle(nodeId, 'width', '250px')
|
||||||
|
→ el.style.setProperty('width', '250px')
|
||||||
|
→ iframe visually updates ✅
|
||||||
|
|
||||||
|
18. 120ms later: LiveArtboard sends REQUEST_ELEMENT_STYLES
|
||||||
|
→ SDK: getComputedStyle now returns '250px' (inline style overrides)
|
||||||
|
→ ELEMENT_STYLES response → panel shows accurate computed value
|
||||||
|
|
||||||
|
19. Diff tab: pendingChanges = [{ key: 'width', before: '200px', after: '250px' }]
|
||||||
|
→ "Export diff →" button active
|
||||||
|
→ Code tab shows generated file patch
|
||||||
|
→ "Send to Agent" sends diff to AI for code application
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 Target Flow (WebSocket — not built yet)
|
||||||
|
|
||||||
|
Same as above, except steps 5–9 use WebSocket instead of postMessage:
|
||||||
|
|
||||||
|
```
|
||||||
|
5. Canvas: LiveArtboard connects to wss://originmain.com/api/sdk/abc123
|
||||||
|
6. @originmain/dev (server runtime): connects to same WSS URL with SDK token
|
||||||
|
7. Bridge authenticates token, pairs the two connections
|
||||||
|
8. All messages flow through the bridge instead of postMessage
|
||||||
|
→ This unlocks local dev (no Vercel deploy required)
|
||||||
|
→ This enables file-write (SDK has Node.js fs access)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Local Dev Problem & Tunnel Strategy
|
||||||
|
|
||||||
|
### The Problem
|
||||||
|
|
||||||
|
The cloud canvas (`https://originmain.com`) is served over HTTPS. Browsers
|
||||||
|
enforce **mixed content policy**: an HTTPS page cannot iframe `http://localhost:3000`.
|
||||||
|
This means the current SDK approach (iframe + postMessage) **only works for deployed apps**
|
||||||
|
(Vercel, Netlify, etc.) — not for local development.
|
||||||
|
|
||||||
|
### Current Workaround (v1)
|
||||||
|
|
||||||
|
Users must deploy to Vercel (or any HTTPS host) to use the canvas.
|
||||||
|
Vercel's preview deployments are free and instant (`vercel deploy --prod` is optional).
|
||||||
|
|
||||||
|
### Planned Solution (`@originmain/dev` + WebSocket bridge)
|
||||||
|
|
||||||
|
When the WebSocket bridge exists:
|
||||||
|
- `@originmain/dev` runs a Node.js server alongside `next dev`
|
||||||
|
- It connects **outbound** from the user's machine via WSS to the canvas bridge
|
||||||
|
- Outbound WSS from localhost → cloud is always allowed (no mixed content issue)
|
||||||
|
- The canvas receives fiber data via the bridge, not via the iframe
|
||||||
|
- The iframe can be replaced with a screenshot stream or kept pointing to a tunnel URL
|
||||||
|
|
||||||
|
### Tunnel Options (if iframe is still needed for visual rendering)
|
||||||
|
|
||||||
|
| Option | Effort | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| Vercel deploy (v1 recommendation) | Lowest | Works today. No extra tooling. |
|
||||||
|
| `cloudflared tunnel` (user-run) | Low | `npx cloudflared tunnel --url http://localhost:3000` |
|
||||||
|
| Originmain-managed tunnel | High | Requires Originmain to run tunnel infrastructure |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Edge Cases
|
||||||
|
|
||||||
|
### 9.1 React DevTools Coexistence
|
||||||
|
|
||||||
|
React DevTools extension installs its own `__REACT_DEVTOOLS_GLOBAL_HOOK__` first.
|
||||||
|
The SDK detects the existing hook and wraps `onCommitFiberRoot` — both DevTools and
|
||||||
|
Originmain receive fiber commits. Neither overwrites the other.
|
||||||
|
|
||||||
|
### 9.2 HMR (Hot Module Replacement)
|
||||||
|
|
||||||
|
When webpack/turbopack pushes a hot update, React re-renders affected components.
|
||||||
|
`onCommitFiberRoot` fires again → SDK sends a new `FIBER_TREE_UPDATE` → canvas updates.
|
||||||
|
The artboard ID is stable across hot updates (URL fragment persists).
|
||||||
|
|
||||||
|
### 9.3 SPA Navigation
|
||||||
|
|
||||||
|
SPA routers change the URL without a full page reload. React re-renders.
|
||||||
|
`onCommitFiberRoot` fires → new `FIBER_TREE_UPDATE`. Route discovery re-runs via
|
||||||
|
a `popstate` listener. The SDK sends updated `ROUTES_DISCOVERED`.
|
||||||
|
|
||||||
|
### 9.4 React 19 Server Components
|
||||||
|
|
||||||
|
Server Components don't produce fiber nodes in the client tree — they're rendered
|
||||||
|
to RSC payload and hydrated as static DOM. The SDK correctly skips these
|
||||||
|
(they have no `type` function). Only client components appear in the tree.
|
||||||
|
|
||||||
|
### 9.5 Non-React Pages (Static HTML)
|
||||||
|
|
||||||
|
If the app serves a page with no React (e.g., a static landing page),
|
||||||
|
`onCommitFiberRoot` never fires. The `READY` message is still sent.
|
||||||
|
After 8 seconds with no `FIBER_TREE_UPDATE`, `LiveArtboard` calls
|
||||||
|
`onStaticPageDetected()` → the "No React detected" banner appears.
|
||||||
|
|
||||||
|
### 9.6 Multiple Artboards, Same App
|
||||||
|
|
||||||
|
Multiple artboards can iframe the same app at different routes. Each gets a
|
||||||
|
unique `#__om_artboard=<id>` in its URL. The SDK is a module singleton — it runs
|
||||||
|
once per page load — and binds to the single artboard ID from the URL fragment.
|
||||||
|
Each iframe is a separate browsing context with its own SDK instance.
|
||||||
|
|
||||||
|
### 9.7 CSP (`script-src 'self'`)
|
||||||
|
|
||||||
|
Since the SDK ships in the app's own bundle (not injected externally), CSP
|
||||||
|
`script-src 'self'` does not block it. The SDK is part of the same origin as the app.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. What Remains To Be Built
|
||||||
|
|
||||||
|
Priority order for next implementation sprint:
|
||||||
|
|
||||||
|
### Priority 1 — Publish packages (unblocks users) ✅ BUILD COMPLETE
|
||||||
|
|
||||||
|
- [x] Add esbuild build step for `@originmain/live` → `packages/live-sdk/build.mjs`
|
||||||
|
- [x] Add esbuild + tsc build step for `@originmain/next` → `packages/next/build.mjs`
|
||||||
|
- [x] Update exports in both `package.json` files to point to `dist/`
|
||||||
|
- [x] Add `@originmain/live` as workspace dependency of `@originmain/next`
|
||||||
|
- [x] Root `pnpm sdk:build` script for one-command build
|
||||||
|
- [ ] `npm publish` `@originmain/live` — ready to publish, command: `cd packages/live-sdk && npm publish`
|
||||||
|
- [ ] `npm publish` `@originmain/next` — depends on live being published first
|
||||||
|
|
||||||
|
### Priority 2 — WebSocket Bridge (unblocks local dev)
|
||||||
|
|
||||||
|
- [ ] `packages/app/src/app/api/sdk/[projectId]/route.ts`
|
||||||
|
- Accept WSS upgrade from `@originmain/dev`
|
||||||
|
- Authenticate via `Authorization: Bearer <sdk-token>` header
|
||||||
|
- Pair with the canvas session for the same projectId
|
||||||
|
- Bidirectional message routing
|
||||||
|
- Handle reconnect / heartbeat
|
||||||
|
|
||||||
|
### Priority 3 — `@originmain/dev` package (file-write, local dev)
|
||||||
|
|
||||||
|
- [ ] `packages/dev/src/client.ts` — re-export `@originmain/live`
|
||||||
|
- [ ] `packages/dev/src/server.ts` — Node.js WebSocket client, connects to bridge
|
||||||
|
- [ ] `packages/dev/src/file-writer.ts` — applies design panel patches to source files
|
||||||
|
- Parse `PATCH_ELEMENT_STYLE` messages → locate source file via `callSite`
|
||||||
|
- Rewrite Tailwind classes / CSS modules / inline styles
|
||||||
|
- [ ] `packages/next/src/index.ts` — extend `withOriginmain()` to also start the server runtime
|
||||||
|
|
||||||
|
### Priority 4 — SDK token issuance
|
||||||
|
|
||||||
|
- [ ] Project settings UI: "Generate SDK token" button
|
||||||
|
- [ ] `packages/app/src/app/api/sdk/token/route.ts` — create/rotate tokens
|
||||||
|
- [ ] Store tokens in Supabase (scoped to project, revocable)
|
||||||
|
|
||||||
|
### Priority 5 — IsolationFrame migration
|
||||||
|
|
||||||
|
- [ ] Migrate `IsolationFrame.tsx` from proxy URL to SDK-based approach
|
||||||
|
- Currently still requires the CLI proxy to serve `/__om_isolation__/*`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Document owner: engineering*
|
||||||
|
*Last updated: 2026-05-13*
|
||||||
|
*Next review: when WebSocket bridge is implemented*
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
"lint": "eslint . --max-warnings 0",
|
"lint": "eslint . --max-warnings 0",
|
||||||
"test": "vitest run --coverage",
|
"test": "vitest run --coverage",
|
||||||
"build": "pnpm --filter @originmain/app run build",
|
"build": "pnpm --filter @originmain/app run build",
|
||||||
|
"sdk:build": "pnpm --filter @originmain/live run build && pnpm --filter @originmain/next run build",
|
||||||
"dev": "pnpm --filter @originmain/app run dev",
|
"dev": "pnpm --filter @originmain/app run dev",
|
||||||
"cli:build": "pnpm --filter @originmain/cli run build",
|
"cli:build": "pnpm --filter @originmain/cli run build",
|
||||||
"cli:dev": "node packages/cli/dist/cli.js dev",
|
"cli:dev": "node packages/cli/dist/cli.js dev",
|
||||||
|
|||||||
@@ -492,7 +492,7 @@ export function Artboard({
|
|||||||
onSnapshotReady={(dataUrl, nodeId) => setElementSnapshot(id, nodeId, dataUrl)}
|
onSnapshotReady={(dataUrl, nodeId) => setElementSnapshot(id, nodeId, dataUrl)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Static-page banner — shown when the proxy serves a non-React page */}
|
{/* Static-page banner — shown when no React commits arrive after load */}
|
||||||
{isStaticPage && (
|
{isStaticPage && (
|
||||||
<div style={{
|
<div style={{
|
||||||
position: 'absolute', bottom: 0, left: 0, right: 0,
|
position: 'absolute', bottom: 0, left: 0, right: 0,
|
||||||
@@ -505,7 +505,9 @@ export function Artboard({
|
|||||||
fontFamily: "'JetBrains Mono', monospace", fontSize: '0.5875rem',
|
fontFamily: "'JetBrains Mono', monospace", fontSize: '0.5875rem',
|
||||||
color: '#1C1917', letterSpacing: '-0.01em',
|
color: '#1C1917', letterSpacing: '-0.01em',
|
||||||
}}>
|
}}>
|
||||||
Static HTML page — no React components detected. Navigate to a React route to enable inspection.
|
No React detected — add{' '}
|
||||||
|
<strong>import "@originmain/live"</strong>
|
||||||
|
{' '}before React in your app, then redeploy.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -472,9 +472,10 @@ export function Canvas() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── URL onboarding overlay ───────────────────────────────── */
|
/* ── URL onboarding overlay ───────────────────────────────── */
|
||||||
// Shown when the canvas has no artboards. Lets the user paste their CLI proxy
|
// Shown when the canvas has no artboards. Lets the user paste their app URL
|
||||||
// URL to auto-create the first artboard; route discovery will then fire and
|
// (Vercel, Netlify, or any deployment where @originmain/live is installed).
|
||||||
// populate the remaining pages automatically.
|
// Route discovery fires after the first React commit and populates remaining
|
||||||
|
// pages automatically.
|
||||||
function UrlOnboardingOverlay({
|
function UrlOnboardingOverlay({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
projectId,
|
projectId,
|
||||||
@@ -547,18 +548,30 @@ function UrlOnboardingOverlay({
|
|||||||
Connect your app
|
Connect your app
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontFamily: "'Inter', sans-serif", fontSize: '0.7rem', color: 'rgba(255,255,255,0.35)', lineHeight: 1.5 }}>
|
<div style={{ fontFamily: "'Inter', sans-serif", fontSize: '0.7rem', color: 'rgba(255,255,255,0.35)', lineHeight: 1.5 }}>
|
||||||
Paste the CLI proxy URL — all your app's pages will be auto-rendered as artboards
|
Paste your app URL — all your pages will be auto-rendered as artboards
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* CLI hint */}
|
{/* SDK install hint */}
|
||||||
<div style={{
|
<div style={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||||
width: '100%', padding: '6px 10px',
|
<div style={{
|
||||||
background: 'rgba(51,133,255,0.08)', border: '1px solid rgba(51,133,255,0.18)',
|
padding: '6px 10px',
|
||||||
borderRadius: 6, fontFamily: "'JetBrains Mono', monospace",
|
background: 'rgba(51,133,255,0.08)', border: '1px solid rgba(51,133,255,0.18)',
|
||||||
fontSize: '0.5625rem', color: 'rgba(51,133,255,0.75)', letterSpacing: '-0.01em',
|
borderRadius: 6, fontFamily: "'JetBrains Mono', monospace",
|
||||||
}}>
|
fontSize: '0.5625rem', color: 'rgba(51,133,255,0.75)', letterSpacing: '-0.01em',
|
||||||
npx @originmain/cli dev --target http://localhost:3000
|
}}>
|
||||||
|
npm install @originmain/live
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
padding: '6px 10px',
|
||||||
|
background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)',
|
||||||
|
borderRadius: 6, fontFamily: "'JetBrains Mono', monospace",
|
||||||
|
fontSize: '0.5625rem', color: 'rgba(255,255,255,0.35)', letterSpacing: '-0.01em',
|
||||||
|
}}>
|
||||||
|
{'// layout.tsx — must be before React'}
|
||||||
|
<br />
|
||||||
|
{'import "@originmain/live";'}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* URL input */}
|
{/* URL input */}
|
||||||
@@ -569,7 +582,7 @@ function UrlOnboardingOverlay({
|
|||||||
value={url}
|
value={url}
|
||||||
onChange={e => { setUrl(e.target.value); setErrorMsg(''); }}
|
onChange={e => { setUrl(e.target.value); setErrorMsg(''); }}
|
||||||
onKeyDown={e => { if (e.key === 'Enter') void handleConnect(); e.stopPropagation(); }}
|
onKeyDown={e => { if (e.key === 'Enter') void handleConnect(); e.stopPropagation(); }}
|
||||||
placeholder="http://localhost:4170"
|
placeholder="https://your-app.vercel.app"
|
||||||
style={{
|
style={{
|
||||||
flex: 1, background: 'rgba(255,255,255,0.05)',
|
flex: 1, background: 'rgba(255,255,255,0.05)',
|
||||||
border: '1px solid rgba(255,255,255,0.12)', borderRadius: 6,
|
border: '1px solid rgba(255,255,255,0.12)', borderRadius: 6,
|
||||||
|
|||||||
@@ -206,6 +206,9 @@ export function LiveArtboard({
|
|||||||
const removeElementEvent = useCanvas((s) => s.removeElementEvent);
|
const removeElementEvent = useCanvas((s) => s.removeElementEvent);
|
||||||
const clearRemoveElement = useCanvas((s) => s.clearRemoveElement);
|
const clearRemoveElement = useCanvas((s) => s.clearRemoveElement);
|
||||||
|
|
||||||
|
// Ref so the refresh timer can be cancelled when a faster patch arrives.
|
||||||
|
const styleRefreshTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const mine = styleEditQueue.filter((e) => e.artboardId === id);
|
const mine = styleEditQueue.filter((e) => e.artboardId === id);
|
||||||
if (mine.length === 0 || !isReadyRef.current) return;
|
if (mine.length === 0 || !isReadyRef.current) return;
|
||||||
@@ -213,6 +216,17 @@ export function LiveArtboard({
|
|||||||
sendMessage('PATCH_ELEMENT_STYLE', { nodeId: e.nodeId, property: e.property, value: e.value });
|
sendMessage('PATCH_ELEMENT_STYLE', { nodeId: e.nodeId, property: e.property, value: e.value });
|
||||||
}
|
}
|
||||||
clearStyleEdits(id);
|
clearStyleEdits(id);
|
||||||
|
|
||||||
|
// Refresh the design panel after the browser has applied the inline styles.
|
||||||
|
// We debounce at 120 ms so rapid dragging (color picker, resize) only fires
|
||||||
|
// one REQUEST_ELEMENT_STYLES at the end of the gesture, not on every event.
|
||||||
|
if (styleRefreshTimerRef.current) clearTimeout(styleRefreshTimerRef.current);
|
||||||
|
styleRefreshTimerRef.current = setTimeout(() => {
|
||||||
|
const nodeId = mine[mine.length - 1]?.nodeId;
|
||||||
|
if (nodeId && isReadyRef.current) {
|
||||||
|
sendMessage('REQUEST_ELEMENT_STYLES', { nodeId });
|
||||||
|
}
|
||||||
|
}, 120);
|
||||||
}, [id, styleEditQueue, sendMessage, clearStyleEdits]);
|
}, [id, styleEditQueue, sendMessage, clearStyleEdits]);
|
||||||
|
|
||||||
// ── Children style edit queue (PATCH_CHILDREN_STYLE — paragraph spacing etc.) ──
|
// ── Children style edit queue (PATCH_CHILDREN_STYLE — paragraph spacing etc.) ──
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useState, useMemo, useEffect } from 'react';
|
|||||||
import { Badge } from '@fluentui/react-components';
|
import { Badge } from '@fluentui/react-components';
|
||||||
import { useCanvas } from '@/store/canvas';
|
import { useCanvas } from '@/store/canvas';
|
||||||
import { useCanvasTheme } from '@/store/canvasTheme';
|
import { useCanvasTheme } from '@/store/canvasTheme';
|
||||||
|
import { useHistory } from '@/store/history';
|
||||||
import { useDlf } from '@/hooks/useDlf';
|
import { useDlf } from '@/hooks/useDlf';
|
||||||
import { checkComponentConstraints } from '@originmain/design-language';
|
import { checkComponentConstraints } from '@originmain/design-language';
|
||||||
import type { Violation } from '@originmain/design-language';
|
import type { Violation } from '@originmain/design-language';
|
||||||
@@ -92,11 +93,13 @@ export function DesignTab({
|
|||||||
const {
|
const {
|
||||||
patchStyleEdit,
|
patchStyleEdit,
|
||||||
patchChildrenStyleEdit,
|
patchChildrenStyleEdit,
|
||||||
|
setComponentStyles,
|
||||||
indexerStatus,
|
indexerStatus,
|
||||||
selectedComponentHasDirectText,
|
selectedComponentHasDirectText,
|
||||||
selectedComponentHasParagraphChildren,
|
selectedComponentHasParagraphChildren,
|
||||||
setActiveViolations,
|
setActiveViolations,
|
||||||
} = useCanvas();
|
} = useCanvas();
|
||||||
|
const { pushEdit } = useHistory();
|
||||||
const { dlf } = useDlf(workspaceId);
|
const { dlf } = useDlf(workspaceId);
|
||||||
|
|
||||||
// Re-run constraint checks whenever selected component or active DLF changes.
|
// Re-run constraint checks whenever selected component or active DLF changes.
|
||||||
@@ -149,9 +152,35 @@ export function DesignTab({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── patch: design panel → live artboard + history + optimistic panel refresh ──
|
||||||
|
// Three things happen on every edit:
|
||||||
|
// 1. patchStyleEdit → PATCH_ELEMENT_STYLE → SDK → inline style on DOM element
|
||||||
|
// 2. setComponentStyles (optimistic) → panel inputs immediately show the new
|
||||||
|
// value without waiting for the next REQUEST_ELEMENT_STYLES round-trip
|
||||||
|
// 3. pushEdit → history store → Diff tab tracks it → code export works
|
||||||
const patch = (prop: string, val: string) => {
|
const patch = (prop: string, val: string) => {
|
||||||
if (!artboardId || !componentId) return;
|
if (!artboardId || !componentId) return;
|
||||||
|
|
||||||
|
// 1. Send to iframe.
|
||||||
patchStyleEdit(artboardId, componentId, prop, val);
|
patchStyleEdit(artboardId, componentId, prop, val);
|
||||||
|
|
||||||
|
// 2. Optimistically reflect the change in the design panel immediately.
|
||||||
|
if (styles) {
|
||||||
|
setComponentStyles({ ...styles, [prop]: val });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Push to history so the Diff tab can generate a code patch.
|
||||||
|
pushEdit(artboardId, {
|
||||||
|
componentId,
|
||||||
|
componentName: componentData?.name ?? componentId,
|
||||||
|
changes: [{
|
||||||
|
key: prop,
|
||||||
|
before: styles?.[prop] ?? '',
|
||||||
|
after: val,
|
||||||
|
changeType: 'modified',
|
||||||
|
}],
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const patchChildren = (selector: string, prop: string, val: string) => {
|
const patchChildren = (selector: string, prop: string, val: string) => {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,40 @@
|
|||||||
|
# @originmain/live
|
||||||
|
|
||||||
|
Browser SDK for [Originmain](https://originmain.com) — installs the React fiber hook that enables live component inspection and design editing in the Originmain canvas.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @originmain/live
|
||||||
|
# or
|
||||||
|
pnpm add @originmain/live
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Import **before React** in your app entry point:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// app/layout.tsx (or pages/_app.tsx)
|
||||||
|
import '@originmain/live'; // ← must be first
|
||||||
|
import React from 'react';
|
||||||
|
// ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use the [`@originmain/next`](https://www.npmjs.com/package/@originmain/next) plugin which injects it automatically:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// next.config.ts
|
||||||
|
import { withOriginmain } from '@originmain/next';
|
||||||
|
export default withOriginmain({ reactStrictMode: true });
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
- Installs `__REACT_DEVTOOLS_GLOBAL_HOOK__` **before** React evaluates (module load time), so React captures fiber commits from the very first render.
|
||||||
|
- Activates **only** when the page runs inside an Originmain artboard iframe (detected via `#__om_artboard=<id>` in the URL fragment).
|
||||||
|
- **Complete no-op** in production or any non-Originmain context — zero runtime cost.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// ── @originmain/live build script ────────────────────────────────────────────
|
||||||
|
// Produces a single self-contained browser ESM bundle under dist/:
|
||||||
|
//
|
||||||
|
// dist/index.js — side-effect-only browser module, no external deps
|
||||||
|
//
|
||||||
|
// Run: node build.mjs (or via "pnpm build")
|
||||||
|
//
|
||||||
|
// Design notes:
|
||||||
|
// • Platform 'browser' — esbuild replaces process.env.NODE_ENV and
|
||||||
|
// avoids injecting Node built-in shims.
|
||||||
|
// • format 'esm' — the published package is "type": "module"; webpack/
|
||||||
|
// turbopack will tree-shake and include it in the user's bundle.
|
||||||
|
// • bundle: true — @originmain/live has no runtime npm dependencies so
|
||||||
|
// bundling produces one fully self-contained file with no require() calls.
|
||||||
|
// • minify: true — the hook ships inside the user's production bundle;
|
||||||
|
// every byte matters.
|
||||||
|
// • No type declarations needed — the package is a side-effect-only import
|
||||||
|
// (`import '@originmain/live'`) with no exported symbols.
|
||||||
|
|
||||||
|
import { build } from 'esbuild';
|
||||||
|
|
||||||
|
await build({
|
||||||
|
entryPoints: ['src/index.ts'],
|
||||||
|
bundle: true,
|
||||||
|
platform: 'browser',
|
||||||
|
format: 'esm',
|
||||||
|
target: ['es2020', 'chrome88', 'firefox78', 'safari14'],
|
||||||
|
minify: true,
|
||||||
|
sourcemap: false, // keep bundle clean; source is MIT-licensed anyway
|
||||||
|
outfile: 'dist/index.js',
|
||||||
|
logLevel: 'info',
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(' ✓ dist/index.js (browser ESM bundle)');
|
||||||
@@ -5,13 +5,15 @@
|
|||||||
"description": "Originmain live rendering SDK — installs fiber hook for component inspection. Import before React.",
|
"description": "Originmain live rendering SDK — installs fiber hook for component inspection. Import before React.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts"
|
".": "./dist/index.js"
|
||||||
},
|
},
|
||||||
"files": ["src", "README.md"],
|
"files": ["dist", "README.md"],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"build": "node build.mjs",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"esbuild": "^0.25.0",
|
||||||
"typescript": "^5.5.0"
|
"typescript": "^5.5.0"
|
||||||
},
|
},
|
||||||
"keywords": ["originmain", "react", "devtools", "fiber", "design-engineering"],
|
"keywords": ["originmain", "react", "devtools", "fiber", "design-engineering"],
|
||||||
|
|||||||
+447
-289
@@ -3,46 +3,131 @@
|
|||||||
// module body. React checks for __REACT_DEVTOOLS_GLOBAL_HOOK__ exactly once at
|
// module body. React checks for __REACT_DEVTOOLS_GLOBAL_HOOK__ exactly once at
|
||||||
// import time; any later installation is too late.
|
// import time; any later installation is too late.
|
||||||
//
|
//
|
||||||
// Full bidirectional protocol:
|
// Full bidirectional protocol (matches packages/renderer/src/protocol.ts):
|
||||||
// Renderer → Host : READY, FIBER_TREE_UPDATE, COMPONENT_SELECTED, COMPONENT_DESELECTED, ERROR
|
// Renderer → Host : READY, FIBER_TREE_UPDATE, COMPONENT_SELECTED,
|
||||||
// Host → Renderer : SET_DESIGN_TOKENS, NAVIGATE, SELECT_COMPONENT, DESELECT
|
// COMPONENT_DESELECTED, ELEMENT_STYLES, ROUTES_DISCOVERED,
|
||||||
|
// THUMBNAIL_READY, SNAPSHOT_READY, ERROR
|
||||||
|
// Host → Renderer : SET_DESIGN_TOKENS, NAVIGATE, SELECT_COMPONENT, DESELECT,
|
||||||
|
// REQUEST_ELEMENT_STYLES, PATCH_ELEMENT_STYLE,
|
||||||
|
// PATCH_CHILDREN_STYLE, REMOVE_ELEMENT,
|
||||||
|
// CAPTURE_THUMBNAIL, CAPTURE_SNAPSHOT, CANCEL_SNAPSHOT
|
||||||
//
|
//
|
||||||
// Node IDs are stable path strings: "Component:idx/Child:idx/…"
|
// Activation guard — checked in priority order:
|
||||||
// This survives re-renders as long as the component tree structure is unchanged.
|
// 1. URL fragment: location.hash contains __om_artboard=<id>
|
||||||
|
// 2. window.name: starts with "om:" (works for same-origin iframes)
|
||||||
|
// 3. postMessage handshake: sends __om_init_request to parent, waits for reply
|
||||||
//
|
//
|
||||||
// Guard: only activates when window.name starts with "om:" — the prefix set by
|
// Chrome 88+ strips window.name on cross-origin iframe loads (Spectre
|
||||||
// LiveArtboard.tsx on the <iframe name="om:{id}"> element. Outside Originmain
|
// mitigation). The URL fragment approach is immune to this because LiveArtboard
|
||||||
// iframes this module is a complete no-op.
|
// appends #__om_artboard=<id> to the iframe src, which survives cross-origin
|
||||||
|
// navigation. The postMessage handshake is a final fallback.
|
||||||
//
|
//
|
||||||
// This file is intentionally self-contained (no @originmain/* imports) because
|
// This file is intentionally self-contained (no @originmain/* imports) so it
|
||||||
// it ships as a public npm package and must work standalone.
|
// ships as a standalone npm package without workspace dependencies.
|
||||||
|
|
||||||
const RENDERER_SOURCE = 'originmain-renderer';
|
const RENDERER_SOURCE = 'originmain-renderer';
|
||||||
const HOST_SOURCE = 'originmain-host';
|
const HOST_SOURCE = 'originmain-host';
|
||||||
const NAME_PREFIX = 'om:';
|
const NAME_PREFIX = 'om:';
|
||||||
|
|
||||||
// ── Guard ─────────────────────────────────────────────────────────────────────
|
// ── Artboard ID resolution ────────────────────────────────────────────────────
|
||||||
|
// Returns null if we're not inside an Originmain artboard iframe at all.
|
||||||
|
|
||||||
function isOriginmainIframe(): boolean {
|
function resolveArtboardIdSync(): string | null {
|
||||||
|
// Not in an iframe at all — bail immediately.
|
||||||
|
try { if (window.parent === window) return null; }
|
||||||
|
catch { return null; }
|
||||||
|
|
||||||
|
// 1. URL fragment: #__om_artboard=<id> (primary — cross-origin safe)
|
||||||
try {
|
try {
|
||||||
return (
|
const match = window.location.hash.match(/__om_artboard=([^&]+)/);
|
||||||
window.parent !== window &&
|
if (match?.[1]) return decodeURIComponent(match[1]);
|
||||||
typeof window.name === 'string' &&
|
} catch { /* */ }
|
||||||
window.name.startsWith(NAME_PREFIX)
|
|
||||||
);
|
// 2. window.name: "om:<id>" (works for same-origin iframes)
|
||||||
} catch {
|
try {
|
||||||
return false; // Accessing window.parent can throw in certain sandboxed contexts.
|
if (typeof window.name === 'string' && window.name.startsWith(NAME_PREFIX)) {
|
||||||
|
return window.name.slice(NAME_PREFIX.length);
|
||||||
|
}
|
||||||
|
} catch { /* */ }
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Hook installation ─────────────────────────────────────────────────────────
|
||||||
|
// We install the DevTools hook unconditionally when inside ANY iframe, because
|
||||||
|
// React evaluates __REACT_DEVTOOLS_GLOBAL_HOOK__ at module load time. If we
|
||||||
|
// wait for the artboard ID we're already too late. The hook stays dormant until
|
||||||
|
// the artboard ID is resolved (either synchronously or via postMessage).
|
||||||
|
|
||||||
|
(function bootstrap() {
|
||||||
|
// Not in a frame at all — complete no-op.
|
||||||
|
try { if (window.parent === window) return; }
|
||||||
|
catch { return; }
|
||||||
|
|
||||||
|
// ── Install the DevTools hook immediately ────────────────────────────────
|
||||||
|
// React reads __REACT_DEVTOOLS_GLOBAL_HOOK__ exactly once when its module
|
||||||
|
// body runs. We must be here first.
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const g = globalThis as any;
|
||||||
|
|
||||||
|
type Hook = {
|
||||||
|
renderers: Map<unknown, unknown>;
|
||||||
|
supportsFiber: boolean;
|
||||||
|
_isDisabled: boolean;
|
||||||
|
inject?: (...a: unknown[]) => void;
|
||||||
|
onCommitFiberRoot?: (...a: unknown[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
let hook: Hook = g.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
||||||
|
if (!hook) {
|
||||||
|
hook = { renderers: new Map(), supportsFiber: true, _isDisabled: false };
|
||||||
|
g.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (isOriginmainIframe()) {
|
// ── Resolve artboard ID ──────────────────────────────────────────────────
|
||||||
installFiberHook();
|
const syncId = resolveArtboardIdSync();
|
||||||
}
|
if (syncId) {
|
||||||
|
startMainLoop(hook, syncId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Core ──────────────────────────────────────────────────────────────────────
|
// No ID found synchronously — try the postMessage handshake.
|
||||||
|
// The parent (LiveArtboard.tsx) listens for __om_init_request and responds
|
||||||
|
// with { __om_init_response: true, artboardId: id }.
|
||||||
|
let resolved = false;
|
||||||
|
|
||||||
function installFiberHook(): void {
|
function onHandshakeReply(event: MessageEvent) {
|
||||||
const artboardId = window.name.slice(NAME_PREFIX.length);
|
const d = event.data as { __om_init_response?: boolean; artboardId?: string } | null;
|
||||||
|
if (d?.__om_init_response === true && typeof d.artboardId === 'string') {
|
||||||
|
if (!resolved) {
|
||||||
|
resolved = true;
|
||||||
|
window.removeEventListener('message', onHandshakeReply);
|
||||||
|
startMainLoop(hook, d.artboardId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('message', onHandshakeReply);
|
||||||
|
try {
|
||||||
|
window.parent.postMessage({ __om_init_request: true }, '*');
|
||||||
|
} catch { /* sandboxed — postMessage blocked */ }
|
||||||
|
|
||||||
|
// Give up after 10 s to avoid a stale listener.
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!resolved) window.removeEventListener('message', onHandshakeReply);
|
||||||
|
}, 10_000);
|
||||||
|
})();
|
||||||
|
|
||||||
|
// ── Main loop (runs once artboard ID is known) ────────────────────────────────
|
||||||
|
|
||||||
|
function startMainLoop(hook: {
|
||||||
|
renderers: Map<unknown, unknown>;
|
||||||
|
supportsFiber: boolean;
|
||||||
|
_isDisabled: boolean;
|
||||||
|
inject?: (...a: unknown[]) => void;
|
||||||
|
onCommitFiberRoot?: (...a: unknown[]) => void;
|
||||||
|
}, artboardId: string): void {
|
||||||
|
|
||||||
// ── postMessage helper ────────────────────────────────────────────────────
|
// ── postMessage helper ────────────────────────────────────────────────────
|
||||||
function post(msg: Record<string, unknown>): void {
|
function post(msg: Record<string, unknown>): void {
|
||||||
@@ -51,88 +136,50 @@ function installFiberHook(): void {
|
|||||||
{ source: RENDERER_SOURCE, artboardId, message: msg },
|
{ source: RENDERER_SOURCE, artboardId, message: msg },
|
||||||
'*',
|
'*',
|
||||||
);
|
);
|
||||||
} catch {
|
} catch { /* parent frame unreachable */ }
|
||||||
// Parent frame unreachable — silently ignore.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Runtime state ─────────────────────────────────────────────────────────
|
// ── Runtime state ─────────────────────────────────────────────────────────
|
||||||
// nodeMap: nodeId → { domRect (snapshot), fiber (live reference for re-measurement) }
|
let nodeMap = new Map<string, { domRect: DomRect | null; fiber: FiberLike }>();
|
||||||
let nodeMap = new Map<string, { domRect: DomRect | null; fiber: FiberLike }>();
|
|
||||||
// fiberMap: fiber object → nodeId for O(1) hit-test lookup via __reactFiber$ DOM keys.
|
|
||||||
// A null value means the fiber is unnamed/transparent and not directly selectable.
|
|
||||||
let fiberMap = new WeakMap<object, string | null>();
|
let fiberMap = new WeakMap<object, string | null>();
|
||||||
let selectedNodeId: string | null = null;
|
let selectedNodeId: string | null = null;
|
||||||
let highlightEl: HTMLElement | null = null;
|
let highlightEl: HTMLElement | null = null;
|
||||||
|
let snapshotAborted = false;
|
||||||
// ── React DevTools global hook ─────────────────────────────────────────────
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// ── Fiber hook — onCommitFiberRoot ────────────────────────────────────────
|
||||||
const _prevCommit = hook.onCommitFiberRoot;
|
const _prevCommit = hook.onCommitFiberRoot;
|
||||||
|
|
||||||
hook.onCommitFiberRoot = function onCommitFiberRoot(...args: unknown[]) {
|
hook.onCommitFiberRoot = function onCommitFiberRoot(...args: unknown[]) {
|
||||||
// Delegate to any pre-existing handler (React DevTools extension) first.
|
|
||||||
if (typeof _prevCommit === 'function') {
|
if (typeof _prevCommit === 'function') {
|
||||||
try { _prevCommit.apply(this, args); }
|
try { _prevCommit.apply(this, args); } catch { /* don't break existing DevTools */ }
|
||||||
catch { /* don't break existing DevTools */ }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// args[1] is the FiberRoot object — { current: Fiber }
|
|
||||||
const root = args[1] as { current: FiberLike } | undefined;
|
const root = args[1] as { current: FiberLike } | undefined;
|
||||||
if (!root?.current) return;
|
if (!root?.current) return;
|
||||||
|
|
||||||
// Reset both maps before each walk so stale entries from the previous tree
|
|
||||||
// don't accumulate. fiberMap is a WeakMap so it self-cleans, but nodeMap
|
|
||||||
// must be rebuilt from scratch on every commit.
|
|
||||||
nodeMap = new Map();
|
nodeMap = new Map();
|
||||||
fiberMap = new WeakMap();
|
fiberMap = new WeakMap();
|
||||||
|
|
||||||
const tree = serializeFiber(root.current, '');
|
const tree = serializeFiber(root.current, '');
|
||||||
post({ type: 'FIBER_TREE_UPDATE', root: tree });
|
post({ type: 'FIBER_TREE_UPDATE', root: tree });
|
||||||
// Re-sync the highlight ring after each React commit (component may move).
|
|
||||||
if (selectedNodeId) updateHighlight();
|
if (selectedNodeId) updateHighlight();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
post({ type: 'ERROR', message: String(err) });
|
post({ type: 'ERROR', message: String(err) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Fiber serialization (stable path-based IDs) ───────────────────────────
|
// ── Fiber serialization ───────────────────────────────────────────────────
|
||||||
// ID format: "ComponentName:siblingIndex/ChildName:siblingIndex/..."
|
|
||||||
//
|
|
||||||
// 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 (old approach) caused
|
|
||||||
// entire subtrees to vanish from the tree.
|
|
||||||
|
|
||||||
function serializeFiber(
|
function serializeFiber(fiber: FiberLike | null, parentId: string): SerializedNode | null {
|
||||||
fiber: FiberLike | null,
|
|
||||||
parentId: string,
|
|
||||||
): SerializedNode | null {
|
|
||||||
if (!fiber) return null;
|
if (!fiber) return null;
|
||||||
|
|
||||||
const name = getDisplayName(fiber);
|
const name = getDisplayName(fiber);
|
||||||
if (!name) {
|
if (!name) {
|
||||||
// Unnamed root fiber (HostRoot) — collectChildren handles Fragment recursively.
|
|
||||||
const children: SerializedNode[] = [];
|
const children: SerializedNode[] = [];
|
||||||
collectChildren(fiber, parentId, children);
|
collectChildren(fiber, parentId, children);
|
||||||
if (children.length === 1) return children[0] ?? null;
|
if (children.length === 1) return children[0] ?? null;
|
||||||
if (children.length === 0) return null;
|
if (children.length === 0) return null;
|
||||||
// Multiple named children at root — wrap in a synthetic root node.
|
|
||||||
return { id: '__root__', name: '__root__', props: {}, children };
|
return { id: '__root__', name: '__root__', props: {}, children };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +193,16 @@ function installFiberHook(): void {
|
|||||||
};
|
};
|
||||||
if (rect) node.domRect = rect;
|
if (rect) node.domRect = rect;
|
||||||
|
|
||||||
// Register in both maps for O(1) lookup.
|
// Attach call-site if present (React dev builds expose _debugSource).
|
||||||
|
const src = (fiber as FiberLike & { _debugSource?: { fileName?: string; lineNumber?: number; columnNumber?: number } })._debugSource;
|
||||||
|
if (src?.fileName && typeof src.lineNumber === 'number') {
|
||||||
|
node.callSite = {
|
||||||
|
fileName: src.fileName,
|
||||||
|
lineNumber: src.lineNumber,
|
||||||
|
...(src.columnNumber !== undefined ? { columnNumber: src.columnNumber } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
nodeMap.set(nodeId, { domRect: rect, fiber });
|
nodeMap.set(nodeId, { domRect: rect, fiber });
|
||||||
fiberMap.set(fiber, nodeId);
|
fiberMap.set(fiber, nodeId);
|
||||||
|
|
||||||
@@ -154,18 +210,14 @@ function installFiberHook(): void {
|
|||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect all named descendants of fiber.child into out[], transparently
|
|
||||||
// flattening unnamed intermediates (Fragments, Providers, wrappers).
|
|
||||||
function collectChildren(fiber: FiberLike, parentId: string, out: SerializedNode[]): void {
|
function collectChildren(fiber: FiberLike, parentId: string, out: SerializedNode[]): void {
|
||||||
let child = fiber.child;
|
let child = fiber.child;
|
||||||
while (child) {
|
while (child) {
|
||||||
const name = getDisplayName(child);
|
const name = getDisplayName(child);
|
||||||
if (name) {
|
if (name) {
|
||||||
const serialized = serializeFiber(child, parentId);
|
const s = serializeFiber(child, parentId);
|
||||||
if (serialized) out.push(serialized);
|
if (s) out.push(s);
|
||||||
} else {
|
} else {
|
||||||
// Unnamed (Fragment / Context / Provider / forwardRef wrapper etc.):
|
|
||||||
// mark as non-selectable and flatten children directly into our level.
|
|
||||||
fiberMap.set(child, null);
|
fiberMap.set(child, null);
|
||||||
collectChildren(child, parentId, out);
|
collectChildren(child, parentId, out);
|
||||||
}
|
}
|
||||||
@@ -179,13 +231,13 @@ function installFiberHook(): void {
|
|||||||
if (typeof type === 'string') return type;
|
if (typeof type === 'string') return type;
|
||||||
if (typeof type === 'function') {
|
if (typeof type === 'function') {
|
||||||
return (type as { displayName?: string; name?: string }).displayName
|
return (type as { displayName?: string; name?: string }).displayName
|
||||||
?? (type as { name?: string }).name
|
?? (type as { name?: string }).name
|
||||||
?? null;
|
?? null;
|
||||||
}
|
}
|
||||||
if (typeof type === 'object' && type !== null && '$$typeof' in type) {
|
if (typeof type === 'object' && type !== null && '$$typeof' in type) {
|
||||||
return (type as { displayName?: string; name?: string }).displayName
|
return (type as { displayName?: string; name?: string }).displayName
|
||||||
?? (type as { name?: string }).name
|
?? (type as { name?: string }).name
|
||||||
?? null;
|
?? null;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -217,15 +269,11 @@ function installFiberHook(): void {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Highlight overlay (blue ring inside the iframe) ───────────────────────
|
// ── Highlight overlay ─────────────────────────────────────────────────────
|
||||||
// updateHighlight re-measures from the live fiber stateNode so the ring stays
|
|
||||||
// accurate even after scroll (between React commits).
|
|
||||||
|
|
||||||
function updateHighlight(): void {
|
function updateHighlight(): void {
|
||||||
const info = selectedNodeId ? nodeMap.get(selectedNodeId) : undefined;
|
const info = selectedNodeId ? nodeMap.get(selectedNodeId) : undefined;
|
||||||
if (!info) { removeHighlight(); return; }
|
if (!info) { removeHighlight(); return; }
|
||||||
|
|
||||||
// Re-measure from the live DOM element for scroll accuracy.
|
|
||||||
const rect = getDomRect(info.fiber) ?? info.domRect;
|
const rect = getDomRect(info.fiber) ?? info.domRect;
|
||||||
if (rect && rect.width > 0 && rect.height > 0) {
|
if (rect && rect.width > 0 && rect.height > 0) {
|
||||||
renderHighlight(rect);
|
renderHighlight(rect);
|
||||||
@@ -261,48 +309,42 @@ function installFiberHook(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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', () => {
|
window.addEventListener('scroll', () => {
|
||||||
if (selectedNodeId) updateHighlight();
|
if (selectedNodeId) updateHighlight();
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
// ── Click-to-select (capturing phase) ────────────────────────────────────
|
// ── Click-to-select ───────────────────────────────────────────────────────
|
||||||
// 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.
|
|
||||||
|
|
||||||
function getFiberKey(el: Element): string | null {
|
function getFiberKey(el: Element): string | null {
|
||||||
const keys = Object.keys(el);
|
const keys = Object.keys(el);
|
||||||
for (let i = 0; i < keys.length; i++) {
|
for (let i = 0; i < keys.length; i++) {
|
||||||
const k = keys[i];
|
const k = keys[i];
|
||||||
if (k !== undefined && k.startsWith('__reactFiber$')) return k;
|
if (k?.startsWith('__reactFiber$')) return k;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('click', (event: MouseEvent) => {
|
document.addEventListener('click', (event: MouseEvent) => {
|
||||||
// Ignore clicks on our own highlight overlay.
|
|
||||||
if (event.target === highlightEl) return;
|
if (event.target === highlightEl) return;
|
||||||
|
|
||||||
const el = document.elementFromPoint(event.clientX, event.clientY);
|
const el = document.elementFromPoint(event.clientX, event.clientY);
|
||||||
|
|
||||||
// Walk the DOM upward, trying to find a tracked React fiber at each level.
|
|
||||||
let current: Element | null = el;
|
let current: Element | null = el;
|
||||||
while (current && current !== document.documentElement) {
|
while (current && current !== document.documentElement) {
|
||||||
const fiberKey = getFiberKey(current);
|
const fiberKey = getFiberKey(current);
|
||||||
if (fiberKey) {
|
if (fiberKey) {
|
||||||
// Walk the fiber's return (parent) chain to find the nearest tracked node.
|
|
||||||
let fiber: FiberLike | null =
|
let fiber: FiberLike | null =
|
||||||
(current as unknown as Record<string, unknown>)[fiberKey] as FiberLike | null;
|
(current as unknown as Record<string, unknown>)[fiberKey] as FiberLike | null;
|
||||||
while (fiber) {
|
while (fiber) {
|
||||||
const nodeId = fiberMap.get(fiber);
|
const nodeId = fiberMap.get(fiber);
|
||||||
if (nodeId) {
|
if (nodeId) {
|
||||||
// Re-measure from the live element for accurate post-scroll rect.
|
|
||||||
const liveEl = fiber.stateNode;
|
const liveEl = fiber.stateNode;
|
||||||
if (liveEl && typeof liveEl === 'object' && 'getBoundingClientRect' in liveEl) {
|
if (liveEl && typeof liveEl === 'object' && 'getBoundingClientRect' in liveEl) {
|
||||||
const r = (liveEl as Element).getBoundingClientRect();
|
const r = (liveEl as Element).getBoundingClientRect();
|
||||||
post({ type: 'COMPONENT_SELECTED', nodeId, rect: { x: r.x, y: r.y, width: r.width, height: r.height } });
|
post({
|
||||||
|
type: 'COMPONENT_SELECTED',
|
||||||
|
nodeId,
|
||||||
|
rect: { x: r.x, y: r.y, width: r.width, height: r.height },
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -312,10 +354,271 @@ function installFiberHook(): void {
|
|||||||
current = current.parentElement;
|
current = current.parentElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nothing found — clear the selection.
|
|
||||||
post({ type: 'COMPONENT_DESELECTED' });
|
post({ type: 'COMPONENT_DESELECTED' });
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
|
// ── Element style inspection ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
const INSPECTED_PROPS = [
|
||||||
|
'color', 'font-family', 'font-size', 'font-weight', 'line-height',
|
||||||
|
'letter-spacing', 'text-align', 'text-transform', 'text-decoration',
|
||||||
|
'display', 'width', 'height',
|
||||||
|
'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
|
||||||
|
'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
|
||||||
|
'flex-direction', 'align-items', 'justify-content', 'gap',
|
||||||
|
'position', 'top', 'right', 'bottom', 'left',
|
||||||
|
'background-color', 'border-radius', 'opacity',
|
||||||
|
'box-shadow', 'border-width', 'border-color', 'border-style',
|
||||||
|
'overflow', 'cursor', 'transition',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
function respondWithStyles(nodeId: string): void {
|
||||||
|
const info = nodeMap.get(nodeId);
|
||||||
|
const styles: Record<string, string> = {};
|
||||||
|
let hasDirectText = false;
|
||||||
|
let hasParagraphChildren = false;
|
||||||
|
|
||||||
|
if (info?.fiber?.stateNode && typeof info.fiber.stateNode === 'object'
|
||||||
|
&& 'nodeType' in (info.fiber.stateNode as object)) {
|
||||||
|
try {
|
||||||
|
const el = info.fiber.stateNode as Element;
|
||||||
|
const computed = window.getComputedStyle(el);
|
||||||
|
|
||||||
|
for (const prop of INSPECTED_PROPS) {
|
||||||
|
const val = computed.getPropertyValue(prop);
|
||||||
|
if (val) styles[prop] = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Structural flags for the Typography panel.
|
||||||
|
const childNodes = el.childNodes;
|
||||||
|
for (let i = 0; i < childNodes.length; i++) {
|
||||||
|
const n = childNodes[i];
|
||||||
|
if (n?.nodeType === Node.TEXT_NODE && n.textContent?.trim()) {
|
||||||
|
hasDirectText = true;
|
||||||
|
}
|
||||||
|
if ((n as Element)?.tagName === 'P') {
|
||||||
|
hasParagraphChildren = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch { /* element may be detached */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
post({ type: 'ELEMENT_STYLES', nodeId, styles, hasDirectText, hasParagraphChildren });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Style patching ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function patchElementStyle(nodeId: string, property: string, value: string): void {
|
||||||
|
const info = nodeMap.get(nodeId);
|
||||||
|
if (!info?.fiber?.stateNode || typeof info.fiber.stateNode !== 'object'
|
||||||
|
|| !('nodeType' in (info.fiber.stateNode as object))) return;
|
||||||
|
try {
|
||||||
|
const el = info.fiber.stateNode as HTMLElement;
|
||||||
|
value === '' ? el.style.removeProperty(property) : el.style.setProperty(property, value);
|
||||||
|
} catch { /* detached */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchChildrenStyle(
|
||||||
|
parentNodeId: string,
|
||||||
|
selector: string,
|
||||||
|
property: string,
|
||||||
|
value: string,
|
||||||
|
): void {
|
||||||
|
const info = nodeMap.get(parentNodeId);
|
||||||
|
if (!info?.fiber?.stateNode || typeof info.fiber.stateNode !== 'object'
|
||||||
|
|| !('nodeType' in (info.fiber.stateNode as object))) return;
|
||||||
|
try {
|
||||||
|
const parent = info.fiber.stateNode as Element;
|
||||||
|
parent.querySelectorAll(selector).forEach((child) => {
|
||||||
|
if (child.parentElement === parent) {
|
||||||
|
value === ''
|
||||||
|
? (child as HTMLElement).style.removeProperty(property)
|
||||||
|
: (child as HTMLElement).style.setProperty(property, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch { /* detached */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeElement(nodeId: string): void {
|
||||||
|
const info = nodeMap.get(nodeId);
|
||||||
|
if (!info?.fiber?.stateNode || typeof info.fiber.stateNode !== 'object'
|
||||||
|
|| !('nodeType' in (info.fiber.stateNode as object))) return;
|
||||||
|
try {
|
||||||
|
(info.fiber.stateNode as HTMLElement).style.setProperty('display', 'none');
|
||||||
|
} catch { /* detached */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Design tokens ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function applyTokens(tokens: Record<string, string>): void {
|
||||||
|
const root = document.documentElement;
|
||||||
|
for (const [k, v] of Object.entries(tokens)) {
|
||||||
|
root.style.setProperty(k, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Navigation ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function doNavigate(path: string): void {
|
||||||
|
try {
|
||||||
|
history.pushState(null, '', path);
|
||||||
|
window.dispatchEvent(new PopStateEvent('popstate', { state: null }));
|
||||||
|
} catch { /* navigation not available */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Screenshot capture (html2canvas) ─────────────────────────────────────
|
||||||
|
// Loaded on demand from the proxy's embedded bundle (/__om_h2c__.js) or
|
||||||
|
// from unpkg. Falls back to null if neither is available.
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
type Html2CanvasFn = (el: HTMLElement, opts?: Record<string, unknown>) => Promise<HTMLCanvasElement>;
|
||||||
|
|
||||||
|
let html2canvasCache: Html2CanvasFn | null | 'pending' = null;
|
||||||
|
|
||||||
|
async function loadHtml2Canvas(): Promise<Html2CanvasFn | null> {
|
||||||
|
if (html2canvasCache !== null && html2canvasCache !== 'pending') return html2canvasCache;
|
||||||
|
if (html2canvasCache === 'pending') {
|
||||||
|
// Wait for the in-flight load.
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const check = setInterval(() => {
|
||||||
|
if (html2canvasCache !== 'pending') {
|
||||||
|
clearInterval(check);
|
||||||
|
resolve(html2canvasCache as Html2CanvasFn | null);
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
html2canvasCache = 'pending';
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const g = globalThis as any;
|
||||||
|
|
||||||
|
// Already on window (e.g. loaded by the proxy script).
|
||||||
|
if (typeof g.html2canvas === 'function') {
|
||||||
|
html2canvasCache = g.html2canvas as Html2CanvasFn;
|
||||||
|
return html2canvasCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try the proxy-embedded bundle first (works offline, no CSP issues).
|
||||||
|
const candidates = [
|
||||||
|
'/__om_h2c__.js',
|
||||||
|
'https://unpkg.com/html2canvas@1.4.1/dist/html2canvas.min.js',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const src of candidates) {
|
||||||
|
try {
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
const s = document.createElement('script');
|
||||||
|
s.src = src;
|
||||||
|
s.onload = () => resolve();
|
||||||
|
s.onerror = () => reject(new Error(`Failed to load ${src}`));
|
||||||
|
document.head.appendChild(s);
|
||||||
|
});
|
||||||
|
if (typeof g.html2canvas === 'function') {
|
||||||
|
html2canvasCache = g.html2canvas as Html2CanvasFn;
|
||||||
|
return html2canvasCache;
|
||||||
|
}
|
||||||
|
} catch { /* try next source */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
html2canvasCache = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function captureThumbnail(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const h2c = await loadHtml2Canvas();
|
||||||
|
if (!h2c) { post({ type: 'THUMBNAIL_READY', dataUrl: null }); return; }
|
||||||
|
|
||||||
|
const canvas = await h2c(document.body, {
|
||||||
|
scale: 0.5,
|
||||||
|
useCORS: true,
|
||||||
|
allowTaint: true,
|
||||||
|
logging: false,
|
||||||
|
imageTimeout: 5000,
|
||||||
|
});
|
||||||
|
post({ type: 'THUMBNAIL_READY', dataUrl: canvas.toDataURL('image/jpeg', 0.7) });
|
||||||
|
} catch {
|
||||||
|
post({ type: 'THUMBNAIL_READY', dataUrl: null });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function captureSnapshot(nodeId: string): Promise<void> {
|
||||||
|
snapshotAborted = false;
|
||||||
|
try {
|
||||||
|
const info = nodeMap.get(nodeId);
|
||||||
|
if (!info?.fiber?.stateNode || typeof info.fiber.stateNode !== 'object'
|
||||||
|
|| !('nodeType' in (info.fiber.stateNode as object))) {
|
||||||
|
post({ type: 'SNAPSHOT_READY', dataUrl: null, nodeId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const h2c = await loadHtml2Canvas();
|
||||||
|
if (!h2c || snapshotAborted) { post({ type: 'SNAPSHOT_READY', dataUrl: null, nodeId }); return; }
|
||||||
|
|
||||||
|
const el = info.fiber.stateNode as HTMLElement;
|
||||||
|
const canvas = await h2c(el, { scale: 2, useCORS: true, allowTaint: true, logging: false });
|
||||||
|
if (snapshotAborted) { post({ type: 'SNAPSHOT_READY', dataUrl: null, nodeId }); return; }
|
||||||
|
|
||||||
|
post({ type: 'SNAPSHOT_READY', dataUrl: canvas.toDataURL('image/png'), nodeId });
|
||||||
|
} catch {
|
||||||
|
post({ type: 'SNAPSHOT_READY', dataUrl: null, nodeId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Route discovery ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function humanLabel(path: string): string {
|
||||||
|
if (path === '/') return 'Home';
|
||||||
|
return path
|
||||||
|
.split('/')
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((s) => s.charAt(0).toUpperCase() + s.slice(1).replace(/-/g, ' '))
|
||||||
|
.join(' / ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function discoverRoutes(): void {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const routes: Array<{ path: string; label: string }> = [];
|
||||||
|
|
||||||
|
const addRoute = (path: string, hint?: string) => {
|
||||||
|
if (!path || path.startsWith('#') || seen.has(path)) return;
|
||||||
|
seen.add(path);
|
||||||
|
routes.push({ path, label: hint?.trim().slice(0, 50) || humanLabel(path) });
|
||||||
|
};
|
||||||
|
|
||||||
|
addRoute(window.location.pathname, document.title || undefined);
|
||||||
|
|
||||||
|
document.querySelectorAll<HTMLAnchorElement>('a[href]').forEach((a) => {
|
||||||
|
try {
|
||||||
|
const raw = (a.getAttribute('href') ?? '').trim();
|
||||||
|
if (raw.startsWith('/')) { addRoute(raw, a.textContent ?? undefined); return; }
|
||||||
|
const url = new URL(a.href, window.location.href);
|
||||||
|
if (url.origin !== window.location.origin) return;
|
||||||
|
addRoute(url.pathname, a.textContent ?? undefined);
|
||||||
|
} catch { /* malformed href */ }
|
||||||
|
});
|
||||||
|
|
||||||
|
nodeMap.forEach(({ fiber }) => {
|
||||||
|
const name = fiber ? getDisplayName(fiber) : null;
|
||||||
|
if (name && /^(Link|NavLink|NextLink|RouterLink|a)$/.test(name)) {
|
||||||
|
const href = fiber?.memoizedProps?.['href'] ?? fiber?.memoizedProps?.['to'];
|
||||||
|
if (typeof href === 'string' && href.startsWith('/')) addRoute(href);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (routes.length > 0) post({ type: 'ROUTES_DISCOVERED', routes });
|
||||||
|
}
|
||||||
|
|
||||||
|
let routeDiscoveryScheduled = false;
|
||||||
|
setTimeout(discoverRoutes, 800);
|
||||||
|
window.addEventListener('popstate', () => {
|
||||||
|
if (!routeDiscoveryScheduled) {
|
||||||
|
routeDiscoveryScheduled = true;
|
||||||
|
setTimeout(() => { routeDiscoveryScheduled = false; discoverRoutes(); }, 300);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ── Host → Renderer message handler ──────────────────────────────────────
|
// ── Host → Renderer message handler ──────────────────────────────────────
|
||||||
|
|
||||||
window.addEventListener('message', (event: MessageEvent) => {
|
window.addEventListener('message', (event: MessageEvent) => {
|
||||||
@@ -327,12 +630,14 @@ function installFiberHook(): void {
|
|||||||
tokens?: Record<string, string>;
|
tokens?: Record<string, string>;
|
||||||
path?: string;
|
path?: string;
|
||||||
nodeId?: string;
|
nodeId?: string;
|
||||||
|
parentNodeId?: string;
|
||||||
|
selector?: string;
|
||||||
property?: string;
|
property?: string;
|
||||||
value?: string | undefined;
|
value?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
if (!data || data.source !== HOST_SOURCE) return;
|
if (!data || data.source !== HOST_SOURCE) return;
|
||||||
if (data.artboardId !== artboardId) return;
|
if (data.artboardId !== artboardId) return;
|
||||||
const msg = data.message;
|
const msg = data.message;
|
||||||
if (!msg) return;
|
if (!msg) return;
|
||||||
|
|
||||||
@@ -344,10 +649,7 @@ function installFiberHook(): void {
|
|||||||
if (msg.path) doNavigate(msg.path);
|
if (msg.path) doNavigate(msg.path);
|
||||||
break;
|
break;
|
||||||
case 'SELECT_COMPONENT':
|
case 'SELECT_COMPONENT':
|
||||||
if (msg.nodeId) {
|
if (msg.nodeId) { selectedNodeId = msg.nodeId; updateHighlight(); }
|
||||||
selectedNodeId = msg.nodeId;
|
|
||||||
updateHighlight();
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case 'DESELECT':
|
case 'DESELECT':
|
||||||
selectedNodeId = null;
|
selectedNodeId = null;
|
||||||
@@ -361,203 +663,59 @@ function installFiberHook(): void {
|
|||||||
patchElementStyle(msg.nodeId, msg.property, msg.value ?? '');
|
patchElementStyle(msg.nodeId, msg.property, msg.value ?? '');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'PATCH_CHILDREN_STYLE':
|
||||||
|
if (msg.parentNodeId && msg.selector && msg.property && msg.value !== undefined) {
|
||||||
|
patchChildrenStyle(msg.parentNodeId, msg.selector, msg.property, msg.value ?? '');
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'REMOVE_ELEMENT':
|
case 'REMOVE_ELEMENT':
|
||||||
if (msg.nodeId) removeElement(msg.nodeId);
|
if (msg.nodeId) removeElement(msg.nodeId);
|
||||||
break;
|
break;
|
||||||
|
case 'CAPTURE_THUMBNAIL':
|
||||||
|
void captureThumbnail();
|
||||||
|
break;
|
||||||
|
case 'CAPTURE_SNAPSHOT':
|
||||||
|
if (msg.nodeId) void captureSnapshot(msg.nodeId);
|
||||||
|
break;
|
||||||
|
case 'CANCEL_SNAPSHOT':
|
||||||
|
snapshotAborted = true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Element style inspection ──────────────────────────────────────────────
|
// ── READY signal ──────────────────────────────────────────────────────────
|
||||||
// Reads computed CSS properties from the fiber node's DOM element and posts
|
|
||||||
// them back as ELEMENT_STYLES. We extract a curated subset covering the
|
|
||||||
// properties designers care about (typography, layout, visual) rather than the
|
|
||||||
// full ~300-property computed style object.
|
|
||||||
|
|
||||||
const INSPECTED_PROPS = [
|
let rootFontSizePx: number | undefined;
|
||||||
// Typography
|
try {
|
||||||
'color', 'font-family', 'font-size', 'font-weight', 'line-height',
|
const computed = window.getComputedStyle(document.documentElement);
|
||||||
'letter-spacing', 'text-align', 'text-transform', 'text-decoration',
|
const parsed = parseFloat(computed.fontSize);
|
||||||
// Layout
|
if (!isNaN(parsed)) rootFontSizePx = parsed;
|
||||||
'display', 'width', 'height',
|
} catch { /* */ }
|
||||||
'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
|
|
||||||
'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
|
|
||||||
'flex-direction', 'align-items', 'justify-content', 'gap',
|
|
||||||
'position', 'top', 'right', 'bottom', 'left',
|
|
||||||
// Visual
|
|
||||||
'background-color', 'border-radius', 'opacity',
|
|
||||||
'box-shadow', 'border-width', 'border-color', 'border-style',
|
|
||||||
'overflow', 'cursor', 'transition',
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
function respondWithStyles(nodeId: string): void {
|
post({ type: 'READY', rootFontSizePx });
|
||||||
const info = nodeMap.get(nodeId);
|
|
||||||
const styles: Record<string, string> = {};
|
|
||||||
|
|
||||||
if (info?.fiber?.stateNode && typeof info.fiber.stateNode === 'object'
|
|
||||||
&& 'nodeType' in (info.fiber.stateNode as object)) {
|
|
||||||
try {
|
|
||||||
const computed = window.getComputedStyle(info.fiber.stateNode as Element);
|
|
||||||
for (const prop of INSPECTED_PROPS) {
|
|
||||||
const val = computed.getPropertyValue(prop);
|
|
||||||
if (val) styles[prop] = val;
|
|
||||||
}
|
|
||||||
} catch { /* element may be detached */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
post({ type: 'ELEMENT_STYLES', nodeId, styles });
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Inline style patching ─────────────────────────────────────────────────
|
|
||||||
// Applies a single CSS property as an inline style on the component's DOM
|
|
||||||
// element. Non-destructive — does NOT modify source files. The change is
|
|
||||||
// immediately visible in the live render and can be recorded as a diff.
|
|
||||||
|
|
||||||
// ── Route discovery ───────────────────────────────────────────────────────
|
|
||||||
// Scans same-origin <a href> links in the current page and any React Router /
|
|
||||||
// Next.js Link components whose props contain an href, then posts the unique
|
|
||||||
// set of paths as ROUTES_DISCOVERED. Called once after the first React commit
|
|
||||||
// and again on every SPA navigation.
|
|
||||||
|
|
||||||
function humanLabel(path: string): string {
|
|
||||||
if (path === '/') return 'Home';
|
|
||||||
return path
|
|
||||||
.split('/')
|
|
||||||
.filter(Boolean)
|
|
||||||
.map((s) => s.charAt(0).toUpperCase() + s.slice(1).replace(/-/g, ' '))
|
|
||||||
.join(' / ');
|
|
||||||
}
|
|
||||||
|
|
||||||
function discoverRoutes(): void {
|
|
||||||
const seen = new Set<string>();
|
|
||||||
const routes: Array<{ path: string; label: string }> = [];
|
|
||||||
|
|
||||||
const addRoute = (path: string, hint?: string) => {
|
|
||||||
if (seen.has(path)) return;
|
|
||||||
// Skip hash-only anchors and external paths
|
|
||||||
if (!path || path.startsWith('#')) return;
|
|
||||||
seen.add(path);
|
|
||||||
routes.push({ path, label: hint?.trim().slice(0, 50) || humanLabel(path) });
|
|
||||||
};
|
|
||||||
|
|
||||||
// Current route first
|
|
||||||
addRoute(window.location.pathname, document.title || undefined);
|
|
||||||
|
|
||||||
// Scan real <a> elements — accept root-relative paths regardless of origin
|
|
||||||
// so that CLI-proxied pages (where links still point to the original domain)
|
|
||||||
// are handled correctly alongside direct same-origin connections.
|
|
||||||
document.querySelectorAll<HTMLAnchorElement>('a[href]').forEach((a) => {
|
|
||||||
try {
|
|
||||||
const rawHref = (a.getAttribute('href') ?? '').trim();
|
|
||||||
// Root-relative paths are always valid routes.
|
|
||||||
if (rawHref.startsWith('/')) {
|
|
||||||
addRoute(rawHref, a.textContent ?? undefined);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Absolute URLs — only add if same-origin (direct connection).
|
|
||||||
const url = new URL(a.href, window.location.href);
|
|
||||||
if (url.origin !== window.location.origin) return;
|
|
||||||
addRoute(url.pathname, a.textContent ?? undefined);
|
|
||||||
} catch { /* malformed href */ }
|
|
||||||
});
|
|
||||||
|
|
||||||
// Also scan fiber tree for Link / NavLink / next/link props
|
|
||||||
nodeMap.forEach(({ fiber }) => {
|
|
||||||
const name = fiber ? getDisplayName(fiber) : null;
|
|
||||||
if (name && /^(Link|NavLink|NextLink|RouterLink|a)$/.test(name)) {
|
|
||||||
const href = fiber?.memoizedProps?.['href'] ?? fiber?.memoizedProps?.['to'];
|
|
||||||
if (typeof href === 'string' && href.startsWith('/')) {
|
|
||||||
addRoute(href);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (routes.length > 0) post({ type: 'ROUTES_DISCOVERED', routes });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-discover after every React commit (covers lazy-loaded nav items)
|
|
||||||
const _originalCommit = hook.onCommitFiberRoot;
|
|
||||||
let routeDiscoveryScheduled = false;
|
|
||||||
const _wrappedCommitForRoutes = hook.onCommitFiberRoot;
|
|
||||||
void _wrappedCommitForRoutes; // suppress unused warning — keep original chain intact
|
|
||||||
|
|
||||||
// One-time discovery 800ms after first READY (DOM settled)
|
|
||||||
setTimeout(discoverRoutes, 800);
|
|
||||||
|
|
||||||
// Re-discover on every SPA navigation
|
|
||||||
window.addEventListener('popstate', () => {
|
|
||||||
if (!routeDiscoveryScheduled) {
|
|
||||||
routeDiscoveryScheduled = true;
|
|
||||||
setTimeout(() => { routeDiscoveryScheduled = false; discoverRoutes(); }, 300);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Element removal ───────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function removeElement(nodeId: string): void {
|
|
||||||
const info = nodeMap.get(nodeId);
|
|
||||||
if (!info?.fiber?.stateNode || typeof info.fiber.stateNode !== 'object'
|
|
||||||
|| !('nodeType' in (info.fiber.stateNode as object))) return;
|
|
||||||
try {
|
|
||||||
(info.fiber.stateNode as HTMLElement).style.setProperty('display', 'none');
|
|
||||||
} catch { /* detached */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
function patchElementStyle(nodeId: string, property: string, value: string): void {
|
|
||||||
const info = nodeMap.get(nodeId);
|
|
||||||
if (!info?.fiber?.stateNode || typeof info.fiber.stateNode !== 'object'
|
|
||||||
|| !('nodeType' in (info.fiber.stateNode as object))) return;
|
|
||||||
try {
|
|
||||||
const el = info.fiber.stateNode as HTMLElement;
|
|
||||||
if (value === '') {
|
|
||||||
el.style.removeProperty(property);
|
|
||||||
} else {
|
|
||||||
el.style.setProperty(property, value);
|
|
||||||
}
|
|
||||||
} catch { /* element may be detached */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyTokens(tokens: Record<string, string>): void {
|
|
||||||
const root = document.documentElement;
|
|
||||||
for (const [k, v] of Object.entries(tokens)) {
|
|
||||||
root.style.setProperty(k, v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function doNavigate(path: string): void {
|
|
||||||
try {
|
|
||||||
history.pushState(null, '', path);
|
|
||||||
window.dispatchEvent(new PopStateEvent('popstate', { state: null }));
|
|
||||||
} catch { /* navigation not available */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Ready signal ──────────────────────────────────────────────────────────
|
|
||||||
post({ type: 'READY' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Internal types ────────────────────────────────────────────────────────────
|
// ── Internal types ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
interface FiberLike {
|
interface FiberLike {
|
||||||
type: unknown;
|
type: unknown;
|
||||||
index: number;
|
index: number;
|
||||||
child: FiberLike | null;
|
child: FiberLike | null;
|
||||||
sibling: FiberLike | null;
|
sibling: FiberLike | null;
|
||||||
/** Parent fiber — needed for click-to-select chain walk via __reactFiber$ keys. */
|
return: FiberLike | null;
|
||||||
return: FiberLike | null;
|
stateNode: unknown;
|
||||||
stateNode: unknown;
|
|
||||||
memoizedProps: Record<string, unknown> | null;
|
memoizedProps: Record<string, unknown> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DomRect {
|
interface DomRect {
|
||||||
x: number;
|
x: number; y: number; width: number; height: number;
|
||||||
y: number;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SerializedNode {
|
interface SerializedNode {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
props: Record<string, string | number | boolean | null>;
|
props: Record<string, string | number | boolean | null>;
|
||||||
children: SerializedNode[];
|
children: SerializedNode[];
|
||||||
domRect?: DomRect;
|
domRect?: DomRect;
|
||||||
|
callSite?: { fileName: string; lineNumber: number; columnNumber?: number };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# @originmain/next
|
||||||
|
|
||||||
|
Next.js plugin for [Originmain](https://originmain.com) — automatically injects the Originmain live SDK before React loads, enabling the canvas to inspect your component tree in real time.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @originmain/next
|
||||||
|
# or
|
||||||
|
pnpm add @originmain/next
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Wrap your Next.js config with `withOriginmain`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// next.config.ts
|
||||||
|
import { withOriginmain } from '@originmain/next';
|
||||||
|
|
||||||
|
const nextConfig = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withOriginmain(nextConfig);
|
||||||
|
```
|
||||||
|
|
||||||
|
Or in CommonJS format:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// next.config.js
|
||||||
|
const { withOriginmain } = require('@originmain/next');
|
||||||
|
|
||||||
|
module.exports = withOriginmain({
|
||||||
|
reactStrictMode: true,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
1. Prepends `import '@originmain/live'` to **every page entry point** at build time via webpack entry modification.
|
||||||
|
2. The live SDK installs `__REACT_DEVTOOLS_GLOBAL_HOOK__` before React's module body evaluates — this is required for React to capture component commits.
|
||||||
|
3. The hook activates **only** when the page runs inside an Originmain artboard iframe. In all other contexts it is a complete no-op with zero runtime cost.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Next.js `>=14.0.0`
|
||||||
|
- Node.js `>=18`
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// ── @originmain/next build script ────────────────────────────────────────────
|
||||||
|
// Produces ESM + CJS bundles and TypeScript declarations under dist/:
|
||||||
|
//
|
||||||
|
// dist/index.js — ESM bundle for next.config.mjs / next.config.ts
|
||||||
|
// dist/index.cjs — CJS bundle for next.config.js (legacy require())
|
||||||
|
// dist/index.d.ts — TypeScript declarations for withOriginmain()
|
||||||
|
//
|
||||||
|
// Run: node build.mjs (or via "pnpm build")
|
||||||
|
|
||||||
|
import { build } from 'esbuild';
|
||||||
|
import { execFileSync } from 'node:child_process';
|
||||||
|
import { createRequire } from 'node:module';
|
||||||
|
|
||||||
|
const req = createRequire(import.meta.url);
|
||||||
|
// Resolve the tsc binary explicitly — avoids relying on PATH.
|
||||||
|
const tscBin = req.resolve('typescript/bin/tsc');
|
||||||
|
|
||||||
|
const SHARED = {
|
||||||
|
entryPoints: ['src/index.ts'],
|
||||||
|
bundle: true,
|
||||||
|
platform: 'node',
|
||||||
|
target: ['node18'],
|
||||||
|
// 'next' is a peer dep — leave it external so users' own copy is used.
|
||||||
|
external: ['next'],
|
||||||
|
logLevel: 'info',
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── ESM bundle ────────────────────────────────────────────────────────────────
|
||||||
|
await build({
|
||||||
|
...SHARED,
|
||||||
|
format: 'esm',
|
||||||
|
outfile: 'dist/index.js',
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── CJS bundle ────────────────────────────────────────────────────────────────
|
||||||
|
// Required for projects where next.config.js uses module.exports = ...
|
||||||
|
await build({
|
||||||
|
...SHARED,
|
||||||
|
format: 'cjs',
|
||||||
|
outfile: 'dist/index.cjs',
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── TypeScript declarations ───────────────────────────────────────────────────
|
||||||
|
// withOriginmain() is a typed export — consumers need the .d.ts so their IDE
|
||||||
|
// and type-checker know the function's signature.
|
||||||
|
execFileSync(process.execPath, [tscBin, '--project', 'tsconfig.build.json'], {
|
||||||
|
stdio: 'inherit',
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(' ✓ dist/index.js (ESM)');
|
||||||
|
console.log(' ✓ dist/index.cjs (CJS)');
|
||||||
|
console.log(' ✓ dist/index.d.ts (types)');
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "@originmain/next",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": false,
|
||||||
|
"description": "Originmain Next.js plugin — wraps next.config.js to auto-inject the live SDK before React loads.",
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"require": "./dist/index.cjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": ["dist", "README.md"],
|
||||||
|
"scripts": {
|
||||||
|
"build": "node build.mjs",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@originmain/live": "workspace:*"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"next": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"next": { "optional": false }
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"esbuild": "^0.25.0",
|
||||||
|
"typescript": "^5.5.0"
|
||||||
|
},
|
||||||
|
"keywords": ["originmain", "next", "nextjs", "react", "plugin"],
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
// ── @originmain/next ──────────────────────────────────────────────────────────
|
||||||
|
// Next.js plugin that injects @originmain/live before React loads, enabling
|
||||||
|
// the Originmain canvas to inspect React component trees in real-time.
|
||||||
|
//
|
||||||
|
// Usage (next.config.ts or next.config.js):
|
||||||
|
//
|
||||||
|
// import { withOriginmain } from '@originmain/next';
|
||||||
|
//
|
||||||
|
// const nextConfig = { /* your config */ };
|
||||||
|
// export default withOriginmain(nextConfig);
|
||||||
|
//
|
||||||
|
// What it does:
|
||||||
|
// 1. Prepends `import '@originmain/live'` to every page entry point at build
|
||||||
|
// time via webpack entry modification.
|
||||||
|
// 2. The live SDK installs __REACT_DEVTOOLS_GLOBAL_HOOK__ before React's module
|
||||||
|
// body evaluates, so React captures component commits from the very first render.
|
||||||
|
// 3. The hook is a complete no-op when the app is NOT running inside an
|
||||||
|
// Originmain artboard iframe — zero runtime cost in production.
|
||||||
|
//
|
||||||
|
// Environment:
|
||||||
|
// The SDK activates ONLY when the page is rendered inside an Originmain iframe
|
||||||
|
// (detected via URL fragment: #__om_artboard=<id>). No opt-in flag or env var
|
||||||
|
// needed — it self-activates in the right context and stays dormant otherwise.
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
type NextConfig = Record<string, any>;
|
||||||
|
|
||||||
|
/** The entry point that installs the Originmain fiber hook before React. */
|
||||||
|
const LIVE_SDK_ENTRY = '@originmain/live';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps a Next.js config to inject the Originmain live SDK into every page
|
||||||
|
* entry point. The SDK is a no-op outside Originmain artboard iframes.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* // next.config.ts
|
||||||
|
* import { withOriginmain } from '@originmain/next';
|
||||||
|
* export default withOriginmain({ reactStrictMode: true });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function withOriginmain(nextConfig: NextConfig = {}): NextConfig {
|
||||||
|
return {
|
||||||
|
...nextConfig,
|
||||||
|
|
||||||
|
webpack(
|
||||||
|
config: WebpackConfig,
|
||||||
|
context: { buildId: string; dev: boolean; isServer: boolean; nextRuntime?: string },
|
||||||
|
) {
|
||||||
|
// Only patch the client-side bundle. The fiber hook is browser-only.
|
||||||
|
// isServer covers both Node.js runtime and Edge runtime (nextRuntime).
|
||||||
|
if (!context.isServer) {
|
||||||
|
config.entry = prependLiveSdk(config.entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delegate to any existing webpack customisation in the user's config.
|
||||||
|
if (typeof nextConfig.webpack === 'function') {
|
||||||
|
return nextConfig.webpack(config, context);
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Entry prepend helper ──────────────────────────────────────────────────────
|
||||||
|
// Next.js entry can be a plain object, a function returning an object, or a
|
||||||
|
// function returning a Promise. We wrap all three shapes uniformly.
|
||||||
|
|
||||||
|
type EntryValue = string | string[] | EntryObject;
|
||||||
|
type EntryObject = Record<string, string | string[]>;
|
||||||
|
type EntryFn = () => EntryValue | Promise<EntryValue>;
|
||||||
|
type Entry = EntryValue | EntryFn;
|
||||||
|
|
||||||
|
function prependLiveSdk(entry: Entry): EntryFn {
|
||||||
|
return async () => {
|
||||||
|
const resolved = typeof entry === 'function' ? await entry() : entry;
|
||||||
|
return injectIntoEntries(resolved as EntryObject);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function injectIntoEntries(entries: EntryObject): EntryObject {
|
||||||
|
const out: EntryObject = {};
|
||||||
|
for (const [key, value] of Object.entries(entries)) {
|
||||||
|
out[key] = prependToChunk(value);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prependToChunk(chunk: string | string[]): string[] {
|
||||||
|
const arr = Array.isArray(chunk) ? chunk : [chunk];
|
||||||
|
// Avoid duplicating if already present (e.g. running withOriginmain twice).
|
||||||
|
if (arr.includes(LIVE_SDK_ENTRY)) return arr;
|
||||||
|
return [LIVE_SDK_ENTRY, ...arr];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Minimal webpack types (avoids adding webpack as a dev dep) ────────────────
|
||||||
|
|
||||||
|
interface WebpackConfig {
|
||||||
|
entry: Entry;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": false,
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
"declaration": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"module": "NodeNext"
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "preserve"
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
Generated
+287
@@ -255,6 +255,9 @@ importers:
|
|||||||
|
|
||||||
packages/live-sdk:
|
packages/live-sdk:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
esbuild:
|
||||||
|
specifier: ^0.25.0
|
||||||
|
version: 0.25.12
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.5.0
|
specifier: ^5.5.0
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
@@ -278,6 +281,22 @@ importers:
|
|||||||
specifier: ^5.5.0
|
specifier: ^5.5.0
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
|
|
||||||
|
packages/next:
|
||||||
|
dependencies:
|
||||||
|
'@originmain/live':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../live-sdk
|
||||||
|
next:
|
||||||
|
specifier: '>=14.0.0'
|
||||||
|
version: 15.5.15(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||||
|
devDependencies:
|
||||||
|
esbuild:
|
||||||
|
specifier: ^0.25.0
|
||||||
|
version: 0.25.12
|
||||||
|
typescript:
|
||||||
|
specifier: ^5.5.0
|
||||||
|
version: 5.9.3
|
||||||
|
|
||||||
packages/origin-graph:
|
packages/origin-graph:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@originmain/diff-engine':
|
'@originmain/diff-engine':
|
||||||
@@ -431,6 +450,12 @@ packages:
|
|||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [aix]
|
os: [aix]
|
||||||
|
|
||||||
|
'@esbuild/aix-ppc64@0.25.12':
|
||||||
|
resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [aix]
|
||||||
|
|
||||||
'@esbuild/aix-ppc64@0.28.0':
|
'@esbuild/aix-ppc64@0.28.0':
|
||||||
resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==}
|
resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -443,6 +468,12 @@ packages:
|
|||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [android]
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/android-arm64@0.25.12':
|
||||||
|
resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
'@esbuild/android-arm64@0.28.0':
|
'@esbuild/android-arm64@0.28.0':
|
||||||
resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==}
|
resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -455,6 +486,12 @@ packages:
|
|||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [android]
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/android-arm@0.25.12':
|
||||||
|
resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
'@esbuild/android-arm@0.28.0':
|
'@esbuild/android-arm@0.28.0':
|
||||||
resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==}
|
resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -467,6 +504,12 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [android]
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/android-x64@0.25.12':
|
||||||
|
resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
'@esbuild/android-x64@0.28.0':
|
'@esbuild/android-x64@0.28.0':
|
||||||
resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==}
|
resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -479,6 +522,12 @@ packages:
|
|||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
|
'@esbuild/darwin-arm64@0.25.12':
|
||||||
|
resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
'@esbuild/darwin-arm64@0.28.0':
|
'@esbuild/darwin-arm64@0.28.0':
|
||||||
resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==}
|
resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -491,6 +540,12 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
|
'@esbuild/darwin-x64@0.25.12':
|
||||||
|
resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
'@esbuild/darwin-x64@0.28.0':
|
'@esbuild/darwin-x64@0.28.0':
|
||||||
resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==}
|
resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -503,6 +558,12 @@ packages:
|
|||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [freebsd]
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@esbuild/freebsd-arm64@0.25.12':
|
||||||
|
resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
'@esbuild/freebsd-arm64@0.28.0':
|
'@esbuild/freebsd-arm64@0.28.0':
|
||||||
resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==}
|
resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -515,6 +576,12 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [freebsd]
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@esbuild/freebsd-x64@0.25.12':
|
||||||
|
resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
'@esbuild/freebsd-x64@0.28.0':
|
'@esbuild/freebsd-x64@0.28.0':
|
||||||
resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==}
|
resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -527,6 +594,12 @@ packages:
|
|||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-arm64@0.25.12':
|
||||||
|
resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
'@esbuild/linux-arm64@0.28.0':
|
'@esbuild/linux-arm64@0.28.0':
|
||||||
resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==}
|
resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -539,6 +612,12 @@ packages:
|
|||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-arm@0.25.12':
|
||||||
|
resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
'@esbuild/linux-arm@0.28.0':
|
'@esbuild/linux-arm@0.28.0':
|
||||||
resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==}
|
resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -551,6 +630,12 @@ packages:
|
|||||||
cpu: [ia32]
|
cpu: [ia32]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-ia32@0.25.12':
|
||||||
|
resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
'@esbuild/linux-ia32@0.28.0':
|
'@esbuild/linux-ia32@0.28.0':
|
||||||
resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==}
|
resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -563,6 +648,12 @@ packages:
|
|||||||
cpu: [loong64]
|
cpu: [loong64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-loong64@0.25.12':
|
||||||
|
resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [loong64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
'@esbuild/linux-loong64@0.28.0':
|
'@esbuild/linux-loong64@0.28.0':
|
||||||
resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==}
|
resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -575,6 +666,12 @@ packages:
|
|||||||
cpu: [mips64el]
|
cpu: [mips64el]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-mips64el@0.25.12':
|
||||||
|
resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [mips64el]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
'@esbuild/linux-mips64el@0.28.0':
|
'@esbuild/linux-mips64el@0.28.0':
|
||||||
resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==}
|
resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -587,6 +684,12 @@ packages:
|
|||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-ppc64@0.25.12':
|
||||||
|
resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
'@esbuild/linux-ppc64@0.28.0':
|
'@esbuild/linux-ppc64@0.28.0':
|
||||||
resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==}
|
resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -599,6 +702,12 @@ packages:
|
|||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-riscv64@0.25.12':
|
||||||
|
resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
'@esbuild/linux-riscv64@0.28.0':
|
'@esbuild/linux-riscv64@0.28.0':
|
||||||
resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==}
|
resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -611,6 +720,12 @@ packages:
|
|||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-s390x@0.25.12':
|
||||||
|
resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
'@esbuild/linux-s390x@0.28.0':
|
'@esbuild/linux-s390x@0.28.0':
|
||||||
resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==}
|
resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -623,12 +738,24 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-x64@0.25.12':
|
||||||
|
resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
'@esbuild/linux-x64@0.28.0':
|
'@esbuild/linux-x64@0.28.0':
|
||||||
resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==}
|
resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/netbsd-arm64@0.25.12':
|
||||||
|
resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [netbsd]
|
||||||
|
|
||||||
'@esbuild/netbsd-arm64@0.28.0':
|
'@esbuild/netbsd-arm64@0.28.0':
|
||||||
resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==}
|
resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -641,12 +768,24 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [netbsd]
|
os: [netbsd]
|
||||||
|
|
||||||
|
'@esbuild/netbsd-x64@0.25.12':
|
||||||
|
resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [netbsd]
|
||||||
|
|
||||||
'@esbuild/netbsd-x64@0.28.0':
|
'@esbuild/netbsd-x64@0.28.0':
|
||||||
resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==}
|
resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [netbsd]
|
os: [netbsd]
|
||||||
|
|
||||||
|
'@esbuild/openbsd-arm64@0.25.12':
|
||||||
|
resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [openbsd]
|
||||||
|
|
||||||
'@esbuild/openbsd-arm64@0.28.0':
|
'@esbuild/openbsd-arm64@0.28.0':
|
||||||
resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==}
|
resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -659,12 +798,24 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [openbsd]
|
os: [openbsd]
|
||||||
|
|
||||||
|
'@esbuild/openbsd-x64@0.25.12':
|
||||||
|
resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [openbsd]
|
||||||
|
|
||||||
'@esbuild/openbsd-x64@0.28.0':
|
'@esbuild/openbsd-x64@0.28.0':
|
||||||
resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==}
|
resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [openbsd]
|
os: [openbsd]
|
||||||
|
|
||||||
|
'@esbuild/openharmony-arm64@0.25.12':
|
||||||
|
resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [openharmony]
|
||||||
|
|
||||||
'@esbuild/openharmony-arm64@0.28.0':
|
'@esbuild/openharmony-arm64@0.28.0':
|
||||||
resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==}
|
resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -677,6 +828,12 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [sunos]
|
os: [sunos]
|
||||||
|
|
||||||
|
'@esbuild/sunos-x64@0.25.12':
|
||||||
|
resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [sunos]
|
||||||
|
|
||||||
'@esbuild/sunos-x64@0.28.0':
|
'@esbuild/sunos-x64@0.28.0':
|
||||||
resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==}
|
resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -689,6 +846,12 @@ packages:
|
|||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@esbuild/win32-arm64@0.25.12':
|
||||||
|
resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
'@esbuild/win32-arm64@0.28.0':
|
'@esbuild/win32-arm64@0.28.0':
|
||||||
resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==}
|
resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -701,6 +864,12 @@ packages:
|
|||||||
cpu: [ia32]
|
cpu: [ia32]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@esbuild/win32-ia32@0.25.12':
|
||||||
|
resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
'@esbuild/win32-ia32@0.28.0':
|
'@esbuild/win32-ia32@0.28.0':
|
||||||
resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==}
|
resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -713,6 +882,12 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@esbuild/win32-x64@0.25.12':
|
||||||
|
resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
'@esbuild/win32-x64@0.28.0':
|
'@esbuild/win32-x64@0.28.0':
|
||||||
resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==}
|
resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -2145,6 +2320,11 @@ packages:
|
|||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
esbuild@0.25.12:
|
||||||
|
resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
esbuild@0.28.0:
|
esbuild@0.28.0:
|
||||||
resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==}
|
resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -2979,147 +3159,225 @@ snapshots:
|
|||||||
'@esbuild/aix-ppc64@0.21.5':
|
'@esbuild/aix-ppc64@0.21.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/aix-ppc64@0.25.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/aix-ppc64@0.28.0':
|
'@esbuild/aix-ppc64@0.28.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@esbuild/android-arm64@0.21.5':
|
'@esbuild/android-arm64@0.21.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-arm64@0.25.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/android-arm64@0.28.0':
|
'@esbuild/android-arm64@0.28.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@esbuild/android-arm@0.21.5':
|
'@esbuild/android-arm@0.21.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-arm@0.25.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/android-arm@0.28.0':
|
'@esbuild/android-arm@0.28.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@esbuild/android-x64@0.21.5':
|
'@esbuild/android-x64@0.21.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-x64@0.25.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/android-x64@0.28.0':
|
'@esbuild/android-x64@0.28.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@esbuild/darwin-arm64@0.21.5':
|
'@esbuild/darwin-arm64@0.21.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/darwin-arm64@0.25.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/darwin-arm64@0.28.0':
|
'@esbuild/darwin-arm64@0.28.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@esbuild/darwin-x64@0.21.5':
|
'@esbuild/darwin-x64@0.21.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/darwin-x64@0.25.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/darwin-x64@0.28.0':
|
'@esbuild/darwin-x64@0.28.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@esbuild/freebsd-arm64@0.21.5':
|
'@esbuild/freebsd-arm64@0.21.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/freebsd-arm64@0.25.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/freebsd-arm64@0.28.0':
|
'@esbuild/freebsd-arm64@0.28.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@esbuild/freebsd-x64@0.21.5':
|
'@esbuild/freebsd-x64@0.21.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/freebsd-x64@0.25.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/freebsd-x64@0.28.0':
|
'@esbuild/freebsd-x64@0.28.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@esbuild/linux-arm64@0.21.5':
|
'@esbuild/linux-arm64@0.21.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-arm64@0.25.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/linux-arm64@0.28.0':
|
'@esbuild/linux-arm64@0.28.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@esbuild/linux-arm@0.21.5':
|
'@esbuild/linux-arm@0.21.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-arm@0.25.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/linux-arm@0.28.0':
|
'@esbuild/linux-arm@0.28.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@esbuild/linux-ia32@0.21.5':
|
'@esbuild/linux-ia32@0.21.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-ia32@0.25.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/linux-ia32@0.28.0':
|
'@esbuild/linux-ia32@0.28.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@esbuild/linux-loong64@0.21.5':
|
'@esbuild/linux-loong64@0.21.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-loong64@0.25.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/linux-loong64@0.28.0':
|
'@esbuild/linux-loong64@0.28.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@esbuild/linux-mips64el@0.21.5':
|
'@esbuild/linux-mips64el@0.21.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-mips64el@0.25.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/linux-mips64el@0.28.0':
|
'@esbuild/linux-mips64el@0.28.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@esbuild/linux-ppc64@0.21.5':
|
'@esbuild/linux-ppc64@0.21.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-ppc64@0.25.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/linux-ppc64@0.28.0':
|
'@esbuild/linux-ppc64@0.28.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@esbuild/linux-riscv64@0.21.5':
|
'@esbuild/linux-riscv64@0.21.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-riscv64@0.25.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/linux-riscv64@0.28.0':
|
'@esbuild/linux-riscv64@0.28.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@esbuild/linux-s390x@0.21.5':
|
'@esbuild/linux-s390x@0.21.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-s390x@0.25.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/linux-s390x@0.28.0':
|
'@esbuild/linux-s390x@0.28.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@esbuild/linux-x64@0.21.5':
|
'@esbuild/linux-x64@0.21.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-x64@0.25.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/linux-x64@0.28.0':
|
'@esbuild/linux-x64@0.28.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/netbsd-arm64@0.25.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/netbsd-arm64@0.28.0':
|
'@esbuild/netbsd-arm64@0.28.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@esbuild/netbsd-x64@0.21.5':
|
'@esbuild/netbsd-x64@0.21.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/netbsd-x64@0.25.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/netbsd-x64@0.28.0':
|
'@esbuild/netbsd-x64@0.28.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/openbsd-arm64@0.25.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/openbsd-arm64@0.28.0':
|
'@esbuild/openbsd-arm64@0.28.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@esbuild/openbsd-x64@0.21.5':
|
'@esbuild/openbsd-x64@0.21.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/openbsd-x64@0.25.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/openbsd-x64@0.28.0':
|
'@esbuild/openbsd-x64@0.28.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/openharmony-arm64@0.25.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/openharmony-arm64@0.28.0':
|
'@esbuild/openharmony-arm64@0.28.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@esbuild/sunos-x64@0.21.5':
|
'@esbuild/sunos-x64@0.21.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/sunos-x64@0.25.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/sunos-x64@0.28.0':
|
'@esbuild/sunos-x64@0.28.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@esbuild/win32-arm64@0.21.5':
|
'@esbuild/win32-arm64@0.21.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-arm64@0.25.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/win32-arm64@0.28.0':
|
'@esbuild/win32-arm64@0.28.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@esbuild/win32-ia32@0.21.5':
|
'@esbuild/win32-ia32@0.21.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-ia32@0.25.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/win32-ia32@0.28.0':
|
'@esbuild/win32-ia32@0.28.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@esbuild/win32-x64@0.21.5':
|
'@esbuild/win32-x64@0.21.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-x64@0.25.12':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/win32-x64@0.28.0':
|
'@esbuild/win32-x64@0.28.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -5107,6 +5365,35 @@ snapshots:
|
|||||||
'@esbuild/win32-ia32': 0.21.5
|
'@esbuild/win32-ia32': 0.21.5
|
||||||
'@esbuild/win32-x64': 0.21.5
|
'@esbuild/win32-x64': 0.21.5
|
||||||
|
|
||||||
|
esbuild@0.25.12:
|
||||||
|
optionalDependencies:
|
||||||
|
'@esbuild/aix-ppc64': 0.25.12
|
||||||
|
'@esbuild/android-arm': 0.25.12
|
||||||
|
'@esbuild/android-arm64': 0.25.12
|
||||||
|
'@esbuild/android-x64': 0.25.12
|
||||||
|
'@esbuild/darwin-arm64': 0.25.12
|
||||||
|
'@esbuild/darwin-x64': 0.25.12
|
||||||
|
'@esbuild/freebsd-arm64': 0.25.12
|
||||||
|
'@esbuild/freebsd-x64': 0.25.12
|
||||||
|
'@esbuild/linux-arm': 0.25.12
|
||||||
|
'@esbuild/linux-arm64': 0.25.12
|
||||||
|
'@esbuild/linux-ia32': 0.25.12
|
||||||
|
'@esbuild/linux-loong64': 0.25.12
|
||||||
|
'@esbuild/linux-mips64el': 0.25.12
|
||||||
|
'@esbuild/linux-ppc64': 0.25.12
|
||||||
|
'@esbuild/linux-riscv64': 0.25.12
|
||||||
|
'@esbuild/linux-s390x': 0.25.12
|
||||||
|
'@esbuild/linux-x64': 0.25.12
|
||||||
|
'@esbuild/netbsd-arm64': 0.25.12
|
||||||
|
'@esbuild/netbsd-x64': 0.25.12
|
||||||
|
'@esbuild/openbsd-arm64': 0.25.12
|
||||||
|
'@esbuild/openbsd-x64': 0.25.12
|
||||||
|
'@esbuild/openharmony-arm64': 0.25.12
|
||||||
|
'@esbuild/sunos-x64': 0.25.12
|
||||||
|
'@esbuild/win32-arm64': 0.25.12
|
||||||
|
'@esbuild/win32-ia32': 0.25.12
|
||||||
|
'@esbuild/win32-x64': 0.25.12
|
||||||
|
|
||||||
esbuild@0.28.0:
|
esbuild@0.28.0:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@esbuild/aix-ppc64': 0.28.0
|
'@esbuild/aix-ppc64': 0.28.0
|
||||||
|
|||||||
+2
-1
@@ -22,7 +22,8 @@
|
|||||||
"@originmain/multiplayer": ["./packages/multiplayer/src/index.ts"],
|
"@originmain/multiplayer": ["./packages/multiplayer/src/index.ts"],
|
||||||
"@originmain/platform": ["./packages/platform/src/index.ts"],
|
"@originmain/platform": ["./packages/platform/src/index.ts"],
|
||||||
"@originmain/cli": ["./packages/cli/src/index.ts"],
|
"@originmain/cli": ["./packages/cli/src/index.ts"],
|
||||||
"@originmain/live": ["./packages/live-sdk/src/index.ts"]
|
"@originmain/live": ["./packages/live-sdk/src/index.ts"],
|
||||||
|
"@originmain/next": ["./packages/next/src/index.ts"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user