Scope: a teaching prototype for Vite 5/6. Combine sections 1–3 into one plugin file and install
npm i -D @babel/parser@7. It scans directly declared named functions in one directory level; it does not inject imports at identifier use sites. File changes trigger a full reload.
This is a plug-in prototype that works directly with Vite, integrating:
- ✅ Scan APIs and generate a module with named exports
- ✅ Inject virtual module
virtual:env(emulate import.meta.env) - ✅ Invalidate virtual modules and reload after file changes
- ✅ Virtual module system (virtual:xxx)
🌟 Overall effect (finally achievable)
import { useFoo, compute } from 'virtual:auto-api'
import { VITE_API_URL, MODE } from 'virtual:env'
// File changes reload the page; no self-accept handler is needed.Plug-in capabilities:
Automatically scan API folders (such as
src/composables)Collect function names in a virtual module; consumers still import them explicitly
Generate virtual module:
virtual:auto-api→ export automatic APIvirtual:env→ export public-prefix variables and built-in mode fields
import.meta.envstays under Vite control; use explicit virtual-module imports hereWatch env-file creation, edits and deletion, then reload
📦 1. Overall structure of the plug-in
A complete plugin looks like this:
import { promises as fs } from 'node:fs'
import path from 'node:path'
import { loadEnv } from 'vite'
import { parse } from '@babel/parser'
export default function AutoAllPlugin(options = {}) {
const apiDir = options.apiDir || 'src/composables'
const apiId = '\0virtual:auto-api'
const envId = '\0virtual:env'
let config
let env = {}
const readEnv = () => ({
...loadEnv(config.mode, config.envDir, 'VITE_'),
MODE: config.mode, DEV: !config.isProduction,
PROD: config.isProduction, BASE_URL: config.base
})
return {
name: 'vite:auto-all-plugin',
configResolved(resolved) {
config = resolved
env = readEnv()
},
resolveId(id) {
if (id === 'virtual:auto-api') return apiId
if (id === 'virtual:env') return envId
},
async load(id) {
if (id === apiId) return generateAutoApiModule(path.resolve(config.root, apiDir), config.root)
if (id === envId) return generateEnvModule(env)
},
configureServer(server) {
const apiRoot = path.resolve(config.root, apiDir)
const envFiles = ['.env', '.env.local', '.env.' + config.mode, '.env.' + config.mode + '.local']
.map(file => path.resolve(config.envDir, file))
server.watcher.add([apiRoot, ...envFiles])
const refresh = file => {
const absolute = path.resolve(file)
const relative = path.relative(apiRoot, absolute)
const inApi = relative !== '..' && !relative.startsWith('..' + path.sep) && !path.isAbsolute(relative)
let id
if (envFiles.includes(absolute)) { env = readEnv(); id = envId }
else if (inApi && /\.[jt]s$/.test(absolute)) id = apiId
if (!id) return
const mod = server.moduleGraph.getModuleById(id)
if (mod) server.moduleGraph.invalidateModule(mod)
server.ws.send({ type: 'full-reload', path: '*' })
}
for (const event of ['add', 'change', 'unlink']) server.watcher.on(event, refresh)
server.watcher.once('close', () => {
for (const event of ['add', 'change', 'unlink']) server.watcher.off(event, refresh)
})
}
}
}🧩 2. Automatic API import module (virtual:auto-api)
Similar to unplugin-auto-import, you need to scan the folder:
async function generateAutoApiModule(apiDir, root) {
let entries
try {
entries = await fs.readdir(apiDir, { withFileTypes: true })
} catch (error) {
if (error.code === 'ENOENT') return 'export {}'
throw error
}
const owners = new Map()
const exportsList = []
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
if (!entry.isFile() || !/\.[jt]s$/.test(entry.name) || entry.name.endsWith('.d.ts')) continue
const filepath = path.join(apiDir, entry.name)
const code = await fs.readFile(filepath, 'utf8')
const ast = parse(code, { sourceType: 'module', plugins: ['typescript'] })
const names = ast.program.body
.filter(node => node.type === 'ExportNamedDeclaration' && node.declaration?.type === 'FunctionDeclaration'
&& !node.declaration.declare)
.map(node => node.declaration.id.name)
for (const name of names) {
if (owners.has(name)) throw new Error('Duplicate API export: ' + name)
owners.set(name, filepath)
}
if (names.length) {
const id = '/@fs/' + filepath.replace(/\\/g, '/')
exportsList.push('export { ' + names.join(', ') + ' } from ' + JSON.stringify(id))
}
}
return exportsList.join('\n') || 'export {}'
}📌 Features:
- Automatically scan API folders
- Generate import expression
- Export directly declared named functions, including async; skip strings, comments and other export forms
- Invalidate the module and reload after API file creation, edits or deletion
🧩 3. Generate virtual environment variable module (virtual:env)
Vite's import.meta.env is an object, we turn it into a module export:
function generateEnvModule(env) {
const named = Object.entries(env)
.filter(([key]) => /^[A-Z_][A-Z0-9_]*$/.test(key))
.map(([key, value]) => 'export const ' + key + ' = ' + JSON.stringify(value) + ';')
return named.join('\n') + '\nexport default ' + JSON.stringify(env) + ';'
}Effect:
import { VITE_API_URL, MODE } from 'virtual:env'🧩 4. Boundary with built-in Vite import.meta.env
Text replacement can alter strings and comments and introduce undeclared identifiers. Import explicitly here:
import { MODE } from 'virtual:env'
console.log(MODE)But more recommended:
- Let users manually
import { MODE } from 'virtual:env' - or automatic injection (more complex: AST injection, not in this example)
🔥 5. HMR hot update logic
The snippets summarize the section 1 watcher sequence; enclosing scope is omitted, so they are not standalone plugins.
① API file changes → Update virtual:auto-api
// API paths are checked relative to apiRoot.
// On add/change/unlink, invalidate apiId if it has been loaded.② .env or .env.* changes → update virtual:env
// Match the four env files for config.mode exactly.
// Read public env again, then invalidate envId if loaded.③ moduleGraph.invalidateModule
const moduleNode = server.moduleGraph.getModuleById(id)
if (moduleNode) server.moduleGraph.invalidateModule(moduleNode)④ Send a full-reload message
server.ws.send({ type: 'full-reload', path: '*' })This strategy reloads the page without requiring application modules to self-accept HMR.
🧪 6. Final complete effect
📌 Use automatic API:
import { useUser, useFetch } from 'virtual:auto-api'📌 Use virtual env:
import { VITE_APP_NAME, MODE } from 'virtual:env'📌 Reload automatically after file changes:
// The configureServer watcher invalidates virtual modules,
// then sends a full-reload message to the client.