13.4 · difficulty 4/4 · 20 min read
Project 10: Build a Simplified Auto-Import Plugin
Apply everything you've learned about the plugin system — implement a real, working auto-import plugin that covers all the major hooks: transform / resolveId / load / generateBundle / configureServer.
Vite 8.1Stable
Feature Goal
ts
// vite.config.ts
autoImport({
imports: {
vue: ["ref", "computed", "reactive", "watch"],
"vue-router": ["useRouter", "useRoute"],
},
dts: "auto-imports.d.ts",
})
// Application code: no imports needed
const count = ref(0)
const router = useRouter()Full Implementation
Core Data Structure
ts
import type { Plugin } from "vite"
import { readFileSync, writeFileSync, existsSync } from "node:fs"
import { resolve } from "node:path"
interface AutoImportOptions {
imports: Record<string, string[]>
dts?: string | false
}
// Build a reverse lookup table: identifier → source
function buildLookup(imports: Record<string, string[]>): Map<string, string> {
const lookup = new Map<string, string>()
for (const [source, identifiers] of Object.entries(imports)) {
for (const id of identifiers) {
lookup.set(id, source)
}
}
return lookup
}transform hook: Core Injection Logic
ts
// Detect existing imports to avoid duplicate injection
function parseExistingImports(code: string): Set<string> {
const existing = new Set<string>()
const importRe = /import\s*\{([^}]+)\}\s*from\s*["'][^"']+["']/g
let match: RegExpExecArray | null
while ((match = importRe.exec(code)) !== null) {
for (const name of match[1].split(",")) {
existing.add(name.trim().split(" as ")[0].trim())
}
}
return existing
}
function transform(code: string, id: string, lookup: Map<string, string>): { code: string; map: null } | null {
if (!id.match(/\.(ts|tsx|js|jsx|vue)$/)) return null
if (id.includes("node_modules")) return null
if (!code) return null
const existing = parseExistingImports(code)
const toInject = new Map<string, Set<string>>()
// Scan the code for identifiers
// Note: this is a simplified regex version; production should use an AST
const identifierRe = /(?<![.'"\\w])\\b([a-zA-Z_$][a-zA-Z0-9_$]*)\\b/g
let match: RegExpExecArray | null
while ((match = identifierRe.exec(code)) !== null) {
const identifier = match[1]
if (existing.has(identifier)) continue // already manually imported
const source = lookup.get(identifier)
if (!source) continue
const set = toInject.get(source) ?? new Set()
set.add(identifier)
toInject.set(source, set)
}
if (toInject.size === 0) return null
const importLines = [...toInject.entries()]
.map(([source, names]) => `import { ${[...names].join(", ")} } from "${source}"`)
.join("\n")
return { code: `${importLines}\n${code}`, map: null }
}generateBundle hook: Generate .d.ts
ts
function generateDts(lookup: Map<string, string>, dtsPath: string): void {
const lines = [
"// Auto-generated by vite-plugin-auto-import, do not edit manually",
"// eslint-disable-next-line",
"export {}",
"",
"declare global {",
]
for (const [identifier, source] of lookup) {
lines.push(` const ${identifier}: typeof import("${source}")["${identifier}"]`)
}
lines.push("}")
writeFileSync(resolve(dtsPath), lines.join("\n"))
}Complete Plugin
ts
export function autoImport(options: AutoImportOptions): Plugin {
const { dts = "auto-imports.d.ts" } = options
const lookup = buildLookup(options.imports)
return {
name: "vite-plugin-auto-import",
transform(code, id) {
return transform(code, id, lookup)
},
buildEnd() {
if (!dts) return
generateDts(lookup, dts as string)
},
}
}Running the Project
bash
cd examples/plugin-auto-import
pnpm install
pnpm devUse Vue APIs like ref and computed directly in src/main.ts without any imports.
Check the generated auto-imports.d.ts to confirm that the type declarations are correct.
Advanced: Incremental Processing
For large projects, scanning all APIs on every file is too slow. An incremental strategy:
ts
// Track which APIs each file actually uses
const usedMap = new Map<string, Set<string>>()
transform(code, id) {
const result = transformWithTracking(code, id, lookup, usedMap)
return result
},
buildEnd() {
if (!dts) return
// Only generate declarations for APIs that are actually used
const usedIdentifiers = new Map<string, string>()
for (const identifiers of usedMap.values()) {
for (const id of identifiers) {
usedIdentifiers.set(id, lookup.get(id)!)
}
}
generateDts(usedIdentifiers, dts as string)
},Self-check
- Why inject imports in
transformrather thanload? - Why does
parseExistingImportscheck for existing imports? What goes wrong if you skip this check? - What is the difference between generating
.d.tsinbuildEndversusgenerateBundle? - Does this implementation work with Vue SFCs (
<script setup>)? Why or why not?
ts
// Add the following features to this auto-import plugin:
// 1. Support custom preset functions:
// imports: { from: "my-utils", as: { myFn: "utilFn" } }
// Effect: inject myFn as an alias for utilFn
// 2. Support glob-based ignore rules:
// ignore: ["**/node_modules/**", "**/*.spec.ts"]
// 3. Add a dev-mode-only configureServer hook so you can visit
// http://localhost:5173/__auto-import to see all detected API usage
export function enhancedAutoImport(options: {
imports: Record<string, string[]>
aliases?: Record<string, string>
ignore?: string[]
dts?: string
}): Plugin {
// TODO
}