Skip to content

feat(vue-jsx): allow esbuild to perform ts transformation #621

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions packages/plugin-vue-jsx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions packages/plugin-vue-jsx/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
46 changes: 35 additions & 11 deletions packages/plugin-vue-jsx/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ function vueJsxPlugin(options: Options = {}): Plugin {
babelPlugins = [],
defineComponentName = ['defineComponent'],
tsPluginOptions = {},
tsTransform,
...babelPluginOptions
} = options
const filter = createFilter(include, exclude)
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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) {
Expand Down
2 changes: 2 additions & 0 deletions packages/plugin-vue-jsx/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ export interface Options extends VueJSXPluginOptions, FilterOptions {
/** @default ['defineComponent'] */
defineComponentName?: string[]
tsPluginOptions?: any
/** @default 'babel' */
tsTransform?: 'babel' | 'built-in'
}
Original file line number Diff line number Diff line change
@@ -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')
})
37 changes: 37 additions & 0 deletions playground/vue-jsx-ts-built-in/decorators-legacy/DecoratorsTs.ts
Original file line number Diff line number Diff line change
@@ -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}`,
)
})
33 changes: 33 additions & 0 deletions playground/vue-jsx-ts-built-in/decorators-legacy/DecoratorsTsx.tsx
Original file line number Diff line number Diff line change
@@ -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 () => (
<button class="decorators-legacy-tsx" onClick={inc}>
{`decorators legacy tsx ${counter.value.value}`}
</button>
)
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<script setup lang="ts">
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
}
}

const Component = defineComponent(() => {
class Counter {
value = ref(7)

@methodDecorator
increment() {}
}

const counter = new Counter()
const inc = () => counter.increment()

return () =>
h(
'button',
{
class: 'decorators-legacy-vue-ts',
onClick: inc,
},
`decorators legacy vue ts ${counter.value.value}`,
)
})
</script>
<template>
<Component />
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<script setup lang="tsx">
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
}
}
const Component = defineComponent(() => {
class Counter {
value = ref(8)
@methodDecorator
increment() {}
}
const counter = new Counter()
const inc = () => counter.increment()
return () => (
<button class="decorators-legacy-vue-tsx" onClick={inc}>
{`decorators legacy vue tsx ${counter.value.value}`}
</button>
)
})
</script>
<template>
<Component />
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"target": "es2020",
"experimentalDecorators": true
}
}
31 changes: 31 additions & 0 deletions playground/vue-jsx-ts-built-in/decorators/DecoratorsTs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { defineComponent, h, ref } from 'vue'

function methodDecorator(originalMethod: () => void, context: unknown) {
return function (this: { value: { value: number } }) {
const result = originalMethod.call(this)
this.value.value += 1
return result
}
}

export default defineComponent(() => {
class Counter {
value = ref(1)

@methodDecorator
increment() {}
}

const counter = new Counter()
const inc = () => counter.increment()

return () =>
h(
'button',
{
class: 'decorators-ts',
onClick: inc,
},
`decorators ts ${counter.value.value}`,
)
})
27 changes: 27 additions & 0 deletions playground/vue-jsx-ts-built-in/decorators/DecoratorsTsx.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { defineComponent, ref } from 'vue'

function methodDecorator(originalMethod: () => void, context: unknown) {
return function (this: { value: { value: number } }) {
const result = originalMethod.call(this)
this.value.value += 1
return result
}
}

export default defineComponent(() => {
class Counter {
value = ref(2)

@methodDecorator
increment() {}
}

const counter = new Counter()
const inc = () => counter.increment()

return () => (
<button class="decorators-tsx" onClick={inc}>
{`decorators tsx ${counter.value.value}`}
</button>
)
})
Loading
Loading