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
AMD modules unnecessarily uses exports #2979
Comments
Just curious as I am not an experienced AMD user: What is the relation between "circular logic" and the "exports" dependency? In which situations would returning an object fail and you need to use "exports"? |
Hello! I'll try to explain to the best of my knowledge. The reference here is from RequireJS's API documentation: https://requirejs.org/docs/api.html#circular I'll try to use this nonsensical Repl as an example: link So to put it simply, if for some reason we have two modules that depend on each other (or more files that form a dependency circle, eg. a -> b -> c -> a), then when the loading reaches the circular point there is no way to resolve the latest load. In our example (thinking as AMDs), we have loaded "a.js" (think of this as dynamic import, so a Promise) but that file lists a dependency "b.js". So before we can call the actual factory function of "a.js" we need to load "b.js". So we do a dynamic import for "b.js" and get an AMD file that lists "a.js" right back at us as a dependency. We've already loaded "a.js" and we probably have a reference to the Promise that it returned, but we're actively trying to resolve that Promise and the best we can do here is to throw or leave the loading Promise forever unresolved. However, if "a.js" had a dependency "exports", then RequireJS would create a new object for "a.js" and inject that as dependency to the module factory while also making note that this module is now equal to that particular object. Now when we load "b.js" and see that "b.js" requires "a.js" again, we can simply resolve that dependency by giving the module factory the object that earlier was attributed to "a.js". Now, admittedly when we call "b.js" with this object, it doesn't contain anything yet. We just need to hope that no exports are, as of yet, used when the factory is first called. The return value (or exports object if 'exports' was listed as a dependency again) of "b.js" will then be marked down and passed to "a.js"'s module factory. Finally "a.js"'s module factory gets to run and populate the object that was created as the "exports" object earlier. |
Thanks, that makes sense. So I guess the use of the |
Mmm, my personal opinion is that Rollup shouldn't worry about external dependencies causing dependency cycles. As long as Rollup's chunking algorithm doesn't create cycles on its own (or can somehow marks these cycles to use exports in AMD format cases), then everything beyond that should be on the user. At the very least, the current case makes very little sense where default exports turn into plain returns and named exports instead use the 'exports' object. Both export types are equally liable to causing non-resolvable cycles with AMD modules. |
My personal opinion is that Rollup should preserve the behaviour of ES modules as well as possible, and circular dependency support is a feature, even though it should be handled with care. It is rather trivial to create cycles with code-splitting: To me it sounds more like a feature request for a flag that will reduce compatibility. |
Mm, yeah. I guess you're right. Only thing that can be then counted as a bug is the difference in AMD module format between named exports and default exports. But that bug is (at least to me) very beneficial for as long as there is no flag to reduce compatibility :) |
We've just switched the jQuery source from AMD to ES modules and modified our build process to rely on Rollup. We'd still want to generate a separate One example is the define( function() {
"use strict";
return window.document;
} ); and would now be: define(['exports'], function (exports) { 'use strict';
var document = window.document;
exports.default = document;
}); With the new form, we'd have to require jQuery consumers to read the The original semantics are important to jQuery so if Rollup doesn't support this use case we'll either have to find another tool for this part of the build or post-process Rollup output which can be brittle. |
Hum. At least previously But I guess this may have changed in newer versions that are not reflected on the website? |
No, the website is always using the latest version by default. An AMD module returning only a default export should always use a return statement. It should only switch to using |
@aapoalas That's the main module, though; I'm transforming individual modules. You can try it yourself by cloning jQuery, running:
and seeing what's generated in the jQuery source doesn't have import cycles which I just verified using |
What WILL work (and might be better suited for you?) is generating an In a way, I wrote an article describing that pattern: https://levelup.gitconnected.com/code-splitting-for-libraries-bundling-for-npm-with-rollup-1-0-2522c7437697 |
@lukastaegert I've reported the issue with I've also tried your suggestion of not using Compare the source at Code that generated these files is at Let me know if you want to move this part of the discussion elsewhere as it's getting off topic. |
I think you should be using an input object instead. |
Exactly. The paths are not broken by Rollup, they are broken because you move them around after generating the output and do not preserve their relative positions. Rollup's output is meant to be kept as it is concerning the relations of the modules. If you want generated chunks to be in nested paths, an input object can help: input: {
'var/arr' : 'src/var/arr.js',
// ... other entry points
} With the default configuration, the output object will then look like
And imports from to and from other chunks will be resolved correctly. Nevertheless I see that you are actually making EVERYTHING an entry point, in which case there would be no advantage to this technique over |
I wanted to first make sure our existing setup is replicable, especially that we'll also expose our new ES modules and if we want to bundle AMD in what we publish into a few entry points we'll have to do that with ESM as well - and we don't bundle ESM at all right now (except for producing the single final file). I'll try your suggestions, thanks. |
If ESM module handles circular deps correctly: import _testCircularModule from 'recursive_dependency_bare';
const obj = {
factory() {
return _testCircularModule;
}
}
export default obj; Rollup converted AMD format which would not handle circular dep: define(['recursive_amd_bare'], function (_testCircularModule) { 'use strict';
_testCircularModule = _testCircularModule && _testCircularModule.hasOwnProperty('default') ? _testCircularModule['default'] : _testCircularModule;
const obj = {
factory() {
return _testCircularModule;
}
};
return obj;
}); If i'm understanding this correctly, the only way to get the equivalent ESM functionality would be to set |
How Do We Reproduce?
Export non-circular, named dependencies from a file and set the output format to AMD.
Repl link
Expected Behavior
Since the file does not contain any circular logic, there is no reason for the AMD module to import the special
"exports
" dependency. A simple return statement would do.Actual Behavior
The "exports" dependency is set without a fail.
The text was updated successfully, but these errors were encountered: