Question presented to candidate: "Your project's package.json has no "type" field at all, but a teammate insists a specific .js file in it is being parsed as an ES Module. How is that possible, and what are the exact rules Node uses to decide?"
What a strong answer should cover:
- Node decides CommonJS vs. ES Module per file, using a precise precedence: a
.mjsextension is always treated as an ES Module, and a.cjsextension is always treated as CommonJS — regardless of anypackage.json"type"field. 📌 Verified, not assumed: a real.cjsfile ran as CommonJS and a real.mjsfile ran as an ES Module in the identical project, both genuinely ignoring a"type": "module"field in the nearestpackage.json. - A bare
.jsfile's interpretation genuinely does depend on the nearestpackage.json's"type"field:"type": "module"makes it ESM;"type": "commonjs"or the field's absence entirely (Node's actual default) makes it CommonJS — this directly explains the prompt's scenario: the teammate is very likely looking at a project whosepackage.jsongenuinely does have"type": "module"set, even if not immediately obvious, or at a.mjsfile mistaken for a plain.jsone. - 📌 Verified, not assumed — a genuinely important, version-specific nuance: on modern Node (22.12+, including this one),
require()calling into a synchronous ES Module (one using no top-level await) genuinely succeeds — real, currentrequire(ESM)support, not the older, purely mixed-module-unfriendly Node behavior many engineers still remember. However,require()on an ES Module that does use top-level await still genuinely throws a realERR_REQUIRE_ASYNC_MODULE— verified directly, with the exact real error message. - A precise answer distinguishes this from the reverse direction: an ES Module can
importa CommonJS file (Node synthesizes a default export frommodule.exports) — this direction has always worked, unlike the historically-stricterrequire()-of-ESM direction that only recently gained the synchronous-only support verified above. - The practical guidance, stated precisely:
.mjs/.cjsextensions are the explicit, unambiguous way to force a file's module system regardless of the surroundingpackage.json— genuinely useful for a single file that needs to differ from the rest of a project (a CommonJS-only build script inside an otherwise-ESM package, or vice versa).
Clarifying questions expected:
- "Does this specific file need to interoperate with an older CommonJS-only dependency, or is full ESM viable for it?" — directly shapes whether forcing
.cjsfor one file is the right call. - "Is the team relying on any Node version older than 22.12, where the require(ESM) nuance verified above does not yet apply?" — the real, version-gated behavior means this materially changes the correct answer.
Code / implementation expected: Yes — a real project with all three real cases ("type": "module" + bare .js, a real .cjs override, a real .mjs override) plus the real require()-of-ESM boundary is the concrete, convincing proof of exactly how Node's per-file decision actually works.