[6a3a178] | 1 | 'use strict'
|
---|
| 2 |
|
---|
| 3 | const npa = require('npm-package-arg')
|
---|
| 4 | const semver = require('semver')
|
---|
| 5 | const { checkEngine } = require('npm-install-checks')
|
---|
| 6 | const normalizeBin = require('npm-normalize-package-bin')
|
---|
| 7 |
|
---|
| 8 | const engineOk = (manifest, npmVersion, nodeVersion) => {
|
---|
| 9 | try {
|
---|
| 10 | checkEngine(manifest, npmVersion, nodeVersion)
|
---|
| 11 | return true
|
---|
| 12 | } catch (_) {
|
---|
| 13 | return false
|
---|
| 14 | }
|
---|
| 15 | }
|
---|
| 16 |
|
---|
| 17 | const isBefore = (verTimes, ver, time) =>
|
---|
| 18 | !verTimes || !verTimes[ver] || Date.parse(verTimes[ver]) <= time
|
---|
| 19 |
|
---|
| 20 | const avoidSemverOpt = { includePrerelease: true, loose: true }
|
---|
| 21 | const shouldAvoid = (ver, avoid) =>
|
---|
| 22 | avoid && semver.satisfies(ver, avoid, avoidSemverOpt)
|
---|
| 23 |
|
---|
| 24 | const decorateAvoid = (result, avoid) =>
|
---|
| 25 | result && shouldAvoid(result.version, avoid)
|
---|
| 26 | ? { ...result, _shouldAvoid: true }
|
---|
| 27 | : result
|
---|
| 28 |
|
---|
| 29 | const pickManifest = (packument, wanted, opts) => {
|
---|
| 30 | const {
|
---|
| 31 | defaultTag = 'latest',
|
---|
| 32 | before = null,
|
---|
| 33 | nodeVersion = process.version,
|
---|
| 34 | npmVersion = null,
|
---|
| 35 | includeStaged = false,
|
---|
| 36 | avoid = null,
|
---|
| 37 | avoidStrict = false
|
---|
| 38 | } = opts
|
---|
| 39 |
|
---|
| 40 | const { name, time: verTimes } = packument
|
---|
| 41 | const versions = packument.versions || {}
|
---|
| 42 |
|
---|
| 43 | if (avoidStrict) {
|
---|
| 44 | const looseOpts = {
|
---|
| 45 | ...opts,
|
---|
| 46 | avoidStrict: false
|
---|
| 47 | }
|
---|
| 48 |
|
---|
| 49 | const result = pickManifest(packument, wanted, looseOpts)
|
---|
| 50 | if (!result || !result._shouldAvoid) {
|
---|
| 51 | return result
|
---|
| 52 | }
|
---|
| 53 |
|
---|
| 54 | const caret = pickManifest(packument, `^${result.version}`, looseOpts)
|
---|
| 55 | if (!caret || !caret._shouldAvoid) {
|
---|
| 56 | return {
|
---|
| 57 | ...caret,
|
---|
| 58 | _outsideDependencyRange: true,
|
---|
| 59 | _isSemVerMajor: false
|
---|
| 60 | }
|
---|
| 61 | }
|
---|
| 62 |
|
---|
| 63 | const star = pickManifest(packument, '*', looseOpts)
|
---|
| 64 | if (!star || !star._shouldAvoid) {
|
---|
| 65 | return {
|
---|
| 66 | ...star,
|
---|
| 67 | _outsideDependencyRange: true,
|
---|
| 68 | _isSemVerMajor: true
|
---|
| 69 | }
|
---|
| 70 | }
|
---|
| 71 |
|
---|
| 72 | throw Object.assign(new Error(`No avoidable versions for ${name}`), {
|
---|
| 73 | code: 'ETARGET',
|
---|
| 74 | name,
|
---|
| 75 | wanted,
|
---|
| 76 | avoid,
|
---|
| 77 | before,
|
---|
| 78 | versions: Object.keys(versions)
|
---|
| 79 | })
|
---|
| 80 | }
|
---|
| 81 |
|
---|
| 82 | const staged = (includeStaged && packument.stagedVersions &&
|
---|
| 83 | packument.stagedVersions.versions) || {}
|
---|
| 84 | const restricted = (packument.policyRestrictions &&
|
---|
| 85 | packument.policyRestrictions.versions) || {}
|
---|
| 86 |
|
---|
| 87 | const time = before && verTimes ? +(new Date(before)) : Infinity
|
---|
| 88 | const spec = npa.resolve(name, wanted || defaultTag)
|
---|
| 89 | const type = spec.type
|
---|
| 90 | const distTags = packument['dist-tags'] || {}
|
---|
| 91 |
|
---|
| 92 | if (type !== 'tag' && type !== 'version' && type !== 'range') {
|
---|
| 93 | throw new Error('Only tag, version, and range are supported')
|
---|
| 94 | }
|
---|
| 95 |
|
---|
| 96 | // if the type is 'tag', and not just the implicit default, then it must
|
---|
| 97 | // be that exactly, or nothing else will do.
|
---|
| 98 | if (wanted && type === 'tag') {
|
---|
| 99 | const ver = distTags[wanted]
|
---|
| 100 | // if the version in the dist-tags is before the before date, then
|
---|
| 101 | // we use that. Otherwise, we get the highest precedence version
|
---|
| 102 | // prior to the dist-tag.
|
---|
| 103 | if (isBefore(verTimes, ver, time)) {
|
---|
| 104 | return decorateAvoid(versions[ver] || staged[ver] || restricted[ver], avoid)
|
---|
| 105 | } else {
|
---|
| 106 | return pickManifest(packument, `<=${ver}`, opts)
|
---|
| 107 | }
|
---|
| 108 | }
|
---|
| 109 |
|
---|
| 110 | // similarly, if a specific version, then only that version will do
|
---|
| 111 | if (wanted && type === 'version') {
|
---|
| 112 | const ver = semver.clean(wanted, { loose: true })
|
---|
| 113 | const mani = versions[ver] || staged[ver] || restricted[ver]
|
---|
| 114 | return isBefore(verTimes, ver, time) ? decorateAvoid(mani, avoid) : null
|
---|
| 115 | }
|
---|
| 116 |
|
---|
| 117 | // ok, sort based on our heuristics, and pick the best fit
|
---|
| 118 | const range = type === 'range' ? wanted : '*'
|
---|
| 119 |
|
---|
| 120 | // if the range is *, then we prefer the 'latest' if available
|
---|
| 121 | // but skip this if it should be avoided, in that case we have
|
---|
| 122 | // to try a little harder.
|
---|
| 123 | const defaultVer = distTags[defaultTag]
|
---|
| 124 | if (defaultVer &&
|
---|
| 125 | (range === '*' || semver.satisfies(defaultVer, range, { loose: true })) &&
|
---|
| 126 | !shouldAvoid(defaultVer, avoid)) {
|
---|
| 127 | const mani = versions[defaultVer]
|
---|
| 128 | if (mani && isBefore(verTimes, defaultVer, time)) {
|
---|
| 129 | return mani
|
---|
| 130 | }
|
---|
| 131 | }
|
---|
| 132 |
|
---|
| 133 | // ok, actually have to sort the list and take the winner
|
---|
| 134 | const allEntries = Object.entries(versions)
|
---|
| 135 | .concat(Object.entries(staged))
|
---|
| 136 | .concat(Object.entries(restricted))
|
---|
| 137 | .filter(([ver, mani]) => isBefore(verTimes, ver, time))
|
---|
| 138 |
|
---|
| 139 | if (!allEntries.length) {
|
---|
| 140 | throw Object.assign(new Error(`No versions available for ${name}`), {
|
---|
| 141 | code: 'ENOVERSIONS',
|
---|
| 142 | name,
|
---|
| 143 | type,
|
---|
| 144 | wanted,
|
---|
| 145 | before,
|
---|
| 146 | versions: Object.keys(versions)
|
---|
| 147 | })
|
---|
| 148 | }
|
---|
| 149 |
|
---|
| 150 | const sortSemverOpt = { loose: true }
|
---|
| 151 | const entries = allEntries.filter(([ver, mani]) =>
|
---|
| 152 | semver.satisfies(ver, range, { loose: true }))
|
---|
| 153 | .sort((a, b) => {
|
---|
| 154 | const [vera, mania] = a
|
---|
| 155 | const [verb, manib] = b
|
---|
| 156 | const notavoida = !shouldAvoid(vera, avoid)
|
---|
| 157 | const notavoidb = !shouldAvoid(verb, avoid)
|
---|
| 158 | const notrestra = !restricted[a]
|
---|
| 159 | const notrestrb = !restricted[b]
|
---|
| 160 | const notstagea = !staged[a]
|
---|
| 161 | const notstageb = !staged[b]
|
---|
| 162 | const notdepra = !mania.deprecated
|
---|
| 163 | const notdeprb = !manib.deprecated
|
---|
| 164 | const enginea = engineOk(mania, npmVersion, nodeVersion)
|
---|
| 165 | const engineb = engineOk(manib, npmVersion, nodeVersion)
|
---|
| 166 | // sort by:
|
---|
| 167 | // - not an avoided version
|
---|
| 168 | // - not restricted
|
---|
| 169 | // - not staged
|
---|
| 170 | // - not deprecated and engine ok
|
---|
| 171 | // - engine ok
|
---|
| 172 | // - not deprecated
|
---|
| 173 | // - semver
|
---|
| 174 | return (notavoidb - notavoida) ||
|
---|
| 175 | (notrestrb - notrestra) ||
|
---|
| 176 | (notstageb - notstagea) ||
|
---|
| 177 | ((notdeprb && engineb) - (notdepra && enginea)) ||
|
---|
| 178 | (engineb - enginea) ||
|
---|
| 179 | (notdeprb - notdepra) ||
|
---|
| 180 | semver.rcompare(vera, verb, sortSemverOpt)
|
---|
| 181 | })
|
---|
| 182 |
|
---|
| 183 | return decorateAvoid(entries[0] && entries[0][1], avoid)
|
---|
| 184 | }
|
---|
| 185 |
|
---|
| 186 | module.exports = (packument, wanted, opts = {}) => {
|
---|
| 187 | const mani = pickManifest(packument, wanted, opts)
|
---|
| 188 | const picked = mani && normalizeBin(mani)
|
---|
| 189 | const policyRestrictions = packument.policyRestrictions
|
---|
| 190 | const restricted = (policyRestrictions && policyRestrictions.versions) || {}
|
---|
| 191 |
|
---|
| 192 | if (picked && !restricted[picked.version]) {
|
---|
| 193 | return picked
|
---|
| 194 | }
|
---|
| 195 |
|
---|
| 196 | const { before = null, defaultTag = 'latest' } = opts
|
---|
| 197 | const bstr = before ? new Date(before).toLocaleString() : ''
|
---|
| 198 | const { name } = packument
|
---|
| 199 | const pckg = `${name}@${wanted}` +
|
---|
| 200 | (before ? ` with a date before ${bstr}` : '')
|
---|
| 201 |
|
---|
| 202 | const isForbidden = picked && !!restricted[picked.version]
|
---|
| 203 | const polMsg = isForbidden ? policyRestrictions.message : ''
|
---|
| 204 |
|
---|
| 205 | const msg = !isForbidden ? `No matching version found for ${pckg}.`
|
---|
| 206 | : `Could not download ${pckg} due to policy violations:\n${polMsg}`
|
---|
| 207 |
|
---|
| 208 | const code = isForbidden ? 'E403' : 'ETARGET'
|
---|
| 209 | throw Object.assign(new Error(msg), {
|
---|
| 210 | code,
|
---|
| 211 | type: npa.resolve(packument.name, wanted).type,
|
---|
| 212 | wanted,
|
---|
| 213 | versions: Object.keys(packument.versions),
|
---|
| 214 | name,
|
---|
| 215 | distTags: packument['dist-tags'],
|
---|
| 216 | defaultTag
|
---|
| 217 | })
|
---|
| 218 | }
|
---|