Table of Contents

1. Package Managers (npm, yarn, pnpm) 2. package.json Deep Dive 3. Module Resolution 4. Bundlers: Webpack, Vite, esbuild 5. Transpilers: Babel, SWC, tsc 6. Linting & Formatting 7. npm audit & Security 8. Monorepos & Workspaces 9. Task Runners & Scripts 10. Putting It All Together

1. Package Managers (npm, yarn, pnpm)

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)

Key Commands

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

Why pnpm?

pnpm stores packages in a global content-addressable store and symlinks them into your project. This means:
  • Packages are never duplicated on disk (saves GBs across projects)
  • Installs are faster because packages are hard-linked
  • Strict node_modules prevents "phantom dependencies" (using packages you didn't declare)

2. package.json Deep Dive

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
  }
}

Semver Ranges

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 vs devDependencies: 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.

3. Module Resolution

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

Node Module Resolution Algorithm

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!)
ESM gotcha: In ESM, you must include the file extension: import { add } from "./math.js" (not "./math"). TypeScript with "moduleResolution": "NodeNext" enforces this. Bundlers like Vite handle it for you.

tsconfig moduleResolution Options

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"

4. Bundlers: Webpack, Vite, esbuild

What a Bundler Does

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

Webpack

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"] }
};

Vite

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 }
});

esbuild

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

tsup (Library Bundler)

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

5. Transpilers: Babel, SWC, tsc

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)

Modern Approach: Strip Types, Check Separately

JSON
{
  "scripts": {
    "build": "tsup src/index.ts",
    "typecheck": "tsc --noEmit",
    "ci": "npm run typecheck && npm run build && npm run test"
  }
}
Don't use tsc as your build tool. Use it only for type checking (--noEmit). Let esbuild/swc/tsup handle the actual compilation -- they're 10-100x faster because they skip type checking.

Babel Config (babel.config.json)

JSON
{
  "presets": [
    ["@babel/preset-env", { "targets": "node 20" }],
    "@babel/preset-typescript"
  ]
}

6. Linting & Formatting

ESLint

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",
    }
  }
);

Prettier (Formatting)

Bash
npx prettier --write src/    # format all files
npx prettier --check src/    # check without writing (CI)

Biome (ESLint + Prettier alternative)

Bash
# Single tool that replaces ESLint + Prettier
npx @biomejs/biome check src/      # lint + format check
npx @biomejs/biome check --fix src/ # auto-fix both

7. npm audit & Security

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

What npm audit Actually Does

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
Not all audit warnings matter. A "critical" vulnerability in a dev-only tool (like a test framework) that never runs in production is often low-risk. Focus on vulnerabilities in dependencies that run in production.

Lockfile Importance

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.

8. Monorepos & Workspaces

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

Turborepo

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":  {}
  }
}

9. Task Runners & Scripts

npm Scripts

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"
  }
}

Lifecycle Scripts

Text
preinstall  → before npm install
postinstall → after npm install
prepare     → after install + before publish (great for husky)
prepublish  → before npm publish

npm-run-all / concurrently

JSON
{
  "scripts": {
    "dev": "concurrently \"vite\" \"tsc --watch --noEmit\"",
    "check-all": "npm-run-all --parallel lint typecheck test"
  }
}

10. Putting It All Together

Recommended TS project setup (2025):
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

Starter Commands

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