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

Prevents hoisting through peer dependencies #4086

Merged
merged 4 commits into from
Aug 7, 2017

Conversation

arcanis
Copy link
Member

@arcanis arcanis commented Aug 3, 2017

Should fix #3933 - not yet ready, the simple reproduction script pass, but not the real-world case.

@BYK BYK self-assigned this Aug 3, 2017
@arcanis
Copy link
Member Author

arcanis commented Aug 3, 2017

K, I think I got it. Seems like the issue is the order the dependencies are processed. We currently sort them by name, so in some case we might do the check "does the peer dependency exists?" before the peer dependency has been traversed. Only issue with this theory is that ajv should be traversed before ajv-keyword, but apparently not.

@arcanis
Copy link
Member Author

arcanis commented Aug 3, 2017

Aaaah found it, the compared patterns are ajv-keywords@^1.0.0 and ajv@^4.7.0, not the package names, so ajv-keywords comes first.

@arcanis
Copy link
Member Author

arcanis commented Aug 3, 2017

Just pushed a fix that makes sure we're hoisting each package only once its peer dependencies have been hoisted themselves, recursively. Only remains the linting.

ping @CrabDude, what do you think?

let sortedQueue = [];
const availableSet = new Set();

for (let hasChanged = true; queue.length > 0 && hasChanged;) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This really begs for a while loop

const pkg = this.resolver.getStrictResolvedPattern(pattern);

const peerDependencies = Object.keys(pkg.peerDependencies || {});
const areDependenciesFulfilled = peerDependencies.every(peerDependency => availableSet.has(peerDependency));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code inside the 2nd-level for loop looks a bit expensive to me. Do you think we can find more efficient ways to do this? Open for discussion.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In theory it could be possible to avoid checking every fulfilled dependency every time, since we can just skip the ones we've already validated. In practice, the gain would be inconsequential and would depend on the engine optimizations more than anything else. Additionally, peer dependencies are rare enough for this code not to have a major (or even minor) impact on the perf. It's unlikely more than a single pass will be ever required.

That being said, there's maybe one optimization that can be useful, which is to just skip this whole codepath if there's no peer dependency (avoids us to do what is essentially a manual slice). Will do this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nevermind, one needs to iterate on the dependencies to know if they have peer dependencies, so it can't really be optimized either. Eh. 😞

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally, peer dependencies are rare enough for this code not to have a major (or even minor) impact on the perf. It's unlikely more than a single pass will be ever required.

I think this is a dangerous assumption in the long run since people are using peerDependencies more and more with many libraries as they are the "right way" for things like plugins etc.

@@ -291,8 +326,24 @@ export default class PackageHoister {
}
}

const peerDependencies = Object.keys(info.pkg.peerDependencies || {});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May be we need a helper function, getPeerDependencies since I see this Object.keys(pkg.peerDependencies || {}) in multiple places now.

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 feel like the indirection would make the code less clear. Why make a function call to an helper when you can just call the function this helper calls? It only matters if we plan to change the info.pkg underlying type and suddenly it wasn't a mapping of the package.json anymore, but I don't see what we would have to gain in doing that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like the indirection would make the code less clear.

It would actually make the code clearer since it gives a name to what you are doing and it is shorter: less cognitive overload. Also makes that part of the code separately testable and less prone to errors and easier to refactor since you hoisted up repeated code.


if (existing) {
info.addHistory(`Found a peer dependency requirement at ${checkKey}`);
break hoistLoop;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It almost looks like this part should become a separate method to avoid this label usage.

Copy link
Member Author

@arcanis arcanis Aug 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know - despite being usually avoided, the label has a great use case here, and I feel like the hoister is complex enough to warrant some of its logic to be kept inline, so that we don't have to jump everywhere in the file to understand its process. There's already too many functions imo :p

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know - despite being usually avoided, the label has a great use case here, and I feel like the hoister is complex enough to warrant some of its logic to be kept inline

With this line of thinking, we can increase complexity infinitely instead of trying to find simpler ways of doing things.

There's already too many functions imo :p

Too many functions >> too much code in a single function

@arcanis
Copy link
Member Author

arcanis commented Aug 4, 2017

The next to last appveyor build failed because of a Github 500, and the last one failed because of a segfault. Third time's the charm ...

Copy link
Member

@BYK BYK left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this code needs rework and some simplification (not just the part you've added but in general) but I'm accepting since it resolves an important bug.

@BYK BYK merged commit 4402b3b into yarnpkg:master Aug 7, 2017
@mikegreiling
Copy link

Will this end up in an upcoming release anytime soon? I see this was part of the Yarn 1.0 card. Will there be any releases between now and v1.0?

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

Successfully merging this pull request may close these issues.

Improper hoisting of peerDependencies: webpack, ajv, ajv-keywords
3 participants