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: Local package dependency resolving (refs #3897) #3945

Merged
merged 1 commit into from Jul 26, 2017
Merged

Fix: Local package dependency resolving (refs #3897) #3945

merged 1 commit into from Jul 26, 2017

Conversation

ghost
Copy link

@ghost ghost commented Jul 18, 2017

Summary
File protocol dependencies will now be de-duped correctly. E.g. './a', 'a', and '/absolute/path/to/a' will be treated equivalently. Fixes issue #3897.

Test plan
Test added to cover the above cases.

@ghost ghost changed the title Bugfix #3897: Fixing local package dependency resolving Bugfix 3897: Fixing local package dependency resolving Jul 18, 2017
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.

Thanks a lot for the fix. I've added some comments. My concern is mostly around the tests but if you can factor in my suggestions for the main code, that'd be great!

@@ -184,6 +186,8 @@ export default class Lockfile {

if (remote.resolved) {
seen.set(remote.resolved, obj);
} else if (remote.type == 'copy' && remote.reference && remote.hash) {
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 you need a helper:

const keyForRemote = remote => remote.reference && remote.hash ? `${remote.reference}#${remote.hash}` : null;

@@ -388,14 +388,18 @@ export default class PackageResolver {
}
}

if (getExoticResolver(version) && manifest) {
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 check manifest first to avoid the function call when it is missing?

@@ -388,14 +388,18 @@ export default class PackageResolver {
}
}

if (getExoticResolver(version) && manifest) {
this.exoticRangeMatch(patterns.map(p => this.getStrictResolvedPattern(p)), manifest);
Copy link
Member

Choose a reason for hiding this comment

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

You can do patterns.map(this.getStrictResolvedPattern.bind(this)), manifest); instead.

@@ -410,7 +414,10 @@ export default class PackageResolver {
});

const maxValidRange = semver.maxSatisfying(versionNumbers, range);
if (!maxValidRange) {

if (!maxValidRange && getExoticResolver(range) && manifest) {
Copy link
Member

Choose a reason for hiding this comment

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

Same as above, check manifest first?


if (!maxValidRange && getExoticResolver(range) && manifest) {
return this.exoticRangeMatch(resolvedPatterns, manifest);
} else if (!maxValidRange) {
Copy link
Member

Choose a reason for hiding this comment

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

Looks like this part needs some refactoring:

if (!maxValidRange) {
    return manifest && getExoticResolver(range) ? this.exoticRangeMatch(resolvedPatterns, manifest) : null;
}

exoticRangeMatch(resolvedPkgs: Iterable<Manifest>, manifest: Manifest): ?Manifest {
if (!manifest._remote) {
return null;
} else if (manifest._remote.type != 'copy' || !manifest._remote.reference) {
Copy link
Member

Choose a reason for hiding this comment

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

You return null in both cases so why not:

if (!(manifest._remote && manifest._remote.reference && manifest._remote.type === 'copy')) {
    return null;
}

This also makes your intent clearer.

return null;
}

for (const pkg of resolvedPkgs) {
Copy link
Member

Choose a reason for hiding this comment

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

const matchedPkg = resolvedPkgs.find(pkg => {
    const remote = pkg._remote;
    return remote && remote.reference.manifest._remote.reference && remote.type === 'copy');
});

if (matchedPkg) { 
    manifest._remote = match._remote;
}

return matchedPkg;

@@ -437,6 +437,24 @@ test.concurrent('install with file: protocol as default', (): Promise<void> => {
});
});

test.concurrent('install file: dedupe dependencies', async (): Promise<void> => {
await runInstall({}, 'install-file-dedupe-dependencies', async (config, reporter) => {
expect(await fs.exists(path.join(config.cwd, 'node_modules', 'a', 'node_modules'))).toEqual(false);
Copy link
Member

Choose a reason for hiding this comment

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

Add a comment above this stating what you are testing or be more explicit with your test, please. I'm guessing this is to make sure b is not added as a sub-dependency of a, right?

const pkgJsonContents = await fs.readFile(path.join(config.cwd, 'c/package.json'));
const pkgJson = JSON.parse(pkgJsonContents);
pkgJson.dependencies = {b: `file:${path.join(config.cwd, 'b')}`};
await fs.writeFile(path.join(config.cwd, 'c/package.json'), JSON.stringify(pkgJson));
Copy link
Member

Choose a reason for hiding this comment

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

Why are we doing this? Feels like this needs to be a separate test case or handled via a yarn add b@file:... command instead of directly manipulating the package.json file in the fixture.

const reInstall = new Install({}, config, reporter, lockfile);
await reInstall.init();

expect(await fs.exists(path.join(config.cwd, 'node_modules', 'c', 'node_modules'))).toEqual(false);
Copy link
Member

Choose a reason for hiding this comment

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

Again, what are we testing? I'm guessing you are testing now c also doesn't have any sub-dependencies even if we added b to it. I'd suggest creating a separate test case for this whole scenario. It would make your test cases more understandable and atomic so if something goes wrong in the future, we won't look at this test and try to decipher what exactly went wrong :)

@ghost
Copy link
Author

ghost commented Jul 18, 2017

Thanks for the review. I'll work on those changes.

@ghost
Copy link
Author

ghost commented Jul 21, 2017

Code refactored and test split into two separate cases with better comments.

@ghost ghost changed the title Bugfix 3897: Fixing local package dependency resolving Fix: Local package dependency resolving (refs #3897) Jul 21, 2017
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.

Nice!

I'll wait for you to respond to my inline comments and then I'll merge with or without changes per your preference.

Thanks for sticking to this!

@@ -156,7 +160,8 @@ export default class Lockfile {
invariant(ref, 'Package is missing a reference');
invariant(remote, 'Package is missing a remote');

const seenPattern = remote.resolved && seen.get(remote.resolved);
const altRemoteKey = keyForRemote(remote);
Copy link
Member

Choose a reason for hiding this comment

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

I think we should merge all this functionality into keyForRemote and just use it everywhere without the remote.resolved checks. What do you think?

Copy link
Author

@ghost ghost Jul 25, 2017

Choose a reason for hiding this comment

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

Yes this is better. It looks like there may be one case where a remote may have a resolved prop but a falsy hash prop, so I added a check for remote.resolved in keyForRemote.

}

const matchedPkg = resolvedPkgs.find(({_remote: pkgRemote}) => {
return pkgRemote && pkgRemote.reference === remote.reference && pkgRemote.type === 'copy';
Copy link
Member

Choose a reason for hiding this comment

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

No need for return:

resolvedPkgs.find(({_remote: pkgRemote}) => pkgRemote && pkgRemote.reference === remote.reference && pkgRemote.type === 'copy');

Copy link
Author

Choose a reason for hiding this comment

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

This one exceeded the max line length lint rule, so I left it as is for now.

Copy link
Member

@BYK BYK Jul 26, 2017

Choose a reason for hiding this comment

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

Prettier should fix it automatically (yarn prettier). Or you can do it manually:

resolvedPkgs.find(
  ({_remote: pkgRemote}) =>
    pkgRemote && pkgRemote.reference === remote.reference && pkgRemote.type === 'copy'
);

test.concurrent('install file: dedupe dependencies 2', (): Promise<void> => {
return runInstall({}, 'install-file-dedupe-dependencies-2', async (config, reporter) => {
// Add b as a dependency, using an absolute path
await add(config, reporter, {}, [`b@file:${path.join(config.cwd, 'b')}`]);
Copy link
Member

Choose a reason for hiding this comment

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

fs.realpath might be better than using path.join with config.cwd since it is guaranteed to be absolute where as config.cwd can still be a relative path. Unlikely, but just pointing it out.

Copy link
Author

Choose a reason for hiding this comment

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

Ended up using path.resolve for this since it allows multiple paths to be passed as arguments. Should give the same functionality as fs.realpath.

Copy link
Member

Choose a reason for hiding this comment

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

Nice!

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.

Final rejection I promise. It's about the concern around that potentially missing return statement.

@@ -388,14 +388,18 @@ export default class PackageResolver {
}
}

if (manifest && getExoticResolver(version)) {
this.exoticRangeMatch(patterns.map(this.getStrictResolvedPattern.bind(this)), manifest);
Copy link
Member

Choose a reason for hiding this comment

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

Did you mean to return this? Otherwise I'm a bit confused since we return null right after this.

Copy link
Author

Choose a reason for hiding this comment

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

Yep, good catch. Updated.

Copy link
Member

Choose a reason for hiding this comment

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

Well, this means the tests don't cover this line :( we should add a test case in a follow-up.

Copy link
Author

Choose a reason for hiding this comment

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

Yes, I'll look into why the added tests don't seem to be covering this.

File protocol dependencies will now be deduped correctly. E.g. './a',
'a', and '/absolute/path/to/a' will all be deduped.
@BYK
Copy link
Member

BYK commented Jul 26, 2017

LGTM, will merge once Travis passes.

@BYK BYK merged commit 6d793ae into yarnpkg:master Jul 26, 2017
@BYK BYK self-assigned this Jul 26, 2017
@ghost ghost deleted the local-pkg-resolving branch August 2, 2017 03:38
BYK pushed a commit that referenced this pull request Aug 4, 2017
**Summary**
Adding an additional test for an uncovered method from pull request #3945. The previous tests did not cover this, since the method, `getExactVersionMatch`, is only called if a package's `fresh` prop is false or the frozen flag is set. The fixtures directory for this test contains a lockfile to meet this condition.

If the `manifest && getExoticResolver(version)` condition does not evaluate properly the `multiplePackagesCantUnpackInSameDestination` warning is logged. This test checks for that.

**Test plan**

New tests, obviously :)
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.

1 participant