-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Conversation
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.
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!
src/lockfile/wrapper.js
Outdated
@@ -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) { |
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.
May be you need a helper:
const keyForRemote = remote => remote.reference && remote.hash ? `${remote.reference}#${remote.hash}` : null;
src/package-resolver.js
Outdated
@@ -388,14 +388,18 @@ export default class PackageResolver { | |||
} | |||
} | |||
|
|||
if (getExoticResolver(version) && manifest) { |
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.
May be check manifest
first to avoid the function call when it is missing?
src/package-resolver.js
Outdated
@@ -388,14 +388,18 @@ export default class PackageResolver { | |||
} | |||
} | |||
|
|||
if (getExoticResolver(version) && manifest) { | |||
this.exoticRangeMatch(patterns.map(p => this.getStrictResolvedPattern(p)), manifest); |
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.
You can do patterns.map(this.getStrictResolvedPattern.bind(this)), manifest);
instead.
src/package-resolver.js
Outdated
@@ -410,7 +414,10 @@ export default class PackageResolver { | |||
}); | |||
|
|||
const maxValidRange = semver.maxSatisfying(versionNumbers, range); | |||
if (!maxValidRange) { | |||
|
|||
if (!maxValidRange && getExoticResolver(range) && manifest) { |
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.
Same as above, check manifest
first?
src/package-resolver.js
Outdated
|
||
if (!maxValidRange && getExoticResolver(range) && manifest) { | ||
return this.exoticRangeMatch(resolvedPatterns, manifest); | ||
} else if (!maxValidRange) { |
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 like this part needs some refactoring:
if (!maxValidRange) {
return manifest && getExoticResolver(range) ? this.exoticRangeMatch(resolvedPatterns, manifest) : null;
}
src/package-resolver.js
Outdated
exoticRangeMatch(resolvedPkgs: Iterable<Manifest>, manifest: Manifest): ?Manifest { | ||
if (!manifest._remote) { | ||
return null; | ||
} else if (manifest._remote.type != 'copy' || !manifest._remote.reference) { |
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.
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.
src/package-resolver.js
Outdated
return null; | ||
} | ||
|
||
for (const pkg of resolvedPkgs) { |
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.
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); |
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.
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)); |
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.
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); |
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.
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 :)
Thanks for the review. I'll work on those changes. |
Code refactored and test split into two separate cases with better comments. |
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.
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!
src/lockfile/wrapper.js
Outdated
@@ -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); |
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 think we should merge all this functionality into keyForRemote
and just use it everywhere without the remote.resolved
checks. What do you think?
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.
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
.
src/package-resolver.js
Outdated
} | ||
|
||
const matchedPkg = resolvedPkgs.find(({_remote: pkgRemote}) => { | ||
return pkgRemote && pkgRemote.reference === remote.reference && pkgRemote.type === 'copy'; |
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.
No need for return
:
resolvedPkgs.find(({_remote: pkgRemote}) => pkgRemote && pkgRemote.reference === remote.reference && pkgRemote.type === 'copy');
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.
This one exceeded the max line length lint rule, so I left it as is for now.
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.
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')}`]); |
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.
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.
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.
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
.
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.
Nice!
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.
Final rejection I promise. It's about the concern around that potentially missing return
statement.
src/package-resolver.js
Outdated
@@ -388,14 +388,18 @@ export default class PackageResolver { | |||
} | |||
} | |||
|
|||
if (manifest && getExoticResolver(version)) { | |||
this.exoticRangeMatch(patterns.map(this.getStrictResolvedPattern.bind(this)), manifest); |
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.
Did you mean to return this? Otherwise I'm a bit confused since we return null
right after this.
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.
Yep, good catch. Updated.
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.
Well, this means the tests don't cover this line :( we should add a test case in a follow-up.
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.
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.
LGTM, will merge once Travis passes. |
**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 :)
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.