improved a lot of things
This commit is contained in:
+82
-11
@@ -11,7 +11,7 @@
|
||||
|-------|-----------------------------|--------------|------------------------------------------------------------------|
|
||||
| 0 | Infrastructure & DevOps | ⏳ Deferred | Skipped for now per plan |
|
||||
| 1 | Canvas UI Shell | ✅ Complete | ArtboardTree, CodebaseFileTree, CodeDiffPanel |
|
||||
| 2 | Live Rendering Engine | ✅ Complete | postMessage protocol, fiber hook, MF config, LiveArtboard iframe |
|
||||
| 2 | Live Rendering Engine | ✅ Revised | CLI proxy + Live SDK; see RENDERING-ARCHITECTURE.md |
|
||||
| 3 | Visual Editing & Diff Engine| ✅ Complete | Diff engine (49 tests), SelectionOverlay, history store |
|
||||
| 4 | Origin Graph & Data Store | ✅ Complete | 3 migrations, RLS, Zod types, query helpers |
|
||||
| 5 | Design Language Runtime | ✅ Complete | New package, JSON Schema, Zod validator, token pipeline |
|
||||
@@ -49,16 +49,52 @@
|
||||
|
||||
---
|
||||
|
||||
## Layer 2 — Live Rendering Engine ✅
|
||||
## Layer 2 — Live Rendering Engine ✅ (Revised 2026-04-29)
|
||||
|
||||
**Completed:** 2026-04-25
|
||||
**Original:** 2026-04-25 — postMessage protocol, fiber hook, MF config, LiveArtboard iframe
|
||||
**Revised:** 2026-04-29 — Replaced non-functional cross-origin injection with two working modes
|
||||
|
||||
### Files created
|
||||
- **`packages/renderer/src/protocol.ts`** — `HostMessage`/`RendererMessage` unions, `isHostEnvelope()`, `isRendererEnvelope()`, `createHostEnvelope()`, `createRendererEnvelope()`; source discriminants `'originmain-host'` / `'originmain-renderer'`
|
||||
- **`packages/renderer/src/fiber-hook.ts`** — `buildFiberHookScript(artboardId)` generates injectable script that patches `__REACT_DEVTOOLS_GLOBAL_HOOK__.onCommitFiberRoot`, walks Fiber tree, posts `FIBER_TREE_UPDATE` envelopes to `window.parent`
|
||||
- **`packages/renderer/src/module-federation.ts`** — `createRendererHostConfig()` and `createRemoteConfig()` MF host config helpers with shared React singleton
|
||||
- **`packages/renderer/src/index.ts`** — exports all of the above
|
||||
- **`packages/app/src/components/canvas/LiveArtboard.tsx`** — React iframe component; `sandbox="allow-scripts allow-same-origin allow-forms"`; injects fiber hook on `READY`; sends `SET_DESIGN_TOKENS` on token change
|
||||
> See **`RENDERING-ARCHITECTURE.md`** for the full design document.
|
||||
|
||||
### Why the original approach was non-functional
|
||||
The original `LiveArtboard.tsx` tried `iframe.contentDocument.createElement('script')` to inject the fiber hook. This fails for cross-origin iframes (the browser blocks DOM access), and the READY handshake had a circular dependency. See `RENDERING-ARCHITECTURE.md` §2 for details.
|
||||
|
||||
### Two rendering modes (both use the same postMessage protocol)
|
||||
|
||||
#### Mode A — CLI Proxy (`@originmain/cli`)
|
||||
For active local development. User runs `npx @originmain/cli dev --target http://localhost:3000` and enters the proxy URL (`http://localhost:4170`) into the artboard.
|
||||
|
||||
The proxy:
|
||||
1. Strips `X-Frame-Options` / CSP headers from responses
|
||||
2. Injects the fiber hook `<script>` into HTML responses (before React loads)
|
||||
3. Passes WebSocket upgrades through for HMR
|
||||
|
||||
#### Mode B — Live SDK (`@originmain/live`)
|
||||
For preview deployments / GitHub integration. User installs `@originmain/live` in their app and imports it before React:
|
||||
```ts
|
||||
import '@originmain/live'; // must be first import
|
||||
import React from 'react';
|
||||
```
|
||||
|
||||
The SDK installs `__REACT_DEVTOOLS_GLOBAL_HOOK__` at module evaluation time and only activates inside an Originmain iframe (detected by `window.name` prefix `om:`).
|
||||
|
||||
### Files created / modified
|
||||
- **`packages/renderer/src/fiber-hook.ts`** — Added `buildProxyFiberHookScript()`: generic fiber hook that reads `window.name` for artboard ID (used by both CLI proxy and SDK). Original `buildFiberHookScript(id)` preserved but deprecated.
|
||||
- **`packages/cli/`** (NEW) — CLI package:
|
||||
- `src/cli.ts` — CLI entry: `originmain dev --target <url> [--port <n>]`
|
||||
- `src/proxy.ts` — HTTP reverse proxy: header stripping, HTML injection, WebSocket passthrough
|
||||
- `src/inject.ts` — HTML injection logic: inserts `<script>` after `<head>`
|
||||
- **`packages/live-sdk/`** (NEW) — SDK package:
|
||||
- `src/hook.ts` — Self-contained fiber hook: installs `__REACT_DEVTOOLS_GLOBAL_HOOK__`, serializes fiber tree, sends postMessage to parent
|
||||
- `src/index.ts` — Side-effect re-export
|
||||
- **`packages/app/src/components/canvas/LiveArtboard.tsx`** — Removed broken `injectFiberHook()`. Added `name={`om:${id}`}` to iframe. READY handler no longer attempts injection.
|
||||
- **`packages/app/src/components/canvas/Artboard.tsx`** — Updated `EmptyArtboardContent` to show proxy URL instructions and correct placeholder.
|
||||
- **`packages/integrations/src/connectors/github.ts`** — Fixed `renderUrl`: now accepts `deploymentUrl` option (from deployment_status webhook) instead of using `pr.html_url` (which is un-iframeable).
|
||||
|
||||
### Key design decisions
|
||||
- **Artboard ID via `window.name`**: `LiveArtboard` sets `name="om:{id}"` on the iframe. The fiber hook reads `window.name` to tag postMessage envelopes. Persists across SPA navigation, supports multiple artboards on one proxy.
|
||||
- **No WebSocket relay needed**: postMessage works cross-origin; the CLI proxy makes the iframe renderable by stripping blocking headers. No additional relay infrastructure required.
|
||||
- **Zero-dep SDK**: `@originmain/live` has no dependencies; it's a ~2 KB side-effect import that's a no-op outside Originmain iframes.
|
||||
|
||||
---
|
||||
|
||||
@@ -161,7 +197,9 @@ Each connector validates untrusted webhook JSON at the boundary with Zod (`parse
|
||||
| `packages/app` | Active | Next.js 15 app; Layers 1–3 complete |
|
||||
| `packages/diff-engine` | Complete | 49 tests, ~88% coverage |
|
||||
| `packages/ui` | Active | Theme + FluentProvider |
|
||||
| `packages/renderer` | Complete | Layer 2: postMessage protocol, fiber hook, MF config |
|
||||
| `packages/renderer` | Complete | Layer 2: postMessage protocol, fiber hook (proxy-compatible), MF config |
|
||||
| `packages/cli` | Complete | Layer 2: CLI proxy for live rendering (`originmain dev`) |
|
||||
| `packages/live-sdk` | Complete | Layer 2: `@originmain/live` SDK for preview deployments |
|
||||
| `packages/origin-graph` | Complete | Layer 4: migrations, RLS, Zod types, query helpers; 45 tests, 100% types.ts coverage |
|
||||
| `packages/design-language` | Complete | Layer 5: DLF schema, validator, token pipeline |
|
||||
| `packages/ai-layer` | Complete | Layer 6: gateway, 5 features, prompt library; SDK v0.91.1, adaptive thinking |
|
||||
@@ -347,6 +385,38 @@ The following is a second-pass audit of all real product gaps, independent of la
|
||||
- **`cmp-card.hero-card` bug fixed** — was `background: var(--fg)` which = near-white in dark mode (white text on white card). Fixed to always-dark `#06060E` with blue `rgba(51,133,255,0.4)` glow border; eyebrow/tags get blue tint; body contrast improved.
|
||||
- **Bottom CTA (`#cta-bottom`) bug fixed** — was `background: var(--bg-inv)` which = light gray in dark mode, making `btn-outline-white` invisible. Fixed to always-dark `#06060E`; headline/subtext use fixed white alphas; gradient intensity slightly increased.
|
||||
|
||||
### ✅ Completed — Session 7 (2026-04-29)
|
||||
|
||||
#### Rendering Architecture Overhaul
|
||||
|
||||
The original Layer 2 rendering system used direct cross-origin iframe script injection, which was non-functional (see RENDERING-ARCHITECTURE.md §2). Replaced with two working modes:
|
||||
|
||||
- **`RENDERING-ARCHITECTURE.md`** (NEW) — Full design document: problem statement, two rendering modes, postMessage protocol, sequence diagrams, edge cases, package reference, migration notes.
|
||||
|
||||
- **`packages/renderer/src/fiber-hook.ts`** — Added `buildProxyFiberHookScript()`: generates a generic fiber hook script that reads `window.name` for artboard ID routing. Exported from package index. Original `buildFiberHookScript(id)` preserved as deprecated.
|
||||
|
||||
- **`packages/cli/`** (NEW) — `@originmain/cli` CLI package:
|
||||
- `src/cli.ts` — Entry: `originmain dev --target <url> [--port <n>]` with argument validation
|
||||
- `src/proxy.ts` — Reverse HTTP proxy: strips X-Frame-Options/CSP, injects fiber hook into HTML, passes WebSocket upgrades for HMR, adds CORS headers
|
||||
- `src/inject.ts` — HTML injection: inserts `<script>` after `<head>` (with `<html>` and prepend fallbacks)
|
||||
- `src/index.ts` — Programmatic API export
|
||||
|
||||
- **`packages/live-sdk/`** (NEW) — `@originmain/live` SDK package:
|
||||
- `src/hook.ts` — Self-contained fiber hook: installs `__REACT_DEVTOOLS_GLOBAL_HOOK__`, wraps existing DevTools handler, serializes fiber tree, sends postMessage with artboard ID from `window.name`. No-op when not in an Originmain iframe.
|
||||
- `src/index.ts` — Side-effect re-export
|
||||
|
||||
- **`packages/app/src/components/canvas/LiveArtboard.tsx`** — Removed broken `injectFiberHook()` helper. Added `name={`om:${id}`}` to iframe for artboard ID routing. READY handler no longer attempts script injection.
|
||||
|
||||
- **`packages/app/src/components/canvas/Artboard.tsx`** — `EmptyArtboardContent` updated: shows CLI proxy command hint, placeholder changed to `:4170`, URL input hint explains both connection modes.
|
||||
|
||||
- **`packages/integrations/src/connectors/github.ts`** — Fixed `renderUrl`: accepts `deploymentUrl` option from caller instead of using `pr.html_url` (which is un-iframeable). Uses `exactOptionalPropertyTypes`-safe conditional spread.
|
||||
|
||||
- **`packages/integrations/src/types.ts`** — `OriginIngester.ingest()` now accepts optional `opts` parameter for connector-specific configuration.
|
||||
|
||||
- **`tsconfig.base.json`** — Added path aliases for `@originmain/cli` and `@originmain/live`.
|
||||
|
||||
- **TypeScript**: Full monorepo `pnpm -r run typecheck` passes — all 12 packages, zero errors.
|
||||
|
||||
### 🔧 Remaining — Lower Priority
|
||||
|
||||
- [ ] **Multiplayer**: Wire `MultiplayerAdapter` into app (install `@liveblocks/client` + `@liveblocks/react`; create `packages/app/src/lib/liveblocks.ts`)
|
||||
@@ -354,6 +424,7 @@ The following is a second-pass audit of all real product gaps, independent of la
|
||||
- [ ] **Redis rate limiter**: Replace in-process rate limiter in `agent-bridge/src/rate-limiter.ts` for multi-instance MCP support
|
||||
- [ ] **Product analytics**: Instrument PostHog for activation/engagement metrics (GTM requirement)
|
||||
- [ ] **Onboarding wizard**: Step-by-step "connect your app" flow from signup to first live artboard
|
||||
- [ ] **GitHub App + deployment_status webhook**: Full OAuth flow to connect GitHub repos + auto-populate `deploymentUrl` from Vercel/Netlify deployment webhooks
|
||||
- [ ] **Marketing page — remaining `section-dark` / `--bg-inv` surfaces**: other sections using `background: var(--bg-inv)` (e.g. `#features-alt`, footer) should be audited for the same light-mode-in-dark-mode inversion issue if the marketing page is expected to fully support theme toggling
|
||||
|
||||
*Last updated: 2026-04-29 — Session 6 complete*
|
||||
*Last updated: 2026-04-29 — Session 7 complete*
|
||||
|
||||
@@ -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*
|
||||
@@ -352,12 +352,18 @@ function EmptyArtboardContent({
|
||||
setEditing(false);
|
||||
};
|
||||
|
||||
const hintStyle: React.CSSProperties = {
|
||||
margin: 0, fontSize: 10, color: '#A1A1AA', textAlign: 'center',
|
||||
lineHeight: 1.55, fontFamily: "'JetBrains Mono', ui-monospace, monospace",
|
||||
letterSpacing: '-0.01em',
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width, height, display: 'flex', flexDirection: 'column',
|
||||
alignItems: 'center', justifyContent: 'center',
|
||||
background: '#F8F8FA', gap: 12, padding: 20,
|
||||
background: '#F8F8FA', gap: 10, padding: 20,
|
||||
}}
|
||||
>
|
||||
{/* Artboard name */}
|
||||
@@ -366,20 +372,23 @@ function EmptyArtboardContent({
|
||||
</div>
|
||||
|
||||
{editing ? (
|
||||
<div style={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<div style={{ width: '100%', maxWidth: 280, display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<input
|
||||
autoFocus
|
||||
type="url"
|
||||
value={urlValue}
|
||||
onChange={(e) => setUrlValue(e.target.value)}
|
||||
placeholder="http://localhost:3000"
|
||||
placeholder="http://localhost:4170"
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') void save(); if (e.key === 'Escape') setEditing(false); e.stopPropagation(); }}
|
||||
style={{
|
||||
padding: '7px 10px', borderRadius: 6, border: '1.5px solid #0066FF',
|
||||
fontSize: 11, fontFamily: 'inherit', outline: 'none', width: '100%',
|
||||
boxSizing: 'border-box' as const,
|
||||
fontSize: 11, fontFamily: 'inherit', width: '100%',
|
||||
boxSizing: 'border-box' as const, color: '#0A0A0A', background: '#fff',
|
||||
}}
|
||||
/>
|
||||
<p style={hintStyle}>
|
||||
Use the CLI proxy URL or a preview deployment URL with @originmain/live installed
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
<button
|
||||
onClick={() => void save()} disabled={saving}
|
||||
@@ -396,6 +405,7 @@ function EmptyArtboardContent({
|
||||
style={{
|
||||
padding: '6px 10px', borderRadius: 5, border: '1px solid #E4E4E7',
|
||||
background: '#fff', fontSize: 11, cursor: 'pointer', fontFamily: 'inherit',
|
||||
color: '#3F3F46',
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
@@ -415,8 +425,11 @@ function EmptyArtboardContent({
|
||||
<path d="M6 8l2.5 2.5L12 6" stroke="#0066FF" strokeWidth="1.3" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p style={{ margin: 0, fontSize: 11, color: '#71717A', textAlign: 'center', lineHeight: 1.5, maxWidth: 180 }}>
|
||||
Connect a running app URL to enable live rendering
|
||||
<p style={{ margin: 0, fontSize: 11, color: '#52525B', textAlign: 'center', lineHeight: 1.5, maxWidth: 220 }}>
|
||||
Connect your running app to enable live component inspection
|
||||
</p>
|
||||
<p style={hintStyle}>
|
||||
npx @originmain/cli dev --target :3000
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setEditing(true)}
|
||||
@@ -426,7 +439,7 @@ function EmptyArtboardContent({
|
||||
cursor: 'pointer', fontFamily: 'inherit',
|
||||
}}
|
||||
>
|
||||
Connect app →
|
||||
Enter proxy URL
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import {
|
||||
buildFiberHookScript,
|
||||
createHostEnvelope,
|
||||
isRendererEnvelope,
|
||||
} from '@originmain/renderer';
|
||||
@@ -12,7 +11,9 @@ import type { FiberNode, RendererMessage } from '@originmain/renderer';
|
||||
|
||||
export interface LiveArtboardProps {
|
||||
id: string;
|
||||
/** URL of the connected application route to render */
|
||||
/** URL of the connected application route to render.
|
||||
* For live dev: the CLI proxy URL (e.g., http://localhost:4170)
|
||||
* For previews: the Vercel/Netlify preview URL (with @originmain/live SDK) */
|
||||
src: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
@@ -58,8 +59,9 @@ export function LiveArtboard({
|
||||
const msg: RendererMessage = event.data.message;
|
||||
switch (msg.type) {
|
||||
case 'READY':
|
||||
// Inject fiber hook after the renderer signals it's ready
|
||||
injectFiberHook(iframeRef.current, id);
|
||||
// The fiber hook is already installed — either by the CLI proxy
|
||||
// (injected into the HTML response) or by @originmain/live SDK
|
||||
// (imported before React in the user's app). No injection needed.
|
||||
if (designTokens) sendMessage('SET_DESIGN_TOKENS', { tokens: designTokens });
|
||||
onReady?.();
|
||||
break;
|
||||
@@ -85,6 +87,10 @@ export function LiveArtboard({
|
||||
return (
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
// The name attribute carries the artboard ID to the fiber hook.
|
||||
// The hook reads window.name to tag postMessage envelopes.
|
||||
// Format: "om:<artboardId>"
|
||||
name={`om:${id}`}
|
||||
src={src}
|
||||
title={`artboard-${id}`}
|
||||
// Security: allow-scripts required to run React; allow-same-origin required
|
||||
@@ -101,17 +107,3 @@ export function LiveArtboard({
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
function injectFiberHook(iframe: HTMLIFrameElement | null, artboardId: string) {
|
||||
if (!iframe?.contentDocument) return;
|
||||
try {
|
||||
const script = iframe.contentDocument.createElement('script');
|
||||
script.textContent = buildFiberHookScript(artboardId);
|
||||
iframe.contentDocument.head.appendChild(script);
|
||||
} catch {
|
||||
// Cross-origin or sandboxing prevents injection — renderer must include the
|
||||
// hook script itself in that case.
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "@originmain/cli",
|
||||
"version": "0.0.1",
|
||||
"private": false,
|
||||
"description": "Originmain CLI — reverse proxy for live React component inspection.",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"originmain": "./dist/cli.js"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "tsc -p tsconfig.build.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@originmain/renderer": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"typescript": "^5.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// ── Originmain CLI ───────────────────────────────────────────────────────────
|
||||
// Usage:
|
||||
// npx @originmain/cli dev --target http://localhost:3000 [--port 4170]
|
||||
//
|
||||
// Starts a reverse proxy that enables live React component inspection
|
||||
// in Originmain artboards. See RENDERING-ARCHITECTURE.md for details.
|
||||
|
||||
import { parseArgs } from 'node:util';
|
||||
import { startProxy } from './proxy.js';
|
||||
|
||||
const DEFAULT_PORT = 4170;
|
||||
|
||||
function printUsage(): void {
|
||||
console.log(`
|
||||
\x1b[36m\x1b[1mOriginmain CLI\x1b[0m
|
||||
|
||||
Usage:
|
||||
originmain dev --target <url> [--port <number>]
|
||||
|
||||
Options:
|
||||
--target, -t Target dev server URL (required)
|
||||
Example: http://localhost:3000
|
||||
--port, -p Proxy listen port (default: ${DEFAULT_PORT})
|
||||
--help, -h Show this help
|
||||
|
||||
Example:
|
||||
npx @originmain/cli dev --target http://localhost:3000
|
||||
`);
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
// Parse arguments
|
||||
const { values, positionals } = parseArgs({
|
||||
args: process.argv.slice(2),
|
||||
options: {
|
||||
target: { type: 'string', short: 't' },
|
||||
port: { type: 'string', short: 'p' },
|
||||
help: { type: 'boolean', short: 'h' },
|
||||
},
|
||||
allowPositionals: true,
|
||||
strict: false,
|
||||
});
|
||||
|
||||
const command = positionals[0];
|
||||
|
||||
if (values.help || !command) {
|
||||
printUsage();
|
||||
process.exit(command ? 0 : 1);
|
||||
}
|
||||
|
||||
if (command !== 'dev') {
|
||||
console.error(` Unknown command: ${command}\n Run "originmain --help" for usage.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate --target
|
||||
const target = values.target;
|
||||
if (!target || typeof target !== 'string') {
|
||||
console.error(' Error: --target is required.\n Example: originmain dev --target http://localhost:3000');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate target URL
|
||||
let targetUrl: URL;
|
||||
try {
|
||||
targetUrl = new URL(target);
|
||||
} catch {
|
||||
console.error(` Error: Invalid target URL: ${target}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (targetUrl.protocol !== 'http:' && targetUrl.protocol !== 'https:') {
|
||||
console.error(` Error: Target must be http:// or https:// (got ${targetUrl.protocol})`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Parse port
|
||||
const port = values.port ? parseInt(values.port as string, 10) : DEFAULT_PORT;
|
||||
if (Number.isNaN(port) || port < 1 || port > 65535) {
|
||||
console.error(` Error: Invalid port: ${values.port}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Start proxy
|
||||
const proxy = startProxy({ target, port });
|
||||
|
||||
// Graceful shutdown
|
||||
function shutdown(): void {
|
||||
console.log('\n Shutting down proxy...');
|
||||
proxy.close();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
process.on('SIGINT', shutdown);
|
||||
process.on('SIGTERM', shutdown);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,4 @@
|
||||
// Programmatic API — allows using the proxy from other Node.js code
|
||||
export { startProxy } from './proxy.js';
|
||||
export type { ProxyOptions } from './proxy.js';
|
||||
export { injectFiberHook } from './inject.js';
|
||||
@@ -0,0 +1,45 @@
|
||||
// ── HTML Injection ───────────────────────────────────────────────────────────
|
||||
// Injects the Originmain fiber hook <script> into an HTML response body.
|
||||
// The script must appear BEFORE any other scripts so that
|
||||
// __REACT_DEVTOOLS_GLOBAL_HOOK__ is installed before React evaluates.
|
||||
|
||||
import { buildProxyFiberHookScript } from '@originmain/renderer';
|
||||
|
||||
/** The fiber hook script wrapped in a <script> tag, generated once at startup. */
|
||||
let cachedScriptTag: string | undefined;
|
||||
|
||||
function getScriptTag(): string {
|
||||
if (cachedScriptTag === undefined) {
|
||||
cachedScriptTag = `<script data-originmain-fiber-hook>${buildProxyFiberHookScript()}</script>`;
|
||||
}
|
||||
return cachedScriptTag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject the fiber hook script into an HTML string.
|
||||
*
|
||||
* Injection strategy (in order of preference):
|
||||
* 1. After `<head...>` — standard position for early scripts
|
||||
* 2. After `<html...>` — fallback if <head> is missing
|
||||
* 3. Prepend to document — final fallback
|
||||
*/
|
||||
export function injectFiberHook(html: string): string {
|
||||
const tag = getScriptTag();
|
||||
|
||||
// Try after <head>
|
||||
const headMatch = /<head[^>]*>/i.exec(html);
|
||||
if (headMatch) {
|
||||
const insertAt = headMatch.index + headMatch[0].length;
|
||||
return html.slice(0, insertAt) + tag + html.slice(insertAt);
|
||||
}
|
||||
|
||||
// Try after <html>
|
||||
const htmlMatch = /<html[^>]*>/i.exec(html);
|
||||
if (htmlMatch) {
|
||||
const insertAt = htmlMatch.index + htmlMatch[0].length;
|
||||
return html.slice(0, insertAt) + tag + html.slice(insertAt);
|
||||
}
|
||||
|
||||
// Final fallback: prepend
|
||||
return tag + html;
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
// ── Reverse Proxy ────────────────────────────────────────────────────────────
|
||||
// HTTP reverse proxy that:
|
||||
// 1. Forwards all requests to the target dev server
|
||||
// 2. Strips X-Frame-Options and CSP frame-ancestors from responses
|
||||
// 3. Injects the Originmain fiber hook into HTML responses
|
||||
// 4. Passes WebSocket upgrades through for HMR
|
||||
//
|
||||
// Uses only Node.js built-ins — no external dependencies.
|
||||
|
||||
import { createServer, request as httpRequest } from 'node:http';
|
||||
import { connect as netConnect } from 'node:net';
|
||||
import type { IncomingMessage, ServerResponse } from 'node:http';
|
||||
import type { Socket } from 'node:net';
|
||||
import { injectFiberHook } from './inject.js';
|
||||
|
||||
export interface ProxyOptions {
|
||||
/** Target dev server URL, e.g. "http://localhost:3000" */
|
||||
target: string;
|
||||
/** Port for the proxy to listen on (default: 4170) */
|
||||
port: number;
|
||||
}
|
||||
|
||||
/** Headers to strip from proxied responses (case-insensitive). */
|
||||
const STRIP_RESPONSE_HEADERS = new Set([
|
||||
'x-frame-options',
|
||||
'content-security-policy',
|
||||
'content-security-policy-report-only',
|
||||
]);
|
||||
|
||||
/** CORS headers added to every response for cross-origin API compatibility. */
|
||||
const CORS_HEADERS: Record<string, string> = {
|
||||
'access-control-allow-origin': '*',
|
||||
'access-control-allow-methods': '*',
|
||||
'access-control-allow-headers': '*',
|
||||
'access-control-allow-credentials': 'true',
|
||||
};
|
||||
|
||||
/**
|
||||
* Start the reverse proxy server.
|
||||
* Returns a cleanup function that shuts down the server.
|
||||
*/
|
||||
export function startProxy(opts: ProxyOptions): { close: () => void } {
|
||||
const targetUrl = new URL(opts.target);
|
||||
const targetHost = targetUrl.hostname;
|
||||
const targetPort = parseInt(targetUrl.port || '80', 10);
|
||||
|
||||
const server = createServer((clientReq: IncomingMessage, clientRes: ServerResponse) => {
|
||||
// ── Build the outgoing request to the target ─────────────────────────
|
||||
|
||||
// Clone headers, stripping Accept-Encoding so the target sends
|
||||
// uncompressed HTML (avoids needing to decompress before injection).
|
||||
const outHeaders: Record<string, string | string[]> = {};
|
||||
for (const [key, val] of Object.entries(clientReq.headers)) {
|
||||
if (key.toLowerCase() === 'accept-encoding') continue;
|
||||
if (key.toLowerCase() === 'host') {
|
||||
outHeaders[key] = `${targetHost}:${targetPort}`;
|
||||
continue;
|
||||
}
|
||||
if (val !== undefined) {
|
||||
outHeaders[key] = val;
|
||||
}
|
||||
}
|
||||
|
||||
const proxyReq = httpRequest(
|
||||
{
|
||||
hostname: targetHost,
|
||||
port: targetPort,
|
||||
path: clientReq.url ?? '/',
|
||||
method: clientReq.method,
|
||||
headers: outHeaders,
|
||||
},
|
||||
(proxyRes) => {
|
||||
// ── Process response headers ───────────────────────────────────
|
||||
|
||||
const resHeaders: Record<string, string | string[]> = {};
|
||||
|
||||
for (const [key, val] of Object.entries(proxyRes.headers)) {
|
||||
if (STRIP_RESPONSE_HEADERS.has(key.toLowerCase())) continue;
|
||||
if (val !== undefined) {
|
||||
resHeaders[key] = val;
|
||||
}
|
||||
}
|
||||
|
||||
// Add CORS headers
|
||||
for (const [key, val] of Object.entries(CORS_HEADERS)) {
|
||||
resHeaders[key] = val;
|
||||
}
|
||||
|
||||
// ── Determine if this is an HTML response ──────────────────────
|
||||
|
||||
const contentType = (proxyRes.headers['content-type'] ?? '').toLowerCase();
|
||||
const isHtml = contentType.includes('text/html');
|
||||
|
||||
if (!isHtml) {
|
||||
// Non-HTML: stream through unchanged (headers already stripped)
|
||||
clientRes.writeHead(proxyRes.statusCode ?? 200, resHeaders);
|
||||
proxyRes.pipe(clientRes);
|
||||
return;
|
||||
}
|
||||
|
||||
// HTML: buffer the full body, inject the script, then send.
|
||||
const chunks: Buffer[] = [];
|
||||
proxyRes.on('data', (chunk: Buffer) => chunks.push(chunk));
|
||||
proxyRes.on('end', () => {
|
||||
const rawHtml = Buffer.concat(chunks).toString('utf-8');
|
||||
const injectedHtml = injectFiberHook(rawHtml);
|
||||
const body = Buffer.from(injectedHtml, 'utf-8');
|
||||
|
||||
// Update Content-Length to match the injected body
|
||||
resHeaders['content-length'] = String(body.byteLength);
|
||||
// Remove Transfer-Encoding: chunked since we send the full body
|
||||
delete resHeaders['transfer-encoding'];
|
||||
|
||||
clientRes.writeHead(proxyRes.statusCode ?? 200, resHeaders);
|
||||
clientRes.end(body);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
proxyReq.on('error', (err) => {
|
||||
console.error(`[originmain proxy] Target request failed: ${err.message}`);
|
||||
if (!clientRes.headersSent) {
|
||||
clientRes.writeHead(502, { 'content-type': 'text/plain' });
|
||||
}
|
||||
clientRes.end(`Originmain proxy: could not reach target at ${opts.target}\n${err.message}`);
|
||||
});
|
||||
|
||||
// Pipe the client request body to the target
|
||||
clientReq.pipe(proxyReq);
|
||||
});
|
||||
|
||||
// ── WebSocket upgrade passthrough (for HMR) ─────────────────────────────
|
||||
|
||||
server.on('upgrade', (req: IncomingMessage, clientSocket: Socket, head: Buffer) => {
|
||||
// Open a TCP connection to the target and forward the HTTP upgrade
|
||||
const targetSocket = netConnect(targetPort, targetHost, () => {
|
||||
// Reconstruct the raw HTTP upgrade request
|
||||
const reqLine = `${req.method} ${req.url} HTTP/${req.httpVersion}\r\n`;
|
||||
const headers = Object.entries(req.headers)
|
||||
.filter(([key]) => key.toLowerCase() !== 'host')
|
||||
.map(([key, val]) => `${key}: ${Array.isArray(val) ? val.join(', ') : val ?? ''}`)
|
||||
.join('\r\n');
|
||||
const hostHeader = `host: ${targetHost}:${targetPort}`;
|
||||
|
||||
targetSocket.write(`${reqLine}${hostHeader}\r\n${headers}\r\n\r\n`);
|
||||
if (head.length > 0) {
|
||||
targetSocket.write(head);
|
||||
}
|
||||
|
||||
// Pipe bidirectionally
|
||||
targetSocket.pipe(clientSocket);
|
||||
clientSocket.pipe(targetSocket);
|
||||
});
|
||||
|
||||
targetSocket.on('error', (err) => {
|
||||
console.error(`[originmain proxy] WebSocket proxy error: ${err.message}`);
|
||||
clientSocket.destroy();
|
||||
});
|
||||
|
||||
clientSocket.on('error', () => {
|
||||
targetSocket.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(opts.port, () => {
|
||||
const proxyUrl = `http://localhost:${opts.port}`;
|
||||
console.log('');
|
||||
console.log(' \x1b[36m\x1b[1mOriginmain\x1b[0m proxy running');
|
||||
console.log('');
|
||||
console.log(` Target: ${opts.target}`);
|
||||
console.log(` Proxy: \x1b[1m${proxyUrl}\x1b[0m`);
|
||||
console.log('');
|
||||
console.log(' Paste the proxy URL into your Originmain artboard\'s');
|
||||
console.log(' "Connect app" field to enable live rendering.');
|
||||
console.log('');
|
||||
console.log(' \x1b[2mFiber hook injection ........ active\x1b[0m');
|
||||
console.log(' \x1b[2mX-Frame-Options stripping ... active\x1b[0m');
|
||||
console.log(' \x1b[2mWebSocket passthrough ....... active\x1b[0m');
|
||||
console.log('');
|
||||
});
|
||||
|
||||
return {
|
||||
close() {
|
||||
server.close();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": false,
|
||||
"outDir": "dist",
|
||||
"declaration": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -52,12 +52,17 @@ export const githubIngester: OriginIngester<GitHubPullRequestPayload> = {
|
||||
return GitHubPullRequestPayloadSchema.parse(raw);
|
||||
},
|
||||
|
||||
ingest(payload): IngestionResult {
|
||||
ingest(payload, opts?: { deploymentUrl?: string }): IngestionResult {
|
||||
const { pull_request: pr, repository } = payload;
|
||||
|
||||
// The render URL points to the head commit's deployed preview if available.
|
||||
// Conventionally: https://<pr-number>.<preview-domain> — caller overrides as needed.
|
||||
const renderUrl = pr.html_url;
|
||||
// The render URL should point to a preview deployment (e.g., Vercel or
|
||||
// Netlify auto-deploy), NOT to the GitHub PR page. GitHub.com sets
|
||||
// X-Frame-Options: deny, so pr.html_url cannot be iframed.
|
||||
//
|
||||
// The caller supplies deploymentUrl from a deployment_status webhook or
|
||||
// from the GitHub Deployments API. If unavailable, renderUrl is omitted
|
||||
// and the user can enter a URL manually in the artboard.
|
||||
const renderUrl = opts?.deploymentUrl;
|
||||
|
||||
return {
|
||||
origin: {
|
||||
@@ -78,7 +83,7 @@ export const githubIngester: OriginIngester<GitHubPullRequestPayload> = {
|
||||
},
|
||||
},
|
||||
artboardTitle: `PR #${pr.number}: ${pr.title}`,
|
||||
renderUrl,
|
||||
...(renderUrl !== undefined ? { renderUrl } : {}),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -14,8 +14,9 @@ export interface IngestionResult {
|
||||
export interface OriginIngester<TPayload> {
|
||||
/** Validates and parses the raw webhook payload. Throws ZodError on invalid input. */
|
||||
parsePayload(raw: unknown): TPayload;
|
||||
/** Converts a validated payload into an IngestionResult. */
|
||||
ingest(payload: TPayload): IngestionResult;
|
||||
/** Converts a validated payload into an IngestionResult.
|
||||
* @param opts — Connector-specific options (e.g., deploymentUrl for GitHub). */
|
||||
ingest(payload: TPayload, opts?: Record<string, unknown>): IngestionResult;
|
||||
}
|
||||
|
||||
// ── Shared helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "@originmain/live",
|
||||
"version": "0.0.1",
|
||||
"private": false,
|
||||
"description": "Originmain live rendering SDK — installs fiber hook for component inspection. Import before React.",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"files": ["src", "README.md"],
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.0"
|
||||
},
|
||||
"keywords": ["originmain", "react", "devtools", "fiber", "design-engineering"],
|
||||
"license": "MIT"
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
// ── Originmain Fiber Hook ────────────────────────────────────────────────────
|
||||
// This module installs a React DevTools–compatible global hook BEFORE React
|
||||
// evaluates its module body. React checks for __REACT_DEVTOOLS_GLOBAL_HOOK__
|
||||
// exactly once at import time; any later installation is too late.
|
||||
//
|
||||
// The hook only activates when the app is iframed by Originmain (detected by
|
||||
// the `om:` prefix in `window.name`, which LiveArtboard.tsx sets on the
|
||||
// <iframe> element). Outside an Originmain iframe the module is a no-op.
|
||||
//
|
||||
// This file is intentionally self-contained — no imports from other
|
||||
// @originmain/* packages — because it ships as a public npm package.
|
||||
|
||||
const RENDERER_SOURCE = 'originmain-renderer';
|
||||
const NAME_PREFIX = 'om:';
|
||||
|
||||
// ── Guard: only run inside an Originmain iframe ──────────────────────────────
|
||||
|
||||
function isOriginmainIframe(): boolean {
|
||||
try {
|
||||
return window.parent !== window
|
||||
&& typeof window.name === 'string'
|
||||
&& window.name.startsWith(NAME_PREFIX);
|
||||
} catch {
|
||||
// Accessing window.parent can throw in certain sandboxed contexts.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isOriginmainIframe()) {
|
||||
installFiberHook();
|
||||
}
|
||||
|
||||
// ── Core ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
function installFiberHook(): void {
|
||||
const artboardId = window.name.slice(NAME_PREFIX.length);
|
||||
|
||||
function post(msg: Record<string, unknown>): void {
|
||||
try {
|
||||
window.parent.postMessage(
|
||||
{ source: RENDERER_SOURCE, artboardId, message: msg },
|
||||
'*',
|
||||
);
|
||||
} catch {
|
||||
// Parent frame unreachable — silently ignore.
|
||||
}
|
||||
}
|
||||
|
||||
// Install or wrap the global hook. If React DevTools is already present,
|
||||
// we wrap its onCommitFiberRoot so both receive commits.
|
||||
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;
|
||||
}
|
||||
|
||||
const originalOnCommit = hook.onCommitFiberRoot;
|
||||
|
||||
hook.onCommitFiberRoot = function onCommitFiberRoot(...args: unknown[]) {
|
||||
// Delegate to the previous handler (React DevTools) first.
|
||||
if (typeof originalOnCommit === 'function') {
|
||||
try { originalOnCommit.apply(this, args); }
|
||||
catch { /* don't break DevTools */ }
|
||||
}
|
||||
|
||||
try {
|
||||
// args[1] is the FiberRoot — { current: Fiber }
|
||||
const root = args[1] as { current: FiberLike } | undefined;
|
||||
if (!root?.current) return;
|
||||
const tree = serializeFiber(root.current);
|
||||
post({ type: 'FIBER_TREE_UPDATE', root: tree });
|
||||
} catch (err) {
|
||||
post({ type: 'ERROR', message: String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// Signal readiness.
|
||||
post({ type: 'READY' });
|
||||
}
|
||||
|
||||
// ── Fiber Serialization ──────────────────────────────────────────────────────
|
||||
|
||||
interface FiberLike {
|
||||
type: unknown;
|
||||
index: number;
|
||||
child: FiberLike | null;
|
||||
sibling: FiberLike | null;
|
||||
stateNode: unknown;
|
||||
memoizedProps: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
interface SerializedNode {
|
||||
id: string;
|
||||
name: string;
|
||||
props: Record<string, string | number | boolean | null>;
|
||||
children: SerializedNode[];
|
||||
domRect?: { x: number; y: number; width: number; height: number };
|
||||
}
|
||||
|
||||
function serializeFiber(fiber: FiberLike | null): SerializedNode | null {
|
||||
if (!fiber) return null;
|
||||
|
||||
const name = getDisplayName(fiber);
|
||||
if (!name) return serializeFiber(fiber.child);
|
||||
|
||||
const rect = getDomRect(fiber);
|
||||
const node: SerializedNode = {
|
||||
id: String(fiber.index || Math.random()),
|
||||
name,
|
||||
props: serializeProps(fiber.memoizedProps),
|
||||
children: [],
|
||||
};
|
||||
if (rect) {
|
||||
node.domRect = rect;
|
||||
}
|
||||
|
||||
let child = fiber.child;
|
||||
while (child) {
|
||||
const serialized = serializeFiber(child);
|
||||
if (serialized) node.children.push(serialized);
|
||||
child = child.sibling;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
function getDisplayName(fiber: FiberLike): string | null {
|
||||
const type = fiber.type;
|
||||
if (!type) return null;
|
||||
if (typeof type === 'string') return type;
|
||||
if (typeof type === 'function') {
|
||||
return (type as { displayName?: string; name?: string }).displayName
|
||||
?? (type as { name?: string }).name
|
||||
?? null;
|
||||
}
|
||||
if (typeof type === 'object' && type !== null && '$$typeof' in type) {
|
||||
return (type as { displayName?: string; name?: string }).displayName
|
||||
?? (type as { name?: string }).name
|
||||
?? null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getDomRect(fiber: FiberLike): { x: number; y: number; width: number; height: number } | null {
|
||||
try {
|
||||
const dom = fiber.stateNode;
|
||||
if (dom && typeof dom === 'object' && 'getBoundingClientRect' in dom) {
|
||||
const r = (dom as Element).getBoundingClientRect();
|
||||
return { x: r.x, y: r.y, width: r.width, height: r.height };
|
||||
}
|
||||
} catch { /* no DOM node */ }
|
||||
return null;
|
||||
}
|
||||
|
||||
function serializeProps(
|
||||
props: Record<string, unknown> | null,
|
||||
): Record<string, string | number | boolean | null> {
|
||||
if (!props || typeof props !== 'object') return {};
|
||||
const out: Record<string, string | number | boolean | null> = {};
|
||||
for (const key in props) {
|
||||
if (key === 'children') continue;
|
||||
const val = props[key];
|
||||
const t = typeof val;
|
||||
if (t === 'string' || t === 'number' || t === 'boolean' || val === null) {
|
||||
out[key] = val as string | number | boolean | null;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// @originmain/live — Originmain fiber hook for live component inspection.
|
||||
//
|
||||
// Usage:
|
||||
// import '@originmain/live'; // MUST be before any React import
|
||||
// import React from 'react';
|
||||
// ...
|
||||
//
|
||||
// This is a side-effect-only import. It installs __REACT_DEVTOOLS_GLOBAL_HOOK__
|
||||
// before React evaluates, enabling Originmain to inspect the component tree,
|
||||
// read props, and generate diffs. The hook only activates when the app is
|
||||
// rendered inside an Originmain artboard iframe — otherwise it is a complete
|
||||
// no-op with zero runtime cost.
|
||||
|
||||
import './hook.js';
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -110,5 +110,121 @@ export function buildFiberHookScript(artboardId: string): string {
|
||||
})(${JSON.stringify(artboardId)});`;
|
||||
}
|
||||
|
||||
// ── Proxy-compatible fiber hook script ────────────────────────────────────────
|
||||
// Unlike buildFiberHookScript (which bakes in an artboard ID), this version
|
||||
// reads the artboard ID from `window.name` at runtime. The iframe element sets
|
||||
// `name="om:<artboardId>"` and this script extracts the ID.
|
||||
//
|
||||
// This script is injected by the CLI proxy (`@originmain/cli`) into every HTML
|
||||
// response, and is also used by the `@originmain/live` SDK. It is fully
|
||||
// self-contained — no imports, no dependencies.
|
||||
|
||||
export function buildProxyFiberHookScript(): string {
|
||||
return `(function() {
|
||||
// Only activate inside an Originmain iframe
|
||||
if (window.parent === window) return;
|
||||
|
||||
var NAME_PREFIX = 'om:';
|
||||
var artboardId = '';
|
||||
try {
|
||||
if (typeof window.name === 'string' && window.name.indexOf(NAME_PREFIX) === 0) {
|
||||
artboardId = window.name.slice(NAME_PREFIX.length);
|
||||
}
|
||||
} catch (e) { /* window.name access denied — not our iframe */ }
|
||||
if (!artboardId) return;
|
||||
|
||||
var SOURCE = ${JSON.stringify(RENDERER_SOURCE)};
|
||||
|
||||
function post(msg) {
|
||||
try { window.parent.postMessage({ source: SOURCE, artboardId: artboardId, message: msg }, '*'); }
|
||||
catch (e) { /* parent unreachable */ }
|
||||
}
|
||||
|
||||
// Install the React DevTools global hook BEFORE React loads.
|
||||
// If React DevTools extension is already present, wrap its handler.
|
||||
var hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
||||
if (!hook) {
|
||||
hook = { renderers: new Map(), supportsFiber: true, _isDisabled: false };
|
||||
window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook;
|
||||
}
|
||||
|
||||
var originalOnCommit = hook.onCommitFiberRoot;
|
||||
|
||||
hook.onCommitFiberRoot = function(rendererId, root, priorityLevel, didError) {
|
||||
if (typeof originalOnCommit === 'function') {
|
||||
try { originalOnCommit.call(this, rendererId, root, priorityLevel, didError); }
|
||||
catch (e) { /* don't break DevTools */ }
|
||||
}
|
||||
try {
|
||||
var fiberRoot = root.current;
|
||||
var tree = serializeFiber(fiberRoot);
|
||||
post({ type: 'FIBER_TREE_UPDATE', root: tree });
|
||||
} catch (err) {
|
||||
post({ type: 'ERROR', message: String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
function serializeFiber(fiber) {
|
||||
if (!fiber) return null;
|
||||
var name = getDisplayName(fiber);
|
||||
if (!name) return serializeFiber(fiber.child) || null;
|
||||
|
||||
var rect = getDomRect(fiber);
|
||||
var node = {
|
||||
id: String(fiber.index || Math.random()),
|
||||
name: name,
|
||||
props: serializeProps(fiber.memoizedProps),
|
||||
children: [],
|
||||
};
|
||||
if (rect) node.domRect = rect;
|
||||
|
||||
var child = fiber.child;
|
||||
while (child) {
|
||||
var serialized = serializeFiber(child);
|
||||
if (serialized) node.children.push(serialized);
|
||||
child = child.sibling;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
function getDisplayName(fiber) {
|
||||
var type = fiber.type;
|
||||
if (!type) return null;
|
||||
if (typeof type === 'string') return type;
|
||||
if (typeof type === 'function') return type.displayName || type.name || null;
|
||||
if (type.$$typeof) return type.displayName || type.name || null;
|
||||
return null;
|
||||
}
|
||||
|
||||
function getDomRect(fiber) {
|
||||
try {
|
||||
var dom = fiber.stateNode;
|
||||
if (dom && typeof dom.getBoundingClientRect === 'function') {
|
||||
var r = dom.getBoundingClientRect();
|
||||
return { x: r.x, y: r.y, width: r.width, height: r.height };
|
||||
}
|
||||
} catch (e) { /* no DOM node */ }
|
||||
return null;
|
||||
}
|
||||
|
||||
function serializeProps(props) {
|
||||
if (!props || typeof props !== 'object') return {};
|
||||
var out = {};
|
||||
for (var key in props) {
|
||||
if (key === 'children') continue;
|
||||
var val = props[key];
|
||||
var t = typeof val;
|
||||
if (t === 'string' || t === 'number' || t === 'boolean' || val === null) {
|
||||
out[key] = val;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Signal that the fiber hook is installed and the iframe is ready.
|
||||
post({ type: 'READY' });
|
||||
})();`;
|
||||
}
|
||||
|
||||
// Re-export types for convenience
|
||||
export type { FiberNode, DOMRectLike };
|
||||
|
||||
@@ -16,7 +16,7 @@ export type {
|
||||
RendererEnvelope,
|
||||
} from './protocol.js';
|
||||
|
||||
export { buildFiberHookScript } from './fiber-hook.js';
|
||||
export { buildFiberHookScript, buildProxyFiberHookScript } from './fiber-hook.js';
|
||||
|
||||
export {
|
||||
createRendererHostConfig,
|
||||
|
||||
Generated
+19
@@ -150,6 +150,19 @@ importers:
|
||||
specifier: ^5.5.0
|
||||
version: 5.9.3
|
||||
|
||||
packages/cli:
|
||||
dependencies:
|
||||
'@originmain/renderer':
|
||||
specifier: workspace:*
|
||||
version: link:../renderer
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^22.0.0
|
||||
version: 22.19.17
|
||||
typescript:
|
||||
specifier: ^5.5.0
|
||||
version: 5.9.3
|
||||
|
||||
packages/design-language:
|
||||
dependencies:
|
||||
zod:
|
||||
@@ -207,6 +220,12 @@ importers:
|
||||
specifier: ^5.5.0
|
||||
version: 5.9.3
|
||||
|
||||
packages/live-sdk:
|
||||
devDependencies:
|
||||
typescript:
|
||||
specifier: ^5.5.0
|
||||
version: 5.9.3
|
||||
|
||||
packages/multiplayer:
|
||||
dependencies:
|
||||
'@liveblocks/client':
|
||||
|
||||
+3
-1
@@ -20,7 +20,9 @@
|
||||
"@originmain/integrations": ["./packages/integrations/src/index.ts"],
|
||||
"@originmain/design-language": ["./packages/design-language/src/index.ts"],
|
||||
"@originmain/multiplayer": ["./packages/multiplayer/src/index.ts"],
|
||||
"@originmain/platform": ["./packages/platform/src/index.ts"]
|
||||
"@originmain/platform": ["./packages/platform/src/index.ts"],
|
||||
"@originmain/cli": ["./packages/cli/src/index.ts"],
|
||||
"@originmain/live": ["./packages/live-sdk/src/index.ts"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user