Skip to content

How to auto-import components in .tsx? files #836

Open

Description

@atox996

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

  • Follow our Code of Conduct
  • Read the Contributing Guide.
  • Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
  • Check that this is a concrete bug. For Q&A, please open a GitHub Discussion instead.
  • The provided reproduction is a minimal reproducible of the bug.

Activity

  1. userquin commented on Apr 15, 2025

    @userquin
    Member

    shouldn't be <TForm> instead <Form>

  2. atox996 commented on Apr 16, 2025

    @atox996
    Author

    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'

  3. userquin commented on Apr 16, 2025

    @userquin
    Member

    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

  4. atox996 commented on Apr 17, 2025

    @atox996
    Author

    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'];
      }
    }
  5. l246804 commented on Oct 24, 2025

    @l246804

    +1

  6. yanhao98 commented on Oct 24, 2025

    @yanhao98

    我目前使用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']
      // ...
    }
  7. MaLuns commented on Oct 31, 2025

    @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+):\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']
    }
  8. l246804 commented on Oct 31, 2025

    @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+):\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.

  9. MaLuns commented on Oct 31, 2025

    @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+):\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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions