package.json is the manifest npm, bundlers, and Node read to understand your
package. A single misplaced field can break imports for every consumer or
silently publish your entire working tree. This is a searchable reference of the
fields you actually touch, with what each one does and where it bites.
How it works
Each field has a defined value type (string, object, array, or boolean) and is read by a specific tool: npm for install and publish behavior, Node for module resolution, and bundlers for entry-point selection. The reference groups fields by purpose:
- Identity —
name,version,private,license. - Entry points —
main,module,types,exports,bin,type. - Dependencies —
dependencies,devDependencies,peerDependencies,optionalDependencies. - Publishing —
files,publishConfig,engines,os,cpu. - Tooling —
scripts,workspaces.
The fields most likely to cause problems in practice
exports encapsulation
When exports is present, it completely overrides main and makes every file in
the package unreachable except those explicitly listed. This breaks packages whose
consumers import from deep paths like import { helper } from 'pkg/src/utils'.
When adding exports to an existing package, audit your consumers for deep imports
before publishing — many will break silently if Node cannot find the subpath in
your map.
The conditional exports map allows different resolution for different module
systems and environments:
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
}
}
type: "module" and file extensions
Setting "type": "module" treats all .js files as ESM. Files that need to stay
CommonJS must use .cjs. The reverse is also true: with "type": "commonjs"
(the default), ESM files need .mjs. Mixing types without correct extensions
causes “require() of ES Module” or “Cannot use import statement” errors that are
confusing to diagnose because they depend on which consumers import the package.
peerDependencies vs. dependencies
If your package requires React, listing it as a dependency installs a second
copy of React when the consumer already has it — leading to “invalid hook call”
errors and similar multiple-instance problems. List it as a peerDependency
instead, specifying a compatible version range. The consumer’s own React
installation will be used.
files and the publish allowlist
Without a files field, npm publishes everything not in .npmignore or
.gitignore. This frequently publishes test directories, build scripts, local
config, and environment files. Adding files with an explicit allowlist (typically
your dist folder plus a readme and license) keeps the tarball small and avoids
accidentally shipping internal tooling.
"files": ["dist", "README.md", "LICENSE"]
engines version constraints
engines is advisory by default — npm warns but does not block installation on
mismatched Node versions. To make it a hard block, set engine-strict = true
in .npmrc. This is useful for packages that use Node APIs not available in older
versions, but be conservative with the minimum version or you will block otherwise
compatible users.
Dependency types at a glance
| Field | When installed | Use for |
|---|---|---|
dependencies | Always | Runtime requirements |
devDependencies | Local + CI only | Build tools, test frameworks |
peerDependencies | Declared but not installed | Shared singletons (React, Vue, etc.) |
optionalDependencies | Installed if possible | Binaries with OS-specific builds |
Tips and gotchas
The modern resolution order is exports first (if present, it overrides
main), then main for legacy. Always ship a types or typesVersions
mapping so TypeScript consumers get types. Remember that an exports map
encapsulates the package — anything not listed becomes unreachable, which
catches teams that relied on deep imports. For dual ESM/CJS packages, pair
type with conditional exports so the right file loads under import versus
require.