English | 中文
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.
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
This repository is a technical proof-of-concept demonstrating SWC plugin development with Rust/WASM.
- 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
-
Compile-time Injection (Rust/WASM) - Custom SWC plugin written in Rust, compiled to
wasm32-wasip1, injects uniquedata-vididentifiers into each JSX element during compilation -
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 -
Runtime Editing - React component identifies editable elements via
data-vidin the browser, style modifications directly manipulate DOM for live preview -
Backend AST Writeback - On save, Next.js API route uses
@swc/coreto re-parse source file, locate target node by global index, and updatestyleattribute -
Hot Update Sync - File write triggers Next.js HMR for seamless page updates
┌─────────────────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────────────────────┘
- Node.js 18+
- Rust toolchain (for plugin development)
- wasm32-wasip1 target:
rustup target add wasm32-wasip1
# Install dependencies
npm install
# Start development server (Turbopack)
npm run devVisit http://localhost:3000
cd swc-plugin
cargo build --target wasm32-wasip1 --release
cp target/wasm32-wasip1/release/swc_plugin.wasm ../swc_plugin.wasmreact-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
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_corev50.2.3 (compatible with Next.js 16.1) - Compiles to
wasm32-wasip1target - Global index counter increments for every JSX element (depth-first order)
VID Format: filename:elementName:globalIndex
- Example:
page.tsx:h1:6means the 7th JSX element in page.tsx, which is anh1
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
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)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-editDynamic 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
}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 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": "..." }
}| 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 |
| 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) |
- Development Mode Only - Edit functionality is only available when running
npm run dev - Version Control - Changes are written directly to source files, Git usage is recommended
- Dynamic Content - Elements containing
{state}expressions cannot be edited (toast notification shown) - Text Elements - Only supports p, span, button, h1-h6, a, label, div and similar text elements
- SWC Version Compatibility - Plugin requires swc_core v47-51 for Next.js 16.1
MIT