Skip to content

ByteBard97/vuemorphic

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

73 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

vuemorphic

Vuemorphic

An agentic harness for automated React→Vue 3 SFC translation, powered by LangGraph and Claude Code.

What It Does

Vuemorphic drives a two-phase pipeline that reads a React JSX codebase, analyzes its structure, and produces idiomatic Vue 3 Single File Components — one component at a time, in dependency order, verified at every step.

Built to port Flora CAD — a native-plants landscape design tool — from a React prototype to a Vue 3 production app.

Pipeline

Phase What it does
A — Analysis JSX AST extraction, idiom detection, Vue skeleton generation, SQLite import, topological sort
B — Translation LangGraph loop: pick → prompt → claude --print → verify → commit or queue for review
flowchart LR

    subgraph phaseA ["Phase A — Analysis"]
        direction TB
        jsxFiles[".jsx source files"]
        astExtract["ts-morph AST<br>extraction"]
        idiomDetect["idiom detection"]
        skeleton["Vue SFC skeleton<br>generation"]
        dbImport["JSON → SQLite<br>manifest import"]
        topoSort["topological sort<br>by dependency"]
        jsxFiles --> astExtract --> idiomDetect --> skeleton --> dbImport --> topoSort
    end

    manifest[("vuemorphic.db<br>+ Vue skeletons")]

    subgraph phaseB ["Phase B — Translation"]
        direction TB
        pickNode["pick next node<br>(haiku, topo order)"]
        buildCtx["build context<br>React src + Vue deps + idioms"]
        claudeCall["claude --print --no-tools<br>(haiku by default)"]
        verifySnip["verify<br>remnant · postfilter · compile · vue-tsc"]
        gitCommit["git commit<br>feat(convert): NodeId"]
        pickNode --> buildCtx --> claudeCall --> verifySnip
        verifySnip -->|PASS| gitCommit
        verifySnip -->|FAIL| review["queue for review<br>---BLOCKED--- form"]
    end

    snippets[/"corpora/claude-design-vue/<br>(git repo, one commit per component)"/]

    phaseA ==> manifest
    manifest ==> phaseB
    phaseB ==> snippets

    classDef io fill:#fed7aa,stroke:#c2410c,color:#374151
    classDef ai fill:#ddd6fe,stroke:#6d28d9,color:#374151
    classDef success fill:#a7f3d0,stroke:#047857,color:#374151
    classDef review fill:#fecaca,stroke:#b91c1c,color:#374151

    class manifest,snippets io
    class claudeCall ai
    class gitCommit success
    class review review
Loading

Example: React → Vue 3

Five real before/after pairs are in examples/ — a simple control, a complex drag control, a layout screen, and Pill (3 visual variants).

A typical conversion. Input (React JSX):

function TweakSlider({ label, value, min = 0, max = 100, step = 1, unit = '', onChange }) {
  return (
    <TweakRow label={label} value={`${value}${unit}`}>
      <input type="range" className="twk-slider" min={min} max={max} step={step}
             value={value} onChange={(e) => onChange(Number(e.target.value))} />
    </TweakRow>
  );
}

Output (Vue 3 SFC, auto-generated):

<template>
  <TweakRow :label="label" :value="`${value}${unit}`">
    <input
      type="range"
      class="twk-slider"
      :min="min" :max="max" :step="step" :value="value"
      @change="(e) => emit('change', Number((e.target as HTMLInputElement).value))"
    />
  </TweakRow>
</template>

<script setup lang="ts">
import TweakRow from './TweakRow.vue'

interface TweakSliderProps {
  label: string
  value: number
  min?: number
  max?: number
  step?: number
  unit?: string
}

const props = withDefaults(defineProps<TweakSliderProps>(), { min: 0, max: 100, step: 1, unit: '' })
const emit = defineEmits<{ change: [value: number] }>()
</script>

The agent: inferred onChangeemit('change', ...), translated classNameclass, typed e.target as HTMLInputElement, converted prop defaults to withDefaults, and typed the defineEmits signature — all without being told to.

Results

Across three real Claude Design projects (produced with Claude Artifacts):

Project Components Auto-converted Manual fixes Pass rate
argus (X-Plane Sim HiFi) 46 41 (89%) 5 (11%) 100%
jj-canvas (Jujutsu VCS UI) 30 28 (93%) 2 (7%) 100%
signal-canvas (Design canvas) 43 ~40 (92%) ~3 (8%) 100%

All 119 resulting .vue files pass vue-tsc --noEmit with no errors.

Pixel comparison vs. original (Playwright screenshot diff, after fixing 5 Vue 3.5 rendering bugs found during conversion):

Screen Before After
signal-canvas mf-main-light 68.9% diff 24.1% diff
signal-canvas mf-main-dark 55.9% diff 16.2% diff
argus v2-main 30.4% diff 17.7% diff

Remaining diff is primarily SVG <defs> pattern fills (Chrome renders them black before Vue's reactive updates propagate — not fixable with the current approach) and font rendering noise.

Vue 3.5 bugs discovered during conversion (documented in the prompt template so the agent avoids them):

  1. Props destructuring (const { w } = props) loses reactivity — values evaluate as NaN
  2. Numeric CSS values need explicit "px" suffix — :style="{ left: 812 }" is silently ignored
  3. SVG <defs> reactive bindings (gradients, patterns) aren't applied by Chrome at first paint
  4. fillOpacity/strokeOpacity camelCase props are ignored — use :style="{ fillOpacity: 0.5 }"
  5. SVG presentation attributes need kebab-case — Vue doesn't auto-convert strokeWidthstroke-width

Quick Start

You'll need to bring your own React corpus. Configure vuemorphic.config.json to point source_repo at your React source directory and target_repo at where you want the Vue output. See the config file for all options.

All runtime output (databases, manifests, snippets, review queue) goes into temp/ automatically — that folder is gitignored.

# Install (requires uv)
uv sync

# Phase A: extract AST, detect idioms, generate Vue skeletons, import to SQLite
vuemorphic phase-a --heuristic-tiers

# Phase B: translate all nodes in topological order (parallel, 10 workers)
vuemorphic phase-b

# Translate first 3 nodes only (for testing)
vuemorphic phase-b --max-nodes 3

# Inspect blocked nodes (failed with ---BLOCKED--- form)
vuemorphic blocked

# Manually escalate a blocked node to sonnet tier
vuemorphic escalate NodeId --tier sonnet

Backends

Phase B supports two backends — set backend in vuemorphic.config.json:

Backend Config Requirement
"claude" (default) "start_tier": "haiku" Claude Max subscription
"anthropic-api" "start_tier": "haiku" ANTHROPIC_API_KEY env var

An Ollama backend is implemented ("backend": "ollama") but has not been run end-to-end — treat it as experimental and expect rough edges.

Run with Anthropic API key

export ANTHROPIC_API_KEY=sk-ant-...

# Set backend in vuemorphic.config.json
{ "backend": "anthropic-api", "start_tier": "haiku" }

vuemorphic phase-b

Project Structure

vuemorphic/
├── src/vuemorphic/
│   ├── agents/          # Prompt assembly (context.py) and Claude invocation (invoke.py)
│   │   └── prompt_template.py  # Conversion prompt template (triple-quoted, easy to edit)
│   ├── analysis/        # Phase A: component_contracts.py extracts Vue-ready contracts
│   ├── graph/           # LangGraph StateGraph (nodes.py, graph.py, state.py)
│   ├── models/          # SQLite manifest (manifest.py, db.py) and ComponentContract
│   ├── skeleton/        # Vue SFC skeleton builder (script, template, style sections)
│   ├── verification/    # 5-tier Vue verification pipeline (verify.py)
│   └── cli.py           # Typer CLI entry point
├── phase_a_scripts/     # ts-morph JSX scripts (A1 AST extraction, A2 idiom detection)
├── corpora/             # Source React codebase + output Vue project (gitignored locally)
├── docs/                # Architecture docs and research
├── idiom_dictionary.md  # React→Vue idiom guidance injected into prompts
└── vuemorphic.config.json  # Paths, model tiers, parallelism config

How Translation Works

Every React component in the source codebase becomes a node in vuemorphic.db. Nodes are processed in topological order: by the time a component is translated, all its dependencies are already in Vue.

For each node, Phase B:

  1. Assembles a prompt with the React JSX source, Vue skeleton, converted dependency .vue files, and relevant idiom guidance
  2. Calls claude --print --no-tools as a subprocess (Claude Max subscription auth — no API key needed)
  3. Verifies the output through 5 tiers: React remnant check → postfilter → @vue/compiler-sfc parse → vue-tsc --noEmit → PASS
  4. On PASS: git commit to the Vue project repo with the agent's summary as the commit message
  5. On FAIL: queues for human review with the agent's structured ---BLOCKED--- diagnosis form
stateDiagram-v2
    direction LR

    [*] --> pick_next_node

    state "pick_next_node<br>claim next eligible node (haiku)" as pick_next_node
    state "build_context<br>React src + Vue deps + idiom hints" as build_context
    state "invoke_agent<br>claude --print --no-tools" as invoke_agent
    state "verify<br>remnant · postfilter · compile · vue-tsc" as verify
    state "requeue_node<br>cascade error in other file — retry later" as requeue_node
    state "update_manifest<br>git commit, mark CONVERTED" as update_manifest
    state "queue_for_review<br>BLOCKED form recorded" as queue_for_review

    pick_next_node --> build_context : nodes remain
    pick_next_node --> [*] : all done or hard-stopped

    build_context --> invoke_agent
    invoke_agent --> verify

    verify --> update_manifest : PASS
    verify --> requeue_node : CASCADE (errors in other files)
    verify --> queue_for_review : FAIL

    requeue_node --> pick_next_node
    update_manifest --> pick_next_node
    queue_for_review --> pick_next_node

    pick_next_node:::primary
    invoke_agent:::ai
    verify:::decision
    update_manifest:::success
    queue_for_review:::error
    requeue_node:::trigger

    classDef primary fill:#3b82f6,stroke:#1e3a5f,color:#ffffff
    classDef ai fill:#ddd6fe,stroke:#6d28d9,color:#374151
    classDef decision fill:#fef3c7,stroke:#b45309,color:#374151
    classDef success fill:#a7f3d0,stroke:#047857,color:#374151
    classDef error fill:#fecaca,stroke:#b91c1c,color:#374151
    classDef trigger fill:#fed7aa,stroke:#c2410c,color:#374151
Loading

Parallel Mode

Phase B runs multiple workers in parallel using git worktrees. Each worker gets an isolated copy of the Vue target project, converts and verifies independently, then cherry-picks its commits back to the main branch. No cascade cross-contamination between workers.

# Set parallelism in vuemorphic.config.json
{ "parallelism": 10, ... }

# Workers create corpora/claude-design-vue-worker-{N}/ automatically
# and clean up after themselves

Failure Diagnosis

When an agent can't complete a conversion, it fills in a structured ---BLOCKED--- form:

---BLOCKED---
CATEGORY: info_gap | prompt_confusion | tooling | complexity | cascade | unknown
MISSING:  what specific information was absent
TRIED:    what approaches were attempted
FIX:      one concrete change to the harness or prompt that would help

This is stored in the DB and surfaced by vuemorphic blocked. Use vuemorphic escalate to manually promote a node to a higher model tier after reviewing the diagnosis.

Known Limitations

  • Scope: Claude Design artifacts. The pipeline was built and tested on React apps generated by Claude's artifact tool (claude.ai). These tend to be single-file or few-file JSX projects using inline styles — no Tailwind, no styled-components, no complex build setup. Performance on production React codebases is untested.

  • SVG <defs> patterns. Chrome doesn't apply Vue's reactive bindings to <pattern>, <linearGradient>, or <radialGradient> fills before first paint. The workaround (direct :fill bindings instead of url(#id)) produces correct behavior but loses the indirection. Canvases with heavy SVG pattern textures will look different until this Chrome behavior changes.

  • One haiku attempt per node. With max_attempts: {haiku: 1} the failure rate on complex layout components is ~10%. Enabling multiple attempts (or escalation to sonnet) drops this to ~2% but costs more.

  • No router/state management migration. The tool translates components. It doesn't migrate React Router to Vue Router, React Context to Pinia, or React hooks to composables — that's handled by the idiom dictionary hints, but complex patterns may require manual review.

  • vue-tsc strict mode required. The verification pipeline runs vue-tsc --noEmit. Projects with lenient TypeScript configs may silently accept incorrect types that stricter projects would catch.

License

MIT

About

Agentic React→Vue 3 translation harness using LangGraph and Claude Code

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors