Skip to content

rburnham52/react-component-converter

Repository files navigation

React Component Converter (rcc)

A CLI tool that converts React shadcn/ui components to Svelte 5 and Vue 3 using a two-stage pipeline with Mitosis as the intermediate representation.

Features

  • React to Svelte 5: Full support for Svelte 5 runes ($props, $bindable, $derived)
  • React to Vue 3: Composition API with <script setup> syntax
  • CVA Support: Preserves class-variance-authority patterns
  • forwardRef Handling: Converts to framework-native ref patterns
  • Radix Primitives: Extracts props from @radix-ui/* components
  • cn() Utility: Maintains Tailwind class merging patterns
  • Multi-component Files: Handles files with multiple exported components

Quick Start

# Install dependencies
pnpm install

# Build all packages
pnpm build

# Convert a component to Vue
pnpm rcc convert ./button.tsx -t vue -o ./Button.vue

# Convert a component to Svelte
pnpm rcc convert ./button.tsx -t svelte -o ./Button.svelte

# Convert all components in a file
pnpm rcc convert ./card.tsx -t vue -a

Architecture

React TSX → preParse plugins (ts-morph) → parseJsx() → postParse → componentToX() → postGenerate → Output

Project Structure

react-component-converter/
├── modules/
│   ├── core/              # Converter, plugins, Mitosis integration
│   ├── parser/            # React component parsing (ts-morph)
│   └── cli/               # Command-line interface
├── playground/            # React components for testing
└── demos/
    ├── vue-demo/          # Vue 3 demo app
    └── svelte-demo/       # Svelte 5 demo app

CLI Commands

convert - Direct Conversion

Convert React components directly to target framework:

# Single component output
pnpm rcc convert ./Button.tsx -t vue -o ./Button.vue
pnpm rcc convert ./Button.tsx -t svelte -o ./Button.svelte

# All components in file (outputs to directory)
pnpm rcc convert ./Card.tsx -t vue -a -o ./components/

# Options
-t, --target <framework>   Target framework: vue | svelte
-o, --output <path>        Output file or directory
-a, --all                  Convert all components in file
-v, --verbose              Enable verbose output
--no-typescript            Output JavaScript instead of TypeScript
--svelte-version <version> Svelte version: 4 | 5 (default: 5)

parse - Parse to IR

Parse React component and output intermediate representation:

pnpm rcc parse ./Button.tsx
pnpm rcc parse ./Button.tsx -o ./button.ir.json

compile - Compile from IR

Compile Mitosis IR to target framework:

pnpm rcc compile ./button.ir.json -t vue
pnpm rcc compile ./button.ir.json -t svelte -o ./Button.svelte

Supported Patterns

Class Variance Authority (CVA)

// Input: React
const buttonVariants = cva(
  "inline-flex items-center justify-center rounded-md",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground",
        destructive: "bg-destructive text-destructive-foreground",
      },
      size: {
        default: "h-10 px-4 py-2",
        sm: "h-9 rounded-md px-3",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
);

CVA definitions are preserved in both Vue and Svelte output with proper TypeScript types.

React.forwardRef

// Input: React
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, ...props }, ref) => (
    <button ref={ref} className={cn(buttonVariants(), className)} {...props} />
  )
);
<!-- Output: Vue -->
<script setup lang="ts">
const elementRef = ref<HTMLButtonElement | null>(null);
defineExpose({ elementRef });
</script>
<template>
  <button ref="elementRef" :class="cn(buttonVariants(), props.class)" v-bind="$attrs">
    <slot />
  </button>
</template>
<!-- Output: Svelte -->
<script lang="ts">
let { ref = $bindable(null), ...restProps }: Props = $props();
</script>
<button bind:this={ref} class={cn(buttonVariants(), className)} {...restProps}>
  {@render children?.()}
</button>

Radix Primitives

Components using @radix-ui/* primitives automatically have their props extracted:

// Input: React (Switch using @radix-ui/react-switch)
const Switch = React.forwardRef<
  React.ElementRef<typeof SwitchPrimitives.Root>,
  React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
  <SwitchPrimitives.Root ref={ref} className={cn(...)} {...props}>
    <SwitchPrimitives.Thumb className={cn(...)} />
  </SwitchPrimitives.Root>
));

The converter automatically extracts props like checked, onCheckedChange, disabled from the Radix primitive definitions and generates proper state handling:

<!-- Output: Svelte -->
<script lang="ts">
let { checked, onCheckedChange, disabled, ...restProps }: Props = $props();
const dataState = $derived(checked ? 'checked' : 'unchecked');

function toggle() {
  if (!disabled) {
    checked = !checked;
    onCheckedChange?.(checked);
  }
}
</script>
<button type="button" role="switch" aria-checked={checked} data-state={dataState} onclick={toggle}>
  <span data-state={dataState}>...</span>
</button>

cn() Utility

The cn() utility for Tailwind class merging is preserved:

// Input
cn("base-class", variant && "variant-class", className)

// Output (both frameworks)
cn("base-class", variant && "variant-class", className)

Both demos include the cn utility at @/lib/utils (Vue) or $lib/utils (Svelte).

Data Flow

1. preParse Phase (React Analyzer Plugin)

React TSX
    ↓
[ts-morph Analysis]
├── Find component definitions (forwardRef, arrow functions)
├── Extract CVA configurations
├── Analyze props from TypeScript interfaces
├── Detect Radix primitive usage
├── Categorize imports
└── Transform code for Mitosis compatibility
    ↓
Transformed code + metadata

2. Parse Phase (Mitosis)

Transformed code
    ↓
[Mitosis parseJsx()]
    ↓
MitosisComponent (IR)

3. Generation Phase (Mitosis + postGenerate Plugins)

MitosisComponent
    ↓
[componentToSvelte() / componentToVue()]
    ↓
[postGenerate plugins (e.g., Svelte5RunesPlugin)]
    ↓
[Prettier formatting]
    ↓
Vue SFC or Svelte 5 Component

Core Types

PropDefinition

interface PropDefinition {
  name: string;
  type: string;
  optional: boolean;
  defaultValue?: unknown;
  isVariant: boolean;           // From CVA variants
  allowedValues?: string[];     // Variant options
  isStateProp?: boolean;        // Controls data-state attribute
  dataStateValues?: {           // For Radix state props
    true: string;
    false: string;
  };
}

CvaConfig

interface CvaConfig {
  name: string;                 // e.g., "buttonVariants"
  baseClasses: string;
  variants: Record<string, Record<string, string>>;
  defaultVariants: Record<string, string>;
  compoundVariants?: Array<{
    conditions: Record<string, string>;
    classes: string;
  }>;
}

ReactComponentMeta

interface ReactComponentMeta {
  cva?: CvaConfig;
  forwardRef?: {
    elementType: string;        // e.g., "HTMLButtonElement"
    paramName: string;          // e.g., "ref"
  };
  usesCn: boolean;
  originalImports: OriginalImport[];
}

Module Details

@react-component-converter/core

Converter engine using Mitosis with plugins:

  • Converter: convert() and convertAll() functions
  • Plugins:
    • react-analyzer-plugin - ts-morph analysis for CVA, forwardRef, cn patterns
    • shadcn-plugin - shadcn/ui-specific metadata extraction
    • svelte5-runes-plugin - Svelte 5 runes syntax transformation
  • Types: PropDefinition, CvaConfig, ExtendedMitosisComponent, etc.
  • Mappings: Radix primitive props, icon mappings, framework equivalents

Key transformations for Vue:

  • React.forwardRefref() + defineExpose()
  • Props → defineProps<Props>() + withDefaults()
  • className:class binding
  • Children → <slot />

Key transformations for Svelte:

  • React.forwardRef$bindable() + bind:this
  • Props → $props() destructuring
  • classNameclass attribute
  • Children → {@render children?.()}
  • State derivation → $derived()

@react-component-converter/parser

React component analysis using ts-morph (used by react-analyzer-plugin):

  • Analyzers:
    • cva.ts - Extract CVA definitions
    • forward-ref.ts - Detect forwardRef patterns
    • props.ts - Extract props from interfaces/types
    • imports.ts - Categorize and transform imports

react-component-converter (CLI)

Commander.js-based CLI with commands:

  • parse - Output IR JSON
  • compile - Compile IR to framework
  • convert - Direct conversion (parse + compile)

Demos

Running the Vue Demo

cd demos/vue-demo
pnpm dev

Running the Svelte Demo

cd demos/svelte-demo
pnpm dev

Converting Components to Demos

# Convert all playground components to Vue
for file in playground/src/components/ui/*.tsx; do
  name=$(basename "$file" .tsx)
  Name=$(echo "$name" | sed -r 's/(^|-)([a-z])/\U\2/g')
  pnpm rcc convert "$file" -t vue -o "demos/vue-demo/src/lib/components/ui/$Name.vue" -a
done

# Convert all playground components to Svelte
for file in playground/src/components/ui/*.tsx; do
  name=$(basename "$file" .tsx)
  Name=$(echo "$name" | sed -r 's/(^|-)([a-z])/\U\2/g')
  pnpm rcc convert "$file" -t svelte -o "demos/svelte-demo/src/lib/components/ui/$Name.svelte" -a
done

Extending the Converter

Adding Radix Primitive Support

Edit modules/core/src/mappings/radix-props.ts:

export const RADIX_PRIMITIVE_PROPS: RadixPrimitivePropsConfig = {
  '@radix-ui/react-new-component': {
    Root: [
      { name: 'open', type: 'boolean', optional: true, isStateProp: true,
        dataStateValues: { true: 'open', false: 'closed' } },
      { name: 'onOpenChange', type: '(open: boolean) => void', optional: true },
    ],
  },
};

Adding Icon Mappings

Edit modules/core/src/mappings/icons.ts:

export const iconMappings: Record<string, IconMapping> = {
  Check: {
    source: 'lucide-react',
    svelte: 'lucide-svelte',
    vue: 'lucide-vue-next',
  },
};

Limitations

  • Complex Hooks: Custom React hooks are not converted; they need manual rewriting
  • Context: React Context is not automatically converted to Svelte context or Vue provide/inject
  • Compound Components: Components with complex parent-child relationships may need manual adjustment
  • Radix Primitives: Full Radix behavior requires using equivalent libraries (bits-ui for Svelte, radix-vue for Vue)

Development

# Install dependencies
pnpm install

# Build all packages
pnpm build

# Run tests
pnpm test

# Type check
pnpm typecheck

# Lint
pnpm lint

License

MIT

About

React Component Converter

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •