endencies
npm automatically adds node_modules/.bin to the PATH for every script execution. This means you can invoke locally installed tools without npx or global installations. This ensures that every developer and CI runner uses the exact same tool versions defined in package.json.
Implementation:
{
"scripts": {
"compile": "esbuild src/index.ts --bundle --outfile=dist/app.js --platform=node",
"lint": "biome check src/",
"test": "vitest run"
}
}
Rationale: Using esbuild, biome, and vitest directly relies on local devDependencies. This eliminates version mismatches and reduces setup time. Avoid npx inside scripts for local tools; it adds unnecessary overhead and can mask missing dependencies.
2. Orchestrate with Lifecycle Hooks
Lifecycle hooks allow you to attach pre- and post-conditions to standard commands. This enforces quality gates without requiring developers to remember complex command sequences.
Implementation:
{
"scripts": {
"prebuild": "npm run lint && npm run typecheck",
"build": "esbuild src/index.ts --bundle --outfile=dist/app.js",
"postbuild": "node scripts/notify-build.js",
"prepare": "husky install"
}
}
Rationale:
prebuild ensures code quality before compilation. If linting or typechecking fails, the build aborts immediately.
postbuild handles cleanup or notifications only after a successful build.
prepare runs automatically after npm install, setting up git hooks or other environment requirements.
3. Handle Arguments and Environment Variables
Scripts often need dynamic configuration. npm provides mechanisms for argument injection and environment variable handling that work across platforms when combined with the right tools.
Argument Injection:
Use -- to pass arguments to the underlying command.
npm run compile -- --minify
npm run test -- --reporter=verbose
Environment Variables:
For cross-platform compatibility, use cross-env to set variables.
{
"scripts": {
"serve": "cross-env NODE_ENV=production node dist/app.js"
}
}
Rationale: Direct environment variable assignment (e.g., NODE_ENV=prod command) fails on Windows CMD. cross-env abstracts this difference, ensuring scripts run identically on all OSes.
4. Execution Strategies: Sequential vs. Parallel
npm scripts support chaining, but raw shell operators (&&, &, ;) are not cross-platform. Use npm-run-all for reliable parallel and sequential execution.
Implementation:
{
"scripts": {
"validate": "npm-run-all --parallel lint typecheck test",
"deploy": "npm-run-s build test push:prod",
"clean": "rimraf dist build .turbo"
}
}
Rationale:
npm-run-all --parallel runs tasks concurrently, reducing total execution time.
npm-run-s (run-sequential) ensures tasks run one after another, stopping on failure.
rimraf replaces rm -rf, providing cross-platform file deletion.
5. Monorepo Script Execution
In workspace-based projects, npm provides flags to run scripts across packages efficiently.
Implementation:
{
"workspaces": ["packages/*"],
"scripts": {
"build": "npm run build --workspaces-if-present",
"test": "npm run test --workspaces-if-present"
}
}
Rationale: --workspaces-if-present runs the script in all workspace packages that define it, skipping those that don't. This prevents errors in mixed monorepos where not all packages have the same scripts.
Pitfall Guide
1. The Windows Shell Trap
Explanation: Using bash-specific syntax like rm -rf, export VAR=val, or & for parallel execution breaks on Windows.
Fix: Replace rm -rf with rimraf, use cross-env for variables, and use npm-run-all for parallel tasks.
2. The Semicolon Trap
Explanation: Chaining commands with ; executes the next command regardless of the previous exit code. This can lead to deploying broken builds.
Fix: Use && for sequential execution to ensure the chain stops on failure, or use npm-run-s.
3. Hook Recursion and Side Effects
Explanation: Creating hooks that call the same script can cause infinite loops. For example, a pretest script that accidentally triggers test again.
Fix: Audit hook dependencies carefully. Ensure hooks only perform setup or validation, not the main action.
4. Global Dependency Drift
Explanation: Relying on globally installed tools in scripts leads to version mismatches between developers and CI.
Fix: Install all CLI tools as devDependencies and invoke them via npm scripts. Remove global tool usage from workflows.
5. Silent Failures in Chains
Explanation: Some tools exit with code 0 even on warnings, causing chains to continue incorrectly.
Fix: Configure tools to fail on warnings (e.g., --fail-on-warnings flags) and verify exit codes in CI.
6. Environment Variable Portability
Explanation: Hardcoding environment variables in scripts or using OS-specific syntax breaks portability.
Fix: Use cross-env for setting variables and .env files for loading them. Never hardcode secrets in scripts.
7. Script Bloat and Complexity
Explanation: Packing complex logic into a single script string makes it unreadable and hard to debug.
Fix: Extract complex logic into Node.js scripts in a scripts/ directory and invoke them from package.json.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Parallel Tasks | npm-run-all --parallel | Native npm integration, cross-platform, no extra config | Low |
| Complex Parallel | concurrently | Advanced features like prefixes and kill-others | Low (extra dep) |
| Env Vars | cross-env | Abstracts OS differences, reliable | Low |
| File Cleanup | rimraf | Cross-platform rm -rf replacement | Low |
| Monorepo Scripts | --workspaces-if-present | Safe execution across packages | None |
| Complex Logic | Node.js scripts in scripts/ | Readability, debugging, version control | Low |
Configuration Template
Copy this template to establish a production-ready script structure. Adjust tool names and paths to match your stack.
{
"name": "my-project",
"version": "1.0.0",
"scripts": {
"dev": "cross-env NODE_ENV=development esbuild src/index.ts --bundle --outfile=dist/app.js --watch",
"build": "npm-run-s prebuild compile postbuild",
"prebuild": "npm-run-s lint typecheck",
"compile": "esbuild src/index.ts --bundle --outfile=dist/app.js --platform=node",
"postbuild": "rimraf dist/*.map",
"test": "vitest run",
"test:watch": "vitest",
"lint": "biome check src/",
"lint:fix": "biome check --write src/",
"typecheck": "tsc --noEmit",
"clean": "rimraf dist build .turbo node_modules/.cache",
"prepare": "husky install",
"validate": "npm-run-all --parallel lint typecheck test"
},
"devDependencies": {
"cross-env": "^7.0.3",
"rimraf": "^5.0.0",
"npm-run-all": "^4.1.5",
"husky": "^8.0.0",
"esbuild": "^0.19.0",
"vitest": "^0.34.0",
"biome": "^1.4.0",
"typescript": "^5.0.0"
}
}
Quick Start Guide
- Initialize Dependencies: Run
npm install --save-dev cross-env rimraf npm-run-all to add cross-platform tools.
- Define Scripts: Add the core scripts to
package.json using the template above, adapting tool names as needed.
- Verify Execution: Run
npm run validate to ensure parallel tasks work, then npm run build to test the lifecycle hooks.
- Commit and Share: Commit
package.json and package-lock.json. New developers can now run npm install and immediately use the standardized scripts.