index.js 6.7 KB

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