Build Tools

The toolchain that transforms your source code into production-ready bundles.

Why Build

Browsers can't run your source code directly. Build tools bridge the gap.

// Your source code uses things browsers don't understand:
// - TypeScript (.ts, .tsx)
// - JSX (<Component />)
// - CSS Modules, Tailwind
// - Import from node_modules
// - Environment variables
// - Modern syntax older browsers don't support

// The build pipeline transforms source → browser-ready code:
//
// Source files          Build process           Output
// ─────────────        ─────────────          ──────────
// .tsx, .ts      →  TypeScript compiler  →  .js
// JSX            →  Babel/SWC transform  →  createElement calls
// 100+ modules   →  Bundler              →  few optimized files
// Modern JS      →  Transpiler           →  compatible JS
// Tailwind CSS   →  PostCSS              →  optimized CSS
// Images/fonts   →  Asset pipeline       →  optimized assets

// The build toolchain:
// Bundler      → Vite, Webpack, Rollup, esbuild
// Transpiler   → SWC, Babel, TypeScript compiler
// Linter       → ESLint, Biome
// Formatter    → Prettier, Biome
// Type checker → TypeScript (tsc)
// Test runner  → Vitest, Jest
// Dev server   → Vite, webpack-dev-server

Bundlers

Combine many files into few optimized bundles.

// Vite — the modern standard:
// Instant dev server (no bundling during dev!)
// Uses Rollup for production builds
// Blazing fast with native ES modules

// Create a Vite project:
// $ pnpm create vite my-app --template react-ts

// vite.config.ts:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  build: {
    outDir: "dist",
    sourcemap: true,
  },
  server: {
    port: 3000,
  },
});

// Commands:
// pnpm dev    → start dev server (instant)
// pnpm build  → production bundle
// pnpm preview → preview production build locally

Vite is the recommended bundler for new projects. It's fast because it uses native ES modules in development — no bundling needed.

1 / 2

Transpilers

Transform modern/extended syntax into standard JavaScript.

// TypeScript → JavaScript:
// Input (app.tsx):
interface User {
  name: string;
  age: number;
}
const greet = (user: User): string => {
  return "Hello, " + user.name;
};

// Output (app.js) — types are removed:
const greet = (user) => {
  return "Hello, " + user.name;
};

// JSX → JavaScript:
// Input:
const App = () => <div className="app"><h1>Hello</h1></div>;

// Output:
const App = () => createElement("div", { className: "app" },
  createElement("h1", null, "Hello")
);

// Modern syntax → older browsers:
// Input (modern):
const items = data?.items ?? [];
const doubled = items.map(x => x * 2);

// Output (for older browsers):
var _data;
var items = ((_data = data) === null ? void 0 : _data.items) || [];
var doubled = items.map(function(x) { return x * 2; });

// Transpiler options:
// SWC    — Rust-based, extremely fast (used by Next.js, Vite)
// Babel  — JavaScript-based, most plugins, slower
// esbuild — Go-based, fast, used by Vite for dev
// tsc    — TypeScript compiler (type-checking + transpiling)

// Most projects: SWC for speed, tsc for type-checking only
// tsconfig.json controls what TypeScript allows/targets

Linters & Formatters

Catch bugs and enforce consistent code style automatically.

// ESLint — catches bugs and enforces rules:
// eslint.config.js (flat config):
import js from "@eslint/js";
import tseslint from "typescript-eslint";

export default [
  js.configs.recommended,
  ...tseslint.configs.recommended,
  {
    rules: {
      "no-unused-vars": "warn",
      "no-console": "warn",
      "@typescript-eslint/no-explicit-any": "error",
    },
  },
];

// Run: pnpm eslint .
// Auto-fix: pnpm eslint . --fix

// Prettier — formats code consistently:
// .prettierrc:
{
  "semi": true,
  "singleQuote": false,
  "tabWidth": 2,
  "trailingComma": "all",
  "printWidth": 80
}

// Run: pnpm prettier --write .

// Biome — all-in-one (lint + format, fast):
// Single tool replacing ESLint + Prettier
// Written in Rust, 10-100x faster

// Typical setup (scripts in package.json):
"scripts": {
  "lint": "eslint .",
  "lint:fix": "eslint . --fix",
  "format": "prettier --write .",
  "check": "tsc --noEmit && eslint . && prettier --check ."
}

// Pre-commit hooks (lint-staged + husky):
// Run linting only on staged files before each commit
// Prevents bad code from entering the repo
// .husky/pre-commit: pnpm lint-staged
// lint-staged config: "*.{ts,tsx}": ["eslint --fix", "prettier --write"]

Dev Server

Hot Module Replacement — see changes instantly without refreshing.

// Vite dev server features:
// 1. Instant startup (no bundling — serves ES modules directly)
// 2. Hot Module Replacement (HMR) — updates without page reload
// 3. Source maps — debug original TypeScript in browser
// 4. Proxy API requests to avoid CORS
// 5. HTTPS support for local development

// How HMR works:
// 1. You edit a file and save
// 2. Vite detects the change (file watcher)
// 3. Only the changed module is re-compiled
// 4. Update is sent via WebSocket to browser
// 5. Browser applies change WITHOUT full reload
// 6. React state is preserved!

// vite.config.ts — dev server options:
export default defineConfig({
  server: {
    port: 3000,
    open: true,          // auto-open browser
    proxy: {
      "/api": {
        target: "http://localhost:8080",
        changeOrigin: true,
      },
    },
  },
});

// Environment variables:
// .env:
VITE_API_URL=https://api.example.com
VITE_APP_TITLE=My App

// Access in code (only VITE_ prefixed):
const apiUrl = import.meta.env.VITE_API_URL;

// .env.development — dev only
// .env.production  — production build only
// .env.local       — local overrides (gitignored)

// Full dev workflow:
// $ pnpm dev       → starts dev server with HMR
// Edit code → see changes instantly in browser
// $ pnpm build     → creates production bundle
// $ pnpm preview   → test production build locally

FAQ

Common questions about build tools.