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
fix(node-resolve): mark module as external if resolved module is external #799
Conversation
4e663d9
to
e9b4c28
Compare
@lukastaegert got a strange issue happening where I think To reproduce: Add a debuggers in the following places: const resolved = await doResolveId(this, importee, importer, opts);
debugger;
if (resolved) {
const resolvedResolved = await this.resolve(resolved.id, importer, { skipSelf: true });
debugger;
if (resolvedResolved && resolvedResolved.external) {
return false;
}
} Then in the node-resolve dir run
and connect the debugger. You'll see it gets stuck in a loop where it never gets to the second debugger, so it seems Any ideas? Will open an issue on rollup soon if I can't figure it out. It ends up in some native code which is getting hard to follow. |
Ah looks like it's commonjs doing a |
So the reason this doesn't work right now is because we resolve the requested id, and then use |
|
3249bdc
to
bab212a
Compare
@tjenkinson looks like a conflict there that needs resoluton. @lukastaegert @LarsDenBakker please weigh in on this when you have the opportunity |
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.
Looks mostly good but I am not happy about the loop prevention mechanism.
packages/node-resolve/src/index.js
Outdated
if (resolved && !inNestedResolve) { | ||
// if a plugin invoked by `this.resolve` also does `this.resolve` | ||
// we need to break the loop | ||
inNestedResolve = true; |
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 think having a global flag here is a good idea. As resolving is inherently asynchronous, concurrent resolves can mess up the value of this flag. I fear this problem needs to be solved in Rollup core by correctly tracking uses of the skipSelf
flag: If a plugin uses this.resolve
with the skipSelf
flag, then Rollup needs to remember this when calling resolveId
of other plugins so that when another plugin subsequently calls this.resolve
for THE SAME ID (that is important), the original plugin is still skipped (and this should be remembered transitively). I believe this is the issue you are trying to solve?
I thought about this myself some time ago but did not implement anything yet, maybe I should.
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 yep. I was thinking it would only be called once at a time on a single instance, but wasn't thinking about multiple ids in parallel.
I believe this is the issue you are trying to solve?
I think that would work for this case, although the difference with what you proposed is that the node-resolve
plugin would be skipped completely when this.resolve
invoked from commonjs meaning later plugins might get invoked that wouldn't usually, vs now where it would still stop when node-resolve
returns something (although what it returns now is not really the correct thing, given it could map to an external).
Not sure where the best place to break the chain is, and also when breaking the chain if there is a sensible value that could be returned or if we should actually signal an infinite loop back to the thing that called this.resolve
.
Confusing problem to think about :)
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.
True. But to my understanding, this change is not essential to the functionality you are implementing, right? So you could remove it from here without any regressions and one could always add something later, be it here or in Rollup core.
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.
Not quite understanding. The change here to do the nested this.resolve
is needed (unless there is another solution I'm missing) to fix #609
I don't mind if the fix is entirely here (with a tweak to keep a inNestedResolve
flag against the id) or partly here (to do the this.resolve
) but with a core change to prevent the loops.
Also this isn't a blocker for me btw. I just saw the issue and decided to attempt to fix it, so not sure it's a particularly urgent problem to solve.
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 sorry, should have read the entire thread. The problem is that this is an issue in core that affects all plugins. But to fix it just for node-resolve, you could maintain a data structure that contains the source/importer of what we are just resolving to remember to skip exactly this call when resolving. From Rollup side, each combination will be resolved exactly once. But you could clean up the data structure once a resolution is done to account for plugins resolving the same modules at a later stage.
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'm fine with removing that extra test and simplifying the plugin to use the core change. The edge case is so many layers deep to think about it does seem unlikely, and there's still the way of opting out if needed in the future like you mentioned in the docs.
It's bugging me though I still can't actually understand why the extra loop round is fixing the test with commonjs 🤔
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.
Maybe the question is more why it was broken in the first place without the second round. The reason is that the CommonJS plugin was doing this:
this.resolve(importee, importer, {
skipSelf: true,
custom: { 'node-resolve': { isRequire: isProxyModule || isRequiredModule } }
})
passing special options to the node-resolve plugin to change resolution, and this is the call that was skipped. Admittedly we could also try to hash the custom options object in the loop prevention, but this is dangerous as we cannot go for object identity as it is likely recreated for each call, and there is no requirement to have it serialisable via JSON stringify.
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.
We could do a JSON.stringify in a try-catch as a "best effort" approach.
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.
But now I have written this nice description and I am feeling the actual logic is getting more and more complex. Maybe we start with what we have now and see if this causes issues anywhere.
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.
Ahhh thanks that was the missing piece. Agree ideally the custom
should also be part of the key, but waiting until that actually causes a problem (if it does) seems reasonable. Will update this PR
f0fd052
to
b2d5c6b
Compare
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.
Looks good from my side
Updated. This is now a breaking change given at least rollup 2.42.0 required |
Looks good, thanks for putting in the work! |
…ernal (#799) BREAKING CHANGES: Now requires rollup@^2.42.0 * fix(node-resolve): mark module as external if resolved module is external fixes #609 * don't use ?. * prevent infinite loops * check both importee and importer in nested this.resolve and add more tests * upgrade to rollup 2.42.0 to simplify nested `this.resolve`
The following now fails with import resolve from '@rollup/plugin-node-resolve';
await resolve().resolveId("rollup"); Any ideas what I'm doing wrong here? :/ |
Hi @lazka that’s not how the plug-in is meant to me used. It’s meant to be passed to rollup. What exactly are you trying to do? |
I see. I was using the plugin API to figure out the path of packages so I can copy assets (css/images etc) from them and pass that to the copy() plugin. If that's not supported feel free to ignore. I've mocked the resolve() call for now so this keeps working :/ |
Ok. Could you use I think to make what you had before work you’ll need to now const instance = resolve();
await instance.resolveId.call(instance, "rollup"); but I haven’t tested that and it’s not the official supported way of using the plug-in |
Actually sorry that suggestion won’t work. |
When I last tried I couldn't get it to work for non-js packages, but I now see that |
rollup plugin not working due to rollup/plugins#799 (comment)
Lgtm |
Rollup Plugin Name:
node-resolve
This PR contains:
Are tests included?
Breaking Changes?
List any relevant issue numbers: fixes #609
Description
Recommend telling github to ignore the whitespace changes for review. A lot of the code just moved location
If the ID given to this plugin resolves to something which itself resolves to an external, then we should mark the ID the plugin got as external. As a user I think I'd expect
node-resolve
to either resolve to a file, or leave the import alone. Not sure resolving to something that's then an external would be a normal use case?There's a special case where
this.resolve()
might result in another plugin (i.e. commonjs) also doing athis.resolve()
causing an infinte loop. I added some logic to handle this case. Not sure if there's a better way?