-
-
Notifications
You must be signed in to change notification settings - Fork 594
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
feature(commonjs): support for destructuring require for named imports #414
Conversation
f713b10
to
92de54f
Compare
So this means I might have different behavior between these: const { foo } = require('bar');
console.log(foo); and const bar = require('bar');
console.log(bar.foo); How do other tools deal with this? |
@LarsDenBakker They don't. It's just that in modules that are designed to be compatible with CJS, they add the named exports as members of the default export, or omit the default export. In those cases it will keep the same behavior. You can refer to the discussion in #400 and refer to the links I put there. |
92de54f
to
44bfe5e
Compare
I don't know - this IMHO is a dangerous thing to do. While it can improve some scenarios it can lead to obscure bugs in others - I certainly wouldn't like to bump into an issue caused by this as a developer. The node ecosystem is pushing towards very strict rules in terms of CJS and ESM interoperability and I feel that introducing yet-another-non-strict semantic to the ecosystem at this point in time shouldn't be discouraged. The issue presented by @LarsDenBakker is IMHO a very good argument against this. |
@Andarist please note that this only affect how you import named exports from ESM and not from CJS. I have yet to see a situation where this would break things with some module.
|
I'm worried that this might affect existing packages that destructure from the default export. The problem is not even that this might affect such packages but it might also affect their, unaware, consumers. Could you describe in detail what exact mix of files is this fixing? |
This affects only the situation where a commonjs ‘require’s an ES module, and that ES module has named exports. Let’s say that there’s a CJS module that requires an ES module.
|
@LarsDenBakker @lukastaegert thoughts on the latest round of comments? |
@@ -0,0 +1,4 @@ | |||
const { a, b } = require('./foo.js'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what if a default-style require call lives here together:
const { a, b } = require('./foo.js');
const def = require('./foo.js');
t.is(a, 1);
t.is(b, 2);
t.is(def, ???);
It would be good to test this since it feels undefined at the moment.
This seems in contrast to the interop implemented in Webpack right? Surely matching Webpack should be a goal to at least offer some consistency here? Consumer-driven interpretation does seem risky to me. |
There is one issue here that should be considered: Replacing a destructuring import with a named import will create a live-binding where before there was none. On the other hand, I expect
I hope by tomorrow I might be able to post some output examples because I think that is what is missing here to advance the discussion. Not sure what to do about the live-binding. If this really turns out to be an issue, we should add a flag to deactivate this.
What do you mean by "matching Webpack"? Do you have examples of what code Webpack generates and what code this PR creates instead, otherwise I feel this is not really helping the discussion but creating churn. Also I tend to state that "matching Webpack" is not a goal, working output is, and if we can create more efficient output that does the same, we should go for it. |
I think this is more about the fact that the Webpack is only used as an example, the issue specifically being that following patterns that are "unique" will only lead to compatibility woes when transitioning between tools. RollupJS can certainly make the decision to do that, but this concern is certainly a factor in making that decision. |
These generated named exports are not visible to the user and just used for internal optimization. If it behaves the same, there should be no concerns. With the same argument you could say a bundler should not put any modules into the same files because then there are tricks involved. This plugin is already doing a ton of tricks, which even involve scope-hoisting CJS modules that are written in a compatible way. If something is actually not working as expected with this change, please point out what specifically that is, and ideally by pointing at the generated code. |
I don't personally use this plugin but am following simply because I was cc'd and care about cross-tooling interop especially since it affects all of us through usage. So please excuse if I don't have all the context here! From looking a bit more closely it seems like currently
I believe this is what Webpack does, but perhaps @sokra can clarify on the interop here. |
Yep, in webpack |
My two cents are that webpack’s way of doing things aren’t perfect either.
Besides, module authors usually go the extra mile to make sure the package will work for everyone. This includes adding named exports as members of the default exports. And this is the only case I’ve ever seen anyone destructuring a The popular npm package |
I don’t know if that’s an issue. But if we want to improve compatibility with webpack we could detect the usage of But if you’re destructuring |
What do you here mean by “live-binding”? |
One of the goals of RollupJS is to follow the language specification itself. Under this transformation, I believe the following invariant no longer holds: const m = require('mod');
const { name } = m; being equivalent to: const { name } = require('mod'); Perhaps we can focus on this deviation from the spec as being at the root of the concern then? |
export let foo = 1
export function changeFoo() { foo = 2 } The change of foo is reflected in the importing module as this is a live binding. When u destructure a thing you „disconnect” it from having this property of being a live binding, it is only read at assignment time in the requiring file. |
b: b␊ | ||
});␊ | ||
␊ | ||
const { a: a$1, b: b$1 } = foo;␊ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not really understand. My expectation was that with this PR, the destructuring would be replaced by direct variable accesses, and the reified namespace object would not really be needed. But in all test cases, I still see the destructuring. And I fail to generate a test case where anything is improved. How come?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Were you expecting rollup
itself to optimize this case or the plugin?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was expecting the plugin create matching named exports and imports and replace the destructuring with those imports. Then Rollup would see no reason to create the namespace object.
At the moment, it is not really clear to me what this PR actually changes in terms of generated code. Could you give some examples with input code, generated output without this PR, generated output with this PR to get an idea of what this PR actually does?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I think I understand now what I misunderstood.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So what this really does is if there are named exports,
const { foo } = require('bar');
no longer becomes
import bar from 'bar';
const { foo } = bar;
but rather
import * as bar from 'bar';
const { foo } = bar;
Is that essentially correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interestingly I stumbled upon a bug in our logic here that predates your PR:
If I write
exports.foo = 42;
exports.foo = 43;
in my imported file, Rollup throws a "Duplicate export 'foo'" error. Alas...
Except that I do not get it to work, see my other comment, my feeling is that this is one of the "95% of the users will benefit from this, but it could cause issues for 5% because it changes semantics" things. Not sure how we should handle this, the benefits of such a feature could be tremendous in terms of generated code quality and size. |
This PR is not sacred, it does not fix a bug but improves semantics of interop with ESM. There is also the language semantics issue (mentioned by @guybedford). |
Could you elaborate o this one? Note: adding
I can't say I agree with this statement - IMHO usually, people have no idea how to get all of this "right" as this stuff is overly complicated in practice and it's hard to expect from module authors to know all intricacies related to this upfront
This happens, but I wouldn't say that this is super common and, on a personal level, I would advise against doing this, ever. It creates false sense of how things are supposed to work, creates multiple ways of doing something and very often prevents tree-shaking from working correctly. General note from me - I'm just really worried that this will make Rollup working differently than webpack (and yes, IMHO this is a problem) and other tools. The problem is not that this would work differently per se, but it's a problem for consumers, transitive dependencies, etc because a particular library could work when bundled with one tool but wouldn't work when bundled with other. And this affects not only module authors, but more importantly - regular users, who most of the time have no idea about all of this CJS vs ESM interop stuff and can trip over things like this badly. Interop in the community is very important from my point of view. |
As you said, they rarely recognize it being a breaking change. This is an issue.
I guess it depends on which modules you’ve used I guess.
I don’t have anything against it, more than any other backwards-compat trick out there. It’s all about an author trying to provide compatibility.
I do not have opinion on this matter. When I first switched to rollup, it was nearly impossible to roll most cjs packages anyway. |
I played around with this problem a bit today for Snowpack, and appreciate what a tough problem this is. If I understand correctly, in Rollup a CJS I was going to recommend that this plugin move closer to Webpack's behavior mentioned above and convert any I have a demo of this working for Snowpack if anyone is interested, but it requires the user to send the plugin a list of known ESM modules ahead of time. This works for our usecase, but wouldn't make sense for most users. Does a plugin have a way to check whether an imported dependency is ESM or CJS? If that was possible, I could get rid of that odd workaround and have something worth submitting for PR. |
This is not simple, but has been done before, basically by this plugin. What it does is to add a transform hook that first scans each file for its format to build a table. This is done in two passes: First a simple string matching to look for CJS keywords module, exports, require. If none are found, we assume it is ESM. Otherwise we actually Now if we want to know if an import is CJS or ESM, we postpone the hook that wants to know that until the corresponding table entry has been generated. There is just one downside: When you do this in a setup where you also have the commonjs plugin, the CJS detection would need to run BEFORE the commonjs plugin, while your import resolution would likely want to run AFTER the commonjs plugin. So basically you would need two plugins. Maybe it would be simpler to make the table built by this plugin available to other plugins... |
Regarding this PR, my feeling at the moment is that I would rather not have special handling of the import based on the use of destructuring. Instead, we should try to fix interop as a whole. There are two things to do. The simple one is to align importing ESM with how Rollup's "auto" export mode works https://rollupjs.org/guide/en/#outputexports:
So that means in inverse, a require should ONLY return the default export if it exists and THERE ARE NO NAMED EXPORTS, otherwise it should return the namespace. This should have been fixed a long time ago. On a larger scale, though, I think we should eventually switch to always using the namespace by default, because that is what mostly everyone else (Webpack, Babel, TypeScript) apparently is doing. As there are many packages that may break by this, though, there should be an easy way to list packages where instead we want require to return the default. |
What would that mean for the |
It will not go away. It is the only way to create a CJS package that exports one thing via |
So if you go with "always using the namespace by default" in here wont keeping It seems really that those 2 semantics are fighting with each other badly so having them both as defaults like this - even just keeping |
Actually, doing that for |
@Andarist The problem is the following: But that is not the point to be discussed here, the point is that there is demand for being able to create such CJS files and taking this ability away from our users seems stupid to me, or rather like trying to force a war "either assign your export to a key of So I would rather see Rollup's point to educate users about the consequences of their choice. If the library does not have an ESM output but just CJS, there is nothing wrong with "auto" mode. And if they want to change, maybe it should be their choice if and when to do a breaking change release. Also, there is no "rollup ecosystem" where this is concerned. There are libraries that are built with a myriad of tools, one of them being Rollup, and there are also a ton of hand-crafted libraries. And yes, it will amplify the problem but only for Rollup itself. But I do not really want to discuss this in more detail right now because I have been through a week of a ton of discussions around interop fueled by the question how to support package.exports in Node and it has been massively exhausting. To be honest, working on Rollup was giving me energy and inspiration but now it feels like depressing churn, even starting to ruin my family vacation in part. The thing is, I have the feeling nobody seems to respect and understand why these things like "auto" mode exist in the first place and I feel like I have to explain things over and make calls all the time that nobody likes anyway. At least it feels like there are always people who kindof pressure me into doing things one way or another instead of trying to understand the dilemmas I am facing. Maybe I should just ignore this discussion and go back to building actually cool features, the backlog is VERY long. Sorry I had to say this here, it is definitely not your fault, I just needed to vent some air. |
@lukastaegert I think you're doing a great job balancing a lot of different needs. This stuff is hard work, burnout is real! Make sure you're taking some time for yourself ❤️ |
It could be useful to distinguish the cases of auto by treating the user entry points differently to the internal modules. Entry points with auto = user wants to customize their own interface, so the tricks can be allowed fine. Perhaps a new |
@lukastaegert I highly appreciate your work, I follow it closely since My intention was never to pressure you to go in any direction - I know this stuff is really complicated and you have the best intentions for the ecosystem as a whole. There is just not a single good answer to this problem and it's OK to have different points of view. I was purely interested in more detailed reasoning behind the proposed change as it seemed (and you have confirmed it) that this would amplify the mentioned problem for Rollup users and this has seemed like a problem to me. Please don't feel pressure to answer to those discussions that are happening immediately, take your free time with your family. Take it all at your own pace, even when not being on vacation. I know, and probably most of us here know it as well, it's often hard as we are deeply, often emotionally, invested in this stuff and we are very eager to discuss this at any time, often hurting our work-life balance. I feel that everyone here would totally understand waiting for a response longer. |
From the comments on this PR it seems that we're at a bit of an impasse on exactly how it should work. After having let this marinate for a week+, anyone have opinions/thoughts on whether we should move forward in this direction, pivot, or close the PR? |
@shellscape I think that for the very least we should postpone this, as we seem to have other interoperability issues due to latest removal of an old feature. |
@shellscape @lukastaegert seems to me like this can be closed as interoperability was resolved at the root, right? |
Agree |
Rollup Plugin Name:
commonjs
This PR contains:
Are tests included?
Breaking Changes?
List any relevant issue numbers:
#400 - Kind of relevant but not really. I mean it won't solve the issue unless they move to named imports anyway.
Description
This PR adds detection of "named imports" in CJS, which do not really exist but as people are starting to fake it with object destructuring it becomes relevant.
So what happens now is that if you are using object destructuring syntax with
require
, it tries to detect whether there are named exports in the imported module.default
in case you importdefault
.default
export as usual, and it will destructure that as usual.All existing tests pass without any change to snapshots or configuration, and the new tests are passing with flying colors for both CJS->CJS and EJS->CJS.
Theoretical breaking changes
In theory, if someone has existing code where an ES-compatible CJS module is imported AND it has both
default
and named exports, AND they are using object destructuring in therequire
statement - the behavior will change.But in every such module I've seen where CJS compatibility is required, people exported the "named exports" over the
default
object too.