index.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  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. !restricted[defaultVer] &&
  108. !shouldAvoid(defaultVer, avoid)) {
  109. const mani = versions[defaultVer]
  110. const ok = mani &&
  111. isBefore(verTimes, defaultVer, time) &&
  112. engineOk(mani, npmVersion, nodeVersion) &&
  113. !mani.deprecated &&
  114. !staged[defaultVer]
  115. if (ok) {
  116. return mani
  117. }
  118. }
  119. // ok, actually have to sort the list and take the winner
  120. const allEntries = Object.entries(versions)
  121. .concat(Object.entries(staged))
  122. .concat(Object.entries(restricted))
  123. .filter(([ver]) => isBefore(verTimes, ver, time))
  124. if (!allEntries.length) {
  125. throw Object.assign(new Error(`No versions available for ${name}`), {
  126. code: 'ENOVERSIONS',
  127. name,
  128. type,
  129. wanted,
  130. before,
  131. versions: Object.keys(versions),
  132. })
  133. }
  134. const sortSemverOpt = { loose: true }
  135. const entries = allEntries.filter(([ver]) =>
  136. semver.satisfies(ver, range, { loose: true }))
  137. .sort((a, b) => {
  138. const [vera, mania] = a
  139. const [verb, manib] = b
  140. const notavoida = !shouldAvoid(vera, avoid)
  141. const notavoidb = !shouldAvoid(verb, avoid)
  142. const notrestra = !restricted[vera]
  143. const notrestrb = !restricted[verb]
  144. const notstagea = !staged[vera]
  145. const notstageb = !staged[verb]
  146. const notdepra = !mania.deprecated
  147. const notdeprb = !manib.deprecated
  148. const enginea = engineOk(mania, npmVersion, nodeVersion)
  149. const engineb = engineOk(manib, npmVersion, nodeVersion)
  150. // sort by:
  151. // - not an avoided version
  152. // - not restricted
  153. // - not staged
  154. // - not deprecated and engine ok
  155. // - engine ok
  156. // - not deprecated
  157. // - semver
  158. return (notavoidb - notavoida) ||
  159. (notrestrb - notrestra) ||
  160. (notstageb - notstagea) ||
  161. ((notdeprb && engineb) - (notdepra && enginea)) ||
  162. (engineb - enginea) ||
  163. (notdeprb - notdepra) ||
  164. semver.rcompare(vera, verb, sortSemverOpt)
  165. })
  166. return decorateAvoid(entries[0] && entries[0][1], avoid)
  167. }
  168. module.exports = (packument, wanted, opts = {}) => {
  169. const mani = pickManifest(packument, wanted, opts)
  170. const picked = mani && normalizeBin(mani)
  171. const policyRestrictions = packument.policyRestrictions
  172. const restricted = (policyRestrictions && policyRestrictions.versions) || {}
  173. if (picked && !restricted[picked.version]) {
  174. return picked
  175. }
  176. const { before = null, defaultTag = 'latest' } = opts
  177. const bstr = before ? new Date(before).toLocaleString() : ''
  178. const { name } = packument
  179. const pckg = `${name}@${wanted}` +
  180. (before ? ` with a date before ${bstr}` : '')
  181. const isForbidden = picked && !!restricted[picked.version]
  182. const polMsg = isForbidden ? policyRestrictions.message : ''
  183. const msg = !isForbidden ? `No matching version found for ${pckg}.`
  184. : `Could not download ${pckg} due to policy violations:\n${polMsg}`
  185. const code = isForbidden ? 'E403' : 'ETARGET'
  186. throw Object.assign(new Error(msg), {
  187. code,
  188. type: npa.resolve(packument.name, wanted).type,
  189. wanted,
  190. versions: Object.keys(packument.versions ?? {}),
  191. name,
  192. distTags: packument['dist-tags'],
  193. defaultTag,
  194. })
  195. }