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 @@ + + diff --git a/playground/vue-jsx-ts-built-in/decorators-legacy/DecoratorsVueTsx.vue b/playground/vue-jsx-ts-built-in/decorators-legacy/DecoratorsVueTsx.vue new file mode 100644 index 00000000..ea1e4d4f --- /dev/null +++ b/playground/vue-jsx-ts-built-in/decorators-legacy/DecoratorsVueTsx.vue @@ -0,0 +1,37 @@ + + diff --git a/playground/vue-jsx-ts-built-in/decorators-legacy/tsconfig.json b/playground/vue-jsx-ts-built-in/decorators-legacy/tsconfig.json new file mode 100644 index 00000000..02179ebf --- /dev/null +++ b/playground/vue-jsx-ts-built-in/decorators-legacy/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "target": "es2020", + "experimentalDecorators": true + } +} diff --git a/playground/vue-jsx-ts-built-in/decorators/DecoratorsTs.ts b/playground/vue-jsx-ts-built-in/decorators/DecoratorsTs.ts new file mode 100644 index 00000000..229e37da --- /dev/null +++ b/playground/vue-jsx-ts-built-in/decorators/DecoratorsTs.ts @@ -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}`, + ) +}) diff --git a/playground/vue-jsx-ts-built-in/decorators/DecoratorsTsx.tsx b/playground/vue-jsx-ts-built-in/decorators/DecoratorsTsx.tsx new file mode 100644 index 00000000..c7f0b923 --- /dev/null +++ b/playground/vue-jsx-ts-built-in/decorators/DecoratorsTsx.tsx @@ -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 () => ( + + ) +}) diff --git a/playground/vue-jsx-ts-built-in/decorators/DecoratorsVueTs.vue b/playground/vue-jsx-ts-built-in/decorators/DecoratorsVueTs.vue new file mode 100644 index 00000000..f1864123 --- /dev/null +++ b/playground/vue-jsx-ts-built-in/decorators/DecoratorsVueTs.vue @@ -0,0 +1,36 @@ + + diff --git a/playground/vue-jsx-ts-built-in/decorators/DecoratorsVueTsx.vue b/playground/vue-jsx-ts-built-in/decorators/DecoratorsVueTsx.vue new file mode 100644 index 00000000..f8ec9bf1 --- /dev/null +++ b/playground/vue-jsx-ts-built-in/decorators/DecoratorsVueTsx.vue @@ -0,0 +1,32 @@ + + diff --git a/playground/vue-jsx-ts-built-in/decorators/tsconfig.json b/playground/vue-jsx-ts-built-in/decorators/tsconfig.json new file mode 100644 index 00000000..b7e80643 --- /dev/null +++ b/playground/vue-jsx-ts-built-in/decorators/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "target": "es2020", + "experimentalDecorators": false + } +} diff --git a/playground/vue-jsx-ts-built-in/index.html b/playground/vue-jsx-ts-built-in/index.html new file mode 100644 index 00000000..a285a008 --- /dev/null +++ b/playground/vue-jsx-ts-built-in/index.html @@ -0,0 +1,2 @@ +
+ diff --git a/playground/vue-jsx-ts-built-in/main.jsx b/playground/vue-jsx-ts-built-in/main.jsx new file mode 100644 index 00000000..e3784603 --- /dev/null +++ b/playground/vue-jsx-ts-built-in/main.jsx @@ -0,0 +1,26 @@ +import { createApp } from 'vue' +import DecoratorsTs from './decorators/DecoratorsTs' +import DecoratorsTsx from './decorators/DecoratorsTsx' +import DecoratorsVueTs from './decorators/DecoratorsVueTs.vue' +import DecoratorsVueTsx from './decorators/DecoratorsVueTsx.vue' +import DecoratorsLegacyTs from './decorators-legacy/DecoratorsTs' +import DecoratorsLegacyTsx from './decorators-legacy/DecoratorsTsx' +import DecoratorsLegacyVueTs from './decorators-legacy/DecoratorsVueTs.vue' +import DecoratorsLegacyVueTsx from './decorators-legacy/DecoratorsVueTsx.vue' + +function App() { + return ( + <> + + + + + + + + + + ) +} + +createApp(App).mount('#app') diff --git a/playground/vue-jsx-ts-built-in/package.json b/playground/vue-jsx-ts-built-in/package.json new file mode 100644 index 00000000..9909db4e --- /dev/null +++ b/playground/vue-jsx-ts-built-in/package.json @@ -0,0 +1,20 @@ +{ + "name": "@vitejs/test-vue-jsx-ts-built-in", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "debug": "node --inspect-brk vite", + "preview": "vite preview" + }, + "dependencies": { + "vue": "catalog:" + }, + "devDependencies": { + "@babel/plugin-syntax-decorators": "^7.27.1", + "@vitejs/plugin-vue": "workspace:*", + "@vitejs/plugin-vue-jsx": "workspace:*" + } +} diff --git a/playground/vue-jsx-ts-built-in/vite.config.js b/playground/vue-jsx-ts-built-in/vite.config.js new file mode 100644 index 00000000..cff83d7c --- /dev/null +++ b/playground/vue-jsx-ts-built-in/vite.config.js @@ -0,0 +1,28 @@ +import { defineConfig } from 'vite' +import vueJsxPlugin from '@vitejs/plugin-vue-jsx' +import vuePlugin from '@vitejs/plugin-vue' +import babelPluginSyntaxDecorators from '@babel/plugin-syntax-decorators' + +export default defineConfig({ + plugins: [ + vueJsxPlugin({ + tsTransform: 'built-in', + babelPlugins: [ + [ + babelPluginSyntaxDecorators, + // to test decorators we use only method decorators + // they have the same syntax in 'legacy' and in '2023-11' + { version: '2023-11' }, + ], + ], + }), + vuePlugin(), + ], + build: { + // to make tests faster + minify: false, + }, + optimizeDeps: { + disabled: true, + }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 61dc9197..d2fe8568 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -144,6 +144,9 @@ importers: '@babel/core': specifier: ^7.28.0 version: 7.28.0 + '@babel/plugin-syntax-typescript': + specifier: ^7.27.1 + version: 7.27.1(@babel/core@7.28.0) '@babel/plugin-transform-typescript': specifier: ^7.28.0 version: 7.28.0(@babel/core@7.28.0) @@ -316,6 +319,22 @@ importers: specifier: workspace:* version: link:../../packages/plugin-vue-jsx + playground/vue-jsx-ts-built-in: + dependencies: + vue: + specifier: 'catalog:' + version: 3.5.17(typescript@5.8.3) + devDependencies: + '@babel/plugin-syntax-decorators': + specifier: ^7.27.1 + version: 7.27.1(@babel/core@7.28.0) + '@vitejs/plugin-vue': + specifier: workspace:* + version: link:../../packages/plugin-vue + '@vitejs/plugin-vue-jsx': + specifier: workspace:* + version: link:../../packages/plugin-vue-jsx + playground/vue-legacy: dependencies: vue: @@ -514,11 +533,6 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - '@babel/parser@7.27.5': - resolution: {integrity: sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==} - engines: {node: '>=6.0.0'} - hasBin: true - '@babel/parser@7.27.7': resolution: {integrity: sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q==} engines: {node: '>=6.0.0'} @@ -565,6 +579,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-decorators@7.27.1': + resolution: {integrity: sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-import-assertions@7.26.0': resolution: {integrity: sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==} engines: {node: '>=6.9.0'} @@ -4490,10 +4510,6 @@ snapshots: dependencies: '@babel/types': 7.28.0 - '@babel/parser@7.27.5': - dependencies: - '@babel/types': 7.28.0 - '@babel/parser@7.27.7': dependencies: '@babel/types': 7.28.0 @@ -4541,6 +4557,11 @@ snapshots: dependencies: '@babel/core': 7.28.0 + '@babel/plugin-syntax-decorators@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-import-assertions@7.26.0(@babel/core@7.28.0)': dependencies: '@babel/core': 7.28.0 @@ -5914,7 +5935,7 @@ snapshots: '@vue/compiler-core@3.5.17': dependencies: - '@babel/parser': 7.27.7 + '@babel/parser': 7.28.0 '@vue/shared': 3.5.17 entities: 4.5.0 estree-walker: 2.0.2 @@ -5944,7 +5965,7 @@ snapshots: '@vue/compiler-sfc@3.5.17': dependencies: - '@babel/parser': 7.27.5 + '@babel/parser': 7.28.0 '@vue/compiler-core': 3.5.17 '@vue/compiler-dom': 3.5.17 '@vue/compiler-ssr': 3.5.17