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.

{
  "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.

{
  "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:

{
  "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:

  1. Modern Runtimes and Bundlers: Look for the exports field first. If exports is present, main and module are ignored.
  2. Older Bundlers (ESM awareness): If exports is missing, bundlers check for module to load ESM code.
  3. Legacy Node.js / CJS Fallback: If neither exports nor module applies, tools fall back to main.

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.