09 · Build Tooling Intro¶
Why build tools exist¶
Modern JavaScript is usually written as many small modules, sometimes with syntax browsers don't natively support (JSX, TypeScript). Build tools take that source code and produce optimized files a browser can actually load quickly — bundling, transpiling, and minifying along the way.
npm scripts¶
package.json's scripts field turns arbitrary shell commands into short,
memorable names — the same interface regardless of what's running
underneath.
{
"name": "my-app",
"scripts": {
"start": "node server.js",
"dev": "vite",
"build": "vite build",
"test": "jest",
"lint": "eslint ."
}
}
npm run dev # starts the Vite dev server
npm run build # produces an optimized production build
npm test # "test" is special — runs without needing "run"
npm start # "start" is also special — same shortcut
Chaining and composing scripts¶
Scripts can call other scripts, letting you build up a pipeline instead of one giant command.
{
"scripts": {
"lint": "eslint .",
"test": "jest",
"build": "vite build",
"verify": "npm run lint && npm run test && npm run build"
}
}
What a bundler actually does¶
A bundler starts from an entry file, follows every import/require it
finds, and combines everything into a small number of output files —
resolving the dependency graph so the browser doesn't need to make dozens of
separate requests.
A bundler turns these two files into a single output file (roughly):
// dist/main.bundled.js (simplified illustration of the output)
function add(a, b) {
return a + b;
}
console.log(add(2, 3));
Along the way, bundlers typically also minify (strip whitespace/comments and shorten variable names) and tree-shake (remove exported code that's never actually imported anywhere).
Vite — the fast, modern default¶
Vite serves source files directly during development using native browser ES modules (near-instant startup, no bundling needed), then switches to a production bundler (Rollup) only for the final build.
npm create vite@latest my-app -- --template vanilla
cd my-app
npm install
npm run dev # instant dev server with hot reload
npm run build # bundled, minified output in dist/
webpack — the established, highly configurable option¶
webpack bundles for both development and production using a config file that describes entry points, output location, and "loaders" that teach it how to handle non-JS file types (CSS, images, TypeScript, etc.).
// webpack.config.js
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist"),
},
module: {
rules: [
{ test: /\.css$/, use: ["style-loader", "css-loader"] },
],
},
mode: "production", // enables minification and optimizations
};
Vite vs. webpack at a glance¶
| Aspect | Vite | webpack |
|---|---|---|
| Dev server startup | near-instant (native ES modules) | slower (bundles everything upfront) |
| Configuration | minimal, sensible defaults | explicit, highly configurable |
| Production build | uses Rollup under the hood | uses its own bundler |
| Ecosystem maturity | newer, rapidly adopted | very mature, huge plugin ecosystem |
| Best for | most new projects | projects needing deep, custom control |
Environment-specific config and dev vs. prod¶
Both tools distinguish a development build (fast rebuilds, readable output, source maps) from a production build (minified, optimized, no dev-only code) — always check which mode you're running before debugging "why is this so slow/large."
# Vite automatically sets import.meta.env.MODE
npm run dev # "development"
npm run build # "production"
How It Actually Works¶
A bundler's core job is building a module dependency graph by statically parsing
every import/require statement into an AST (much like V8's own parser does) without
executing any code — this static analysis is exactly what enables tree-shaking:
if the bundler's graph shows a named export is never imported anywhere in the reachable
graph, it can prove that code is dead and strip it, but only for ES modules, where
imports/exports are static by spec. CommonJS's dynamic require(computedPath) defeats
this analysis, which is a major reason ESM-authored libraries tree-shake far better
than CommonJS ones.
Source maps solve a real mismatch: the code actually executing in the browser (minified,
transpiled, bundled into one file) bears no resemblance to the code you wrote across
many files. A source map is a compact, VLQ-encoded table of (generated position) ->
(original file, line, column) mappings; when the DevTools debugger hits a breakpoint,
it looks up the currently-executing bytecode's generated position in that table and
displays your original source instead. Dev-mode bundlers (Vite, esbuild) skip most
bundling entirely during development, instead serving native ES modules directly to the
browser and relying on HTTP/2 to make many small requests cheap — bundling is deferred
to the production build step specifically because the browser's own module loader can
do the dependency resolution for you at dev time, faster than a bundler can rebuild a
whole graph on every save.
Exercise¶
Initialize a new project with npm init -y, add "dev", "build", and
"test" scripts to package.json (they can point to placeholder commands
like echo "building..." if you don't have Vite/webpack installed), and add
a "verify" script that chains lint, test, and build together with
&&. Run npm run verify and confirm it executes each step in order.