The JavaScript ecosystem has a lot of tooling. This page cuts through the noise: package managers, bundlers, transpilers, linters, and module resolution -- what each does, when to use it, and how they fit together.
Text
Tool Lock File node_modules Speed
──────────────────────────────────────────────────────────
npm package-lock.json Flat (hoisted) Baseline
yarn yarn.lock Flat (hoisted) Faster (cache)
pnpm pnpm-lock.yaml Symlinked (strict) Fastest (content-addressable)
Bash
# Install all dependencies
npm install # or: yarn, pnpm install
# Add a dependency
npm install express # or: yarn add express, pnpm add express
# Add a dev dependency
npm install -D vitest # or: yarn add -D, pnpm add -D
# Remove a dependency
npm uninstall express
# Run a script from package.json
npm run build # or: yarn build, pnpm build
# Run a binary from node_modules/.bin
npx vitest # or: yarn dlx, pnpm dlx
# Clean install (CI, reproducible)
npm ci # deletes node_modules, installs from lock file exactly
JSON
{
"name": "my-project",
"version": "1.0.0",
"type": "module", // ← ESM by default (.js files use import/export)
"main": "./dist/index.cjs", // ← CJS entry point
"module": "./dist/index.js", // ← ESM entry point (bundlers)
// Modern exports field (replaces main/module)
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
},
"./utils": "./dist/utils.js"
},
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"test": "vitest",
"lint": "eslint src/",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"express": "^4.18.0" // ^ = compatible (4.18.x, 4.19.x, etc.)
},
"devDependencies": {
"typescript": "~5.3.0", // ~ = patch only (5.3.0, 5.3.1, etc.)
"vitest": "^1.0.0"
},
"engines": {
"node": ">=20.0.0" // enforce minimum Node version
}
}
Text
^1.2.3 → >=1.2.3 <1.0.0 (any 1.x.x, most common)
~1.2.3 → >=1.2.3 <1.3.0 (only patch updates)
1.2.3 → exactly 1.2.3 (pinned)
* → any version (dangerous)
>=2.0.0 → 2.0.0 or higher
dependencies are needed at runtime (express, react). devDependencies are only needed during development (typescript, vitest, eslint). When someone installs your package, devDependencies are NOT installed.
When you write import x from "foo", the resolver figures out what file "foo" actually points to.
Text
import "./math" → Relative: looks for ./math.ts, ./math.js, ./math/index.ts
import "express" → Bare specifier: looks in node_modules
import "node:fs" → Node built-in (explicit)
import "@scope/pkg" → Scoped package in node_modules/@scope/pkg
Text
require("express")
1. Check node_modules/express/package.json "main" field
2. Check node_modules/express/index.js
3. If not found, go to parent directory's node_modules/
4. Repeat up to filesystem root
ESM resolution (import):
1. Check package.json "exports" field first
2. Fall back to "main" field
3. No automatic extension resolution (.js must be explicit!)
import { add } from "./math.js" (not "./math"). TypeScript with "moduleResolution": "NodeNext" enforces this. Bundlers like Vite handle it for you.
Text
"node" → Legacy CJS resolution (Node 12 style)
"node16" → Respects package.json "exports", requires extensions
"nodenext" → Same as node16 (latest Node resolution)
"bundler" → For Vite/Webpack: no extensions required, supports "exports"
Text
Many source files → Bundle → One or few output files
src/index.ts ─┐
src/utils.ts ─┤→ [Bundler] → dist/bundle.js
src/api.ts ─┘ dist/bundle.css
The original. Extremely configurable, plugin-heavy, slower than modern alternatives.
JavaScript
// webpack.config.js
export default {
entry: "./src/index.ts",
output: { filename: "bundle.js", path: "./dist" },
module: {
rules: [
{ test: /\.tsx?$/, use: "ts-loader" },
{ test: /\.css$/, use: ["style-loader", "css-loader"] },
]
},
resolve: { extensions: [".ts", ".tsx", ".js"] }
};
Dev server uses native ESM (no bundling during dev = instant start). Production uses Rollup.
JavaScript
// vite.config.ts
import { defineConfig } from "vite";
export default defineConfig({
build: { outDir: "dist" },
server: { port: 3000 }
});
Written in Go. 10-100x faster than Webpack. No config file needed.
Bash
# Bundle a TS file to JS
esbuild src/index.ts --bundle --outfile=dist/bundle.js --platform=node
# Minify for production
esbuild src/index.ts --bundle --minify --outfile=dist/bundle.min.js
Built on esbuild. The go-to for bundling TypeScript libraries.
Bash
# Outputs CJS + ESM + .d.ts files
tsup src/index.ts --format cjs,esm --dts
Text
Tool Speed Config Best For
───────────────────────────────────────────────────
Webpack Slow Heavy Legacy projects, complex apps
Vite Fast Minimal Frontend apps, dev experience
esbuild Fastest Minimal Bundling, quick builds
tsup Fast Zero-config TypeScript libraries
Rollup Medium Moderate Libraries, tree-shaking
Transpilers convert modern JS/TS into older JS that more environments can run.
Text
Tool Written In Speed Does Type Checking?
──────────────────────────────────────────────────────
tsc TypeScript Slow ✅ Yes (that's the point)
Babel JavaScript Medium ❌ No (strips types only)
SWC Rust Very Fast ❌ No (strips types only)
JSON
{
"scripts": {
"build": "tsup src/index.ts",
"typecheck": "tsc --noEmit",
"ci": "npm run typecheck && npm run build && npm run test"
}
}
--noEmit). Let esbuild/swc/tsup handle the actual compilation -- they're 10-100x faster because they skip type checking.
JSON
{
"presets": [
["@babel/preset-env", { "targets": "node 20" }],
"@babel/preset-typescript"
]
}
Bash
# Setup
npm init @eslint/config@latest
# Run
npx eslint src/
npx eslint --fix src/ # auto-fix
JavaScript
// eslint.config.js (flat config, ESLint 9+)
import tseslint from "typescript-eslint";
export default tseslint.config(
tseslint.configs.recommended,
{
rules: {
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-explicit-any": "warn",
}
}
);
Bash
npx prettier --write src/ # format all files
npx prettier --check src/ # check without writing (CI)
Bash
# Single tool that replaces ESLint + Prettier
npx @biomejs/biome check src/ # lint + format check
npx @biomejs/biome check --fix src/ # auto-fix both
Bash
# Check for known vulnerabilities
npm audit
# Auto-fix what it can (updates to patched versions)
npm audit fix
# Force fix (may include breaking changes)
npm audit fix --force
# See detailed info about a vulnerability
npm audit --json
Text
1. Reads your package-lock.json
2. Sends the dependency tree to the npm registry
3. Registry checks against the GitHub Advisory Database
4. Returns vulnerabilities with severity levels:
- critical, high, moderate, low, info
dependencies that run in production.
The lock file (package-lock.json / yarn.lock / pnpm-lock.yaml) pins exact versions. Always commit it. Without it, npm install can resolve different versions on different machines.
A monorepo holds multiple packages/apps in one repository. Workspaces let them share dependencies.
Text
my-monorepo/
├── package.json ← root, defines workspaces
├── packages/
│ ├── shared/ ← @myorg/shared
│ │ ├── package.json
│ │ └── src/
│ ├── api/ ← @myorg/api
│ │ ├── package.json
│ │ └── src/
│ └── web/ ← @myorg/web
│ ├── package.json
│ └── src/
JSON
// root package.json
{
"private": true,
"workspaces": ["packages/*"]
}
Bash
# Run a script in a specific workspace
npm run build --workspace=packages/api
# pnpm (more ergonomic)
pnpm --filter @myorg/api build
# Turborepo: run across all workspaces with caching
npx turbo build
The go-to task runner for monorepos. Caches results, runs tasks in parallel, respects dependency order.
JSON
// turbo.json
{
"tasks": {
"build": { "dependsOn": ["^build"], "outputs": ["dist/**"] },
"test": { "dependsOn": ["build"] },
"lint": {}
}
}
JSON
{
"scripts": {
"dev": "vite",
"build": "tsc --noEmit && vite build",
"preview": "vite preview",
"test": "vitest",
"test:ci": "vitest run --coverage",
"lint": "eslint src/",
"format": "prettier --write src/",
"typecheck": "tsc --noEmit",
"clean": "rm -rf dist node_modules",
"preinstall": "npx only-allow pnpm"
}
}
Text
preinstall → before npm install
postinstall → after npm install
prepare → after install + before publish (great for husky)
prepublish → before npm publish
JSON
{
"scripts": {
"dev": "concurrently \"vite\" \"tsc --watch --noEmit\"",
"check-all": "npm-run-all --parallel lint typecheck test"
}
}
Text
Tool Choice Why
─────────────────────────────────────────────
Package mgr pnpm Fast, strict, disk efficient
Bundler tsup/esbuild Fast, zero-config for libs
Dev server Vite Instant HMR, ESM-native
Type check tsc --noEmit Gold standard
Linter ESLint 9+ Flat config, TS support
Formatter Prettier No debates about style
Test runner Vitest Fast, Vite-compatible
Monorepo Turborepo Caching, parallel tasks
Bash
# New TS project from scratch
mkdir my-project && cd my-project
pnpm init
pnpm add -D typescript @types/node
npx tsc --init
# Or use a starter template
pnpm create vite my-app --template vanilla-ts