Unpacking Vite: The Modern Build Tool for the Web

Streamline your frontend tooling with Vite. Discover the features this "build tool" brings along to become the center of your dev environment.

2 days ago   •   5 min read

By Benjamin Justice
Depiction of a blue letter "V" with connected dots, representing the build tool "Vite" and how it connects loose ends
Table of contents

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.

  1. 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.
  2. 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.

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

Spread the word