package.json answers three audiences at once. npm 2,036 reads name, version and the dependency objects; Node reads type, exports and imports when it resolves a specifier (How Node Resolves a Specifier); bundlers and publishers read files, sideEffects and publishConfig. A field one audience ignores another treats as law, which is why a manifest that "works locally" can still install broken:
{
"name": "@webcoding/slugify-lite",
"version": "1.3.0",
"license": "MIT",
"type": "module",
"engines": { "node": ">=20.19" },
"exports": { ".": "./dist/index.js", "./package.json": "./package.json" },
"types": "./dist/index.d.ts",
"files": ["dist", "README.md"],
"sideEffects": false,
"repository": "github:example-org/slugify-lite",
"scripts": { "build": "node scripts/build.js", "test": "node --test" },
"publishConfig": { "access": "public", "provenance": true }
}name must be at most 214 characters, URL-safe and lowercase; a scope (@webcoding/) costs nothing and gives you a namespace. license takes an SPDX identifier — "MIT", not "MIT License" — because tooling parses it. engines is advisory unless the installing user sets engine-strict, so treat it as documentation.
files is an allowlist: everything outside it is left out of the tarball, far safer than an .npmignore blocklist that silently ships the sources or secrets you forget to add to it. sideEffects: false promises bundlers that importing your module and not using the result changes nothing, which is what makes tree shaking legal — and drops live code if the promise is false.