fix: Bump inquirer to v3 and promisify everything (#34)

BREAKING CHANGE: Drop support for Node < v4. This uses native Promises available from Node v4.

* fix: Bump inquirer to v3.0.1. Fixes #33 to improve Windows support.

* refactor: Promisify everything as inquirer uses Promises from 1.0.0 onwards
This commit is contained in:
Jeroen Engels 2017-02-15 22:25:32 +01:00 committed by GitHub
parent 1e73194f54
commit 1305a7cd92
14 changed files with 164 additions and 193 deletions

73
cli.js
View file

@ -39,39 +39,31 @@ var argv = yargs
}) })
.argv; .argv;
function startGeneration(argv, cb) { function startGeneration(argv) {
argv.files return Promise.all(
.map(function (file) { argv.files.map(file => {
return path.join(cwd, file); const filePath = path.join(cwd, file);
}) return util.markdown.read(filePath)
.forEach(function (file) { .then(fileContent => {
util.markdown.read(file, function (error, fileContent) {
if (error) {
return cb(error);
}
var newFileContent = generate(argv, argv.contributors, fileContent); var newFileContent = generate(argv, argv.contributors, fileContent);
util.markdown.write(file, newFileContent, cb); return util.markdown.write(filePath, newFileContent);
}); });
}); })
);
} }
function addContribution(argv, cb) { function addContribution(argv) {
var username = argv._[1]; var username = argv._[1];
var contributions = argv._[2]; var contributions = argv._[2];
// Add or update contributor in the config file // Add or update contributor in the config file
updateContributors(argv, username, contributions, function (error, data) { return updateContributors(argv, username, contributions)
if (error) { .then(data => {
return onError(error);
}
argv.contributors = data.contributors; argv.contributors = data.contributors;
startGeneration(argv, function (error) { return startGeneration(argv)
if (error) { .then(() => {
return cb(error); if (argv.commit) {
return util.git.commit(argv, data);
} }
if (!argv.commit) {
return cb();
}
return util.git.commit(argv, data, cb);
}); });
}); });
} }
@ -81,10 +73,10 @@ function onError(error) {
console.error(error.message); console.error(error.message);
process.exit(1); process.exit(1);
} }
process.exit(); process.exit(0);
} }
function promptForCommand(argv, cb) { function promptForCommand(argv) {
var questions = [{ var questions = [{
type: 'list', type: 'list',
name: 'command', name: 'command',
@ -99,17 +91,24 @@ function promptForCommand(argv, cb) {
when: !argv._[0], when: !argv._[0],
default: 0 default: 0
}]; }];
inquirer.prompt(questions, function treatAnswers(answers) {
return cb(answers.command || argv._[0]); return inquirer.prompt(questions)
.then(answers => {
return answers.command || argv._[0];
}); });
} }
promptForCommand(argv, function (command) { promptForCommand(argv)
if (command === 'init') { .then(command => {
init(onError); switch (command) {
} else if (command === 'generate') { case 'init':
startGeneration(argv, onError); return init();
} else if (command === 'add') { case 'generate':
addContribution(argv, onError); return startGeneration(argv);
} case 'add':
}); return addContribution(argv);
default:
throw new Error(`Unknown command ${command}`);
}
})
.catch(onError);

View file

@ -30,21 +30,19 @@ function updateExistingContributor(options, username, contributions) {
}); });
} }
function addNewContributor(options, username, contributions, infoFetcher, cb) { function addNewContributor(options, username, contributions, infoFetcher) {
infoFetcher(username, function (error, userData) { return infoFetcher(username)
if (error) { .then(userData => {
return cb(error);
}
var contributor = _.assign(userData, { var contributor = _.assign(userData, {
contributions: formatContributions(options, [], contributions) contributions: formatContributions(options, [], contributions)
}); });
return cb(null, options.contributors.concat(contributor)); return options.contributors.concat(contributor);
}); });
} }
module.exports = function addContributor(options, username, contributions, infoFetcher, cb) { module.exports = function addContributor(options, username, contributions, infoFetcher) {
if (_.find({login: username}, options.contributors)) { if (_.find({login: username}, options.contributors)) {
return cb(null, updateExistingContributor(options, username, contributions)); return Promise.resolve(updateExistingContributor(options, username, contributions));
} }
return addNewContributor(options, username, contributions, infoFetcher, cb); return addNewContributor(options, username, contributions, infoFetcher);
}; };

View file

@ -1,8 +1,8 @@
import test from 'ava'; import test from 'ava';
import addContributor from './add'; import addContributor from './add';
function mockInfoFetcher(username, cb) { function mockInfoFetcher(username) {
return cb(null, { return Promise.resolve({
login: username, login: username,
name: 'Some name', name: 'Some name',
avatar_url: 'www.avatar.url', avatar_url: 'www.avatar.url',
@ -34,27 +34,27 @@ function fixtures() {
return {options}; return {options};
} }
test.cb('should callback with error if infoFetcher fails', t => { test('should callback with error if infoFetcher fails', t => {
const {options} = fixtures(); const {options} = fixtures();
const username = 'login3'; const username = 'login3';
const contributions = ['doc']; const contributions = ['doc'];
function infoFetcher(username, cb) { function infoFetcher() {
return cb(new Error('infoFetcher error')); return Promise.reject(new Error('infoFetcher error'));
} }
return addContributor(options, username, contributions, infoFetcher, function (error) { return t.throws(
t.is(error.message, 'infoFetcher error'); addContributor(options, username, contributions, infoFetcher),
t.end(); 'infoFetcher error'
}); );
}); });
test.cb('should add new contributor at the end of the list of contributors', t => { test('should add new contributor at the end of the list of contributors', t => {
const {options} = fixtures(); const {options} = fixtures();
const username = 'login3'; const username = 'login3';
const contributions = ['doc']; const contributions = ['doc'];
return addContributor(options, username, contributions, mockInfoFetcher, function (error, contributors) { return addContributor(options, username, contributions, mockInfoFetcher)
t.falsy(error); .then(contributors => {
t.is(contributors.length, 3); t.is(contributors.length, 3);
t.deepEqual(contributors[2], { t.deepEqual(contributors[2], {
login: 'login3', login: 'login3',
@ -65,18 +65,17 @@ test.cb('should add new contributor at the end of the list of contributors', t =
'doc' 'doc'
] ]
}); });
t.end();
}); });
}); });
test.cb('should add new contributor at the end of the list of contributors with a url link', t => { test('should add new contributor at the end of the list of contributors with a url link', t => {
const {options} = fixtures(); const {options} = fixtures();
const username = 'login3'; const username = 'login3';
const contributions = ['doc']; const contributions = ['doc'];
options.url = 'www.foo.bar'; options.url = 'www.foo.bar';
return addContributor(options, username, contributions, mockInfoFetcher, function (error, contributors) { return addContributor(options, username, contributions, mockInfoFetcher)
t.falsy(error); .then(contributors => {
t.is(contributors.length, 3); t.is(contributors.length, 3);
t.deepEqual(contributors[2], { t.deepEqual(contributors[2], {
login: 'login3', login: 'login3',
@ -87,28 +86,26 @@ test.cb('should add new contributor at the end of the list of contributors with
{type: 'doc', url: 'www.foo.bar'} {type: 'doc', url: 'www.foo.bar'}
] ]
}); });
t.end();
}); });
}); });
test.cb(`should not update an existing contributor's contributions where nothing has changed`, t => { test(`should not update an existing contributor's contributions where nothing has changed`, t => {
const {options} = fixtures(); const {options} = fixtures();
const username = 'login2'; const username = 'login2';
const contributions = ['blog', 'code']; const contributions = ['blog', 'code'];
return addContributor(options, username, contributions, mockInfoFetcher, function (error, contributors) { return addContributor(options, username, contributions, mockInfoFetcher)
t.falsy(error); .then(contributors => {
t.deepEqual(contributors, options.contributors); t.deepEqual(contributors, options.contributors);
t.end();
}); });
}); });
test.cb(`should update an existing contributor's contributions if a new type is added`, t => { test(`should update an existing contributor's contributions if a new type is added`, t => {
const {options} = fixtures(); const {options} = fixtures();
const username = 'login1'; const username = 'login1';
const contributions = ['bug']; const contributions = ['bug'];
return addContributor(options, username, contributions, mockInfoFetcher, function (error, contributors) { return addContributor(options, username, contributions, mockInfoFetcher)
t.falsy(error); .then(contributors => {
t.is(contributors.length, 2); t.is(contributors.length, 2);
t.deepEqual(contributors[0], { t.deepEqual(contributors[0], {
login: 'login1', login: 'login1',
@ -120,18 +117,17 @@ test.cb(`should update an existing contributor's contributions if a new type is
'bug' 'bug'
] ]
}); });
t.end();
}); });
}); });
test.cb(`should update an existing contributor's contributions if a new type is added with a link`, t => { test(`should update an existing contributor's contributions if a new type is added with a link`, t => {
const {options} = fixtures(); const {options} = fixtures();
const username = 'login1'; const username = 'login1';
const contributions = ['bug']; const contributions = ['bug'];
options.url = 'www.foo.bar'; options.url = 'www.foo.bar';
return addContributor(options, username, contributions, mockInfoFetcher, function (error, contributors) { return addContributor(options, username, contributions, mockInfoFetcher)
t.falsy(error); .then(contributors => {
t.is(contributors.length, 2); t.is(contributors.length, 2);
t.deepEqual(contributors[0], { t.deepEqual(contributors[0], {
login: 'login1', login: 'login1',
@ -143,6 +139,5 @@ test.cb(`should update an existing contributor's contributions if a new type is
{type: 'bug', url: 'www.foo.bar'} {type: 'bug', url: 'www.foo.bar'}
] ]
}); });
t.end();
}); });
}); });

View file

@ -1,24 +1,22 @@
'use strict'; 'use strict';
var request = require('request'); var pify = require('pify');
var request = pify(require('request'));
module.exports = function getUserInfo(username, cb) { module.exports = function getUserInfo(username) {
request.get({ return request.get({
url: 'https://api.github.com/users/' + username, url: 'https://api.github.com/users/' + username,
headers: { headers: {
'User-Agent': 'request' 'User-Agent': 'request'
} }
}, function (error, res) { })
if (error) { .then(res => {
return cb(error);
}
var body = JSON.parse(res.body); var body = JSON.parse(res.body);
var user = { return {
login: body.login, login: body.login,
name: body.name || username, name: body.name || username,
avatar_url: body.avatar_url, avatar_url: body.avatar_url,
profile: body.blog || body.html_url profile: body.blog || body.html_url
}; };
return cb(null, user);
}); });
}; };

View file

@ -2,18 +2,15 @@ import test from 'ava';
import nock from 'nock'; import nock from 'nock';
import getUserInfo from './github'; import getUserInfo from './github';
test.cb('should handle errors', t => { test('should handle errors', t => {
nock('https://api.github.com') nock('https://api.github.com')
.get('/users/nodisplayname') .get('/users/nodisplayname')
.replyWithError(404); .replyWithError(404);
getUserInfo('nodisplayname', err => { return t.throws(getUserInfo('nodisplayname'));
t.truthy(err);
t.end();
});
}); });
test.cb('should fill in the name when null is returned', t => { test('should fill in the name when null is returned', t => {
nock('https://api.github.com') nock('https://api.github.com')
.get('/users/nodisplayname') .get('/users/nodisplayname')
.reply(200, { .reply(200, {
@ -23,14 +20,13 @@ test.cb('should fill in the name when null is returned', t => {
html_url: 'https://github.com/nodisplayname' html_url: 'https://github.com/nodisplayname'
}); });
getUserInfo('nodisplayname', (err, info) => { return getUserInfo('nodisplayname')
t.falsy(err); .then(info => {
t.is(info.name, 'nodisplayname'); t.is(info.name, 'nodisplayname');
t.end();
}); });
}); });
test.cb('should fill in the name when an empty string is returned', t => { test('should fill in the name when an empty string is returned', t => {
nock('https://api.github.com') nock('https://api.github.com')
.get('/users/nodisplayname') .get('/users/nodisplayname')
.reply(200, { .reply(200, {
@ -40,9 +36,8 @@ test.cb('should fill in the name when an empty string is returned', t => {
html_url: 'https://github.com/nodisplayname' html_url: 'https://github.com/nodisplayname'
}); });
getUserInfo('nodisplayname', (err, info) => { return getUserInfo('nodisplayname')
t.falsy(err); .then(info => {
t.is(info.name, 'nodisplayname'); t.is(info.name, 'nodisplayname');
t.end();
}); });
}); });

View file

@ -10,20 +10,24 @@ function isNewContributor(contributorList, username) {
return !_.find({login: username}, contributorList); return !_.find({login: username}, contributorList);
} }
module.exports = function addContributor(options, username, contributions, cb) { module.exports = function addContributor(options, username, contributions) {
prompt(options, username, contributions, function (answers) { const answersP = prompt(options, username, contributions);
add(options, answers.username, answers.contributions, github, function (error, contributors) { const contributorsP = answersP
if (error) { .then(answers => add(options, answers.username, answers.contributions, github));
return cb(error);
} const writeContributorsP = contributorsP.then(
util.configFile.writeContributors(options.config, contributors, function (error) { contributors => util.configFile.writeContributors(options.config, contributors)
return cb(error, { );
username: answers.username,
contributions: answers.contributions, return Promise.all([answersP, contributorsP, writeContributorsP])
contributors: contributors, .then(res => {
newContributor: isNewContributor(options.contributors, answers.username) const answers = res[0];
}); const contributors = res[1];
}); return {
}); username: answers.username,
contributions: answers.contributions,
contributors: contributors,
newContributor: isNewContributor(options.contributors, answers.username)
};
}); });
}; };

View file

@ -33,14 +33,12 @@ function getQuestions(options, username, contributions) {
}]; }];
} }
module.exports = function prompt(options, username, contributions, cb) { module.exports = function prompt(options, username, contributions) {
var defaults = { var defaults = {
username: username, username: username,
contributions: contributions && contributions.split(',') contributions: contributions && contributions.split(',')
}; };
var questions = getQuestions(options, username, contributions); var questions = getQuestions(options, username, contributions);
inquirer.prompt(questions, _.flow( return inquirer.prompt(questions)
_.assign(defaults), .then(_.assign(defaults));
cb
));
}; };

View file

@ -1,35 +1,25 @@
'use strict'; 'use strict';
var _ = require('lodash/fp');
var series = require('async/series');
var util = require('../util'); var util = require('../util');
var prompt = require('./prompt'); var prompt = require('./prompt');
var initContent = require('./init-content'); var initContent = require('./init-content');
var configFile = util.configFile; var configFile = util.configFile;
var markdown = util.markdown; var markdown = util.markdown;
function injectInFile(file, fn, cb) { function injectInFile(file, fn) {
markdown.read(file, function (error, content) { return markdown.read(file)
if (error) { .then(content => markdown.write(file, fn(content)));
return cb(error);
}
markdown.write(file, fn(content), cb);
});
} }
module.exports = function init(callback) { module.exports = function init() {
prompt(function postPrompt(result) { return prompt()
var tasks = [ .then(result => {
function writeConfig(cb) { return configFile.writeConfig('.all-contributorsrc', result.config)
configFile.writeConfig('.all-contributorsrc', result.config, cb); .then(() => injectInFile(result.contributorFile, initContent.addContributorsList))
}, .then(() => {
function addContributorsList(cb) { if (result.badgeFile) {
injectInFile(result.contributorFile, initContent.addContributorsList, cb); return injectInFile(result.badgeFile, initContent.addBadge);
}, }
result.badgeFile && function addBadge(cb) { });
injectInFile(result.badgeFile, initContent.addBadge, cb); });
}
];
series(_.compact(tasks), callback);
});
}; };

View file

@ -49,29 +49,27 @@ var uniqueFiles = _.flow(
_.uniq _.uniq
); );
module.exports = function prompt(cb) { module.exports = function prompt() {
git.getRepoInfo(function (error, repoInfo) { return git.getRepoInfo()
if (error) { .then(repoInfo => {
return cb(error);
}
if (repoInfo) { if (repoInfo) {
questions[0].default = repoInfo.projectName; questions[0].default = repoInfo.projectName;
questions[1].default = repoInfo.projectOwner; questions[1].default = repoInfo.projectOwner;
} }
inquirer.prompt(questions, function treatAnswers(answers) { return inquirer.prompt(questions);
var config = { })
.then(answers => {
return {
config: {
projectName: answers.projectName, projectName: answers.projectName,
projectOwner: answers.projectOwner, projectOwner: answers.projectOwner,
files: uniqueFiles([answers.contributorFile, answers.badgeFile]), files: uniqueFiles([answers.contributorFile, answers.badgeFile]),
imageSize: answers.imageSize, imageSize: answers.imageSize,
commit: answers.commit, commit: answers.commit,
contributors: [] contributors: []
}; },
return cb({ contributorFile: answers.contributorFile,
config: config, badgeFile: answers.badgeFile
contributorFile: answers.contributorFile, };
badgeFile: answers.badgeFile
});
});
}); });
}; };

View file

@ -1,6 +1,7 @@
'use strict'; 'use strict';
var fs = require('fs'); var fs = require('fs');
var pify = require('pify');
var _ = require('lodash/fp'); var _ = require('lodash/fp');
function readConfig(configPath) { function readConfig(configPath) {
@ -14,19 +15,19 @@ function readConfig(configPath) {
} }
} }
function writeConfig(configPath, content, cb) { function writeConfig(configPath, content) {
return fs.writeFile(configPath, JSON.stringify(content, null, 2) + '\n', cb); return pify(fs.writeFile)(configPath, JSON.stringify(content, null, 2) + '\n');
} }
function writeContributors(configPath, contributors, cb) { function writeContributors(configPath, contributors) {
var config; var config;
try { try {
config = readConfig(configPath); config = readConfig(configPath);
} catch (error) { } catch (error) {
return cb(error); return Promise.reject(error);
} }
var content = _.assign(config, {contributors: contributors}); var content = _.assign(config, {contributors: contributors});
return writeConfig(configPath, content, cb); return writeConfig(configPath, content);
} }
module.exports = { module.exports = {

View file

@ -5,14 +5,9 @@ const absentFile = './abc';
const expected = 'Configuration file not found: ' + absentFile; const expected = 'Configuration file not found: ' + absentFile;
test('Reading an absent configuration file throws a helpful error', t => { test('Reading an absent configuration file throws a helpful error', t => {
t.throws(() => { t.throws(() => configFile.readConfig(absentFile), expected);
configFile.readConfig(absentFile);
}, expected);
}); });
test.cb('Writing contributors in an absent configuration file throws a helpful error', t => { test('Writing contributors in an absent configuration file throws a helpful error', t => {
configFile.writeContributors(absentFile, [], error => { t.throws(configFile.writeContributors(absentFile, []), expected);
t.is(error.message, expected);
t.end();
});
}); });

View file

@ -3,10 +3,11 @@
var path = require('path'); var path = require('path');
var spawn = require('child_process').spawn; var spawn = require('child_process').spawn;
var _ = require('lodash/fp'); var _ = require('lodash/fp');
var pify = require('pify');
var commitTemplate = '<%= (newContributor ? "Add" : "Update") %> @<%= username %> as a contributor'; var commitTemplate = '<%= (newContributor ? "Add" : "Update") %> @<%= username %> as a contributor';
function getRemoteOriginData(cb) { var getRemoteOriginData = pify(cb => {
var output = ''; var output = '';
var git = spawn('git', 'config --get remote.origin.url'.split(' ')); var git = spawn('git', 'config --get remote.origin.url'.split(' '));
git.stdout.on('data', function (data) { git.stdout.on('data', function (data) {
@ -17,7 +18,7 @@ function getRemoteOriginData(cb) {
git.on('close', function () { git.on('close', function () {
cb(null, output); cb(null, output);
}); });
} });
function parse(originUrl) { function parse(originUrl) {
var result = /:(\w+)\/([A-Za-z0-9-_]+)/.exec(originUrl); var result = /:(\w+)\/([A-Za-z0-9-_]+)/.exec(originUrl);
@ -31,33 +32,27 @@ function parse(originUrl) {
}; };
} }
function getRepoInfo(cb) { function getRepoInfo() {
getRemoteOriginData(function (error, originUrl) { return getRemoteOriginData()
if (error) { .then(parse);
return cb(error);
}
return cb(null, parse(originUrl));
});
} }
function spawnGitCommand(args, cb) { var spawnGitCommand = pify((args, cb) => {
var git = spawn('git', args); var git = spawn('git', args);
git.stderr.on('data', cb); git.stderr.on('data', cb);
git.on('close', cb); git.on('close', cb);
} });
function commit(options, data, cb) { function commit(options, data) {
var files = options.files.concat(options.config); var files = options.files.concat(options.config);
var absolutePathFiles = files.map(function (file) { var absolutePathFiles = files.map(file => {
return path.resolve(process.cwd(), file); return path.resolve(process.cwd(), file);
}); });
spawnGitCommand(['add'].concat(absolutePathFiles), function (error) { return spawnGitCommand(['add'].concat(absolutePathFiles))
if (error) { .then(() => {
return cb(error); var commitMessage = _.template(options.commitTemplate || commitTemplate)(data);
} return spawnGitCommand(['commit', '-m', commitMessage]);
var commitMessage = _.template(options.commitTemplate || commitTemplate)(data); });
spawnGitCommand(['commit', '-m', commitMessage], cb);
});
} }
module.exports = { module.exports = {

View file

@ -1,13 +1,14 @@
'use strict'; 'use strict';
var fs = require('fs'); var fs = require('fs');
var pify = require('pify');
function read(filePath, cb) { function read(filePath) {
fs.readFile(filePath, 'utf8', cb); return pify(fs.readFile)(filePath, 'utf8');
} }
function write(filePath, content, cb) { function write(filePath, content) {
fs.writeFile(filePath, content, cb); return pify(fs.writeFile)(filePath, content);
} }
function injectContentBetween(lines, content, startIndex, endIndex) { function injectContentBetween(lines, content, startIndex, endIndex) {

View file

@ -5,6 +5,9 @@
"bin": { "bin": {
"all-contributors": "cli.js" "all-contributors": "cli.js"
}, },
"engines": {
"node": ">=4"
},
"scripts": { "scripts": {
"test": "xo && nyc ava", "test": "xo && nyc ava",
"semantic-release": "semantic-release pre && npm publish && semantic-release post" "semantic-release": "semantic-release pre && npm publish && semantic-release post"
@ -25,8 +28,9 @@
"homepage": "https://github.com/jfmengels/all-contributors-cli#readme", "homepage": "https://github.com/jfmengels/all-contributors-cli#readme",
"dependencies": { "dependencies": {
"async": "^2.0.0-rc.1", "async": "^2.0.0-rc.1",
"inquirer": "^0.12.0", "inquirer": "^3.0.1",
"lodash": "^4.11.2", "lodash": "^4.11.2",
"pify": "^2.3.0",
"request": "^2.72.0", "request": "^2.72.0",
"yargs": "^4.7.0" "yargs": "^4.7.0"
}, },