Post

JavaScript Bundlers: Vite, webpack, esbuild and tsc

How JavaScript and TypeScript code is compiled and bundled with tsc, Vite, webpack and esbuild, which config files they need, how to check and scan the output, and a GitHub Actions pipeline.

JavaScript Bundlers: Vite, webpack, esbuild and tsc

Once a package manager has installed dependencies, a compiler or bundler turns TypeScript, JSX and modern JavaScript into files that browsers or Node.js can run. For front ends this means bundling, tree shaking, code splitting and minification; for libraries and back ends it is often just the TypeScript compiler.

This post is part of Build Ecosystems by Programming Language. Installing and locking dependencies is covered in npm, pnpm and Yarn.

At a glance

CategoryCommon tools
Compilers / transpilerstsc (TypeScript), SWC, Babel, esbuild
BundlersVite (Rollup / Rolldown for production), webpack, Rspack, Rollup, esbuild, Turbopack (Next.js), tsup (libraries)
Build filestsconfig.json, vite.config.ts, webpack.config.js, rollup.config.mjs, tsup.config.ts, .browserslistrc, .swcrc, babel.config.js
Lint and formatESLint + typescript-eslint, Prettier, Biome
SAST (open source)Semgrep, CodeQL, eslint-plugin-security, SonarQube Community Build
SAST (commercial)Checkmarx One, Veracode, Snyk Code, SonarQube Server
SCAnpm audit, Retire.js (for vendored browser libraries), OSV-Scanner, Snyk, Socket
FrameworksReact, Vue, Angular, Svelte, Next.js, Nuxt
Test and coverageVitest, Jest, Playwright, Cypress; V8 / Istanbul coverage
Bundle analysisrollup-plugin-visualizer, webpack-bundle-analyzer, source-map-explorer, size-limit
Build outputdist/ with hashed JS/CSS chunks, source maps, static assets

How the build works

In CI, each stage of the generic build pipeline maps to a bundler command, plugin or scanner. Press play, or step through it:

  1. Source + configs: A commit triggers CI. tsconfig.json and the bundler config (vite.config.ts, webpack.config.js) define how sources become dist/.
  2. Install: npm ci installs the exact dependency tree from the lockfile, including the bundler and compiler.
  3. Lint and type check: npx tsc --noEmit fails on type errors, and npx eslint . --max-warnings 0 fails on any lint warning.
  4. Bundle: npx vite build writes hashed JS/CSS chunks, source maps and assets to dist/. webpack, Rspack and esbuild are alternatives.
  5. Test and coverage: npx vitest run --coverage runs unit tests with V8 coverage; Playwright or Cypress test in the browser.
  6. Security scans in parallel: semgrep scan --config p/typescript checks our code, npm audit (and Retire.js for vendored browser libraries) checks dependencies, and Gitleaks scans for secrets.
  7. Quality gate: Type check, lint, tests and scans must all pass before dist/ is accepted as a release candidate.
  8. Fail the build (fail): A failed check stops the pipeline at the gate, so nothing is packaged, signed or published.
  9. Analyse the bundle (pass): size-limit or source-map-explorer checks what went into dist/, so bundle-size regressions are caught.
  10. SBOM and signing (pass): An SBOM of the bundled dependencies is generated with cyclonedx-npm, and the dist/ artifact is signed with Sigstore cosign.
  11. Store the build (pass): The CI job uploads dist/ as a build artifact (actions/upload-artifact) for the deploy stage.
  • Bundlers do not type-check. Vite, esbuild and SWC strip types for speed; run tsc --noEmit as a separate CI step or type errors reach production.
  • Content hashing (index-3f9a2c.js) lets you cache assets forever and invalidate by filename.
  • Environment variables are baked in at build time. Anything exposed to the client (VITE_*, NEXT_PUBLIC_*) ends up in the bundle; never put secrets there.

Which tool when

SituationTypical choice
New SPA (React, Vue, Svelte)Vite
Existing large app with custom loaderswebpack (or Rspack as a faster drop-in)
Node.js service or CLI as one fileesbuild
npm library with ESM + CJS + typestsup or Rollup, plus tsc for .d.ts
Next.js / Nuxt / AngularThe framework’s own builder (Turbopack/webpack, Vite, esbuild)

Build files

FilePurposeCommit?
tsconfig.jsonTypeScript compiler options, strict, paths, targetYes
vite.config.tsVite plugins, aliases, build target, chunkingYes
webpack.config.jsEntries, loaders, plugins, optimisationYes
rollup.config.mjs / tsup.config.tsLibrary output formatsYes
.browserslistrcTarget browsers for CSS/JS transformsYes
.env, .env.productionBuild-time variablesOnly non-secret defaults
dist/Build outputNo

Commands by category

1
2
3
4
5
6
7
8
npx tsc --noEmit                                 # type check
npx vite build                                   # production bundle (Vite)
npx webpack --mode production                    # production bundle (webpack)
npx esbuild src/index.ts --bundle --platform=node --minify --outfile=dist/index.js   # single-file Node bundle
npx eslint . --max-warnings 0                    # lint
semgrep scan --config p/typescript --error       # SAST
npx vitest run --coverage                        # tests + coverage
npx source-map-explorer 'dist/assets/*.js'       # bundle size analysis

Minimal project

1
2
3
4
5
6
7
8
9
10
11
12
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  build: {
    target: 'es2022',
    sourcemap: 'hidden',          // generate maps for error tracking, don't link them publicly
    chunkSizeWarningLimit: 500,
  },
})
1
2
3
4
5
6
7
8
9
10
11
12
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "jsx": "react-jsx",
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

Frameworks

FrameworkStart a project (and its builder)
Reactnpm create vite@latest web -- --template react-ts (Vite)
Vuenpm create vue@latest (Vite)
Angularnpx @angular/cli new web (esbuild-based application builder)
Sveltenpx sv create web (Vite)
Next.jsnpx create-next-app@latest web (Turbopack / webpack)

CI with GitHub Actions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
name: frontend-ci
on: [push, pull_request]
permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version-file: .nvmrc
          cache: npm

      - run: npm ci
      - run: npx tsc --noEmit
      - run: npx eslint . --max-warnings 0
      - run: npx vitest run --coverage
      - run: npx vite build
        env:
          VITE_API_URL: ${{ vars.API_URL }}   # public, non-secret value only

      - uses: actions/upload-artifact@v4
        with:
          name: web-dist
          path: dist/

Best practices

  • Type-check separately (tsc --noEmit) because the bundler will not fail on type errors.
  • Set a performance budget (chunk size warning, size-limit) and fail the build when exceeded.
  • Generate source maps, but keep them private: upload them to your error tracker instead of serving them publicly.
  • Build once, deploy many: avoid environment-specific bundles where possible; load runtime config from a small config.json fetched at startup.
  • Keep secrets out of the bundle; anything in client-side env vars is public.
  • Scan vendored libraries (files copied into public/) with Retire.js, because npm audit cannot see them.
This post is licensed under CC BY 4.0 by the author.