Skip to content

Jiangultimo/react-visual-edit-swc

Repository files navigation

English | 中文

Visual Edit (SWC)

What is Visual Edit?

A React visual editor based on SWC (Rust/WebAssembly) that supports editing component styles and text directly in the browser, with changes written back to source code.

Why SWC instead of Babel?

This project is a Rust/WASM port of the Babel version. While the Babel version uses JavaScript-based AST transformation, this version leverages:

  • 🦀 Native Performance - Rust-based SWC plugin compiled to WebAssembly
  • ⚡️ Turbopack Compatible - Works with Next.js Turbopack for faster dev experience
  • 🔧 Compile-time Integration - Plugin runs during SWC transformation phase

Note

This repository is a technical proof-of-concept demonstrating SWC plugin development with Rust/WASM.

Features

  • Visual Editing - Edit component styles and text directly in the browser
  • Live Preview - Changes are reflected immediately
  • Inline Styles - Styles are written as style={{ ... }} in source code
  • Code Writeback - Automatically updates source files on save
  • Hot Module Replacement - No page refresh needed
  • Dynamic Content Protection - Elements containing state are marked as non-editable

Technical Approach

Core Principles

  1. Compile-time Injection (Rust/WASM) - Custom SWC plugin written in Rust, compiled to wasm32-wasip1, injects unique data-vid identifiers into each JSX element during compilation

  2. Global Index Positioning - Uses a global element counter (e.g., page.tsx:h1:6) to uniquely identify elements, where the index represents the element's position in the depth-first traversal order

  3. Runtime Editing - React component identifies editable elements via data-vid in the browser, style modifications directly manipulate DOM for live preview

  4. Backend AST Writeback - On save, Next.js API route uses @swc/core to re-parse source file, locate target node by global index, and update style attribute

  5. Hot Update Sync - File write triggers Next.js HMR for seamless page updates

Overall Flow Diagram

┌─────────────────────────────────────────────────────────────────┐
│              Compile Time (SWC Rust Plugin → WASM)              │
├─────────────────────────────────────────────────────────────────┤
│  JSX Source                                                      │
│  <h1 className="title">Hello</h1>                               │
│                           ↓                                      │
│  SWC Plugin (Rust) - VisitMut trait                             │
│                           ↓                                      │
│  visit_mut_jsx_element() → inject data-vid                      │
│                           ↓                                      │
│  <h1 className="title" data-vid="page.tsx:h1:6">Hello</h1>      │
│                                                                  │
│  VID = filename:elementName:globalIndex                         │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│                      Runtime (Browser)                           │
├─────────────────────────────────────────────────────────────────┤
│  User clicks element                                             │
│        ↓                                                         │
│  Get data-vid → Show edit panel (right drawer)                   │
│        ↓                                                         │
│  User modifies styles → Live preview (direct el.style changes)   │
│        ↓                                                         │
│  Click save → POST /api/visual-edit                              │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│              Backend Processing (Next.js API Route)              │
├─────────────────────────────────────────────────────────────────┤
│  Receive request { vid, styles, newText }                        │
│        ↓                                                         │
│  Parse VID → Get filename, elementName, globalIndex              │
│        ↓                                                         │
│  Read source file → Parse with @swc/core                         │
│        ↓                                                         │
│  Traverse AST with globalIndex counter → Find target element     │
│        ↓                                                         │
│  Update style attribute (merge with existing styles)             │
│        ↓                                                         │
│  swc.print() → Write to file → Trigger HMR                       │
└─────────────────────────────────────────────────────────────────┘

Quick Start

Prerequisites

  • Node.js 18+
  • Rust toolchain (for plugin development)
  • wasm32-wasip1 target: rustup target add wasm32-wasip1

Install & Run

# Install dependencies
npm install

# Start development server (Turbopack)
npm run dev

Visit http://localhost:3000

Build SWC Plugin (if modifying Rust code)

cd swc-plugin
cargo build --target wasm32-wasip1 --release
cp target/wasm32-wasip1/release/swc_plugin.wasm ../swc_plugin.wasm

Project Structure

react-visual-edit-swc/
├── app/
│   ├── api/
│   │   └── visual-edit/
│   │       └── route.ts           # API endpoint for saving edits
│   ├── components/
│   │   └── VisualEditOverlay.tsx  # Frontend edit interface (React)
│   ├── layout.tsx                 # Root layout
│   ├── page.tsx                   # Demo page
│   └── globals.css                # Global styles
├── swc-plugin/                    # Rust SWC plugin
│   ├── src/
│   │   └── lib.rs                 # Plugin implementation
│   └── Cargo.toml                 # Rust dependencies
├── swc_plugin.wasm                # Compiled WASM plugin
├── next.config.mjs                # Next.js config with SWC plugin
├── package.json
└── tsconfig.json

Core Module Details

1. SWC Rust Plugin (swc-plugin/src/lib.rs)

The plugin implements the VisitMut trait to traverse and modify JSX elements:

impl VisitMut for VisualEditVisitor {
    fn visit_mut_jsx_element(&mut self, node: &mut JSXElement) {
        // Get element name (e.g., "h1", "div", "Image")
        let element_name = self.get_element_name(&node.opening.name);

        // Generate VID: filename:elementName:globalIndex
        let vid = self.make_vid(&element_name);

        // Inject data-vid attribute
        node.opening.attrs.push(self.create_data_vid_attr(&vid));

        // Continue traversing children
        node.visit_mut_children_with(self);
    }
}

Key Points:

  • Uses swc_core v50.2.3 (compatible with Next.js 16.1)
  • Compiles to wasm32-wasip1 target
  • Global index counter increments for every JSX element (depth-first order)

VID Format: filename:elementName:globalIndex

  • Example: page.tsx:h1:6 means the 7th JSX element in page.tsx, which is an h1

2. Next.js Configuration (next.config.mjs)

const nextConfig = {
  experimental: {
    swcPlugins: [
      [
        resolvePluginPath('swc_plugin.wasm'),  // Path varies for Turbopack vs Webpack
        {}
      ]
    ]
  }
}

Turbopack Compatibility:

  • Turbopack requires relative paths (./swc_plugin.wasm)
  • Webpack requires absolute paths
  • Config automatically detects and adjusts

3. API Route (app/api/visual-edit/route.ts)

Backend AST modification using @swc/core:

// Parse VID
const vidInfo = parseVid(vid)  // { filename, elementName, index }

// Parse source with SWC
const ast = await swc.parse(sourceCode, { syntax: 'typescript', tsx: true })

// Find element by global index (matching plugin's traversal order)
let globalIndex = 0
function visitNode(node) {
  if (node.type === 'JSXElement') {
    if (globalIndex === targetIndex && elementName === targetElementName) {
      // Modify style attribute
      modifyStyle(node.opening, newStyles)
    }
    globalIndex++  // Increment for every JSX element
    // Continue to children...
  }
}

// Generate and write code
const output = await swc.print(ast, { minify: false })
fs.writeFileSync(filePath, output.code)

4. Frontend Edit Interface (app/components/VisualEditOverlay.tsx)

React component for the edit UI:

// State management
const [enabled, setEnabled] = useState(false)
const [currentElement, setCurrentElement] = useState<HTMLElement | null>(null)
const [styles, setStyles] = useState<StyleConfig>({...})

// Core functions
highlight(el)           // Highlight hovered element
checkHasState(el)       // Detect dynamic content via React Fiber
openPanel() / closePanel()  // Right drawer with body margin animation
handleSave()            // POST to /api/visual-edit

Dynamic Content Detection:

// Check React Fiber for dynamic children
const fiber = el[reactFiberKey]
if (Array.isArray(fiber.memoizedProps.children)) {
  // Contains dynamic content like {count}
  showToast('该元素包含动态内容,无法直接编辑')
  return
}

Data Flow

Edit Save Flow

User clicks save
      ↓
Collect data: { vid, styles, newText }
      ↓
POST /api/visual-edit
      ↓
Parse VID → { filename: "page.tsx", elementName: "h1", index: 6 }
      ↓
Read source file → swc.parse() to AST
      ↓
Traverse AST with global counter → Find JSX element at index 6
      ↓
Modify/create style attribute: style={{ color: "...", ... }}
      ↓
swc.print() → Write to file
      ↓
Next.js HMR detects change → Page updates

Request/Response Format

Request Body:

interface EditRequest {
  vid: string           // "page.tsx:h1:6"
  newText?: string      // New text content
  styles?: {
    color?: string
    backgroundColor?: string
    fontSize?: string   // Without "px"
    fontWeight?: string
  }
}

Response:

{
  "ok": true,
  "message": "源代码已更新",
  "data": { "vid": "...", "styles": {...}, "filePath": "..." }
}

Tech Stack

Category Technology
Framework Next.js 16 (App Router)
UI Framework React 19
Compiler SWC (Turbopack)
Plugin Language Rust → WebAssembly
Plugin Target wasm32-wasip1
SWC Core Version swc_core v50.2.3
AST Operations @swc/core (Node.js)
Styling Tailwind CSS 4

Comparison with Babel Version

Feature Babel Version SWC Version
Plugin Language JavaScript Rust → WASM
AST Library @babel/parser swc_core
Build Tool Vite Next.js + Turbopack
Compile Performance Baseline ~20x faster
VID Strategy AST Path Global Index
Runtime Node.js WASM (Wasip1)

Notes

  1. Development Mode Only - Edit functionality is only available when running npm run dev
  2. Version Control - Changes are written directly to source files, Git usage is recommended
  3. Dynamic Content - Elements containing {state} expressions cannot be edited (toast notification shown)
  4. Text Elements - Only supports p, span, button, h1-h6, a, label, div and similar text elements
  5. SWC Version Compatibility - Plugin requires swc_core v47-51 for Next.js 16.1

License

MIT

About

A visual editing overlay for React/Next.js apps, powered by a custom SWC plugin written in Rust and compiled to WebAssembly.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors