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

Prioritize direct dependency if available #192

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

thatsmydoing
Copy link

This is more of a proof of concept and request for comments. This PR adds support for prioritizing direct dependencies (ones listed in package.json).

For example, if our package.json has the following

{
  "devDependencies": {
    "@types/react": "^17.0.49",
    "@types/react-redux": "^7.1.24"
  }
}

at the time of writing, this will result in these yarn.lock entries

"@types/react@*":
  version "18.0.18"
  resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.18.tgz#9f16f33d57bc5d9dca848d12c3572110ff9429ac"
  integrity sha512-6hI08umYs6NaiHFEEGioXnxJ+oEhY3eRz8VCUaudZmGdtvPviCJB8mgaMxaDWAdPSYd4eFavrPk2QIolwbLYrg==
  dependencies:
    "@types/prop-types" "*"
    "@types/scheduler" "*"
    csstype "^3.0.2"

"@types/react@^17.0.49":
  version "17.0.49"
  resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.49.tgz#df87ba4ca8b7942209c3dc655846724539dc1049"
  integrity sha512-CCBPMZaPhcKkYUTqFs/hOWqKjPxhTEmnZWjlHHgIMop67DsXywf9B5Os9Hz8KSacjNOgIdnZVJamwl232uxoPg==
  dependencies:
    "@types/prop-types" "*"
    "@types/scheduler" "*"
    csstype "^3.0.2"

@types/react-redux pulls in @types/react@* which is correct since it does work with any react version. However, this is problematic for us since when we use the react-redux types, it leaks in the newer react 18 types which are not compatible with the react 17 types. So we absolutely want both to resolve to 17.0.49.

This can be worked around using yarn resolutions which is what we're doing now but it would be nice if yarn-deduplicate could do this for us.

With the default highest strategy, these won't be deduplicated. We could use fewer to deduplicate here but that affects everything else but we'd rather it apply only to direct dependencies like @types/react here.

This PR adds a --package-json flag which you can point to an existing package.json where it can get information on which versions are direct dependencies and then prioritizes those on top of whatever strategy is picked.

Is this sound? I'm not too confident with how this interacts with strategies (and should this be a strategy in itself?)

src/cli.ts Outdated
@@ -63,10 +65,12 @@ if (strategy !== 'highest' && strategy !== 'fewer') {

try {
const yarnLock = fs.readFileSync(file, 'utf8');
const packageJson = packageJsonPath ? fs.readFileSync(packageJsonPath, 'utf8') : undefined;
Copy link
Owner

Choose a reason for hiding this comment

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

Nit: I rather use null instead of undefined in this case. It feels more explicit to me.

version "0.1.0"
resolved "http://example.com/a-package/0.1.0"

other-package@>=1.0.0:
Copy link
Owner

Choose a reason for hiding this comment

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

We should remove any mention to other-package in this test, as it is not related to the test intention.

Copy link
Author

Choose a reason for hiding this comment

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

This is actually important as it verifies that packages that are not in package.json fall back to the highest strategy.


test('prioritizes direct requirements if present', () => {
const yarn_lock = outdent`
a-package@*:
Copy link
Owner

Choose a reason for hiding this comment

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

Ideally (and I acknowledge this is slightly out of the scope of the PR), we should use a similar yarn.lock to test all strategies (i.e. the first two tests in this file). That way it's more clear what's the different behaviour of each strategy.

@scinos
Copy link
Owner

scinos commented Sep 24, 2022

Interesting idea. I do see the problem you are trying to solve, and this seems like a neat solution.

You mentioned fewer strategy to solve this, but even if we restrict it to @types/react I don't think it will work (or rather, if it works, it's by accident): fewer has no reason to chose 17.0.49 over 18.0.18 (assuming you have only one instance of each, of course).

I think this should be its own strategy, let's call it direct. So the logic is (just putting in words what you already did in code): let's say we have ranges A, B and C overlapping. If A is also defined in package.json, it will dedupe them to A. If A is not defined, it will fallback to highest (I think allowing direct and customize the fallback strategy is too much complexity).

On a higher level, this PR highlight the deeper problem of not having control of which version is used to deduplicate. Using strategies is just a proxy to automatically select the versions. Using direct is another way of saying "use the version I already wrote in that file". Maybe we need something else, something more direct. I'm thinking an interactive strategy which should ask the user for the version (something similar to https://classic.yarnpkg.com/lang/en/docs/cli/upgrade-interactive/). But of course this is way beyond the scope of this PR, it's just a rambling :)

Before merging this PR I'd like to see:

  • Reframe this as a new strategy
  • Add docs in README
  • Address the (very minor) issues in the comments.

I may help with those if you want, but I can't commit to a timeline right now.

@@ -107,9 +137,15 @@ const computePackageInstances = (packages: Packages, name: string, useMostCommon
// Extract the list of unique versions for this package
const versions:Versions = new Map();
for (const packageInstance of packageInstances) {
if (versions.has(packageInstance.installedVersion)) continue;
if (versions.has(packageInstance.installedVersion)) {
const existingPackage = versions.get(packageInstance.installedVersion)!;
Copy link
Owner

Choose a reason for hiding this comment

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

I don't get why we need to add isDirectDependency to every dep in the tree.

Wouldn't checking directDependencies.has(entryName) when sorting the versions (line 177) suffice? Especially when we encapsulate this logic into its own strategy.

Copy link
Author

Choose a reason for hiding this comment

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

I've moved this down here so only Version needs the isDirectDependency flag. There's no entryName to check in the version sorting step so it's not that easy to do it there. If we absolutely want to avoid having this, I suppose it's possible to loop over satisfies in the sorting function and check if any requested version is a direct dependency but that'd just be worse for performance.

Copy link
Author

@thatsmydoing thatsmydoing left a comment

Choose a reason for hiding this comment

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

Thanks for the feedback, I'll rework it into a strategy instead.

version "0.1.0"
resolved "http://example.com/a-package/0.1.0"

other-package@>=1.0.0:
Copy link
Author

Choose a reason for hiding this comment

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

This is actually important as it verifies that packages that are not in package.json fall back to the highest strategy.

Copy link
Author

@thatsmydoing thatsmydoing left a comment

Choose a reason for hiding this comment

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

How about this?

@@ -13,7 +13,7 @@ program
.usage('[options] [yarn.lock path (default: yarn.lock)]')
.option(
'-s, --strategy <strategy>',
'deduplication strategy. Valid values: fewer, highest. Default is "highest"',
'deduplication strategy. Valid values: fewer, highest, direct.',
Copy link
Author

Choose a reason for hiding this comment

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

commander already shows the default value so I removed Default is "highest" as it was redundant.

@@ -107,9 +137,15 @@ const computePackageInstances = (packages: Packages, name: string, useMostCommon
// Extract the list of unique versions for this package
const versions:Versions = new Map();
for (const packageInstance of packageInstances) {
if (versions.has(packageInstance.installedVersion)) continue;
if (versions.has(packageInstance.installedVersion)) {
const existingPackage = versions.get(packageInstance.installedVersion)!;
Copy link
Author

Choose a reason for hiding this comment

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

I've moved this down here so only Version needs the isDirectDependency flag. There's no entryName to check in the version sorting step so it's not that easy to do it there. If we absolutely want to avoid having this, I suppose it's possible to loop over satisfies in the sorting function and check if any requested version is a direct dependency but that'd just be worse for performance.

@scinos
Copy link
Owner

scinos commented Dec 11, 2022

Sorry, I haven't been available to review this change. I'll have a look as soon as possible.

@burtek
Copy link

burtek commented May 15, 2023

This would actually be nice to have :)

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.

3 participants