Vite Plugin Basics
Understanding Vite’s plugin system matters because:
- Your electron-vite project (markdown-task-planner) uses Vite plugins
- Vitest reuses Vite’s plugin pipeline for test transforms
- Many Vite contribution opportunities are in the built-in plugins
Plugin Interface
A Vite plugin is an object with named hooks:
import type { Plugin } from "vite";
function myPlugin(): Plugin {
return {
name: "my-plugin",
// Rollup-compatible hooks (run in both dev and build)
resolveId(source, importer) {
// Resolve import paths
if (source === "virtual:my-module") {
return "\0virtual:my-module"; // \0 prefix = virtual module
}
},
load(id) {
// Provide module content
if (id === "\0virtual:my-module") {
return 'export const value = 42;';
}
},
transform(code, id) {
// Transform module content
if (id.endsWith(".custom")) {
return { code: transformCustomFormat(code), map: null };
}
},
// Vite-specific hooks
configureServer(server) {
// Add middleware to dev server
server.middlewares.use((req, res, next) => {
if (req.url === "/api/health") {
res.end("ok");
return;
}
next();
});
},
transformIndexHtml(html) {
// Transform index.html
return html.replace("</head>", '<script>/*injected*/</script></head>');
},
};
}
Hook Execution Order
Dev Server Request:
1. resolveId - "react" -> "/node_modules/react/index.js"
2. load - read the file (or provide virtual content)
3. transform - JSX -> JS, TS -> JS, etc.
4. (cached for subsequent requests)
Build:
1. buildStart
2. resolveId -> load -> transform (for each module)
3. renderChunk (modify output chunks)
4. generateBundle (write files)
Plugin Ordering
// vite.config.ts
export default defineConfig({
plugins: [
pluginA(), // runs first
pluginB(), // runs second
],
});
Plugins can force ordering with enforce:
{
name: "must-run-first",
enforce: "pre", // before normal plugins
transform(code) { ... }
}
{
name: "must-run-last",
enforce: "post", // after normal plugins
transform(code) { ... }
}
Execution order: enforce: 'pre' -> Vite internal plugins -> normal -> enforce: 'post'
Plugins You Already Use
@vitejs/plugin-react (mysukari.com, markdown-task-planner)
import react from "@vitejs/plugin-react";
// What it does:
// 1. transform: JSX/TSX -> React.createElement calls (via Babel or SWC)
// 2. transform: injects React Refresh (HMR for components)
// 3. resolveId: handles React imports
vite-tsconfig-paths (mysukari.com)
import tsconfigPaths from "vite-tsconfig-paths";
// What it does:
// 1. resolveId: reads tsconfig.json paths and resolves aliases
// Example: "@/components/Button" -> "/src/components/Button.tsx"
electron-vite (markdown-task-planner)
electron-vite is a wrapper that runs three Vite configs:
- Main process (Node.js target)
- Preload scripts (Node.js target, sandboxed)
- Renderer (browser target, your React UI)
Each has its own plugin pipeline, which is why your @vitejs/plugin-react only applies to the renderer.
Built-in Plugins (Vite Core)
These run automatically. Understanding them helps with Vite contributions:
| Plugin | File | What it does |
|---|---|---|
vite:resolve | plugins/resolve.ts | Module resolution (bare imports, aliases) |
vite:css | plugins/css.ts | CSS modules, PostCSS, preprocessors |
vite:oxc | plugins/oxc.ts | TS/JSX transforms via OXC (replaced esbuild in Vite 8) |
vite:html | plugins/html.ts | HTML entry processing, asset injection |
vite:import-analysis | plugins/importAnalysis.ts | Rewrite bare imports for browser |
vite:optimized-deps | optimizer/ | Pre-bundle node_modules deps |
Contribution opportunities like #18061 (fetchpriority attribute) and #20979 (importmap placement) are in plugins/html.ts.
Writing a Minimal Plugin
// Log every transformed module
function logTransforms(): Plugin {
return {
name: "log-transforms",
enforce: "post",
transform(code, id) {
if (!id.includes("node_modules")) {
console.log(`Transformed: ${id} (${code.length} chars)`);
}
},
};
}
Next Steps
- Vite Architecture - full architecture deep-dive
- Vite Dev Server - request pipeline internals
- Vite Research - contribution opportunities