1'use strict' 2 3const npa = require('npm-package-arg') 4const semver = require('semver') 5const { checkEngine } = require('npm-install-checks') 6const normalizeBin = require('npm-normalize-package-bin') 7 8const engineOk = (manifest, npmVersion, nodeVersion) => { 9 try { 10 checkEngine(manifest, npmVersion, nodeVersion) 11 return true 12 } catch (_) { 13 return false 14 } 15} 16 17const isBefore = (verTimes, ver, time) => 18 !verTimes || !verTimes[ver] || Date.parse(verTimes[ver]) <= time 19 20const avoidSemverOpt = { includePrerelease: true, loose: true } 21const shouldAvoid = (ver, avoid) => 22 avoid && semver.satisfies(ver, avoid, avoidSemverOpt) 23 24const decorateAvoid = (result, avoid) => 25 result && shouldAvoid(result.version, avoid) 26 ? { ...result, _shouldAvoid: true } 27 : result 28 29const 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 186module.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} 219