Package.json Entry Points: main, module, and exports
In JavaScript and Node.js package development,
package.json relies on the main,
module, and exports fields to tell runtime
environments and bundlers where to find a library’s code. This article
explains the role of each field, how they handle different module
formats like CommonJS (CJS) and ECMAScript Modules (ESM), their
precedence, and how modern conditional exports provide strict control
over your package’s public API.
The main Field
The main field is the legacy standard for defining the
primary entry point of a package.
- Primary Use Case: CommonJS (CJS) environments,
particularly standard Node.js
require()calls. - Compatibility: Supported by virtually all versions of Node.js, bundlers, and build tools.
- Behavior: When a consumer executes
require('my-package'), the runtime resolves the file path specified inmain.
{
"name": "my-package",
"main": "./dist/index.cjs.js"
}If main is omitted, Node.js defaults to looking for an
index.js file in the package’s root directory.
The module Field
The module field is a community-adopted standard created
by bundler tools such as Webpack, Rollup, and Vite.
- Primary Use Case: Providing an ESM entry point for bundlers to enable tree-shaking and dead-code elimination.
- Compatibility: Recognized by modern bundlers, but ignored natively by Node.js runtimes.
- Behavior: When a build tool processes
import { feature } from 'my-package', it checks themodulefield to import the ESM version rather than the CJS version listed inmain.
{
"name": "my-package",
"main": "./dist/index.cjs.js",
"module": "./dist/index.esm.js"
}The exports Field
Introduced natively in Node.js 12.7.0+, the exports
field is the modern standard for defining package entry points. It
supersedes both main and module in modern
runtimes and modern bundlers.
The exports field introduces two major capabilities:
1. Package Encapsulation
By default, defining an exports field restricts
consumers from accessing internal files that are not explicitly exposed.
Deep imports (e.g., import 'my-package/internal/util.js')
will throw an error unless mapped.
{
"name": "my-package",
"exports": {
".": "./dist/index.js",
"./feature": "./dist/feature.js"
}
}2. Conditional Exports
Conditional exports allow a single package to serve different files based on the environment, module system, or TypeScript definitions:
import: Used when the package is loaded viaimportor dynamicimport().require: Used when the package is loaded viarequire().types: Used by TypeScript to locate type declarations.browser/node: Used to serve browser-specific or Node-specific implementations.default: The fallback entry point if no other condition matches.
{
"name": "my-package",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"default": "./dist/index.cjs"
},
"./subpath": {
"types": "./dist/subpath.d.ts",
"import": "./dist/subpath.mjs",
"require": "./dist/subpath.cjs"
}
}
}Note: Order matters in conditional exports. Conditions listed earlier take precedence over conditions listed later.
Precedence and Resolution Rules
When resolving an import or require statement, tools follow a strict hierarchy:
- Modern Runtimes and Bundlers: Look for the
exportsfield first. Ifexportsis present,mainandmoduleare ignored. - Older Bundlers (ESM awareness): If
exportsis missing, bundlers check formoduleto load ESM code. - Legacy Node.js / CJS Fallback: If neither
exportsnormoduleapplies, tools fall back tomain.
Best Practices for Dual-Package Publishing
To ensure compatibility across modern runtimes, older build pipelines, and TypeScript, combine all three fields:
{
"name": "my-package",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
}
}This structure ensures that modern Node.js and modern bundlers use
exports, older bundlers use module, legacy
Node.js environments resolve main, and TypeScript correctly
resolves type declarations across all configurations.