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.
Posted
DevOps pipeline loop motif with the title Bundling JavaScript with Vite and webpack
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.
dist/ 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:
Source + configs: A commit triggers CI. tsconfig.json and the bundler config (vite.config.ts, webpack.config.js) define how sources become dist/.
Install: npm ci installs the exact dependency tree from the lockfile, including the bundler and compiler.
Lint and type check: npx tsc --noEmit fails on type errors, and npx eslint . --max-warnings 0 fails on any lint warning.
Bundle: npx vite build writes hashed JS/CSS chunks, source maps and assets to dist/. webpack, Rspack and esbuild are alternatives.
Test and coverage: npx vitest run --coverage runs unit tests with V8 coverage; Playwright or Cypress test in the browser.
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.
Quality gate: Type check, lint, tests and scans must all pass before dist/ is accepted as a release candidate.
Fail the build (fail): A failed check stops the pipeline at the gate, so nothing is packaged, signed or published.
Analyse the bundle (pass): size-limit or source-map-explorer checks what went into dist/, so bundle-size regressions are caught.
SBOM and signing (pass): An SBOM of the bundled dependencies is generated with cyclonedx-npm, and the dist/ artifact is signed with Sigstore cosign.
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
Situation
Typical choice
New SPA (React, Vue, Svelte)
Vite
Existing large app with custom loaders
webpack (or Rspack as a faster drop-in)
Node.js service or CLI as one file
esbuild
npm library with ESM + CJS + types
tsup or Rollup, plus tsc for .d.ts
Next.js / Nuxt / Angular
The framework’s own builder (Turbopack/webpack, Vite, esbuild)
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.tsimport{defineConfig}from'vite'importreactfrom'@vitejs/plugin-react'exportdefaultdefineConfig({plugins:[react()],build:{target:'es2022',sourcemap:'hidden',// generate maps for error tracking, don't link them publiclychunkSizeWarningLimit:500,},})