Node.js Package Module Import/Export Rules and Conditional Exports – A Comprehensive Guide
This article explains the latest Node.js package module specifications, covering how the "type", "exports", and "imports" fields in package.json determine ESM or CommonJS loading, the history of version changes, subpath mappings, conditional exports, and best practices for dual-module packages.
Introduction – Node.js has introduced new package module specifications that allow developers to control which files and APIs are exposed, improving code hygiene and dependency management. The article presents a detailed study of these rules.
Version history – A table shows key changes such as the addition of the "exports" pattern in v14.13.0, the "imports" field in v14.6.0, and the removal of special flags for conditional exports in later versions.
Determining the module system – Node.js treats a file as an ES module (ESM) when it ends with .mjs , or when it ends with .js and the nearest parent package.json has "type": "module". All other files are considered CommonJS, unless they have .cjs or explicit flags like --input-type=module or --input-type=commonjs .
// my-app.js is treated as an ES module because package.json "type" is "module"
import './startup/init.js';
import 'commonjs-package'; // loads as CommonJS because the package lacks "type" or has "type":"commonjs"
import './node_modules/commonjs-package/index.js'; // also loads as CommonJSFiles ending with .mjs are always ESM, and files ending with .cjs are always CommonJS, regardless of the surrounding package.json .
import './legacy-file.cjs'; // loaded as CommonJS
import 'commonjs-package/src/index.mjs'; // loaded as ES modulePackage entry points – Two fields define entry points: "main" (the classic field) and "exports" (a more powerful replacement). When both are present, "exports" takes precedence. "exports" can also define conditional exports for different environments (import vs. require, node vs. browser, etc.).
{
"main": "./main.js",
"exports": "./main.js"
}Using conditional exports:
{
"main": "./main-require.cjs",
"exports": {
"import": "./main-module.js",
"require": "./main-require.cjs"
},
"type": "module"
}Subpath exports – Developers can expose specific subpaths, e.g., "./submodule": "./src/submodule.js". Attempting to import an undefined subpath results in ERR_PACKAGE_PATH_NOT_EXPORTED .
import submodule from 'es-module-package/submodule'; // loads ./src/submodule.js
import submodule from 'es-module-package/private-module.js'; // throws ERR_PACKAGE_PATH_NOT_EXPORTEDSubpath imports – The "imports" field (prefixed with "#") defines internal mappings used only inside the package.
{
"imports": {
"#dep": {
"node": "dep-node-native",
"default": "./dep-polyfill.js"
}
},
"dependencies": {
"dep-node-native": "^1.0.0"
}
}Subpath patterns – For packages with many subpaths, patterns like "./features/*": "./src/features/*.js" reduce boilerplate.
import featureX from 'es-module-package/features/x'; // loads ./src/features/x.js
import internalZ from '#internal/z'; // loads ./src/internal/z.jsConditional exports – Conditions such as "import", "require", "node", and "default" allow different files to be served based on how the package is consumed.
{
"main": "./main-require.cjs",
"exports": {
"import": "./main-module.js",
"require": "./main-require.cjs"
},
"type": "module"
}Dual CommonJS/ESM packages – Packages can provide both module formats, but loading both can cause the "dual package hazard" (different instances, broken instanceof checks, etc.). Recommended approaches include using an ES module wrapper or isolating shared state in a common file.
// package.json
{
"type": "module",
"main": "./index.cjs",
"exports": {
"import": "./wrapper.mjs",
"require": "./index.cjs"
}
}
// index.cjs
exports.name = 'value';
// wrapper.mjs
import cjsModule from './index.cjs';
export const name = cjsModule.name;When state must be shared, both entry points can import a common state.cjs file, ensuring a single source of truth.
// index.cjs
const state = require('./state.cjs');
module.exports.state = state;
// index.mjs
import state from './state.cjs';
export { state };Key package.json fields – "name", "main", "type", "exports", and "imports" are the fields used by Node.js to resolve modules. The article also notes that many popular libraries have not yet adopted "exports", but internal packages can start using it to limit public APIs and avoid bugs.
Overall, the guide provides best‑practice recommendations for defining package entry points, using conditional and subpath exports, handling dual module formats safely, and leveraging the new Node.js package module features to create robust, maintainable libraries.
ByteDance Dali Intelligent Technology Team
Technical practice sharing from the ByteDance Dali Intelligent Technology Team
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.