nano-staged
Version:
Tiny tool to run commands for modified, staged, and committed git files.
131 lines (109 loc) • 3.34 kB
JavaScript
import { normalize, relative, resolve, isAbsolute } from 'path'
import c from 'picocolors'
import { globToRegex } from './glob-to-regex.js'
import { stringArgvToArray } from './utils.js'
import { TaskRunnerError } from './errors.js'
import { executor } from './executor.js'
import { toArray } from './utils.js'
export function createCmdRunner({
cwd = process.cwd(),
type = 'staged',
rootPath = '',
config = {},
files = [],
} = {}) {
const runner = {
async generateCmdTasks() {
const cmdTasks = []
for (const [pattern, cmds] of Object.entries(config)) {
const matches = globToRegex(pattern, { extended: true, globstar: pattern.includes('/') })
const isFn = typeof cmds === 'function'
const task_files = []
const tasks = []
for (let file of files) {
file = normalize(relative(cwd, normalize(resolve(rootPath, file)))).replace(/\\/g, '/')
if (!pattern.startsWith('../') && (file.startsWith('..') || isAbsolute(file))) {
continue
}
if (matches.regex.test(file)) {
task_files.push(resolve(cwd, file))
}
}
const file_count = task_files.length
const commands = toArray(isFn ? await cmds({ filenames: task_files, type }) : cmds)
const suffix = file_count ? file_count + (file_count > 1 ? ' files' : ' file') : 'no files'
for (const command of commands) {
const [cmd, ...args] = stringArgvToArray(command)
if (file_count) {
tasks.push({
title: command,
run: async () =>
executor(cmd, isFn ? args : args.concat(task_files), {
cwd: rootPath,
}),
pattern,
})
}
}
cmdTasks.push({
title: pattern + c.dim(` - ${suffix}`),
file_count,
tasks,
})
}
return cmdTasks
},
async run(parentTask) {
const errors = []
try {
await Promise.all(
parentTask.tasks.map(async (task) => {
task.parent = parentTask
try {
if (task.file_count) {
task.state = 'run'
await runner.runTask(task)
task.state = 'done'
} else {
task.state = 'warn'
}
} catch (err) {
task.state = 'fail'
errors.push(...err)
}
})
)
if (errors.length) {
throw new TaskRunnerError(errors.join('\n\n'))
}
} catch (err) {
throw err
}
},
async runTask(parentTask) {
let skipped = false
let errors = []
for (const task of parentTask.tasks) {
task.parent = parentTask
try {
if (skipped) {
task.state = 'warn'
continue
}
task.state = 'run'
await task.run()
task.state = 'done'
} catch (error) {
skipped = true
task.title = c.red(task.title)
task.state = 'fail'
errors.push(`${c.red(task.pattern)} ${c.dim('>')} ${task.title}:\n` + error.trim())
}
}
if (errors.length) {
throw errors
}
},
}
return runner
}