219 lines
6.7 KiB
JavaScript
219 lines
6.7 KiB
JavaScript
|
'use strict'
|
||
|
|
||
|
const npa = require('npm-package-arg')
|
||
|
const semver = require('semver')
|
||
|
const { checkEngine } = require('npm-install-checks')
|
||
|
const normalizeBin = require('npm-normalize-package-bin')
|
||
|
|
||
|
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
|
||
|
|
||
|
const avoidSemverOpt = { includePrerelease: true, loose: true }
|
||
|
const shouldAvoid = (ver, avoid) =>
|
||
|
avoid && semver.satisfies(ver, avoid, avoidSemverOpt)
|
||
|
|
||
|
const decorateAvoid = (result, avoid) =>
|
||
|
result && shouldAvoid(result.version, avoid)
|
||
|
? { ...result, _shouldAvoid: true }
|
||
|
: result
|
||
|
|
||
|
const pickManifest = (packument, wanted, opts) => {
|
||
|
const {
|
||
|
defaultTag = 'latest',
|
||
|
before = null,
|
||
|
nodeVersion = process.version,
|
||
|
npmVersion = null,
|
||
|
includeStaged = false,
|
||
|
avoid = null,
|
||
|
avoidStrict = false,
|
||
|
} = opts
|
||
|
|
||
|
const { name, time: verTimes } = packument
|
||
|
const versions = packument.versions || {}
|
||
|
|
||
|
if (avoidStrict) {
|
||
|
const looseOpts = {
|
||
|
...opts,
|
||
|
avoidStrict: false,
|
||
|
}
|
||
|
|
||
|
const result = pickManifest(packument, wanted, looseOpts)
|
||
|
if (!result || !result._shouldAvoid) {
|
||
|
return result
|
||
|
}
|
||
|
|
||
|
const caret = pickManifest(packument, `^${result.version}`, looseOpts)
|
||
|
if (!caret || !caret._shouldAvoid) {
|
||
|
return {
|
||
|
...caret,
|
||
|
_outsideDependencyRange: true,
|
||
|
_isSemVerMajor: false,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const star = pickManifest(packument, '*', looseOpts)
|
||
|
if (!star || !star._shouldAvoid) {
|
||
|
return {
|
||
|
...star,
|
||
|
_outsideDependencyRange: true,
|
||
|
_isSemVerMajor: true,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
throw Object.assign(new Error(`No avoidable versions for ${name}`), {
|
||
|
code: 'ETARGET',
|
||
|
name,
|
||
|
wanted,
|
||
|
avoid,
|
||
|
before,
|
||
|
versions: Object.keys(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
|
||
|
const distTags = packument['dist-tags'] || {}
|
||
|
|
||
|
if (type !== 'tag' && type !== 'version' && type !== 'range') {
|
||
|
throw new Error('Only tag, version, and range are supported')
|
||
|
}
|
||
|
|
||
|
// 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 decorateAvoid(versions[ver] || staged[ver] || restricted[ver], avoid)
|
||
|
} else {
|
||
|
return pickManifest(packument, `<=${ver}`, opts)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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) ? decorateAvoid(mani, avoid) : null
|
||
|
}
|
||
|
|
||
|
// ok, sort based on our heuristics, and pick the best fit
|
||
|
const range = type === 'range' ? wanted : '*'
|
||
|
|
||
|
// if the range is *, then we prefer the 'latest' if available
|
||
|
// but skip this if it should be avoided, in that case we have
|
||
|
// to try a little harder.
|
||
|
const defaultVer = distTags[defaultTag]
|
||
|
if (defaultVer &&
|
||
|
(range === '*' || semver.satisfies(defaultVer, range, { loose: true })) &&
|
||
|
!shouldAvoid(defaultVer, avoid)) {
|
||
|
const mani = versions[defaultVer]
|
||
|
if (mani && isBefore(verTimes, defaultVer, time)) {
|
||
|
return mani
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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 versions available for ${name}`), {
|
||
|
code: 'ENOVERSIONS',
|
||
|
name,
|
||
|
type,
|
||
|
wanted,
|
||
|
before,
|
||
|
versions: Object.keys(versions),
|
||
|
})
|
||
|
}
|
||
|
|
||
|
const sortSemverOpt = { loose: 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 notavoida = !shouldAvoid(vera, avoid)
|
||
|
const notavoidb = !shouldAvoid(verb, avoid)
|
||
|
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 an avoided version
|
||
|
// - not restricted
|
||
|
// - not staged
|
||
|
// - not deprecated and engine ok
|
||
|
// - engine ok
|
||
|
// - not deprecated
|
||
|
// - semver
|
||
|
return (notavoidb - notavoida) ||
|
||
|
(notrestrb - notrestra) ||
|
||
|
(notstageb - notstagea) ||
|
||
|
((notdeprb && engineb) - (notdepra && enginea)) ||
|
||
|
(engineb - enginea) ||
|
||
|
(notdeprb - notdepra) ||
|
||
|
semver.rcompare(vera, verb, sortSemverOpt)
|
||
|
})
|
||
|
|
||
|
return decorateAvoid(entries[0] && entries[0][1], avoid)
|
||
|
}
|
||
|
|
||
|
module.exports = (packument, wanted, opts = {}) => {
|
||
|
const mani = pickManifest(packument, wanted, opts)
|
||
|
const picked = mani && normalizeBin(mani)
|
||
|
const policyRestrictions = packument.policyRestrictions
|
||
|
const restricted = (policyRestrictions && policyRestrictions.versions) || {}
|
||
|
|
||
|
if (picked && !restricted[picked.version]) {
|
||
|
return picked
|
||
|
}
|
||
|
|
||
|
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,
|
||
|
})
|
||
|
}
|