Skip to content

Commit

Permalink
feat/issue-1398: refactored makeCmdTasks function and added unit test…
Browse files Browse the repository at this point in the history
… cases
  • Loading branch information
Rohit Kumar committed Apr 2, 2024
1 parent e769fd3 commit 0682aff
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 42 deletions.
136 changes: 94 additions & 42 deletions lib/makeCmdTasks.js
Expand Up @@ -6,6 +6,92 @@ import { getInitialState } from './state.js'

const debugLog = debug('lint-staged:makeCmdTasks')

/**
* Returns whether the command is a function or not and the resolved command
*
* @param {Function|string} cmd
* @param {Array<string>} files
* @returns {Object} Object containing whether the command is a function and the resolved command
*/
const getResolvedCommand = async (cmd, files) => {
// command function may return array of commands that already include `stagedFiles`
const isFn = typeof cmd === 'function'
/** Pass copy of file list to prevent mutation by function from config file. */
const resolved = isFn ? await cmd([...files]) : cmd
return { resolved, isFn }
}

/**
* Validates whether a command is a function and if the command is valid
*
* @param {string|object} command
* @param {boolean} isFn
* @param {string|object} resolved
* @throws {Error} If the command is not valid
*/
const validateCommand = (command, isFn, resolved) => {
if ((isFn && typeof command !== 'string' && typeof command !== 'object') || !command) {
throw new Error(
configurationError(
'[Function]',
'Function task should return a string or an array of strings or an object',
resolved
)
)
}
}

/**
* Handles function configuration and pushes the tasks into the task array
*
* @param {object} command
* @param {Array} cmdTasks
* @param {string|object} resolved
* @throws {Error} If the function configuration is not valid
*/
const handleFunctionConfig = (command, cmdTasks, resolved) => {
if (typeof command.title === 'string' && typeof command.task === 'function') {
const task = async (ctx = getInitialState()) => {
try {
await command.task()
} catch (e) {
throw makeErr(command.title, e, ctx)
}
}
cmdTasks.push({
title: command.title,
task,
})
} else {
throw new Error(
configurationError(
'[Function]',
'Function task should return object with title and task where title should be string and task should be function',
resolved
)
)
}
}

/**
* Handles regular configuration and pushes the tasks into the task array
*
* @param {object} params
* @param {Array} cmdTasks
*/
const handleRegularConfig = ({ command, cwd, files, gitDir, isFn, shell, verbose }, cmdTasks) => {
const task = resolveTaskFn({ command, cwd, files, gitDir, isFn, shell, verbose })
cmdTasks.push({ title: command, command, task })
}

/**
* Ensures the input is an array. If the input is not an array, it wraps the input inside an array.
*
* @param {Array|string|object} input
* @returns {Array} Returns the input as an array
*/
const ensureArray = (input) => (Array.isArray(input) ? input : [input])

/**
* Creates and returns an array of listr tasks which map to the given commands.
*
Expand All @@ -19,54 +105,20 @@ const debugLog = debug('lint-staged:makeCmdTasks')
*/
export const makeCmdTasks = async ({ commands, cwd, files, gitDir, shell, verbose }) => {
debugLog('Creating listr tasks for commands %o', commands)
const commandArray = Array.isArray(commands) ? commands : [commands]
const commandArray = ensureArray(commands)
const cmdTasks = []

for (const cmd of commandArray) {
// command function may return array of commands that already include `stagedFiles`
const isFn = typeof cmd === 'function'

/** Pass copy of file list to prevent mutation by function from config file. */
const resolved = isFn ? await cmd([...files]) : cmd

const resolvedArray = Array.isArray(resolved) ? resolved : [resolved] // Wrap non-array command as array
const { resolved, isFn } = await getResolvedCommand(cmd, files)
const resolvedArray = ensureArray(resolved)

for (const command of resolvedArray) {
// If the function linter didn't return string | string[] | object it won't work
// Do the validation here instead of `validateConfig` to skip evaluating the function multiple times
if ((isFn && typeof command !== 'string' && typeof command !== 'object') || !command) {
throw new Error(
configurationError(
'[Function]',
'Function task should return a string or an array of strings or an object',
resolved
)
)
} else if (isFn && typeof command === 'object') {
if (typeof command.title === 'string' && typeof command.task === 'function') {
const task = async (ctx = getInitialState()) => {
try {
await command.task()
} catch (e) {
throw makeErr(command.title, e, ctx)
}
}
cmdTasks.push({
title: command.title,
task,
})
} else {
throw new Error(
configurationError(
'[Function]',
'Function task should return object with title and task where title should be string and task should be function',
resolved
)
)
}
validateCommand(command, isFn, resolved)

if (isFn && typeof command === 'object') {
handleFunctionConfig(command, cmdTasks, resolved)
} else {
const task = resolveTaskFn({ command, cwd, files, gitDir, isFn, shell, verbose })
cmdTasks.push({ title: command, command, task })
handleRegularConfig({ command, cwd, files, gitDir, isFn, shell, verbose }, cmdTasks)
}
}
}
Expand Down
42 changes: 42 additions & 0 deletions test/unit/makeCmdTasks.spec.js
Expand Up @@ -143,4 +143,46 @@ describe('makeCmdTasks', () => {
/** ...but the original file list was not mutated */
expect(files).toEqual(['test.js'])
})

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

it('should throw error when function task returns object without proper title and task', async () => {
await expect(
makeCmdTasks({
commands: () => ({ title: 'test' }), // Missing task function
gitDir,
files: ['test.js'],
})
).rejects.toThrowErrorMatchingInlineSnapshot(`
"✖ Validation Error:
Invalid value for '[Function]': { title: 'test' }
Function task should return object with title and task where title should be string and task should be function"
`)
})

it('should throw error when function task fails', async () => {
const failingTask = () => {
throw new Error('Task failed')
}

const res = await makeCmdTasks({
commands: () => ({ title: 'test', task: failingTask }),
gitDir,
files: ['test.js'],
})

const [linter] = res
await expect(linter.task()).rejects.toThrow()
})
})

0 comments on commit 0682aff

Please sign in to comment.