Skip to main content

npm Scripts

What are npm scripts?โ€‹

The "scripts" field in package.json lets you define named commands for your project. When you run npm run <name>, npm spawns a shell and executes the corresponding command โ€” with node_modules/.bin added to the PATH. This means any locally installed CLI tool is available without a global install.

package.json
{
"scripts": {
"greet": "echo Hello from npm scripts"
}
}
npm run greet
# Output: Hello from npm scripts

For a broader overview of npm itself, see npm.

Aliz policy โ€” scripts as the single entry pointโ€‹

danger

All project commands must be defined as npm scripts. No loose shell scripts, Makefiles, or Python scripts for build/dev/test workflows. npm run <name> is the universal interface.

Why this matters:

  • Discoverability โ€” there's one place to look for every command: package.json.
  • CI consistency โ€” CI pipelines call npm run build, not a custom script buried in a folder.
  • Onboarding speed โ€” new team members run npm run dev on day one without hunting for docs.
danger

Cross-platform only. Scripts must work on Windows, macOS, and Linux. Never use bash-specific syntax (rm -rf, cp -r, export VAR=val). Use cross-platform packages instead โ€” see the Cross-platform scripts section.

Common script namesโ€‹

ScriptPurposeTypical command
devStart dev servervite
buildProduction buildtsc && vite build
startRun production server / previewnode server.js
testRun unit testsvitest run
test:watchRun tests in watch modevitest
lintRun lintereslint .
formatRun formatterprettier --write .
typecheckType-check without emittsc --noEmit
cleanRemove build artifactsrimraf dist coverage
previewPreview production build locallyvite preview

The scripts start, test, stop, and restart have built-in shorthands โ€” you can run them without the run keyword:

npm test        # same as npm run test
npm start # same as npm run start
tip

Use colon-namespacing to group related scripts: test:watch, test:e2e, lint:fix. This keeps the scripts section scannable and makes intent clear.

Lifecycle scriptsโ€‹

Pre and post hooksโ€‹

npm automatically runs pre<name> before and post<name> after any script:

package.json
{
"scripts": {
"prebuild": "rimraf dist",
"build": "vite build",
"postbuild": "echo Build complete"
}
}

Running npm run build executes: prebuild โ†’ build โ†’ postbuild.

npm lifecycle eventsโ€‹

Some scripts run automatically at specific points in the npm lifecycle:

  • prepare โ€” runs after npm install in development. Use it for Husky setup or build steps that must exist after install.
  • prepublishOnly โ€” runs before npm publish. Use it for final checks or builds before publishing a package.
  • postinstall โ€” runs after install completes. Use sparingly โ€” it slows down installs for consumers of your package.

Passing argumentsโ€‹

Use the -- separator to pass arguments through to the underlying command. Everything after -- is appended to the script command.

npm run test -- --coverage
# Executes: vitest run --coverage

npm run lint -- --fix
# Executes: eslint . --fix

Composing scriptsโ€‹

Running in seriesโ€‹

The && operator runs commands sequentially. It works cross-platform in npm scripts because npm spawns a shell that supports it on both Windows and Unix:

package.json
{
"scripts": {
"build": "tsc --noEmit && vite build"
}
}

Running in parallelโ€‹

For parallel execution, use a dedicated package:

  • npm-run-all2 โ€” lightweight, designed for npm scripts. run-p runs in parallel, run-s runs in series:

    package.json
    {
    "scripts": {
    "validate": "run-p lint typecheck test"
    }
    }
  • concurrently โ€” better for long-running processes (like dev servers) with labeled, color-coded output:

    package.json
    {
    "scripts": {
    "dev": "concurrently \"vite\" \"tsc --watch\""
    }
    }

Keeping scripts shortโ€‹

If a script gets long, break it into sub-scripts and compose them:

package.json
{
"scripts": {
"build": "run-s clean typecheck build:vite",
"build:vite": "vite build",
"typecheck": "tsc --noEmit",
"clean": "rimraf dist"
}
}
tip

Prefer composing named sub-scripts over long inline commands. Each script should do one thing and have a clear name.

Cross-platform scriptsโ€‹

Different team members use different operating systems, and CI environments may differ from local machines. Scripts must work everywhere.

Instead of (bash)Use (cross-platform)Package
rm -rf distrimraf distrimraf
cp -r src/ dest/shx cp -r src/ dest/shx
mkdir -p dirshx mkdir -p dirshx
export NODE_ENV=prod && cmdcross-env NODE_ENV=prod cmdcross-env
source .env && cmddotenv -- cmddotenv-cli
danger

Platform-specific commands in npm scripts will be flagged in code review. Always use the cross-platform alternative.

Environment variablesโ€‹

Built-in npm variablesโ€‹

npm exposes metadata about the current package as environment variables prefixed with npm_:

  • npm_package_name โ€” the package name from package.json
  • npm_package_version โ€” the package version
  • npm_lifecycle_event โ€” the name of the currently running script

Setting custom variablesโ€‹

Use cross-env for inline variables and dotenv-cli for .env files:

package.json
{
"scripts": {
"build:staging": "cross-env VITE_API_URL=https://staging.example.com vite build",
"dev:local": "dotenv -e .env.local -- vite"
}
}

Workspacesโ€‹

In a monorepo with npm workspaces, you can target scripts at specific packages or run them across all workspaces. See npm Workspaces for declaration-order caveats, the full -w targeting semantics, and --include-workspace-root.

Run a script in a specific workspace:

npm run build -w packages/ui

Run a script across all workspaces:

npm run test --workspaces
# or shorthand
npm run test -ws

Use --if-present to skip workspaces that don't define the script:

npm run lint -ws --if-present

Full exampleโ€‹

A well-organized scripts section for a typical Aliz project:

package.json
{
"scripts": {
"dev": "vite",
"build": "run-s clean typecheck build:vite",
"build:vite": "vite build",
"preview": "vite preview",
"test": "vitest run",
"test:watch": "vitest",
"test:e2e": "playwright test",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"typecheck": "tsc --noEmit",
"clean": "rimraf dist coverage",
"validate": "run-p lint typecheck test",
"prepare": "husky"
}
}

This structure follows all Aliz conventions: colon-namespaced groups, cross-platform commands, composed sub-scripts, and a validate script for pre-push checks.

Resourcesโ€‹

  • npm โ€” overview of npm as a package manager
  • Node.js โ€” JavaScript runtime fundamentals
  • npm scripts documentation โ€” official reference
  • npm-run-all2 โ€” run multiple scripts in series or parallel
  • cross-env โ€” set environment variables cross-platform
  • rimraf โ€” cross-platform rm -rf
  • shx โ€” portable shell commands
  • concurrently โ€” run commands concurrently with labeled output
  • dotenv-cli โ€” load .env files for any command