Vite calls itself "the build tool for the web", but it contains a dev server. My past build tools did not contain a dev server. Vite is more like a comprehensive development environment that includes a dev server and a build command which both make use of powerful features designed to streamline the modern web development workflow.
The Vision: Vite as Part of a Modern Toolkit
Vite is a key piece in a larger vision for a high-performance web development ecosystem. This vision includes:
- Vite: A fast build tool and development server.
- Vitest: A fast test runner that shares Vite's configuration and principles.
- Rolldown: A fast, Rust-based bundler currently in preview, designed to be a Rollup-compatible successor to eventually power Vite's build command.
- Oxc: A high-speed JavaScript language toolchain for parsing, transforming, resolving, linting and more. Key parts of Oxc are already integrated into Vite and Vitest.
Dev Server vs. Build Command
Vite's core functionality is split into two primary commands, which share many of the same features and configuration options.
- Dev Server: Vite's development server is known for its incredible speed. It leverages native ES modules and avoids manual bundling during development. When you change a file, it only transpiles and serves that specific module, resulting in Hot Module Replacement (HMR) that is nearly instantaneous. It focuses on transpiling code quickly and leaves complex tasks like type checking to your IDE or a separate process.
- Build Command: For production, Vite uses Rollup for bundling. Rollup's focus on ES modules enables excellent tree-shaking, ensuring your final build is as small as possible. The build command is flexible enough for a variety of scenarios, from static websites and libraries to custom bundling for CMS platforms like WordPress or Rails.
Opinionated, Yet Customizable
Vite is intentionally opinionated, which means many common tasks work out-of-the-box with minimal configuration. This simplicity is one of its major strengths. However, it still offers extensive customization, particularly for the build process, which you can configure using a vite.config.js
file.
Scaffolding Your Project
Vite offers an interactive command to quickly set up a new project:
$ npm create vite@latest
This command will prompt you to select a framework and a variant (e.g., Pure React with TypeScript, or React with Typescript and React Router v7), providing you with a project pre-configured with recommended defaults.
Shared Features
Vite's dev server and build command share many functionalities, creating a consistent experience.
- Static Assets: Vite handles static assets through a
public
directory, and it automatically resolves imports for various file types like SVGs and PNGs without compilation errors. Any files in yourpublic
directory are always included in your bundle. - Client Types: Vite provides shims that offer type support for static asset imports and
import.meta.env
, preventing TypeScript complaints. - DotEnv and Modes: Vite has a robust environment variable system. It introduces "modes" to handle different scenarios (e.g.,
development
,production
,staging
). These modes automatically load corresponding.env
files and can be used in your code viaimport.meta.env.MODE
. This approach avoids issues withNODE_ENV
, which should always be set to "production" to avoid issues with dependencies. - CSS Support: Vite provides built-in support for PostCSS, enabling the use of CSS Modules, inline imports and it supports CSS pre-processors like SCSS, SASS or LESS, allowing for flexible styling workflows.
- JSX & TSX: With the right plugins (like
@vitejs/plugin-react
), Vite provides out-of-the-box support for JSX and TSX. - HTML Processing: The dev server offers file-based routing by default and intelligently processes HTML files, bundling only the assets that are explicitly referenced.
- Plugin Catalog: Vite has an extensive plugin ecosystem, but it's important to be cautious of supply chain attacks. Popular plugins include @vitejs/plugin-react (for Babel/JSX support), vite-plugin-lib-inject-css (for better css file management in component libraries), and vite-plugin-dts (for generating TypeScript definition files).
Library Mode
One of Vite's most powerful features is its Library Mode, which uses Rollup to create tree-shakable component libraries with minimal configuration.
One of the more unintuitive aspects of this mode, however, is configuring multiple entry points to retain file structure for better tree-shaking. The following code snippet is based on Rollup's example in their documentation and uses glob pattern matching to retrieve all file names to include as entrypoints:
import path from "node:path";
import { fileURLToPath } from "node:url";
import { glob } from "glob";
/**
* turn every file into an entry point as recommended by rollup
* (https://rollupjs.org/configuration-options/#input)
*/
function createEntryPointPerFile(): Record<string, string> {
return Object.fromEntries(
glob
// get all ts and tsx files in src/components
.sync(`${LIBRARY_SRC_ROOT}/**/*.{ts,tsx}`, {
// ignore test files
ignore: [`${LIBRARY_SRC_ROOT}/**/*.test.*`],
})
.map((relativeFilePath) => {
const moduleKey = getLibraryModuleKey(relativeFilePath);
const absolutePath = getAbsolutePath(relativeFilePath);
return [moduleKey, absolutePath];
}),
);
}
/**
* Module key is the path relative to the library src root,
* without extension.
* e.g. `src/components/atoms/Button/Button.tsx`
* => `atoms/Button/Button`
*/
function getLibraryModuleKey(filePath: string): string {
const withoutExtension = withoutExtension(filePath);
const moduleKey = path.relative(LIBRARY_SRC_ROOT, withoutExtension);
return normalizeToPosix(moduleKey);
}
function withoutExtension(filePath: string): string {
const extension = path.extname(filePath);
return filePath.slice(0, filePath.length - extension.length);
}
function normalizeToPosix(filePath: string): string {
return filePath.split(path.sep).join("/");
}
function getAbsolutePath(relativeFilePath: string): string {
const fileUrl = new URL(relativeFilePath, import.meta.url);
return fileURLToPath(fileUrl);
}
Code sample on how to create a single entry point per .ts/.tsx file
Conclusion & Vite Config Example
Vite's integrated approach and focus on speed make it a compelling choice for both application and library development. Its opinionated nature simplifies complex setups, while its extensive customization and powerful features offer the flexibility needed for more advanced use cases.
Lastly, I would like to show you the vite config file I used for a react component library. It implements a few features mentioned above and may help you as a loose reference. Every plugin and config has comments on what it does or why I used it.
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { libInjectCss } from "vite-plugin-lib-inject-css";
import dtsPlugin from "vite-plugin-dts";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { glob } from "glob";
const ROOT_DIR = path.dirname(fileURLToPath(import.meta.url));
const LIBRARY_SRC_ROOT = "./src/components";
// https://vite.dev/config/
export default defineConfig({
publicDir: "public",
css: {
modules: {
localsConvention: "camelCase" //enable usage of style.className
},
},
plugins: [
react(),
libInjectCss(), // inline css as "import <file>.css" in output
dtsPlugin({
// generate .d.ts type files in build
tsconfigPath: "tsconfig.app.json",
include: LIBRARY_SRC_ROOT,
exclude: "**/*.test.ts*",
}),
],
build: {
lib: {
// config for library mode
entry: path.resolve(ROOT_DIR, `${LIBRARY_SRC_ROOT}/index.ts`),
formats: ["es"],
},
rollupOptions: {
// strip code from these libraries from the build output
// https://rollupjs.org/configuration-options/#external
external: [
"react", "react-dom", "react/jsx-runtime",
new RegExp("@mui/.*"), new RegExp("@emotion/.*")
],
input: createEntryPointPerFile(),
// configure build output. entries for js files, assets for rest
// output automatically includes content of the public directory
// (https://vite.dev/guide/assets.html#the-public-directory)
output: {
entryFileNames: "[name].js",
assetFileNames: "assets/[name][extname]",
},
},
},
});
/**
* turn every file into an entry point as recommended by rollup
* (https://rollupjs.org/configuration-options/#input)
*/
function createEntryPointPerFile(): Record<string, string> {
return Object.fromEntries(
glob
// get all ts and tsx files in src/components
.sync(`${LIBRARY_SRC_ROOT}/**/*.{ts,tsx}`, {
// ignore test files
ignore: [`${LIBRARY_SRC_ROOT}/**/*.test.*`],
})
.map((relativeFilePath) => {
const moduleKey = getLibraryModuleKey(relativeFilePath);
const absolutePath = getAbsolutePath(relativeFilePath);
return [moduleKey, absolutePath];
}),
);
}
/**
* Module key is the path relative to the library src root
* without extension.
* e.g. `src/components/atoms/Button/Button.tsx`
* => `atoms/Button/Button`
*/
function getLibraryModuleKey(filePath: string): string {
const withoutExtension = withoutExtension(filePath);
const moduleKey = path.relative(LIBRARY_SRC_ROOT, withoutExtension);
return normalizeToPosix(moduleKey);
}
function withoutExtension(filePath: string): string {
const extension = path.extname(filePath);
return filePath.slice(0, filePath.length - extension.length);
}
function normalizeToPosix(filePath: string): string {
return filePath.split(path.sep).join("/");
}
function getAbsolutePath(relativeFilePath: string): string {
const fileUrl = new URL(relativeFilePath, import.meta.url);
return fileURLToPath(fileUrl);
}
Example vite-config.ts file for a react component library