Skip to content

module: fix extensionless CJS files in type:module packages#62083

Open
mcollina wants to merge 2 commits intonodejs:mainfrom
mcollina:fix/extensionless-cjs-in-type-module
Open

module: fix extensionless CJS files in type:module packages#62083
mcollina wants to merge 2 commits intonodejs:mainfrom
mcollina:fix/extensionless-cjs-in-type-module

Conversation

@mcollina
Copy link
Member

@mcollina mcollina commented Mar 3, 2026

Summary

  • PR module: fix extensionless entry with explicit type=commonjs #61600 made the CJS loader respect package.json type field for extensionless files, but also enforced type: "module" which breaks CJS extensionless files inside ESM packages (e.g. yargs v17)
  • Only enforce type: "commonjs" for extensionless files; for type: "module", leave format undefined so V8's syntax detection determines the correct format
  • Adds regression test that require()s a CJS extensionless file from a type: "module" package and verifies exports are correct

Test plan

  • Existing test test/es-module/test-extensionless-esm-type-commonjs.js still passes (ESM syntax in type: "commonjs" still throws SyntaxError)
  • New test case verifies CJS extensionless file in type: "module" package returns correct exports via require()
  • Manual verification: require() of fixture returns { hello: 'world' } (was returning empty ESM namespace before fix)

Fixes: #61971
Refs: yargs/yargs#2509
Refs: #61600

@nodejs-github-bot
Copy link
Collaborator

Review requested:

  • @nodejs/loaders

@nodejs-github-bot nodejs-github-bot added module Issues and PRs related to the module subsystem. needs-ci PRs that need a full CI run. labels Mar 3, 2026
PR nodejs#61600 made the CJS loader respect the package.json
type field for extensionless files, but also enforced
type: "module" which breaks CJS extensionless files
inside ESM packages (e.g. yargs v17).

Only enforce type: "commonjs" for extensionless files.
For type: "module", leave format undefined so V8's
syntax detection determines the correct format,
allowing CJS extensionless files in ESM packages to
continue working.

Fixes: nodejs#61971
Refs: yargs/yargs#2509
@mcollina mcollina force-pushed the fix/extensionless-cjs-in-type-module branch from 3e6aa5a to 6e3ed67 Compare March 3, 2026 10:37
@codecov
Copy link

codecov bot commented Mar 3, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.65%. Comparing base (e78bf55) to head (519235b).
⚠️ Report is 19 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main   #62083   +/-   ##
=======================================
  Coverage   89.64%   89.65%           
=======================================
  Files         676      676           
  Lines      206249   206326   +77     
  Branches    39518    39527    +9     
=======================================
+ Hits       184892   184980   +88     
+ Misses      13479    13471    -8     
+ Partials     7878     7875    -3     
Files with missing lines Coverage Δ
lib/internal/modules/cjs/loader.js 98.14% <100.00%> (-0.22%) ⬇️

... and 32 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Member

@joyeecheung joyeecheung left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code LGTM with a correction on comment

Co-authored-by: Joyee Cheung <joyeec9h3@gmail.com>
@joyeecheung joyeecheung added the request-ci Add this label to start a Jenkins CI on a PR. label Mar 3, 2026
@github-actions github-actions bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Mar 3, 2026
@nodejs-github-bot
Copy link
Collaborator

@nodejs-github-bot
Copy link
Collaborator

Copy link
Member

@GeoffreyBooth GeoffreyBooth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • PR module: fix extensionless entry with explicit type=commonjs #61600 made the CJS loader respect package.json type field for extensionless files, but also enforced type: "module" which breaks CJS extensionless files inside ESM packages (e.g. yargs v17)
  • Only enforce type: "commonjs" for extensionless files; for type: "module", leave format undefined so V8's syntax detection determines the correct format

I think this proposed behavior is incorrect; according to our spec, extensionless files within a package scope with an explicit type field follow the format of the type field—that is, they behave however .js files would behave. We never do syntax detection when there's an explicit type field.

I read yargs/yargs#2509 and it's unfortunate that they seemed to rely on what was buggy behavior. But this PR seems to make several breaking changes, going against our own documented behaviors for how the type field works and how syntax detection works. I'm not sure what to do instead; perhaps just revert #61600 and re-land it as semver-major?

Marking as request changes so that we discuss this before landing. If others disagree with my analysis here I'm happy to be persuaded I'm wrong; I understand there's probably an eagerness to unbreak a popular library but let's not be so hasty that we cause other bugs.

@aduh95
Copy link
Contributor

aduh95 commented Mar 4, 2026

this PR seems to make several breaking changes

Can you list them? I don't see what would be breaking about it

Comment on lines +1935 to +1938
// Extensionless files skip the .js suffix check above. When type is commonjs,
// follow it so ESM syntax surfaces as SyntaxError instead of silently
// delegating to ESM. For type: module, leave format undefined so our syntax
// detection handles it (allowing CJS extensionless files in ESM packages).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fwiw this seems right to me; i remain surprised that extensionless ESM was ever possible, and in all the module group discussions, i thought we concluded it basically wouldn't ever be (unless the "type" default changed)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not recent, it has been possible since v20, so it would be too late to do anything about it.

@mcollina
Copy link
Member Author

mcollina commented Mar 4, 2026

@GeoffreyBooth I didn't put much thought into the spec for this. My main concern was to fix this without having to revert that commit. I'm happy either way.

The alternative would be to revert that commit from 25.x and land it after the problematic behavior is fixed in the ecosystem.

We should be doing something soon about this.

@joyeecheung
Copy link
Member

joyeecheung commented Mar 4, 2026

according to our spec, extensionless files within a package scope with an explicit type field follow the format of the type field—that is, they behave however .js files would behave. We never do syntax detection when there's an explicit type field.

This is a case where we have contradicting documentations: while in the esm part of the documentation it says the module format always respect the type field for extension files, in the cjs part of the documentation it has been including this exception that's not present in the spec, as far back as v16 from #41383 - so yargs is technically following a documented exception, the problem is more on the Node.js side that it has conflicting documentation.

when the nearest parent package.json file contains a top-level field "type" with a value of "module", those files will be recognized as CommonJS modules only if they are being included via require(), not when used as the command-line entry point of the program

#61600 made the extensionless behavior match what's in the ESM spec (so reverting it is widening this exception to ESM, not eliminating it) without counting this exception in the CJS documentation, and this PR only adds that CJS exception back. I agree the saner behavior would be to respect type field always (this is the status quo after #61600 fixed the mismatch), and remove this exception from the CJS documentation, but that should be a semver-major. For existing releases, the best we can do IMO is to combine #61600 and this PR to reduce the exception surface without introducing a regression.

Comment on lines +1935 to +1937
// Extensionless files skip the .js suffix check above. When type is commonjs,
// follow it so ESM syntax surfaces as SyntaxError instead of silently
// delegating to ESM. For type: module, leave format undefined so our syntax
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would type: commonjs "silently delegate to ESM"? Surely that should be a loud syntax detection case with a warning?

Copy link
Member

@joyeecheung joyeecheung Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Surely that should be a loud syntax detection case with a warning?

I think that might be missed in the syntax detection implementation to add a warning in this path, so it never emitted a warning in this case, and the comment was just describing the previous behavior - it should probably be removed because we generally don't talk about "what a previous bug looked like" in comments, only in commit logs.

Suggested change
// Extensionless files skip the .js suffix check above. When type is commonjs,
// follow it so ESM syntax surfaces as SyntaxError instead of silently
// delegating to ESM. For type: module, leave format undefined so our syntax
// Extensionless files skip the .js suffix check above. When type is commonjs, follow it so ESM
// syntax surfaces as SyntaxError. For type: module, leave format undefined so our syntax

Comment on lines +1941 to 1943
if (typeFromPjson === 'commonjs') {
format = typeFromPjson;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to have extensionless files entirely ignore the package.json "type"? That seems like a more pragmatic approach given that they are designed to exist outside of package boundaries in most "binary execution" use cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

module Issues and PRs related to the module subsystem. needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[v25.7] yargs v17 compatibility issue

10 participants