Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: truncate command title to stdout width #865

Merged
merged 1 commit into from May 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey! I was wondering if there's a reason why this dependency was locked to a specific version instead of using ^?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No reason, I'm pretty sure I just installed it with yarn add. Thanks for noticing, I'll create a fix for relaxing to ^.

"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