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()
+})