diff --git a/__tests__/commands/add.js b/__tests__/commands/add.js index 26890aec45..6ddee8784f 100644 --- a/__tests__/commands/add.js +++ b/__tests__/commands/add.js @@ -986,3 +986,41 @@ test.concurrent('preserves unaffected bin links after adding to workspace packag expect(await fs.exists(`${config.cwd}/packages/workspace-2/node_modules/.bin/workspace-1`)).toEqual(true); }); }); + +test.concurrent('installs "latest" instead of maxSatisfying if it satisfies requested pattern', (): Promise => { + // Scenario: + // If a registry contains versions [1.0.0, 1.0.1, 1.0.2] and latest:1.0.1 + // (note that "latest" is not the "newest" version) + // If yarn add ^1.0.0 is run, it should choose `1.0.1` because it is "latest" and satisfies the range, + // not `1.0.2` even though it is newer. + // This is behavior defined by the NPM implementation. See: + // * https://github.com/yarnpkg/yarn/issues/3560 + // * https://git.io/vFmau + // + // In this test, `ui-select` has a max version of `0.20.0` but a `latest:0.19.8` + return runAdd(['ui-select@^0.X'], {}, 'latest-version-in-package', async (config, reporter, previousAdd) => { + const lockfile = explodeLockfile(await fs.readFile(path.join(config.cwd, 'yarn.lock'))); + const patternIndex = lockfile.indexOf('ui-select@^0.X:'); + const versionIndex = patternIndex + 1; + const actualVersion = lockfile[versionIndex]; + + expect(actualVersion).toContain('0.19.8'); + }); +}); + +test.concurrent('installs "latest" instead of maxSatisfying if no requested pattern', (): Promise => { + // Scenario: + // If a registry contains versions [1.0.0, 1.0.1, 1.0.2] and latest:1.0.1 + // If `yarn add` is run, it should choose `1.0.1` because it is "latest", not `1.0.2` even though it is newer. + // In other words, when no range is explicitely given, Yarn should choose "latest". + // + // In this test, `ui-select` has a max version of `0.20.0` but a `latest:0.19.8` + return runAdd(['ui-select'], {}, 'latest-version-in-package', async (config, reporter, previousAdd) => { + const lockfile = explodeLockfile(await fs.readFile(path.join(config.cwd, 'yarn.lock'))); + const patternIndex = lockfile.indexOf('ui-select@^0.19.8:'); + const versionIndex = patternIndex + 1; + const actualVersion = lockfile[versionIndex]; + + expect(actualVersion).toContain('0.19.8'); + }); +}); diff --git a/__tests__/fixtures/request-cache/GET/registry.yarnpkg.com/ui-select.bin b/__tests__/fixtures/request-cache/GET/registry.yarnpkg.com/ui-select.bin new file mode 100644 index 0000000000..12d49f7b91 Binary files /dev/null and b/__tests__/fixtures/request-cache/GET/registry.yarnpkg.com/ui-select.bin differ diff --git a/__tests__/fixtures/request-cache/GET/registry.yarnpkg.com/ui-select/-/ui-select-0.19.8.tgz.bin b/__tests__/fixtures/request-cache/GET/registry.yarnpkg.com/ui-select/-/ui-select-0.19.8.tgz.bin new file mode 100644 index 0000000000..0d28de6e5c Binary files /dev/null and b/__tests__/fixtures/request-cache/GET/registry.yarnpkg.com/ui-select/-/ui-select-0.19.8.tgz.bin differ diff --git a/__tests__/fixtures/request-cache/GET/registry.yarnpkg.com/ui-select/-/ui-select-0.20.0.tgz.bin b/__tests__/fixtures/request-cache/GET/registry.yarnpkg.com/ui-select/-/ui-select-0.20.0.tgz.bin new file mode 100644 index 0000000000..fc9052b7c5 Binary files /dev/null and b/__tests__/fixtures/request-cache/GET/registry.yarnpkg.com/ui-select/-/ui-select-0.20.0.tgz.bin differ diff --git a/src/resolvers/registries/npm-resolver.js b/src/resolvers/registries/npm-resolver.js index b17fa7c9e7..a2ed0651e7 100644 --- a/src/resolvers/registries/npm-resolver.js +++ b/src/resolvers/registries/npm-resolver.js @@ -14,6 +14,7 @@ const inquirer = require('inquirer'); const tty = require('tty'); const invariant = require('invariant'); const path = require('path'); +const semver = require('semver'); const NPM_REGISTRY = /http[s]:\/\/registry.npmjs.org/g; const NPM_REGISTRY_ID = 'npm'; @@ -41,6 +42,14 @@ export default class NpmResolver extends RegistryResolver { range = body['dist-tags'][range]; } + // If the latest tag in the registry satisfies the requested range, then use that. + // Otherwise we will fall back to semver maxSatisfying. + // This mimics logic in NPM. See issue #3560 + const latestVersion = body['dist-tags'] ? body['dist-tags'].latest : undefined; + if (latestVersion && semver.satisfies(latestVersion, range)) { + return body.versions[latestVersion]; + } + const satisfied = await config.resolveConstraints(Object.keys(body.versions), range); if (satisfied) { return body.versions[satisfied];