| |

npm 15 ๐ŸŒ Structuring a Package for Publishing (main, module, types, exports)

โฑ๏ธ Estimated read: 14 min | |

A published package is consumed by many different resolvers: Node.js in CommonJS mode, Node.js in ESM mode, TypeScript with legacy module resolution, TypeScript with modern bundler or node16 resolution, and bundlers like Vite and Webpack. Each resolver looks at package.json differently. The four fields that determine whether a package works across all of them are main, module, types, and exports. Getting them right means every consumer finds the correct file. Getting them wrong means some consumers see types while others do not, some load the ESM build while others load CJS, and the failure modes are diagnosed through tsc --traceResolution output that is difficult to read even for package authors .

This chapter covers each field, the precedence rules between them, and the exports map that modern packages should use as the primary entry-point definition. It examines the condition ordering rules that determine whether TypeScript finds declarations, the dual-format pattern for packages that ship both ESM and CommonJS, and the validation tool that simulates every consumer scenario before publishing.

Key point: exports takes precedence over main and module in modern resolvers. Once exports is defined, unlisted subpaths become inaccessible. The types condition must be the first key inside each exports condition block, or TypeScript will read the JavaScript file as types and fail to find declarations .


Why these fields exist

The resolver fragmentation problem. Node.js historically used main to locate a package’s entry point. Bundlers later adopted module as a convention for the ESM build. TypeScript introduced types to locate declaration files. None of these fields was designed together, and none of them knows about subpath exports or conditional resolution. A package that only sets main and types works under legacy node module resolution and almost nowhere else. Modern consumers using moduleResolution: "bundler" or "node16" report “Cannot find module or its corresponding type declarations” .

The encapsulation problem. Without exports, every file in a published package is importable. A consumer can reach into my-package/lib/internal/util.js and depend on an implementation detail that the author never intended to be public. When the author refactors, the consumer breaks. The exports field exists to define the public API surface explicitly: only listed subpaths are accessible, and everything else throws ERR_PACKAGE_PATH_NOT_EXPORTED . This encapsulation is a feature, not a restriction.

The dual-format problem. A package that ships only ESM cannot be require()d by a CommonJS consumer. A package that ships only CJS cannot be imported by an ESM consumer without interop shims. The exports field solves this with conditional resolution: the import condition points to the ESM build, the require condition points to the CJS build, and both are listed in the same map . The trade-off is the “dual package hazard”โ€”two module instances that do not share stateโ€”but for most libraries the hazard is manageable, and the alternative is excluding half the ecosystem.

The types resolution problem. TypeScript resolves declaration files through exports when moduleResolution is set to bundler, node16, or nodenext. If the types condition is not first in the condition block, TypeScript evaluates import or require first, resolves to a .js file, and reports that it cannot find declarations . The ordering is not a style preference; it is a resolution requirement.


a. The Legacy Fields: main, module, types

main is the oldest entry-point field. It defines the file loaded when the package is require()d or imported by a resolver that does not understand exports. It is supported by every version of Node.js and by tools like jsDelivr that read main for CDN publishing . For a CJS package, main points to the CJS entry file. For an ESM package, it points to the ESM entry.

{
  "main": "./dist/index.cjs"
}

module is not an official Node.js field. It is a convention adopted by bundlersโ€”Rollup, Webpack, esbuild, and Viteโ€”to locate the ESM build of a package . Node.js does not read module. The field exists because bundlers can tree-shake ESM more effectively than CJS, so they prefer the ESM build when it is available. A package that ships both formats sets main to the CJS file and module to the ESM file.

{
  "main": "./dist/index.cjs",
  "module": "./dist/index.js"
}

types points to the declaration file that TypeScript reads when the package is imported. For a package with a single entry point, types is a single path. For a package with subpath exports, types alone is insufficient because it does not map subpaths to their declarations. That mapping is handled by the types condition inside exports .

{
  "types": "./dist/index.d.ts"
}

The limitation of these three fields is that they describe a single entry point. A package that exposes my-package and my-package/utils needs a mechanism that maps each subpath to its implementation and its declarations. That mechanism is exports.


b. The exports Map: Conditional Resolution and Encapsulation

exports is a map from import specifiers to files, with optional conditions that select different files based on the environment performing the resolution. The simplest form is a string that replaces main:

{
  "exports": "./dist/index.js"
}

The conditional form selects different files for import and require. This is the pattern for dual-format packages:

{
  "type": "module",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    }
  }
}

The types condition must be the first key in the object. Resolvers walk the condition map top-down and stop at the first match. If import appears before types, TypeScript matches import, resolves to the .js file, and fails to find declarations . The correct order is always types first, then import, then require, then default.

Subpath exports extend the map to additional import specifiers. A package that exposes a utils module defines it explicitly:

{
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    },
    "./utils": {
      "types": "./dist/utils.d.ts",
      "import": "./dist/utils.js",
      "require": "./dist/utils.cjs"
    },
    "./package.json": "./package.json"
  }
}

The ./package.json entry is not decorative. Toolingโ€”Vite, Webpack, monorepo linkers, and type-version detectorsโ€”reads package.json to discover package metadata. Without an explicit export for it, those tools error under moduleResolution: "bundler" . Exposing it is the standard practice for any package that defines exports.

Wildcard patterns handle groups of subpaths without listing each one:

{
  "exports": {
    "./features/*": "./dist/features/*.js"
  }
}

A consumer importing my-package/features/auth resolves to ./dist/features/auth.js. The pattern exports every file in the features directory, which is appropriate when the directory is intentionally public. For directories that are not public, omit the wildcard and list only the specific subpaths that are part of the API .


c. Dual-Format Packages and Validation

A dual-format package ships both ESM and CJS builds, with exports mapping each condition to the appropriate file. The package’s type field determines how .js files are interpreted: "module" means .js is ESM, and CJS files use the .cjs extension. The reverse is also possibleโ€”"commonjs" as the type with .mjs for ESMโ€”but the ESM-first convention is more common in modern packages .

{
  "name": "my-lib",
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    },
    "./package.json": "./package.json"
  },
  "files": ["dist"],
  "engines": { "node": ">=18" }
}

The main and module fields are retained for legacy resolvers that do not read exports. The exports map is the authoritative definition for modern resolvers. The files field is an allow-list of what ships in the tarball; dist is included, source files are not .

Validation before publishing is the step that catches resolver failures. The @arethetypeswrong/cli tool simulates every consumer scenarioโ€”Node16 ESM, Node16 CJS, bundler resolution, legacy node resolutionโ€”and reports which ones fail. A package that passes this check resolves correctly for every consumer type .

# Pack the package and check type resolution for every consumer scenario
npx @arethetypeswrong/cli --pack .

The npm pack --dry-run command complements this by showing exactly which files will ship. A dist directory that is missing from the tarball produces a package that installs but fails to import. Reviewing the dry-run output before every publish catches the packaging errors that resolver checks cannot .


Complete Example Session

// ============================================
// PART 1: MINIMAL LEGACY CONFIGURATION
// ============================================
// package.json โ€” works only under legacy node resolution
{
  "name": "my-lib",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts"
}


// ============================================
// PART 2: ADD MODULE FOR BUNDLERS
// ============================================
// Bundlers prefer the ESM build for tree-shaking
{
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts"
}


// ============================================
// PART 3: ADD EXPORTS FOR MODERN RESOLVERS
// ============================================
// exports takes precedence over main and module
{
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    }
  }
}


// ============================================
// PART 4: EXPOSE PACKAGE.JSON FOR TOOLING
// ============================================
// Vite, Webpack, and type-version detectors read it
{
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    },
    "./package.json": "./package.json"
  }
}


// ============================================
// PART 5: ADD A SUBPATH EXPORT
// ============================================
// Exposes my-lib/utils as a separate entry point
{
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    },
    "./utils": {
      "types": "./dist/utils.d.ts",
      "import": "./dist/utils.js",
      "require": "./dist/utils.cjs"
    },
    "./package.json": "./package.json"
  }
}


// ============================================
// PART 6: WILDCARD SUBPATH EXPORT
// ============================================
// Exposes every file in dist/features/
{
  "exports": {
    "./features/*": {
      "types": "./dist/features/*.d.ts",
      "import": "./dist/features/*.js",
      "require": "./dist/features/*.cjs"
    }
  }
}


// ============================================
// PART 7: CONTROL WHAT SHIPS
// ============================================
// files is an allow-list; .env and tests are excluded
{
  "files": ["dist"],
  "engines": { "node": ">=18" }
}


// ============================================
// PART 8: BUILD BEFORE PUBLISH
// ============================================
// prepublishOnly runs only on npm publish
{
  "scripts": {
    "build": "tsup src/index.ts --format cjs,esm --dts --clean",
    "prepublishOnly": "npm run build"
  }
}


// ============================================
// PART 9: VALIDATE BEFORE PUBLISHING
// ============================================
// npm pack --dry-run shows the tarball contents
// @arethetypeswrong/cli checks resolver compatibility
// npx @arethetypeswrong/cli --pack .


// ============================================
// PART 10: COMPLETE CONFIGURATION
// ============================================
{
  "name": "my-lib",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    },
    "./utils": {
      "types": "./dist/utils.d.ts",
      "import": "./dist/utils.js",
      "require": "./dist/utils.cjs"
    },
    "./package.json": "./package.json"
  },
  "files": ["dist"],
  "engines": { "node": ">=18" },
  "scripts": {
    "build": "tsup src/index.ts --format cjs,esm --dts --clean",
    "prepublishOnly": "npm run build"
  }
}

The ten parts trace the progression from a legacy-only configuration to a complete dual-format package with subpath exports and validation. The critical step is Part 3, where exports is introduced: from that point on, the map defines the public API, and unlisted paths are inaccessible.


Quick Reference

Field Precedence

ResolverReadsPrecedence
Node.js (modern)exports firstexports > main
Bundlers (Vite, Webpack)exports, then module, then mainexports > module > main
TypeScript (bundler/node16)exports with types conditionexports > types
TypeScript (legacy node)types, then maintypes > main

exports Condition Order

OrderConditionMatches When
1typesTypeScript resolving declarations
2importESM import or import()
3requireCommonJS require()
4defaultFallback, always matches last

Common File Layouts

FormatType FieldEntry ExtensionDeclaration
CJS onlycommonjs.js.d.ts
ESM onlymodule.js.d.ts
Dual (ESM-first)module.js (ESM), .cjs (CJS).d.ts

Best Practices

โœ… Do This:

// Put types first in every exports condition block
"exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } } // โœ…
// Expose package.json for tooling
"./package.json": "./package.json"                                      // โœ…
// Use files as an allow-list
"files": ["dist"]                                                       // โœ…
// Keep main and module for legacy resolvers
"main": "./dist/index.cjs", "module": "./dist/index.js"                 // โœ…
# Validate resolver compatibility before publish
npx @arethetypeswrong/cli --pack .                                      # โœ…

โŒ Don’t Do This:

// Put import before types (TS skips declarations)
"exports": { ".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" } } // โŒ
// Define exports without package.json subpath
"exports": { ".": "./dist/index.js" }  // Tooling errors under bundler  // โŒ
// Ship without files allow-list
// Tests, .env, and source files end up in tarball                      // โŒ
// Rely only on main and types
// Modern resolvers find nothing                                        // โŒ

Common Pitfalls

PitfallWhy It HappensFix
TypeScript cannot find declarationstypes not first in exports blockMove types to first position
Subpath import failsNo exports entry for that subpathAdd explicit subpath export
Tooling errors reading package.jsonNo ./package.json exportAdd "./package.json": "./package.json"
CJS consumer cannot require packageNo require conditionAdd require pointing to .cjs file
Files missing from published tarballfiles excludes distAdd dist to files array
Legacy resolver failsNo main fieldKeep main alongside exports

Real-World Examples

1. Single ESM Entry

"exports": { ".": "./dist/index.js" }

2. Dual Format with Types

"exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs" } }

3. Subpath Export

"./utils": { "types": "./dist/utils.d.ts", "import": "./dist/utils.js" }

4. Wildcard Features

"./features/*": "./dist/features/*.js"

5. Expose package.json

"./package.json": "./package.json"

6. Files Allow-List

"files": ["dist"]

7. Engine Requirement

"engines": { "node": ">=18" }

8. Build Before Publish

"prepublishOnly": "npm run build"

9. Validate Resolver Compatibility

npx @arethetypeswrong/cli --pack .

10. Dry-Run Pack

npm pack --dry-run

Visual

Resolver Precedence

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  package.json                                                โ”‚
โ”‚                                                              โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                        โ”‚
โ”‚  โ”‚ exports         โ”‚  Modern Node, bundlers, TS             โ”‚
โ”‚  โ”‚ (highest)       โ”‚  Conditional resolution                โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                        โ”‚
โ”‚           โ”‚ NOT PRESENT                                     โ”‚
โ”‚           โ–ผ                                                  โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                        โ”‚
โ”‚  โ”‚ module          โ”‚  Bundlers only                         โ”‚
โ”‚  โ”‚                 โ”‚  ESM build for tree-shaking            โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                        โ”‚
โ”‚           โ”‚ NOT PRESENT                                     โ”‚
โ”‚           โ–ผ                                                  โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                        โ”‚
โ”‚  โ”‚ main            โ”‚  Legacy Node, CDN tools                โ”‚
โ”‚  โ”‚                 โ”‚  CJS or ESM entry                      โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                        โ”‚
โ”‚                                                              โ”‚
โ”‚  types is read by TypeScript in parallel.                    โ”‚
โ”‚  When exports exists, the types condition controls           โ”‚
โ”‚  declaration resolution for modern TS.                       โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

exports Condition Ordering

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  CORRECT ORDER                    WRONG ORDER                โ”‚
โ”‚                                                              โ”‚
โ”‚  {                                {                          โ”‚
โ”‚    "types": "./index.d.ts",         "import": "./index.js",  โ”‚
โ”‚    "import": "./index.js",          "types": "./index.d.ts"  โ”‚
โ”‚    "require": "./index.cjs"       }                          โ”‚
โ”‚  }                                                           โ”‚
โ”‚                                                              โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”              โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”       โ”‚
โ”‚  โ”‚ TS matches      โ”‚              โ”‚ TS matches      โ”‚       โ”‚
โ”‚  โ”‚ types first     โ”‚              โ”‚ import first    โ”‚       โ”‚
โ”‚  โ”‚ Finds .d.ts     โ”‚              โ”‚ Finds .js       โ”‚       โ”‚
โ”‚  โ”‚                 โ”‚              โ”‚ No declarations โ”‚       โ”‚
โ”‚  โ”‚ SUCCESS         โ”‚              โ”‚ FAILURE         โ”‚       โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜              โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜       โ”‚
โ”‚                                                              โ”‚
โ”‚  Resolvers walk top-down and stop at the first match.        โ”‚
โ”‚  types must always be first.                                 โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Dual Format Exports Map

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  exports: {                                                  โ”‚
โ”‚    ".": {                                                    โ”‚
โ”‚      "types": "./dist/index.d.ts",                           โ”‚
โ”‚      "import": "./dist/index.js",   โ† ESM consumers          โ”‚
โ”‚      "require": "./dist/index.cjs"  โ† CJS consumers          โ”‚
โ”‚    },                                                        โ”‚
โ”‚    "./utils": {                                              โ”‚
โ”‚      "types": "./dist/utils.d.ts",                           โ”‚
โ”‚      "import": "./dist/utils.js",                            โ”‚
โ”‚      "require": "./dist/utils.cjs"                           โ”‚
โ”‚    },                                                        โ”‚
โ”‚    "./package.json": "./package.json"  โ† tooling             โ”‚
โ”‚  }                                                           โ”‚
โ”‚                                                              โ”‚
โ”‚  import 'my-lib'        โ†’ index.js                           โ”‚
โ”‚  require('my-lib')      โ†’ index.cjs                          โ”‚
โ”‚  import 'my-lib/utils'  โ†’ utils.js                           โ”‚
โ”‚  import 'my-lib/internal' โ†’ ERR_PACKAGE_PATH_NOT_EXPORTED    โ”‚
โ”‚                                                              โ”‚
โ”‚  Unlisted paths are inaccessible by design.                  โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Validation Before Publish

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  BEFORE npm publish                                          โ”‚
โ”‚                                                              โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                        โ”‚
โ”‚  โ”‚ npm pack        โ”‚  Shows exact tarball contents          โ”‚
โ”‚  โ”‚ --dry-run       โ”‚  Catches missing dist/ files           โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                        โ”‚
โ”‚           โ–ผ                                                  โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                        โ”‚
โ”‚  โ”‚ @arethetypes    โ”‚  Simulates every consumer scenario     โ”‚
โ”‚  โ”‚ wrong/cli       โ”‚  Node16 ESM, CJS, bundler, legacy      โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                        โ”‚
โ”‚           โ–ผ                                                  โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                        โ”‚
โ”‚  โ”‚ All resolvers   โ”‚  Package works for every consumer      โ”‚
โ”‚  โ”‚ pass            โ”‚                                        โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                        โ”‚
โ”‚                                                              โ”‚
โ”‚  A package that passes both checks resolves correctly        โ”‚
โ”‚  for Node, bundlers, and TypeScript.                         โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
mainLegacy entry point; CJS or ESM
moduleBundler convention; ESM build
typesTypeScript declarations for single entry
exportsModern entry-point map; takes precedence
Condition ordertypes โ†’ import โ†’ require โ†’ default
Subpath encapsulationUnlisted paths throw ERR_PACKAGE_PATH_NOT_EXPORTED
./package.json exportRequired for tooling under bundler resolution
files fieldAllow-list of what ships in tarball
Validation@arethetypeswrong/cli --pack .

Key takeaways:

  • exports is the authoritative entry-point definition in modern resolvers. Once it exists, main and module are fallbacks for legacy tools. The map defines the public API; unlisted subpaths are inaccessible by design .
  • types must come first in every exports condition block. Resolvers stop at the first match. If import comes before types, TypeScript resolves to the JavaScript file and fails to find declarations .
  • Expose ./package.json explicitly. Tooling reads it for metadata, and without an export entry, bundler-resolution consumers error before they can import anything .
  • Keep main and module for legacy resolvers. exports covers modern consumers, but older Node versions and some CDN tools still read main. The dual configuration costs nothing and prevents a class of compatibility failures .
  • Validate with @arethetypeswrong/cli before publishing. The tool simulates every consumer scenarioโ€”Node16 ESM, Node16 CJS, bundler resolution, legacy nodeโ€”and reports which ones fail. A package that passes resolves correctly for every consumer type .
  • Use files as an allow-list. An ignore file fails open; an allow-list fails closed. "files": ["dist"] ships only the build output, excluding source files, tests, and any secrets that might be lying around .

Remember: The four fields are not alternatives to each other. They are layers of a compatibility strategy. main serves the oldest resolvers. module serves bundlers that predate exports. types serves TypeScript under legacy resolution. exports serves everything modern and defines what the package actually exposes. A package that sets all four correctly works for every consumer. A package that sets only main and types works for some consumers and fails silently for others. The validation stepโ€”npm pack --dry-run and @arethetypeswrong/cliโ€”is what confirms that the configuration matches reality before the package is published to the registry.

Was this helpful?


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!