diff --git a/lib/config.js b/lib/config.js index 54d2c28..ac8d197 100644 --- a/lib/config.js +++ b/lib/config.js @@ -7,117 +7,118 @@ /*jslint node: true */ /*global console */ - 'use strict'; - var fs = require('fs'), - path = require('path'), - lang = require('./lang'), - file = require('./file'), - homeDir = require('./homeDir'), - overrideConfigUrl = path.join(homeDir, '.voloconfig'), - localConfigUrl = path.join(homeDir, '.voloconfiglocal'), - - data, overrideConfig, localConfig, contents; - - // The defaults to use. - data = lang.delegate({ - "volo": { - version: "0.2.10" - }, - - "npmRegistry": "https://registry.npmjs.org/", - - "github": { - "userAgent": "volo/0.2.10", - "scheme": "https", - "host": "github.com", - "apiHost": "api.github.com", - "searchHost": "api.github.com", - "rawUrlPattern": "https://raw.github.com/{owner}/{repo}/{version}/{file}", - "searchPath": "/legacy/repos/search/{query}?language=JavaScript", - "searchOverrides": { - "amd": { - "underscore": "amdjs/underscore", - "backbone": "amdjs/backbone" - } - }, - "typeOverrides": { - "dojo/dijit": "directory" - }, - - "auth": { - "domain": "https://api.github.com", - "authPath": "/authorizations", - "scopes": ["repo"], - "note": "Allow volo to interact with your repos.", - "noteUrl": "https://github.com/volojs/volo" - } - }, - - "command": { - "add": { - "discard": { - ".gitignore": true, - "test": true, - "tests": true, - "doc": true, - "docs": true, - "example": true, - "examples": true, - "demo": true, - "demos": true - } - } - } - }); - - //Allow a local config at homeDir + '.config.js' - if (file.exists(overrideConfigUrl)) { - contents = (fs.readFileSync(overrideConfigUrl, 'utf8') || '').trim(); - - if (contents) { - overrideConfig = JSON.parse(contents); - lang.mixin(data, overrideConfig, true); - } - } - - module.exports = { - get: function () { - return data; - }, - - //Simple local config. No fancy JSON object merging just plain mixing - //of top level properties. - getLocal: function () { - var contents; - - if (!localConfig) { - if (file.exists(localConfigUrl)) { - - contents = (fs.readFileSync(localConfigUrl, 'utf8') || '').trim(); - - if (contents) { - localConfig = JSON.parse(contents); - } - } - - if (!localConfig) { - localConfig = {}; - } - } - - return localConfig; - }, - - saveLocal: function () { - //Make sure the directory exists - try { - file.mkdirs(path.dirname(localConfigUrl)); - fs.writeFileSync(localConfigUrl, JSON.stringify(localConfig, null, ' ')); - } catch (e) { - console.error('Cannot save local config, continuing without saving.'); - return ''; - } - - return localConfigUrl; - } - }; + 'use strict'; + var fs = require('fs'), + path = require('path'), + lang = require('./lang'), + file = require('./file'), + homeDir = require('./homeDir'), + overrideConfigUrl = path.join(homeDir, '.voloconfig'), + localConfigUrl = path.join(homeDir, '.voloconfiglocal'), + + data, overrideConfig, localConfig, contents; + + // The defaults to use. + data = lang.delegate({ + "volo": { + version: "0.2.10" + }, + + "npmRegistry": "https://registry.npmjs.org/", + + "github": { + "userAgent": "volo/0.2.10", + "scheme": "https", + "host": "github.com", + "apiHost": "api.github.com", + "searchHost": "api.github.com", + "rawUrlPattern": "https://raw.github.com/{owner}/{repo}/{version}/{file}", + "rawApiPattern": "https://api.github.com/repos/{owner}/{repo}/contents/?path={file}&ref={version}", + "searchPath": "/legacy/repos/search/{query}?language=JavaScript", + "searchOverrides": { + "amd": { + "underscore": "amdjs/underscore", + "backbone": "amdjs/backbone" + } + }, + "typeOverrides": { + "dojo/dijit": "directory" + }, + + "auth": { + "domain": "https://api.github.com", + "authPath": "/authorizations", + "scopes": ["repo"], + "note": "Allow volo to interact with your repos.", + "noteUrl": "https://github.com/volojs/volo" + } + }, + + "command": { + "add": { + "discard": { + ".gitignore": true, + "test": true, + "tests": true, + "doc": true, + "docs": true, + "example": true, + "examples": true, + "demo": true, + "demos": true + } + } + } + }); + + //Allow a local config at homeDir + '.config.js' + if (file.exists(overrideConfigUrl)) { + contents = (fs.readFileSync(overrideConfigUrl, 'utf8') || '').trim(); + + if (contents) { + overrideConfig = JSON.parse(contents); + lang.mixin(data, overrideConfig, true); + } + } + + module.exports = { + get: function () { + return data; + }, + + //Simple local config. No fancy JSON object merging just plain mixing + //of top level properties. + getLocal: function () { + var contents; + + if (!localConfig) { + if (file.exists(localConfigUrl)) { + + contents = (fs.readFileSync(localConfigUrl, 'utf8') || '').trim(); + + if (contents) { + localConfig = JSON.parse(contents); + } + } + + if (!localConfig) { + localConfig = {}; + } + } + + return localConfig; + }, + + saveLocal: function () { + //Make sure the directory exists + try { + file.mkdirs(path.dirname(localConfigUrl)); + fs.writeFileSync(localConfigUrl, JSON.stringify(localConfig, null, ' ')); + } catch (e) { + console.error('Cannot save local config, continuing without saving.'); + return ''; + } + + return localConfigUrl; + } + }; diff --git a/lib/github.js b/lib/github.js index a5a3475..ed93709 100644 --- a/lib/github.js +++ b/lib/github.js @@ -9,30 +9,30 @@ 'use strict'; var q = require('q'), - https = require('https'), - querystring = require('querystring'), - semver = require('semver'), - lang = require('./lang'), - venv = require('./v'), - githubAuth = require('./github/auth'), - config = require('./config').get().github, - scheme = config.scheme, - version = require('./version'), - host = config.host, - apiHost = config.apiHost, - - //See if it is a semantic version range-ish thing. - semVerRangeRegExp = /^(>|<|~|\d+\.x|\d+\.\d+\.x)/, - - //Only use n, n.n, or n.n.n versions, and do not include ones - //that are n.n.npre2 or n.n.nbeta2 - versionRegExp = /^(v)?(\d+\.)?(\d+\.)?(\d+)$/; + https = require('https'), + querystring = require('querystring'), + semver = require('semver'), + lang = require('./lang'), + venv = require('./v'), + githubAuth = require('./github/auth'), + config = require('./config').get().github, + scheme = config.scheme, + version = require('./version'), + host = config.host, + apiHost = config.apiHost, + + //See if it is a semantic version range-ish thing. + semVerRangeRegExp = /^(>|<|~|\d+\.x|\d+\.\d+\.x)/, + + //Only use n, n.n, or n.n.n versions, and do not include ones + //that are n.n.npre2 or n.n.nbeta2 + versionRegExp = /^(v)?(\d+\.)?(\d+\.)?(\d+)$/; //Helper to encode the query for search as an URL-encoded value. function escape(text) { - //The V2 search API freaks with "." in the name. So convert them - //just to escaped spaces (+) to get some kind of usable result. - return querystring.escape(text).replace(/\./g, '+'); + //The V2 search API freaks with "." in the name. So convert them + //just to escaped spaces (+) to get some kind of usable result. + return querystring.escape(text).replace(/\./g, '+'); } /** @@ -45,230 +45,242 @@ function escape(text) { * token: the auth token to use for the request. */ function github(args, opts) { - opts = opts || {}; - - var req, - options = {}, - localAuth = githubAuth.getLocal(), - d = q.defer(); - - if (typeof args === 'string') { - args = { - host: apiHost, - path: '/' + args, - method: opts.method || 'GET' - }; - } - - function retryWithAuth() { - return githubAuth.fetch().then(function (info) { - options.token = info.token; - return github(args, options); - }); - } - - //Create a local options object, so as not to leak auth data out. - lang.mixin(options, opts); - - options.contentType = options.contentType || - 'application/json'; - - if (options.content && typeof options.content !== 'string') { - options.content = JSON.stringify(options.content); - } - - req = https.request(args, function (response) { - //console.log("statusCode: ", response.statusCode); - //console.log("headers: ", response.headers); - var body = ''; - - response.on('data', function (data) { - body += data; - }); - - response.on('end', function () { - var err; - if (response.statusCode === 404) { - venv(process.cwd()).env - .prompt(args.host + args.path + ' does not exist. ' + - 'Is this a private repo [n]?').then(function (answer) { - answer = answer && answer.toLowerCase(); - if (answer && answer.indexOf('y') === 0) { - retryWithAuth().then(d.resolve, d.fail); - } else { - err = new Error(args.host + args.path + ' does not exist. ' + - 'Is this a private repo [n]?'); - err.response = response; - d.reject(err); - } - }) - .fail(d); - } else if (response.statusCode === 200 || - response.statusCode === 201) { - //Convert the response into an object - d.resolve(JSON.parse(body)); - } else if (response.statusCode === 403 && !options.token) { - console.log('GitHub auth required for ' + args.host + - args.path + ': ' + body); - - //Try to get a token from the user, and retry. - retryWithAuth().then(d.resolve, d.fail); - } else { - err = new Error(args.host + args.path + ' returned status: ' + - response.statusCode + '. ' + body); - err.response = response; - d.reject(err); - } - }); - }).on('error', function (e) { - d.reject(e); - }); - - //Due to GitHub API limits, it is best to do calls as a given - //user. - if (!options.token && localAuth && localAuth.token) { - options.token = localAuth.token; - } - - if (options.token) { - req.setHeader('Authorization', 'token ' + options.token); - } - - req.setHeader('User-Agent', config.userAgent); - - if (options.content) { - req.setHeader('Content-Type', options.contentType); - req.setHeader('Content-Length', options.content.length); - req.write(options.content); - } - - req.end(); - - return d.promise; + opts = opts || {}; + + var req, + options = {}, + localAuth = githubAuth.getLocal(), + d = q.defer(); + + if (typeof args === 'string') { + args = { + host: apiHost, + path: '/' + args, + method: opts.method || 'GET' + }; + } + + function retryWithAuth() { + return githubAuth.fetch().then(function (info) { + options.token = info.token; + return github(args, options); + }); + } + + //Create a local options object, so as not to leak auth data out. + lang.mixin(options, opts); + + options.contentType = options.contentType || + 'application/json'; + + if (options.content && typeof options.content !== 'string') { + options.content = JSON.stringify(options.content); + } + + req = https.request(args, function (response) { + //console.log("statusCode: ", response.statusCode); + //console.log("headers: ", response.headers); + var body = ''; + + response.on('data', function (data) { + body += data; + }); + + response.on('end', function () { + var err; + if (response.statusCode === 404) { + venv(process.cwd()).env + .prompt(args.host + args.path + ' does not exist. ' + + 'Is this a private repo [n]?').then(function (answer) { + answer = answer && answer.toLowerCase(); + if (answer && answer.indexOf('y') === 0) { + retryWithAuth().then(d.resolve, d.fail); + } else { + err = new Error(args.host + args.path + ' does not exist. ' + + 'Is this a private repo [n]?'); + err.response = response; + d.reject(err); + } + }) + .fail(d); + } else if (response.statusCode === 200 || + response.statusCode === 201) { + //Convert the response into an object + d.resolve(JSON.parse(body)); + } else if (response.statusCode === 403 && !options.token) { + console.log('GitHub auth required for ' + args.host + + args.path + ': ' + body); + + //Try to get a token from the user, and retry. + retryWithAuth().then(d.resolve, d.fail); + } else { + err = new Error(args.host + args.path + ' returned status: ' + + response.statusCode + '. ' + body); + err.response = response; + d.reject(err); + } + }); + }).on('error', function (e) { + d.reject(e); + }); + + //Due to GitHub API limits, it is best to do calls as a given + //user. + if (!options.token && localAuth && localAuth.token) { + options.token = localAuth.token; + } + + if (options.token) { + req.setHeader('Authorization', 'token ' + options.token); + } + + req.setHeader('User-Agent', config.userAgent); + + if (options.content) { + req.setHeader('Content-Type', options.contentType); + req.setHeader('Content-Length', options.content.length); + req.write(options.content); + } + + req.end(); + + return d.promise; } github.url = function (path) { - return scheme + '://' + host + '/' + path; + return scheme + '://' + host + '/' + path; }; github.apiUrl = function (path) { - return scheme + '://' + apiHost + '/' + path; + return scheme + '://' + apiHost + '/' + path; }; github.rawUrl = function (ownerPlusRepo, version, specificFile) { - var parts = ownerPlusRepo.split('/'), - owner = parts[0], - repo = parts[1]; - - return config.rawUrlPattern - .replace(/\{owner\}/g, owner) - .replace(/\{repo\}/g, repo) - .replace(/\{version\}/g, version) - .replace(/\{file\}/g, specificFile); + var parts = ownerPlusRepo.split('/'), + owner = parts[0], + repo = parts[1]; + + return config.rawUrlPattern + .replace(/\{owner\}/g, owner) + .replace(/\{repo\}/g, repo) + .replace(/\{version\}/g, version) + .replace(/\{file\}/g, specificFile); +}; + +github.rawApiUrl = function (ownerPlusRepo, version, specificFile) { + var parts = ownerPlusRepo.split('/'), + owner = parts[0], + repo = parts[1]; + + return config.rawApiPattern + .replace(/\{owner\}/g, owner) + .replace(/\{repo\}/g, repo) + .replace(/\{version\}/g, version) + .replace(/\{file\}/g, specificFile); }; github.zipballUrl = function (ownerPlusRepo, version) { - return github.apiUrl('repos/' + ownerPlusRepo + '/zipball/' + version); + return github.apiUrl('repos/' + ownerPlusRepo + '/zipball/' + version); }; github.tags = function (ownerPlusRepo) { - return github('repos/' + ownerPlusRepo + '/tags').then(function (data) { - data = data.map(function (data) { - return data.name; - }); + return github('repos/' + ownerPlusRepo + '/tags').then(function (data) { + data = data.map(function (data) { + return data.name; + }); - return data; - }); + return data; + }); }; github.repo = function (ownerPlusRepo) { - return github('repos/' + ownerPlusRepo); + return github('repos/' + ownerPlusRepo); }; github.versionTags = function (ownerPlusRepo) { - return github.tags(ownerPlusRepo).then(function (tagNames) { - //Only collect tags that are version tags. - tagNames = tagNames.filter(function (tag) { - return versionRegExp.test(tag); - }); - - //Now order the tags in tag order. - tagNames.sort(version.compare); - - //Default to master branch if no version tags available. - if (!tagNames.length) { - return github.masterBranch(ownerPlusRepo).then(function (branchName) { - return [branchName]; - }); - } - - return tagNames; - }); + return github.tags(ownerPlusRepo).then(function (tagNames) { + //Only collect tags that are version tags. + tagNames = tagNames.filter(function (tag) { + return versionRegExp.test(tag); + }); + + //Now order the tags in tag order. + tagNames.sort(version.compare); + + //Default to master branch if no version tags available. + if (!tagNames.length) { + return github.masterBranch(ownerPlusRepo).then(function (branchName) { + return [branchName]; + }); + } + + return tagNames; + }); }; github.latestTag = function (ownerRepoVersion) { - //If ownerPlusRepo includes the version, just use that. - var parts = ownerRepoVersion.split('/'), - ownerRepo = parts[0] + '/' + parts[1], - version = parts[2]; - - //Set owner - if (version) { - //Figure out it is a semver, and if so, then find the best match. - if (semVerRangeRegExp.test(version)) { - return github.versionTags(ownerRepo).then(function (tagNames) { - return semver.maxSatisfying(tagNames, version); - }); - } else { - //Just a plain, explicit version, use it. First, check if it - //is a version tag and if it should add or remove a 'v' prefix - //to match a real tag. - return github.tags(ownerRepo).then(function (tags) { - if (tags.indexOf(version) !== -1) { - return version; - } else if (versionRegExp.test(version)) { - var tempVersion; - //May be a vVersion vs version mismatch, try to match - //the other way. - if (version.charAt(0) === 'v') { - //try without the v - tempVersion = version.substring(1); - } else { - //try with the v - tempVersion = 'v' + version; - } - if (tags.indexOf(tempVersion) !== -1) { - return tempVersion; - } else { - //Could still be a branch name, but a branch - //that looks like a version number - return version; - } - } else { - //Probably a branch name, give it a shot - return version; - } - }); - } - } else { - return github.versionTags(ownerRepo).then(function (tagNames) { - return tagNames[0]; - }); - } + //If ownerPlusRepo includes the version, just use that. + var parts = ownerRepoVersion.split('/'), + ownerRepo = parts[0] + '/' + parts[1], + version = parts[2]; + + //Set owner + if (version) { + //Figure out it is a semver, and if so, then find the best match. + if (semVerRangeRegExp.test(version)) { + return github.versionTags(ownerRepo).then(function (tagNames) { + return semver.maxSatisfying(tagNames, version); + }); + } else { + //Just a plain, explicit version, use it. First, check if it + //is a version tag and if it should add or remove a 'v' prefix + //to match a real tag. + return github.tags(ownerRepo).then(function (tags) { + if (tags.indexOf(version) !== -1) { + return version; + } else if (versionRegExp.test(version)) { + var tempVersion; + //May be a vVersion vs version mismatch, try to match + //the other way. + if (version.charAt(0) === 'v') { + //try without the v + tempVersion = version.substring(1); + } else { + //try with the v + tempVersion = 'v' + version; + } + if (tags.indexOf(tempVersion) !== -1) { + return tempVersion; + } else { + //Could still be a branch name, but a branch + //that looks like a version number + return version; + } + } else { + //Probably a branch name, give it a shot + return version; + } + }); + } + } else { + return github.versionTags(ownerRepo).then(function (tagNames) { + return tagNames[0]; + }); + } }; github.masterBranch = function (ownerPlusRepo) { - return github('repos/' + ownerPlusRepo).then(function (data) { - return data.master_branch || 'master'; - }); + return github('repos/' + ownerPlusRepo).then(function (data) { + return data.master_branch || 'master'; + }); }; github.search = function (query) { - return github({ - host: config.searchHost, - path: config.searchPath.replace(/\{query\}/, escape(query)), - method: 'GET' - }); + return github({ + host: config.searchHost, + path: config.searchPath.replace(/\{query\}/, escape(query)), + method: 'GET' + }); }; module.exports = github; diff --git a/lib/resolve/github.js b/lib/resolve/github.js index 62773ba..e645507 100644 --- a/lib/resolve/github.js +++ b/lib/resolve/github.js @@ -8,322 +8,354 @@ 'use strict'; var urlLib = require('url'), - archive = require('../archive'), - github = require('../github'), - githubAuth = require('../github/auth'), - net = require('../net'), - mime = require('../mime'), - ghConfig = require('../config').get().github, - search = require('../../commands/search'), - q = require('q'), - qutil = require('../qutil'), - jsSuffixRegExp = /\.js$/, - versionTagRegExp = /^(<|>|~|v)?\d+/, - versionRegExp = /\{version\}/g, - ghHostRegExp = new RegExp('(^|\\.)' + ghConfig.host + '$'); + archive = require('../archive'), + github = require('../github'), + githubAuth = require('../github/auth'), + net = require('../net'), + mime = require('../mime'), + ghConfig = require('../config').get().github, + search = require('../../commands/search'), + q = require('q'), + qutil = require('../qutil'), + jsSuffixRegExp = /\.js$/, + versionTagRegExp = /^(<|>|~|v)?\d+/, + versionRegExp = /\{version\}/g, + ghHostRegExp = new RegExp('(^|\\.)' + ghConfig.host + '$'), + useApi = false; function makeAuthHeaderOptions(url, options) { - var localAuth = githubAuth.getLocal(), - token = localAuth && localAuth.token, - args = urlLib.parse(url); - - options = options || {}; - - //Only send the auth token to github.com and only if on https. - if (args.protocol === 'https:' && ghHostRegExp.test(args.hostname)) { - if (!options.headers) { - options.headers = {}; - } - //Always set user agent. - options.headers['User-Agent'] = ghConfig.userAgent; - - if (token) { - options.headers.Authorization = 'token ' + token; - } - } - - return options; + var localAuth = githubAuth.getLocal(), + token = localAuth && localAuth.token, + args = urlLib.parse(url); + + options = options || {}; + + //Only send the auth token to github.com and only if on https. + if (args.protocol === 'https:' && ghHostRegExp.test(args.hostname)) { + if (!options.headers) { + options.headers = {}; + } + //Always set user agent. + options.headers['User-Agent'] = ghConfig.userAgent; + + // prevent base64 encoding of file contents + // not sure about API version being beta below + if (options.raw === true) { + options.headers['Accept'] = 'application/vnd.github.beta.raw'; + delete options.raw; + } + + if (token) { + options.headers.Authorization = 'token ' + token; + } + } + return options; } function resolveGithub(archiveName, fragment, options, callback, errback) { - var parts = archiveName.split('/'), - originalFragment = fragment, - d = qutil.convert(callback, errback), - isArchive = false, - isSingleFile = false, - scheme = 'github', - overrideFragmentIndex, - url, - tag, - versionOnlyTag, - ownerPlusRepo, - version, - localName, - packageInfo, - browserInfo, - precleanedLocalName, - token, - localAuth; - - function cleanLocalName(name, tag) { - //Removes any .js from the local name (added later if appropriate), - //and removes any version tag from the local name (local name should - //be version agnostic). - var regExp = new RegExp('[-\\.\\_]?' + tag + '[-\\.\\_]?'); - - //Do js regexp first in case the regexp removes the dot for the file - //extension. - return name.replace(jsSuffixRegExp, '').replace(regExp, ''); - } - - function fetchBrowserPackageJson(ownerPlusRepo, tag) { - //Check the repo for a package.json file that may have info on - //an install url or something. - var pkgUrl = github.rawUrl(ownerPlusRepo, tag, 'package.json'), - dPkg = q.defer(); - - net.getJson(pkgUrl, makeAuthHeaderOptions(pkgUrl)).then(function (pkg) { - if (pkg) { - packageInfo = pkg; - browserInfo = pkg.volo || pkg.browser; - } - dPkg.resolve(browserInfo); - }, function (err) { - //Do not care about errors, it will be common for projects - //to not have a package.json. - dPkg.resolve(); - }); - - return dPkg.promise; - } - - d.resolve(q.call(function () { - var version; - - if (parts.length === 2) { - //If second part is a version ID, then still need to do a search - if (versionTagRegExp.test(parts[1])) { - version = parts.pop(); - archiveName = parts[0]; - } - } - - if (parts.length === 1) { - //Need to do a search for a repo. - return search.api(archiveName, options).then(function (results) { - var archive; - if (results && results.length) { - //Choose the first archive name whose last segment matches - //the search or, if no match, the first result. - results.forEach(function (item) { - if (!archive) { - if (archiveName === item.archive.split('/').pop()) { - archive = item.archive; - } - } - }); - if (!archive) { - archive = results[0].archive; - } - console.log('Using github repo "' + archive + - '" for "' + archiveName + '"...'); - return archive + (version ? '/' + version : ''); - } else { - throw new Error('No search results for: ' + archiveName); - } - }); - } - return archiveName; - }).then(function (archiveName) { - parts = archiveName.split('/'); - - localName = parts[1]; - - ownerPlusRepo = parts[0] + '/' + parts[1]; - version = parts[2]; - - //Fetch the latest version - return github.latestTag(ownerPlusRepo + (version ? '/' + version : '')); - }).then(function (tagResult) { - tag = tagResult; - - //Some version tags have a 'v' prefix, remove that for token - //replacements used below. - versionOnlyTag = tag.replace(/^v/, ''); - - return fetchBrowserPackageJson(ownerPlusRepo, tag); - }).then(function (voloInfo) { - if (!voloInfo) { - //Get the master branch, then fetch its package.json - return github.masterBranch(ownerPlusRepo) - .then(function (branchName) { - return fetchBrowserPackageJson(ownerPlusRepo, branchName); - }); - } - - return voloInfo; - }).then(function (voloInfo) { - //If no voloInfo, see if there is something in the volojs/repos - //repo. - if (!voloInfo) { - var pkgDeferred = q.defer(), - pkgUrl = github.rawUrl('volojs/repos', 'master', - ownerPlusRepo + '/package.json'); - - net.getJson(pkgUrl, makeAuthHeaderOptions(pkgUrl)).then(function (pkg) { - if (pkg) { - if (!packageInfo) { - packageInfo = pkg; - } - if (pkg.volo) { - browserInfo = pkg.volo; - packageInfo.volo = browserInfo; - } - if (pkg.browser) { - browserInfo = pkg.browser; - packageInfo.browser = browserInfo; - } - } - pkgDeferred.resolve(); - }, function (err) { - //Do not care about errors, it will be common for projects - //to not have a package.json. - pkgDeferred.resolve(); - }); - return pkgDeferred.promise; - } - }).then(function () { - var zipHeadOptions; - - //Helper to set up info to an archive - function setAsArchive() { - isArchive = true; - url = github.zipballUrl(ownerPlusRepo, tag); - - //Remove any ".js" from the name since it can conflict - //with AMD loading. - localName = localName.replace(jsSuffixRegExp, ''); - } - - //If there is a specific url to finding the file, - //for instance jQuery releases are put on a CDN and are not - //committed to github, use the url. - if (fragment || (browserInfo && browserInfo.url)) { - //If a specific file in the repo, do not need the full - //zipball, just use a raw github url to get it. However, - //it may just be a directory, so check github first. - if (fragment) { - url = github.rawUrl(ownerPlusRepo, tag, fragment); - //Confirm it is for a single file. If get a 200, then - //it is a real single file (probably .js file). Otherwise - //the fragment is - return net.head(url, makeAuthHeaderOptions(url)).then(function (response) { - var index; - - //Remove any trailing slash for local name. - localName = fragment.replace(/\/$/, ''); - - //Adjust local name to be the last segment of a path. - index = localName.lastIndexOf('/'); - if (index !== -1) { - localName = localName.substring(index + 1); - } - - precleanedLocalName = localName; - localName = cleanLocalName(localName, versionOnlyTag); - - //Strip off extension name, but only if it was not - //done by cleanLocalName. - if (!jsSuffixRegExp.test(precleanedLocalName)) { - index = localName.lastIndexOf('.'); - if (index !== -1) { - localName = localName.substring(0, index); - } - } - - if (response.statusCode >= 200 && - response.statusCode < 300) { - - fragment = null; - isSingleFile = true; - } else { - //Not a single js file - setAsArchive(); - } - }); - } else { - //An browserInfo.url situation. - url = browserInfo.url.replace(versionRegExp, versionOnlyTag); - - //Pull off any fragment IDs for archive urls that just - //reference an individual file/directory. - overrideFragmentIndex = url.indexOf('#'); - - //Remove any ".js" from the name since it can conflict - //with AMD loading, and remove any version from the local name. - precleanedLocalName = localName; - localName = cleanLocalName(localName, versionOnlyTag); - - if (overrideFragmentIndex !== -1) { - //If no explicit fragment specified, then use the one - //in this browserInfo. - if (!fragment) { - fragment = url.substring(overrideFragmentIndex + 1); - - //Also update the localName - localName = cleanLocalName(fragment.split('/').pop(), versionOnlyTag); - } - url = url.substring(0, overrideFragmentIndex); - } - - //Do a HEAD request to determine the content type, if it - //is a zip file. - zipHeadOptions = { - followRedirects: true - }; - - return net.head(url, makeAuthHeaderOptions(url, zipHeadOptions)).then(function (response) { - var contentType = response.headers['content-type']; - if (mime.archiveTypes[contentType]) { - //A zip file - isArchive = true; - - //Revert localName to previous value, without any - //version replacement, since the archive is the install - //target, and no need to clean a version number from it. - localName = precleanedLocalName; - } else { - //A single file to download - //Change the localName to be the file name of the URL. - localName = cleanLocalName(url.split('/').pop(), versionOnlyTag); - - fragment = null; - isSingleFile = true; - } - }); - } - } else { - setAsArchive(); - } - }).then(function () { - var urlOptions = makeAuthHeaderOptions(url); - return { - id: scheme + ':' + ownerPlusRepo + '/' + tag + - (originalFragment ? '#' + originalFragment : ''), - scheme: scheme, - url: url, - urlHeaders: urlOptions.headers, - isArchive: isArchive, - isSingleFile: isSingleFile, - fragment: fragment, - localName: localName, - packageInfo: packageInfo, - ownerPlusRepo: ownerPlusRepo - }; - })); - - return d.promise; + var parts = archiveName.split('/'), + originalFragment = fragment, + d = qutil.convert(callback, errback), + isArchive = false, + isSingleFile = false, + scheme = 'github', + overrideFragmentIndex, + url, + tag, + versionOnlyTag, + ownerPlusRepo, + version, + localName, + packageInfo, + browserInfo, + precleanedLocalName, + token, + localAuth; + + function cleanLocalName(name, tag) { + //Removes any .js from the local name (added later if appropriate), + //and removes any version tag from the local name (local name should + //be version agnostic). + var regExp = new RegExp('[-\\.\\_]?' + tag + '[-\\.\\_]?'); + + //Do js regexp first in case the regexp removes the dot for the file + //extension. + return name.replace(jsSuffixRegExp, '').replace(regExp, ''); + } + + function fetchBrowserPackageJson(ownerPlusRepo, tag) { + //Check the repo for a package.json file that may have info on + //an install url or something. + + // if we have a token, then we'll use the API + // if not, assume public repo and use the raw URL + var localAuth = githubAuth.getLocal(), + options = null, + token = localAuth && localAuth.token; + if (token) { + // global, we'll use this later as well + useApi = true; + options = { raw: true }; + } + + var pkgUrl = (useApi === true ? github.rawApiUrl(ownerPlusRepo, tag, 'package.json') : github.rawUrl(ownerPlusRepo, tag, 'package.json')); + var dPkg = q.defer(); + + // default to raw being true since that works on both public and private repos + // and the check for auth already have auth info at this point + net.getJson(pkgUrl, makeAuthHeaderOptions(pkgUrl, options)).then(function (pkg) { + if (pkg) { + packageInfo = pkg; + browserInfo = pkg.volo || pkg.browser; + } + dPkg.resolve(browserInfo); + }, function (err) { + console.log('Cannot download package.json for repo, but continuing.'); + console.log('If this is a private repo, the volo.url property will NOT be used for downloading.'); + dPkg.resolve(); + }); + + return dPkg.promise; + } + + d.resolve(q.call(function () { + var version; + + if (parts.length === 2) { + //If second part is a version ID, then still need to do a search + if (versionTagRegExp.test(parts[1])) { + version = parts.pop(); + archiveName = parts[0]; + } + } + + if (parts.length === 1) { + //Need to do a search for a repo. + return search.api(archiveName, options).then(function (results) { + var archive; + if (results && results.length) { + //Choose the first archive name whose last segment matches + //the search or, if no match, the first result. + results.forEach(function (item) { + if (!archive) { + if (archiveName === item.archive.split('/').pop()) { + archive = item.archive; + } + } + }); + if (!archive) { + archive = results[0].archive; + } + console.log('Using github repo "' + archive + + '" for "' + archiveName + '"...'); + return archive + (version ? '/' + version : ''); + } else { + throw new Error('No search results for: ' + archiveName); + } + }); + } + return archiveName; + }).then(function (archiveName) { + parts = archiveName.split('/'); + + localName = parts[1]; + + ownerPlusRepo = parts[0] + '/' + parts[1]; + version = parts[2]; + + //Fetch the latest version + return github.latestTag(ownerPlusRepo + (version ? '/' + version : '')); + }).then(function (tagResult) { + tag = tagResult; + + //Some version tags have a 'v' prefix, remove that for token + //replacements used below. + versionOnlyTag = tag.replace(/^v/, ''); + + return fetchBrowserPackageJson(ownerPlusRepo, tag); + }).then(function (voloInfo) { + if (!voloInfo) { + //Get the master branch, then fetch its package.json + return github.masterBranch(ownerPlusRepo) + .then(function (branchName) { + return fetchBrowserPackageJson(ownerPlusRepo, branchName); + }); + } + + return voloInfo; + }).then(function (voloInfo) { + //If no voloInfo, see if there is something in the volojs/repos + //repo. + if (!voloInfo) { + var pkgDeferred = q.defer(), + pkgUrl = github.rawUrl('volojs/repos', 'master', + ownerPlusRepo + '/package.json'); + + net.getJson(pkgUrl, makeAuthHeaderOptions(pkgUrl)).then(function (pkg) { + if (pkg) { + if (!packageInfo) { + packageInfo = pkg; + } + if (pkg.volo) { + browserInfo = pkg.volo; + packageInfo.volo = browserInfo; + } + if (pkg.browser) { + browserInfo = pkg.browser; + packageInfo.browser = browserInfo; + } + } + pkgDeferred.resolve(); + }, function (err) { + //Do not care about errors, it will be common for projects + //to not have a package.json. + pkgDeferred.resolve(); + }); + return pkgDeferred.promise; + } + }).then(function () { + var zipHeadOptions; + + //Helper to set up info to an archive + function setAsArchive() { + isArchive = true; + url = github.zipballUrl(ownerPlusRepo, tag); + + //Remove any ".js" from the name since it can conflict + //with AMD loading. + localName = localName.replace(jsSuffixRegExp, ''); + } + + //If there is a specific url to finding the file, + //for instance jQuery releases are put on a CDN and are not + //committed to github, use the url. + if (fragment || (browserInfo && browserInfo.url)) { + //If a specific file in the repo, do not need the full + //zipball, just use a raw github url to get it. However, + //it may just be a directory, so check github first. + if (fragment) { + url = github.rawUrl(ownerPlusRepo, tag, fragment); + //Confirm it is for a single file. If get a 200, then + //it is a real single file (probably .js file). Otherwise + //the fragment is + return net.head(url, makeAuthHeaderOptions(url)).then(function (response) { + var index; + + //Remove any trailing slash for local name. + localName = fragment.replace(/\/$/, ''); + + //Adjust local name to be the last segment of a path. + index = localName.lastIndexOf('/'); + if (index !== -1) { + localName = localName.substring(index + 1); + } + + precleanedLocalName = localName; + localName = cleanLocalName(localName, versionOnlyTag); + + //Strip off extension name, but only if it was not + //done by cleanLocalName. + if (!jsSuffixRegExp.test(precleanedLocalName)) { + index = localName.lastIndexOf('.'); + if (index !== -1) { + localName = localName.substring(0, index); + } + } + + if (response.statusCode >= 200 && + response.statusCode < 300) { + + fragment = null; + isSingleFile = true; + } else { + //Not a single js file + setAsArchive(); + } + }); + } else { + //An browserInfo.url situation. + url = browserInfo.url.replace(versionRegExp, versionOnlyTag); + + //Pull off any fragment IDs for archive urls that just + //reference an individual file/directory. + overrideFragmentIndex = url.indexOf('#'); + + //Remove any ".js" from the name since it can conflict + //with AMD loading, and remove any version from the local name. + precleanedLocalName = localName; + localName = cleanLocalName(localName, versionOnlyTag); + + if (overrideFragmentIndex !== -1) { + //If no explicit fragment specified, then use the one + //in this browserInfo. + if (!fragment) { + fragment = url.substring(overrideFragmentIndex + 1); + + //Also update the localName + localName = cleanLocalName(fragment.split('/').pop(), versionOnlyTag); + } + url = url.substring(0, overrideFragmentIndex); + } + + //Do a HEAD request to determine the content type, if it + //is a zip file. + zipHeadOptions = { + followRedirects: true + }; + + return net.head(url, makeAuthHeaderOptions(url, zipHeadOptions)).then(function (response) { + var contentType = response.headers['content-type']; + if (mime.archiveTypes[contentType]) { + //A zip file + isArchive = true; + + //Revert localName to previous value, without any + //version replacement, since the archive is the install + //target, and no need to clean a version number from it. + localName = precleanedLocalName; + } else { + //A single file to download + //Change the localName to be the file name of the URL. + localName = cleanLocalName(url.split('/').pop(), versionOnlyTag); + + fragment = null; + isSingleFile = true; + } + }); + } + } else { + setAsArchive(); + } + }).then(function () { + // may need to use the API is private repo + var options = (useApi === true ? { raw: true } : null); + if (useApi === true) { + var path = url.split(ghConfig.host)[1].split(ownerPlusRepo)[1].split(tag)[1].substr(1); + // this still fails but may not be worth pursuing further + // the API limits items to 1 MB in size + // better path may be to list the repo contents and use + // volo commands to delete all files EXCEPT the once specified as the download + url = github.rawApiUrl(ownerPlusRepo, tag, path); + } + console.log(url); + var urlOptions = makeAuthHeaderOptions(url, options); + return { + id: scheme + ':' + ownerPlusRepo + '/' + tag + + (originalFragment ? '#' + originalFragment : ''), + scheme: scheme, + url: url, + urlHeaders: urlOptions.headers, + isArchive: isArchive, + isSingleFile: isSingleFile, + fragment: fragment, + localName: localName, + packageInfo: packageInfo, + ownerPlusRepo: ownerPlusRepo + }; + })); + + return d.promise; } module.exports = resolveGithub;