diff --git a/packages/plugin-vue-jsx/README.md b/packages/plugin-vue-jsx/README.md
index 3fd94da9..c97c3296 100644
--- a/packages/plugin-vue-jsx/README.md
+++ b/packages/plugin-vue-jsx/README.md
@@ -43,6 +43,34 @@ Default: `['defineComponent']`
The name of the function to be used for defining components. This is useful when you have a custom `defineComponent` function.
+### tsTransform
+
+Type: `'babel' | 'built-in'`
+
+Default: `'babel'`
+
+Defines how `typescript` transformation is handled for `.tsx` files.
+
+`'babel'` - `typescript` transformation is handled by `@babel/plugin-transform-typescript` during `babel` invocation for JSX transformation.
+
+`'built-in'` - `babel` is invoked only for JSX transformation and then `typescript` transformation is handled by the same toolchain used for `.ts` files (currently `esbuild`).
+
+### babelPlugins
+
+Type: `any[]`
+
+Default: `undefined`
+
+Provide additional plugins for `babel` invocation for JSX transformation.
+
+### tsPluginOptions
+
+Type: `any`
+
+Default: `undefined`
+
+Defines options for `@babel/plugin-transform-typescript` plugin.
+
## HMR Detection
This plugin supports HMR of Vue JSX components. The detection requirements are:
diff --git a/packages/plugin-vue-jsx/package.json b/packages/plugin-vue-jsx/package.json
index 16ada141..9e592b66 100644
--- a/packages/plugin-vue-jsx/package.json
+++ b/packages/plugin-vue-jsx/package.json
@@ -36,6 +36,7 @@
"homepage": "https://github.com/vitejs/vite-plugin-vue/tree/main/packages/plugin-vue-jsx#readme",
"dependencies": {
"@babel/core": "^7.28.0",
+ "@babel/plugin-syntax-typescript": "^7.27.1",
"@babel/plugin-transform-typescript": "^7.28.0",
"@rolldown/pluginutils": "^1.0.0-beta.24",
"@vue/babel-plugin-jsx": "^1.4.0"
diff --git a/packages/plugin-vue-jsx/src/index.ts b/packages/plugin-vue-jsx/src/index.ts
index 0ca5cebe..817d029a 100644
--- a/packages/plugin-vue-jsx/src/index.ts
+++ b/packages/plugin-vue-jsx/src/index.ts
@@ -49,6 +49,7 @@ function vueJsxPlugin(options: Options = {}): Plugin {
babelPlugins = [],
defineComponentName = ['defineComponent'],
tsPluginOptions = {},
+ tsTransform,
...babelPluginOptions
} = options
const filter = createFilter(include, exclude)
@@ -67,9 +68,17 @@ function vueJsxPlugin(options: Options = {}): Plugin {
return {
// only apply esbuild to ts files
// since we are handling jsx and tsx now
- esbuild: {
- include: /\.ts$/,
- },
+ esbuild:
+ tsTransform === 'built-in'
+ ? {
+ // For 'built-in' we still need esbuild to transform ts syntax for `.tsx` files.
+ // So we add `.jsx` extension to `exclude` and keep original `include`.
+ // https://github.com/vitejs/vite/blob/v6.3.5/packages/vite/src/node/plugins/esbuild.ts#L246
+ exclude: /\.jsx?$/,
+ }
+ : {
+ include: /\.ts$/,
+ },
define: {
__VUE_OPTIONS_API__:
parseDefine(config.define?.__VUE_OPTIONS_API__) ?? true,
@@ -108,6 +117,9 @@ function vueJsxPlugin(options: Options = {}): Plugin {
},
transform: {
+ // Use 'pre' stage for 'built-in'
+ // to run jsx transformation before esbuild transformation.
+ order: tsTransform === 'built-in' ? 'pre' : undefined,
filter: {
id: {
include: include ? makeIdFiltersToMatchWithQuery(include) : undefined,
@@ -123,14 +135,26 @@ function vueJsxPlugin(options: Options = {}): Plugin {
if (filter(id) || filter(filepath)) {
const plugins = [[jsx, babelPluginOptions], ...babelPlugins]
if (id.endsWith('.tsx') || filepath.endsWith('.tsx')) {
- plugins.push([
- // @ts-ignore missing type
- await import('@babel/plugin-transform-typescript').then(
- (r) => r.default,
- ),
- // @ts-ignore
- { ...tsPluginOptions, isTSX: true, allowExtensions: true },
- ])
+ if (tsTransform === 'built-in') {
+ // For 'built-in' add "syntax" plugin
+ // to enable parsing without transformation.
+ plugins.push([
+ // @ts-ignore missing type
+ await import('@babel/plugin-syntax-typescript').then(
+ (r) => r.default,
+ ),
+ { isTSX: true },
+ ])
+ } else {
+ plugins.push([
+ // @ts-ignore missing type
+ await import('@babel/plugin-transform-typescript').then(
+ (r) => r.default,
+ ),
+ // @ts-ignore
+ { ...tsPluginOptions, isTSX: true, allowExtensions: true },
+ ])
+ }
}
if (!ssr && !needHmr) {
diff --git a/packages/plugin-vue-jsx/src/types.ts b/packages/plugin-vue-jsx/src/types.ts
index 0b9f4783..cd7b0de0 100644
--- a/packages/plugin-vue-jsx/src/types.ts
+++ b/packages/plugin-vue-jsx/src/types.ts
@@ -11,4 +11,6 @@ export interface Options extends VueJSXPluginOptions, FilterOptions {
/** @default ['defineComponent'] */
defineComponentName?: string[]
tsPluginOptions?: any
+ /** @default 'babel' */
+ tsTransform?: 'babel' | 'built-in'
}
diff --git a/playground/vue-jsx-ts-built-in/__tests__/vue-jsx-ts-built-in.spec.ts b/playground/vue-jsx-ts-built-in/__tests__/vue-jsx-ts-built-in.spec.ts
new file mode 100644
index 00000000..79005d7c
--- /dev/null
+++ b/playground/vue-jsx-ts-built-in/__tests__/vue-jsx-ts-built-in.spec.ts
@@ -0,0 +1,32 @@
+import { expect, test } from 'vitest'
+import { page } from '~utils'
+
+test('should render', async () => {
+ expect(await page.textContent('.decorators-ts')).toMatch('1')
+ expect(await page.textContent('.decorators-tsx')).toMatch('2')
+ expect(await page.textContent('.decorators-vue-ts')).toMatch('3')
+ expect(await page.textContent('.decorators-vue-tsx')).toMatch('4')
+ expect(await page.textContent('.decorators-legacy-ts')).toMatch('5')
+ expect(await page.textContent('.decorators-legacy-tsx')).toMatch('6')
+ expect(await page.textContent('.decorators-legacy-vue-ts')).toMatch('7')
+ expect(await page.textContent('.decorators-legacy-vue-tsx')).toMatch('8')
+})
+
+test('should update', async () => {
+ await page.click('.decorators-ts')
+ expect(await page.textContent('.decorators-ts')).toMatch('2')
+ await page.click('.decorators-tsx')
+ expect(await page.textContent('.decorators-tsx')).toMatch('3')
+ await page.click('.decorators-vue-ts')
+ expect(await page.textContent('.decorators-vue-ts')).toMatch('4')
+ await page.click('.decorators-vue-tsx')
+ expect(await page.textContent('.decorators-vue-tsx')).toMatch('5')
+ await page.click('.decorators-legacy-ts')
+ expect(await page.textContent('.decorators-legacy-ts')).toMatch('6')
+ await page.click('.decorators-legacy-tsx')
+ expect(await page.textContent('.decorators-legacy-tsx')).toMatch('7')
+ await page.click('.decorators-legacy-vue-ts')
+ expect(await page.textContent('.decorators-legacy-vue-ts')).toMatch('8')
+ await page.click('.decorators-legacy-vue-tsx')
+ expect(await page.textContent('.decorators-legacy-vue-tsx')).toMatch('9')
+})
diff --git a/playground/vue-jsx-ts-built-in/decorators-legacy/DecoratorsTs.ts b/playground/vue-jsx-ts-built-in/decorators-legacy/DecoratorsTs.ts
new file mode 100644
index 00000000..6fbf7b91
--- /dev/null
+++ b/playground/vue-jsx-ts-built-in/decorators-legacy/DecoratorsTs.ts
@@ -0,0 +1,37 @@
+import { defineComponent, h, ref } from 'vue'
+
+function methodDecorator(
+ target: unknown,
+ propertyKey: string,
+ descriptor: PropertyDescriptor,
+) {
+ const originalMethod = descriptor.value
+ descriptor.value = function () {
+ const result = originalMethod.call(this)
+ this.value.value += 1
+ return result
+ }
+}
+
+export default defineComponent(() => {
+ class Counter {
+ value = ref(5)
+
+ // @ts-expect-error typecheck script does not use local tsconfig.json
+ @methodDecorator
+ increment() {}
+ }
+
+ const counter = new Counter()
+ const inc = () => counter.increment()
+
+ return () =>
+ h(
+ 'button',
+ {
+ class: 'decorators-legacy-ts',
+ onClick: inc,
+ },
+ `decorators legacy ts ${counter.value.value}`,
+ )
+})
diff --git a/playground/vue-jsx-ts-built-in/decorators-legacy/DecoratorsTsx.tsx b/playground/vue-jsx-ts-built-in/decorators-legacy/DecoratorsTsx.tsx
new file mode 100644
index 00000000..b2d72435
--- /dev/null
+++ b/playground/vue-jsx-ts-built-in/decorators-legacy/DecoratorsTsx.tsx
@@ -0,0 +1,33 @@
+import { defineComponent, ref } from 'vue'
+
+function methodDecorator(
+ target: unknown,
+ propertyKey: string,
+ descriptor: PropertyDescriptor,
+) {
+ const originalMethod = descriptor.value
+ descriptor.value = function () {
+ const result = originalMethod.call(this)
+ this.value.value += 1
+ return result
+ }
+}
+
+export default defineComponent(() => {
+ class Counter {
+ value = ref(6)
+
+ // @ts-expect-error typecheck script does not use local tsconfig.json
+ @methodDecorator
+ increment() {}
+ }
+
+ const counter = new Counter()
+ const inc = () => counter.increment()
+
+ return () => (
+
+ )
+})
diff --git a/playground/vue-jsx-ts-built-in/decorators-legacy/DecoratorsVueTs.vue b/playground/vue-jsx-ts-built-in/decorators-legacy/DecoratorsVueTs.vue
new file mode 100644
index 00000000..a0ee2386
--- /dev/null
+++ b/playground/vue-jsx-ts-built-in/decorators-legacy/DecoratorsVueTs.vue
@@ -0,0 +1,41 @@
+
+
+