npm-pick-manifest
Advanced tools
Comparing version 5.0.0 to 6.0.0
@@ -5,2 +5,22 @@ # Changelog | ||
## [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) | ||
@@ -7,0 +27,0 @@ |
235
index.js
'use strict' | ||
const figgyPudding = require('figgy-pudding') | ||
const npa = require('npm-package-arg') | ||
const semver = require('semver') | ||
const { checkEngine } = require('npm-install-checks') | ||
const PickerOpts = figgyPudding({ | ||
defaultTag: { default: 'latest' }, | ||
enjoyBy: {}, | ||
includeDeprecated: { default: false } | ||
}) | ||
const engineOk = (manifest, npmVersion, nodeVersion) => { | ||
try { | ||
checkEngine(manifest, npmVersion, nodeVersion) | ||
return true | ||
} catch (_) { | ||
return false | ||
} | ||
} | ||
module.exports = pickManifest | ||
function pickManifest (packument, wanted, opts) { | ||
opts = PickerOpts(opts) | ||
const time = opts.enjoyBy && packument.time && +(new Date(opts.enjoyBy)) | ||
const spec = npa.resolve(packument.name, wanted) | ||
const isBefore = (verTimes, ver, time) => | ||
!verTimes || !verTimes[ver] || Date.parse(verTimes[ver]) <= time | ||
const pickManifest = (packument, wanted, opts) => { | ||
const { | ||
defaultTag = 'latest', | ||
before = null, | ||
nodeVersion = process.version, | ||
npmVersion = null, | ||
includeStaged = false | ||
} = opts | ||
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 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) | ||
} | ||
} | ||
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 | ||
// 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 | ||
} | ||
let target | ||
// ok, sort based on our heuristics, and pick the best fit | ||
const range = type === 'range' ? wanted : '*' | ||
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') | ||
// 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 | ||
} | ||
} | ||
const tagVersion = distTags[opts.defaultTag] | ||
// 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 ( | ||
!target && | ||
tagVersion && | ||
packument.versions[tagVersion] && | ||
enjoyableBy(tagVersion) && | ||
semver.satisfies(tagVersion, wanted, true) | ||
) { | ||
target = tagVersion | ||
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 && !opts.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) | ||
} | ||
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 }) | ||
}) | ||
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 | ||
} | ||
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}${ | ||
opts.enjoyBy | ||
? ` with an Enjoy By date of ${ | ||
new Date(opts.enjoyBy).toLocaleString() | ||
}. Maybe try a different date?` | ||
: '' | ||
}` | ||
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}` : '') | ||
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' | ||
} | ||
const isForbidden = picked && !!restricted[picked.version] | ||
const polMsg = isForbidden ? policyRestrictions.message : '' | ||
err.name = packument.name | ||
err.type = type | ||
err.wanted = wanted | ||
err.versions = versions | ||
err.distTags = distTags | ||
err.defaultTag = opts.defaultTag | ||
throw err | ||
} else { | ||
return manifest | ||
} | ||
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 | ||
}) | ||
} |
{ | ||
"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.", | ||
@@ -10,4 +10,6 @@ "main": "index.js", | ||
"scripts": { | ||
"coverage": "tap", | ||
"lint": "standard", | ||
"postrelease": "npm publish", | ||
"posttest": "standard", | ||
"posttest": "npm run lint", | ||
"prepublishOnly": "git push --follow-tags", | ||
@@ -31,3 +33,3 @@ "prerelease": "npm t", | ||
"dependencies": { | ||
"figgy-pudding": "^3.5.1", | ||
"npm-install-checks": "^4.0.0", | ||
"npm-package-arg": "^8.0.0", | ||
@@ -34,0 +36,0 @@ "semver": "^7.0.0" |
@@ -1,2 +0,2 @@ | ||
# npm-pick-manifest [![npm version](https://img.shields.io/npm/v/npm-pick-manifest.svg)](https://npm.im/npm-pick-manifest) [![license](https://img.shields.io/npm/l/npm-pick-manifest.svg)](https://npm.im/npm-pick-manifest) [![Travis](https://img.shields.io/travis/npm/npm-pick-manifest.svg)](https://travis-ci.org/npm/npm-pick-manifest) [![AppVeyor](https://ci.appveyor.com/api/projects/status/github/npm/npm-pick-manifest?svg=true)](https://ci.appveyor.com/project/npm/npm-pick-manifest) [![Coverage Status](https://coveralls.io/repos/github/npm/npm-pick-manifest/badge.svg?branch=latest)](https://coveralls.io/github/npm/npm-pick-manifest?branch=latest) | ||
# npm-pick-manifest [![npm version](https://img.shields.io/npm/v/npm-pick-manifest.svg)](https://npm.im/npm-pick-manifest) [![license](https://img.shields.io/npm/l/npm-pick-manifest.svg)](https://npm.im/npm-pick-manifest) [![Travis](https://img.shields.io/travis/npm/npm-pick-manifest.svg)](https://travis-ci.org/npm/npm-pick-manifest) [![Coverage Status](https://coveralls.io/repos/github/npm/npm-pick-manifest/badge.svg?branch=latest)](https://coveralls.io/github/npm/npm-pick-manifest?branch=latest) | ||
@@ -14,3 +14,2 @@ [`npm-pick-manifest`](https://github.com/npm/npm-pick-manifest) is a standalone | ||
* [Features](#features) | ||
* [Contributing](#contributing) | ||
* [API](#api) | ||
@@ -33,13 +32,8 @@ * [`pickManifest()`](#pick-manifest) | ||
* 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. | ||
### Contributing | ||
The npm-pick-manifest team enthusiastically welcomes contributions and project participation! | ||
There's a bunch of things you can do if you want to contribute! The [Contributor | ||
Guide](CONTRIBUTING.md) has all the information you need for everything from | ||
reporting bugs to contributing entire new features. Please don't hesitate to | ||
jump in if you'd like to, or even ask us questions if something isn't clear. | ||
### API | ||
@@ -49,3 +43,3 @@ | ||
Returns the manifest that matches `selector`, or throws an error. | ||
Returns the manifest that best matches `selector`, or throws an error. | ||
@@ -71,18 +65,71 @@ Packuments are anything returned by metadata URLs from the npm registry. That | ||
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. | ||
#### <a name="pick-manifest-options"></a> Options | ||
If `opts.enjoyBy` 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 | ||
`enjoyBy` are considered. | ||
All options are optional. | ||
If `opts.includeDeprecated` passed in as true, deprecated versions will be | ||
selected. By default, deprecated versions other than `defaultTag` are ignored. | ||
* `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. |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
18876
134
132
1
+ Addednpm-install-checks@^4.0.0
+ Addednpm-install-checks@4.0.0(transitive)
- Removedfiggy-pudding@^3.5.1
- Removedfiggy-pudding@3.5.2(transitive)