package.json Fields for Distribution
The package.json fields that control how npm packages distribute: main, module, exports, types, files, sideEffects, and their precedence across tools.
Every field in package.json that touches distribution — main, module, exports, types, files, and sideEffects — is read by a different resolver at a different point in the pipeline, and no single tool reads all of them the same way. This guide is for library authors who need one authoritative map of which field controls what, in what order tools consult them, and how to configure each one so Node.js, webpack, Vite, and TypeScript all agree on what your package looks like.
Quick-Reference: Key Terms
| Term | Definition | Reference |
|---|---|---|
main |
Legacy CommonJS entry point, read by Node.js when no exports map exists and by bundlers that predate conditional exports. |
Field Precedence |
module |
Non-standard ESM entry point read only by bundlers (webpack, Rollup, Vite), never by Node.js directly. | Field Precedence |
exports |
Structured, conditional map that overrides main/module in Node 12.7+ and modern bundlers. |
Mastering the exports Field |
types |
Top-level declaration file path used by TypeScript’s legacy node resolution mode. |
Field Precedence |
files |
Whitelist of paths included in the published tarball; works alongside .npmignore. |
files Field & npm pack |
sideEffects |
Declares whether a package’s modules can be safely dropped by tree-shaking bundlers when unused. | Implementing the sideEffects Flag |
Core Concepts
The canonical fully-annotated package.json
The snippet below shows every distribution-relevant field in one place, annotated with which tool consults it.
{
"name": "@acme/toolkit",
"version": "3.1.0",
"type": "module",
"main": "./dist/cjs/index.cjs",
"module": "./dist/esm/index.mjs",
"types": "./dist/cjs/index.d.ts",
"exports": {
".": {
"types": "./dist/esm/index.d.ts",
"import": "./dist/esm/index.mjs",
"require": "./dist/cjs/index.cjs",
"default": "./dist/esm/index.mjs"
},
"./package.json": "./package.json"
},
"files": [
"dist",
"!dist/**/*.test.js"
],
"sideEffects": false
}
Each field here serves a distinct audience: main and module cover tools that never learned to read exports; the exports map is the modern, authoritative source for Node.js and current bundlers; types is a fallback for TypeScript’s older resolution mode; files controls the tarball contents independently of any resolution logic; and sideEffects is read exclusively by tree-shaking bundlers, never by Node.js.
Why redundant fields still matter
It would be simpler to declare only exports and drop main, module, and the top-level types. In practice, some consumers still run tooling old enough to ignore exports entirely — Node.js below 12.7, webpack 4, or TypeScript with moduleResolution: "node". Keeping the legacy fields pointed at equivalent artifacts (not stale ones) means those consumers degrade gracefully instead of failing outright. The main, module, and exports field precedence guide covers exactly which field wins when more than one is present.
{
"main": "./dist/cjs/index.cjs",
"module": "./dist/esm/index.mjs",
"types": "./dist/cjs/index.d.ts"
}
If main points at an outdated build while exports points at a current one, consumers on old tooling silently receive stale code — a common source of “it works for me but not for this user” bug reports.
Hazard and Failure-Mode Inventory
HAZARD PREVENTION
Symptom:
npm publishsucceeds, but the installed package is missingdist/entirely and only source files are usable.Root cause: No
filesfield and no.npmignore, so npm defaults to publishing everything not covered by its built-in ignore list — which does not know your build output directory name.Fix: Add an explicit
filesarray listing your build output directory, and verify withnpm pack --dry-runas described in the files field guide.
HAZARD PREVENTION
Symptom: Consumers using webpack report duplicated CSS or lost side effects (a polyfill silently vanishes) after upgrading your package.
Root cause:
"sideEffects": falsewas set on a package that still imports polyfills or CSS at module scope for their side effects, so the bundler drops those imports as unused.Fix: Either keep
sideEffectsan array naming the exact files with side effects, or refactor away from side-effecting module-scope code. See Implementing the sideEffects Flag Correctly.
HAZARD PREVENTION
Symptom: TypeScript reports
Could not find a declaration file for module '@acme/toolkit'even though.d.tsfiles exist in the tarball.Root cause: The top-level
typesfield points at a path that does not exist in the publishedfilesallowlist, or theexportsmap’stypescondition is missing or ordered afterimport/require.Fix: Confirm the declaration path is included by
files, and place"types"first in everyexportsbranch — never afterimportorrequire.
HAZARD PREVENTION
Symptom:
npm installworks butrequire()throwsCannot find module './package.json'when a tool tries to read package metadata at runtime.Root cause: A strict
exportsmap was added without an explicit"./package.json": "./package.json"entry, and Node.js now refuses any path not listed inexports.Fix: Always add the
./package.jsonself-reference alongside your other export entries.
Decision Guide
Choosing which fields to set depends on which consumers you must support. If you only target Node.js 14+ and modern bundlers, exports alone (plus files and sideEffects) is sufficient. If you support older tooling or a wide npm audience, keep main, module, and top-level types as compatible fallbacks pointed at equivalent builds.
Step-by-Step Implementation
Step 1 — Set the legacy entry fields
{
"main": "./dist/cjs/index.cjs",
"module": "./dist/esm/index.mjs"
}
Expected result: any bundler or Node.js version that predates exports still resolves a working entry point.
Step 2 — Add exports
{
"exports": {
".": {
"types": "./dist/esm/index.d.ts",
"import": "./dist/esm/index.mjs",
"require": "./dist/cjs/index.cjs",
"default": "./dist/esm/index.mjs"
}
}
}
Expected result: Node 12.7+ and modern bundlers ignore main/module and resolve through this map instead.
Step 3 — Declare types
{
"types": "./dist/cjs/index.d.ts"
}
Expected result: TypeScript running moduleResolution: "node" (no exports support) still finds a declaration file.
Step 4 — Restrict files
{
"files": ["dist"]
}
Expected result: npm pack --dry-run lists only files under dist/ plus npm’s always-included files (package.json, README, LICENSE).
Step 5 — Mark sideEffects
{
"sideEffects": false
}
Expected result: webpack and Rollup drop unused exports across your package’s module graph during tree-shaking.
Tooling Validation
# Confirm the tarball contains only what files/exports expect
npm pack --dry-run
# Static analysis of exports paths, condition ordering, and types resolution
npx publint --strict
# Confirm TypeScript resolves types under every consumer mode
npx attw --pack .
Sample publint pass output:
✓ exports["."]["types"] resolves to ./dist/esm/index.d.ts
✓ "files" allowlist matches build output directory
✓ No issues found
Compatibility Matrix
| Field | Node.js 12.7+ | webpack 5+ | Vite 3+ | TypeScript 4.7+ |
|---|---|---|---|---|
main |
Fallback only | Fallback only | Fallback only | Ignored if exports present |
module |
Never read | Fallback (resolve.mainFields) |
Fallback | Never read |
exports |
Authoritative | Authoritative | Authoritative | Authoritative (node16/bundler) |
types |
N/A | N/A | N/A | Fallback for legacy node mode |
files |
N/A (npm-only) | N/A | N/A | N/A |
sideEffects |
Ignored | Authoritative | Authoritative | Ignored |
Guides in This Section
- main, module, and exports Field Precedence — which field wins when several are present, across Node.js, webpack, Vite, and TypeScript.
- The files Field and Controlling npm pack — restricting the published tarball with
files,.npmignore, andnpm pack --dry-run.
Related
- Mastering the package.json Exports Field — deep dive on conditional exports, sub-paths, and environment-specific routing.
- Implementing the sideEffects Flag Correctly — full guidance on declaring
sideEffectsaccurately for tree-shaking bundlers. - Navigating the Dual-Package Hazard — why conflicting
main/exportsentries can duplicate singleton state. - Understanding ESM vs CJS Module Formats — the format distinction that every field on this page ultimately routes between.
Back to Module System Fundamentals & Dual-Package Resolution