npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

vue-to-tsx

v0.4.3

Published

Convert Vue Single File Components (.vue) to Vue TSX (.tsx + .css)

Readme

vue-to-tsx

npm version

Convert Vue Single File Components (.vue) to Vue TSX (.tsx + .css).

This is not a React migration tool. The output is Vue TSX -- it stays within the Vue ecosystem, using defineComponent, Vue's JSX transform, and plain CSS imports.

Why?

Vue's Single File Component format is a well-designed authoring experience. But it comes with a cost: .vue files only work with custom tooling. Your editor needs a Vue-specific extension. Your bundler needs a Vue plugin. Your linter, your test runner, your CI -- everything in the chain needs to know what a .vue file is.

TSX changes that. A .tsx file is just TypeScript. It works everywhere TypeScript works -- no plugins, no extensions, no custom language servers. You get:

  • Native TypeScript support -- full type checking, refactoring, and go-to-definition without Volar or any editor extension
  • Standard tooling -- any bundler, linter, test runner, or CI pipeline that supports TypeScript works out of the box
  • All of Vue's power -- defineComponent, ref, computed, watch, slots, emits, provide/inject -- it all works in TSX
  • Full Nuxt compatibility -- Nuxt auto-imports, composables, and middleware work identically in .tsx files
  • Better composition -- components are just functions returning JSX, making it natural to compose, split, and reuse rendering logic
  • No lock-in -- your code is portable TypeScript, not a framework-specific file format

vue-to-tsx automates the conversion so you can migrate gradually, file by file, without rewriting anything by hand.

Features

  • Template to JSX conversion (v-if/v-for/v-show/v-model, slots, events)
  • <script setup> to defineComponent with full macro support (defineProps, defineEmits, defineSlots, defineExpose, defineOptions, defineModel)
  • Type-based defineEmits converted to runtime emits option (call signature and Vue 3.3+ shorthand forms, including kebab-case event names)
  • Automatic .value unwrapping for ref/computed identifiers in JSX expressions (string-literal-aware -- won't corrupt 'statement' or 'default' inside quotes). Also detects use* composable return values as refs (e.g., useLocalStorage, useDark)
  • Automatic props. prefixing for prop identifiers in template expressions (also string-literal-aware)
  • Vue built-in components (Teleport, KeepAlive, Transition, TransitionGroup, Suspense) auto-imported from vue
  • Auto-imports Vue APIs used in runtime props/emits (PropType, ref, computed, etc.)
  • v-for uses a runtime helper that supports arrays, objects, and numbers (matching Vue's runtime behavior)
  • Static class and dynamic :class merged into a single attribute (no duplicate class props)
  • .vue import paths automatically stripped (e.g., import Foo from './Foo.vue' becomes './Foo')
  • Scoped CSS extracted to plain .css files (side-effect import)
  • Handles complex patterns: v-if/v-else-if/v-else chains, dynamic components, named/scoped slots
  • Optional LLM fallback for patterns that can't be converted deterministically (Anthropic and OpenAI)
  • CLI for batch conversion and library API for programmatic use

Installation

# bun
bun add -d vue-to-tsx

# npm
npm install -D vue-to-tsx

# pnpm
pnpm add -D vue-to-tsx

CLI usage

# Convert a single file
vue-to-tsx src/components/MyComponent.vue

# Convert a directory recursively
vue-to-tsx src/components/

# Convert and delete original .vue files (in-place replacement)
vue-to-tsx src/components/ --delete

# Convert with LLM fallback for complex patterns
vue-to-tsx src/components/ --llm

# Write output to a specific directory
vue-to-tsx src/components/ --out-dir converted/

# Preview what would happen without writing anything
vue-to-tsx src/components/ --dry-run --delete

Library API

import { convert } from 'vue-to-tsx';

const source = `
<template>
  <div class="container">
    <h1>{{ title }}</h1>
    <button @click="handleClick">Click me</button>
  </div>
</template>

<script setup lang="ts">
const props = defineProps<{ title: string }>();
const emit = defineEmits<{ click: [] }>();

function handleClick() {
  emit('click');
}
</script>

<style scoped>
.container {
  padding: 16px;
}
</style>
`;

const result = await convert(source, {
  componentName: 'MyComponent',
});

console.log(result.tsx);        // The generated .tsx file
console.log(result.css);        // The generated .css file (or null)
console.log(result.warnings);   // Any conversion warnings
console.log(result.fallbacks);  // Items that need manual review

How it works

  1. Template to JSX -- The Vue template AST (from @vue/compiler-sfc) is walked and converted to JSX. Directives like v-if become ternary expressions, v-for uses a runtime helper (_renderList) that handles arrays, objects, and numbers, @click becomes onClick, etc.

  2. Script setup to defineComponent -- <script setup> macros (defineProps, defineEmits, defineSlots, etc.) are extracted and rewritten into a defineComponent call with proper setup() function.

  3. Scoped CSS to plain CSS -- <style scoped> blocks are extracted to plain .css files and imported as side-effect imports (import './Component.css'). Vue-specific pseudo-selectors (:deep, :slotted, :global) are stripped.

  4. LLM fallback -- When a template pattern can't be converted deterministically (e.g., complex custom directives), it's marked with a fallback comment. With --llm enabled, these are sent to an LLM for resolution.

Key differences between Vue SFC and TSX

Vue SFC templates are forgiving by design -- they silently handle things that would be errors in plain TypeScript. This convenience has a cost: it hides real bugs and makes code harder to reason about. TSX is explicit. The table below shows what vue-to-tsx does to bridge the gap, and why the TSX version is often the better one.

| Vue SFC (implicit) | Generated TSX (explicit) | Why it matters | |---|---|---| | {{ count }} auto-unwraps refs | {count.value} | In TSX, refs are regular objects. You must access .value explicitly. This catches bugs where you forget to create a ref in the first place. | | {{ title }} auto-exposes props | {props.title} | Vue templates magically expose prop names as local variables. TSX makes the data source clear -- you always know when something comes from props. | | v-for="item in obj" works on arrays, objects, and numbers | _renderList(obj, (item) => ...) | Vue's v-for silently iterates objects via Object.keys() and generates ranges from numbers. TSX has no such magic -- _renderList is a runtime helper that matches Vue's behavior exactly. | | const x = useLocalStorage(...) auto-unwrapped in template | x.value in JSX | Vue templates auto-unwrap any ref, but TSX can't. The converter detects use* composable return values (following Vue's naming convention) and adds .value. | | <Teleport>, <KeepAlive>, etc. resolve magically | import { Teleport } from 'vue' | Vue's compiler knows about built-in components. In TSX, they're just identifiers -- they need explicit imports like anything else. | | @click.prevent="handler" | onClick={withModifiers(handler, ['prevent'])} | Event modifiers are template-only syntax. TSX uses Vue's withModifiers runtime helper. | | $attrs, $slots, $emit | attrs, slots, emit from setup() context | Template globals don't exist in TSX. They come from the setup function's second argument. | | <script setup> macros (defineProps, defineEmits) | defineComponent({ props: {...}, emits: [...] }) | Compiler macros are template-only magic. TSX uses standard defineComponent options. Type-based defineEmits is converted to a runtime emits array. | | import Foo from './Foo.vue' | import Foo from './Foo' | .vue extensions are stripped since you're importing .tsx files now. | | <style scoped> | Plain .css with side-effect import | Scoped styles use data attributes that only work with Vue's compiler. The CSS is extracted as a plain file and imported directly (import './Component.css'). | | class="foo" :class="{ active: x }" (two attributes) | class={['foo', { active: x }]} (merged) | Vue templates merge multiple class attributes at runtime. JSX doesn't -- duplicate props overwrite each other. The converter merges them into one. |

Output formatting

The converter produces syntactically valid TSX but does not format or prettify it. Run your project's formatter on the output files to match your codebase style:

# Prettier
bunx prettier --write "src/**/*.tsx"

# Biome
bunx biome format --write "src/**/*.tsx"

# dprint
bunx dprint fmt "src/**/*.tsx"

LLM-powered fallback

The deterministic converter handles the vast majority of Vue patterns -- v-if/v-for/v-show, v-model, slots, events, macros, CSS extraction, and more. But roughly 5% of real-world Vue code uses patterns that have no single correct JSX translation. For these, vue-to-tsx offers an AI-powered fallback that understands Vue semantics and produces idiomatic JSX.

What triggers the fallback

  • Custom directives -- v-focus, v-tooltip, v-click-outside, and any app-specific directives
  • v-memo -- performance hint with no direct JSX equivalent
  • Complex slot forwarding -- dynamically passing through $slots to child components
  • Dynamic components with complex :is -- <component :is="someCondition ? CompA : CompB" /> with non-trivial expressions

Without --llm

These patterns are marked with a // TODO: vue-to-tsx comment so you can resolve them manually:

{/* TODO: vue-to-tsx - Custom directive "v-tooltip" cannot be converted deterministically */}
{/* Original: <span v-tooltip="helpText">Hover me</span> */}

With --llm

The same pattern is intelligently converted, producing a working JSX equivalent:

<Tooltip text={helpText.value}>
  <span>Hover me</span>
</Tooltip>

All fallback items in a file are batched into a single API call, so even a file with multiple custom directives only makes one request. This keeps costs low and latency minimal.

Setup

Set an API key for your preferred provider:

# Anthropic (default if both are set)
export ANTHROPIC_API_KEY=sk-ant-...

# OpenAI
export OPENAI_API_KEY=sk-...

The provider is auto-detected from whichever API key is set. If both are set, Anthropic is preferred. You can override this with VUE_TO_TSX_LLM_PROVIDER:

export VUE_TO_TSX_LLM_PROVIDER=openai  # force OpenAI even if ANTHROPIC_API_KEY is set

Then pass --llm to the CLI:

vue-to-tsx src/components/ --llm

Model override

Default models: claude-sonnet-4-6 (Anthropic), gpt-5.2-codex (OpenAI). Override via CLI flag or env var:

# CLI flag
vue-to-tsx src/components/ --llm --llm-model gpt-4o-mini

# Environment variable
export VUE_TO_TSX_LLM_MODEL=claude-haiku-4-5-20251001

Context-aware healing

The LLM receives the full original Vue SFC and the generated TSX so far, giving it complete context to produce accurate replacements. Responses are validated to reject any output that still contains Vue template syntax (v-if, v-for, @click, etc.) — invalid replacements are discarded and the TODO fallback comment is preserved.

Programmatic usage

const result = await convert(source, {
  componentName: 'MyComponent',
  llm: true,
  llmModel: 'claude-sonnet-4-6',
});

Options

The convert() function accepts an options object:

interface ConvertOptions {
  componentName?: string;  // Component name (derived from filename if not provided)
  llm?: boolean;           // Enable LLM fallback (default: false)
  llmModel?: string;       // LLM model to use (auto-detected from provider)
}

| Option | CLI flag | Default | Description | |--------|----------|---------|-------------| | componentName | (from filename) | PascalCase of filename | Name used in defineComponent | | llm | --llm | false | Enable AI-powered fallback for unconvertible patterns | | llmModel | --llm-model | Auto (provider-dependent) | LLM model ID for fallback resolution |

Contributing

See CONTRIBUTING.md.

License

MIT