[6a3a178] | 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 | }
|
---|