improved a lot of things
This commit is contained in:
@@ -0,0 +1,512 @@
|
||||
# Rendering Architecture
|
||||
|
||||
> Definitive design document for Originmain's two live-rendering modes.
|
||||
> Created 2026-04-29. Supersedes the original Layer 2 approach (direct
|
||||
> cross-origin iframe script injection, which is non-functional — see
|
||||
> "Why the Previous Approach Failed" below).
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#1-overview)
|
||||
2. [Why the Previous Approach Failed](#2-why-the-previous-approach-failed)
|
||||
3. [Architecture Overview](#3-architecture-overview)
|
||||
4. [Mode A — CLI Proxy (Live Development)](#4-mode-a--cli-proxy-live-development)
|
||||
5. [Mode B — Live SDK (Preview Deployments / GitHub)](#5-mode-b--live-sdk-preview-deployments--github)
|
||||
6. [PostMessage Protocol (Unchanged)](#6-postmessage-protocol-unchanged)
|
||||
7. [Artboard ID Routing via `window.name`](#7-artboard-id-routing-via-windowname)
|
||||
8. [Sequence Diagrams](#8-sequence-diagrams)
|
||||
9. [Edge Cases](#9-edge-cases)
|
||||
10. [Package Reference](#10-package-reference)
|
||||
11. [Migration Notes](#11-migration-notes)
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
Originmain needs to render a user's **running React application** inside
|
||||
an artboard iframe on the canvas, while simultaneously extracting the
|
||||
React **fiber tree** (component names, props, DOM rects) so the
|
||||
Inspector, SelectionOverlay, and Diff Engine can operate on live data.
|
||||
|
||||
Two rendering modes serve different workflows:
|
||||
|
||||
| Mode | When to use | How the app loads | Who installs the fiber hook |
|
||||
|------|-------------|-------------------|----------------------------|
|
||||
| **A — CLI Proxy** | Active local development | User's dev server proxied through `@originmain/cli` | The proxy injects it into every HTML response |
|
||||
| **B — Live SDK** | Preview deployments, CI, async review | Vercel/Netlify preview URL or any hosted app | `@originmain/live` npm package (imported before React) |
|
||||
|
||||
Both modes use the **same postMessage protocol** (`packages/renderer/src/protocol.ts`)
|
||||
and the **same `LiveArtboard.tsx`** component. The only difference is
|
||||
who is responsible for installing the fiber hook before React loads.
|
||||
|
||||
---
|
||||
|
||||
## 2. Why the Previous Approach Failed
|
||||
|
||||
The original Layer 2 implementation attempted direct cross-origin script
|
||||
injection via `iframe.contentDocument`. This fails for three independent
|
||||
reasons, any one of which is fatal:
|
||||
|
||||
### 2.1 Cross-Origin DOM Access Is Blocked
|
||||
|
||||
`LiveArtboard.tsx` called `iframe.contentDocument.createElement('script')`.
|
||||
When the iframe's origin (`http://localhost:3000`) differs from the
|
||||
host (`http://localhost:3001` or `https://app.originmain.com`), the
|
||||
browser returns `null` for `contentDocument`. The try/catch in
|
||||
`injectFiberHook()` silently swallowed this error.
|
||||
|
||||
### 2.2 Circular READY Handshake
|
||||
|
||||
The `READY` message was generated by the injected fiber hook script
|
||||
itself (line 106 in `fiber-hook.ts`). But the host waited for `READY`
|
||||
before injecting the script. Neither side could proceed first.
|
||||
|
||||
### 2.3 Hook Must Precede React Module Evaluation
|
||||
|
||||
`__REACT_DEVTOOLS_GLOBAL_HOOK__` must exist before React's module body
|
||||
runs. React checks for this object exactly once, at import time. Any
|
||||
script injected after React has loaded is too late — React will never
|
||||
register with a hook installed after the fact.
|
||||
|
||||
### 2.4 X-Frame-Options / CSP Blocking
|
||||
|
||||
Many apps (including GitHub pages) send `X-Frame-Options: DENY` or
|
||||
`Content-Security-Policy: frame-ancestors 'none'`. The iframe renders
|
||||
blank. No browser-side workaround exists.
|
||||
|
||||
---
|
||||
|
||||
## 3. Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Originmain Canvas (host) │
|
||||
│ │
|
||||
│ ┌──────────────┐ postMessage ┌──────────────────────┐ │
|
||||
│ │ LiveArtboard │ ◄──────────────── │ iframe │ │
|
||||
│ │ (listens) │ READY │ name="om:{id}" │ │
|
||||
│ │ │ FIBER_TREE_UPDATE │ │ │
|
||||
│ │ │ COMPONENT_SELECTED │ ┌────────────────┐ │ │
|
||||
│ └──────────────┘ │ │ Fiber Hook │ │ │
|
||||
│ │ │ │ (reads │ │ │
|
||||
│ ▼ │ │ window.name) │ │ │
|
||||
│ ┌──────────┐ │ └────────────────┘ │ │
|
||||
│ │Inspector │ │ ▲ │ │
|
||||
│ │Overlay │ │ │ installed │ │
|
||||
│ │DiffEngine│ │ ┌──┴─────────┐ │ │
|
||||
│ └──────────┘ │ │ Mode A: │ │ │
|
||||
│ │ │ CLI Proxy │ │ │
|
||||
│ │ │ Mode B: │ │ │
|
||||
│ │ │ Live SDK │ │ │
|
||||
│ │ └────────────┘ │ │
|
||||
│ └──────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Key invariant: **the fiber hook is always installed before React loads**.
|
||||
The two modes differ only in *who* ensures this.
|
||||
|
||||
---
|
||||
|
||||
## 4. Mode A — CLI Proxy (Live Development)
|
||||
|
||||
### 4.1 User Experience
|
||||
|
||||
```bash
|
||||
# Terminal 1 — user's dev server (as usual)
|
||||
npm run dev # → http://localhost:3000
|
||||
|
||||
# Terminal 2 — Originmain proxy
|
||||
npx @originmain/cli dev --target http://localhost:3000
|
||||
# → Proxy listening on http://localhost:4170
|
||||
# → Paste this URL into your Originmain artboard
|
||||
```
|
||||
|
||||
The user enters `http://localhost:4170` as the artboard's render URL
|
||||
in Originmain. The iframe loads through the proxy.
|
||||
|
||||
### 4.2 What the Proxy Does
|
||||
|
||||
For every HTTP request:
|
||||
|
||||
1. **Forward** the request to the target dev server (`localhost:3000`)
|
||||
2. **Strip** response headers that block iframing:
|
||||
- `X-Frame-Options` (any value)
|
||||
- `Content-Security-Policy` `frame-ancestors` directive
|
||||
3. **For HTML responses only** (Content-Type contains `text/html`):
|
||||
- Strip `Accept-Encoding` from the outgoing request so the target
|
||||
sends uncompressed HTML (avoids decompressing gzip/brotli)
|
||||
- Buffer the full response body
|
||||
- Inject the fiber hook `<script>` tag immediately after `<head>`
|
||||
(or after `<html>`, or at document start as fallback)
|
||||
- Update `Content-Length` to match the modified body
|
||||
4. **For non-HTML responses**: stream through unchanged (only headers stripped)
|
||||
|
||||
### 4.3 WebSocket Passthrough (HMR)
|
||||
|
||||
Modern dev servers (Next.js, Vite, Webpack) use WebSocket for Hot
|
||||
Module Replacement. The proxy listens for HTTP `Upgrade` requests and
|
||||
opens a raw TCP tunnel to the target server, passing bytes
|
||||
bidirectionally. HMR continues working normally.
|
||||
|
||||
### 4.4 CORS Headers
|
||||
|
||||
The proxy adds permissive CORS headers to all responses:
|
||||
```
|
||||
Access-Control-Allow-Origin: *
|
||||
Access-Control-Allow-Methods: *
|
||||
Access-Control-Allow-Headers: *
|
||||
```
|
||||
This prevents issues when the user's app makes absolute-URL API calls
|
||||
that resolve to `localhost:3000` from an iframe at `localhost:4170`.
|
||||
|
||||
### 4.5 The Injected Script
|
||||
|
||||
The proxy injects a self-contained `<script>` block that:
|
||||
|
||||
1. Checks `window.parent !== window` (only runs when iframed)
|
||||
2. Reads `window.name` for the artboard ID (set by `LiveArtboard`)
|
||||
3. Installs `__REACT_DEVTOOLS_GLOBAL_HOOK__` if not present
|
||||
4. Wraps `onCommitFiberRoot` to serialize the fiber tree on every commit
|
||||
5. Sends `READY` via `postMessage` to the parent
|
||||
6. On each React commit, sends `FIBER_TREE_UPDATE` via `postMessage`
|
||||
|
||||
The script is ~90 lines, has zero dependencies, and is generated once
|
||||
at proxy startup by `buildProxyFiberHookScript()` in
|
||||
`packages/renderer/src/fiber-hook.ts`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Mode B — Live SDK (Preview Deployments / GitHub)
|
||||
|
||||
### 5.1 User Experience
|
||||
|
||||
```bash
|
||||
npm install @originmain/live
|
||||
```
|
||||
|
||||
```ts
|
||||
// MUST be the very first import in your app entry point
|
||||
// (before React, before anything else)
|
||||
import '@originmain/live';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
// ... rest of app
|
||||
```
|
||||
|
||||
The SDK is a zero-dependency side-effect import. At module evaluation
|
||||
time (before React's module body runs), it installs the fiber hook.
|
||||
|
||||
### 5.2 When It Activates
|
||||
|
||||
The SDK only activates when **all** of these are true:
|
||||
|
||||
- `window.parent !== window` (the app is inside an iframe)
|
||||
- `window.name` starts with `om:` (the iframe was created by Originmain)
|
||||
|
||||
When running standalone (not iframed, or iframed by something other
|
||||
than Originmain), the SDK is a complete no-op: no global hook
|
||||
installed, no messages sent, zero runtime cost.
|
||||
|
||||
### 5.3 GitHub Integration Flow
|
||||
|
||||
```
|
||||
1. User connects GitHub repo to Originmain workspace
|
||||
2. User installs @originmain/live in their app
|
||||
3. User pushes code → Vercel/Netlify deploys preview
|
||||
4. GitHub sends deployment_status webhook → Originmain
|
||||
5. Originmain creates artboard with preview deployment URL
|
||||
6. Canvas iframes the preview URL
|
||||
7. @originmain/live (bundled in preview) detects Originmain iframe
|
||||
8. Fiber data flows via postMessage → full inspection works
|
||||
```
|
||||
|
||||
### 5.4 Why Preview URLs Work Without a Proxy
|
||||
|
||||
Vercel and Netlify preview deployments do **not** set
|
||||
`X-Frame-Options` or `frame-ancestors` by default. They are designed
|
||||
to be embeddable (Vercel's own preview comments embed them). This
|
||||
means the iframe renders correctly without header stripping.
|
||||
|
||||
If a user's deployment provider does block iframing, they should use
|
||||
Mode A (CLI proxy) instead, or configure their provider's headers.
|
||||
|
||||
### 5.5 Production Opt-Out
|
||||
|
||||
The SDK should not ship to production. Recommended patterns:
|
||||
|
||||
```ts
|
||||
// Option 1: conditional import (Vite / Webpack)
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
await import('@originmain/live');
|
||||
}
|
||||
|
||||
// Option 2: separate entry point
|
||||
// dev.tsx imports @originmain/live, then imports main.tsx
|
||||
// prod.tsx imports main.tsx directly
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. PostMessage Protocol (Unchanged)
|
||||
|
||||
The protocol from `packages/renderer/src/protocol.ts` is unchanged.
|
||||
Both modes use the same envelope format:
|
||||
|
||||
### Host → Renderer (Originmain → iframe)
|
||||
|
||||
| Type | Payload | Purpose |
|
||||
|------|---------|---------|
|
||||
| `SET_DESIGN_TOKENS` | `{ tokens: Record<string, string> }` | Push DLF tokens as CSS variables |
|
||||
| `NAVIGATE` | `{ path: string }` | Navigate to a route within the app |
|
||||
| `SELECT_COMPONENT` | `{ nodeId: string }` | Highlight a component |
|
||||
| `DESELECT` | — | Clear selection |
|
||||
| `INJECT_FIBER_HOOK` | — | Legacy; no longer used |
|
||||
|
||||
### Renderer → Host (iframe → Originmain)
|
||||
|
||||
| Type | Payload | Purpose |
|
||||
|------|---------|---------|
|
||||
| `READY` | — | Fiber hook installed, app loaded |
|
||||
| `FIBER_TREE_UPDATE` | `{ root: FiberNode }` | Full serialized fiber tree |
|
||||
| `COMPONENT_SELECTED` | `{ nodeId, rect }` | User clicked a component in the iframe |
|
||||
| `COMPONENT_DESELECTED` | — | Selection cleared |
|
||||
| `ERROR` | `{ message: string }` | Hook or serialization error |
|
||||
|
||||
### Envelope Format
|
||||
|
||||
```ts
|
||||
// Renderer → Host
|
||||
{
|
||||
source: 'originmain-renderer', // discriminant
|
||||
artboardId: string, // from window.name
|
||||
message: RendererMessage
|
||||
}
|
||||
|
||||
// Host → Renderer
|
||||
{
|
||||
source: 'originmain-host', // discriminant
|
||||
artboardId: string,
|
||||
message: HostMessage
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Artboard ID Routing via `window.name`
|
||||
|
||||
Each `<iframe>` element gets a `name` attribute set by `LiveArtboard`:
|
||||
|
||||
```tsx
|
||||
<iframe name={`om:${artboardId}`} src={proxyOrPreviewUrl} ... />
|
||||
```
|
||||
|
||||
The fiber hook (whether injected by the proxy or by the SDK) reads
|
||||
`window.name` at initialization:
|
||||
|
||||
```js
|
||||
var artboardId = '';
|
||||
if (window.name && window.name.indexOf('om:') === 0) {
|
||||
artboardId = window.name.slice(3);
|
||||
}
|
||||
if (!artboardId) return; // not an Originmain iframe — do nothing
|
||||
```
|
||||
|
||||
This design has three advantages over the previous baked-in ID approach:
|
||||
|
||||
1. **Multiple artboards can share one proxy** — each iframe has a
|
||||
unique name, but all load through the same `localhost:4170` proxy.
|
||||
2. **SPA navigation preserves the ID** — `window.name` persists
|
||||
across navigations within the same browsing context.
|
||||
3. **The proxy is artboard-agnostic** — it injects one generic script
|
||||
for all requests. No per-artboard configuration needed.
|
||||
|
||||
---
|
||||
|
||||
## 8. Sequence Diagrams
|
||||
|
||||
### Mode A — CLI Proxy
|
||||
|
||||
```
|
||||
User's Dev Server CLI Proxy (:4170) Originmain Canvas
|
||||
│ │ │
|
||||
│ │ iframe src=:4170/ │
|
||||
│ │ ◄──────────────────────── │
|
||||
│ GET / │ │
|
||||
│ ◄──────────────────── │ │
|
||||
│ │ │
|
||||
│ 200 OK (HTML) │ │
|
||||
│ ──────────────────── ▶ │ │
|
||||
│ │ │
|
||||
│ strip X-Frame-Options │
|
||||
│ inject <script> after <head> │
|
||||
│ update Content-Length │
|
||||
│ │ │
|
||||
│ │ 200 OK (modified HTML) │
|
||||
│ │ ─────────────────────────▶ │
|
||||
│ │ │
|
||||
│ │ postMessage: READY │
|
||||
│ │ ─────────────────────────▶ │
|
||||
│ │ │
|
||||
│ │ (React loads, commits) │
|
||||
│ │ │
|
||||
│ │ postMessage: │
|
||||
│ │ FIBER_TREE_UPDATE │
|
||||
│ │ ─────────────────────────▶ │
|
||||
│ │ │
|
||||
│ │ Inspector / Overlay / │
|
||||
│ │ DiffEngine now have │
|
||||
│ │ live fiber data │
|
||||
```
|
||||
|
||||
### Mode B — Live SDK
|
||||
|
||||
```
|
||||
User's App (preview URL) Originmain Canvas
|
||||
│ │
|
||||
│ iframe src=preview-url.vercel.app │
|
||||
│ ◄─────────────────────────────────────── │
|
||||
│ │
|
||||
│ @originmain/live evaluates (side effect) │
|
||||
│ → installs __REACT_DEVTOOLS_GLOBAL_HOOK__ │
|
||||
│ → reads window.name → artboard ID │
|
||||
│ → sends postMessage: READY │
|
||||
│ ──────────────────────────────────────────▶ │
|
||||
│ │
|
||||
│ React evaluates → registers with hook │
|
||||
│ React renders → onCommitFiberRoot fires │
|
||||
│ │
|
||||
│ postMessage: FIBER_TREE_UPDATE │
|
||||
│ ──────────────────────────────────────────▶ │
|
||||
│ │
|
||||
│ (identical from here on) │
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Edge Cases
|
||||
|
||||
### 9.1 HMR / Hot Module Replacement
|
||||
|
||||
The CLI proxy passes WebSocket `Upgrade` requests through to the
|
||||
target server via raw TCP socket tunneling. The user's HMR continues
|
||||
working. When a hot update changes the component tree, React commits
|
||||
again, the fiber hook fires, and the canvas receives an updated tree.
|
||||
|
||||
### 9.2 Server-Side Rendering (SSR)
|
||||
|
||||
The proxy receives the server-rendered HTML. The fiber hook script is
|
||||
injected before any application scripts. When React hydrates the SSR
|
||||
output, it finds the hook already installed and registers. Hydration
|
||||
commits trigger `onCommitFiberRoot` just like client-side renders.
|
||||
|
||||
### 9.3 React DevTools Coexistence
|
||||
|
||||
If the user has the React DevTools browser extension installed, it
|
||||
will have already set `__REACT_DEVTOOLS_GLOBAL_HOOK__`. The fiber
|
||||
hook checks for an existing hook and wraps the existing
|
||||
`onCommitFiberRoot` rather than replacing it. Both Originmain and
|
||||
React DevTools receive fiber commits independently.
|
||||
|
||||
### 9.4 Non-React Applications
|
||||
|
||||
If the proxied app does not use React, the hook installs but
|
||||
`onCommitFiberRoot` never fires. The `READY` message is still sent.
|
||||
The artboard displays the app visually, but the Inspector shows no
|
||||
component tree. This is by design — Originmain supports visual
|
||||
review of any web app, with React-specific features only activating
|
||||
when React is detected.
|
||||
|
||||
### 9.5 Multiple Artboards, One Proxy
|
||||
|
||||
Several artboards can point to the same proxy URL with different
|
||||
routes (e.g., `localhost:4170/dashboard` and `localhost:4170/settings`).
|
||||
Each iframe has a unique `window.name`, so fiber updates are correctly
|
||||
routed to their respective artboards.
|
||||
|
||||
### 9.6 SPA Navigation Inside the Iframe
|
||||
|
||||
SPA routers change the URL without a full page reload. React does not
|
||||
unmount — it re-renders. The fiber hook, already installed, continues
|
||||
receiving commits. `window.name` persists across SPA navigations.
|
||||
|
||||
### 9.7 Full Page Navigation Inside the Iframe
|
||||
|
||||
If the user's app performs a full page navigation (e.g., `<a href>`
|
||||
without client-side routing), the browser requests the new page from
|
||||
the proxy. The proxy injects the fiber hook into the new HTML response.
|
||||
`window.name` is preserved by the browser across same-frame
|
||||
navigations. The new page's React instance finds the hook and
|
||||
registers. A new `READY` message fires.
|
||||
|
||||
### 9.8 Compressed Responses
|
||||
|
||||
The proxy strips `Accept-Encoding` from outgoing requests to the
|
||||
target, causing the target to respond with uncompressed HTML. This
|
||||
avoids the complexity of decompressing gzip/brotli/deflate before
|
||||
injection. The performance impact is negligible over localhost.
|
||||
|
||||
### 9.9 Chunked Transfer-Encoding
|
||||
|
||||
The proxy buffers the full HTML response before injecting the script.
|
||||
For streaming SSR responses, this adds latency equal to the full
|
||||
document size. This is acceptable for a development tool.
|
||||
|
||||
---
|
||||
|
||||
## 10. Package Reference
|
||||
|
||||
### `@originmain/cli` (`packages/cli`)
|
||||
|
||||
| Export / Command | Description |
|
||||
|------------------|-------------|
|
||||
| `originmain dev --target <url> [--port <n>]` | Start the reverse proxy |
|
||||
| `--target` | Required. The user's dev server URL |
|
||||
| `--port` | Proxy listen port (default: 4170) |
|
||||
|
||||
Dependencies: `@originmain/renderer` (for `buildProxyFiberHookScript`).
|
||||
Node.js built-ins only for proxy (`node:http`, `node:net`, `node:url`).
|
||||
|
||||
### `@originmain/live` (`packages/live-sdk`)
|
||||
|
||||
| Export | Description |
|
||||
|--------|-------------|
|
||||
| `import '@originmain/live'` | Side-effect import — installs fiber hook |
|
||||
|
||||
Zero dependencies. ~2 KB minified. No-op when not in an Originmain iframe.
|
||||
|
||||
### `@originmain/renderer` (`packages/renderer`) — updated
|
||||
|
||||
| Export | Description |
|
||||
|--------|-------------|
|
||||
| `buildProxyFiberHookScript()` | Returns the generic fiber hook script (reads `window.name`) |
|
||||
| `buildFiberHookScript(id)` | **Deprecated.** Legacy baked-in ID version |
|
||||
| (all other exports) | Unchanged |
|
||||
|
||||
---
|
||||
|
||||
## 11. Migration Notes
|
||||
|
||||
### LiveArtboard.tsx Changes
|
||||
|
||||
1. **Add** `name={`om:${id}`}` to the `<iframe>` element
|
||||
2. **Remove** the `injectFiberHook()` call from the `READY` handler
|
||||
3. **Keep** the `postMessage` listener — the protocol is identical
|
||||
|
||||
### Artboard.tsx Changes
|
||||
|
||||
1. **Update** `EmptyArtboardContent` to show proxy instructions
|
||||
2. **No changes** to fiber handling, SelectionOverlay, or diff logic
|
||||
|
||||
### GitHub Connector Fix
|
||||
|
||||
`packages/integrations/src/connectors/github.ts` must set `renderUrl`
|
||||
to the **deployment preview URL** (from a `deployment_status` webhook),
|
||||
not `pr.html_url` (which is the GitHub PR page and blocks iframing).
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 2026-04-29*
|
||||
Reference in New Issue
Block a user