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.


Which tool reads which package.json field Four stages showing that main and module are read by legacy bundlers, exports is read by Node.js and modern bundlers, types is read by TypeScript, and files plus sideEffects are read by npm pack and tree-shaking bundlers respectively. Distribution field → resolver map main / module legacy bundlers exports Node.js, webpack 5+ types TypeScript compiler files / sideEffects npm pack, tree-shaking

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 publish succeeds, but the installed package is missing dist/ entirely and only source files are usable.

Root cause: No files field 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 files array listing your build output directory, and verify with npm pack --dry-run as 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": false was 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 sideEffects an 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.ts files exist in the tarball.

Root cause: The top-level types field points at a path that does not exist in the published files allowlist, or the exports map’s types condition is missing or ordered after import/require.

Fix: Confirm the declaration path is included by files, and place "types" first in every exports branch — never after import or require.

HAZARD PREVENTION

Symptom: npm install works but require() throws Cannot find module './package.json' when a tool tries to read package metadata at runtime.

Root cause: A strict exports map was added without an explicit "./package.json": "./package.json" entry, and Node.js now refuses any path not listed in exports.

Fix: Always add the ./package.json self-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.

Which distribution fields to set A top-to-bottom decision sequence: start from your build output, decide whether legacy tooling must be supported, add exports either way, then restrict files and declare sideEffects before publishing. Build ESM + CJS output Support tooling pre-Node 12.7? Yes → keep main/module/types Add exports + files + sideEffects Validate with publint, then publish

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



Back to Module System Fundamentals & Dual-Package Resolution