diff --git a/lib/actions/install.js b/lib/actions/install.js index 175af7e3..38e56c3d 100644 --- a/lib/actions/install.js +++ b/lib/actions/install.js @@ -15,66 +15,61 @@ const install = module.exports; * Combine package manager cmd line arguments and run the `install` command. * * During the `install` step, every command will be scheduled to run once, on the - * run loop. (So don't combine the callback with `this.async()`) + * run loop. This means you can use `Promise.then` to log information, but don't + * return it or mix it with `this.async` as it'll dead lock the process. * * @param {String} installer Which package manager to use * @param {String|Array} [paths] Packages to install. Use an empty string for `npm install` * @param {Object} [options] Options to pass to `dargs` as arguments - * @param {Function} [cb] * @param {Object} [spawnOptions] Options to pass `child_process.spawn`. ref * https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options + * @return {Promise} Resolved on installation success, rejected otherwise */ -install.runInstall = function (installer, paths, options, cb, spawnOptions) { - if (!cb && _.isFunction(options)) { - cb = options; - options = {}; - } - - options = options || {}; - spawnOptions = spawnOptions || {}; - cb = cb || (() => {}); - paths = Array.isArray(paths) ? paths : (paths && paths.split(' ')) || []; - - let args = ['install'].concat(paths).concat(dargs(options)); - - // Yarn uses the `add` command to specifically add a package to a project - if (installer === 'yarn' && paths.length > 0) { - args[0] = 'add'; - } - - // Only for npm, use a minimum cache of one day - if (installer === 'npm') { - args = args.concat(['--cache-min', 24 * 60 * 60]); - } - - // Return early if we're skipping installation - if (this.options.skipInstall) { - cb(); - return this; - } - - this.env.runLoop.add('install', done => { - this.emit(`${installer}Install`, paths); - this.spawnCommand(installer, args, spawnOptions) - .on('error', err => { - console.log(chalk.red('Could not finish installation. \n') + - 'Please install ' + installer + ' with ' + - chalk.yellow('npm install -g ' + installer) + ' and try again.' - ); - cb(err); - }) - .on('exit', err => { - this.emit(`${installer}Install:end`, paths); - cb(err); - done(); - }); - }, { - once: installer + ' ' + args.join(' '), - run: false +install.runInstall = function (installer, paths, options, spawnOptions) { + return new Promise((resolve, reject) => { + options = options || {}; + spawnOptions = spawnOptions || {}; + paths = Array.isArray(paths) ? paths : (paths && paths.split(' ')) || []; + + let args = ['install'].concat(paths).concat(dargs(options)); + + // Yarn uses the `add` command to specifically add a package to a project + if (installer === 'yarn' && paths.length > 0) { + args[0] = 'add'; + } + + // Only for npm, use a minimum cache of one day + if (installer === 'npm') { + args = args.concat(['--cache-min', 24 * 60 * 60]); + } + + // Return early if we're skipping installation + if (this.options.skipInstall) { + return resolve(); + } + + this.env.runLoop.add('install', done => { + this.emit(`${installer}Install`, paths); + this.spawnCommand(installer, args, spawnOptions) + .on('error', err => { + console.log(chalk.red('Could not finish installation. \n') + + 'Please install ' + installer + ' with ' + + chalk.yellow('npm install -g ' + installer) + ' and try again.' + ); + reject(err); + done(); + }) + .on('exit', () => { + this.emit(`${installer}Install:end`, paths); + resolve(); + done(); + }); + }, { + once: installer + ' ' + args.join(' '), + run: false + }); }); - - return this; }; /** @@ -84,18 +79,15 @@ install.runInstall = function (installer, paths, options, cb, spawnOptions) { * @example * this.installDependencies({ * bower: true, - * npm: true, - * callback: function () { - * console.log('Everything is ready!'); - * } - * }); + * npm: true + * }).then(() => console.log('Everything is ready!')); * * @param {Object} [options] * @param {Boolean} [options.npm=true] - whether to run `npm install` * @param {Boolean} [options.bower=true] - whether to run `bower install` * @param {Boolean} [options.yarn=false] - whether to run `yarn install` * @param {Boolean} [options.skipMessage=false] - whether to log the used commands - * @param {Function} [options.callback] - call once all commands have run + * @return {Promise} Resolved once done, rejected if errors */ install.installDependencies = function (options) { options = options || {}; @@ -108,30 +100,33 @@ install.installDependencies = function (options) { '<% if (!skipInstall) { %> If this fails, try running the command yourself.<% } %>\n\n') }; - if (_.isFunction(options)) { - options = { - callback: options - }; - } - if (options.npm !== false) { msg.commands.push('npm install'); commands.push(cb => { - this.npmInstall(null, null, cb); + this.npmInstall(null, null).then( + val => cb(null, val), + cb + ); }); } if (options.yarn === true) { msg.commands.push('yarn install'); commands.push(cb => { - this.yarnInstall(null, null, cb); + this.yarnInstall(null, null).then( + val => cb(null, val), + cb + ); }); } if (options.bower !== false) { msg.commands.push('bower install'); commands.push(cb => { - this.bowerInstall(null, null, cb); + this.bowerInstall(null, null).then( + val => cb(null, val), + cb + ); }); } @@ -146,7 +141,14 @@ install.installDependencies = function (options) { this.log(msg.template(tplValues)); } - async.parallel(commands, options.callback || _.noop); + return new Promise((resolve, reject) => { + async.parallel(commands, (err, results) => { + if (err) { + return reject(err); + } + resolve(results); + }); + }); }; /** @@ -156,11 +158,11 @@ install.installDependencies = function (options) { * * @param {String|Array} [cmpnt] Components to install * @param {Object} [options] Options to pass to `dargs` as arguments - * @param {Function} [cb] * @param {Object} [spawnOptions] Options to pass `child_process.spawn`. + * @return {Promise} Resolved if install successful, rejected otherwise */ -install.bowerInstall = function (cmpnt, options, cb, spawnOptions) { - return this.runInstall('bower', cmpnt, options, cb, spawnOptions); +install.bowerInstall = function (cmpnt, options, spawnOptions) { + return this.runInstall('bower', cmpnt, options, spawnOptions); }; /** @@ -170,11 +172,11 @@ install.bowerInstall = function (cmpnt, options, cb, spawnOptions) { * * @param {String|Array} [pkgs] Packages to install * @param {Object} [options] Options to pass to `dargs` as arguments - * @param {Function} [cb] * @param {Object} [spawnOptions] Options to pass `child_process.spawn`. + * @return {Promise} Resolved if install successful, rejected otherwise */ -install.npmInstall = function (pkgs, options, cb, spawnOptions) { - return this.runInstall('npm', pkgs, options, cb, spawnOptions); +install.npmInstall = function (pkgs, options, spawnOptions) { + return this.runInstall('npm', pkgs, options, spawnOptions); }; /** @@ -184,9 +186,9 @@ install.npmInstall = function (pkgs, options, cb, spawnOptions) { * * @param {String|Array} [pkgs] Packages to install * @param {Object} [options] Options to pass to `dargs` as arguments - * @param {Function} [cb] * @param {Object} [spawnOptions] Options to pass `child_process.spawn`. + * @return {Promise} Resolved if install successful, rejected otherwise */ -install.yarnInstall = function (pkgs, options, cb, spawnOptions) { - return this.runInstall('yarn', pkgs, options, cb, spawnOptions); +install.yarnInstall = function (pkgs, options, spawnOptions) { + return this.runInstall('yarn', pkgs, options, spawnOptions); }; diff --git a/test/install.js b/test/install.js index 2a1cf9dd..87d71028 100644 --- a/test/install.js +++ b/test/install.js @@ -29,8 +29,7 @@ describe('Base (actions/install mixin)', () => { }); describe('#runInstall()', () => { - it('takes a config object and passes it to the spawned process', function (done) { - const callbackSpy = sinon.spy(); + it('takes a config object and passes it to the spawned process', function () { const options = { save: true }; @@ -41,18 +40,18 @@ describe('Base (actions/install mixin)', () => { }; // Args: installer, paths, options, cb - this.dummy.runInstall('nestedScript', ['path1', 'path2'], options, callbackSpy, spawnEnv); - this.dummy.run(() => { - sinon.assert.calledWithExactly( - this.spawnCommandStub, - 'nestedScript', - ['install', 'path1', 'path2', '--save'], - spawnEnv - ); - - sinon.assert.calledOnce(callbackSpy); - done(); - }); + var promise = this.dummy + .runInstall('nestedScript', ['path1', 'path2'], options, spawnEnv) + .then(() => { + sinon.assert.calledWithExactly( + this.spawnCommandStub, + 'nestedScript', + ['install', 'path1', 'path2', '--save'], + spawnEnv + ); + }); + this.dummy.run(); + return promise; }); describe('with --skip-install', () => { @@ -80,22 +79,10 @@ describe('Base (actions/install mixin)', () => { }); }); - it('call callback if skipInstall', function (done) { - const spy = sinon.spy(); - this.dummy.runInstall('npm', ['install'], spy); - this.dummy.run(() => { - sinon.assert.calledOnce(spy); - done(); - }); - }); - - it('call callback if skipInstall', function (done) { - const spy = sinon.spy(); - this.dummy.runInstall('yarn', ['install'], spy); - this.dummy.run(() => { - sinon.assert.calledOnce(spy); - done(); - }); + it('resolve Promise if skipInstall', function () { + var promise = this.dummy.runInstall('npm', ['install']); + this.dummy.run(); + return promise; }); }); }); @@ -111,8 +98,8 @@ describe('Base (actions/install mixin)', () => { }); }); - it('spawn a bower process with formatted options', function (done) { - this.dummy.bowerInstall('jquery', {saveDev: true}, () => { + it('spawn a bower process with formatted options', function () { + var promise = this.dummy.bowerInstall('jquery', {saveDev: true}).then(() => { sinon.assert.calledOnce(this.spawnCommandStub); sinon.assert.calledWithExactly( this.spawnCommandStub, @@ -120,10 +107,9 @@ describe('Base (actions/install mixin)', () => { ['install', 'jquery', '--save-dev'], {} ); - - done(); }); this.dummy.run(); + return promise; }); }); @@ -138,20 +124,20 @@ describe('Base (actions/install mixin)', () => { }); }); - it('run without callback', function (done) { + it('run with options', function (done) { this.dummy.npmInstall('yo', {save: true}); this.dummy.run(() => { - sinon.assert.calledOnce(this.spawnCommandStub); + sinon.assert.calledWithExactly(this.spawnCommandStub, 'npm', ['install', 'yo', '--save', '--cache-min', 86400], {}); done(); }); }); - it('run with callback', function (done) { - this.dummy.npmInstall('yo', {save: true}, () => { + it('resolve Promise on success', function () { + var promise = this.dummy.npmInstall('yo').then(() => { sinon.assert.calledOnce(this.spawnCommandStub); - done(); }); this.dummy.run(); + return promise; }); }); @@ -166,7 +152,7 @@ describe('Base (actions/install mixin)', () => { }); }); - it('run without callback', function (done) { + it('run with options', function (done) { this.dummy.yarnInstall('yo', {dev: true}); this.dummy.run(() => { sinon.assert.calledOnce(this.spawnCommandStub); @@ -175,40 +161,35 @@ describe('Base (actions/install mixin)', () => { }); }); - it('run with callback', function (done) { - this.dummy.yarnInstall('yo', () => { + it('resolve promise on success', function () { + var promise = this.dummy.yarnInstall('yo').then(() => { sinon.assert.calledOnce(this.spawnCommandStub); sinon.assert.calledWithExactly(this.spawnCommandStub, 'yarn', ['add', 'yo'], {}); - done(); }); this.dummy.run(); + return promise; }); }); describe('#installDependencies()', () => { - it('spawn npm and bower', function (done) { - this.dummy.installDependencies(() => { + it('spawn npm and bower', function () { + var promise = this.dummy.installDependencies().then(() => { sinon.assert.calledTwice(this.spawnCommandStub); sinon.assert.calledWithExactly(this.spawnCommandStub, 'bower', ['install'], {}); sinon.assert.calledWithExactly(this.spawnCommandStub, 'npm', ['install', '--cache-min', 86400], {}); - done(); }); this.dummy.run(); + return promise; }); - it('execute a callback after installs', function (done) { - this.dummy.installDependencies({callback: done}); - this.dummy.run(); - }); - - it('spawn yarn', function (done) { - this.dummy.installDependencies({yarn: true, npm: false, callback: function () { + it('spawn yarn', function () { + var promise = this.dummy.installDependencies({yarn: true, npm: false}).then(() => { sinon.assert.calledTwice(this.spawnCommandStub); sinon.assert.calledWithExactly(this.spawnCommandStub, 'bower', ['install'], {}); sinon.assert.calledWithExactly(this.spawnCommandStub, 'yarn', ['install'], {}); - done(); - }.bind(this)}); + }); this.dummy.run(); + return promise; }); }); });