Skip to main content

Tutorial: Multi-Framework Plugin with Mitosis

Create a plugin with components that work across React, Vue, and Svelte.

Overview

We'll build a "Charts" plugin with:

  • Line chart component
  • Bar chart component
  • Pie chart component

Using Mitosis to compile to all frameworks.

Step 1: Set Up the Plugin

npx autodeploybase plugin create charts
cd plugins/charts

Update the structure:

plugins/charts/
├── manifest.json
├── generator.js
├── mitosis.config.js
├── src/
│ └── components/
│ ├── LineChart.lite.tsx
│ ├── BarChart.lite.tsx
│ └── PieChart.lite.tsx
├── output/ # Generated
├── templates/
│ ├── shared/
│ ├── next/
│ ├── nuxt/
│ └── sveltekit/
└── package.json

Step 2: Configure Mitosis

plugins/charts/mitosis.config.js
module.exports = {
files: 'src/components/**/*.lite.tsx',
targets: ['react', 'vue', 'svelte', 'solid', 'qwik'],
dest: 'output',
options: {
react: {
typescript: true,
stylesType: 'style-tag'
},
vue: {
typescript: true,
api: 'composition'
},
svelte: {
typescript: true
},
solid: {
typescript: true
},
qwik: {
typescript: true
}
}
};

Step 3: Write Mitosis Components

LineChart

src/components/LineChart.lite.tsx
import { useState, onMount } from '@builder.io/mitosis';

interface DataPoint {
label: string;
value: number;
}

interface LineChartProps {
data: DataPoint[];
width?: number;
height?: number;
color?: string;
}

export default function LineChart(props: LineChartProps) {
const [pathD, setPathD] = useState('');

const width = props.width || 400;
const height = props.height || 200;
const color = props.color || '#6366f1';

onMount(() => {
if (!props.data || props.data.length === 0) return;

const maxValue = Math.max(...props.data.map(d => d.value));
const xStep = width / (props.data.length - 1);

const points = props.data.map((point, index) => {
const x = index * xStep;
const y = height - (point.value / maxValue) * height;
return `${x},${y}`;
});

setPathD(`M ${points.join(' L ')}`);
});

return (
<div class="chart-container">
<svg width={width} height={height} class="line-chart">
<path
d={pathD}
fill="none"
stroke={color}
stroke-width="2"
/>
{props.data.map((point, index) => (
<circle
key={index}
cx={(index * width) / (props.data.length - 1)}
cy={height - (point.value / Math.max(...props.data.map(d => d.value))) * height}
r="4"
fill={color}
/>
))}
</svg>
<div class="chart-labels">
{props.data.map((point) => (
<span key={point.label} class="label">{point.label}</span>
))}
</div>
</div>
);
}

BarChart

src/components/BarChart.lite.tsx
import { useState } from '@builder.io/mitosis';

interface BarData {
label: string;
value: number;
color?: string;
}

interface BarChartProps {
data: BarData[];
width?: number;
height?: number;
}

export default function BarChart(props: BarChartProps) {
const [hoveredIndex, setHoveredIndex] = useState(-1);

const width = props.width || 400;
const height = props.height || 200;

const maxValue = Math.max(...(props.data || []).map(d => d.value));
const barWidth = width / (props.data?.length || 1) - 10;

return (
<div class="chart-container">
<svg width={width} height={height} class="bar-chart">
{(props.data || []).map((item, index) => {
const barHeight = (item.value / maxValue) * (height - 20);
const x = index * (barWidth + 10) + 5;
const y = height - barHeight - 20;

return (
<g key={index}>
<rect
x={x}
y={y}
width={barWidth}
height={barHeight}
fill={item.color || '#6366f1'}
opacity={hoveredIndex === index ? 1 : 0.8}
onMouseEnter={() => setHoveredIndex(index)}
onMouseLeave={() => setHoveredIndex(-1)}
/>
<text
x={x + barWidth / 2}
y={height - 5}
text-anchor="middle"
class="bar-label"
>
{item.label}
</text>
</g>
);
})}
</svg>
</div>
);
}

PieChart

src/components/PieChart.lite.tsx
import { useState } from '@builder.io/mitosis';

interface SliceData {
label: string;
value: number;
color: string;
}

interface PieChartProps {
data: SliceData[];
size?: number;
}

export default function PieChart(props: PieChartProps) {
const [activeSlice, setActiveSlice] = useState(-1);

const size = props.size || 200;
const radius = size / 2 - 10;
const center = size / 2;

function getSlicePath(startAngle: number, endAngle: number) {
const startX = center + radius * Math.cos(startAngle);
const startY = center + radius * Math.sin(startAngle);
const endX = center + radius * Math.cos(endAngle);
const endY = center + radius * Math.sin(endAngle);
const largeArc = endAngle - startAngle > Math.PI ? 1 : 0;

return `M ${center} ${center} L ${startX} ${startY} A ${radius} ${radius} 0 ${largeArc} 1 ${endX} ${endY} Z`;
}

const total = (props.data || []).reduce((sum, d) => sum + d.value, 0);
let currentAngle = -Math.PI / 2;

return (
<div class="chart-container">
<svg width={size} height={size} class="pie-chart">
{(props.data || []).map((slice, index) => {
const sliceAngle = (slice.value / total) * 2 * Math.PI;
const path = getSlicePath(currentAngle, currentAngle + sliceAngle);
currentAngle += sliceAngle;

return (
<path
key={index}
d={path}
fill={slice.color}
opacity={activeSlice === index ? 1 : 0.9}
onMouseEnter={() => setActiveSlice(index)}
onMouseLeave={() => setActiveSlice(-1)}
/>
);
})}
</svg>
<div class="legend">
{(props.data || []).map((slice) => (
<div key={slice.label} class="legend-item">
<span class="legend-color" style={{ backgroundColor: slice.color }} />
<span>{slice.label}: {slice.value}</span>
</div>
))}
</div>
</div>
);
}

Step 4: Add Styles

src/styles/charts.css
.chart-container {
display: flex;
flex-direction: column;
gap: 1rem;
}

.chart-labels {
display: flex;
justify-content: space-between;
font-size: 0.75rem;
color: #666;
}

.bar-label {
font-size: 0.75rem;
fill: #666;
}

.legend {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}

.legend-item {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
}

.legend-color {
width: 12px;
height: 12px;
border-radius: 2px;
}

Step 5: Compile Components

Add npm scripts:

package.json
{
"scripts": {
"build": "mitosis compile",
"build:watch": "mitosis compile --watch"
}
}
npm run build

Output:

output/
├── react/
│ ├── LineChart.tsx
│ ├── BarChart.tsx
│ └── PieChart.tsx
├── vue/
│ ├── LineChart.vue
│ ├── BarChart.vue
│ └── PieChart.vue
├── svelte/
│ ├── LineChart.svelte
│ ├── BarChart.svelte
│ └── PieChart.svelte
└── ...

Step 6: Write the Generator

generator.js
import { sdk } from 'autodeploybase';

export default async function generate(context) {
const { projectPath, framework } = context;

// Map framework to Mitosis output
const frameworkMap = {
next: 'react',
nuxt: 'vue',
sveltekit: 'svelte',
remix: 'react',
astro: 'react',
react: 'react'
};

const outputFramework = frameworkMap[framework] || 'react';

// Copy compiled components
await sdk.copyTemplates({
source: `./output/${outputFramework}`,
destination: `${projectPath}/src/components/charts`
});

// Copy styles
await sdk.copyTemplates({
source: './src/styles',
destination: `${projectPath}/src/styles`
});

// Register CSS
await sdk.registerCSS({
imports: ['@/styles/charts.css']
});

// Copy framework-specific exports
await sdk.copyTemplates({
source: `./templates/${framework}`,
destination: projectPath
});
}

Step 7: Add Framework Exports

templates/next/components/charts/index.ts
export { default as LineChart } from './LineChart';
export { default as BarChart } from './BarChart';
export { default as PieChart } from './PieChart';
templates/nuxt/components/charts/index.ts
export { default as LineChart } from './LineChart.vue';
export { default as BarChart } from './BarChart.vue';
export { default as PieChart } from './PieChart.vue';

Step 8: Usage

React/Next.js

import { LineChart, BarChart, PieChart } from '@/components/charts';

export function Dashboard() {
const data = [
{ label: 'Jan', value: 100 },
{ label: 'Feb', value: 150 },
{ label: 'Mar', value: 200 },
];

return (
<div>
<LineChart data={data} color="#10b981" />
<BarChart data={data} />
</div>
);
}

Vue/Nuxt

<template>
<div>
<LineChart :data="data" color="#10b981" />
<BarChart :data="data" />
</div>
</template>

<script setup>
import { LineChart, BarChart } from '@/components/charts';

const data = [
{ label: 'Jan', value: 100 },
{ label: 'Feb', value: 150 },
{ label: 'Mar', value: 200 },
];
</script>

Testing Across Frameworks

# Test with Next.js
npx autodeploybase init test-next --framework next --plugins ./plugins/charts

# Test with Nuxt
npx autodeploybase init test-nuxt --framework nuxt --plugins ./plugins/charts

# Test with SvelteKit
npx autodeploybase init test-svelte --framework sveltekit --plugins ./plugins/charts

Verify components render correctly in each framework.