sis, transformation, and code generation. Unlike syntax replacers that operate on token streams, this approach builds a framework-agnostic representation of intent before mapping to target idioms.
Architecture Rationale
- Parsing & AST Construction: The compiler ingests Vue Single File Components (SFCs), extracting template markup,
<script setup> logic, TypeScript types, and <style> blocks into a unified abstract syntax tree.
- Semantic Graph Construction: The compiler resolves reactive bindings, computes dependency graphs for
computed and watch, maps slot contracts, and identifies style scoping boundaries. This stage strips framework-specific syntax while preserving behavioral semantics.
- Framework-Specific Transformation: The semantic graph is mapped to React's execution model. Reactive state becomes
useReactive proxies, computed values become useComputed memoized selectors, event emitters become callback props following React's onEventName convention, and scoped styles become hashed CSS modules with data-attribute isolation.
- Code Generation: The transformed graph is serialized into readable, rule-compliant TypeScript/TSX. Dependency arrays are auto-calculated, memoization boundaries are applied where semantic caching is required, and type definitions are preserved or adapted to React's prop typing system.
Implementation Example
Consider a ReportFilter component in Vue that manages reactive filters, derives filtered results, emits selection changes, and uses scoped styling with a dynamic slot for row rendering.
Vue Source
<script setup lang="ts">
import { reactive, computed } from 'vue'
const emit = defineEmits<{
(e: 'selection-change', ids: string[]): void
}>()
const filters = reactive({
status: 'active',
category: null as string | null
})
const filteredItems = computed(() => {
return props.items.filter(item => {
if (filters.status && item.status !== filters.status) return false
if (filters.category && item.category !== filters.category) return false
return true
})
})
const handleSelect = (id: string) => {
emit('selection-change', [id])
}
defineProps<{
items: Array<{ id: string; name: string; status: string; category: string }>
}>()
</script>
<template>
<div class="filter-panel">
<select v-model="filters.status" class="status-select">
<option value="active">Active</option>
<option value="archived">Archived</option>
</select>
<ul class="item-list">
<li v-for="item in filteredItems" :key="item.id" class="item-row">
<slot :item="item" :on-select="handleSelect" />
</li>
</ul>
</div>
</template>
<style scoped>
.filter-panel {
padding: 16px;
background: #f8f9fa;
}
.status-select {
margin-bottom: 12px;
padding: 6px 10px;
}
.item-row {
padding: 8px 0;
border-bottom: 1px solid #e2e8f0;
}
</style>
Compiled React Output
import { memo } from 'react'
import { useReactive, useComputed } from '@vureact/runtime-core'
import './report-filter-7a3b9c.css'
interface IReportFilterProps {
items: Array<{ id: string; name: string; status: string; category: string }>
onSelectionChange?: (ids: string[]) => void
children?: (slotProps: { item: IReportFilterProps['items'][0]; onSelect: (id: string) => void }) => React.ReactNode
}
const ReportFilter = memo(({ items, onSelectionChange, children }: IReportFilterProps) => {
const filters = useReactive({
status: 'active',
category: null as string | null
})
const filteredItems = useComputed(() => {
return items.filter(item => {
if (filters.status && item.status !== filters.status) return false
if (filters.category && item.category !== filters.category) return false
return true
})
}, [items, filters.status, filters.category])
const handleSelect = (id: string) => {
onSelectionChange?.([id])
}
return (
<div className="filter-panel" data-css-7a3b9c>
<select
value={filters.status}
onChange={(e) => (filters.status = e.target.value)}
className="status-select"
data-css-7a3b9c
>
<option value="active">Active</option>
<option value="archived">Archived</option>
</select>
<ul className="item-list" data-css-7a3b9c>
{filteredItems.map((item) => (
<li key={item.id} className="item-row" data-css-7a3b9c>
{children?.({ item, onSelect: handleSelect })}
</li>
))}
</ul>
</div>
)
})
export default ReportFilter
Compiled Scoped CSS
.filter-panel[data-css-7a3b9c] {
padding: 16px;
background: #f8f9fa;
}
.status-select[data-css-7a3b9c] {
margin-bottom: 12px;
padding: 6px 10px;
}
.item-row[data-css-7a3b9c] {
padding: 8px 0;
border-bottom: 1px solid #e2e8f0;
}
Why This Architecture Works
- Dependency Resolution: The compiler analyzes reactive reads inside
computed and watch callbacks, then generates precise React dependency arrays. This eliminates manual array maintenance and prevents stale closure bugs.
- Event Contract Translation: Vue's
defineEmits uses kebab-case event names. React expects camelCase callback props prefixed with on. The compiler automatically maps selection-change to onSelectionChange, preserving type safety and IDE autocompletion.
- Style Isolation Preservation: Scoped CSS in Vue uses attribute selectors under the hood. The compiler replicates this by hashing the component name, injecting
data-css-{hash} attributes into rendered nodes, and scoping CSS rules accordingly. No runtime style injection is required.
- Pure React Output: The generated code contains zero Vue runtime imports. It relies on standard React patterns (
memo, controlled inputs, callback props) and a lightweight runtime core only for reactive proxy utilities. This ensures compatibility with React DevTools, SSR frameworks, and existing linting rules.
Pitfall Guide
Cross-framework compilation introduces specific failure modes when teams treat it as a black-box converter rather than a semantic translation layer.
1. Ignoring Style Scoping Boundaries
Explanation: Developers often assume CSS will naturally isolate after migration. Without explicit scoping markers, styles leak into sibling components or global namespaces.
Fix: Ensure the compiler injects data-attribute selectors or CSS module hashes. Verify that build pipelines process scoped styles through the same transformation stage as templates and scripts.
2. Overriding Auto-Generated Memoization
Explanation: The compiler applies React.memo and dependency arrays based on semantic analysis. Manually wrapping components in additional memo or useMemo calls can break reactive updates or cause unnecessary re-renders.
Fix: Trust the compiler's memoization boundaries. Only override when profiling reveals specific performance bottlenecks, and document the deviation in code comments.
3. Assuming AI Can Resolve Semantic Drift
Explanation: AI models lack build-time type context and cannot guarantee deterministic output across model versions. Relying on AI to "fix" compiler output introduces instability.
Fix: Use AI strictly for post-compilation tasks: naming cleanup, test generation, or documentation. Keep the compiler as the source of truth for structural transformations.
4. Breaking Event Naming Conventions
Explanation: Vue emits kebab-case events (submit-form), while React expects camelCase callbacks (onSubmitForm). Manual overrides that ignore this convention break type checking and IDE support.
Fix: Configure the compiler to enforce React's onEventName convention. Validate emitted events against TypeScript interfaces during the build step.
5. Migrating Monolithically Instead of Incrementally
Explanation: Attempting to convert an entire codebase in a single PR creates integration hell, breaks CI pipelines, and makes rollback impossible.
Fix: Adopt a module-by-module migration strategy. Compile individual components, run existing unit tests, and gradually replace Vue imports. Use feature flags to toggle between legacy and compiled versions during transition.
6. Neglecting TypeScript Type Preservation
Explanation: Framework translation can strip generic constraints, union types, or prop validation rules if the semantic graph doesn't preserve type metadata.
Fix: Configure the compiler to extract and regenerate TypeScript interfaces. Run tsc --noEmit after compilation to verify type compatibility before merging.
7. Mismanaging Reactive Proxy Boundaries
Explanation: Vue's reactive creates deeply nested proxies. React's state model prefers shallow updates. Blindly mapping nested objects without understanding update patterns causes missed re-renders.
Fix: Use the compiler's useReactive utility, which implements a controlled proxy layer that triggers React updates on nested mutations. Avoid manual useState replacements for complex reactive graphs.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Greenfield React project with Vue-experienced team | Semantic-aware compiler | Preserves developer velocity, eliminates Hook learning curve, guarantees deterministic output | Low (setup + CI integration) |
| Legacy Vue codebase with complex state graphs | Incremental compiler migration + AI-assisted test generation | Maintains behavioral fidelity while accelerating test coverage and documentation | Medium (phased rollout) |
| Mixed framework team requiring runtime coexistence | Runtime bridging (temporary) | Enables gradual transition without immediate rewrites | High (bundle bloat, debugging fragmentation) |
| Strict compliance/audit environment | Semantic compiler + deterministic CI pipeline | Guarantees reproducible builds, type safety, and audit trails | Low (initial config, long-term savings) |
Configuration Template
// vureact.config.ts
import { defineConfig } from '@vureact/compiler'
export default defineConfig({
input: 'src/components/**/*.vue',
output: 'src/components-compiled',
framework: 'react',
typescript: {
preserveGenerics: true,
generatePropInterfaces: true,
strictNullChecks: true
},
reactivity: {
autoDependencyTracking: true,
proxyDepth: 'deep',
memoizationStrategy: 'semantic'
},
styling: {
scopeStrategy: 'data-attribute',
hashAlgorithm: 'sha256',
generateModules: true
},
events: {
namingConvention: 'react-camelcase',
prefix: 'on',
typeGeneration: true
},
ci: {
deterministicOutput: true,
failOnTypeMismatch: true,
generateSourceMaps: true
}
})
Quick Start Guide
- Install the compiler and runtime core:
npm install @vureact/compiler @vureact/runtime-core
- Create configuration file: Add
vureact.config.ts to your project root using the template above, adjusting input/output paths to match your structure.
- Run initial compilation: Execute
npx vureact compile to transform Vue SFCs into React components. Verify output in the designated directory.
- Integrate with build pipeline: Add the compile step to your
package.json scripts and CI workflow. Run tsc --noEmit and existing test suites to validate behavioral parity before merging.