Skip to content
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

[commonjs] Add defaultIsModuleExports option to match Node.js behavior #838

Merged
merged 4 commits into from Apr 3, 2021

Conversation

nicolo-ribaudo
Copy link
Contributor

@nicolo-ribaudo nicolo-ribaudo commented Mar 18, 2021

Rollup Plugin Name: commonjs

This PR contains:

  • bugfix
  • feature
  • refactor
  • documentation
  • other

Are tests included?

  • yes (bugfixes and features will not be merged without tests)
  • no

Breaking Changes?

  • yes (breaking changes will not be merged unless absolutely necessary)
  • no

List any relevant issue numbers: Fixes #635

Description

I was trying to modify Babel's sources (ref) to that they work:

  1. When compiled to CJS with Babel
  2. When compiled to ESM in Node.js
  3. When bundled with Rollup

I achieved (2) by authoring our sources according to what Node.js expects.
I achieved (1) using a small custom Babel plugin, and we'll make it an option (babel/babel#12838) of @babel/plugin-transform-modules-commonjs in the next minor version.
I didn't realize (3) was not working, I now opened a PR with another custom Babel plugin (babel/babel#13017) which replaces import foo with import * as foo for the few CJS deps we have that use __esModule = true.

I think that this option should eventually default to true in the future, but we are probably not ready for it yet.

@@ -174,6 +174,13 @@ If you set `esmExternals` to `true`, this plugins assumes that all external depe

You can also supply an array of ids to be treated as ES modules, or a function that will be passed each external id to determine if it is an ES module.

### `nodeDefaultImport`
Copy link
Member

Choose a reason for hiding this comment

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

Thanks for moving forward here!

I was thinking about the name of the option and I am not happy about this choice as it references an environment (Node) that may change while IMO it should rather reflect functionality. So what this is about is as I understand it is to skip the default import interop in favour of assuming the default is module.exports. There are several ways to handle this (names are just suggestions):

  • moduleExportsIsDefault: true | false | "auto" where "auto" is the current behaviour (could also be "detect")
  • cjsDefaultExport: "module.exports" | "default" | "auto" where "auto" is the current behaviour (or cjsDefaultImport depending which way you view it)

Admittedly that the version where the default export is always exports.default may not be as useful, but what do I know. Could be a micro-optimisation for some. Thoughts?

Copy link
Member

Choose a reason for hiding this comment

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

Not sure we should change the default, but I keep twisting the idea of a config plugin in my head that would just configure commonjs, node-resolve and rollup for Node compatibility.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have a slight preference for the moduleExportsIsDefault: true | false | "auto" (or defaultIsModuleExports? since we are giving a value to the "default" binding) option, since typing "module.exports" inside a string (thus often without autocompletion) can be a bit harder.

There are already precedents for boolean | string options, such as requireReturnsDefault.

Admittedly that the version where the default export is always exports.default may not be as useful, but what do I know. Could be a micro-optimisation for some. Thoughts?

We actually have an option in Babel exactly for that (https://babeljs.io/docs/en/babel-plugin-transform-modules-commonjs#nointerop), even if we don't have data about how many people use it.

Copy link
Member

Choose a reason for hiding this comment

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

But you do not have the false case, i.e. always assuming the default export is module.exports.default, right? That's the one where I am not sure people really would use it (and if we should even add it).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

noInterop: true means "always get .default, without first checking if it has __esModule: https://github.com/babel/babel/tree/main/packages/babel-plugin-transform-modules-commonjs/test/fixtures/noInterop/import-default-only

Copy link
Member

Choose a reason for hiding this comment

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

Ah I see. So reading babel/babel#12838 I understand now that you are also going for a three-state flag 👍

or defaultIsModuleExports

I would be fine with that as well.

named: named
};

export default input.default;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure about this one. Should we avoid injecting the default export when it's not present? But then, how do we handle cases where input.default exists but it's not easily statically analyzable?

Copy link
Member

Choose a reason for hiding this comment

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

Good question. I would lean towards adding it always for now, though maybe as undefined, as this is similar to the current behaviour.

@nicolo-ribaudo nicolo-ribaudo changed the title [commonjs] Add nodeDefaultImport option to match Node.js behavior [commonjs] Add defaultIsModuleExports option to match Node.js behavior Mar 19, 2021
@lukastaegert lukastaegert self-requested a review March 20, 2021 05:54
Copy link
Member

@lukastaegert lukastaegert left a comment

Choose a reason for hiding this comment

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

Thanks for putting in the work!

@guybedford guybedford merged commit 53776ee into rollup:master Apr 3, 2021
@guybedford
Copy link
Contributor

Great work, thanks @nicolo-ribaudo for the PR.

@nicolo-ribaudo nicolo-ribaudo deleted the commonjs-node-interop branch April 3, 2021 15:34
@nicolo-ribaudo
Copy link
Contributor Author

Thanks @lukastaegert and @guybedford for the reviews!

@nicolo-ribaudo
Copy link
Contributor Author

nicolo-ribaudo commented Apr 23, 2021

We are releasing Babel 7.14.0 next week, and it contains an option which is almost the same as this one.

Is it ok if we mention in the release post that the next version of @rollup/plugin-commonjs will support this parallel option?

@guybedford
Copy link
Contributor

@nicolo-ribaudo thanks for the heads up, I'll do my best to see if we can be release ready by the end of the week but can't guarantee it.

guybedford pushed a commit that referenced this pull request Apr 30, 2021
…behavior (#838)

* [commonjs] Add `nodeDefaultImport` option to match Node.js behavior

* Undo unrelated changes

* Undo editor autoformatter

* Change option
@jedwards1211
Copy link

jedwards1211 commented Nov 2, 2023

@lukastaegert @nicolo-ribaudo is there not a way to get @rollup/plugin-babel to just output interopRequireDefault calls on external modules like my standalone babel transpilation does?

We still have a lot of external modules that are transpiled CJS with exports.default declarations, and you can probably imagine how painful it would be to try to update all of those packages to either be dual ESM/CJS or have a CJS-style module.exports = default export along with named exports.

@jedwards1211
Copy link

Okay nevermind, seems like I can get it to work by using

    getBabelOutputPlugin({
      plugins: ['@babel/plugin-transform-modules-commonjs'],
    }),

@nicolo-ribaudo
Copy link
Contributor Author

Can you try using interop: auto instead (https://rollupjs.org/configuration-options/#output-interop)? Rollup works better when it receives ESM input, rather than when Babel compiles ESM to CJS for it.

@jedwards1211
Copy link

@nicolo-ribaudo oh, many thanks! Sorry, I hadn't discovered that option.

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.

CommonJS Node.js Default Interop Case
4 participants