@mgutz/task
Version:
no configruation task runner
149 lines (123 loc) • 3.45 kB
JavaScript
const _ = require('lodash')
const toposort = require('toposort')
const {ChildProcess} = require('child_process')
const log = require('./log')
const watch = require('./watch')
// Very simple depdendency resolution. Will execute dependencies once.
const execGraph = (tasks, processed, taskNames) => {
let graph = []
if (!taskNames) return graph
for (const name of taskNames) {
// guard against infinite loop
if (processed.indexOf(name) > -1) {
continue
}
processed.push(name)
const task = _.find(tasks, {name})
if (!isTask(task)) {
throw new Error(`Name not found: ${name}`)
}
if (task.deps) {
for (const dep of task.deps) {
graph.push([dep, name])
}
graph = graph.concat(execGraph(processed, task.deps))
}
graph.push([name, ''])
}
return graph
}
const execOrder = (tasks, name) => {
const deps = toposort(execGraph(tasks, [], [name]))
// remove '' if any
return _.compact(deps)
}
const isTask = task => {
return task && (typeof task.run === 'function' || Array.isArray(task.deps))
}
const isChildProcess = v => v instanceof ChildProcess
//const isPromise = v => v.then && typeof v.then === 'function'
process.on('SIGINT', function() {
log.info('cleaning up...')
for (let name in _childProcesses) {
const proc = _childProcesses[name]
if (proc) {
log.debug(`SIGHUP ${name}`)
process.kill(-proc.pid, 'SIGHUP')
}
}
process.exit()
})
const _childProcesses = {}
const _ran = {}
const runTask = async (task, args) => {
if (task.once && _ran[task.name]) {
log.debug(`SKIP ${task.name} ran once`)
return
}
const childProcess = _childProcesses[task.name]
if (childProcess) {
childProcess.on('close', () => {
_childProcesses[task.name] = null
setImmediate(() => runTask(task, args))
})
log.debug(`SIGHUP ${task.name}`)
// regarding -pid, see https://stackoverflow.com/a/33367711
process.kill(-childProcess.pid, 'SIGHUP')
return
}
log.debug(`RUN ${task.name}...`)
_ran[task.name] = true
const v = await task.run(args)
if (isChildProcess(v)) {
_childProcesses[task.name] = v
return new Promise((resolve, reject) => {
v.on('error', err => {
log.info('error occured', err)
reject(err)
})
})
}
return v
}
const getTask = (tasks, name) =>
_.find(tasks, task => task.name === name && isTask(task))
const run = async (tasks, names, args) => {
if (!tasks) {
throw new Error('`tasks` property is required')
}
if (!tasks.length) {
throw new Error('`tasks` is empty')
}
if (!names) {
throw new Error('`names` is empty')
}
if (typeof names === 'string') {
names = [names]
}
for (const name of names) {
const deps = execOrder(tasks, name)
for (const dep of deps) {
const task = getTask(tasks, dep)
if (task) {
// tasks can just be deps
if (task.run) {
await runTask(task, args)
}
} else {
throw new Error('Object is not a task')
}
}
}
}
const runThenWatch = async (tasks, name, args) => {
const task = getTask(tasks, name)
if (!(task && Array.isArray(task.watch))) {
throw new Error(`${name} is not a watchable task.`)
}
const globs = task.watch
await watch(globs, args, async argsWithEvent => {
return await run(tasks, name, argsWithEvent)
})
}
module.exports = {run, runThenWatch}