Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
fix: truncate command title to stdout width (#865)
This makes sure the task title is as long as possible to fit on a single line of the console output, applying both to regular and functional tasks.
  • Loading branch information
iiroj committed May 22, 2020
1 parent 93bc942 commit b8e1a4a
Show file tree
Hide file tree
Showing 8 changed files with 86 additions and 43 deletions.
59 changes: 36 additions & 23 deletions lib/makeCmdTasks.js
@@ -1,9 +1,27 @@
'use strict'

const cliTruncate = require('cli-truncate')
const debug = require('debug')('lint-staged:make-cmd-tasks')

const resolveTaskFn = require('./resolveTaskFn')
const { createError } = require('./validateConfig')

const debug = require('debug')('lint-staged:make-cmd-tasks')
const STDOUT_COLUMNS_DEFAULT = 80

const listrPrefixLength = {
update: ` X `.length, // indented task title where X is a checkmark or a cross (failure)
verbose: `[STARTED] `.length, // verbose renderer uses 7-letter STARTED/SUCCESS prefixes
}

/**
* Get length of title based on the number of available columns prefix length
* @param {string} renderer The name of the Listr renderer
* @returns {number}
*/
const getTitleLength = (renderer, columns = process.stdout.columns) => {
const prefixLength = listrPrefixLength[renderer] || 0
return (columns || STDOUT_COLUMNS_DEFAULT) - prefixLength
}

/**
* Creates and returns an array of listr tasks which map to the given commands.
Expand All @@ -12,10 +30,11 @@ const debug = require('debug')('lint-staged:make-cmd-tasks')
* @param {Array<string|Function>|string|Function} options.commands
* @param {Array<string>} options.files
* @param {string} options.gitDir
* @param {string} options.renderer
* @param {Boolean} shell
* @param {Boolean} verbose
*/
module.exports = async function makeCmdTasks({ commands, files, gitDir, shell, verbose }) {
const makeCmdTasks = async ({ commands, files, gitDir, renderer, shell, verbose }) => {
debug('Creating listr tasks for commands %o', commands)
const commandArray = Array.isArray(commands) ? commands : [commands]
const cmdTasks = []
Expand All @@ -28,32 +47,26 @@ module.exports = async function makeCmdTasks({ commands, files, gitDir, shell, v
const resolvedArray = Array.isArray(resolved) ? resolved : [resolved] // Wrap non-array command as array

for (const command of resolvedArray) {
let title = isFn ? '[Function]' : command

if (isFn) {
// If the function linter didn't return string | string[] it won't work
// Do the validation here instead of `validateConfig` to skip evaluating the function multiple times
if (typeof command !== 'string') {
throw new Error(
createError(
title,
'Function task should return a string or an array of strings',
resolved
)
// If the function linter didn't return string | string[] it won't work
// Do the validation here instead of `validateConfig` to skip evaluating the function multiple times
if (isFn && typeof command !== 'string') {
throw new Error(
createError(
'[Function]',
'Function task should return a string or an array of strings',
resolved
)
}

const [startOfFn] = command.split(' ')
title += ` ${startOfFn} ...` // Append function name, like `[Function] eslint ...`
)
}

cmdTasks.push({
title,
command,
task: resolveTaskFn({ command, files, gitDir, isFn, shell, verbose }),
})
// Truncate title to single line based on renderer
const title = cliTruncate(command, getTitleLength(renderer))
const task = resolveTaskFn({ command, files, gitDir, isFn, shell, verbose })
cmdTasks.push({ title, command, task })
}
}

return cmdTasks
}

module.exports = makeCmdTasks
1 change: 1 addition & 0 deletions lib/runAll.js
Expand Up @@ -136,6 +136,7 @@ const runAll = async (
commands: task.commands,
files: task.fileList,
gitDir,
renderer: listrOptions.renderer,
shell,
verbose,
})
Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -33,6 +33,7 @@
},
"dependencies": {
"chalk": "^4.0.0",
"cli-truncate": "2.1.0",
"commander": "^5.1.0",
"cosmiconfig": "^6.0.0",
"debug": "^4.1.1",
Expand Down
6 changes: 1 addition & 5 deletions test/index.spec.js
Expand Up @@ -8,14 +8,10 @@ jest.unmock('execa')
import getStagedFiles from '../lib/getStagedFiles'
// eslint-disable-next-line import/first
import lintStaged from '../lib/index'
import { replaceSerializer } from './utils/replaceSerializer'

jest.mock('../lib/getStagedFiles')

const replaceSerializer = (from, to) => ({
test: (val) => typeof val === 'string' && from.test(val),
print: (val) => val.replace(from, to),
})

const mockCosmiconfigWith = (result) => {
cosmiconfig.mockImplementationOnce(() => ({
search: () => Promise.resolve(result),
Expand Down
23 changes: 19 additions & 4 deletions test/integration.test.js
@@ -1,5 +1,6 @@
import makeConsoleMock from 'consolemock'
import fs from 'fs-extra'
import ansiSerializer from 'jest-snapshot-serializer-ansi'
import { nanoid } from 'nanoid'
import normalize from 'normalize-path'
import os from 'os'
Expand All @@ -10,6 +11,7 @@ jest.unmock('execa')

import execGitBase from '../lib/execGit'
import lintStaged from '../lib/index'
import { replaceSerializer } from './utils/replaceSerializer'

jest.setTimeout(20000)

Expand Down Expand Up @@ -751,8 +753,8 @@ describe('lint-staged', () => {
LOG [SUCCESS] Preparing...
LOG [STARTED] Running tasks...
LOG [STARTED] Running tasks for *.js
LOG [STARTED] [Function] git ...
LOG [SUCCESS] [Function] git ...
LOG [STARTED] git stash drop
LOG [SUCCESS] git stash drop
LOG [SUCCESS] Running tasks for *.js
LOG [SUCCESS] Running tasks...
LOG [STARTED] Applying modifications...
Expand Down Expand Up @@ -970,6 +972,19 @@ describe('lint-staged', () => {
})
).rejects.toThrowError()

// Hide filepath from test snapshot because it's not important and varies in CI
const replaceFilepathSerializer = replaceSerializer(
/prettier --write (.*)?$/gm,
`prettier --write FILEPATH`
)

// Awkwardly merge two serializers
expect.addSnapshotSerializer({
test: (val) => ansiSerializer.test(val) || replaceFilepathSerializer.test(val),
print: (val, serialize) =>
replaceFilepathSerializer.print(ansiSerializer.print(val, serialize)),
})

expect(console.printHistory()).toMatchInlineSnapshot(`
"
WARN ‼ Skipping backup because \`--no-stash\` was used.
Expand All @@ -980,8 +995,8 @@ describe('lint-staged', () => {
LOG [SUCCESS] Hiding unstaged changes to partially staged files...
LOG [STARTED] Running tasks...
LOG [STARTED] Running tasks for *.js
LOG [STARTED] [Function] prettier ...
LOG [SUCCESS] [Function] prettier ...
LOG [STARTED] prettier --write FILEPATH
LOG [SUCCESS] prettier --write FILEPATH
LOG [SUCCESS] Running tasks for *.js
LOG [SUCCESS] Running tasks...
LOG [STARTED] Applying modifications...
Expand Down
33 changes: 23 additions & 10 deletions test/makeCmdTasks.spec.js
Expand Up @@ -61,7 +61,7 @@ describe('makeCmdTasks', () => {
it('should work with function task returning a string', async () => {
const res = await makeCmdTasks({ commands: () => 'test', gitDir, files: ['test.js'] })
expect(res.length).toBe(1)
expect(res[0].title).toEqual('[Function] test ...')
expect(res[0].title).toEqual('test')
})

it('should work with function task returning array of string', async () => {
Expand All @@ -71,8 +71,8 @@ describe('makeCmdTasks', () => {
files: ['test.js'],
})
expect(res.length).toBe(2)
expect(res[0].title).toEqual('[Function] test ...')
expect(res[1].title).toEqual('[Function] test2 ...')
expect(res[0].title).toEqual('test')
expect(res[1].title).toEqual('test2')
})

it('should work with function task accepting arguments', async () => {
Expand All @@ -82,8 +82,8 @@ describe('makeCmdTasks', () => {
files: ['test.js', 'test2.js'],
})
expect(res.length).toBe(2)
expect(res[0].title).toEqual('[Function] test ...')
expect(res[1].title).toEqual('[Function] test ...')
expect(res[0].title).toEqual('test test.js')
expect(res[1].title).toEqual('test test2.js')
})

it('should work with array of mixed string and function tasks', async () => {
Expand All @@ -93,17 +93,17 @@ describe('makeCmdTasks', () => {
files: ['test.js', 'test2.js', 'test3.js'],
})
expect(res.length).toBe(5)
expect(res[0].title).toEqual('[Function] test ...')
expect(res[0].title).toEqual('test')
expect(res[1].title).toEqual('test2')
expect(res[2].title).toEqual('[Function] test ...')
expect(res[3].title).toEqual('[Function] test ...')
expect(res[4].title).toEqual('[Function] test ...')
expect(res[2].title).toEqual('test test.js')
expect(res[3].title).toEqual('test test2.js')
expect(res[4].title).toEqual('test test3.js')
})

it('should work with async function tasks', async () => {
const res = await makeCmdTasks({ commands: async () => 'test', gitDir, files: ['test.js'] })
expect(res.length).toBe(1)
expect(res[0].title).toEqual('[Function] test ...')
expect(res[0].title).toEqual('test')
})

it("should throw when function task doesn't return string | string[]", async () => {
Expand All @@ -120,4 +120,17 @@ describe('makeCmdTasks', () => {
Please refer to https://github.com/okonet/lint-staged#configuration for more information..."
`)
})

it('should truncate task title', async () => {
const longString = new Array(1000)
.fill()
.map((_, index) => index)
.join('')

const res = await makeCmdTasks({ commands: () => longString, gitDir, files: ['test.js'] })
expect(res.length).toBe(1)
expect(res[0].title).toMatchInlineSnapshot(
`"0123456789101112131415161718192021222324252627282930313233343536373839404142434…"`
)
})
})
4 changes: 4 additions & 0 deletions test/utils/replaceSerializer.js
@@ -0,0 +1,4 @@
export const replaceSerializer = (from, to) => ({
test: (val) => typeof val === 'string' && from.test(val),
print: (val) => val.replace(from, to),
})
2 changes: 1 addition & 1 deletion yarn.lock
Expand Up @@ -1845,7 +1845,7 @@ cli-cursor@^3.1.0:
dependencies:
restore-cursor "^3.1.0"

cli-truncate@^2.1.0:
cli-truncate@2.1.0, cli-truncate@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7"
integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==
Expand Down

0 comments on commit b8e1a4a

Please sign in to comment.