Skip to content
Created: July 23, 2024

The code targets the Vite 5/6 plugin API and demonstrates auto-importing rather than fully reproducing unplugin-auto-import. Automatic declarations, non-Vue templates, and more complex toolchains require separate integration.

Vite plug-in that implements the "automatic import API" function from 0, the core idea is the same as unplugin-auto-import.

🎯 Final effect example (what the plug-in does) ​

For example, if you write code in a Vue or React project:

ts
const state = ref(0)
watch(state, () => {})
onMounted(() => {})

Even if you didn't write:

ts
import { ref, watch, onMounted } from 'vue'

The plug-in will also be automatically added for you during the compilation phase.

The final compilation becomes:

ts
import { ref, watch, onMounted } from 'vue'

const state = ref(0)
watch(state, () => {})
onMounted(() => {})

🧠 Core principles of plug-ins ​

Vite’s transform() hook can get the file source code:

transform(code, id)

We do three things:

① Scan the code: find out which APIs are used ​

For example ref / computed / watch etc.

② Determine which library these APIs come from (vue/react/vueuse, etc.) ​

③ Automatically generate import statement + prepend at the front of the file ​

🚀 Step 1: Create plug-in file ​

Create plugins/auto-import.js

🚀 Step 2: Write a simplest automatic import plug-in ​

This core example uses an AST and lexical scope to find free identifiers without duplicate imports. Install @babel/parser, @babel/traverse, and magic-string. For Vue SFCs, @vitejs/plugin-vue compiles first, and this plugin processes the resulting JavaScript with post ordering.

js
import { parse } from '@babel/parser'
import traverseModule from '@babel/traverse'
import MagicString from 'magic-string'

const traverse = traverseModule.default || traverseModule

export default function autoImport(options = {}) {
  const imports = options.imports || {
    vue: ['ref', 'reactive', 'computed', 'watch', 'onMounted']
  }
  const sourceByName = new Map()
  for (const [pkg, names] of Object.entries(imports)) {
    for (const name of names) {
      if (sourceByName.has(name) && sourceByName.get(name) !== pkg) {
        throw new Error('Ambiguous auto-import: ' + name)
      }
      sourceByName.set(name, pkg)
    }
  }

  return {
    name: 'vite-plugin-auto-import',
    enforce: 'post',
    transform(code, id) {
      const filename = id.split('?')[0]
      if (id.includes('node_modules') ||
          !/\.(js|jsx|ts|tsx|vue)$/.test(filename) ||
          /[?&]type=style(?:&|$)/.test(id)) return
      const ast = parse(code, {
        sourceType: 'module',
        plugins: filename.endsWith('.ts') ? ['typescript'] : ['typescript', 'jsx']
      })
      const used = new Map()
      traverse(ast, {
        ReferencedIdentifier(p) {
          const name = p.node.name
          if (!sourceByName.has(name) || p.scope.hasBinding(name)) return
          const pkg = sourceByName.get(name)
          if (!used.has(pkg)) used.set(pkg, new Set())
          used.get(pkg).add(name)
        }
      })
      if (!used.size) return
      const statements = [...used].map(([pkg, names]) =>
        'import { ' + [...names].join(', ') + ' } from ' + JSON.stringify(pkg) + ';'
      )
      const result = new MagicString(code)
      result.prepend(statements.join('\n') + '\n')
      return {
        code: result.toString(),
        map: result.generateMap({ source: id, hires: true, includeContent: true })
      }
    }
  }
}

🚀 Step 3: Use plugins in Vite ​

vite.config.js:

js
import vue from '@vitejs/plugin-vue'
import AutoImport from './plugins/auto-import.js'

export default {
  plugins: [
    vue(),
    AutoImport({
      imports: {
        vue: ['ref', 'reactive', 'computed', 'watch', 'onMounted'],
        'vue-router': ['useRoute', 'useRouter']
      }
    })
  ]
}

✨ Now you have an auto-import plugin that actually works! ​

Let's move on to the advanced enhanced version.

🧠 Step 4: Process Vue SFC (only compile <script> content) ​

Do not extract script with a regex and discard an SFC’s template/style. Post ordering lets the example receive modules already compiled by the Vue plugin, while handling module query parameters.

js
const filename = id.split('?')[0]
// 对编译后的 JS 做作用域分析,不能把原始 SFC 当作普通 JS 解析

🧠 Step 5: Avoid repeated imports (really professional) ​

If the user has written it manually:

ts
import { ref } from 'vue'

We want to avoid duplicate additions.

Check whether the reference already has a lexical binding rather than merely testing whether the file contains an import. The core implementation uses:

js
if (sourceByName.has(name) && !p.scope.hasBinding(name)) {
  // 当前引用没有局部声明或导入绑定时,才加入候选导入
}

Scope analysis handles imports, aliases, and local declarations. Comments, strings, and non-reference property names are not imported as free identifiers.

🧠 Step 6: Improve accuracy through AST ​

Use @babel/parser to parse abstract syntax trees:

bash
npm i -D @babel/parser@7 @babel/traverse@7 magic-string@0.30

Example:

js
import { parse } from '@babel/parser'
import traverseModule from '@babel/traverse'
const traverse = traverseModule.default || traverseModule
const ast = parse(code, { sourceType: 'module', plugins: ['typescript', 'jsx'] })

Then detect:

js
const usedApis = new Set()
traverse(ast, {
  ReferencedIdentifier(p) {
    if (!p.scope.hasBinding(p.node.name)) usedApis.add(p.node.name)
  }
})

In this way, the content of the string will not be misjudged.

🧠 Step 7: Build the complete plugin ​

This directory layout supports further extensions; features such as automatic declarations are not implemented in the core example above:

auto-import/
 ├─ index.js
 ├─ utils/scan.js
 ├─ utils/generate.js
 ├─ package.json

Core index.js will be responsible for:

  • Read user configuration
  • Analyze source code
  • Use AST to find used identifiers
  • Automatically generate import
  • Support Vue files
  • Support multi-package mapping
  • Automatic dts generation can be added later

Such as:

ts
// auto-import.d.ts
declare const ref: typeof import('vue')['ref']

You can definitely continue to expand.