Skip to content

Commit

Permalink
feat: add --shell and --quiet flags
Browse files Browse the repository at this point in the history
- `--shell` disables parsing of command strings, enabling advanced shell scripts at the cost of speed and security
- `--quiet` disabled _lint-staged_'s own console progress output, leaving only the linter commandss
  • Loading branch information
iiroj authored and okonet committed Jul 1, 2019
1 parent 04190c8 commit ecf9227
Show file tree
Hide file tree
Showing 11 changed files with 146 additions and 37 deletions.
4 changes: 3 additions & 1 deletion index.js
Expand Up @@ -19,6 +19,8 @@ const debug = debugLib('lint-staged:bin')
cmdline
.version(pkg.version)
.option('-c, --config [path]', 'Path to configuration file')
.option('-x, --shell', 'Use execa’s shell mode to execute linter commands')
.option('-s, --silent', 'Use Listr’s silent renderer')
.option('-d, --debug', 'Enable debug mode')
.parse(process.argv)

Expand All @@ -28,4 +30,4 @@ if (cmdline.debug) {

debug('Running `lint-staged@%s`', pkg.version)

require('./src')(console, cmdline.config, cmdline.debug)
require('./src')(console, cmdline.config, !!cmdline.shell, !!cmdline.silent, !!cmdline.debug)
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -41,6 +41,7 @@
"micromatch": "^3.1.8",
"path-is-inside": "^1.0.2",
"please-upgrade-node": "^3.0.2",
"string-argv": "^0.3.0",
"stringify-object": "^3.2.2"
},
"devDependencies": {
Expand Down
15 changes: 13 additions & 2 deletions src/index.js
Expand Up @@ -44,8 +44,19 @@ function loadConfig(configPath) {

/**
* Root lint-staged function that is called from .bin
* @param {Function} logger
* @param {String} configPath
* @param {Boolean} shellMode Use execa’s shell mode to execute linter commands
* @param {Boolean} silentMode Use Listr’s silent renderer
* @param {Boolean} debugMode Enable debug mode
*/
module.exports = function lintStaged(logger = console, configPath, debugMode) {
module.exports = function lintStaged(
logger = console,
configPath,
shellMode = false,
silentMode = false,
debugMode = false
) {
debug('Loading config using `cosmiconfig`')

return loadConfig(configPath)
Expand All @@ -66,7 +77,7 @@ module.exports = function lintStaged(logger = console, configPath, debugMode) {
debug('Normalized config:\n%O', config)
}

return runAll(config, debugMode)
return runAll(config, shellMode, silentMode, debugMode)
.then(() => {
debug('linters were executed successfully!')
// No errors, exiting with 0
Expand Down
4 changes: 3 additions & 1 deletion src/makeCmdTasks.js
Expand Up @@ -8,10 +8,11 @@ const debug = require('debug')('lint-staged:make-cmd-tasks')
* Creates and returns an array of listr tasks which map to the given commands.
*
* @param {Array<string>|string} commands
* @param {Boolean} shell
* @param {Array<string>} pathsToLint
* @param {Object} [options]
*/
module.exports = async function makeCmdTasks(commands, gitDir, pathsToLint) {
module.exports = async function makeCmdTasks(commands, shell, gitDir, pathsToLint) {
debug('Creating listr tasks for commands %o', commands)

const lintersArray = Array.isArray(commands) ? commands : [commands]
Expand All @@ -20,6 +21,7 @@ module.exports = async function makeCmdTasks(commands, gitDir, pathsToLint) {
title: linter,
task: resolveTaskFn({
linter,
shell,
gitDir,
pathsToLint
})
Expand Down
20 changes: 11 additions & 9 deletions src/resolveTaskFn.js
Expand Up @@ -4,6 +4,7 @@ const chalk = require('chalk')
const dedent = require('dedent')
const execa = require('execa')
const symbols = require('log-symbols')
const stringArgv = require('string-argv')

const debug = require('debug')('lint-staged:task')

Expand All @@ -14,10 +15,11 @@ const debug = require('debug')('lint-staged:task')
* @param {string} cmd
* @return {Promise} child_process
*/
const execLinter = (cmd, execaOptions = {}) => {
const execLinter = (cmd, args, execaOptions = {}) => {
debug('cmd:', cmd)
debug('args:', args)
debug('execaOptions:', execaOptions)
return execa(cmd, execaOptions)
return execa(cmd, args, execaOptions)
}

const successMsg = linter => `${symbols.success} ${linter} passed!`
Expand Down Expand Up @@ -73,13 +75,12 @@ function makeErr(linter, result, context = {}) {
*
* @param {Object} options
* @param {string} options.linter
* @param {Boolean} options.shellMode
* @param {string} options.gitDir
* @param {Array<string>} options.pathsToLint
* @returns {function(): Promise<Array<string>>}
*/
module.exports = function resolveTaskFn(options) {
const { gitDir, linter, pathsToLint } = options

module.exports = function resolveTaskFn({ gitDir, linter, pathsToLint, shell = false }) {
// If `linter` is a function, it should return a string when evaluated with `pathsToLint`.
// Else, it's a already a string
const fnLinter = typeof linter === 'function'
Expand All @@ -88,18 +89,19 @@ module.exports = function resolveTaskFn(options) {
const linters = Array.isArray(linterString) ? linterString : [linterString]

const tasks = linters.map(command => {
// If `linter` is a function, cmd already includes `pathsToLint`.
const cmdWithPaths = fnLinter ? command : `${command} ${pathsToLint.join(' ')}`
const [cmd, ...args] = stringArgv.parseArgsStringToArgv(command)
// If `linter` is a function, args already include `pathsToLint`.
const argsWithPaths = fnLinter ? args : args.concat(pathsToLint)

// Only use gitDir as CWD if we are using the git binary
// e.g `npm` should run tasks in the actual CWD
const execaOptions = { preferLocal: true, reject: false, shell: true }
const execaOptions = { preferLocal: true, reject: false, shell }
if (/^git(\.exe)?/i.test(command) && gitDir !== process.cwd()) {
execaOptions.cwd = gitDir
}

return ctx =>
execLinter(cmdWithPaths, execaOptions).then(result => {
execLinter(cmd, argsWithPaths, execaOptions).then(result => {
if (result.failed || result.killed || result.signal != null) {
throw makeErr(linter, result, ctx)
}
Expand Down
14 changes: 11 additions & 3 deletions src/runAll.js
Expand Up @@ -24,9 +24,17 @@ const MAX_ARG_LENGTH =
/**
* Executes all tasks and either resolves or rejects the promise
* @param config {Object}
* @param {Boolean} shellMode Use execa’s shell mode to execute linter commands
* @param {Boolean} silentMode Use Listr’s silent renderer
* @param {Boolean} debugMode Enable debug mode
* @returns {Promise}
*/
module.exports = async function runAll(config, debugMode) {
module.exports = async function runAll(
config,
shellMode = false,
silentMode = false,
debugMode = false
) {
debug('Running all linter scripts')

const gitDir = await resolveGitDir(config)
Expand Down Expand Up @@ -60,7 +68,7 @@ https://github.com/okonet/lint-staged#using-js-functions-to-customize-linter-com
const tasks = (await generateTasks(config, gitDir, files)).map(task => ({
title: `Running tasks for ${task.pattern}`,
task: async () =>
new Listr(await makeCmdTasks(task.commands, gitDir, task.fileList), {
new Listr(await makeCmdTasks(task.commands, shellMode, gitDir, task.fileList), {
// In sub-tasks we don't want to run concurrently
// and we want to abort on errors
dateFormat: false,
Expand All @@ -77,7 +85,7 @@ https://github.com/okonet/lint-staged#using-js-functions-to-customize-linter-com

const listrOptions = {
dateFormat: false,
renderer: debugMode ? 'verbose' : 'update'
renderer: (silentMode && 'silent') || (debugMode && 'verbose') || 'update'
}

// If all of the configured "linters" should be skipped
Expand Down
20 changes: 16 additions & 4 deletions test/index.spec.js
Expand Up @@ -43,7 +43,7 @@ describe('lintStaged', () => {
'*': 'mytask'
}
mockCosmiconfigWith({ config })
await lintStaged(logger, undefined, true)
await lintStaged(logger, undefined, false, false, true)
expect(logger.printHistory()).toMatchSnapshot()
})

Expand All @@ -66,20 +66,32 @@ describe('lintStaged', () => {

it('should load config file when specified', async () => {
expect.assertions(1)
await lintStaged(logger, path.join(__dirname, '__mocks__', 'my-config.json'), true)
await lintStaged(
logger,
path.join(__dirname, '__mocks__', 'my-config.json'),
false,
false,
true
)
expect(logger.printHistory()).toMatchSnapshot()
})

it('should parse function linter from js config', async () => {
expect.assertions(1)
await lintStaged(logger, path.join(__dirname, '__mocks__', 'advanced-config.js'), true)
await lintStaged(
logger,
path.join(__dirname, '__mocks__', 'advanced-config.js'),
false,
false,
true
)
expect(logger.printHistory()).toMatchSnapshot()
})

it('should load an npm config package when specified', async () => {
expect.assertions(1)
jest.mock('my-lint-staged-config')
await lintStaged(logger, 'my-lint-staged-config', true)
await lintStaged(logger, 'my-lint-staged-config', false, false, true)
expect(logger.printHistory()).toMatchSnapshot()
})

Expand Down
41 changes: 41 additions & 0 deletions test/index2.spec.js
@@ -0,0 +1,41 @@
import Listr from 'listr'
import path from 'path'

// silence console from Jest output
console.log = jest.fn(() => {})
console.error = jest.fn(() => {})

jest.mock('listr')

// eslint-disable-next-line import/first
import lintStaged from '../src/index'

describe('lintStaged', () => {
afterEach(() => {
Listr.mockClear()
})

it('should pass silent flag to Listr', async () => {
expect.assertions(1)
await lintStaged(
console,
path.join(__dirname, '__mocks__', 'my-config.json'),
false,
true,
false
)
expect(Listr.mock.calls[0][1]).toEqual({ dateFormat: false, renderer: 'silent' })
})

it('should pass debug flag to Listr', async () => {
expect.assertions(1)
await lintStaged(
console,
path.join(__dirname, '__mocks__', 'my-config.json'),
false,
false,
true
)
expect(Listr.mock.calls[0][1]).toEqual({ dateFormat: false, renderer: 'verbose' })
})
})
18 changes: 13 additions & 5 deletions test/makeCmdTasks.spec.js
Expand Up @@ -9,13 +9,13 @@ describe('makeCmdTasks', () => {
})

it('should return an array', async () => {
const array = await makeCmdTasks('test', gitDir, ['test.js'])
const array = await makeCmdTasks('test', false, gitDir, ['test.js'])
expect(array).toBeInstanceOf(Array)
})

it('should work with a single command', async () => {
expect.assertions(4)
const res = await makeCmdTasks('test', gitDir, ['test.js'])
const res = await makeCmdTasks('test', false, gitDir, ['test.js'])
expect(res.length).toBe(1)
const [linter] = res
expect(linter.title).toBe('test')
Expand All @@ -27,7 +27,7 @@ describe('makeCmdTasks', () => {

it('should work with multiple commands', async () => {
expect.assertions(9)
const res = await makeCmdTasks(['test', 'test2'], gitDir, ['test.js'])
const res = await makeCmdTasks(['test', 'test2'], false, gitDir, ['test.js'])
expect(res.length).toBe(2)
const [linter1, linter2] = res
expect(linter1.title).toBe('test')
Expand All @@ -37,11 +37,19 @@ describe('makeCmdTasks', () => {
expect(taskPromise).toBeInstanceOf(Promise)
await taskPromise
expect(execa).toHaveBeenCalledTimes(1)
expect(execa).lastCalledWith('test test.js', { preferLocal: true, reject: false, shell: true })
expect(execa).lastCalledWith('test', ['test.js'], {
preferLocal: true,
reject: false,
shell: false
})
taskPromise = linter2.task()
expect(taskPromise).toBeInstanceOf(Promise)
await taskPromise
expect(execa).toHaveBeenCalledTimes(2)
expect(execa).lastCalledWith('test2 test.js', { preferLocal: true, reject: false, shell: true })
expect(execa).lastCalledWith('test2', ['test.js'], {
preferLocal: true,
reject: false,
shell: false
})
})
})

0 comments on commit ecf9227

Please sign in to comment.