|$ curl https://forge-ai.dev/api/markdown?path=docs/npm/scripts
$cat docs/npm-—-task-runners-&-scripts.md
updated Recently·22 min read·published

npm — Task Runners & Scripts

npmAutomationIntermediate
Introduction

The scripts field in package.jsonis npm's built-in task runner. It allows you to define aliases for CLI commands, chain operations through lifecycle hooks, and orchestrate complex build pipelines — all without external task runners like Gulp or Grunt.

Modern JavaScript projects rely heavily on npm scripts for development, testing, building, and deployment automation. Understanding the lifecycle, argument passing, and cross-platform patterns is essential for writing maintainable automation.

Script Lifecycle

npm automatically runs pre and post lifecycle hooks for every script. If you define prebuild, it runs before build. If you define postbuild, it runs after. This pattern enables clean composition without additional tooling.

package.json
JSON
1{
2 "scripts": {
3 "prebuild": "rimraf dist && echo 'Cleaning dist...'",
4 "build": "tsc --project tsconfig.build.json",
5 "postbuild": "echo 'Build complete!' && cp package.json dist/",
6
7 "pretest": "npm run lint",
8 "test": "vitest run",
9 "posttest": "echo 'Tests passed!'",
10
11 "predeploy": "npm run build && npm test",
12 "deploy": "npx wrangler deploy",
13 "postdeploy": "npm run healthcheck",
14
15 "healthcheck": "curl -f https://myapp.com/health"
16 }
17}
HookRuns BeforeTypical Use
prebuildbuild executesClean, lint, type-check
buildMain task (build, test, deploy)
postbuildbuild finishesNotify, cleanup, health check
terminal
Bash
1# npm build triggers:
2# 1. prebuild (if defined)
3# 2. build (the script itself)
4# 3. postbuild (if defined)
5
6npm run build
7
8# Lifecycle output:
9# > prebuild
10# Cleaning dist...
11# > build
12# tsc --project tsconfig.build.json
13# > postbuild
14# Build complete!

info

Lifecycle hooks are inherited for install, publish, and test. These run automatically — you do not need npm run preinstall; npm install triggers it automatically. Custom scripts (like build) require npm run build to trigger their hooks.
Concurrent Scripts

npm runs scripts sequentially by default. For parallel execution, you need external tools like concurrently or npm-run-all. These tools let you run multiple watchers, servers, or build steps simultaneously.

package.json
JSON
1{
2 "scripts": {
3 "dev": "concurrently -n "vite,tsc" -c "green,blue" "vite" "tsc --watch"",
4 "dev:alt": "npm-run-all --parallel vite tsc:watch",
5 "build": "npm-run-all --sequential lint typecheck build:vite",
6 "build:vite": "vite build",
7 "lint": "eslint src/",
8 "typecheck": "tsc --noEmit",
9 "tsc:watch": "tsc --watch --noEmit",
10 "start": "concurrently -k "node server.js" "npm run dev"",
11 "ci": "npm-run-all --sequential lint test build"
12 }
13}
terminal
Bash
1# Install concurrency helpers
2npm install -D concurrently npm-run-all
3
4# Run dev server + TypeScript watcher in parallel
5npm run dev
6
7# Run lint, test, build sequentially
8npm run ci
9
10# concurrently with custom name prefixes
11concurrently \
12 --names "API,WEB,WORKER" \
13 --prefix-colors "cyan,magenta,yellow" \
14 "npm run dev:api" \
15 "npm run dev:web" \
16 "npm run dev:worker"

warning

Be careful with --parallel when scripts share resources (ports, files, databases). Use npm-run-all --race to kill all processes when one exits with an error. The -k flag in concurrently does the same.
Passing Arguments

Arguments after -- are forwarded to the underlying script command. This pattern is essential for tool-specific flags without duplicating script definitions.

package.json
JSON
1{
2 "scripts": {
3 "test": "vitest",
4 "test:watch": "vitest --watch",
5 "lint": "eslint src/",
6 "build": "vite build"
7 }
8}
terminal
Bash
1# Arguments after -- are forwarded
2npm run test -- --run --reporter=verbose
3# Executes: vitest --run --reporter=verbose
4
5npm run lint -- --fix --max-warnings=0
6# Executes: eslint src/ --fix --max-warnings=0
7
8npm run build -- --mode=staging
9# Executes: vite build --mode=staging
10
11# Multiple arguments
12npm run test -- --run --coverage --reporter=json
13# Executes: vitest --run --coverage --reporter=json
🔥

pro tip

Use npm run script -- --flag instead of creating separate scripts like test:coverage or lint:fix. This keeps your package.json lean and gives developers flexibility at the command line.
Cross-Platform Scripts

Shell commands behave differently on Windows vs Unix. Environment variables use $VAR on Unix but %VAR% on Windows. Path separators, glob patterns, and command syntax all vary. The cross-env package normalizes environment variable setting across platforms.

package.json
JSON
1{
2 "scripts": {
3 "start:unix": "NODE_ENV=production node server.js",
4 "start:win": "SET NODE_ENV=production && node server.js",
5 "start": "cross-env NODE_ENV=production node server.js",
6
7 "build:env": "cross-env NODE_ENV=staging API_URL=https://staging.api.com vite build",
8
9 "test:ci": "cross-env CI=true NODE_OPTIONS=--max-old-space-size=4096 vitest run",
10
11 "clean": "cross-env-shell rimraf dist/ && echo 'Cleaned'"
12 }
13}
terminal
Bash
1# Install cross-env
2npm install -D cross-env
3
4# Without cross-env (Unix only)
5NODE_ENV=development node app.js
6
7# With cross-env (works everywhere)
8npx cross-env NODE_ENV=development node app.js
9
10# Multiple variables
11cross-env \
12 NODE_ENV=production \
13 PORT=8080 \
14 LOG_LEVEL=info \
15 node app.js
16
17# Using cross-env-shell for cmd chains
18cross-env-shell NODE_ENV=test "npm run lint && npm test"

Other Cross-Platform Utilities

rimraf — cross-platform rm -rf
mkdirp — cross-platform mkdir -p
which — cross-platform which command
shx — portable shell commands via Node

best practice

Always use cross-env for environment variables in shared scripts. If your team works on mixed OS platforms (macOS, Linux, Windows), every $VAR syntax without cross-env will break on Windows machines. Use rimraf instead of rm -rf for cross-platform compatibility.
Custom Script Runners

Beyond built-in npm scripts, dedicated task runners provide additional features: file watching, incremental builds, dependency graphs between tasks, and plugin systems.

ToolBest ForKey Feature
justProject-level commandsSimple command runner with recipes
makeMulti-language buildsDependency graph, incremental builds
turboMonorepo task orchestrationCaching, parallel execution, remote cache
nxLarge monoreposTask graph, affected commands, plugins
bunSpeed-critical tasksBuilt-in test runner, bun build, Bun.shell
package.json
JSON
1// package.json — using npm scripts directly
2{
3 "scripts": {
4 "dev": "vite",
5 "build": "vite build",
6 "preview": "vite preview",
7 "lint": "eslint src/",
8 "test": "vitest run"
9 }
10}

For most projects, npm scripts with concurrently and cross-env suffice. Reach for turbo or nx when you have a monorepo with 10+ packages and need intelligent caching and task orchestration.

Script Hooks & Lifecycle Events

npm provides lifecycle hooks for built-in operations like install, publish, and version. These hooks run automatically and are commonly used for setup scripts, pre-publish checks, and version bump automation.

package.json
JSON
1{
2 "scripts": {
3 "prepare": "husky || true",
4 "prepublishOnly": "npm run test && npm run build",
5 "preversion": "npm run lint",
6 "version": "npm run build && git add dist/",
7 "postversion": "git push && git push --tags",
8 "preinstall": "npx check-engine",
9 "postinstall": "patch-package"
10 }
11}
HookTriggered ByCommon Use
preparenpm install (both local + ci)Set up git hooks (husky)
prepublishOnlynpm publishRun tests before publishing
preversionnpm versionEnsure clean state before bump
versionnpm versionUpdate dist files after bump
postversionnpm versionPush tags to remote
preinstallnpm installVerify Node.js engine version

warning

The prepare hook runs on every npm install, including in CI. If your prepare script calls a devDependency, it must be installed first. The || true pattern (e.g., "prepare": "husky || true") prevents CI from failing if the tool is not yet installed.
$Blueprint — Engineering Documentation·Section ID: NPM-03·Revision: 1.0