updated stuff

This commit is contained in:
SinachPat
2026-05-13 15:12:51 +01:00
parent 8356e6278c
commit 7bf43fc481
19 changed files with 1632 additions and 308 deletions
+478
View File
@@ -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 59 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*
+1
View File
@@ -8,6 +8,7 @@
"lint": "eslint . --max-warnings 0",
"test": "vitest run --coverage",
"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",
"cli:build": "pnpm --filter @originmain/cli run build",
"cli:dev": "node packages/cli/dist/cli.js dev",
@@ -492,7 +492,7 @@ export function Artboard({
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 && (
<div style={{
position: 'absolute', bottom: 0, left: 0, right: 0,
@@ -505,7 +505,9 @@ export function Artboard({
fontFamily: "'JetBrains Mono', monospace", fontSize: '0.5875rem',
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 &quot;@originmain/live&quot;</strong>
{' '}before React in your app, then redeploy.
</span>
</div>
)}
+21 -8
View File
@@ -472,9 +472,10 @@ export function Canvas() {
}
/* ── URL onboarding overlay ───────────────────────────────── */
// Shown when the canvas has no artboards. Lets the user paste their CLI proxy
// URL to auto-create the first artboard; route discovery will then fire and
// populate the remaining pages automatically.
// Shown when the canvas has no artboards. Lets the user paste their app URL
// (Vercel, Netlify, or any deployment where @originmain/live is installed).
// Route discovery fires after the first React commit and populates remaining
// pages automatically.
function UrlOnboardingOverlay({
workspaceId,
projectId,
@@ -547,18 +548,30 @@ function UrlOnboardingOverlay({
Connect your app
</div>
<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&apos;s pages will be auto-rendered as artboards
Paste your app URL all your pages will be auto-rendered as artboards
</div>
</div>
{/* CLI hint */}
{/* SDK install hint */}
<div style={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 4 }}>
<div style={{
width: '100%', padding: '6px 10px',
padding: '6px 10px',
background: 'rgba(51,133,255,0.08)', border: '1px solid rgba(51,133,255,0.18)',
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>
{/* URL input */}
@@ -569,7 +582,7 @@ function UrlOnboardingOverlay({
value={url}
onChange={e => { setUrl(e.target.value); setErrorMsg(''); }}
onKeyDown={e => { if (e.key === 'Enter') void handleConnect(); e.stopPropagation(); }}
placeholder="http://localhost:4170"
placeholder="https://your-app.vercel.app"
style={{
flex: 1, background: 'rgba(255,255,255,0.05)',
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 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(() => {
const mine = styleEditQueue.filter((e) => e.artboardId === id);
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 });
}
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]);
// ── 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 { useCanvas } from '@/store/canvas';
import { useCanvasTheme } from '@/store/canvasTheme';
import { useHistory } from '@/store/history';
import { useDlf } from '@/hooks/useDlf';
import { checkComponentConstraints } from '@originmain/design-language';
import type { Violation } from '@originmain/design-language';
@@ -92,11 +93,13 @@ export function DesignTab({
const {
patchStyleEdit,
patchChildrenStyleEdit,
setComponentStyles,
indexerStatus,
selectedComponentHasDirectText,
selectedComponentHasParagraphChildren,
setActiveViolations,
} = useCanvas();
const { pushEdit } = useHistory();
const { dlf } = useDlf(workspaceId);
// 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) => {
if (!artboardId || !componentId) return;
// 1. Send to iframe.
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) => {
File diff suppressed because one or more lines are too long
+40
View File
@@ -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
+35
View File
@@ -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)');
+4 -2
View File
@@ -5,13 +5,15 @@
"description": "Originmain live rendering SDK — installs fiber hook for component inspection. Import before React.",
"type": "module",
"exports": {
".": "./src/index.ts"
".": "./dist/index.js"
},
"files": ["src", "README.md"],
"files": ["dist", "README.md"],
"scripts": {
"build": "node build.mjs",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"esbuild": "^0.25.0",
"typescript": "^5.5.0"
},
"keywords": ["originmain", "react", "devtools", "fiber", "design-engineering"],
+391 -233
View File
@@ -3,46 +3,131 @@
// module body. React checks for __REACT_DEVTOOLS_GLOBAL_HOOK__ exactly once at
// import time; any later installation is too late.
//
// Full bidirectional protocol:
// Renderer → Host : READY, FIBER_TREE_UPDATE, COMPONENT_SELECTED, COMPONENT_DESELECTED, ERROR
// Host → Renderer : SET_DESIGN_TOKENS, NAVIGATE, SELECT_COMPONENT, DESELECT
// Full bidirectional protocol (matches packages/renderer/src/protocol.ts):
// Renderer → Host : READY, FIBER_TREE_UPDATE, COMPONENT_SELECTED,
// 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/…"
// This survives re-renders as long as the component tree structure is unchanged.
// Activation guard — checked in priority order:
// 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
// LiveArtboard.tsx on the <iframe name="om:{id}"> element. Outside Originmain
// iframes this module is a complete no-op.
// Chrome 88+ strips window.name on cross-origin iframe loads (Spectre
// mitigation). The URL fragment approach is immune to this because LiveArtboard
// 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
// it ships as a public npm package and must work standalone.
// This file is intentionally self-contained (no @originmain/* imports) so it
// ships as a standalone npm package without workspace dependencies.
const RENDERER_SOURCE = 'originmain-renderer';
const HOST_SOURCE = 'originmain-host';
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 {
return (
window.parent !== window &&
typeof window.name === 'string' &&
window.name.startsWith(NAME_PREFIX)
);
} catch {
return false; // Accessing window.parent can throw in certain sandboxed contexts.
const match = window.location.hash.match(/__om_artboard=([^&]+)/);
if (match?.[1]) return decodeURIComponent(match[1]);
} catch { /* */ }
// 2. window.name: "om:<id>" (works for same-origin iframes)
try {
if (typeof window.name === 'string' && window.name.startsWith(NAME_PREFIX)) {
return window.name.slice(NAME_PREFIX.length);
}
} catch { /* */ }
return null;
}
if (isOriginmainIframe()) {
installFiberHook();
}
// ── 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).
// ── Core ──────────────────────────────────────────────────────────────────────
(function bootstrap() {
// Not in a frame at all — complete no-op.
try { if (window.parent === window) return; }
catch { return; }
function installFiberHook(): void {
const artboardId = window.name.slice(NAME_PREFIX.length);
// ── 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;
}
// ── Resolve artboard ID ──────────────────────────────────────────────────
const syncId = resolveArtboardIdSync();
if (syncId) {
startMainLoop(hook, syncId);
return;
}
// 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 onHandshakeReply(event: MessageEvent) {
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 ────────────────────────────────────────────────────
function post(msg: Record<string, unknown>): void {
@@ -51,88 +136,50 @@ function installFiberHook(): void {
{ source: RENDERER_SOURCE, artboardId, message: msg },
'*',
);
} catch {
// Parent frame unreachable — silently ignore.
}
} catch { /* parent frame unreachable */ }
}
// ── Runtime state ─────────────────────────────────────────────────────────
// nodeMap: nodeId → { domRect (snapshot), fiber (live reference for re-measurement) }
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 selectedNodeId: string | 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;
hook.onCommitFiberRoot = function onCommitFiberRoot(...args: unknown[]) {
// Delegate to any pre-existing handler (React DevTools extension) first.
if (typeof _prevCommit === 'function') {
try { _prevCommit.apply(this, args); }
catch { /* don't break existing DevTools */ }
try { _prevCommit.apply(this, args); } catch { /* don't break existing DevTools */ }
}
try {
// args[1] is the FiberRoot object — { current: Fiber }
const root = args[1] as { current: FiberLike } | undefined;
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();
fiberMap = new WeakMap();
const tree = serializeFiber(root.current, '');
post({ type: 'FIBER_TREE_UPDATE', root: tree });
// Re-sync the highlight ring after each React commit (component may move).
if (selectedNodeId) updateHighlight();
} catch (err) {
post({ type: 'ERROR', message: String(err) });
}
};
// ── Fiber serialization (stable path-based IDs) ───────────────────────────
// 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.
// ── Fiber serialization ───────────────────────────────────────────────────
function serializeFiber(
fiber: FiberLike | null,
parentId: string,
): SerializedNode | null {
function serializeFiber(fiber: FiberLike | null, parentId: string): SerializedNode | null {
if (!fiber) return null;
const name = getDisplayName(fiber);
if (!name) {
// Unnamed root fiber (HostRoot) — collectChildren handles Fragment recursively.
const children: SerializedNode[] = [];
collectChildren(fiber, parentId, children);
if (children.length === 1) return children[0] ?? 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 };
}
@@ -146,7 +193,16 @@ function installFiberHook(): void {
};
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 });
fiberMap.set(fiber, nodeId);
@@ -154,18 +210,14 @@ function installFiberHook(): void {
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 {
let child = fiber.child;
while (child) {
const name = getDisplayName(child);
if (name) {
const serialized = serializeFiber(child, parentId);
if (serialized) out.push(serialized);
const s = serializeFiber(child, parentId);
if (s) out.push(s);
} else {
// Unnamed (Fragment / Context / Provider / forwardRef wrapper etc.):
// mark as non-selectable and flatten children directly into our level.
fiberMap.set(child, null);
collectChildren(child, parentId, out);
}
@@ -217,15 +269,11 @@ function installFiberHook(): void {
return out;
}
// ── Highlight overlay (blue ring inside the iframe) ───────────────────────
// updateHighlight re-measures from the live fiber stateNode so the ring stays
// accurate even after scroll (between React commits).
// ── Highlight overlay ─────────────────────────────────────────────────────
function updateHighlight(): void {
const info = selectedNodeId ? nodeMap.get(selectedNodeId) : undefined;
if (!info) { removeHighlight(); return; }
// Re-measure from the live DOM element for scroll accuracy.
const rect = getDomRect(info.fiber) ?? info.domRect;
if (rect && rect.width > 0 && rect.height > 0) {
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', () => {
if (selectedNodeId) updateHighlight();
}, true);
// ── Click-to-select (capturing phase) ────────────────────────────────────
// Uses document.elementFromPoint to get the live DOM element at the click
// position (accurate even after scroll), then walks the React fiber tree
// upward via __reactFiber$ keys to find the nearest tracked component.
// ── Click-to-select ───────────────────────────────────────────────────────
function getFiberKey(el: Element): string | null {
const keys = Object.keys(el);
for (let i = 0; i < keys.length; i++) {
const k = keys[i];
if (k !== undefined && k.startsWith('__reactFiber$')) return k;
if (k?.startsWith('__reactFiber$')) return k;
}
return null;
}
document.addEventListener('click', (event: MouseEvent) => {
// Ignore clicks on our own highlight overlay.
if (event.target === highlightEl) return;
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;
while (current && current !== document.documentElement) {
const fiberKey = getFiberKey(current);
if (fiberKey) {
// Walk the fiber's return (parent) chain to find the nearest tracked node.
let fiber: FiberLike | null =
(current as unknown as Record<string, unknown>)[fiberKey] as FiberLike | null;
while (fiber) {
const nodeId = fiberMap.get(fiber);
if (nodeId) {
// Re-measure from the live element for accurate post-scroll rect.
const liveEl = fiber.stateNode;
if (liveEl && typeof liveEl === 'object' && 'getBoundingClientRect' in liveEl) {
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;
}
}
@@ -312,78 +354,19 @@ function installFiberHook(): void {
current = current.parentElement;
}
// Nothing found — clear the selection.
post({ type: 'COMPONENT_DESELECTED' });
}, true);
// ── Host → Renderer message handler ──────────────────────────────────────
window.addEventListener('message', (event: MessageEvent) => {
const data = event.data as {
source?: string;
artboardId?: string;
message?: {
type: string;
tokens?: Record<string, string>;
path?: string;
nodeId?: string;
property?: string;
value?: string | undefined;
};
};
if (!data || data.source !== HOST_SOURCE) return;
if (data.artboardId !== artboardId) return;
const msg = data.message;
if (!msg) return;
switch (msg.type) {
case 'SET_DESIGN_TOKENS':
if (msg.tokens) applyTokens(msg.tokens);
break;
case 'NAVIGATE':
if (msg.path) doNavigate(msg.path);
break;
case 'SELECT_COMPONENT':
if (msg.nodeId) {
selectedNodeId = msg.nodeId;
updateHighlight();
}
break;
case 'DESELECT':
selectedNodeId = null;
removeHighlight();
break;
case 'REQUEST_ELEMENT_STYLES':
if (msg.nodeId) respondWithStyles(msg.nodeId);
break;
case 'PATCH_ELEMENT_STYLE':
if (msg.nodeId && msg.property && msg.value !== undefined) {
patchElementStyle(msg.nodeId, msg.property, msg.value ?? '');
}
break;
case 'REMOVE_ELEMENT':
if (msg.nodeId) removeElement(msg.nodeId);
break;
}
});
// ── Element style inspection ──────────────────────────────────────────────
// 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 = [
// Typography
'color', 'font-family', 'font-size', 'font-weight', 'line-height',
'letter-spacing', 'text-align', 'text-transform', 'text-decoration',
// Layout
'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',
// Visual
'background-color', 'border-radius', 'opacity',
'box-shadow', 'border-width', 'border-color', 'border-style',
'overflow', 'cursor', 'transition',
@@ -392,31 +375,198 @@ function installFiberHook(): void {
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 computed = window.getComputedStyle(info.fiber.stateNode as Element);
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 });
post({ type: 'ELEMENT_STYLES', nodeId, styles, hasDirectText, hasParagraphChildren });
}
// ── 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.
// ── 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 ───────────────────────────────────────────────────────
// 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';
@@ -432,58 +582,36 @@ function installFiberHook(): void {
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;
if (!path || path.startsWith('#') || seen.has(path)) 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 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 */ }
});
// 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 (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;
@@ -491,47 +619,80 @@ function installFiberHook(): void {
}
});
// ── Element removal ───────────────────────────────────────────────────────
// ── Host → Renderer message handler ──────────────────────────────────────
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;
window.addEventListener('message', (event: MessageEvent) => {
const data = event.data as {
source?: string;
artboardId?: string;
message?: {
type: string;
tokens?: Record<string, string>;
path?: string;
nodeId?: string;
parentNodeId?: string;
selector?: string;
property?: string;
value?: string;
};
};
if (!data || data.source !== HOST_SOURCE) return;
if (data.artboardId !== artboardId) return;
const msg = data.message;
if (!msg) return;
switch (msg.type) {
case 'SET_DESIGN_TOKENS':
if (msg.tokens) applyTokens(msg.tokens);
break;
case 'NAVIGATE':
if (msg.path) doNavigate(msg.path);
break;
case 'SELECT_COMPONENT':
if (msg.nodeId) { selectedNodeId = msg.nodeId; updateHighlight(); }
break;
case 'DESELECT':
selectedNodeId = null;
removeHighlight();
break;
case 'REQUEST_ELEMENT_STYLES':
if (msg.nodeId) respondWithStyles(msg.nodeId);
break;
case 'PATCH_ELEMENT_STYLE':
if (msg.nodeId && msg.property && msg.value !== undefined) {
patchElementStyle(msg.nodeId, msg.property, msg.value ?? '');
}
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':
if (msg.nodeId) removeElement(msg.nodeId);
break;
case 'CAPTURE_THUMBNAIL':
void captureThumbnail();
break;
case 'CAPTURE_SNAPSHOT':
if (msg.nodeId) void captureSnapshot(msg.nodeId);
break;
case 'CANCEL_SNAPSHOT':
snapshotAborted = true;
break;
}
});
// ── READY signal ──────────────────────────────────────────────────────────
let rootFontSizePx: number | undefined;
try {
(info.fiber.stateNode as HTMLElement).style.setProperty('display', 'none');
} catch { /* detached */ }
}
const computed = window.getComputedStyle(document.documentElement);
const parsed = parseFloat(computed.fontSize);
if (!isNaN(parsed)) rootFontSizePx = parsed;
} catch { /* */ }
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' });
post({ type: 'READY', rootFontSizePx });
}
// ── Internal types ────────────────────────────────────────────────────────────
@@ -541,17 +702,13 @@ interface FiberLike {
index: number;
child: FiberLike | null;
sibling: FiberLike | null;
/** Parent fiber — needed for click-to-select chain walk via __reactFiber$ keys. */
return: FiberLike | null;
stateNode: unknown;
memoizedProps: Record<string, unknown> | null;
}
interface DomRect {
x: number;
y: number;
width: number;
height: number;
x: number; y: number; width: number; height: number;
}
interface SerializedNode {
@@ -560,4 +717,5 @@ interface SerializedNode {
props: Record<string, string | number | boolean | null>;
children: SerializedNode[];
domRect?: DomRect;
callSite?: { fileName: string; lineNumber: number; columnNumber?: number };
}
+52
View File
@@ -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
+53
View File
@@ -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)');
+34
View File
@@ -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"
}
+102
View File
@@ -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;
}
+14
View File
@@ -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"]
}
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"noEmit": true,
"jsx": "preserve"
},
"include": ["src"],
"exclude": ["node_modules"]
}
+287
View File
@@ -255,6 +255,9 @@ importers:
packages/live-sdk:
devDependencies:
esbuild:
specifier: ^0.25.0
version: 0.25.12
typescript:
specifier: ^5.5.0
version: 5.9.3
@@ -278,6 +281,22 @@ importers:
specifier: ^5.5.0
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:
dependencies:
'@originmain/diff-engine':
@@ -431,6 +450,12 @@ packages:
cpu: [ppc64]
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':
resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==}
engines: {node: '>=18'}
@@ -443,6 +468,12 @@ packages:
cpu: [arm64]
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':
resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==}
engines: {node: '>=18'}
@@ -455,6 +486,12 @@ packages:
cpu: [arm]
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':
resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==}
engines: {node: '>=18'}
@@ -467,6 +504,12 @@ packages:
cpu: [x64]
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':
resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==}
engines: {node: '>=18'}
@@ -479,6 +522,12 @@ packages:
cpu: [arm64]
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':
resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==}
engines: {node: '>=18'}
@@ -491,6 +540,12 @@ packages:
cpu: [x64]
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':
resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==}
engines: {node: '>=18'}
@@ -503,6 +558,12 @@ packages:
cpu: [arm64]
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':
resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==}
engines: {node: '>=18'}
@@ -515,6 +576,12 @@ packages:
cpu: [x64]
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':
resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==}
engines: {node: '>=18'}
@@ -527,6 +594,12 @@ packages:
cpu: [arm64]
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':
resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==}
engines: {node: '>=18'}
@@ -539,6 +612,12 @@ packages:
cpu: [arm]
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':
resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==}
engines: {node: '>=18'}
@@ -551,6 +630,12 @@ packages:
cpu: [ia32]
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':
resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==}
engines: {node: '>=18'}
@@ -563,6 +648,12 @@ packages:
cpu: [loong64]
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':
resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==}
engines: {node: '>=18'}
@@ -575,6 +666,12 @@ packages:
cpu: [mips64el]
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':
resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==}
engines: {node: '>=18'}
@@ -587,6 +684,12 @@ packages:
cpu: [ppc64]
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':
resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==}
engines: {node: '>=18'}
@@ -599,6 +702,12 @@ packages:
cpu: [riscv64]
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':
resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==}
engines: {node: '>=18'}
@@ -611,6 +720,12 @@ packages:
cpu: [s390x]
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':
resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==}
engines: {node: '>=18'}
@@ -623,12 +738,24 @@ packages:
cpu: [x64]
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':
resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==}
engines: {node: '>=18'}
cpu: [x64]
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':
resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==}
engines: {node: '>=18'}
@@ -641,12 +768,24 @@ packages:
cpu: [x64]
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':
resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==}
engines: {node: '>=18'}
cpu: [x64]
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':
resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==}
engines: {node: '>=18'}
@@ -659,12 +798,24 @@ packages:
cpu: [x64]
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':
resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==}
engines: {node: '>=18'}
cpu: [x64]
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':
resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==}
engines: {node: '>=18'}
@@ -677,6 +828,12 @@ packages:
cpu: [x64]
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':
resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==}
engines: {node: '>=18'}
@@ -689,6 +846,12 @@ packages:
cpu: [arm64]
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':
resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==}
engines: {node: '>=18'}
@@ -701,6 +864,12 @@ packages:
cpu: [ia32]
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':
resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==}
engines: {node: '>=18'}
@@ -713,6 +882,12 @@ packages:
cpu: [x64]
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':
resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==}
engines: {node: '>=18'}
@@ -2145,6 +2320,11 @@ packages:
engines: {node: '>=12'}
hasBin: true
esbuild@0.25.12:
resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
engines: {node: '>=18'}
hasBin: true
esbuild@0.28.0:
resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==}
engines: {node: '>=18'}
@@ -2979,147 +3159,225 @@ snapshots:
'@esbuild/aix-ppc64@0.21.5':
optional: true
'@esbuild/aix-ppc64@0.25.12':
optional: true
'@esbuild/aix-ppc64@0.28.0':
optional: true
'@esbuild/android-arm64@0.21.5':
optional: true
'@esbuild/android-arm64@0.25.12':
optional: true
'@esbuild/android-arm64@0.28.0':
optional: true
'@esbuild/android-arm@0.21.5':
optional: true
'@esbuild/android-arm@0.25.12':
optional: true
'@esbuild/android-arm@0.28.0':
optional: true
'@esbuild/android-x64@0.21.5':
optional: true
'@esbuild/android-x64@0.25.12':
optional: true
'@esbuild/android-x64@0.28.0':
optional: true
'@esbuild/darwin-arm64@0.21.5':
optional: true
'@esbuild/darwin-arm64@0.25.12':
optional: true
'@esbuild/darwin-arm64@0.28.0':
optional: true
'@esbuild/darwin-x64@0.21.5':
optional: true
'@esbuild/darwin-x64@0.25.12':
optional: true
'@esbuild/darwin-x64@0.28.0':
optional: true
'@esbuild/freebsd-arm64@0.21.5':
optional: true
'@esbuild/freebsd-arm64@0.25.12':
optional: true
'@esbuild/freebsd-arm64@0.28.0':
optional: true
'@esbuild/freebsd-x64@0.21.5':
optional: true
'@esbuild/freebsd-x64@0.25.12':
optional: true
'@esbuild/freebsd-x64@0.28.0':
optional: true
'@esbuild/linux-arm64@0.21.5':
optional: true
'@esbuild/linux-arm64@0.25.12':
optional: true
'@esbuild/linux-arm64@0.28.0':
optional: true
'@esbuild/linux-arm@0.21.5':
optional: true
'@esbuild/linux-arm@0.25.12':
optional: true
'@esbuild/linux-arm@0.28.0':
optional: true
'@esbuild/linux-ia32@0.21.5':
optional: true
'@esbuild/linux-ia32@0.25.12':
optional: true
'@esbuild/linux-ia32@0.28.0':
optional: true
'@esbuild/linux-loong64@0.21.5':
optional: true
'@esbuild/linux-loong64@0.25.12':
optional: true
'@esbuild/linux-loong64@0.28.0':
optional: true
'@esbuild/linux-mips64el@0.21.5':
optional: true
'@esbuild/linux-mips64el@0.25.12':
optional: true
'@esbuild/linux-mips64el@0.28.0':
optional: true
'@esbuild/linux-ppc64@0.21.5':
optional: true
'@esbuild/linux-ppc64@0.25.12':
optional: true
'@esbuild/linux-ppc64@0.28.0':
optional: true
'@esbuild/linux-riscv64@0.21.5':
optional: true
'@esbuild/linux-riscv64@0.25.12':
optional: true
'@esbuild/linux-riscv64@0.28.0':
optional: true
'@esbuild/linux-s390x@0.21.5':
optional: true
'@esbuild/linux-s390x@0.25.12':
optional: true
'@esbuild/linux-s390x@0.28.0':
optional: true
'@esbuild/linux-x64@0.21.5':
optional: true
'@esbuild/linux-x64@0.25.12':
optional: true
'@esbuild/linux-x64@0.28.0':
optional: true
'@esbuild/netbsd-arm64@0.25.12':
optional: true
'@esbuild/netbsd-arm64@0.28.0':
optional: true
'@esbuild/netbsd-x64@0.21.5':
optional: true
'@esbuild/netbsd-x64@0.25.12':
optional: true
'@esbuild/netbsd-x64@0.28.0':
optional: true
'@esbuild/openbsd-arm64@0.25.12':
optional: true
'@esbuild/openbsd-arm64@0.28.0':
optional: true
'@esbuild/openbsd-x64@0.21.5':
optional: true
'@esbuild/openbsd-x64@0.25.12':
optional: true
'@esbuild/openbsd-x64@0.28.0':
optional: true
'@esbuild/openharmony-arm64@0.25.12':
optional: true
'@esbuild/openharmony-arm64@0.28.0':
optional: true
'@esbuild/sunos-x64@0.21.5':
optional: true
'@esbuild/sunos-x64@0.25.12':
optional: true
'@esbuild/sunos-x64@0.28.0':
optional: true
'@esbuild/win32-arm64@0.21.5':
optional: true
'@esbuild/win32-arm64@0.25.12':
optional: true
'@esbuild/win32-arm64@0.28.0':
optional: true
'@esbuild/win32-ia32@0.21.5':
optional: true
'@esbuild/win32-ia32@0.25.12':
optional: true
'@esbuild/win32-ia32@0.28.0':
optional: true
'@esbuild/win32-x64@0.21.5':
optional: true
'@esbuild/win32-x64@0.25.12':
optional: true
'@esbuild/win32-x64@0.28.0':
optional: true
@@ -5107,6 +5365,35 @@ snapshots:
'@esbuild/win32-ia32': 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:
optionalDependencies:
'@esbuild/aix-ppc64': 0.28.0
+2 -1
View File
@@ -22,7 +22,8 @@
"@originmain/multiplayer": ["./packages/multiplayer/src/index.ts"],
"@originmain/platform": ["./packages/platform/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"]
}
}
}