How to auto-import components in .tsx? files
Describe the bug
When using unplugin-vue-components to auto-import components, we noticed that the generated components.d.ts only declares global components for the vue module, for example:
declare module 'vue' {
export interface GlobalComponents {
TForm: typeof import('tdesign-vue-next')['Form'];
}
}
This works well for .vue files, but in .tsx or .ts files using JSX, TypeScript still reports an error:
<TForm>...</TForm>
// ❌ Cannot find name 'TForm'. ts(2552)
This plugin works great in Vue projects — we hope it can become even better with improved TSX support in the future!
Reproduction
none
System Info
windows
Used Package Manager
pnpm
Validations
- [x] Follow our Code of Conduct
- [x] Read the Contributing Guide.
- [x] Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
- [x] Check that this is a concrete bug. For Q&A, please open a GitHub Discussion instead.
- [x] The provided reproduction is a minimal reproducible of the bug.
shouldn't be <TForm> instead <Form>
shouldn't be
<TForm>instead<Form>
This is just a description error, but the actual result is still an error due to the reason of declare module 'vue'
Can you try adding vite-vue-jsx and the tsx extension in the vite vue components config?
https://github.com/unplugin/unplugin-vue-components/blob/main/src/types.ts#L89
Can you try adding vite-vue-jsx and the tsx extension in the vite vue components config?
https://github.com/unplugin/unplugin-vue-components/blob/main/src/types.ts#L89
Yes, I have already done so, but it only recognizes file extensions in the dirs directory, and the resulting components can only be used in the vue module, which makes it impossible to directly use these components in the tsx file
declare module 'vue' {
export interface GlobalComponents {
TForm: typeof import('tdesign-vue-next')['Form'];
}
}
+1
我目前使用naive-ui的解决方法,使用unplugin-auto-import/vite
查看代码/View code
function _getNaiveUiComponentNames() {
// 方法1: 从 web-types.json 读取(推荐)
const webTypesPath = path.resolve('node_modules/naive-ui/web-types.json');
if (fs.existsSync(webTypesPath)) {
const webTypes = JSON.parse(fs.readFileSync(webTypesPath, 'utf-8'));
const components = webTypes.contributions.html['vue-components'];
const componentNames = components.map((component: { name: string }) => component.name);
console.log('naive-ui components count (from web-types.json):', componentNames.length);
return componentNames;
}
// 方法2: 从 volar.d.ts 读取(备选)
const volarPath = path.resolve('node_modules/naive-ui/volar.d.ts');
if (fs.existsSync(volarPath)) {
const volarContent = fs.readFileSync(volarPath, 'utf-8');
// 匹配类似 "NAffix: (typeof import('naive-ui'))['NAffix']" 的行
const regex = /^\s+(N\w+):/gm;
const matches = [...volarContent.matchAll(regex)];
const componentNames = matches.map((match) => match[1]);
console.log('naive-ui components count (from volar.d.ts):', componentNames.length);
return componentNames;
}
console.warn('Could not find naive-ui component metadata files');
return [];
}
AutoImport({
imports: [
{
'naive-ui': [
'useModal',
'useDialog',
'useMessage',
'useNotification',
'useLoadingBar',
..._getNaiveUiComponentNames(),
],
},
],
vueTemplate: true,
}),
这将在auto-imports.d.ts生成全局类型
declare global {
const NA: typeof import('naive-ui')['NA']
const NAffix: typeof import('naive-ui')['NAffix']
// ...
const useModal: typeof import('naive-ui')['useModal']
// ...
}
Refer to #669. You can write a vite plugin to handle it
import fs from 'node:fs'
import { resolve } from 'node:path'
import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite'
const template = (constants: string[]) =>
[
'/* eslint-disable */',
'// @ts-nocheck',
'// Generated by fix-components-dts',
'export {}\n',
'/* prettier-ignore */',
'declare global {',
' ' + constants.join('\n '),
'}\n'
].join('\n')
const fixDts = (source: string, target: string) => {
const fileContent = fs.readFileSync(source, 'utf-8')
const constants: string[] = []
const typeImportRegex = /(\w+):\s*typeof\s*import\(['"]([^'"]+)['"]\)\['(\w+)'\]/g
fileContent.replace(typeImportRegex, (_: string, p1: string, p2: string, p3: string) => {
constants.push(`const ${p1}: typeof import('${p2}')['${p3}']`)
return ''
})
fs.writeFileSync(target, template(constants))
}
export function FixComponentsDts(source: string = 'components.d.ts'): Plugin {
let target = source.replace('.d.ts', '-global.d.ts')
return {
name: 'fix-components-dts',
async configResolved(config: ResolvedConfig) {
source = resolve(config.root, source)
target = resolve(config.root, target)
},
configureServer(server: ViteDevServer) {
const change = (filePath: string) => filePath === source && fixDts(filePath, target)
server.watcher.add(source)
server.watcher.on('add', change)
server.watcher.on('change', change)
},
buildStart() {
if (fs.existsSync(source)) fixDts(source, target)
}
} as Plugin
}
use
Components({
dts: 'types/components.d.ts'
}),
FixComponentsDts('types/components.d.ts') // dts url
result
/* eslint-disable */
// @ts-nocheck
// Generated by fix-components-dts
export {}
/* prettier-ignore */
declare global {
const Test: typeof import('./../components/Test.vue')['default']
}
Refer to #669. You can write a vite plugin to handle it
import fs from 'node:fs' import { resolve } from 'node:path' import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite'
const template = (constants: string[]) => [ '/* eslint-disable /', '// @ts-nocheck', '// Generated by fix-components-dts', 'export {}\n', '/ prettier-ignore */', 'declare global {', ' ' + constants.join('\n '), '}\n' ].join('\n')
const fixDts = (source: string, target: string) => { const fileContent = fs.readFileSync(source, 'utf-8') const constants: string[] = [] const typeImportRegex = /(\w+):\stypeof\simport('"['"])['(\w+)']/g fileContent.replace(typeImportRegex, (_: string, p1: string, p2: string, p3: string) => { constants.push(
const ${p1}: typeof import('${p2}')['${p3}']) return '' }) fs.writeFileSync(target, template(constants)) }export function FixComponentsDts(source: string = 'components.d.ts'): Plugin { let target = source.replace('.d.ts', '-global.d.ts')
return { name: 'fix-components-dts', async configResolved(config: ResolvedConfig) { source = resolve(config.root, source) target = resolve(config.root, target) }, configureServer(server: ViteDevServer) { const change = (filePath: string) => filePath === source && fixDts(filePath, target) server.watcher.add(source) server.watcher.on('add', change) server.watcher.on('change', change) }, buildStart() { if (fs.existsSync(source)) fixDts(source, target) } } as Plugin } use
Components({ dts: 'types/components.d.ts' }), FixComponentsDts('types/components.d.ts') // dts url result
/* eslint-disable */ // @ts-nocheck // Generated by fix-components-dts export {}
/* prettier-ignore */ declare global { const Test: typeof import('./../components/Test.vue')['default'] }
The current issue is that auto-import isn’t working in tsx files, and it’s not just a problem of missing dts type declarations.
Refer to #669. You can write a vite plugin to handle it import fs from 'node:fs' import { resolve } from 'node:path' import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite' const template = (constants: string[]) => [ '/* eslint-disable /', '// @ts-nocheck', '// Generated by fix-components-dts', 'export {}\n', '/ prettier-ignore /', 'declare global {', ' ' + constants.join('\n '), '}\n' ].join('\n') const fixDts = (source: string, target: string) => { const fileContent = fs.readFileSync(source, 'utf-8') const constants: string[] = [] const typeImportRegex = /(\w+):\s_typeof\s_import('"['"])['(\w+)']/g fileContent.replace(typeImportRegex, (_: string, p1: string, p2: string, p3: string) => { constants.push(
const ${p1}: typeof import('${p2}')['${p3}']) return '' }) fs.writeFileSync(target, template(constants)) } export function FixComponentsDts(source: string = 'components.d.ts'): Plugin { let target = source.replace('.d.ts', '-global.d.ts') return { name: 'fix-components-dts', async configResolved(config: ResolvedConfig) { source = resolve(config.root, source) target = resolve(config.root, target) }, configureServer(server: ViteDevServer) { const change = (filePath: string) => filePath === source && fixDts(filePath, target) server.watcher.add(source) server.watcher.on('add', change) server.watcher.on('change', change) }, buildStart() { if (fs.existsSync(source)) fixDts(source, target) } } as Plugin } use Components({ dts: 'types/components.d.ts' }), FixComponentsDts('types/components.d.ts') // dts url result / eslint-disable / // @ts-nocheck // Generated by fix-components-dts export {} / prettier-ignore */ declare global { const Test: typeof import('./../components/Test.vue')['default'] }The current issue is that auto-import isn’t working in tsx files, and it’s not just a problem of missing dts type declarations.
Well, you're right. It works in <script setup lang="tsx">, but it won't be automatically generated in .tsx files.