unplugin-vue-components icon indicating copy to clipboard operation
unplugin-vue-components copied to clipboard

How to auto-import components in .tsx? files

Open atox996 opened this issue 8 months ago • 9 comments

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.

atox996 avatar Apr 15 '25 04:04 atox996

shouldn't be <TForm> instead <Form>

userquin avatar Apr 15 '25 10:04 userquin

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'

atox996 avatar Apr 16 '25 02:04 atox996

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

userquin avatar Apr 16 '25 09:04 userquin

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'];
  }
}

atox996 avatar Apr 17 '25 03:04 atox996

+1

l246804 avatar Oct 24 '25 09:10 l246804

我目前使用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']
  // ...
}

yanhao98 avatar Oct 24 '25 16:10 yanhao98

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']
}

MaLuns avatar Oct 31 '25 08:10 MaLuns

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.

l246804 avatar Oct 31 '25 08:10 l246804

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.

MaLuns avatar Oct 31 '25 10:10 MaLuns