1 | 'use strict'
|
---|
2 | module.exports = npa
|
---|
3 | module.exports.resolve = resolve
|
---|
4 | module.exports.Result = Result
|
---|
5 |
|
---|
6 | const url = require('url')
|
---|
7 | const HostedGit = require('hosted-git-info')
|
---|
8 | const semver = require('semver')
|
---|
9 | const path = global.FAKE_WINDOWS ? require('path').win32 : require('path')
|
---|
10 | const validatePackageName = require('validate-npm-package-name')
|
---|
11 | const { homedir } = require('os')
|
---|
12 |
|
---|
13 | const isWindows = process.platform === 'win32' || global.FAKE_WINDOWS
|
---|
14 | const hasSlashes = isWindows ? /\\|[/]/ : /[/]/
|
---|
15 | const isURL = /^(?:git[+])?[a-z]+:/i
|
---|
16 | const isGit = /^[^@]+@[^:.]+\.[^:]+:.+$/i
|
---|
17 | const isFilename = /[.](?:tgz|tar.gz|tar)$/i
|
---|
18 |
|
---|
19 | function npa (arg, where) {
|
---|
20 | let name
|
---|
21 | let spec
|
---|
22 | if (typeof arg === 'object') {
|
---|
23 | if (arg instanceof Result && (!where || where === arg.where))
|
---|
24 | return arg
|
---|
25 | else if (arg.name && arg.rawSpec)
|
---|
26 | return npa.resolve(arg.name, arg.rawSpec, where || arg.where)
|
---|
27 | else
|
---|
28 | return npa(arg.raw, where || arg.where)
|
---|
29 | }
|
---|
30 | const nameEndsAt = arg[0] === '@' ? arg.slice(1).indexOf('@') + 1 : arg.indexOf('@')
|
---|
31 | const namePart = nameEndsAt > 0 ? arg.slice(0, nameEndsAt) : arg
|
---|
32 | if (isURL.test(arg))
|
---|
33 | spec = arg
|
---|
34 | else if (isGit.test(arg))
|
---|
35 | spec = `git+ssh://${arg}`
|
---|
36 | else if (namePart[0] !== '@' && (hasSlashes.test(namePart) || isFilename.test(namePart)))
|
---|
37 | spec = arg
|
---|
38 | else if (nameEndsAt > 0) {
|
---|
39 | name = namePart
|
---|
40 | spec = arg.slice(nameEndsAt + 1)
|
---|
41 | } else {
|
---|
42 | const valid = validatePackageName(arg)
|
---|
43 | if (valid.validForOldPackages)
|
---|
44 | name = arg
|
---|
45 | else
|
---|
46 | spec = arg
|
---|
47 | }
|
---|
48 | return resolve(name, spec, where, arg)
|
---|
49 | }
|
---|
50 |
|
---|
51 | const isFilespec = isWindows ? /^(?:[.]|~[/]|[/\\]|[a-zA-Z]:)/ : /^(?:[.]|~[/]|[/]|[a-zA-Z]:)/
|
---|
52 |
|
---|
53 | function resolve (name, spec, where, arg) {
|
---|
54 | const res = new Result({
|
---|
55 | raw: arg,
|
---|
56 | name: name,
|
---|
57 | rawSpec: spec,
|
---|
58 | fromArgument: arg != null,
|
---|
59 | })
|
---|
60 |
|
---|
61 | if (name)
|
---|
62 | res.setName(name)
|
---|
63 |
|
---|
64 | if (spec && (isFilespec.test(spec) || /^file:/i.test(spec)))
|
---|
65 | return fromFile(res, where)
|
---|
66 | else if (spec && /^npm:/i.test(spec))
|
---|
67 | return fromAlias(res, where)
|
---|
68 |
|
---|
69 | const hosted = HostedGit.fromUrl(spec, {
|
---|
70 | noGitPlus: true,
|
---|
71 | noCommittish: true,
|
---|
72 | })
|
---|
73 | if (hosted)
|
---|
74 | return fromHostedGit(res, hosted)
|
---|
75 | else if (spec && isURL.test(spec))
|
---|
76 | return fromURL(res)
|
---|
77 | else if (spec && (hasSlashes.test(spec) || isFilename.test(spec)))
|
---|
78 | return fromFile(res, where)
|
---|
79 | else
|
---|
80 | return fromRegistry(res)
|
---|
81 | }
|
---|
82 |
|
---|
83 | function invalidPackageName (name, valid) {
|
---|
84 | const err = new Error(`Invalid package name "${name}": ${valid.errors.join('; ')}`)
|
---|
85 | err.code = 'EINVALIDPACKAGENAME'
|
---|
86 | return err
|
---|
87 | }
|
---|
88 | function invalidTagName (name) {
|
---|
89 | const err = new Error(`Invalid tag name "${name}": Tags may not have any characters that encodeURIComponent encodes.`)
|
---|
90 | err.code = 'EINVALIDTAGNAME'
|
---|
91 | return err
|
---|
92 | }
|
---|
93 |
|
---|
94 | function Result (opts) {
|
---|
95 | this.type = opts.type
|
---|
96 | this.registry = opts.registry
|
---|
97 | this.where = opts.where
|
---|
98 | if (opts.raw == null)
|
---|
99 | this.raw = opts.name ? opts.name + '@' + opts.rawSpec : opts.rawSpec
|
---|
100 | else
|
---|
101 | this.raw = opts.raw
|
---|
102 |
|
---|
103 | this.name = undefined
|
---|
104 | this.escapedName = undefined
|
---|
105 | this.scope = undefined
|
---|
106 | this.rawSpec = opts.rawSpec == null ? '' : opts.rawSpec
|
---|
107 | this.saveSpec = opts.saveSpec
|
---|
108 | this.fetchSpec = opts.fetchSpec
|
---|
109 | if (opts.name)
|
---|
110 | this.setName(opts.name)
|
---|
111 | this.gitRange = opts.gitRange
|
---|
112 | this.gitCommittish = opts.gitCommittish
|
---|
113 | this.hosted = opts.hosted
|
---|
114 | }
|
---|
115 |
|
---|
116 | Result.prototype.setName = function (name) {
|
---|
117 | const valid = validatePackageName(name)
|
---|
118 | if (!valid.validForOldPackages)
|
---|
119 | throw invalidPackageName(name, valid)
|
---|
120 |
|
---|
121 | this.name = name
|
---|
122 | this.scope = name[0] === '@' ? name.slice(0, name.indexOf('/')) : undefined
|
---|
123 | // scoped packages in couch must have slash url-encoded, e.g. @foo%2Fbar
|
---|
124 | this.escapedName = name.replace('/', '%2f')
|
---|
125 | return this
|
---|
126 | }
|
---|
127 |
|
---|
128 | Result.prototype.toString = function () {
|
---|
129 | const full = []
|
---|
130 | if (this.name != null && this.name !== '')
|
---|
131 | full.push(this.name)
|
---|
132 | const spec = this.saveSpec || this.fetchSpec || this.rawSpec
|
---|
133 | if (spec != null && spec !== '')
|
---|
134 | full.push(spec)
|
---|
135 | return full.length ? full.join('@') : this.raw
|
---|
136 | }
|
---|
137 |
|
---|
138 | Result.prototype.toJSON = function () {
|
---|
139 | const result = Object.assign({}, this)
|
---|
140 | delete result.hosted
|
---|
141 | return result
|
---|
142 | }
|
---|
143 |
|
---|
144 | function setGitCommittish (res, committish) {
|
---|
145 | if (committish != null && committish.length >= 7 && committish.slice(0, 7) === 'semver:') {
|
---|
146 | res.gitRange = decodeURIComponent(committish.slice(7))
|
---|
147 | res.gitCommittish = null
|
---|
148 | } else
|
---|
149 | res.gitCommittish = committish === '' ? null : committish
|
---|
150 |
|
---|
151 | return res
|
---|
152 | }
|
---|
153 |
|
---|
154 | function fromFile (res, where) {
|
---|
155 | if (!where)
|
---|
156 | where = process.cwd()
|
---|
157 | res.type = isFilename.test(res.rawSpec) ? 'file' : 'directory'
|
---|
158 | res.where = where
|
---|
159 |
|
---|
160 | // always put the '/' on where when resolving urls, or else
|
---|
161 | // file:foo from /path/to/bar goes to /path/to/foo, when we want
|
---|
162 | // it to be /path/to/foo/bar
|
---|
163 |
|
---|
164 | let specUrl
|
---|
165 | let resolvedUrl
|
---|
166 | const prefix = (!/^file:/.test(res.rawSpec) ? 'file:' : '')
|
---|
167 | const rawWithPrefix = prefix + res.rawSpec
|
---|
168 | let rawNoPrefix = rawWithPrefix.replace(/^file:/, '')
|
---|
169 | try {
|
---|
170 | resolvedUrl = new url.URL(rawWithPrefix, `file://${path.resolve(where)}/`)
|
---|
171 | specUrl = new url.URL(rawWithPrefix)
|
---|
172 | } catch (originalError) {
|
---|
173 | const er = new Error('Invalid file: URL, must comply with RFC 8909')
|
---|
174 | throw Object.assign(er, {
|
---|
175 | raw: res.rawSpec,
|
---|
176 | spec: res,
|
---|
177 | where,
|
---|
178 | originalError,
|
---|
179 | })
|
---|
180 | }
|
---|
181 |
|
---|
182 | // environment switch for testing
|
---|
183 | if (process.env.NPM_PACKAGE_ARG_8909_STRICT !== '1') {
|
---|
184 | // XXX backwards compatibility lack of compliance with 8909
|
---|
185 | // Remove when we want a breaking change to come into RFC compliance.
|
---|
186 | if (resolvedUrl.host && resolvedUrl.host !== 'localhost') {
|
---|
187 | const rawSpec = res.rawSpec.replace(/^file:\/\//, 'file:///')
|
---|
188 | resolvedUrl = new url.URL(rawSpec, `file://${path.resolve(where)}/`)
|
---|
189 | specUrl = new url.URL(rawSpec)
|
---|
190 | rawNoPrefix = rawSpec.replace(/^file:/, '')
|
---|
191 | }
|
---|
192 | // turn file:/../foo into file:../foo
|
---|
193 | if (/^\/\.\.?(\/|$)/.test(rawNoPrefix)) {
|
---|
194 | const rawSpec = res.rawSpec.replace(/^file:\//, 'file:')
|
---|
195 | resolvedUrl = new url.URL(rawSpec, `file://${path.resolve(where)}/`)
|
---|
196 | specUrl = new url.URL(rawSpec)
|
---|
197 | rawNoPrefix = rawSpec.replace(/^file:/, '')
|
---|
198 | }
|
---|
199 | // XXX end 8909 violation backwards compatibility section
|
---|
200 | }
|
---|
201 |
|
---|
202 | // file:foo - relative url to ./foo
|
---|
203 | // file:/foo - absolute path /foo
|
---|
204 | // file:///foo - absolute path to /foo, no authority host
|
---|
205 | // file://localhost/foo - absolute path to /foo, on localhost
|
---|
206 | // file://foo - absolute path to / on foo host (error!)
|
---|
207 | if (resolvedUrl.host && resolvedUrl.host !== 'localhost') {
|
---|
208 | const msg = `Invalid file: URL, must be absolute if // present`
|
---|
209 | throw Object.assign(new Error(msg), {
|
---|
210 | raw: res.rawSpec,
|
---|
211 | parsed: resolvedUrl,
|
---|
212 | })
|
---|
213 | }
|
---|
214 |
|
---|
215 | // turn /C:/blah into just C:/blah on windows
|
---|
216 | let specPath = decodeURIComponent(specUrl.pathname)
|
---|
217 | let resolvedPath = decodeURIComponent(resolvedUrl.pathname)
|
---|
218 | if (isWindows) {
|
---|
219 | specPath = specPath.replace(/^\/+([a-z]:\/)/i, '$1')
|
---|
220 | resolvedPath = resolvedPath.replace(/^\/+([a-z]:\/)/i, '$1')
|
---|
221 | }
|
---|
222 |
|
---|
223 | // replace ~ with homedir, but keep the ~ in the saveSpec
|
---|
224 | // otherwise, make it relative to where param
|
---|
225 | if (/^\/~(\/|$)/.test(specPath)) {
|
---|
226 | res.saveSpec = `file:${specPath.substr(1)}`
|
---|
227 | resolvedPath = path.resolve(homedir(), specPath.substr(3))
|
---|
228 | } else if (!path.isAbsolute(rawNoPrefix))
|
---|
229 | res.saveSpec = `file:${path.relative(where, resolvedPath)}`
|
---|
230 | else
|
---|
231 | res.saveSpec = `file:${path.resolve(resolvedPath)}`
|
---|
232 |
|
---|
233 | res.fetchSpec = path.resolve(where, resolvedPath)
|
---|
234 | return res
|
---|
235 | }
|
---|
236 |
|
---|
237 | function fromHostedGit (res, hosted) {
|
---|
238 | res.type = 'git'
|
---|
239 | res.hosted = hosted
|
---|
240 | res.saveSpec = hosted.toString({ noGitPlus: false, noCommittish: false })
|
---|
241 | res.fetchSpec = hosted.getDefaultRepresentation() === 'shortcut' ? null : hosted.toString()
|
---|
242 | return setGitCommittish(res, hosted.committish)
|
---|
243 | }
|
---|
244 |
|
---|
245 | function unsupportedURLType (protocol, spec) {
|
---|
246 | const err = new Error(`Unsupported URL Type "${protocol}": ${spec}`)
|
---|
247 | err.code = 'EUNSUPPORTEDPROTOCOL'
|
---|
248 | return err
|
---|
249 | }
|
---|
250 |
|
---|
251 | function matchGitScp (spec) {
|
---|
252 | // git ssh specifiers are overloaded to also use scp-style git
|
---|
253 | // specifiers, so we have to parse those out and treat them special.
|
---|
254 | // They are NOT true URIs, so we can't hand them to `url.parse`.
|
---|
255 | //
|
---|
256 | // This regex looks for things that look like:
|
---|
257 | // git+ssh://git@my.custom.git.com:username/project.git#deadbeef
|
---|
258 | //
|
---|
259 | // ...and various combinations. The username in the beginning is *required*.
|
---|
260 | const matched = spec.match(/^git\+ssh:\/\/([^:#]+:[^#]+(?:\.git)?)(?:#(.*))?$/i)
|
---|
261 | return matched && !matched[1].match(/:[0-9]+\/?.*$/i) && {
|
---|
262 | fetchSpec: matched[1],
|
---|
263 | gitCommittish: matched[2] == null ? null : matched[2],
|
---|
264 | }
|
---|
265 | }
|
---|
266 |
|
---|
267 | function fromURL (res) {
|
---|
268 | // eslint-disable-next-line node/no-deprecated-api
|
---|
269 | const urlparse = url.parse(res.rawSpec)
|
---|
270 | res.saveSpec = res.rawSpec
|
---|
271 | // check the protocol, and then see if it's git or not
|
---|
272 | switch (urlparse.protocol) {
|
---|
273 | case 'git:':
|
---|
274 | case 'git+http:':
|
---|
275 | case 'git+https:':
|
---|
276 | case 'git+rsync:':
|
---|
277 | case 'git+ftp:':
|
---|
278 | case 'git+file:':
|
---|
279 | case 'git+ssh:': {
|
---|
280 | res.type = 'git'
|
---|
281 | const match = urlparse.protocol === 'git+ssh:' ? matchGitScp(res.rawSpec)
|
---|
282 | : null
|
---|
283 | if (match) {
|
---|
284 | setGitCommittish(res, match.gitCommittish)
|
---|
285 | res.fetchSpec = match.fetchSpec
|
---|
286 | } else {
|
---|
287 | setGitCommittish(res, urlparse.hash != null ? urlparse.hash.slice(1) : '')
|
---|
288 | urlparse.protocol = urlparse.protocol.replace(/^git[+]/, '')
|
---|
289 | if (urlparse.protocol === 'file:' && /^git\+file:\/\/[a-z]:/i.test(res.rawSpec)) {
|
---|
290 | // keep the drive letter : on windows file paths
|
---|
291 | urlparse.host += ':'
|
---|
292 | urlparse.hostname += ':'
|
---|
293 | }
|
---|
294 | delete urlparse.hash
|
---|
295 | res.fetchSpec = url.format(urlparse)
|
---|
296 | }
|
---|
297 | break
|
---|
298 | }
|
---|
299 | case 'http:':
|
---|
300 | case 'https:':
|
---|
301 | res.type = 'remote'
|
---|
302 | res.fetchSpec = res.saveSpec
|
---|
303 | break
|
---|
304 |
|
---|
305 | default:
|
---|
306 | throw unsupportedURLType(urlparse.protocol, res.rawSpec)
|
---|
307 | }
|
---|
308 |
|
---|
309 | return res
|
---|
310 | }
|
---|
311 |
|
---|
312 | function fromAlias (res, where) {
|
---|
313 | const subSpec = npa(res.rawSpec.substr(4), where)
|
---|
314 | if (subSpec.type === 'alias')
|
---|
315 | throw new Error('nested aliases not supported')
|
---|
316 |
|
---|
317 | if (!subSpec.registry)
|
---|
318 | throw new Error('aliases only work for registry deps')
|
---|
319 |
|
---|
320 | res.subSpec = subSpec
|
---|
321 | res.registry = true
|
---|
322 | res.type = 'alias'
|
---|
323 | res.saveSpec = null
|
---|
324 | res.fetchSpec = null
|
---|
325 | return res
|
---|
326 | }
|
---|
327 |
|
---|
328 | function fromRegistry (res) {
|
---|
329 | res.registry = true
|
---|
330 | const spec = res.rawSpec === '' ? 'latest' : res.rawSpec.trim()
|
---|
331 | // no save spec for registry components as we save based on the fetched
|
---|
332 | // version, not on the argument so this can't compute that.
|
---|
333 | res.saveSpec = null
|
---|
334 | res.fetchSpec = spec
|
---|
335 | const version = semver.valid(spec, true)
|
---|
336 | const range = semver.validRange(spec, true)
|
---|
337 | if (version)
|
---|
338 | res.type = 'version'
|
---|
339 | else if (range)
|
---|
340 | res.type = 'range'
|
---|
341 | else {
|
---|
342 | if (encodeURIComponent(spec) !== spec)
|
---|
343 | throw invalidTagName(spec)
|
---|
344 |
|
---|
345 | res.type = 'tag'
|
---|
346 | }
|
---|
347 | return res
|
---|
348 | }
|
---|