Skip to content
Created: August 15, 2024

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) ​

ts
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 API
    • virtual:env → export public-prefix variables and built-in mode fields
  • import.meta.env stays under Vite control; use explicit virtual-module imports here

  • Watch env-file creation, edits and deletion, then reload

📦 1. Overall structure of the plug-in ​

A complete plugin looks like this:

ts
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:

ts
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:

ts
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:

ts
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:

ts
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 ​

ts
// 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 ​

ts
// Match the four env files for config.mode exactly.
// Read public env again, then invalidate envId if loaded.

③ moduleGraph.invalidateModule ​

ts
const moduleNode = server.moduleGraph.getModuleById(id)
if (moduleNode) server.moduleGraph.invalidateModule(moduleNode)

④ Send a full-reload message ​

ts
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:

ts
import { useUser, useFetch } from 'virtual:auto-api'

📌 Use virtual env:

ts
import { VITE_APP_NAME, MODE } from 'virtual:env'

📌 Reload automatically after file changes:

ts
// The configureServer watcher invalidates virtual modules,
// then sends a full-reload message to the client.