package.json exports
Explicitly declares a package's public entry points — anything not listed isn't importable by consumers, making internal refactoring safe.
"exports": { ".": "./dist/index.js", "./utils": "./dist/utils.js" }
Purpose
The exports field in package.json explicitly declares a package’s public entry points. Anything
not listed there isn’t importable by consumers at all — the mechanism that makes a package’s actual
public surface precise and enforceable, rather than “everything in the file tree.”
Syntax
{
"name": "my-lib",
"exports": {
".": "./dist/index.js",
"./utils": "./dist/utils.js"
}
}
import { thing } from 'my-lib'; // resolves via the "." entry
import { helper } from 'my-lib/utils'; // resolves via the "./utils" entry
import { internal } from 'my-lib/dist/internal.js'; // fails — not listed in exports
Behavior notes
- Before
exportsexisted, every file in a published package was importable by its path — making literally the entire file tree part of the implicit public contract, and any internal reorganization a potential breaking change. exportscan map different entry points for different conditions ("import"vs."require", recall CommonJS versus ES Modules) — letting one package ship both module formats correctly.- Combined with semantic versioning, a well-defined
exportsfield is what lets internal files be freely refactored, renamed, or removed without a version bump — since none of that is part of the actual public contract.
Common mistakes
- Publishing a package with no
exportsfield (or an overly permissive one), leaving the entire internal file structure as an unintentional part of the public API. - Restructuring internal files without updating
exports, breaking a path some consumers had come to rely on even though it was never meant to be public.
Runtime notes
Supported by Node’s module resolution (12.7+) and by modern bundlers; not meaningful in a browser
context outside of build tooling that reads package.json during a build step.