patternjavascriptMajor
package.json exports field: subpath exports and conditional exports
Viewed 0 times
Node.js 12+
package.json exportssubpath exportsconditional exportsESM CJS dual packageERR_PACKAGE_PATH_NOT_EXPORTED
Error Messages
Problem
A published package has both ESM and CJS builds but consumers always get the CJS version, or deep imports like 'my-pkg/utils' break with 'ERR_PACKAGE_PATH_NOT_EXPORTED'.
Solution
Use the 'exports' field (package.json subpath exports) to define explicit entry points and conditions.
// package.json
{
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
},
"./utils": {
"import": "./dist/utils.mjs",
"require": "./dist/utils.cjs",
"types": "./dist/utils.d.ts"
}
},
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts"
}
// package.json
{
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
},
"./utils": {
"import": "./dist/utils.mjs",
"require": "./dist/utils.cjs",
"types": "./dist/utils.d.ts"
}
},
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts"
}
Why
The 'exports' field, introduced in Node.js 12, acts as a package encapsulation boundary. Only paths listed in exports are accessible. Bundlers and Node use the condition keys (import/require/types/browser) to select the right file.
Gotchas
- The 'exports' field takes precedence over 'main' in Node 12+; old tools that ignore 'exports' fall back to 'main'
- Deep imports not listed in exports throw ERR_PACKAGE_PATH_NOT_EXPORTED even if the file exists on disk
- TypeScript needs 'moduleResolution: bundler' or 'node16' to resolve the 'types' condition in exports
- The 'browser' condition is checked by bundlers (Webpack, Rollup, Vite) but not by Node
Context
Publishing a dual ESM/CJS package or a package with multiple entry points
Revisions (0)
No revisions yet.