diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..60aff9b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,94 @@ +--- +################################################################################ +# Template - Node CI +# +# Description: +# This contains the basic information to: install dependencies, run tests, +# get coverage, and run linting on a nodejs project. This template will run +# over the MxN matrix of all operating systems, and all current LTS versions +# of NodeJS. +# +# Dependencies: +# This template assumes that your project is using the `tap` module for +# testing. If you're not using this module, then the step that runs your +# coverage will need to be adjusted. +# +################################################################################ +name: Node CI + +on: [push, pull_request] + +jobs: + build: + strategy: + fail-fast: false + matrix: + node-version: [10.x, 12.x, 13.x] + os: [ubuntu-latest, windows-latest, macOS-latest] + + runs-on: ${{ matrix.os }} + + steps: + # Checkout the repository + - uses: actions/checkout@v2 + # Installs the specific version of Node.js + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + + ################################################################################ + # Install Dependencies + # + # ASSUMPTIONS: + # - The project has a package-lock.json file + # + # Simply run the tests for the project. + ################################################################################ + - name: Install dependencies + run: npm ci + + ################################################################################ + # Run Testing + # + # ASSUMPTIONS: + # - The project has `tap` as a devDependency + # - There is a script called "test" in the package.json + # + # Simply run the tests for the project. + ################################################################################ + - name: Run tests + run: npm test + + ################################################################################ + # Run coverage check + # + # ASSUMPTIONS: + # - The project has `tap` as a devDependency + # - There is a script called "coverage" in the package.json + # + # Coverage should only be posted once, we are choosing the latest LTS of + # node, and ubuntu as the matrix point to post coverage from. We limit + # to the 'push' event so that coverage ins't posted twice from the + # pull-request event, and push event (line 3). + ################################################################################ + - name: Run coverage report + if: github.event_name == 'push' && matrix.node-version == '12.x' && matrix.os == 'ubuntu-latest' + run: npm run coverage + env: + # The environment variable name is leveraged by `tap` + COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + + ################################################################################ + # Run linting + # + # ASSUMPTIONS: + # - There is a script called "lint" in the package.json + # + # We run linting AFTER we run testing and coverage checks, because if a step + # fails in an GitHub Action, all other steps are not run. We don't want to + # fail to run tests or coverage because of linting. It should be the lowest + # priority of all the steps. + ################################################################################ + - name: Run linter + run: npm run lint diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ac3bd33..0000000 --- a/.travis.yml +++ /dev/null @@ -1,5 +0,0 @@ -language: node_js -node_js: - - node - - 12 - - 10 diff --git a/CHANGELOG.md b/CHANGELOG.md index bd65c62..93a44f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,26 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [6.0.0](https://github.com/npm/npm-pick-manifest/compare/v5.0.0...v6.0.0) (2020-02-18) + + +### ⚠ BREAKING CHANGES + +* 'enjoyBy' is no longer an acceptable alias. + +### Features + +* add GitHub Actions file for ci ([8985247](https://github.com/npm/npm-pick-manifest/commit/898524727fa157f46fdf4eb0c11148ae4808226b)) + + +### Bug Fixes + +* Handle edge cases around before:Date and filtering staged publishes ([ed2f92e](https://github.com/npm/npm-pick-manifest/commit/ed2f92e7fdc9cc7836b13ebc73e17d8fd296a07e)) +* remove figgy pudding ([c24fed2](https://github.com/npm/npm-pick-manifest/commit/c24fed25b8f77fbbcc3107030f2dfed55fa54222)) +* remove outdated cruft from docs ([aae7ef7](https://github.com/npm/npm-pick-manifest/commit/aae7ef7625ddddbac0548287e5d57b8f76593322)) +* update some missing {loose:true} semver configs ([4015424](https://github.com/npm/npm-pick-manifest/commit/40154244a3fe1af86462bc1d6165199fc3315c10)) +* Use canonical 'before' config name ([029de59](https://github.com/npm/npm-pick-manifest/commit/029de59bda6d3376f03760a00efe4ac9d997c623)) + ## [5.0.0](https://github.com/npm/npm-pick-manifest/compare/v4.0.0...v5.0.0) (2019-12-15) diff --git a/README.md b/README.md index 7c23093..fd17775 100644 --- a/README.md +++ b/README.md @@ -28,14 +28,17 @@ fetch('https://registry.npmjs.org/npm-pick-manifest').then(res => { ### Features -* Uses npm's exact semver resolution algorithm -* Supports ranges, tags, and versions +* Uses npm's exact [semver resolution algorithm](http://npm.im/semver). +* Supports ranges, tags, and versions. +* Prefers non-deprecated versions to deprecated versions. +* Prefers versions whose `engines` requirements are satisfied over those + that will raise a warning or error at install time. ### API #### `> pickManifest(packument, selector, [opts]) -> manifest` -Returns the manifest that matches `selector`, or throws an error. +Returns the manifest that best matches `selector`, or throws an error. Packuments are anything returned by metadata URLs from the npm registry. That is, they're objects with the following shape (only fields used by @@ -56,22 +59,73 @@ is, they're objects with the following shape (only fields used by } ``` -The algorithm will follow npm's algorithm for semver resolution, and only `tag`, -`range`, and `version` selectors are supported. +The algorithm will follow npm's algorithm for semver resolution, and only +`tag`, `range`, and `version` selectors are supported. The function will throw `ETARGET` if there was no matching manifest, and `ENOVERSIONS` if the packument object has no valid versions in `versions`. If the only matching manifest is included in a `policyRestrictions` section of the packument, then an `E403` is raised. -If `opts.defaultTag` is provided, it will be used instead of `latest`. That is, -if that tag matches the selector, it will be used, even if a higher available -version matches the range. - -If `opts.before` is provided, it should be something that can be passed to -`new Date(x)`, such as a `Date` object or a timestamp string. It will be -used to filter the selected versions such that only versions less than or -equal to `before` are considered. - -If `opts.includeDeprecated` passed in as true, deprecated versions will be -selected. By default, deprecated versions other than `defaultTag` are ignored. +#### Options + +All options are optional. + +* `includeStaged` - Boolean, default `false`. Include manifests in the + `stagedVersions.versions` set, to support installing [staged + packages](https://github.com/npm/rfcs/pull/92) when appropriate. Note + that staged packages are always treated as lower priority than actual + publishes, even when `includeStaged` is set. +* `defaultTag` - String, default `'latest'`. The default `dist-tag` to + install when no specifier is provided. Note that the version indicated + by this specifier will be given top priority if it matches a supplied + semver range. +* `before` - String, Date, or Number, default `null`. This is passed to + `new Date()`, so anything that works there will be valid. Do not + consider _any_ manifests that were published after the date indicated. + Note that this is only relevant when the packument includes a `time` + field listing the publish date of all the packages. +* `nodeVersion` - String, default `process.version`. The Node.js version + to use when checking manifests for `engines` requirement satisfaction. +* `npmVersion` - String, default `null`. The npm version to use when + checking manifest for `engines` requirement satisfaction. (If `null`, + then this particular check is skipped.) + +### Algorithm + +1. Create list of all versions in `versions`, + `policyRestrictions.versions`, and (if `includeStaged` is set) + `stagedVersions.versions`. +2. If a `dist-tag` is requested, + 1. If the manifest is not after the specified `before` date, then + select that from the set. + 2. If the manifest is after the specified `before` date, then re-start + the selection looking for the highest SemVer range that is equal to + or less than the `dist-tag` target. +3. If a specific version is requested, + 1. If the manifest is not after the specified `before` date, then + select the specified manifest. + 2. If the manifest is after the specified `before` date, then raise + `ETARGET` error. (NB: this is a breaking change from v5, where a + specified version would override the `before` setting.) +4. (At this point we know a range is requested.) +5. If the `defaultTag` refers to a `dist-tag` that satisfies the range (or + if the range is `'*'` or `''`), and the manifest is published before the + `before` setting, then select that manifest. +6. If nothing is yet selected, sort by the following heuristics in order, + and select the top item: + 1. Prioritize versions that are not in `policyRestrictions` over those + that are. + 2. Prioritize published versions over staged versions. + 3. Prioritize versions that are not deprecated, and which have a + satisfied engines requirement, over those that are either deprecated + or have an engines mismatch. + 4. Prioritize versions that have a satisfied engines requirement over + those that do not. + 5. Prioritize versions that are not are not deprecated (but have a + mismatched engines requirement) over those that are deprecated. + 6. Prioritize higher SemVer precedence over lower SemVer precedence. +7. If no manifest was selected, raise an `ETARGET` error. +8. If the selected item is in the `policyRestrictions.versions` list, raise + an `E403` error. +9. Return the selected manifest. diff --git a/index.js b/index.js index 49c81c1..5fb1279 100644 --- a/index.js +++ b/index.js @@ -2,133 +2,154 @@ const npa = require('npm-package-arg') const semver = require('semver') +const { checkEngine } = require('npm-install-checks') + +const engineOk = (manifest, npmVersion, nodeVersion) => { + try { + checkEngine(manifest, npmVersion, nodeVersion) + return true + } catch (_) { + return false + } +} + +const isBefore = (verTimes, ver, time) => + !verTimes || !verTimes[ver] || Date.parse(verTimes[ver]) <= time -module.exports = pickManifest -function pickManifest (packument, wanted, opts = {}) { +const pickManifest = (packument, wanted, opts) => { const { defaultTag = 'latest', before = null, - includeDeprecated = false + nodeVersion = process.version, + npmVersion = null, + includeStaged = false } = opts - const time = before && packument.time && +(new Date(before)) - const spec = npa.resolve(packument.name, wanted) + const { name, time: verTimes } = packument + const versions = packument.versions || {} + const staged = (includeStaged && packument.stagedVersions && + packument.stagedVersions.versions) || {} + const restricted = (packument.policyRestrictions && + packument.policyRestrictions.versions) || {} + + const time = before && verTimes ? +(new Date(before)) : Infinity + const spec = npa.resolve(name, wanted || defaultTag) const type = spec.type - if (type === 'version' || type === 'range') { - wanted = semver.clean(wanted, true) || wanted - } const distTags = packument['dist-tags'] || {} - const versions = Object.keys(packument.versions || {}).filter(v => { - return semver.valid(v, true) - }) - const policyRestrictions = packument.policyRestrictions - const restrictedVersions = policyRestrictions - ? Object.keys(policyRestrictions.versions) : [] - function enjoyableBy (v) { - return !time || ( - packument.time[v] && time >= +(new Date(packument.time[v])) - ) + if (type !== 'tag' && type !== 'version' && type !== 'range') { + throw new Error('Only tag, version, and range are supported') } - let err - - if (!versions.length && !restrictedVersions.length) { - err = new Error(`No valid versions available for ${packument.name}`) - err.code = 'ENOVERSIONS' - err.name = packument.name - err.type = type - err.wanted = wanted - throw err + // if the type is 'tag', and not just the implicit default, then it must + // be that exactly, or nothing else will do. + if (wanted && type === 'tag') { + const ver = distTags[wanted] + // if the version in the dist-tags is before the before date, then + // we use that. Otherwise, we get the highest precedence version + // prior to the dist-tag. + if (isBefore(verTimes, ver, time)) { + return versions[ver] || staged[ver] || restricted[ver] + } else { + return pickManifest(packument, `<=${ver}`, opts) + } } - let target - - if (type === 'tag' && enjoyableBy(distTags[wanted])) { - target = distTags[wanted] - } else if (type === 'version') { - target = wanted - } else if (type !== 'range' && enjoyableBy(distTags[wanted])) { - throw new Error('Only tag, version, and range are supported') + // similarly, if a specific version, then only that version will do + if (wanted && type === 'version') { + const ver = semver.clean(wanted, { loose: true }) + const mani = versions[ver] || staged[ver] || restricted[ver] + return isBefore(verTimes, ver, time) ? mani : null } - const tagVersion = distTags[defaultTag] + // ok, sort based on our heuristics, and pick the best fit + const range = type === 'range' ? wanted : '*' - if ( - !target && - tagVersion && - packument.versions[tagVersion] && - enjoyableBy(tagVersion) && - semver.satisfies(tagVersion, wanted, true) - ) { - target = tagVersion + // if the range is *, then we prefer the 'latest' if available + const defaultVer = distTags[defaultTag] + if (defaultVer && (range === '*' || semver.satisfies(defaultVer, range, { loose: true }))) { + const mani = versions[defaultVer] + if (mani && isBefore(verTimes, defaultVer, time)) { + return mani + } } - if (!target && !includeDeprecated) { - const undeprecated = versions.filter(v => !packument.versions[v].deprecated && enjoyableBy(v) - ) - target = semver.maxSatisfying(undeprecated, wanted, true) - } - if (!target) { - const stillFresh = versions.filter(enjoyableBy) - target = semver.maxSatisfying(stillFresh, wanted, true) + // ok, actually have to sort the list and take the winner + const allEntries = Object.entries(versions) + .concat(Object.entries(staged)) + .concat(Object.entries(restricted)) + .filter(([ver, mani]) => isBefore(verTimes, ver, time)) + + if (!allEntries.length) { + throw Object.assign(new Error(`No valid versions available for ${name}`), { + code: 'ENOVERSIONS', + name, + type, + wanted, + versions: Object.keys(versions) + }) } - if (!target && wanted === '*' && enjoyableBy(tagVersion)) { - // This specific corner is meant for the case where - // someone is using `*` as a selector, but all versions - // are pre-releases, which don't match ranges at all. - target = tagVersion - } + const entries = allEntries.filter(([ver, mani]) => + semver.satisfies(ver, range, { loose: true })) + .sort((a, b) => { + const [vera, mania] = a + const [verb, manib] = b + const notrestra = !restricted[a] + const notrestrb = !restricted[b] + const notstagea = !staged[a] + const notstageb = !staged[b] + const notdepra = !mania.deprecated + const notdeprb = !manib.deprecated + const enginea = engineOk(mania, npmVersion, nodeVersion) + const engineb = engineOk(manib, npmVersion, nodeVersion) + // sort by: + // - not restricted + // - not staged + // - not deprecated and engine ok + // - engine ok + // - not deprecated + // - semver + return (notrestrb - notrestra) || + (notstageb - notstagea) || + ((notdeprb && engineb) - (notdepra && enginea)) || + (engineb - enginea) || + (notdeprb - notdepra) || + semver.rcompare(vera, verb, { loose: true }) + }) + + return entries[0] && entries[0][1] +} - if ( - !target && - time && - type === 'tag' && - distTags[wanted] && - !enjoyableBy(distTags[wanted]) - ) { - const stillFresh = versions.filter(v => - enjoyableBy(v) && semver.lte(v, distTags[wanted], true) - ).sort(semver.rcompare) - target = stillFresh[0] - } +module.exports = (packument, wanted, opts = {}) => { + const picked = pickManifest(packument, wanted, opts) + const policyRestrictions = packument.policyRestrictions + const restricted = (policyRestrictions && policyRestrictions.versions) || {} - if (!target && restrictedVersions) { - target = semver.maxSatisfying(restrictedVersions, wanted, true) + if (picked && !restricted[picked.version]) { + return picked } - const manifest = ( - target && - packument.versions[target] - ) - if (!manifest) { - // Check if target is forbidden - const isForbidden = target && policyRestrictions && policyRestrictions.versions[target] - const pckg = `${packument.name}@${wanted}${ - before - ? ` with an Enjoy By date of ${ - new Date(before).toLocaleString() - }. Maybe try a different date?` - : '' - }` - - if (isForbidden) { - err = new Error(`Could not download ${pckg} due to policy violations.\n${policyRestrictions.message}\n`) - err.code = 'E403' - } else { - err = new Error(`No matching version found for ${pckg}.`) - err.code = 'ETARGET' - } - - err.name = packument.name - err.type = type - err.wanted = wanted - err.versions = versions - err.distTags = distTags - err.defaultTag = defaultTag - throw err - } else { - return manifest - } + const { before = null, defaultTag = 'latest' } = opts + const bstr = before ? new Date(before).toLocaleString() : '' + const { name } = packument + const pckg = `${name}@${wanted}` + + (before ? ` with a date before ${bstr}` : '') + + const isForbidden = picked && !!restricted[picked.version] + const polMsg = isForbidden ? policyRestrictions.message : '' + + const msg = !isForbidden ? `No matching version found for ${pckg}.` + : `Could not download ${pckg} due to policy violations:\n${polMsg}` + + const code = isForbidden ? 'E403' : 'ETARGET' + throw Object.assign(new Error(msg), { + code, + type: npa.resolve(packument.name, wanted).type, + wanted, + versions: Object.keys(packument.versions), + name, + distTags: packument['dist-tags'], + defaultTag + }) } diff --git a/package-lock.json b/package-lock.json index 893000b..1b45816 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "npm-pick-manifest", - "version": "5.0.0", + "version": "6.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1683,11 +1683,6 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, - "figgy-pudding": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", - "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==" - }, "figures": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", @@ -3414,6 +3409,21 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, + "npm-install-checks": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-4.0.0.tgz", + "integrity": "sha512-09OmyDkNLYwqKPOnbI8exiOZU2GVVmQp7tgez2BPi5OZC8M82elDAps7sxC4l//uSUtotWqoEIDwjRvWH4qz8w==", + "requires": { + "semver": "^7.1.1" + }, + "dependencies": { + "semver": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.1.3.tgz", + "integrity": "sha512-ekM0zfiA9SCBlsKa2X1hxyxiI4L3B6EbVJkkdgQXnSEEaHlGdvyodMruTiulSRWMMB4NeIuYNMC9rTKTz97GxA==" + } + } + }, "npm-package-arg": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-8.0.0.tgz", diff --git a/package.json b/package.json index 8f2b1c6..fce94e6 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,16 @@ { "name": "npm-pick-manifest", - "version": "5.0.0", + "version": "6.0.0", "description": "Resolves a matching manifest from a package metadata document according to standard npm semver resolution rules.", "main": "index.js", "files": [ "*.js" ], "scripts": { + "coverage": "tap", + "lint": "standard", "postrelease": "npm publish", - "posttest": "standard", + "posttest": "npm run lint", "prepublishOnly": "git push --follow-tags", "prerelease": "npm t", "release": "standard-version -s", @@ -27,7 +29,7 @@ }, "license": "ISC", "dependencies": { - "figgy-pudding": "^3.5.1", + "npm-install-checks": "^4.0.0", "npm-package-arg": "^8.0.0", "semver": "^7.0.0" }, diff --git a/test/index.js b/test/index.js index 2ae90c7..72eeb87 100644 --- a/test/index.js +++ b/test/index.js @@ -169,6 +169,13 @@ test('E403 if version is forbidden, provided a minor version', t => { test('E403 if version is forbidden, provided a major version', t => { const metadata = { + 'dist-tags': { + latest: '2.0.5', + // note: this SHOULD not be allowed, but it's possible that + // a registry proxy may implement policyRestrictions without + // properly modifying dist-tags when it does so. + borked: '2.1.5' + }, policyRestrictions: { versions: { '1.0.0': { version: '1.0.0' }, @@ -184,6 +191,9 @@ test('E403 if version is forbidden, provided a major version', t => { t.throws(() => { pickManifest(metadata, '1') }, { code: 'E403' }, 'got correct error on match failure') + t.throws(() => { + pickManifest(metadata, 'borked') + }, { code: 'E403' }, 'got correct error on policy restricted dist-tag') t.done() }) @@ -239,7 +249,17 @@ test('* ranges use `defaultTag` if no versions match', t => { t.equal( pickManifest(metadata, '*').version, '1.0.0-pre.0', - 'defaulted to `latest`' + 'defaulted to `latest` when wanted is *' + ) + t.equal( + pickManifest(metadata, '', { defaultTag: 'beta' }).version, + '2.0.0-beta.0', + 'used defaultTag for all-prerelease ""' + ) + t.equal( + pickManifest(metadata, '').version, + '1.0.0-pre.0', + 'defaulted to `latest` when wanted is ""' ) t.done() }) @@ -320,7 +340,7 @@ test('matches deprecated versions if we have to', t => { t.done() }) -test('accepts opts.includeDeprecated option to disable skipping', t => { +test('will use deprecated version if no other suitable match', t => { const metadata = { versions: { '1.0.0': { version: '1.0.0' }, @@ -329,9 +349,7 @@ test('accepts opts.includeDeprecated option to disable skipping', t => { '2.0.0': { version: '2.0.0' } } } - const manifest = pickManifest(metadata, '^1.0.0', { - includeDeprecated: true - }) + const manifest = pickManifest(metadata, '^1.1.0') t.equal(manifest.version, '1.1.0', 'picked the right manifest') t.done() }) @@ -347,6 +365,7 @@ test('accepts opts.before option to do date-based cutoffs', t => { '1.0.0': '2018-01-01T00:00:00.000Z', '2.0.0': '2018-01-02T00:00:00.000Z', '2.0.1': '2018-01-03T00:00:00.000Z', + '2.0.2': '2018-01-03T00:00:00.123Z', '3.0.0': '2018-01-04T00:00:00.000Z' }, versions: { @@ -367,10 +386,23 @@ test('accepts opts.before option to do date-based cutoffs', t => { }) t.equal(manifest.version, '2.0.0', 'tag specs pick highest before dist-tag but within the range in question') - manifest = pickManifest(metadata, '3.0.0', { - before: '2018-01-02' + manifest = pickManifest(metadata, '*', { + before: Date.parse('2018-01-03T00:00:00.000Z') }) - t.equal(manifest.version, '3.0.0', 'requesting specific version overrides') + t.equal(manifest.version, '2.0.1', 'numeric timestamp supported with ms accuracy') + + manifest = pickManifest(metadata, '*', { + before: new Date('2018-01-03T00:00:00.000Z') + }) + t.equal(manifest.version, '2.0.1', 'date obj supported with ms accuracy') + + t.throws(() => pickManifest(metadata, '3.0.0', { + before: '2018-01-02' + }), { code: 'ETARGET' }, 'version filtered out by date') + + t.throws(() => pickManifest(metadata, '', { + before: '1918-01-02' + }), { code: 'ENOVERSIONS' }, 'all version filtered out by date') manifest = pickManifest(metadata, '^2', { before: '2018-01-02' @@ -381,6 +413,64 @@ test('accepts opts.before option to do date-based cutoffs', t => { pickManifest(metadata, '^3', { before: '2018-01-02' }) - }, /Enjoy By/, 'range for out-of-range spec fails even if defaultTag avail') + }, /with a date before/, 'range for out-of-range spec fails even if defaultTag avail') t.done() }) + +test('prefers versions that satisfy the engines requirement', t => { + const pack = { + versions: { + '1.0.0': { version: '1.0.0', engines: { node: '>=4' } }, + '1.1.0': { version: '1.1.0', engines: { node: '>=6' } }, + '1.2.0': { version: '1.2.0', engines: { node: '>=8' } }, + '1.3.0': { version: '1.3.0', engines: { node: '>=10' } }, + '1.4.0': { version: '1.4.0', engines: { node: '>=12' } }, + '1.5.0': { version: '1.5.0', engines: { node: '>=14' } } + } + } + + t.equal(pickManifest(pack, '1.x', { nodeVersion: '14.0.0' }).version, '1.5.0') + t.equal(pickManifest(pack, '1.x', { nodeVersion: '12.0.0' }).version, '1.4.0') + t.equal(pickManifest(pack, '1.x', { nodeVersion: '10.0.0' }).version, '1.3.0') + t.equal(pickManifest(pack, '1.x', { nodeVersion: '8.0.0' }).version, '1.2.0') + t.equal(pickManifest(pack, '1.x', { nodeVersion: '6.0.0' }).version, '1.1.0') + t.equal(pickManifest(pack, '1.x', { nodeVersion: '4.0.0' }).version, '1.0.0') + t.equal(pickManifest(pack, '1.x', { nodeVersion: '1.2.3' }).version, '1.5.0', + 'if no engine-match exists, just use whatever') + t.end() +}) + +test('support selecting staged versions if allowed by options', t => { + const pack = { + 'dist-tags': { + latest: '1.0.0', + // note: this SHOULD not be allowed, but it's possible that + // a registry proxy may implement stagedVersions without + // properly modifying dist-tags when it does so. + borked: '2.0.0' + }, + versions: { + '1.0.0': { version: '1.0.0' } + }, + stagedVersions: { + versions: { + '2.0.0': { version: '2.0.0' } + } + }, + time: { + '1.0.0': '2018-01-03T00:00:00.000Z' + } + } + + t.equal(pickManifest(pack, '1||2').version, '1.0.0') + t.equal(pickManifest(pack, '1||2', { includeStaged: true }).version, '1.0.0') + t.equal(pickManifest(pack, '2', { includeStaged: true }).version, '2.0.0') + t.equal(pickManifest(pack, '2', { + includeStaged: true, + before: '2018-01-01' + }).version, '2.0.0', 'version without time entry not subject to before filtering') + t.throws(() => pickManifest(pack, '2'), { code: 'ETARGET' }) + t.throws(() => pickManifest(pack, 'borked'), { code: 'ETARGET' }) + + t.end() +})