tezvyn:

How do you configure a dual ESM/CommonJS package?

AI-drafted, machine-checkedSource: nodejs.orgadvanced
WHAT IT TESTS

Node.js conditional exports and the dual-package hazard.

ANSWER OUTLINE

Use exports.require for .cjs and exports.import for .mjs, keep type unset or explicit, and compile separate artifacts.

WHAT THIS TESTS: This question probes whether you understand Node.js module resolution beyond bundler folklore. Specifically it checks if you know how the exports field works, why conditional exports matter, and how to avoid the dual package hazard where two copies of your module get loaded and break singleton state. It also reveals whether you distinguish between bundler conventions and Node native behavior.

A GOOD ANSWER COVERS: First, use the exports field with nested require and import conditions. The require path points to a CommonJS build using a .cjs extension, while the import path points to an ESM build using .mjs. Second, keep the legacy main field as a fallback while noting that exports takes precedence in modern Node. Third, compile the source twice, once to CJS and once to ESM, since one file cannot safely serve both systems. Fourth, leave the type field undefined so .js defaults to CommonJS, or use explicit extensions to remove ambiguity. Fifth, mention the dual package hazard and mitigate it by avoiding mutable module level state or by using a wrapper if state must be unified.

COMMON WRONG ANSWERS: A major red flag is relying solely on the module field, a bundler convention that Node.js ignores. Another is setting type to module and providing a CommonJS entry without a .cjs extension, which causes Node to reject the file when required. Some suggest telling CommonJS users to use dynamic import, which breaks synchronous require chains. Others propose a single build with runtime environment detection; this fails because ESM syntax causes parse errors in CJS evaluation. Finally, omitting exports and using only main prevents safe tree shaking and correct module enforcement.

LIKELY FOLLOW-UPS: An interviewer might ask how you would handle subpath exports like lodash style deep imports while keeping dual module support. They could ask what happens if both the ESM and CJS versions load and mutate a shared cache or database connection. Another follow up is how TypeScript declaration maps interact with dual packages, or whether you would use tsup, Rollup, or unbuild to generate the two outputs. They might also ask about the default condition and how it interacts with require and import.

ONE CONCRETE EXAMPLE: Imagine a package named utils. The package.json contains exports with a dot key mapping to require pointing to ./dist/index.cjs and import pointing to ./dist/index.mjs. The build script runs tsc to emit CommonJS into dist/cjs and uses Rollup or tsup to emit ESM into dist/esm, then renames or outputs files with the correct extensions. The main field is set to ./dist/index.cjs for legacy consumers. No type field is set, so plain .js files remain CommonJS by default, while the explicit .mjs and .cjs extensions guarantee the correct parser regardless of context.

Read the original → nodejs.org

Get five bites like this every day.

Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.