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: Fix numeric versions resolving to local files #4088

Merged
merged 6 commits into from
Aug 4, 2017
Merged

Fix: Fix numeric versions resolving to local files #4088

merged 6 commits into from
Aug 4, 2017

Conversation

BYK
Copy link
Member

@BYK BYK commented Aug 3, 2017

Summary

Fixes #4013. We were blindly priotizing local files (even if they
are not directories) if there's anything on the file system matching
a pattern for a package. This patch ensures that we only do this if
the pattern is not a valid semver range and the matched local entity is a directory.
Test plan

Added a new unit test and modified an incorrect old one. Also manually
verified that the issue described in #4013 does not happen anymore.

**Summary**

Fixes #4013. We were blindly priotizing local files (even if they
are not directories) if there's anything on the file system matching
a pattern for a package. This patch ensures that we only do this if
the pattern starts with `./`, `../`. `/`, `~/` (and their `\` versions
for Windows) and the matched local entity is a directory. The former
part is in line with [NPM specs](https://docs.npmjs.com/files/package.json#local-paths).

**Test plan**

Added a new unit test and modified an incorrect old one. Also manually
verified that the issue described in #4013 does not happen anymore.
@bestander
Copy link
Member

Afaik "foo": "bar" is legit in npm, and this looks like a breaking Chang anyway

@arcanis
Copy link
Member

arcanis commented Aug 3, 2017

If we want a perfect compatibility we also need to support the following:

{
    "dependencies": {
        "a": "a/b/c"
    }
}

Starting from the third level of depth, NPM tries to install it from ./a/b/c. Note that two levels of depth shouldn't match this codepath, because a/b is a github url (github.com:a/b).

@arcanis
Copy link
Member

arcanis commented Aug 3, 2017

@bestander "foo": "bar" is a valid selector, but it's not resolved on the filesystem. I think it's resolved as a tag of the package.

@BYK
Copy link
Member Author

BYK commented Aug 3, 2017

Afaik "foo": "bar" is legit in npm, and this looks like a breaking Chang anyway

Damn, you're right. Even if it is not documented, npm accepts this. The problem is, it installs the package as bar not foo which is not what we do AFAIK.

@BYK
Copy link
Member Author

BYK commented Aug 3, 2017

Also, npm doesn't priotize folders over remote versions. I just tried it with having a folder named 1 and npm i foo@1 and it installed the remote version.

@BYK
Copy link
Member Author

BYK commented Aug 3, 2017

Looks like the rule is "try file if it is not a valid semver range". Updating accordingly.

@arcanis
Copy link
Member

arcanis commented Aug 3, 2017

Cf my comment about a/b being fetched as a github repository

expect(await fs.readFile(path.join(config.cwd, 'node_modules', 'foo', 'index.js'))).toEqual('foobar;\n');
});
});

test.concurrent("don't install with file: protocol as default if target is a file", (): Promise<void> => {
// $FlowFixMe
return expect(runInstall({lockfile: false}, 'install-file-as-default-no-file')).rejects.toBeDefined();
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the problem here is rejects, Do we use this somewhere else?

Copy link
Member Author

Choose a reason for hiding this comment

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

It is because of that. This was introduced by Jest 20 and @onurtemizkan upgraded flow type definitions and used it in one of his patches.

I think Flow gets confused by how runInstall is defined by bind and cannot determine the output type of expect here.

Copy link
Contributor

Choose a reason for hiding this comment

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

I recommend turning it into an async function and awaiting the expect call. Looks a bit cleaner.

Copy link
Member Author

Choose a reason for hiding this comment

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

@cpojer you don't like returning promises from tests?

Copy link
Contributor

Choose a reason for hiding this comment

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

Not explicitly, no. Turning the function into an async function will implicitly return a promise, which Jest then uses to make the test wait for the promise.

Copy link
Member Author

Choose a reason for hiding this comment

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

Alright. I'll defer this to a later diff tho since it would require a large number of changes in this file (to be consistent) which are not very related to this particular problem. I know I did the lockfile: false changes but it was required to make sure tests were running correctly.

Copy link
Contributor

Choose a reason for hiding this comment

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

Fine with me, file it under the post-post-1.0 tag ;P

@@ -132,21 +132,27 @@ export default class PackageRequest {
}
}

async normalizeRange(pattern: string): Promise<string> {
async normalizeRange(name: string, pattern: string): Promise<string> {
Copy link
Contributor

Choose a reason for hiding this comment

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

If I remember correctly we can delete every Promise.resolve because async/await world we should not use them (i.e. It automatically change the result in Promise.resolve).

Do you think we can start to add some unit tests for package-request functions?

I am afraid that every function in this file is not try to adress one responsability and we couple the code in function just for code duplication. One another proof of this is that we have many similar names. If we can't name something, maybe there is a reason.

Copy link
Member Author

Choose a reason for hiding this comment

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

If I remember correctly we can delete every Promise.resolve because async/await world we should not use them (i.e. It automatically change the result in Promise.resolve).

Oh, right. This now makes it a double promise. Great catch!

Do you think we can start to add some unit tests for package-request functions?

I'd love to but I'd also love to tie them to a "business" purpose. Right now most of these seem to be covered via integration tests. That said I want to do a pass on tests to make them more approachable so may be then?

I am afraid that every function in this file is not try to adress one responsability and we couple the code in function just for code duplication. One another proof of this is that we have many similar names. If we can't name something, maybe there is a reason.

That's a good observation. I think we can give this file a closer look. That said I'd rather not try that in this patch since this PR resolves an important issue and refactoring those may create some back and forths.

@voxsim
Copy link
Contributor

voxsim commented Aug 4, 2017

@BYK I added some comments, If you think that they are out of scope, ignore them :) and thanks for change the flag 'noLockfile' in 'lockfile' :D

@BYK
Copy link
Member Author

BYK commented Aug 4, 2017

@BYK I added some comments, If you think that they are out of scope, ignore them :) and thanks for change the flag 'noLockfile' in 'lockfile' :D

Hey, thanks for the comments! I'll go ahead and fix the Promise.resolve() parts. For more, I'll defer them to follow up to get this important fix in as soon as possible.

if (!semver.validRange(pattern)) {
try {
if ((await fs.stat(path.join(this.config.cwd, pattern))).isDirectory()) {
return `file:${pattern}`;
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we warn if this happens? I kinda feel like this file pattern isn't great.

Copy link
Contributor

Choose a reason for hiding this comment

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

And I'm saying, we should support it, but hint at the user that it's silly.

Copy link
Member Author

Choose a reason for hiding this comment

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

Can you be more specific? Do you think we should warn people when they omit the file: prefix? I think that's reasonable but would it be a warning or info? And what if "they just prefer it that way?". May be this should be controlled this via a config flag: warn-unsafe-file false?

Copy link
Contributor

@cpojer cpojer left a comment

Choose a reason for hiding this comment

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

I'm approving to unblock but I think @arcanis came back to live and can glance over it before merging.

@BYK
Copy link
Member Author

BYK commented Aug 4, 2017

I'm approving to unblock but I think @arcanis came back to live and can glance over it before merging.

👍

return Promise.resolve(`file:${pattern}`);
if (!semver.validRange(pattern)) {
try {
if ((await fs.stat(path.join(this.config.cwd, pattern))).isDirectory()) {
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure this check is the right one:

  • It seems that NPM reports "Could not install from "a/b/c/d" as it does not contain a package.json file" even with an unexisting path
  • It should work not only if the path is a directory, but also if it is a tarball

WDYT?

Copy link
Member

Choose a reason for hiding this comment

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

Is there something preventing us from just fallbacking to the filesystem if the pattern doesn't match validRange?

Copy link
Member Author

Choose a reason for hiding this comment

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

Don't know if NPM supports tarballs. I'm fine with adding a bit more checks. My initial approach was a lot stricter actually: read the package.json file and make sure the name matches the wanted package name, hence that extra name argument in the previous version.

That said when we try to read a package manifest, we automatically fallback to the directory name as package name. This, combined with NPM being quite liberal about paths, lead to this final approach which just checks if it is a directory and it is not a valid semver range.

Awaiting further opinions :)

Copy link
Member Author

Choose a reason for hiding this comment

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

Is there something preventing us from just fallbacking to the filesystem if the pattern doesn't match validRange?

Except for the fact that I find it very dangerous (even this current version), I don't think so.

Copy link
Member Author

Choose a reason for hiding this comment

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

Well, here's one: if there's a typo, it would make the error message quite cryptic.

Copy link
Member

Choose a reason for hiding this comment

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

make sure the name matches the wanted package name, hence that extra name argument in the previous version.

Yeah we shouldn't check that, especially since we have a feature to rename packages, so we might end up installing packages under a different name than their original one.

Except for the fact that I find it very dangerous (even this current version), I don't think so.

How so? It seems less dangerous to me, since it means that whatever the filesystem is, Yarn would behave the same way on two different systems. Otherwise, there's a risk it will fallback to fetching the package from different ways if the file doesn't exists, instead of reporting about the missing file.

Copy link
Member

Choose a reason for hiding this comment

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

That being said, I checked on NPM and it seems that they just silently ignore "foo": "foo.tgz" ... so I guess this PR should be fine :)

@BYK BYK merged commit 39d6fe2 into master Aug 4, 2017
@BYK BYK deleted the one-bug branch August 4, 2017 13:35
BYK added a commit that referenced this pull request Aug 25, 2017
**Summary**

Fixes #4251. Follow up to #4088. Instead of just checking whether the
target is a valid directory, we now check if it contains a
`package.json` file too. This is still different from `npm`'s behavior.
Apparently, `npm` fetches the package info upfront to favor dist-tags
over directories but this comes at a distinct performance penalty and
makes static, deterministic resolution impossible so we are now
deprecating the implicit `file:` protocol in patterns. After a certain
point, we'll remove this code and will require everyone to use `file:`
or at least one of the following path identifiers: `./`, `../`. `/`.

**Test plan**

Updated the existing test for warning check and added a new test for
invalid directories.
BYK added a commit that referenced this pull request Aug 25, 2017
…4257)

**Summary**

Fixes #4251. Follow up to #4088. Instead of just checking whether the
target is a valid directory, we now check if it contains a
`package.json` file too. This is still different from `npm`'s behavior.
Apparently, `npm` fetches the package info upfront to favor dist-tags
over directories but this comes at a distinct performance penalty and
makes static, deterministic resolution impossible so we are now
deprecating the implicit `file:` protocol in patterns. After a certain
point, we'll remove this code and will require everyone to use `file:`
or at least one of the following path identifiers: `./`, `../`. `/`.

**Test plan**

Updated the existing test for warning check and added a new test for
invalid directories.
joaolucasl pushed a commit to joaolucasl/yarn that referenced this pull request Oct 27, 2017
…arnpkg#4257)

**Summary**

Fixes yarnpkg#4251. Follow up to yarnpkg#4088. Instead of just checking whether the
target is a valid directory, we now check if it contains a
`package.json` file too. This is still different from `npm`'s behavior.
Apparently, `npm` fetches the package info upfront to favor dist-tags
over directories but this comes at a distinct performance penalty and
makes static, deterministic resolution impossible so we are now
deprecating the implicit `file:` protocol in patterns. After a certain
point, we'll remove this code and will require everyone to use `file:`
or at least one of the following path identifiers: `./`, `../`. `/`.

**Test plan**

Updated the existing test for warning check and added a new test for
invalid directories.
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.

5 participants