-
-
Notifications
You must be signed in to change notification settings - Fork 590
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
feat(commonjs): support strict require semantics for circular imports #1038
Conversation
It seems like prettier is flagging my build, which is formatted using only ESLint. Should I use prettier now instead of ESLint as they seem to be at odds here? |
|
Unfortunately not. It finds quite a few other issues in other packages but not the ones in my package. Reason seems to be that |
I'm not sure how you are running things on your end, but we don't usually see an issue with this setup. |
I will just try to rewrite the code snippet in question so that ESLint and prettier do not disagree. Funnily, it seems to be that the |
a782ae9
to
e53749d
Compare
The PR looks pretty solid. Here's a couple of failures I encountered with this PR that worked with other bundlers and the alternative commonjs plugin from rollup/rollup#4195 (comment):
There's also a discrepancy when
An unimportant note that won't affect anyone else... As I don't use --- a/packages/commonjs/package.json
+++ b/packages/commonjs/package.json
@@ -66 +66 @@
- "rollup": "^2.59.0-0",
+ "rollup": "2.59.0-0", Edit: the mixed test was simplified. |
Should the |
Thanks for having a look!
The first issue you mention looks to me like it could only be solved from the plugin if we turn the feature on for all modules. I would be hesitant to do that as there is quite some impact from doing that with regard to bundle size and performance and I would regard this very much as an edge case. As long as it works correctly with I will need to look into the other issues, that does look buggy to me. |
It's not uncommon to have a conditional require in debug code that is optimized away in production. Build time conditional polyfills are another example. In any case, I am unable to get that first example working even with the strict require option.
|
Unfortunately, it is not possible to detect conditional requires reliably even with I will also look why the first example does not work with strict semantics. |
The commonjs circular test case in rollup/rollup#4195 (comment) also fails with this commonjs PR for the same reason that If any commonjs global variables (
However, the default commonjs plugin behavior will still continue to fail:
So the obvious plugin fix would be to treat any |
Here the same logic applies as before: What if the file is also imported elsewhere? But as this will be a breaking release anyway, we can experiment here. So if the first "import" of a file that cannot be identified is a In the end, we should eventually teach the node-resolve plugin to identify file types based on extension or package.json, at least in the node_modules folder. Then we no longer need to rely on auto-detection for those files. |
I renamed the option to |
The "magic-string" issue is not a regression but "just" an interop issue. The reason is that due to the presence of the
I can only assume esbuild works because it ignores the However it appears there might be another issue when using CommonJS with strict require semantics as entry points, looking into it. |
So there is an interaction with the |
If memory serves, esbuild loads the CJS version of the module if it is
The magic string bundles have been modified to print which version is being used.
There's another way to have a commonjs
|
97eddef
to
efd0273
Compare
I added a change to the plugin so that it automatically places the node-resolve plugin after commonjs if present and not yet placed correctly |
It'd be ideal if the test case |
Hmm, I'm a bit baffled by this... I am unable to get the commonjs plugin in this PR to work on sources with require cycles when used with the recent version of No output and no errors are produced for me when using the latest version of node-resolve for applications with require cycles. This PR's plugins/packages/commonjs/package.json Line 63 in efd0273
Applications with require cycles do work with this PR's commonjs plugin when using this older version of node-resolve, but not the most recent version. I'm using |
An example of the node-resolve issue above using the commonjs plugin from efd0273:
|
Unfortunately I will need to look into this on Monday. The problem was that node-resolve was preventing the plugin from proxying entry points. |
The following patch adapted from rollup-plugin-strict-cjs gives --- a/packages/commonjs/src/helpers.js
+++ b/packages/commonjs/src/helpers.js
@@ -35,5 +35,9 @@ export function getDefaultExportFromNamespaceIfNotNamed (n) {
export function getAugmentedNamespace(n) {
if (n.__esModule) return n;
- var a = Object.defineProperty({}, '__esModule', {value: true});
+ if (typeof n == "object" && typeof n.default == "function") {
+ var o = function() { return n.default.apply(this, arguments); };
+ o.prototype = n.default.prototype;
+ } else o = {__proto__: null};
+ var a = Object.defineProperty(o, '__esModule', {value: true});
Object.keys(n).forEach(function (k) {
var d = Object.getOwnPropertyDescriptor(n, k);
The CJS
|
…, no longer supports require.cache) (#1038) BREAKING CHANGES: requires Node 12 No longer supports require.cache
…orted function (requires [email protected]) (#1038) BREAKING CHANGES: Requires at least [email protected]
}; | ||
a.prototype = f.prototype; | ||
} else a = {}; | ||
Object.defineProperty(a, '__esModule', {value: true}); | ||
Object.keys(n).forEach(function (k) { | ||
var d = Object.getOwnPropertyDescriptor(n, k); | ||
Object.defineProperty(a, k, d.get ? d : { |
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.
@lukastaegert sorry for tagging you directly. I have an issue in upgrading from "@rollup/plugin-commonjs"@21.1.0
to "@rollup/plugin-commonjs"@latest
. I tracked problem down to version "@rollup/plugin-commonjs"@22.0.0
and it looks like all changes in this version are in this PR. I hope you'll be able to provide valuable insights only by reading my question, that's why I tagged you.
I have a case when n
which arrives to getAugmentedNamespace
function already contains __esModule
property.
As the if (n.__esModule) return n;
line was removed by this PR, the code now throws at the Object.defineProperty(a, k, d.get ? d : {
line because it tries to define __esModule
property again.
Unfortunately my codebase is complex and I didn't managed to create a reproducible repo for opening an issue. Looking into exports of n
it looks similar to react-dom
(we use v17.0.2), but that's all I was able to find. Can you please tell me in which circumstances getAugmentedNamespace
helper is added to the bundle? That may help with creating a repo to reproduce and better understanding of root cause.
How do you feel about adding back the if (n.__esModule) return n;
line or modifying Object.keys(n).forEach(function (k) {
line to be Object.keys(n).filter(k => k !== '__esModule').forEach(function (k) {
?
Thanks!
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.
It is actually described in the readme https://github.com/rollup/plugins/blob/34f4c4f294b1c024552fc9674954a2d6b978e360/packages/commonjs/README.md#requirereturnsdefault in the requireReturnsDefault
section, though the helper code there is not the most current one. It is only used when requireReturnsDefault
is set to false
or not set. It is used when importing an ES module, or when importing a module where the type cannot be determined.
But yes, that is an oversight. I guess the motivation was that __esModule
is a valid ES export that would however then return a namespace object without a prototype while require
usually returns an object that has the Object.prototype
.
A solution could be to make __esModule
configurable. One would need to look into old test cases to see if adding the __esModule
short cut back breaks anything important.
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.
Thank you very much! That was super helpful.
Adding requireReturnsDefault: 'namespace'
seems to be solving issue in my case. react-dom
is external in my case, so I guess this is actually what we want.
I'll try to add back a __esModule
shortcut and see if PR will be green.
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.
@lukastaegert unfortunately adding requireReturnsDefault
option fixes the code for react-dom
but resulted in a bad code generated to bundle some other NPM package. After a number of tries I think adding check back will be a much better fix.
I opened a MR - #1379. Only snapshot tests failed, and I reviewed them one by one and the difference was only in the line which I added back to helper. Which makes me think I didn't break something.
I'll be grateful for your review. Thanks!
Polyfill node builtins manually via individual npm packages and manual shims, to be able to update them manually to maintained versions. Notably, the buffer polyfill has been updated this way, for compatibility with the newest @ledgerhq/hw-app-btc, which uses readUint8() in BtcNew.signMessage without specifying an offset, which is not supported yet by the polyfill bundlded with rollup-plugin-polyfill-node. In order to properly handle the circular dependencies of the now updated readable-stream package, @rollup/plugin-commonjs had to be updated to at least version 22.0.0 to include rollup/plugins#1038 to fix rollup/rollup#1507. Additionally, rollup and @rollup/plugin-node-resolve had been updated to the minimum version compatible with the updated @rollup/plugin-commonjs.
Rollup Plugin Name:
commonjs
This PR contains:
Are tests included?
Breaking Changes?
If yes, then include "BREAKING CHANGES:" in the first commit message body, followed by a description of what is breaking.
This includes the following breaking changes:
this.load
for plugins@rollup/plugin-node-resolve
, it needs to be at least v13.0.6dynamicRequireTargets
, packages that manipulate the commonjs cache like request-promise-native are no longer supportedList any relevant issue numbers:
resolves #985
resolves #988
resolves #1038
resolves #1044
resolves #1014
resolves #1082
resolves rollup/rollup#1507
resolves rollup/rollup#2747
resolves rollup/rollup#3805
resolves rollup/rollup#3816
resolves rollup/rollup#4187
resolves rollup/rollup#4195
resolves rollup/rollup#4216
resolves rollup/rollup#4231
resolves rollup/rollup#4273
Description
This introduces a new option
strictRequires: "auto" | boolean | "debug" | string[]
with a default of "auto". This will allow for some/all files to be wrapped in function closures so that require calls are retained as actual function calls and the first require of a module will run the module while subsequent calls will just return its current value ofmodule.exports
.true
will activate it for all CommonJS files (at a small size/performance penalty),false
will use the old hoisting logic for all files and you can also provide a list of mini match patterns to filter which files use the new mode.Here is an example what this might look like for
strictRequires: ['foo.js']
:As you may already see, it already recognizes that we did not reassign
module.exports
and can therefore just trackexports
, which is calledfoo
here re-using the module name. The variableshasRequiredFoo
andrequireFoo
will also re-use the module name to make the code more readable. If we reassignmodule.exports
, the code will adapt accordingly:and if more advanced things happen, it will change further (this is mirroring the existing logic we have for hoisted modules):
Why not use a shared helper to track module execution?
The implemented approach may produce slightly (but only slightly) larger results than using a shared require function. But it has some advantages:
What about "auto" and "debug" mode?
Here is the real magic: Having a configuration option is ok, but you do not want to learn how to use it correctly. As it turns out, nearly 100% of all issues we have result from cyclic dependencies. And with the new
this.load
helper, we can now figure out if a module is part of a cycle before we finished transforming the module. Thus by default, all modules that are part of a cycle will be wrapped magically and cyclic dependencies should now finally "just work" out of the box!Note that using any value other than "auto" or "debug" for
strictRequires
will disable cycle detection. "debug" on the other hand works like "auto" but in the end emits a warning telling you exactly which modules were wrapped automatically so that you could copy and paste this array for manual tweaking.What about dynamicRequireTargets?
I completely re-implemented this feature so that it is now only about dynamic requires. This should also fix some bugs related to code splitting. The new logic will no longer "register" modules, but there is a central registry for dynamic modules that just contains an object with the
require
functions of all dynamic modules. Modules designated as dynamic require targets are automatically wrapped in functions. The registry itself is only accessed viafunction
helpers, and as afunction
is always hoisted, there should not be any issues even with complex cyclic dependencies.Note however that the current implementation does not fully support everything
dynamicRequireTargets
supported before. Mainly support forrequire.cache
is missing as this feature is no longer easily added to the logic.Further changes
resolveId
hooks was not attached to CommonJS modules. This is fixed now.strictRequires
option is used.options
hook to inject a separate "resolver" plugin as first plugin. This is necessary as the commonjs plugin needs to be aware whenever an entry point is resolved to be able to inject an entry point ESM wrapper ifstrictRequires
is used. Otherwise, "catch-all" resolvers like node-resolve will prevent the commonjs plugin from getting to resolve those. This is an interesting pattern for plugins that need to proxy some modules but usually just callthis.resolve
in theirresolveId
hook anyway to defer to other plugins.ignoreTryCatch
option to external importsdynamicRequireTargets
, ALL usages ofrequire
that are not part of a call expression are replaced with a functioncreateCommonjsRequire('/path/to/module')
so that it is possible to pass them around will still enabling them to be used. This extends tomodule.require
as well and also includes support forrequire.resolve
.