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*