Skip to content

feat(compartment-mapper): support treating top-level return in CJS as a way to export from module #2762

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from

Conversation

naugtur
Copy link
Member

@naugtur naugtur commented Apr 8, 2025

Description

One of the rarely used ways to export from a CJS module is to return a reference instead of setting module.exports to that reference. This PR adds support for that behavior and a tiny fixture in tests to ensure it works in all scenarios.

Security Considerations

N/A

Scaling Considerations

N/A

Documentation Considerations

It's part of CJS and also something very few people know. Documenting it would lead to more confusion than not.

Testing Considerations

Covered with a case in existing test coverage.

Compatibility Considerations

Increases ecosystem compatibility by the few unfortunate packages that use this way of exporting.

Upgrade Considerations

None

Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

@naugtur naugtur force-pushed the naugtur/cjs-top-level-return-exports branch from 2a54916 to 81e0c45 Compare April 8, 2025 11:29
@endojs endojs deleted a comment from Copilot AI Apr 8, 2025
@naugtur naugtur requested a review from Copilot April 8, 2025 14:02
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Comments suppressed due to low confidence (3)

packages/compartment-mapper/src/parse-pre-cjs.js:51

  • Ensure that the test suite includes scenarios where functor.call returns undefined, so that the fallback to module.exports is properly validated.
const returnValue = functor.call(

packages/compartment-mapper/src/parse-cjs.js:58

  • Verify that tests cover cases when functor.call returns a defined value versus undefined, ensuring consistency in export behavior.
const returnValue = functor.call(

packages/compartment-mapper/src/parse-cjs-shared-export-wrapper.js:187

  • Confirm that tests validate both branches of the conditional that selects returnValue or module.exports, ensuring that shared export logic behaves as expected.
const afterExecute = returnValue => {

Comment on lines +187 to +189
const afterExecute = returnValue => {
const finalExports =
returnValue !== undefined ? returnValue : module.exports; // in case it's a getter, only call it once
Copy link
Member

@boneskull boneskull May 7, 2025

Choose a reason for hiding this comment

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

Wait. Is this right?

Executing node -e console.log(require("/path/to/exports-return.js")) reports {} regardless of what was returned. Change it to anything else; it's always {}.

It appears that we need to assert exports-return.js returns an empty object. So... not {magicNumber: 42} at all.

It looks as if the originalExports value comes from the AST? But return must be in module scope, and module.exports needn't be (afaik). So it would be challenging to determine whether or not a module "aborts" with a return before module.exports is assigned to—and not possible in the worst case.

Static analysis can only get us as far as "there is no return in module scope, and the module.exports assignment is not in a block". That's the only case in which we can be reasonably sure about its properties. Otherwise, we won't be able to statically determine a) whether or not the module bailed before assigning exports (empty object) and/or b) which property keys are exported:

if (stuff) {
  return;
} else if (otherStuff) {
  module.exports = {foo: 'bar'};
} else {
  module.exports = {bar: 'baz'};
}

Right???

Maybe what needs to happen is:

  1. Ignore whatever originalExports is, assuming we don't need it for something else
  2. Ignore the return value of functor
  3. Remove the conditional exportsHaveBeenOverwritten and just assume things need to be updated
  4. Cross fingers that this won't break shit

cc @kriskowal who will likely find this abhorrent

Copy link
Member

Choose a reason for hiding this comment

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

Might need to tweak the parser to flag a module's exports as "lol idk"

Copy link
Member Author

Choose a reason for hiding this comment

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

I implemented this because bundlers had it claiming it to be node.js compatibility AFAIR. Now I see Node doesn't have that behavior and am confused. Will report back

Copy link
Member

@boneskull boneskull left a comment

Choose a reason for hiding this comment

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

I think this is busted 😄

@boneskull
Copy link
Member

We will also need to enable the allowReturnOutsideFunction option for calls to @babel/parser (if sourceType is script). This change would need to happen both in @endo/module-source and @endo/evasive-transform.

@naugtur
Copy link
Member Author

naugtur commented May 19, 2025

This turns out to be a behavior specific to some bundlers at most, definitely not what I can observe in node.js. closing.

@naugtur naugtur closed this May 19, 2025
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.

2 participants