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:
const state = ref(0)
watch(state, () => {})
onMounted(() => {})Even if you didn't write:
import { ref, watch, onMounted } from 'vue'The plug-in will also be automatically added for you during the compilation phase.
The final compilation becomes:
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.
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:
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.
const filename = id.split('?')[0]
// 对编译后的 JS 做作用域分析,不能把原始 SFC 当作普通 JS 解析🧠 Step 5: Avoid repeated imports (really professional)
If the user has written it manually:
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:
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:
npm i -D @babel/parser@7 @babel/traverse@7 magic-string@0.30Example:
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:
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.jsonCore 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:
// auto-import.d.ts
declare const ref: typeof import('vue')['ref']You can definitely continue to expand.