Skip to content

Commit

Permalink
fix: truncate command title to stdout width
Browse files Browse the repository at this point in the history
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 20, 2020
1 parent 93bc942 commit 508e869
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 508e869

Please sign in to comment.