Skip to content

Commit c453513

Browse files
feat(vue-jsx): allow esbuild to perform ts transformation (#621)
Co-authored-by: sapphi-red <[email protected]>
1 parent c6bd324 commit c453513

20 files changed

+493
-54
lines changed

packages/plugin-vue-jsx/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,34 @@ Default: `['defineComponent']`
4343

4444
The name of the function to be used for defining components. This is useful when you have a custom `defineComponent` function.
4545

46+
### tsTransform
47+
48+
Type: `'babel' | 'built-in'`
49+
50+
Default: `'babel'`
51+
52+
Defines how `typescript` transformation is handled for `.tsx` files.
53+
54+
`'babel'` - `typescript` transformation is handled by `@babel/plugin-transform-typescript` during `babel` invocation for JSX transformation.
55+
56+
`'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`).
57+
58+
### babelPlugins
59+
60+
Type: `any[]`
61+
62+
Default: `undefined`
63+
64+
Provide additional plugins for `babel` invocation for JSX transformation.
65+
66+
### tsPluginOptions
67+
68+
Type: `any`
69+
70+
Default: `undefined`
71+
72+
Defines options for `@babel/plugin-transform-typescript` plugin.
73+
4674
## HMR Detection
4775

4876
This plugin supports HMR of Vue JSX components. The detection requirements are:

packages/plugin-vue-jsx/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"homepage": "https://github.com/vitejs/vite-plugin-vue/tree/main/packages/plugin-vue-jsx#readme",
3737
"dependencies": {
3838
"@babel/core": "^7.28.3",
39+
"@babel/plugin-syntax-typescript": "^7.27.1",
3940
"@babel/plugin-transform-typescript": "^7.28.0",
4041
"@rolldown/pluginutils": "^1.0.0-beta.34",
4142
"@vue/babel-plugin-jsx": "^1.5.0"

packages/plugin-vue-jsx/src/index.ts

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ function vueJsxPlugin(options: Options = {}): Plugin {
4949
babelPlugins = [],
5050
defineComponentName = ['defineComponent'],
5151
tsPluginOptions = {},
52+
tsTransform,
5253
...babelPluginOptions
5354
} = options
5455
const filter = createFilter(include, exclude)
@@ -67,9 +68,17 @@ function vueJsxPlugin(options: Options = {}): Plugin {
6768
return {
6869
// only apply esbuild to ts files
6970
// since we are handling jsx and tsx now
70-
esbuild: {
71-
include: /\.ts$/,
72-
},
71+
esbuild:
72+
tsTransform === 'built-in'
73+
? {
74+
// For 'built-in' we still need esbuild to transform ts syntax for `.tsx` files.
75+
// So we add `.jsx` extension to `exclude` and keep original `include`.
76+
// https://github.com/vitejs/vite/blob/v6.3.5/packages/vite/src/node/plugins/esbuild.ts#L246
77+
exclude: /\.jsx?$/,
78+
}
79+
: {
80+
include: /\.ts$/,
81+
},
7382
define: {
7483
__VUE_OPTIONS_API__:
7584
parseDefine(config.define?.__VUE_OPTIONS_API__) ?? true,
@@ -113,6 +122,9 @@ function vueJsxPlugin(options: Options = {}): Plugin {
113122
},
114123

115124
transform: {
125+
// Use 'pre' stage for 'built-in'
126+
// to run jsx transformation before esbuild transformation.
127+
order: tsTransform === 'built-in' ? 'pre' : undefined,
116128
filter: {
117129
id: {
118130
include: include ? makeIdFiltersToMatchWithQuery(include) : undefined,
@@ -128,14 +140,26 @@ function vueJsxPlugin(options: Options = {}): Plugin {
128140
if (filter(id) || filter(filepath)) {
129141
const plugins = [[jsx, babelPluginOptions], ...babelPlugins]
130142
if (id.endsWith('.tsx') || filepath.endsWith('.tsx')) {
131-
plugins.push([
132-
// @ts-ignore missing type
133-
await import('@babel/plugin-transform-typescript').then(
134-
(r) => r.default,
135-
),
136-
// @ts-ignore
137-
{ ...tsPluginOptions, isTSX: true, allowExtensions: true },
138-
])
143+
if (tsTransform === 'built-in') {
144+
// For 'built-in' add "syntax" plugin
145+
// to enable parsing without transformation.
146+
plugins.push([
147+
// @ts-ignore missing type
148+
await import('@babel/plugin-syntax-typescript').then(
149+
(r) => r.default,
150+
),
151+
{ isTSX: true },
152+
])
153+
} else {
154+
plugins.push([
155+
// @ts-ignore missing type
156+
await import('@babel/plugin-transform-typescript').then(
157+
(r) => r.default,
158+
),
159+
// @ts-ignore
160+
{ ...tsPluginOptions, isTSX: true, allowExtensions: true },
161+
])
162+
}
139163
}
140164

141165
if (!ssr && !needHmr) {

packages/plugin-vue-jsx/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,6 @@ export interface Options extends VueJSXPluginOptions, FilterOptions {
1111
/** @default ['defineComponent'] */
1212
defineComponentName?: string[]
1313
tsPluginOptions?: any
14+
/** @default 'babel' */
15+
tsTransform?: 'babel' | 'built-in'
1416
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { expect, test } from 'vitest'
2+
import { page } from '~utils'
3+
4+
test('should render', async () => {
5+
expect(await page.textContent('.decorators-ts')).toMatch('1')
6+
expect(await page.textContent('.decorators-tsx')).toMatch('2')
7+
expect(await page.textContent('.decorators-vue-ts')).toMatch('3')
8+
expect(await page.textContent('.decorators-vue-tsx')).toMatch('4')
9+
expect(await page.textContent('.decorators-legacy-ts')).toMatch('5')
10+
expect(await page.textContent('.decorators-legacy-tsx')).toMatch('6')
11+
expect(await page.textContent('.decorators-legacy-vue-ts')).toMatch('7')
12+
expect(await page.textContent('.decorators-legacy-vue-tsx')).toMatch('8')
13+
})
14+
15+
test('should update', async () => {
16+
await page.click('.decorators-ts')
17+
expect(await page.textContent('.decorators-ts')).toMatch('2')
18+
await page.click('.decorators-tsx')
19+
expect(await page.textContent('.decorators-tsx')).toMatch('3')
20+
await page.click('.decorators-vue-ts')
21+
expect(await page.textContent('.decorators-vue-ts')).toMatch('4')
22+
await page.click('.decorators-vue-tsx')
23+
expect(await page.textContent('.decorators-vue-tsx')).toMatch('5')
24+
await page.click('.decorators-legacy-ts')
25+
expect(await page.textContent('.decorators-legacy-ts')).toMatch('6')
26+
await page.click('.decorators-legacy-tsx')
27+
expect(await page.textContent('.decorators-legacy-tsx')).toMatch('7')
28+
await page.click('.decorators-legacy-vue-ts')
29+
expect(await page.textContent('.decorators-legacy-vue-ts')).toMatch('8')
30+
await page.click('.decorators-legacy-vue-tsx')
31+
expect(await page.textContent('.decorators-legacy-vue-tsx')).toMatch('9')
32+
})
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { defineComponent, h, ref } from 'vue'
2+
3+
function methodDecorator(
4+
target: unknown,
5+
propertyKey: string,
6+
descriptor: PropertyDescriptor,
7+
) {
8+
const originalMethod = descriptor.value
9+
descriptor.value = function () {
10+
const result = originalMethod.call(this)
11+
this.value.value += 1
12+
return result
13+
}
14+
}
15+
16+
export default defineComponent(() => {
17+
class Counter {
18+
value = ref(5)
19+
20+
// @ts-expect-error typecheck script does not use local tsconfig.json
21+
@methodDecorator
22+
increment() {}
23+
}
24+
25+
const counter = new Counter()
26+
const inc = () => counter.increment()
27+
28+
return () =>
29+
h(
30+
'button',
31+
{
32+
class: 'decorators-legacy-ts',
33+
onClick: inc,
34+
},
35+
`decorators legacy ts ${counter.value.value}`,
36+
)
37+
})
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { defineComponent, ref } from 'vue'
2+
3+
function methodDecorator(
4+
target: unknown,
5+
propertyKey: string,
6+
descriptor: PropertyDescriptor,
7+
) {
8+
const originalMethod = descriptor.value
9+
descriptor.value = function () {
10+
const result = originalMethod.call(this)
11+
this.value.value += 1
12+
return result
13+
}
14+
}
15+
16+
export default defineComponent(() => {
17+
class Counter {
18+
value = ref(6)
19+
20+
// @ts-expect-error typecheck script does not use local tsconfig.json
21+
@methodDecorator
22+
increment() {}
23+
}
24+
25+
const counter = new Counter()
26+
const inc = () => counter.increment()
27+
28+
return () => (
29+
<button class="decorators-legacy-tsx" onClick={inc}>
30+
{`decorators legacy tsx ${counter.value.value}`}
31+
</button>
32+
)
33+
})
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<script setup lang="ts">
2+
import { defineComponent, h, ref } from 'vue'
3+
4+
function methodDecorator(
5+
target: unknown,
6+
propertyKey: string,
7+
descriptor: PropertyDescriptor,
8+
) {
9+
const originalMethod = descriptor.value
10+
descriptor.value = function () {
11+
const result = originalMethod.call(this)
12+
this.value.value += 1
13+
return result
14+
}
15+
}
16+
17+
const Component = defineComponent(() => {
18+
class Counter {
19+
value = ref(7)
20+
21+
@methodDecorator
22+
increment() {}
23+
}
24+
25+
const counter = new Counter()
26+
const inc = () => counter.increment()
27+
28+
return () =>
29+
h(
30+
'button',
31+
{
32+
class: 'decorators-legacy-vue-ts',
33+
onClick: inc,
34+
},
35+
`decorators legacy vue ts ${counter.value.value}`,
36+
)
37+
})
38+
</script>
39+
<template>
40+
<Component />
41+
</template>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<script setup lang="tsx">
2+
import { defineComponent, h, ref } from 'vue'
3+
4+
function methodDecorator(
5+
target: unknown,
6+
propertyKey: string,
7+
descriptor: PropertyDescriptor,
8+
) {
9+
const originalMethod = descriptor.value
10+
descriptor.value = function () {
11+
const result = originalMethod.call(this)
12+
this.value.value += 1
13+
return result
14+
}
15+
}
16+
17+
const Component = defineComponent(() => {
18+
class Counter {
19+
value = ref(8)
20+
21+
@methodDecorator
22+
increment() {}
23+
}
24+
25+
const counter = new Counter()
26+
const inc = () => counter.increment()
27+
28+
return () => (
29+
<button class="decorators-legacy-vue-tsx" onClick={inc}>
30+
{`decorators legacy vue tsx ${counter.value.value}`}
31+
</button>
32+
)
33+
})
34+
</script>
35+
<template>
36+
<Component />
37+
</template>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extends": "../../tsconfig.json",
3+
"compilerOptions": {
4+
"target": "es2020",
5+
"experimentalDecorators": true
6+
}
7+
}

0 commit comments

Comments
 (0)