mod_06 - Managing Dependencies¶
Dependencies are not free - every one you add is code running in your process with full access to your application's memory and filesystem
Node's dependency model makes it trivially easy to add packages (one npm install away) and trivially easy to accumulate hundreds of transitive dependencies that you've never reviewed. Managing dependencies means understanding what you're pulling in, keeping them updated without breaking things , and knowing when to lock them down or push them out
npm update and outdated¶
# Check for outdated packages
npm outdated
# Example output:
# Package Current Wanted Latest Location
# express 4.17.1 4.18.2 4.18.2 my-app
# lodash 4.17.20 4.17.21 4.17.21 my-app
# jest 28.0.0 28.5.1 29.5.0 my-app
# Update to the "Wanted" version (respects semver ranges in package.json)
npm update
# Update a specific package
npm update express
# Install a specific version (overrides semver range)
npm install express@4.18.2
npm outdated shows three version columns: * Current - what's installed in node_modules * Wanted - the latest version matching your package.json semver range * Latest - the newest published version (may be outside your range)
If you use ^4.17.1, the "Wanted" column shows 4.18.2 - the latest 4.x version. "Latest" might show 5.0.0 - which requires a major upgrade
npm update vs manual upgrade¶
npm update only updates within your existing semver range. If you want to jump major versions:
# Upgrade to a specific major version
npm install express@5
npm dedupe¶
Removes duplicate packages by hoisting them
npm dedupe
# npm will try to flatten the dependency tree:
# Before: a -> lodash@4.17.20 (in a's node_modules)
# b -> lodash@4.17.20 (in b's node_modules)
# After: lodash@4.17.20 (hoisted to top node_modules)
# a and b both resolve to the same instance
#
# ```mermaid
# graph LR
# subgraph "Before dedupe"
# A["a"] --> AL["lodash@4.17.20<br/>in a's node_modules"]
# B["b"] --> BL["lodash@4.17.20<br/>in b's node_modules"]
# end
# subgraph "After dedupe"
# C["a"] --> TOP["lodash@4.17.20<br/>hoisted to top node_modules"]
# D["b"] --> TOP
# end
# ```
npm v7+ does this automatically during install. But running npm dedupe after manual interventions helps clean up
peer dependencies¶
Peer dependencies let a package specify that it needs a certain version of another package , but doesn't install it itself - the consumer must provide it
{
"name": "express-middleware-auth",
"version": "2.0.0",
"peerDependencies": {
"express": "^4.0.0"
}
}
This says: "I work with Express 4.x and you need to have express installed in your project." The package does NOT install express itself - it expects the consuming project to have it
Peer dependencies are used for: * Plugins - a Passport strategy needs Passport installed * Frameworks - a React component needs React installed * Tooling - an ESLint plugin needs ESLint installed
{
"name": "eslint-plugin-security",
"peerDependencies": {
"eslint": ">=8.0.0"
}
}
npm v7+ installs peer dependencies automatically if they're not already present. npm v6 only warns. In v7+, if the peer dependency version range conflicts with an existing dependency, npm fails the install - which is stricter but more deterministic
bundled and optional dependencies¶
bundledDependencies¶
Dependencies bundled with your package when publishing
{
"name": "my-cli-tool",
"dependencies": {
"native-addon": "^1.0.0"
},
"bundledDependencies": ["native-addon"]
}
Bundled dependencies are included in your published tarball - useful for: * Native addons that are painful to rebuild at install time * Packages from private registries * Vendored dependencies that need to ship together
The consumer doesn't fetch bundled deps from the registry - they come with your package. This means faster installs but larger package size
optionalDependencies¶
Dependencies that can fail to install without breaking the install
{
"optionalDependencies": {
"fsevents": "^2.3.0"
}
}
If fsevents fails to install (e.g., on Linux), npm install continues without error. Your code must handle the case where the optional dependency is missing:
let fsevents
try {
fsevents = require('fsevents')
} catch (err) {
// optional dependency not available - use fallback
}
Common use: platform-specific packages like fsevents (macOS file watcher , Linux doesn't need it)
lockfile management¶
package-lock.json is the source of truth for your installed tree
When lockfile conflicts happen¶
You add a dependency , Ahmed adds another , merge conflicts happen in package-lock.json
How to resolve:
# 1. Accept the conflicting lockfile
git checkout --theirs package-lock.json
# 2. Regenerate a consistent lockfile
npm install
# The install command rewrites package-lock.json with a combined dependency tree
# that includes both your changes and Ahmed's changes
Don't manually edit package-lock.json. The JSON structure is fragile and nested - one wrong byte and the file is invalid. Always regenerate via npm install
When to regenerate lockfile completely¶
# Delete and regenerate
rm package-lock.json
npm install
# This only when you're confident the lockfile is corrupted
# You lose the pinned versions - everyone gets fresh resolution
Don't delete package-lock.json casually. It pins every transitive dependency. Removing it means every install resolves to potentially different versions. This is how production-only bugs get introduced
monorepo patterns: npm workspaces¶
Monorepos (multiple packages in a single repository) use npm workspaces
{
"name": "my-monorepo",
"private": true,
"workspaces": [
"packages/*"
]
}
Directory structure:
flowchart TD
root["my-monorepo/"] --> pkg["package.json<br/>root with workspaces config"]
root --> pkgs["packages/"]
pkgs --> api["api/"]
api --> apipkg["package.json<br/>name: @my/api"]
api --> apisrc["src/"]
pkgs --> web["web/"]
web --> webpkg["package.json<br/>name: @my/web"]
web --> websrc["src/"]
pkgs --> shared["shared/"]
shared --> spkg["package.json<br/>name: @my/shared"]
shared --> ssrc["src/"] Workspace commands:
# Install all dependencies for all workspaces
npm install
# Run a script in a specific workspace
npm run test -w @my/api
# Run a script in all workspaces
npm run test --workspaces
# Add a dependency to a specific workspace
npm install lodash -w @my/api
# Run scripts in parallel across workspaces
npm run build --workspaces --if-present
npm workspaces hoist dependencies to the root node_modules when versions are compatible. This reduces disk space and gives you a single lockfile for the entire project
security: transitive dependencies¶
Your direct dependencies have dependencies , and those have dependencies. You're running code from dozens or hundreds of authors
# Count transitive dependencies
npm ls --all | wc -l # might shock you
# Find which package depends on a specific sub-dependency
npm explain lodash
# Audit all transitive dependencies
npm audit
# Check for duplicate packages (wasted space and potential conflicts)
npm dedupe --dry-run
Deprecation warnings¶
When a package is deprecated, npm prints a warning during install
npm WARN deprecated core-js@2.6.12: core-js@<3.23.3 is no longer maintained
Don't ignore these. A deprecated package means no security patches. Run npm outdated and find alternatives
Lockfile auditing¶
# Check what changed in the lockfile
git diff package-lock.json
# Check integrity of installed packages
npm cache verify
When reviewing a PR that updates dependencies , look at: * What packages changed (not just versions - new additions) * Where major versions changed (breaking changes) * What postinstall scripts were added
Tools for dependency management¶
# npm audit - known vulnerabilities
npm audit
# npm outdated - versions behind
npm outdated
# Make status - installed tree
npm ls
# Why is this dependency here?
npm explain some-package
For comprehensive dependency management , consider:
# Automated update tool
npx npm-check-updates
# Dependency dashboard (npm install -g npm-check)
npx npm-check
summary¶
npm outdatedshows Current vs Wanted vs Latest versionsnpm updaterespects semver ranges - usenpm installfor major upgrades- Peer dependencies: plugin expects consumer to provide the dependency
- Bundled dependencies ship with your package; optional deps can fail gracefully
- Don't manually edit package-lock.json - regenerate via
npm install - npm workspaces manage monorepo dependencies with hoisting
- Every transitive dependency is a supply chain risk - audit regularly
- Deprecation warnings mean no more security patches - update or replace
prerequisites¶
mod_05_scripts.md - npm scripts and lifecycle hooks
next -> mod_07_publish_packages.md