Skip to content

Fix require() to return mutable exports for Node.js compat#5847

Merged
anonrig merged 1 commit intomainfrom
yagiz/make-modules-mutable
Jan 28, 2026
Merged

Fix require() to return mutable exports for Node.js compat#5847
anonrig merged 1 commit intomainfrom
yagiz/make-modules-mutable

Conversation

@anonrig
Copy link
Copy Markdown
Member

@anonrig anonrig commented Jan 7, 2026

Fixes #5844 (with the help of our friend Claude)

ES module namespaces have read-only properties by specification, but CommonJS require() is expected to return objects with mutable properties.

@anonrig anonrig requested a review from jasnell January 7, 2026 19:18
@anonrig anonrig requested review from a team as code owners January 7, 2026 19:18
Copy link
Copy Markdown
Collaborator

@jasnell jasnell left a comment

Choose a reason for hiding this comment

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

This is likely breaking and I'm not yet convinced this is the correct fix. Specifically, we have the EXPORT_DEFAULT option and corresponding compat flag that is supposed to fix the issue. The problem is that the original implementation of require() returned the module namespace and not the default export. The compat flag is supposed to update that so that it returns the default export, which is the correct behavior. When that compat flag is not set, it's supposed to maintain the original behavior of returning the module namespace. Before applying any other fixes like this here, we need to investigate if/why the EXPORT_DEFAULT option is not being correctly set.

@codspeed-hq

This comment was marked as off-topic.

@anonrig
Copy link
Copy Markdown
Member Author

anonrig commented Jan 7, 2026

@jasnell I investigated and the EXPORT_DEFAULT mechanism is working correctly - the issue is much more in depth than it initially appeared.

  • EXPORT_DEFAULT is only set for synthetic modules (not for ESM modules)
  • Node built-in modules are ESM modules, not synthetic. They're compiled from TS to JS, so maybeSynthetic == kj::none and EXPORT_DEFAULT is intentionally not set for them.
  • Even if we set EXPORT_DEFAULT for node:timers/promises, the default export of that module will be the namespace object (which is read-only). So we'd still get the same exact error.

I don't see any other way of solving this than the current approach.

@jasnell
Copy link
Copy Markdown
Collaborator

jasnell commented Jan 7, 2026

As mentioned, the proposed approach is likely breaking. This will require more consideration on what the correct approach is. I'm working on streams stuff at the moment but will be able to come back to this either later today or tomorrow.

@jasnell
Copy link
Copy Markdown
Collaborator

jasnell commented Jan 8, 2026

Ok, yeah I remember now about the limitations on the existing compat flag. What needs to happen then is introducing another compat flag that essentially says: for any module type, when require() is used, return the default export if it exists, and if it does not, fallback to the behavior you have here. However, if this new compat flag is turned off, the current behavior should be preserved. The general idea is that require should always return the default export, regardless of the module type.

Copy link
Copy Markdown
Contributor

@vicb vicb left a comment

Choose a reason for hiding this comment

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

When #5844 uses require, that's before the ESBuild bundling phase.

So there should be import in the end.

Note that the issue has import based code working in Node and failing in workerd.

So I think that's something we make sure works and should test.

I won't have time to check tonight but can probably find time to check by Monday.

Another option is to merge that but as experimental until we can double check it actually cover all the use cases and only remove exp / set a default date then.

vicb
vicb previously requested changes Jan 12, 2026
Copy link
Copy Markdown
Contributor

@vicb vicb left a comment

Choose a reason for hiding this comment

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

I added a repro in #5844 (comment)

This PR does not fix it

@anonrig anonrig force-pushed the yagiz/make-modules-mutable branch from 7903a1b to 98fb96f Compare January 27, 2026 16:36
@anonrig anonrig requested review from jasnell and vicb January 27, 2026 16:37
@codecov-commenter
Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 61.53846% with 15 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.14%. Comparing base (727c2e4) to head (98fb96f).

Files with missing lines Patch % Lines
src/workerd/jsg/util.c++ 25.00% 9 Missing ⚠️
src/workerd/api/global-scope.c++ 25.00% 0 Missing and 3 partials ⚠️
src/workerd/jsg/modules.c++ 76.92% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5847      +/-   ##
==========================================
- Coverage   70.14%   70.14%   -0.01%     
==========================================
  Files         407      407              
  Lines      107188   107225      +37     
  Branches    17965    17972       +7     
==========================================
+ Hits        75189    75213      +24     
- Misses      21216    21226      +10     
- Partials    10783    10786       +3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 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.

@vicb vicb dismissed their stale review January 27, 2026 20:19

My concerns have been fixed in a different PR

@anonrig anonrig enabled auto-merge January 28, 2026 00:04
@anonrig anonrig merged commit e8aec71 into main Jan 28, 2026
34 of 37 checks passed
@anonrig anonrig deleted the yagiz/make-modules-mutable branch January 28, 2026 00:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🐛 Bug Report — nodeTimersPromises.setImmediate is read-only

5 participants