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

fix(node-resolve): mark module as external if resolved module is external #799

Merged
merged 6 commits into from Apr 22, 2021

Conversation

tjenkinson
Copy link
Member

@tjenkinson tjenkinson commented Feb 10, 2021

Rollup Plugin Name: node-resolve

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 #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 a this.resolve() causing an infinte loop. I added some logic to handle this case. Not sure if there's a better way?

@tjenkinson
Copy link
Member Author

tjenkinson commented Feb 10, 2021

@lukastaegert got a strange issue happening where I think { skipSelf: true } on resolve is not being respected sometimes for some reason.

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

pnpm run build
../../node_modules/.bin/ava debug --break ./test/browser.js -v -s -m "pkg.browser with mapping to prevent bundle by specifying a value of false"

and connect the debugger. You'll see it gets stuck in a loop where it never gets to the second debugger, so it seems this.resolve() is calling resolveId on the plugin again.

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.

@tjenkinson
Copy link
Member Author

Ah looks like it's commonjs doing a this.resolve() causing a loop

@tjenkinson
Copy link
Member Author

So the reason this doesn't work right now is because we resolve the requested id, and then use this.resolve() to see if any of the other plugins decide it should be an external. The problem is the CommonJS plugin itself calls this.resolve(), meaning we end up in a deadlock. Not sure right now how to solve it. Maybe this is the wrong approach.

@tjenkinson
Copy link
Member Author

tjenkinson commented Feb 10, 2021

pushed an idea that works but would require every plugin that does a this.resolve() to set skipResolvedExternalCheck, which does not seem nice (commit removed)

@tjenkinson tjenkinson marked this pull request as ready for review February 11, 2021 20:04
@shellscape
Copy link
Collaborator

@tjenkinson looks like a conflict there that needs resoluton.

@lukastaegert @LarsDenBakker please weigh in on this when you have the opportunity

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.

Looks mostly good but I am not happy about the loop prevention mechanism.

if (resolved && !inNestedResolve) {
// if a plugin invoked by `this.resolve` also does `this.resolve`
// we need to break the loop
inNestedResolve = true;
Copy link
Member

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.

Copy link
Member Author

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 :)

Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member Author

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 🤔

Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

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.

Copy link
Member Author

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

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.

Looks good from my side

@tjenkinson
Copy link
Member Author

Updated. This is now a breaking change given at least rollup 2.42.0 required

@lukastaegert
Copy link
Member

Looks good, thanks for putting in the work!

@shellscape shellscape merged commit afe2287 into master Apr 22, 2021
@shellscape shellscape deleted the node-resolve-external-regex branch April 22, 2021 13:25
guybedford pushed a commit that referenced this pull request Apr 30, 2021
…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`
@lazka
Copy link

lazka commented May 10, 2021

The following now fails with TypeError: this.resolve is not a function

import resolve from '@rollup/plugin-node-resolve';
await resolve().resolveId("rollup");

Any ideas what I'm doing wrong here? :/

@tjenkinson
Copy link
Member Author

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?

@lazka
Copy link

lazka commented May 11, 2021

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 :/

@tjenkinson
Copy link
Member Author

tjenkinson commented May 11, 2021

Ok. Could you use require.resolve in node or maybe the resolve package directly?

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

@tjenkinson
Copy link
Member Author

Actually sorry that suggestion won’t work. this needs to be the rollup plug-in context which you won’t have

@lazka
Copy link

lazka commented May 11, 2021

Ok. Could you use require.resolve in node or maybe the resolve package directly?

When I last tried I couldn't get it to work for non-js packages, but I now see that require.resolve(packageName + '/package.json') seems to do what I want. I'll give that a try. Thank you!

@Waidmann
Copy link

Waidmann commented Oct 8, 2023

Lgtm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

External modules are resolved / re-written instead of being ignored
5 participants