Getting Started with Vite Plugin Development for Frontend Developers
This article guides pure frontend developers with little or no Node.js experience through the fundamentals of creating Vite plugins, covering concepts such as code injection, file operations, and file routing, and provides practical examples and best‑practice tips to enhance development efficiency.
Preface
If you are a pure frontend developer who has rarely or never written Node.js code, learning to write Vite plugins will give you a smooth experience.
This article summarizes entry‑level techniques and thinking for writing Vite plugins based on personal experience, hoping to help readers.
How to Get Started with Vite Plugins?
First have an idea, then learn. When you encounter problems that cannot be solved with pure frontend code, consider whether a Vite plugin can help. After forming an idea, consult the Vite/Rollup documentation to find the appropriate hooks—this is a good practice.
The Essence of Vite Plugins
When we want to write a Vite plugin, we should have a basic understanding of it.
Vite plugins are used to enhance code.
Code Enhancement
Code enhancement means using Vite's capabilities to modify project code ("magic") to achieve functionality.
The following three examples illustrate what code enhancement is.
Dynamic Code Injection
For example, after the project starts, you may want to print the project version, build time, and build environment in the browser console.
Build time and environment cannot be obtained in the frontend, only in the Vite environment, so a Vite plugin can achieve this.
After forming the idea, you need to know which hook can provide the required data. The configResolved hook can obtain the build environment, giving the plugin a basic shape.
import path from 'path'
import { defineConfig, PluginOption, ResolvedConfig } from 'vite'
import { createRequire } from 'module'
import dayjs from 'dayjs'
import tz from 'dayjs/plugin/timezone'
import utc from 'dayjs/plugin/utc'
dayjs.extend(tz)
dayjs.extend(utc)
function viteLogTime() {
let config: ResolvedConfig
let version: string
const currentTime = dayjs().tz("Asia/Shanghai").format("YYYY-MM-DD HH:mm:ss")
return {
name: 'vite-log-time',
configResolved(_config) {
config = _config
const require = createRequire(import.meta.url)
version = require(path.resolve(config.root, 'package.json')).version
}
}
}At this point you have the needed information; the next step is to decide how to print it to the browser console.
From a pure frontend perspective, you would simply add console.log in the code.
From the Vite plugin perspective, you also add console , but you must decide where and how to inject it.
Vite provides two hooks for code transformation: transform and transformIndexHtml . We will try both.
Using transform , you need to identify the entry file to ensure uniqueness.
import path from 'path'
import { defineConfig, PluginOption, ResolvedConfig } from 'vite'
import { createRequire } from 'module'
import dayjs from 'dayjs'
import tz from 'dayjs/plugin/timezone'
import utc from 'dayjs/plugin/utc'
dayjs.extend(tz)
dayjs.extend(utc)
function viteLogTime() {
let config: ResolvedConfig
let version: string
const currentTime = dayjs().tz("Asia/Shanghai").format("YYYY-MM-DD HH:mm:ss")
return {
name: 'vite-log-time',
configResolved(_config) {
config = _config
const require = createRequire(import.meta.url)
version = require(path.resolve(config.root, 'package.json')).version
},
transform(code, id) {
if(id.endsWith('src/main.tsx')) {
const info = {
mode: config.mode,
currentTime,
version,
}
return {
code: `
console.log('Build Info:', '${JSON.stringify(info)}')
${code}
`,
map: null
}
}
},
}
}After starting the project, the console will display the information.
File Operations
In a Node environment you can manipulate files to improve development efficiency. For example, handling internationalization resources.
Internationalization essentially means translating local language files into other languages and placing them in language files.
import zhCommon from "./zh/common.json"
import zhHome from "./zh/home.json"
import enCommon from "./en/common.json"
import enHome from "./en/home.json"
const languages = ["en", "zh"] as const
const resources = {
en: {
common: enCommon,
home: enHome
},
zh: {
common: zhCommon,
home: zhHome
},
}To avoid repetitive imports of language JSON files, a Vite plugin can collect all translation files in a specified directory and expose them as virtual modules.
export function i18nAlly(options?: I18nAllyOptions): PluginOption {
// Detector instance that collects all translation files in the user‑specified directory
const localeDetector = new LocaleDetector(options)
let server: ViteDevServer
return {
name: 'vite:plugin-i18n-ally',
enforce: 'pre',
async config() {
// Initialize the detector
await localeDetector.init()
},
// Hook for virtual files, see Vite docs
async resolveId(id: string, importer: string) {
const { virtualModules, resolvedIds } = localeDetector.localeModules
if (id in virtualModules) {
return VirtualModule.resolve(id) // e.g. \0/@i18n-ally/virtual:i18n-ally-en
}
return null
},
async load(id) {
const { virtualModules, resolvedIds, modules, modulesWithNamespace } = localeDetector.localeModules
if (id.startsWith(VirtualModule.resolvedPrefix)) {
const idNoPrefix = id.slice(VirtualModule.resolvedPrefix.length)
const resolvedId = idNoPrefix in virtualModules ? idNoPrefix : resolvedIds.get(idNoPrefix)
// If it is a virtual translation file, return its content
if (resolvedId) {
const module = virtualModules[resolvedId]
return typeof module === 'string' ? module : `export default ${JSON.stringify(module)}`
}
}
return null
},
} as PluginOption
}Frontend code can then import the virtual file to obtain translation resources, reducing repetitive work.
File Routing
File‑system routing is usually integrated in frameworks like Next.js, Remix, or Nuxt. It reduces repetitive configuration and makes project structure clearer.
If you use SSR frameworks and also have plain Vite SPA projects, a unified file‑system routing plugin can harmonize development habits.
Below is a simplified example (full code on GitHub).
import type * as Vite from 'vite'
function remixFlatRoutes(options: Options = {}): Vite.PluginOption {
return [
{
name: 'vite-plugin-remix-flat-routes',
// Frontend imports the virtual file to get the assembled routes
async resolveId(id) {
if (id === 'virtual:route') {
return '\0virtual:route'
}
return null
},
async load(id) {
switch (id) {
case '\0virtual:route': {
// Scan project files, find route files, and assemble them
const routes = findRoutes()
const { routesString, componentsString } = await routeUtil.stringifyRoutes(routes)
return {
code: `import React from 'react';
${componentsString}
export const routes = ${routesString};
`,
map: null,
}
}
default:
break
}
return null
},
},
]
}The core idea is to parse route files on the Node side, then expose the assembled data to the frontend via a virtual module.
Conclusion
The examples above are business‑related plugins; Vite also offers many build‑time plugins such as code zip compression and analysis, which are less suitable for beginners.
After writing several Vite plugins, I summarize the following advice:
Know what plugin you want to build before you start.
If you are new to Vite plugins, the hardest part is often choosing the right hook; once you know the problem you need to solve, the documentation or AI can quickly guide you.
In Vite plugins, frontend code is just a string—you can add, delete, or modify it freely; don’t fear breaking things.
Study entry‑level plugin code such as vite-plugin-html or vite-plugin-legacy to understand hook usage.
Below are some of my Vite plugins for reference:
vite-plugin-i18n-ally – automatic lazy‑loading of i18n resources
vite-plugin-remix-flat-routes – file‑system routing in Remix‑style
vite-plugin-public-typescript – inject TypeScript into HTML
vite-plugin-prerelease – dynamically switch between pre‑release and production environments
vite-plugin-istanbul-widget – frontend code coverage reporting tool
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.