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