diff --git a/README.md b/README.md index 8345f4815..c9f7df352 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,28 @@ See [examples](#examples) and [configuration](#configuration) below. > > To mitigate this rename your `commit` npm script to something non git hook namespace like, for example `{ cz: git-cz }` +## Command line flags + +``` +$ ./node_modules/.bin/lint-staged --help + + Usage: lint-staged [options] + + + Options: + + -V, --version output the version number + -c, --config [path] Path to configuration file + -d, --debug Enable debug mode + -h, --help output usage information +``` + +* **`--config [path]`**: This can be used to manually specify the `lint-staged` config file location. However, if the specified file cannot be found, it will error out instead of performing the usual search. +* **`--debug`**: Enabling the debug mode does the following: + - `lint-staged` uses the [debug](https://github.com/visionmedia/debug) module internally to log information about staged files, commands being executed, location of binaries etc. Debug logs, which are automatically enabled by passing the flag, can also be enabled by setting the environment variable `$DEBUG` to `lint-staged*`. + - Use the [`verbose` renderer](https://github.com/SamVerschueren/listr-verbose-renderer) for `listr`. + - Do not pass `--silent` to npm scripts. + ## Configuration Starting with v3.1 you can now use different ways of configuring it: @@ -102,7 +124,6 @@ To set options and keep lint-staged extensible, advanced format can be used. Thi * `concurrent` — *true* — runs linters for each glob pattern simultaneously. If you don’t want this, you can set `concurrent: false` * `chunkSize` — Max allowed chunk size based on number of files for glob pattern. This is important on windows based systems to avoid command length limitations. See [#147](https://github.com/okonet/lint-staged/issues/147) * `subTaskConcurrency` — `1` — Controls concurrency for processing chunks generated for each linter. Execution is **not** concurrent by default(see [#225](https://github.com/okonet/lint-staged/issues/225)) -* `verbose` — *false* — runs lint-staged in verbose mode. When `true` it will use https://github.com/SamVerschueren/listr-verbose-renderer. * `globOptions` — `{ matchBase: true, dot: true }` — [minimatch options](https://github.com/isaacs/minimatch#options) to customize how glob patterns match files. ## Filtering files diff --git a/index.js b/index.js index ae4632d62..0c1c23e2b 100755 --- a/index.js +++ b/index.js @@ -3,11 +3,21 @@ 'use strict' const cmdline = require('commander') +const debugLib = require('debug') const pkg = require('./package.json') +const debug = debugLib('lint-staged:bin') + cmdline .version(pkg.version) .option('-c, --config [path]', 'Path to configuration file') + .option('-d, --debug', 'Enable debug mode') .parse(process.argv) -require('./src')(console, cmdline.config) +if (cmdline.debug) { + debugLib.enable('lint-staged*') +} + +debug('Running `lint-staged@%s`', pkg.version) + +require('./src')(console, cmdline.config, cmdline.debug) diff --git a/package.json b/package.json index 1d7e437b7..c6d1a42fd 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "chalk": "^2.1.0", "commander": "^2.11.0", "cosmiconfig": "^3.1.0", + "debug": "^3.1.0", "dedent": "^0.7.0", "execa": "^0.8.0", "find-parent-dir": "^0.3.0", diff --git a/src/findBin.js b/src/findBin.js index 294b65858..0f644a81c 100644 --- a/src/findBin.js +++ b/src/findBin.js @@ -2,11 +2,14 @@ const npmWhich = require('npm-which')(process.cwd()) -module.exports = function findBin(cmd, scripts, options) { +const debug = require('debug')('lint-staged:find-bin') + +module.exports = function findBin(cmd, scripts, debugMode) { + debug('Resolving binary for command `%s`', cmd) const npmArgs = (bin, args) => // We always add `--` even if args are not defined. This is required // because we pass filenames later. - ['run', options && options.verbose ? undefined : '--silent', bin, '--'] + ['run', debugMode ? undefined : '--silent', bin, '--'] // args could be undefined but we filter that out. .concat(args) .filter(arg => arg !== undefined) @@ -39,6 +42,7 @@ module.exports = function findBin(cmd, scripts, options) { */ if (scripts[cmd] !== undefined) { // Support for scripts from package.json + debug('`%s` resolved to npm script - `%s`', cmd, scripts[cmd]) return { bin: 'npm', args: npmArgs(cmd) } } @@ -47,6 +51,7 @@ module.exports = function findBin(cmd, scripts, options) { const args = parts.splice(1) if (scripts[bin] !== undefined) { + debug('`%s` resolved to npm script - `%s`', bin, scripts[bin]) return { bin: 'npm', args: npmArgs(bin, args) } } @@ -76,5 +81,6 @@ module.exports = function findBin(cmd, scripts, options) { throw new Error(`${bin} could not be found. Try \`npm install ${bin}\`.`) } + debug('Binary for `%s` resolved to `%s`', cmd, bin) return { bin, args } } diff --git a/src/generateTasks.js b/src/generateTasks.js index 25f0e0775..5d3fe73f1 100644 --- a/src/generateTasks.js +++ b/src/generateTasks.js @@ -6,7 +6,11 @@ const pathIsInside = require('path-is-inside') const getConfig = require('./getConfig').getConfig const resolveGitDir = require('./resolveGitDir') +const debug = require('debug')('lint-staged:gen-tasks') + module.exports = function generateTasks(config, relFiles) { + debug('Generating linter tasks') + const normalizedConfig = getConfig(config) // Ensure we have a normalized config const linters = normalizedConfig.linters const globOptions = normalizedConfig.globOptions @@ -29,10 +33,9 @@ module.exports = function generateTasks(config, relFiles) { // Return absolute path after the filter is run .map(file => path.resolve(cwd, file)) - return { - pattern, - commands, - fileList - } + const task = { pattern, commands, fileList } + debug('Generated task: \n%O', task) + + return task }) } diff --git a/src/getConfig.js b/src/getConfig.js index 53b38e594..f9536ec7b 100644 --- a/src/getConfig.js +++ b/src/getConfig.js @@ -12,10 +12,12 @@ const logValidationWarning = require('jest-validate').logValidationWarning const unknownOptionWarning = require('jest-validate/build/warnings').unknownOptionWarning const isGlob = require('is-glob') +const debug = require('debug')('lint-staged:cfg') + /** * Default config object * - * @type {{concurrent: boolean, chunkSize: number, globOptions: {matchBase: boolean, dot: boolean}, linters: {}, subTaskConcurrency: number, renderer: string, verbose: boolean}} + * @type {{concurrent: boolean, chunkSize: number, globOptions: {matchBase: boolean, dot: boolean}, linters: {}, subTaskConcurrency: number, renderer: string}} */ const defaultConfig = { concurrent: true, @@ -26,8 +28,7 @@ const defaultConfig = { }, linters: {}, subTaskConcurrency: 1, - renderer: 'update', - verbose: false + renderer: 'update' } /** @@ -88,10 +89,11 @@ function unknownValidationReporter(config, example, option, options) { * * @param {Object} sourceConfig * @returns {{ - * concurrent: boolean, chunkSize: number, globOptions: {matchBase: boolean, dot: boolean}, linters: {}, subTaskConcurrency: number, renderer: string, verbose: boolean + * concurrent: boolean, chunkSize: number, globOptions: {matchBase: boolean, dot: boolean}, linters: {}, subTaskConcurrency: number, renderer: string * }} */ -function getConfig(sourceConfig) { +function getConfig(sourceConfig, debugMode) { + debug('Normalizing config') const config = defaultsDeep( {}, // Do not mutate sourceConfig!!! isSimple(sourceConfig) ? { linters: sourceConfig } : sourceConfig, @@ -100,18 +102,25 @@ function getConfig(sourceConfig) { // Check if renderer is set in sourceConfig and if not, set accordingly to verbose if (isObject(sourceConfig) && !sourceConfig.hasOwnProperty('renderer')) { - config.renderer = config.verbose ? 'verbose' : 'update' + config.renderer = debugMode ? 'verbose' : 'update' } return config } +const optRmMsg = (opt, helpMsg) => ` Option ${chalk.bold(opt)} was removed. + + ${helpMsg} + + Please remove ${chalk.bold(opt)} from your configuration.` + /** * Runs config validation. Throws error if the config is not valid. * @param config {Object} * @returns config {Object} */ function validateConfig(config) { + debug('Validating config') const exampleConfig = Object.assign({}, defaultConfig, { linters: { '*.js': ['eslint --fix', 'git add'], @@ -120,11 +129,9 @@ function validateConfig(config) { }) const deprecatedConfig = { - gitDir: () => ` Option ${chalk.bold('gitDir')} was removed. - - lint-staged now automatically resolves '.git' directory. - - Please remove ${chalk.bold('gitDir')} from your configuration.` + gitDir: () => optRmMsg('gitDir', "lint-staged now automatically resolves '.git' directory."), + verbose: () => + optRmMsg('verbose', `Use the command line flag ${chalk.bold('--debug')} instead.`) } validate(config, { diff --git a/src/index.js b/src/index.js index cb4d2df3b..834688045 100644 --- a/src/index.js +++ b/src/index.js @@ -11,6 +11,8 @@ const validateConfig = require('./getConfig').validateConfig const printErrors = require('./printErrors') const runAll = require('./runAll') +const debug = require('debug')('lint-staged') + // Find the right package.json at the root of the project const packageJson = require(appRoot.resolve('package.json')) @@ -25,7 +27,8 @@ const errConfigNotFound = new Error('Config could not be found') /** * Root lint-staged function that is called from .bin */ -module.exports = function lintStaged(injectedLogger, configPath) { +module.exports = function lintStaged(injectedLogger, configPath, debugMode) { + debug('Loading config using `cosmiconfig`') const logger = injectedLogger || console const explorer = cosmiconfig('lint-staged', { @@ -39,19 +42,26 @@ module.exports = function lintStaged(injectedLogger, configPath) { .then(result => { if (result == null) throw errConfigNotFound + debug('Successfully loaded config from `%s`:\n%O', result.filepath, result.config) // result.config is the parsed configuration object // result.filepath is the path to the config file that was found - const config = validateConfig(getConfig(result.config)) - - if (config.verbose) { + const config = validateConfig(getConfig(result.config, debugMode)) + if (debugMode) { + // Log using logger to be able to test through `consolemock`. logger.log('Running lint-staged with the following config:') logger.log(stringifyObject(config, { indent: ' ' })) + } else { + // We might not be in debug mode but `DEBUG=lint-staged*` could have + // been set. + debug('Normalized config:\n%O', config) } const scripts = packageJson.scripts || {} + debug('Loaded scripts from package.json:\n%O', scripts) - runAll(scripts, config) + runAll(scripts, config, debugMode) .then(() => { + debug('linters were executed successfully!') // No errors, exiting with 0 process.exitCode = 0 }) diff --git a/src/runAll.js b/src/runAll.js index 69857913d..c5dc5271e 100644 --- a/src/runAll.js +++ b/src/runAll.js @@ -8,13 +8,16 @@ const runScript = require('./runScript') const generateTasks = require('./generateTasks') const resolveGitDir = require('./resolveGitDir') +const debug = require('debug')('lint-staged:run') + /** * Executes all tasks and either resolves or rejects the promise * @param scripts * @param config {Object} * @returns {Promise} */ -module.exports = function runAll(scripts, config) { +module.exports = function runAll(scripts, config, debugMode) { + debug('Running all linter scripts') // Config validation if (!config || !has(config, 'concurrent') || !has(config, 'renderer')) { throw new Error('Invalid config provided to runAll! Use getConfig instead.') @@ -22,15 +25,19 @@ module.exports = function runAll(scripts, config) { const concurrent = config.concurrent const renderer = config.renderer - sgf.cwd = resolveGitDir() + const gitDir = resolveGitDir() + debug('Resolved git directory to be `%s`', gitDir) + sgf.cwd = gitDir return pify(sgf)('ACM').then(files => { /* files is an Object{ filename: String, status: String } */ const filenames = files.map(file => file.filename) + debug('Loaded list of staged files in git:\n%O', filenames) + const tasks = generateTasks(config, filenames).map(task => ({ title: `Running tasks for ${task.pattern}`, task: () => - new Listr(runScript(task.commands, task.fileList, scripts, config), { + new Listr(runScript(task.commands, task.fileList, scripts, config, debugMode), { // In sub-tasks we don't want to run concurrently // and we want to abort on errors dateFormat: false, diff --git a/src/runScript.js b/src/runScript.js index 8956a85f0..61b778792 100644 --- a/src/runScript.js +++ b/src/runScript.js @@ -10,7 +10,11 @@ const calcChunkSize = require('./calcChunkSize') const findBin = require('./findBin') const resolveGitDir = require('./resolveGitDir') -module.exports = function runScript(commands, pathsToLint, scripts, config) { +const debug = require('debug')('lint-staged:run-script') + +module.exports = function runScript(commands, pathsToLint, scripts, config, debugMode) { + debug('Running script with commands %o', commands) + const normalizedConfig = getConfig(config) const chunkSize = normalizedConfig.chunkSize const concurrency = normalizedConfig.subTaskConcurrency @@ -24,7 +28,7 @@ module.exports = function runScript(commands, pathsToLint, scripts, config) { title: linter, task: () => { try { - const res = findBin(linter, scripts, config) + const res = findBin(linter, scripts, debugMode) // Only use gitDir as CWD if we are using the git binary // e.g `npm` should run tasks in the actual CWD @@ -35,6 +39,10 @@ module.exports = function runScript(commands, pathsToLint, scripts, config) { const mapper = pathsChunk => { const args = res.args.concat(pathsChunk) + debug('bin:', res.bin) + debug('args: %O', args) + debug('opts: %o', execaOptions) + return ( execa(res.bin, args, Object.assign({}, execaOptions)) /* If we don't catch, pMap will terminate on first rejection */ diff --git a/test/__mocks__/my-config.json b/test/__mocks__/my-config.json index a5695fde8..cb997df03 100644 --- a/test/__mocks__/my-config.json +++ b/test/__mocks__/my-config.json @@ -1,5 +1,4 @@ { - "verbose": true, "linters": { "*": "mytask" } diff --git a/test/__snapshots__/getConfig.spec.js.snap b/test/__snapshots__/getConfig.spec.js.snap index 7da8d918c..511c5c6fb 100644 --- a/test/__snapshots__/getConfig.spec.js.snap +++ b/test/__snapshots__/getConfig.spec.js.snap @@ -11,7 +11,6 @@ Object { "linters": Object {}, "renderer": "update", "subTaskConcurrency": 1, - "verbose": false, } `; @@ -26,7 +25,6 @@ Object { "linters": Object {}, "renderer": "update", "subTaskConcurrency": 1, - "verbose": false, } `; @@ -47,7 +45,6 @@ Object { }, "renderer": "update", "subTaskConcurrency": 1, - "verbose": false, } `; @@ -72,7 +69,7 @@ WARN ● Validation Warning: Please refer to https://github.com/okonet/lint-staged#configuration for more information..." `; -exports[`validateConfig should print deprecation warning for gitDir option 1`] = ` +exports[`validateConfig should print deprecation warning for deprecated options 1`] = ` " WARN ● Deprecation Warning: @@ -82,6 +79,15 @@ WARN ● Deprecation Warning: Please remove gitDir from your configuration. +Please refer to https://github.com/okonet/lint-staged#configuration for more information... +WARN ● Deprecation Warning: + + Option verbose was removed. + + Use the command line flag --debug instead. + + Please remove verbose from your configuration. + Please refer to https://github.com/okonet/lint-staged#configuration for more information..." `; diff --git a/test/__snapshots__/index.spec.js.snap b/test/__snapshots__/index.spec.js.snap index dbbb08361..39f2d214d 100644 --- a/test/__snapshots__/index.spec.js.snap +++ b/test/__snapshots__/index.spec.js.snap @@ -4,7 +4,6 @@ exports[`lintStaged should load config file when specified 1`] = ` " LOG Running lint-staged with the following config: LOG { - verbose: true, linters: { '*': 'mytask' }, @@ -19,13 +18,12 @@ LOG { }" `; -exports[`lintStaged should not output config in non verbose mode 1`] = `""`; +exports[`lintStaged should not output config in normal mode 1`] = `""`; -exports[`lintStaged should output config in verbose mode 1`] = ` +exports[`lintStaged should output config in debug mode 1`] = ` " LOG Running lint-staged with the following config: LOG { - verbose: true, linters: { '*': 'mytask' }, diff --git a/test/findBin.spec.js b/test/findBin.spec.js index 5ea8246c9..b5bc6acce 100644 --- a/test/findBin.spec.js +++ b/test/findBin.spec.js @@ -11,8 +11,8 @@ describe('findBin', () => { expect(args).toEqual(['run', '--silent', 'my-linter', '--']) }) - it('should return npm run command without --silent in verbose mode', () => { - const { bin, args } = findBin('eslint', { eslint: 'eslint' }, { verbose: true }) + it('should return npm run command without --silent in debug mode', () => { + const { bin, args } = findBin('eslint', { eslint: 'eslint' }, true) expect(bin).toEqual('npm') expect(args).toEqual(['run', 'eslint', '--']) }) diff --git a/test/getConfig.spec.js b/test/getConfig.spec.js index 35ec7c4f9..8f14de8c7 100644 --- a/test/getConfig.spec.js +++ b/test/getConfig.spec.js @@ -11,34 +11,6 @@ describe('getConfig', () => { expect(getConfig({})).toMatchSnapshot() }) - it('should set verbose', () => { - expect(getConfig({})).toEqual( - expect.objectContaining({ - verbose: false - }) - ) - - expect( - getConfig({ - verbose: false - }) - ).toEqual( - expect.objectContaining({ - verbose: false - }) - ) - - expect( - getConfig({ - verbose: true - }) - ).toEqual( - expect.objectContaining({ - verbose: true - }) - ) - }) - it('should set concurrent', () => { expect(getConfig({})).toEqual( expect.objectContaining({ @@ -67,7 +39,7 @@ describe('getConfig', () => { ) }) - it('should set renderer based on verbose key', () => { + it('should set renderer based on debug mode', () => { expect(getConfig({})).toEqual( expect.objectContaining({ renderer: 'update' @@ -84,21 +56,7 @@ describe('getConfig', () => { }) ) - expect( - getConfig({ - verbose: false - }) - ).toEqual( - expect.objectContaining({ - renderer: 'update' - }) - ) - - expect( - getConfig({ - verbose: true - }) - ).toEqual( + expect(getConfig({}, true)).toEqual( expect.objectContaining({ renderer: 'verbose' }) @@ -182,8 +140,7 @@ describe('getConfig', () => { '*.js': 'eslint' }, subTaskConcurrency: 10, - renderer: 'custom', - verbose: true + renderer: 'custom' } expect(getConfig(cloneDeep(src))).toEqual(src) }) @@ -220,14 +177,17 @@ describe('validateConfig', () => { expect(console.printHistory()).toMatchSnapshot() }) - it('should print deprecation warning for gitDir option', () => { - const configWithDeprecatedOpt = { - gitDir: '../', + it('should print deprecation warning for deprecated options', () => { + const baseConfig = { linters: { '*.js': ['eslint --fix', 'git add'] } } - expect(() => validateConfig(getConfig(configWithDeprecatedOpt))).not.toThrow() + const opts = [{ gitDir: '../' }, { verbose: true }] + opts.forEach(opt => { + const configWithDeprecatedOpt = Object.assign(opt, baseConfig) + expect(() => validateConfig(getConfig(configWithDeprecatedOpt))).not.toThrow() + }) expect(console.printHistory()).toMatchSnapshot() }) diff --git a/test/index.spec.js b/test/index.spec.js index c4c95d7c2..01593a9c6 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -15,29 +15,24 @@ const mockCosmiconfigWith = result => { } describe('lintStaged', () => { - let logger - - beforeAll(() => { - logger = makeConsoleMock() - }) + const logger = makeConsoleMock() beforeEach(() => { logger.clearHistory() }) - it('should output config in verbose mode', async () => { + it('should output config in debug mode', async () => { const config = { - verbose: true, linters: { '*': 'mytask' } } mockCosmiconfigWith({ config }) - await lintStaged(logger) + await lintStaged(logger, undefined, true) expect(logger.printHistory()).toMatchSnapshot() }) - it('should not output config in non verbose mode', async () => { + it('should not output config in normal mode', async () => { const config = { '*': 'mytask' } @@ -47,7 +42,7 @@ describe('lintStaged', () => { }) it('should load config file when specified', async () => { - await lintStaged(logger, path.join(__dirname, '__mocks__', 'my-config.json')) + await lintStaged(logger, path.join(__dirname, '__mocks__', 'my-config.json'), true) expect(logger.printHistory()).toMatchSnapshot() }) diff --git a/test/runScript.spec.js b/test/runScript.spec.js index 22301030e..d93321219 100644 --- a/test/runScript.spec.js +++ b/test/runScript.spec.js @@ -109,15 +109,15 @@ describe('runScript', () => { process.cwd = processCwdBkp }) - it('should use --silent in non-verbose mode', async () => { - const [linter] = runScript('test', ['test.js'], scripts, { verbose: false }) + it('should use --silent in normal mode', async () => { + const [linter] = runScript('test', ['test.js'], scripts, {}, false) await linter.task() expect(mockFn).toHaveBeenCalledTimes(1) expect(mockFn).lastCalledWith('npm', ['run', '--silent', 'test', '--', 'test.js'], {}) }) - it('should not use --silent in verbose mode', async () => { - const [linter] = runScript('test', ['test.js'], scripts, { verbose: true }) + it('should not use --silent in debug mode', async () => { + const [linter] = runScript('test', ['test.js'], scripts, {}, true) await linter.task() expect(mockFn).toHaveBeenCalledTimes(1) expect(mockFn).lastCalledWith('npm', ['run', 'test', '--', 'test.js'], {})