From b8e1a4a9683639d961f948283dec0e6dec556493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iiro=20J=C3=A4ppinen?= Date: Fri, 22 May 2020 08:43:33 +0300 Subject: [PATCH] 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. --- lib/makeCmdTasks.js | 59 ++++++++++++++++++++------------- lib/runAll.js | 1 + package.json | 1 + test/index.spec.js | 6 +--- test/integration.test.js | 23 ++++++++++--- test/makeCmdTasks.spec.js | 33 ++++++++++++------ test/utils/replaceSerializer.js | 4 +++ yarn.lock | 2 +- 8 files changed, 86 insertions(+), 43 deletions(-) create mode 100644 test/utils/replaceSerializer.js diff --git a/lib/makeCmdTasks.js b/lib/makeCmdTasks.js index a7c9e84a2..1316906a6 100644 --- a/lib/makeCmdTasks.js +++ b/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. @@ -12,10 +30,11 @@ const debug = require('debug')('lint-staged:make-cmd-tasks') * @param {Array|string|Function} options.commands * @param {Array} 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 = [] @@ -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 diff --git a/lib/runAll.js b/lib/runAll.js index 522a2a0d6..4a86d9c07 100644 --- a/lib/runAll.js +++ b/lib/runAll.js @@ -136,6 +136,7 @@ const runAll = async ( commands: task.commands, files: task.fileList, gitDir, + renderer: listrOptions.renderer, shell, verbose, }) diff --git a/package.json b/package.json index 7c15cc8a7..c9a9c1359 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/index.spec.js b/test/index.spec.js index d23571b58..1233ef1b2 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -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), diff --git a/test/integration.test.js b/test/integration.test.js index df4847af0..80712e2b6 100644 --- a/test/integration.test.js +++ b/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' @@ -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) @@ -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... @@ -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. @@ -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... diff --git a/test/makeCmdTasks.spec.js b/test/makeCmdTasks.spec.js index 967542f74..bdb7a1522 100644 --- a/test/makeCmdTasks.spec.js +++ b/test/makeCmdTasks.spec.js @@ -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 () => { @@ -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 () => { @@ -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 () => { @@ -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 () => { @@ -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…"` + ) + }) }) diff --git a/test/utils/replaceSerializer.js b/test/utils/replaceSerializer.js new file mode 100644 index 000000000..94887b3f2 --- /dev/null +++ b/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), +}) diff --git a/yarn.lock b/yarn.lock index 9e5a16690..a7da6c396 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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==