Skip to content
Created: July 29, 2024

This teaching plugin demonstrates a virtual environment module for Vite 5/6. Environment-file changes trigger a full-page reload in development; it does not replace the built-in env system.

js
import env from 'virtual:env'

console.log(env.VITE_API_URL)

It will automatically read:

  • .env
  • .env.local
  • .env.development
  • Or any custom env file you define

And generate a virtual module virtual:env for any code to import directly.

This type of plug-in is very useful in real projects.

🚀 Final effect ​

You can write in any file:

js
import env from 'virtual:env'

console.log(env)

Output example:

json
{
  "VITE_API_URL": "https://api.xxx.com",
  "VITE_TITLE": "My App"
}

This module is automatically generated and requires no manual maintenance.

🔥 1. Complete plug-in code (plug-in file: plugins/virtualEnv.js) ​

The code uses Vite loadEnv for file precedence and variable expansion, and stores cross-hook state in a closure.

js
import path from 'node:path'
import { loadEnv } from 'vite'

export default function virtualEnvPlugin(options = {}) {
  const id = 'virtual:env'
  const resolvedId = '\0' + id
  let config
  let envDir
  let clientEnv = {}

  function readEnv() {
    return loadEnv(config.mode, envDir, 'VITE_')
  }

  return {
    name: 'vite-plugin-virtual-env',
    configResolved(resolved) {
      config = resolved
      envDir = options.envDir
        ? path.resolve(config.root, options.envDir)
        : config.envDir
      clientEnv = readEnv()
    },
    resolveId(source) {
      if (source === id) return resolvedId
    },
    load(source) {
      if (source === resolvedId) {
        return 'export default ' + JSON.stringify(clientEnv)
      }
    },
    configureServer(server) {
      const files = new Set([
        '.env', '.env.local',
        '.env.' + config.mode, '.env.' + config.mode + '.local'
      ].map(file => path.resolve(envDir, file)))
      server.watcher.add([...files])
      const refresh = file => {
        if (!files.has(path.resolve(file))) return
        clientEnv = readEnv()
        const mod = server.moduleGraph.getModuleById(resolvedId)
        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. Use in vite.config.js ​

js
import { defineConfig } from 'vite'
import virtualEnvPlugin from './plugins/virtualEnv.js'

export default defineConfig({
  plugins: [
    virtualEnvPlugin()
  ]
})

📁 3. Usage examples ​

Assuming your .env.development:

VITE_API_URL=https://api.dev.com
VITE_APP_NAME=Dev App
SECRET_KEY=xxx

In your code you use:

js
import env from 'virtual:env'

console.log(env)

Result (only variables with the VITE_ prefix are exposed):

json
{
  "VITE_API_URL": "https://api.dev.com",
  "VITE_APP_NAME": "Dev App"
}

🔄 4. Support HMR ​

When an environment file is added, changed, or removed, the plugin reloads its data, invalidates any loaded virtual module, and triggers a full-page reload.

For example, change .env.local:

diff
-VITE_APP_NAME="Dev App"
+VITE_APP_NAME="Dev App Updated"

The browser will receive:

full-reload

This is behavior added by this plugin; restart the development server when changing built-in Vite env values as documented.

⚙️ 5. Why does Vite limit exposure to only the VITE_ prefix? ​

Vite’s official policy is also:

只有 VITE_ 前缀的变量才会暴露给浏览器

This is a security policy to avoid leaking sensitive variables to the client.

You can decide whether to strictly limit it, which can be customized in the plug-in:

js
if (k.startsWith('CLIENT_'))

🧠 6. Summary of plug-in principles (important) ​

Virtual module process:

  1. resolveId('virtual:env') → return \0virtual:env
  2. load('\0virtual:env') → return env JS content
  3. The browser can directly get the generated module when importing
  4. .env change → watcher → invalidate the module if present → full-page reload

This is the most beautiful part of Vite’s architecture: Virtual module + HMR + automatic dependency graph refresh