diff --git a/bin/eslint.js b/bin/eslint.js index 061e94767f0b..7bf35d24bc87 100755 --- a/bin/eslint.js +++ b/bin/eslint.js @@ -16,9 +16,10 @@ require("v8-compile-cache"); // Helpers //------------------------------------------------------------------------------ -const useStdIn = (process.argv.indexOf("--stdin") > -1), - init = (process.argv.indexOf("--init") > -1), - debug = (process.argv.indexOf("--debug") > -1); +const useStdIn = process.argv.includes("--stdin"), + init = process.argv.includes("--init"), + debug = process.argv.includes("--debug"), + info = process.argv.includes("--info"); // must do this initialization *before* other requires in order to work if (debug) { @@ -30,9 +31,10 @@ if (debug) { //------------------------------------------------------------------------------ // now we can safely include the other modules that use debug -const cli = require("../lib/cli"), - path = require("path"), - fs = require("fs"); +const path = require("path"), + fs = require("fs"), + cli = require("../lib/cli"), + logger = require("../lib/shared/logging"); //------------------------------------------------------------------------------ // Execution @@ -47,11 +49,11 @@ process.once("uncaughtException", err => { const template = lodash.template(fs.readFileSync(path.resolve(__dirname, `../messages/${err.messageTemplate}.txt`), "utf-8")); const pkg = require("../package.json"); - console.error("\nOops! Something went wrong! :("); - console.error(`\nESLint: ${pkg.version}.\n\n${template(err.messageData || {})}`); + logger.error("\nOops! Something went wrong! :("); + logger.error(`\nESLint: ${pkg.version}.\n\n${template(err.messageData || {})}`); } else { - console.error(err.stack); + logger.error(err.stack); } process.exitCode = 2; @@ -67,6 +69,17 @@ if (useStdIn) { const STDIN_FILE_DESCRIPTOR = 0; process.exitCode = cli.execute(process.argv, fs.readFileSync(STDIN_FILE_DESCRIPTOR, "utf8")); +} else if (info) { + const infoLogger = require("../lib/info"); + + try { + infoLogger.log(); + process.exitCode = 0; + } catch (err) { + process.exitCode = 1; + logger.error(err.message); + logger.error(err.stack); + } } else if (init) { const configInit = require("../lib/init/config-initializer"); @@ -74,8 +87,8 @@ if (useStdIn) { process.exitCode = 0; }).catch(err => { process.exitCode = 1; - console.error(err.message); - console.error(err.stack); + logger.error(err.message); + logger.error(err.stack); }); } else { process.exitCode = cli.execute(process.argv); diff --git a/lib/cli.js b/lib/cli.js index c34545544b10..bdf789ef55d2 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -18,6 +18,7 @@ const fs = require("fs"), path = require("path"), mkdirp = require("mkdirp"), + { execSync } = require("child_process"), { CLIEngine } = require("./cli-engine"), options = require("./options"), log = require("./shared/logging"); @@ -67,7 +68,8 @@ function translateOptions(cliOptions) { fixTypes: cliOptions.fixType, allowInlineConfig: cliOptions.inlineConfig, reportUnusedDisableDirectives: cliOptions.reportUnusedDisableDirectives, - resolvePluginsRelativeTo: cliOptions.resolvePluginsRelativeTo + resolvePluginsRelativeTo: cliOptions.resolvePluginsRelativeTo, + info: cliOptions.info }; } @@ -165,7 +167,6 @@ const cli = { if (currentOptions.version) { // version from package.json log.info(`v${require("../package.json").version}`); - } else if (currentOptions.printConfig) { if (files.length) { log.error("The --print-config option must be used with exactly one file name."); @@ -182,6 +183,22 @@ const cli = { log.info(JSON.stringify(fileConfig, null, " ")); return 0; + } else if (currentOptions.info) { + log.info("\n Environment Information:-\n"); + const packageInfo = { + node: "node -v", + npm: "npm -v", + eslint: "eslint -v" + }; + + Object.keys(packageInfo).forEach(pkg => { + try { + log.info(` ${pkg}: ${execSync(packageInfo[pkg])}`); + } catch (err) { + + // handle err + } + }); } else if (currentOptions.help || (!files.length && !useStdin)) { log.info(options.generateHelp()); diff --git a/lib/info/index.js b/lib/info/index.js new file mode 100644 index 000000000000..dfa27bb138ca --- /dev/null +++ b/lib/info/index.js @@ -0,0 +1,142 @@ +/** + * @fileoverview Log information for debugging purposes + * @author Kai Cataldo + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const path = require("path"); +const spawn = require("cross-spawn"); +const { isEmpty } = require("lodash"); +const logger = require("../shared/logging"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const STRIP_VERSION_REGEX = /v(\d)/u; + +/** + * Checks if a given path is in a given directory. + * @param {string} parentPath - The parent path to check. + * @param {string} childPath - The path to check. + * @returns {boolean} Whether or not the given path is in the given directory. + */ +function isInDirectory(parentPath, childPath) { + return !path.relative(parentPath, childPath).startsWith(".."); +} + +/** + * Synchronously executes a shell command and formats the result. + * @param {string} cmd - The command to execute. + * @param {Array} args - The arguments to be executed with the command. + * @returns {string} The version returned by the command. + */ +function execCommand(cmd, args) { + const process = spawn.sync(cmd, args, { encoding: "utf8" }); + + if (process.error) { + throw process.error; + } + + return process.stdout.trim(); +} + +/** + * Normalizes a version number. + * @param {string} version - The string to normalize. + * @returns {string} The normalized version number. + */ +function normalizeVersionNumber(version) { + return version.replace(STRIP_VERSION_REGEX, "$1"); +} + +/** + * Gets bin version. + * @param {string} bin - The bin to check. + * @returns {string} The normalized version returned by the command. + */ +function getBinVersion(bin) { + const binArgs = ["--version"]; + + try { + return execCommand(bin, binArgs); + } catch (e) { + logger.error(`Error finding ${bin} version running the command ${bin} ${binArgs.join(" ")}`); + throw e; + } +} + +/** + * Gets installed npm package version. + * @param {string} pkg - The package to check. + * @param {boolean} global - Whether to check globally or not. + * @returns {string} The normalized version returned by the command. + */ +function getNpmPackageVersion(pkg, { global = false } = {}) { + const npmBinArgs = ["bin", "-g"]; + const npmLsArgs = ["ls", "--depth=0", "--json", "eslint"]; + + if (global) { + npmLsArgs.push("-g"); + } + + try { + const parsedStdout = JSON.parse(execCommand("npm", npmLsArgs)); + + if (isEmpty(parsedStdout)) { + return "Not found"; + } + + const [, processBinPath] = process.argv; + let npmBinPath; + + try { + npmBinPath = execCommand("npm", npmBinArgs); + } catch (e) { + logger.error(`Error finding npm binary path when running command npm ${npmBinArgs.join(" ")}`); + throw e; + } + + const isGlobal = isInDirectory(npmBinPath, processBinPath); + let version = parsedStdout.dependencies.eslint.version; + + if ((global && isGlobal) || (!global && !isGlobal)) { + version += " (Currently used)"; + } + + return version; + } catch (e) { + logger.error(`Error finding ${pkg} version running the command npm ${npmLsArgs.join(" ")}`); + throw e; + } +} + +/** + * Generates and returns execution environment information. + * @returns {string} A string that contains execution environment information + */ +function generateInfo() { + return [ + "Environment Info:", + "", + `Node version: ${normalizeVersionNumber(getBinVersion("node"))}`, + `npm version: ${normalizeVersionNumber(getBinVersion("npm"))}`, + `Local ESLint version: ${normalizeVersionNumber(getNpmPackageVersion("eslint", { global: false }))}`, + `Global ESLint version: ${normalizeVersionNumber(getNpmPackageVersion("eslint", { global: true }))}` + ].join("\n"); +} + +//------------------------------------------------------------------------------ +// Public Interface +//------------------------------------------------------------------------------ + +module.exports = { + log() { + logger.info(generateInfo()); + } +}; diff --git a/lib/options.js b/lib/options.js index be4c09b8eabb..45113361c81f 100644 --- a/lib/options.js +++ b/lib/options.js @@ -224,6 +224,12 @@ module.exports = optionator({ default: "false", description: "Run config initialization wizard" }, + { + option: "info", + type: "Boolean", + default: "false", + description: "Output environment information for debugging purposes" + }, { option: "debug", type: "Boolean", @@ -246,6 +252,12 @@ module.exports = optionator({ option: "print-config", type: "path::String", description: "Print the configuration for the given file" + }, + { + option: "info", + alias: "i", + type: "Boolean", + description: "Print debugging information concerning the local environmet" } ] }); diff --git a/tests/lib/cli.js b/tests/lib/cli.js index baae75b53cb1..acf8c43bbbf4 100644 --- a/tests/lib/cli.js +++ b/tests/lib/cli.js @@ -1112,4 +1112,11 @@ describe("cli", () => { }); }); + describe("when passing --info", () => { + it("should print out debugging information concerning the local environment", () => { + cli.execute("--info"); + assert.isTrue(log.info.called); + }); + }); + });