var url = require('url') var assert = require('assert') var util = require('util') var semver = require('semver') var HostedGit = require('hosted-git-info') module.exports = npa var isWindows = process.platform === 'win32' || global.FAKE_WINDOWS var slashRe = isWindows ? /\\|[/]/ : /[/]/ var parseName = /^(?:@([^/]+?)[/])?([^/]+?)$/ var nameAt = /^(@([^/]+?)[/])?([^/]+?)@/ var debug = util.debuglog ? util.debuglog('npa') : /\bnpa\b/i.test(process.env.NODE_DEBUG || '') ? function () { console.error('NPA: ' + util.format.apply(util, arguments).split('\n').join('\nNPA: ')) } : function () {} function validName (name) { if (!name) { debug('not a name %j', name) return false } var n = name.trim() if (!n || n.charAt(0) === '.' || !n.match(/^[a-zA-Z0-9]/) || n.match(/[/()&?#|<>@:%\s\\*'"!~`]/) || n.toLowerCase() === 'node_modules' || n !== encodeURIComponent(n) || n.toLowerCase() === 'favicon.ico') { debug('not a valid name %j', name) return false } return n } function npa (arg) { assert.equal(typeof arg, 'string') arg = arg.trim() var res = new Result() res.raw = arg res.scope = null res.escapedName = null // See if it's something like foo@... var nameparse = arg.match(nameAt) debug('nameparse', nameparse) if (nameparse && validName(nameparse[3]) && (!nameparse[2] || validName(nameparse[2]))) { res.name = (nameparse[1] || '') + nameparse[3] res.escapedName = escapeName(res.name) if (nameparse[2]) { res.scope = '@' + nameparse[2] } arg = arg.substr(nameparse[0].length) } else { res.name = null } res.rawSpec = arg res.spec = arg var urlparse = url.parse(arg) debug('urlparse', urlparse) // windows paths look like urls // don't be fooled! if (isWindows && urlparse && urlparse.protocol && urlparse.protocol.match(/^[a-zA-Z]:$/)) { debug('windows url-ish local path', urlparse) urlparse = {} } if (urlparse.protocol || HostedGit.fromUrl(arg)) { return parseUrl(res, arg, urlparse) } // at this point, it's not a url, and not hosted // If it's a valid name, and doesn't already have a name, then assume // $name@"" range // // if it's got / chars in it, then assume that it's local. if (res.name) { if (arg === '') arg = 'latest' var version = semver.valid(arg, true) var range = semver.validRange(arg, true) // foo@... if (version) { res.spec = version res.type = 'version' } else if (range) { res.spec = range res.type = 'range' } else if (slashRe.test(arg)) { parseLocal(res, arg) } else { res.type = 'tag' res.spec = arg } } else { var p = arg.match(parseName) if (p && validName(p[2]) && (!p[1] || validName(p[1]))) { res.type = 'tag' res.spec = 'latest' res.rawSpec = '' res.name = arg res.escapedName = escapeName(res.name) if (p[1]) { res.scope = '@' + p[1] } } else { parseLocal(res, arg) } } return res } function escapeName (name) { // scoped packages in couch must have slash url-encoded, e.g. @foo%2Fbar return name && name.replace('/', '%2f') } function parseLocal (res, arg) { // turns out nearly every character is allowed in fs paths if (/\0/.test(arg)) { throw new Error('Invalid Path: ' + JSON.stringify(arg)) } res.type = 'local' res.spec = arg } function parseUrl (res, arg, urlparse) { var gitHost = HostedGit.fromUrl(arg) if (gitHost) { res.type = 'hosted' res.spec = gitHost.toString() res.hosted = { type: gitHost.type, ssh: gitHost.ssh(), sshUrl: gitHost.sshurl(), httpsUrl: gitHost.https(), gitUrl: gitHost.git(), shortcut: gitHost.shortcut(), directUrl: gitHost.file('package.json') } return res } // check the protocol, and then see if it's git or not switch (urlparse.protocol) { case 'git:': case 'git+http:': case 'git+https:': case 'git+rsync:': case 'git+ftp:': case 'git+ssh:': case 'git+file:': res.type = 'git' res.spec = arg.replace(/^git[+]/, '') break case 'http:': case 'https:': res.type = 'remote' res.spec = arg break case 'file:': res.type = 'local' if (isWindows && arg.match(/^file:\/\/\/?[a-z]:/i)) { // Windows URIs usually parse all wrong, so we just take matters // into our own hands, in this case. res.spec = arg.replace(/^file:\/\/\/?/i, '') } else { res.spec = urlparse.pathname } break default: throw new Error('Unsupported URL Type: ' + arg) } return res } function Result () { if (!(this instanceof Result)) return new Result() } Result.prototype.name = null Result.prototype.type = null Result.prototype.spec = null Result.prototype.raw = null Result.prototype.hosted = null