package.json and Scripts

package.json Anatomy and Scripts

package.json is the manifest of a JavaScript project: its name, the packages it needs, the Node.js 2,131 versions it supports and the commands that develop, test and build it. npm 2,036 init -y creates a minimal one; the file below is typical of a small front-end app after a few installs.

A front-end application's package.jsonJSON
{
  "name": "weather-dashboard",
  "version": "1.2.0",
  "private": true,
  "type": "module",
  "engines": { "node": ">=22.12" },
  "packageManager": "npm@12.0.2",
  "scripts": {
    "dev": "vite",
    "prebuild": "node scripts/clean.js",
    "build": "vite build",
    "test": "vitest run"
  },
  "dependencies": { "date-fns": "^4.4.0" },
  "devDependencies": {
    "vite": "^8.3.0",
    "vitest": "~5.0.0"
  },
  "browserslist": ["baseline widely available"]
}
Key package.json fields for an application
Field Meaning
private: true Blocks accidental publishing to npm
type: "module" .js files use import, not require
engines, packageManager Expected Node.js and package manager versions
dependencies Code imported at run time and shipped to users
devDependencies Build, test and lint tools
browserslist Target browsers for build tools (Standards Bodies)
Version ranges

Dependency versions follow semantic versioning, MAJOR.MINOR.PATCH, where a major bump signals breaking changes. A caret allows newer minor and patch releases (^4.4.0 accepts 4.9.0 but not 5.0.0), a tilde allows only patches (~5.0.0 accepts 5.0.3 but not 5.1.0), and a bare version pins exactly. While the major version is 0, a caret behaves like a tilde: ^0.28.2 stops before 0.29.0. The range only matters when the lockfile is created or updated. npm install keeps the versions in package-lock.json if they still satisfy the ranges; npm update moves within ranges; npm outdated lists what is newer.

Scripts

Each entry in scripts runs with node_modules/.bin added to PATH, so vite finds the local copy without a global install. npm runs pre<name> and post<name> scripts automatically, passes anything after -- to the command, and exposes fields as npm_package_* environment variables. To watch all three at work, point build at a small script and give prebuild and postbuild scripts that each log one line:

scripts/build.js, run by the build script node scripts/build.jsJavaScript
// "prebuild": "node scripts/clean.js"  -> console.log('cleaning dist')
// "postbuild": "node scripts/done.js"  -> console.log('build finished')
const { npm_package_name: name, npm_package_version: version } = process.env;
console.log(`Building ${name} v${version}`);
console.log('Extra arguments:', process.argv.slice(2));
Output
$ npm run build -- --mode staging
npm notice run weather-dashboard@1.2.0 prebuild
npm notice run node scripts/clean.js
cleaning dist
npm notice run weather-dashboard@1.2.0 build
npm notice run node scripts/build.js --mode staging
Building weather-dashboard v1.2.0
Extra arguments: [ '--mode', 'staging' ]
npm notice run weather-dashboard@1.2.0 postbuild
npm notice run node scripts/done.js
build finished

Never edit package-lock.json by hand, and do not delete it to "fix" an install error: that silently upgrades every transitive dependency at once.