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.
{
"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"]
}| 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:
// "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));$ 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.