diff --git a/src/lib/cli/init.ts b/src/lib/cli/init.ts index 129ce81a9fb..a16f5873ad8 100644 --- a/src/lib/cli/init.ts +++ b/src/lib/cli/init.ts @@ -8,6 +8,7 @@ * ------------------------------------------------------------------------------ */ +import { spawnSync, SpawnSyncReturns } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; import { promisify } from 'util'; @@ -19,10 +20,52 @@ import { debug as d } from '../utils/debug'; import * as logger from '../utils/logging'; import * as resourceLoader from '../utils/resource-loader'; import { generateBrowserslistConfig } from './browserslist'; +import { NpmPackage } from '../types'; const debug: debug.IDebugger = d(__filename); const defaultFormatter = 'summary'; +const packageExists = () => { + const packagePath = path.join(process.cwd(), 'package.json'); + + return fs.existsSync(packagePath); // eslint-disable-line no-sync +}; + +const installRules = (rules) => { + const global: boolean = !packageExists(); + + const packages: Array = []; + + for (const [key, value] of Object.entries(rules)) { + if (value !== 'off') { + packages.push(`@sonarwhal/rule-${key}`); + } + } + + const command: string = `npm install ${packages.join(' ')}${global ? ' -g' : ''}`; + + try { + logger.log(`Running command ${command}`); + + const result: SpawnSyncReturns = spawnSync(command, { shell: true }); + + if (result.status !== 0) { + throw new Error(result.output[2].toString()); + } + + logger.log('Packages intalled successfully'); + } catch (err) { + /* + * There was an error installing packages. + * Show message to install packages manually. + */ + logger.error(err); + logger.log(`Something when wrong installing package, please run: +${process.platform === 'linux' ? 'sudo ' : ''}${command} +to install all the rules.`); + } +}; + /** Initiates a wizard to gnerate a valid `.sonarwhalrc` file based on user responses. */ export const initSonarwhalrc = async (options: CLIOptions): Promise => { if (!options.init) { @@ -33,14 +76,13 @@ export const initSonarwhalrc = async (options: CLIOptions): Promise => const connectorKeys: Array = resourceLoader.getCoreConnectors().concat(resourceLoader.getInstalledConnectors()); const formattersKeys: Array = resourceLoader.getCoreFormatters(); - const rulesIds = resourceLoader.getCoreRules(); - const rulesConfig = rulesIds.reduce((config, ruleId) => { - config[ruleId] = 'warning'; - - return config; - }, {}); - - const rules = resourceLoader.loadRules(rulesConfig); + const npmRules: Array = await resourceLoader.getCoreRulesFromNpm(); + const rules = npmRules.map((rule) => { + return { + description: rule.description, + id: rule.name.replace('@sonarwhal/rule-', '') + }; + }); const sonarwhalConfig = { browserslist: [], @@ -56,10 +98,10 @@ export const initSonarwhalrc = async (options: CLIOptions): Promise => const rulesKeys = []; - for (const [key, rule] of rules) { + for (const { description, id } of rules) { rulesKeys.push({ - name: `${key} - ${rule.meta.docs.description}`, - value: key + name: `${id} - ${description}`, + value: id }); } @@ -103,19 +145,15 @@ export const initSonarwhalrc = async (options: CLIOptions): Promise => if (results.default) { logger.log('Using recommended rules'); - rules.forEach((rule, key) => { - if (rule.meta.recommended) { - sonarwhalConfig.rules[key] = 'error'; - } else { - sonarwhalConfig.rules[key] = 'off'; - } + rules.forEach((rule) => { + sonarwhalConfig.rules[rule.id] = 'error'; }); } else { - rules.forEach((rule, key) => { - if (results.rules.includes(key)) { - sonarwhalConfig.rules[key] = 'error'; + rules.forEach((rule) => { + if (results.rules.includes(rule.id)) { + sonarwhalConfig.rules[rule.id] = 'error'; } else { - sonarwhalConfig.rules[key] = 'off'; + sonarwhalConfig.rules[rule.id] = 'off'; } }); } @@ -126,5 +164,7 @@ export const initSonarwhalrc = async (options: CLIOptions): Promise => await promisify(fs.writeFile)(filePath, JSON.stringify(sonarwhalConfig, null, 4), 'utf8'); + await installRules(sonarwhalConfig.rules); + return true; }; diff --git a/tests/lib/cli/init.ts b/tests/lib/cli/init.ts index 87b76a9a927..90108178104 100644 --- a/tests/lib/cli/init.ts +++ b/tests/lib/cli/init.ts @@ -11,9 +11,17 @@ const stubBrowserslistObject = { generateBrowserslistConfig() { } }; const resourceLoader = { getCoreConnectors() { }, getCoreFormatters() { }, - getCoreRules() { }, - getInstalledConnectors() { }, - loadRules() { } + getCoreRulesFromNpm() { }, + getInstalledConnectors() { } +}; +const child = { spawnSync() { } }; +const fs = { + existsSync() { }, + writeFile() { } +}; +const logger = { + error() { }, + log() { } }; const promisifyObject = { promisify() { } }; @@ -25,8 +33,11 @@ const stubUtilObject = { }; proxyquire('../../../src/lib/cli/init', { + '../utils/logging': logger, '../utils/resource-loader': resourceLoader, './browserslist': stubBrowserslistObject, + child_process: child, // eslint-disable-line camelcase + fs, inquirer, util: stubUtilObject }); @@ -58,22 +69,16 @@ const installedConnectors = [ 'installedConnector2' ]; -const rules = ['rule1', 'rule2']; - -const rulesData = new Map([ - ['rule1', { - meta: { - docs: { description: 'description rule 1' }, - recommended: false - } - }], - ['rule2', { - meta: { - docs: { description: 'description rule 2' }, - recommended: true - } - }] -]); +const rules = [ + { + description: 'rule 1 description', + name: '@sonarwhal/rule-rule1' + }, + { + description: 'rule 2 description', + name: '@sonarwhal/rule-rule2' + } +]; const formatters = [ 'formatter1', @@ -85,8 +90,9 @@ test.serial(`Generate should call to "inquirer.prompt" with the right data`, asy sandbox.stub(resourceLoader, 'getCoreConnectors').returns(connectors); sandbox.stub(resourceLoader, 'getCoreFormatters').returns(formatters); sandbox.stub(resourceLoader, 'getInstalledConnectors').returns(installedConnectors); - sandbox.stub(resourceLoader, 'getCoreRules').returns(rules); - sandbox.stub(resourceLoader, 'loadRules').returns(rulesData); + sandbox.stub(resourceLoader, 'getCoreRulesFromNpm').resolves(rules); + sandbox.stub(fs, 'existsSync').returns(true); + sandbox.stub(child, 'spawnSync').returns({ status: 0 }); sandbox.stub(inquirer, 'prompt').resolves({ connector: '', default: '', @@ -97,15 +103,17 @@ test.serial(`Generate should call to "inquirer.prompt" with the right data`, asy await initSonarwhalrc(actions); const questions = (inquirer.prompt as sinon.SinonStub).args[0][0]; - const rulesKeys = rules; t.is(questions[0].choices.length, connectors.length + installedConnectors.length); t.is(questions[1].choices.length, formatters.length); - t.is(questions[3].choices.length, rulesKeys.length); - t.is(questions[3].choices[0].value, rulesKeys[0]); - t.is(questions[3].choices[0].name, `${rulesKeys[0]} - ${rulesData.get(rulesKeys[0]).meta.docs.description}`); - t.is(questions[3].choices[1].value, rulesKeys[1]); - t.is(questions[3].choices[1].name, `${rulesKeys[1]} - ${rulesData.get(rulesKeys[1]).meta.docs.description}`); + t.is(questions[3].choices.length, rules.length); + const rule0Name = rules[0].name.replace('@sonarwhal/rule-', ''); + const rule1Name = rules[1].name.replace('@sonarwhal/rule-', ''); + + t.is(questions[3].choices[0].value, rule0Name); + t.is(questions[3].choices[0].name, `${rule0Name} - ${rules[0].description}`); + t.is(questions[3].choices[1].value, rule1Name); + t.is(questions[3].choices[1].name, `${rule1Name} - ${rules[1].description}`); sandbox.restore(); }); @@ -121,9 +129,10 @@ test.serial(`Generate should call to "fs.writeFile" with the right data`, async sandbox.stub(resourceLoader, 'getCoreConnectors').returns(connectors); sandbox.stub(resourceLoader, 'getCoreFormatters').returns(formatters); - sandbox.stub(resourceLoader, 'getCoreRules').returns(rules); - sandbox.stub(resourceLoader, 'loadRules').returns(rulesData); + sandbox.stub(resourceLoader, 'getCoreRulesFromNpm').resolves(rules); sandbox.stub(inquirer, 'prompt').resolves(questionsResults); + sandbox.stub(child, 'spawnSync').returns({ status: 0 }); + sandbox.stub(fs, 'existsSync').returns(true); await initSonarwhalrc(actions); @@ -148,9 +157,10 @@ test.serial(`If the user choose to use the default rules configuration, all reco sandbox.stub(resourceLoader, 'getCoreConnectors').returns(connectors); sandbox.stub(resourceLoader, 'getCoreFormatters').returns(formatters); - sandbox.stub(resourceLoader, 'getCoreRules').returns(rules); - sandbox.stub(resourceLoader, 'loadRules').returns(rulesData); + sandbox.stub(resourceLoader, 'getCoreRulesFromNpm').resolves(rules); sandbox.stub(inquirer, 'prompt').resolves(questionsResults); + sandbox.stub(child, 'spawnSync').returns({ status: 0 }); + sandbox.stub(fs, 'existsSync').returns(true); await initSonarwhalrc(actions); @@ -159,7 +169,152 @@ test.serial(`If the user choose to use the default rules configuration, all reco t.is(fileData.connector.name, questionsResults.connector); t.true(_.isEqual(fileData.formatters, [questionsResults.formatter])); t.is(fileData.rules.rule2, 'error'); - t.is(fileData.rules.rule1, 'off'); + t.is(fileData.rules.rule1, 'error'); + + sandbox.restore(); +}); + +test.serial('initSonarwhalrc should install all rules if the user choose to install the recommended rules', async (t) => { + const sandbox = sinon.sandbox.create(); + const questionsResults = { + connector: 'chrome', + default: true, + formatter: 'json', + rules: [] + }; + + sandbox.stub(resourceLoader, 'getCoreConnectors').returns(connectors); + sandbox.stub(resourceLoader, 'getCoreFormatters').returns(formatters); + sandbox.stub(resourceLoader, 'getCoreRulesFromNpm').resolves(rules); + sandbox.stub(inquirer, 'prompt').resolves(questionsResults); + sandbox.stub(child, 'spawnSync').returns({ status: 0 }); + sandbox.stub(fs, 'existsSync').returns(true); + t.context.child = child; + + await initSonarwhalrc(actions); + + t.true(t.context.child.spawnSync.args[0][0].includes('@sonarwhal/rule-rule1')); // eslint-disable-line no-sync + t.true(t.context.child.spawnSync.args[0][0].includes('@sonarwhal/rule-rule2')); // eslint-disable-line no-sync + + sandbox.restore(); +}); + +test.serial('initSonarwhalrc should install the rules choosen', async (t) => { + const sandbox = sinon.sandbox.create(); + const questionsResults = { + connector: 'chrome', + default: false, + formatter: 'json', + rules: ['rule2'] + }; + + sandbox.stub(resourceLoader, 'getCoreConnectors').returns(connectors); + sandbox.stub(resourceLoader, 'getCoreFormatters').returns(formatters); + sandbox.stub(resourceLoader, 'getCoreRulesFromNpm').resolves(rules); + sandbox.stub(inquirer, 'prompt').resolves(questionsResults); + sandbox.stub(child, 'spawnSync').returns({ status: 0 }); + sandbox.stub(fs, 'existsSync').returns(true); + t.context.child = child; + + await initSonarwhalrc(actions); + + t.true(t.context.child.spawnSync.args[0][0].includes('@sonarwhal/rule-rule2')); // eslint-disable-line no-sync + + sandbox.restore(); +}); + +test.serial(`If 'package.json' doesn't exist, we should install the packages globally`, async (t) => { + const sandbox = sinon.sandbox.create(); + const questionsResults = { + connector: 'chrome', + default: true, + formatter: 'json', + rules: [] + }; + + sandbox.stub(resourceLoader, 'getCoreConnectors').returns(connectors); + sandbox.stub(resourceLoader, 'getCoreFormatters').returns(formatters); + sandbox.stub(resourceLoader, 'getCoreRulesFromNpm').resolves(rules); + sandbox.stub(inquirer, 'prompt').resolves(questionsResults); + sandbox.stub(child, 'spawnSync').returns({ status: 0 }); + sandbox.stub(fs, 'existsSync').returns(false); + t.context.child = child; + + await initSonarwhalrc(actions); + + t.true(t.context.child.spawnSync.args[0][0].includes('-g')); // eslint-disable-line no-sync + + sandbox.restore(); +}); + +test.serial(`if instalation fails, the user should show a message about how to install the dependencies manually`, async (t) => { + const sandbox = sinon.sandbox.create(); + const questionsResults = { + connector: 'chrome', + default: true, + formatter: 'json', + rules: [] + }; + + sandbox.stub(resourceLoader, 'getCoreConnectors').returns(connectors); + sandbox.stub(resourceLoader, 'getCoreFormatters').returns(formatters); + sandbox.stub(resourceLoader, 'getCoreRulesFromNpm').resolves(rules); + sandbox.stub(inquirer, 'prompt').resolves(questionsResults); + sandbox.stub(logger, 'log').resolves(); + sandbox.stub(logger, 'error').resolves(); + sandbox.stub(child, 'spawnSync').returns({ + output: [null, null, Buffer.from('Error installing packages')], + status: 1 + }); + sandbox.stub(fs, 'existsSync').returns(true); + t.context.logger = logger; + + await initSonarwhalrc(actions); + + const errorMessage = t.context.logger.error.args[0][0]; + const installMessage = t.context.logger.log.args[t.context.logger.log.args.length -1][0]; + + t.true(t.context.logger.error.calledOnce); + t.is(errorMessage.message, 'Error installing packages'); + t.true(installMessage.startsWith(`Something when wrong installing package, please run: +npm install @sonarwhal/rule-rule1 @sonarwhal/rule-rule2 +to install all the rules.`)); + + sandbox.restore(); +}); + +test.serial(`if instalation fails and packages.json doesn't exist, the user should show a message about how to install the dependencies manually`, async (t) => { + const sandbox = sinon.sandbox.create(); + const questionsResults = { + connector: 'chrome', + default: true, + formatter: 'json', + rules: [] + }; + + sandbox.stub(resourceLoader, 'getCoreConnectors').returns(connectors); + sandbox.stub(resourceLoader, 'getCoreFormatters').returns(formatters); + sandbox.stub(resourceLoader, 'getCoreRulesFromNpm').resolves(rules); + sandbox.stub(inquirer, 'prompt').resolves(questionsResults); + sandbox.stub(logger, 'log').resolves(); + sandbox.stub(logger, 'error').resolves(); + sandbox.stub(child, 'spawnSync').returns({ + output: [null, null, Buffer.from('Error installing packages')], + status: 1 + }); + sandbox.stub(fs, 'existsSync').returns(false); + t.context.logger = logger; + + await initSonarwhalrc(actions); + + const errorMessage = t.context.logger.error.args[0][0]; + const installMessage = t.context.logger.log.args[t.context.logger.log.args.length -1][0]; + + t.true(t.context.logger.error.calledOnce); + t.is(errorMessage.message, 'Error installing packages'); + t.true(installMessage.startsWith(`Something when wrong installing package, please run: +npm install @sonarwhal/rule-rule1 @sonarwhal/rule-rule2 -g +to install all the rules.`)); sandbox.restore(); });