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.
| 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.
export const kind = 'ESM by syntax detection';
console.log(kind);(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.