lint-staged
Version:
Lint files staged by git
162 lines (140 loc) • 4.64 kB
JavaScript
import { createDebug } from './debug.js'
import { getSpawnedTask } from './getSpawnedTask.js'
import { configurationError } from './messages.js'
const debugLog = createDebug('lint-staged:getSpawnedTasks')
/**
* Get the maximum length of a command-line argument string based on current platform
*
* https://serverfault.com/questions/69430/what-is-the-maximum-length-of-a-command-line-in-mac-os-x
* https://support.microsoft.com/en-us/help/830473/command-prompt-cmd-exe-command-line-string-limitation
* https://unix.stackexchange.com/a/120652
*/
export const getMaxArgLength = (platform = process.platform) => {
switch (platform) {
case 'darwin':
return 262_144
case 'win32':
return 8_191
default:
return 131_072
}
}
/**
* Chunk `files` so that when joined with `command`, the resulting string fits into `maxArgLength`.
* For function commands, they are evaluated with the list of files and measured afterwards.
*
* @param {string | Function} command
* @param {string[]} files
* @param {number} [maxArgLength]
* @returns {Promise<Array<{ command: string, files: string[] }>>}
*/
export const chunkFilesForCommand = async (command, files, maxArgLength) => {
const isFn = typeof command === 'function'
const resolved = isFn ? await command([...files]) : command
const resolvedCommands = Array.isArray(resolved) ? resolved : [resolved]
if (isFn && resolvedCommands.some((command) => typeof command !== 'string')) {
throw new Error(
configurationError(
'[Function]',
'Function task should return a string or an array of strings',
resolved
)
)
}
if (!maxArgLength || maxArgLength === Infinity || files.length <= 1) {
debugLog(
'Skipping chunking because maxArgLength is %s and there are %s files',
maxArgLength,
files.length
)
return resolvedCommands.map((resolvedCommand) => ({
command: resolvedCommand,
files,
}))
}
const fits = resolvedCommands.every((resolvedCommand) => {
/**
* Really the command is spawned using `tinyexec` and the files passed as an array of
* arguments; but this approximates the length without quoting filenames.
*/
const finalCommandString = isFn ? resolvedCommand : resolvedCommand + ' ' + files.join(' ')
debugLog(
'Resolved an argument string length of %d characters from %d files',
finalCommandString.length,
files.length
)
return finalCommandString.length <= maxArgLength
})
if (fits) {
return resolvedCommands.map((resolvedCommand) => ({
command: resolvedCommand,
files,
}))
}
const middle = Math.ceil(files.length / 2)
const left = files.slice(0, middle)
const right = files.slice(middle)
debugLog(
'Splitting %d files into chunks of %d and %d for maxArgLength of %d',
files.length,
left.length,
right.length,
maxArgLength
)
const commands = await Promise.all([
chunkFilesForCommand(command, left, maxArgLength),
chunkFilesForCommand(command, right, maxArgLength),
])
return commands.flat()
}
/**
* Creates and returns an array of tasks which map to the given commands.
*
* @param {object} options
* @param {AbortController} options.abortController
* @param {Array<string|Function>|string|Function} options.commands
* @param {string} options.continueOnError
* @param {string} options.cwd
* @param {import('./getStagedFiles.js').StagedFile[]} options.files
* @param {number} [options.maxArgLength]
* @param {string} options.topLevelDir
* @param {Boolean} verbose
*/
export const getSpawnedTasks = async ({
abortController,
commands,
continueOnError,
cwd,
files,
maxArgLength,
topLevelDir,
verbose,
}) => {
debugLog('Creating tasks for commands %o', commands)
const commandArray = Array.isArray(commands) ? commands : [commands]
const filepaths = files.map((f) => f.filepath)
if (filepaths.length === 0) {
debugLog('Skipping task generation because no files matched', { commands })
return []
}
const spawnedTasks = await Promise.all(
commandArray.map(async (cmd) => {
const isFn = typeof cmd === 'function'
const chunkedCommands = await chunkFilesForCommand(cmd, filepaths, maxArgLength)
return chunkedCommands.map(({ command, files: chunkedFiles }) => ({
title: command,
task: getSpawnedTask({
abortController,
command,
continueOnError,
cwd,
files: chunkedFiles,
topLevelDir,
isFn,
verbose,
}),
}))
})
)
return spawnedTasks.flat()
}