Frontend Development 14 min read

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.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Getting Started with Vite Plugin Development for Frontend Developers

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

frontendNode.jsviteplugin developmentCode Injection
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.