Virtual Modules in Vite's Plugin Mechanism
2025年9月30日Elecmonkey
This article was translated by AI and has not been manually reviewed.
Virtual Modules are modules dynamically generated inside build tools. They do not correspond to real files in the filesystem; instead, they are generated by plugins during compilation and imported at runtime just like ordinary ESM modules. The official Vite documentation notes that virtual modules allow developers to inject compile-time information into source code through normal ESM import syntax. They are usually named with a virtual: prefix, preferably with the plugin name as a namespace to avoid conflicts, for example virtual:my-module.
Creating Virtual Modules in a Plugin
To implement virtual modules in a Vite plugin, you need two main hooks: resolveId and load. The usual approach is to define a virtual module identifier, for example const virtualId = 'virtual:foo'. In the resolveId(id) hook, if id === virtualId, return an internal ID with a prefix, such as '\0' + virtualId, indicating that this plugin is responsible for handling this import. Then, in the load(id) hook, check whether it is the prefixed internal ID; if it matches, return the module content as a string of code. For example:
// vite-plugin-foo.js(插件示例)
export default function fooPlugin() {
const virtualId = 'virtual:foo'
const resolvedVirtualId = '\0' + virtualId
return {
name: 'vite-plugin-foo',
resolveId(id) {
if (id === virtualId) {
return resolvedVirtualId // 捕获虚拟模块 ID
}
},
load(id) {
if (id === resolvedVirtualId) {
// 返回虚拟模块的导出内容
return 'export const msg = "Hello from virtual module";'
}
}
}
}
Note that the returned resolvedVirtualId has a \0 prefix. This is the Rollup/Vite convention for handling virtual modules and prevents other resolution logic, such as filesystem lookup or aliases, from interfering with this ID.
Example: Definition, Import, and Lifecycle
The following example demonstrates how to define, import, and use a virtual module. Suppose we create a plugin named virtual-sum-plugin.js in a project to dynamically generate a module that calculates the sum of an array:
// virtual-sum-plugin.js
export default function sumPlugin() {
const virtualSumId = 'virtual:sum'
const resolvedId = '\0' + virtualSumId
return {
name: 'vite-plugin-virtual-sum',
resolveId(id) {
if (id === virtualSumId) {
return resolvedId
}
},
load(id) {
if (id === resolvedId) {
// 虚拟模块导出的内容:一个求和函数
return 'export default function sum(arr, i = 0) { return i >= arr.length ? 0 : arr[i] + sum(arr, i + 1); }'
}
}
}
}
Import and use this plugin in vite.config.js:
import sumPlugin from './virtual-sum-plugin.js'
export default {
plugins: [
sumPlugin()
]
}
Now the application code can directly import and use this virtual module:
import sum from 'virtual:sum'
console.log(sum([1, 3, 5, 7, 9])) // 输出 25
As shown above, during the build this module does not come from the filesystem. It is generated from the string returned by the plugin's load hook. In development mode, every time this module is imported, the plugin's load hook is called to obtain its content. In production builds, the Rollup bundler executes the same hook logic.
TypeScript Typing Issues
In TypeScript projects, importing virtual modules requires adding type definitions to a global declaration file.
For example:
declare module 'virtual:*' {
const content: any
export default content
}
This allows TS to accept any import that starts with virtual:.
HMR Updates
Virtual modules also support HMR while the dev server is running. If the generated content of a virtual module depends on external data, such as JSON files, you can implement the handleHotUpdate hook in the plugin to manually update the module content.
When virtual module content changes, you need to use the handleHotUpdate hook to manually trigger module updates; otherwise, Vite will not automatically refresh this kind of module by default. In this hook, you can call moduleGraph.invalidateModule() or return the corresponding module to prompt the client to hot reload.
So What Can Virtual Modules Be Used For?
-
Injecting compile-time/runtime variables: For example, a plugin can generate a module for specific environment variables and import them into the application as constants. The community plugin vite-plugin-env-import demonstrates how to use virtual modules such as
virtual:envandvirtual:env:publicto export variables from.envas static strings. This way, code can directly get environment configuration throughimport { VITE_API } from 'virtual:env:public'and similar imports without manually managingimport.meta.env. -
Dynamic configuration and route generation: Virtual modules can dynamically generate code based on file structure or configuration. For example, a plugin can scan a directory, generate route configuration or certain constant files, and then use them at runtime through
import virtual:.... This avoids manual configuration maintenance.
Then you can refer back to my description:
Vue modules and routes can both be generated dynamically. Modern frontend frameworks and Vue also provide SSR/SSG capabilities, so we can absolutely build our own site generator in a way that feels more like "writing frontend code" rather than "writing a compiler" and constantly dealing with code generation.
By the way, I really like these two elegant projects.
VitePress - Vite & Vue Powered Static Site Generator
Slidev - Presentation Slides for Developers