codekingpro/portable-devtools
114k
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 be that exactly, or nothing else will do.
97 if (wanted && type === 'tag') {
98 const ver = distTags[wanted]
99 // 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.
100 if (isBefore(verTimes, ver, time)) {
101 return decorateAvoid(versions[ver] || staged[ver] || restricted[ver], avoid)
102 } else {
103 return pickManifest(packument, `<=${ver}`, opts)
104 }
105 }
106
107 // similarly, if a specific version, then only that version will do
108 if (wanted && type === 'version') {
109 const ver = semver.clean(wanted, { loose: true })
110 const mani = versions[ver] || staged[ver] || restricted[ver]
111 return isBefore(verTimes, ver, time) ? decorateAvoid(mani, avoid) : null
112 }
113
114 // ok, sort based on our heuristics, and pick the best fit
115 const range = type === 'range' ? wanted : '*'
116
117 // 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.
118 const defaultVer = distTags[defaultTag]
119 if (defaultVer &&
120 (range === '*' || semver.satisfies(defaultVer, range, { loose: true })) &&
121 !restricted[defaultVer] &&
122 !shouldAvoid(defaultVer, avoid)) {
123 const mani = versions[defaultVer]
124 const ok = mani &&
125 isBefore(verTimes, defaultVer, time) &&
126 engineOk(mani, npmVersion, nodeVersion) &&
127 !mani.deprecated &&
128 !staged[defaultVer]
129 if (ok) {
130 return mani
131 }
132 }
133
134 // ok, actually have to sort the list and take the winner
135 const allEntries = Object.entries(versions)
136 .concat(Object.entries(staged))
137 .concat(Object.entries(restricted))
138 .filter(([ver]) => isBefore(verTimes, ver, time))
139
140 if (!allEntries.length) {
141 throw Object.assign(new Error(`No versions available for ${name}`), {
142 code: 'ENOVERSIONS',
143 name,
144 type,
145 wanted,
146 before,
147 versions: Object.keys(versions),
148 })
149 }
150
151 const sortSemverOpt = { loose: true }
152 const entries = allEntries.filter(([ver]) =>
153 semver.satisfies(ver, range, { loose: true }))
154 .sort((a, b) => {
155 const [vera, mania] = a
156 const [verb, manib] = b
157 const notavoida = !shouldAvoid(vera, avoid)
158 const notavoidb = !shouldAvoid(verb, avoid)
159 const notrestra = !restricted[vera]
160 const notrestrb = !restricted[verb]
161 const notstagea = !staged[vera]
162 const notstageb = !staged[verb]
163 const notdepra = !mania.deprecated
164 const notdeprb = !manib.deprecated
165 const enginea = engineOk(mania, npmVersion, nodeVersion)
166 const engineb = engineOk(manib, npmVersion, nodeVersion)
167 // sort by:
168 // - not an avoided version
169 // - not restricted
170 // - not staged
171 // - not deprecated and engine ok
172 // - engine ok
173 // - not deprecated
174 // - semver
175 return (notavoidb - notavoida) ||
176 (notrestrb - notrestra) ||
177 (notstageb - notstagea) ||
178 ((notdeprb && engineb) - (notdepra && enginea)) ||
179 (engineb - enginea) ||
180 (notdeprb - notdepra) ||
181 semver.rcompare(vera, verb, sortSemverOpt)
182 })
183
184 return decorateAvoid(entries[0] && entries[0][1], avoid)
185}
186
187module.exports = (packument, wanted, opts = {}) => {
188 const mani = pickManifest(packument, wanted, opts)
189 const picked = mani && normalizeBin(mani)
190 const policyRestrictions = packument.policyRestrictions
191 const restricted = (policyRestrictions && policyRestrictions.versions) || {}
192
193 if (picked && !restricted[picked.version]) {
194 return picked
195 }
196
197 const { before = null, defaultTag = 'latest' } = opts
198 const bstr = before ? new Date(before).toLocaleString() : ''
199 const { name } = packument
200 const pckg = `${name}@${wanted}` +
201 (before ? ` with a date before ${bstr}` : '')
202
203 const isForbidden = picked && !!restricted[picked.version]
204 const polMsg = isForbidden ? policyRestrictions.message : ''
205
206 const msg = !isForbidden ? `No matching version found for ${pckg}.`
207 : `Could not download ${pckg} due to policy violations:\n${polMsg}`
208
209 const code = isForbidden ? 'E403' : 'ETARGET'
210 throw Object.assign(new Error(msg), {
211 code,
212 type: npa.resolve(packument.name, wanted).type,
213 wanted,
214 versions: Object.keys(packument.versions ?? {}),
215 name,
216 distTags: packument['dist-tags'],
217 defaultTag,
218 })
219}
220 