'use strict';
// Runs `npm install` in cwd
const path = require('path');
const chalk = require('chalk');
const execa = require('../utilities/execa');
const semver = require('semver');
const SilentError = require('silent-error');
const existsSync = require('exists-sync');
const logger = require('heimdalljs-logger')('ember-cli:npm-task');
const Task = require('../models/task');
class NpmTask extends Task {
/**
* @private
* @class NpmTask
* @constructor
* @param {Object} options
*/
constructor(options) {
super(options);
// The command to run: can be 'install' or 'uninstall'
this.command = '';
this.versionConstraints = '3 || 4 || 5';
}
npm(args) {
logger.info('npm: %j', args);
return execa('npm', args, { preferLocal: false });
}
yarn(args) {
logger.info('yarn: %j', args);
return execa('yarn', args, { preferLocal: false });
}
hasYarnLock() {
return existsSync(path.join(this.project.root, 'yarn.lock'));
}
checkYarn() {
return this.yarn(['--version']).then(result => {
let version = result.stdout;
logger.info('yarn --version: %s', version);
}).catch(error => {
logger.error('yarn --version failed: %s', error);
throw error;
});
}
checkNpmVersion() {
return this.npm(['--version']).then(result => {
let version = result.stdout;
logger.info('npm --version: %s', version);
let ok = semver.satisfies(version, this.versionConstraints);
if (!ok) {
logger.warn('npm --version is outside of version constraint: %s', this.versionConstraints);
let below = semver.ltr(version, this.versionConstraints);
if (below) {
throw new SilentError('Ember CLI is now using the global npm, but your npm version is outdated.\n' +
'Please update your global npm version by running: npm install -g npm');
}
this.ui.writeWarnLine('Ember CLI is using the global npm, but your npm version has not yet been ' +
'verified to work with the current Ember CLI release.');
}
}).catch(error => {
logger.error('npm --version failed: %s', error);
if (error.code === 'ENOENT') {
throw new SilentError('Ember CLI is now using the global npm, but was not able to find it.\n' +
'Please install npm using the instructions at https://github.com/npm/npm');
}
throw error;
});
}
/**
* This method will determine what package manager (npm or yarn) should be
* used to install the npm dependencies.
*
* Setting `this.useYarn` to `true` or `false` will force the use of yarn
* or npm respectively.
*
* If `this.useYarn` is not set we check if `yarn.lock` exists and if
* `yarn` is available and in that case set `useYarn` to `true`.
*
* @private
* @method findPackageManager
* @return {Promise}
*/
findPackageManager() {
if (this.useYarn === true) {
logger.info('yarn requested -> trying yarn');
return this.checkYarn().catch(error => {
if (error.code === 'ENOENT') {
throw new SilentError('Yarn could not be found.');
}
throw error;
});
}
if (this.useYarn === false) {
logger.info('npm requested -> using npm');
return this.checkNpmVersion();
}
if (!this.hasYarnLock()) {
logger.info('yarn.lock not found -> using npm');
return this.checkNpmVersion();
}
logger.info('yarn.lock found -> trying yarn');
return this.checkYarn()
.then(() => {
logger.info('yarn found -> using yarn');
this.useYarn = true;
})
.catch(() => {
logger.info('yarn not found -> using npm');
return this.checkNpmVersion();
});
}
run(options) {
this.useYarn = options.useYarn;
return this.findPackageManager().then(() => {
let ui = this.ui;
let startMessage = this.formatStartMessage(options.packages);
let completeMessage = this.formatCompleteMessage(options.packages);
ui.startProgress(chalk.green(startMessage));
let promise;
if (this.useYarn) {
let args = this.toYarnArgs(this.command, options);
promise = this.yarn(args);
} else {
let args = this.toNpmArgs(this.command, options);
promise = this.npm(args);
}
return promise
.finally(() => ui.stopProgress())
.then(() => ui.writeLine(chalk.green(completeMessage)));
});
}
toNpmArgs(command, options) {
let args = [command];
if (options.save) {
args.push('--save');
}
if (options['save-dev']) {
args.push('--save-dev');
}
if (options['save-exact']) {
args.push('--save-exact');
}
if ('optional' in options && !options.optional) {
args.push('--no-optional');
}
if (options.verbose) {
args.push('--loglevel verbose');
} else {
args.push('--loglevel error');
}
if (options.packages) {
args = args.concat(options.packages);
}
return args;
}
toYarnArgs(command, options) {
let args = [];
if (command === 'install') {
if (options.save) {
args.push('add');
} else if (options['save-dev']) {
args.push('add', '--dev');
} else if (options.packages) {
throw new Error(`npm command "${command} ${options.packages.join(' ')}" can not be translated to Yarn command`);
} else {
args.push('install');
}
if (options['save-exact']) {
args.push('--exact');
}
if ('optional' in options && !options.optional) {
args.push('--ignore-optional');
}
} else if (command === 'uninstall') {
args.push('remove');
} else {
throw new Error(`npm command "${command}" can not be translated to Yarn command`);
}
if (options.verbose) {
args.push('--verbose');
}
if (options.packages) {
args = args.concat(options.packages);
}
args.push('--non-interactive');
return args;
}
formatStartMessage(/* packages */) {
return '';
}
formatCompleteMessage(/* packages */) {
return '';
}
}
module.exports = NpmTask;