type and File Extensions

The type Field and File Extensions

Before Node can load a file it must classify it. .mjs is always an ES module, .cjs is always CommonJS, and .js inherits its format from the nearest package.json found by walking up from the file -- the package scope. "type": "module" makes every .js file in that scope an ES module; "type": "commonjs", or no field, makes them CommonJS.

How Node decides the format of a file
File Package scope says Format
a.mjs anything ES module
a.cjs anything CommonJS
a.js "type": "module" ES module
a.js "type": "commonjs" CommonJS
a.js no type field detected from syntax
a.json anything JSON
a.node anything native addon

The detection row is the interesting one. With no type field in scope, Node parses the file as CommonJS; if that fails with an error only ES module syntax could cause, it reparses as ESM and warns about the wasted work.

An ambiguous .js file next to a package.json with no type fieldJSON
export const kind = 'ESM by syntax detection';
console.log(kind);
Output
(node:22908) [MODULE_TYPELESS_PACKAGE_JSON] Warning: Module type of file:///.../loose.js is not
specified and it doesn't parse as CommonJS.
Reparsing as ES module because module syntax was detected. This incurs a performance overhead.
To eliminate this warning, add "type": "module" to .../package.json.
ESM by syntax detection

Detection is a compatibility ramp, not a feature to build on: it costs a double parse per file, it cannot help a file that happens to parse as CommonJS, and --no-experimental-detect-module turns it off -- at which point the file dies with SyntaxError: Unexpected token 'export'. Declare "type" in every package.json you write.

Because the scope is the nearest package.json, a file containing only { "type": "commonjs" } carves out an exception. Drop it in tools/legacy/ and every .js file below gets require, module.exports and __dirname back while the rest of the repository stays on ES modules.