npm Scripts
The scripts field turns package.json into your project's task runner. Instead of memorizing long command lines, you give them names — npm run dev, npm test, npm run build — and everyone on the team uses the same vocabulary. Scripts also chain, hook into lifecycle events, and run locally-installed tools without global installs.
Defining and running
package.json
{
"scripts": {
"dev": "node --watch src/index.js",
"build": "tsc -p .",
"test": "node --test",
"lint": "eslint .",
"start": "node dist/index.js"
}
}npm run dev # run any script by name npm run # list all available scripts npm test # special: no "run" needed npm start # special: no "run" needed
The local .bin secret
Why can you write "build": "tsc" when TypeScript was installed locally, not globally? Because npm prepends node_modules/.bin to PATH for the duration of the script. Every package with a bin entry drops an executable there:
What npm adds to PATH during a script
node_modules/.bin/ ├─ tsc → symlink into node_modules/typescript ├─ eslint → symlink into node_modules/eslint └─ jest → symlink into node_modules/jest So "tsc" in a script resolves here FIRST — no global install needed.
Pre and post hooks
For any script x, npm automatically runs prex before it and postx after — if those scripts exist. This is implicit; you never call them directly:
{
"scripts": {
"prebuild": "rimraf dist",
"build": "tsc -p .",
"postbuild": "echo Build complete"
}
}$ npm run build > prebuild (rimraf dist) > build (tsc -p .) > postbuild (echo Build complete)
Chaining and composing
Combine scripts with shell operators or by calling other scripts. && runs sequentially (stop on failure); & runs in parallel:
{
"scripts": {
"lint": "eslint .",
"typecheck": "tsc --noEmit",
"test": "node --test",
"check": "npm run lint && npm run typecheck && npm run test",
"ci": "npm run check"
}
}Passing arguments
Use -- to forward extra arguments through npm to the underlying command:
npm test -- --watch # passes --watch to the test runner npm run lint -- --fix # passes --fix to eslint
Reading config and env
Scripts run with a rich environment: every package.json field is exposed as npm_package_*, and npm config as npm_config_*:
{
"version": "2.4.1",
"scripts": { "release": "echo Releasing v$npm_package_version" }
}$ npm run release Releasing v2.4.1