import { bold, green, underline, yellow} from "https://deno.land/std@v0.38.0/fmt/colors.ts";import { existsSync } from "https://deno.land/std@v0.38.0/fs/mod.ts";import { Graph } from "./graph.ts";import { abort, debug, DrakeError, env, isFileTask, isNormalTask, log, normalizePrereqs, normalizeTaskName, outOfDate} from "./utils.ts";
export type Action = (this: Task) => any;
export class Task { name: string; desc: string; prereqs: string[]; action?: Action;
constructor(name: string, desc: string, prereqs: string[], action?: Action) { name = normalizeTaskName(name); this.name = name; this.desc = desc; this.prereqs = normalizePrereqs(prereqs); if (action) { this.action = action.bind(this); } }
isOutOfDate(): boolean { if (!isFileTask(this.name)) { return true; } const prereqs: string[] = []; for (const prereq of this.prereqs) { if (!isFileTask(prereq)) { continue; } if (!existsSync(prereq)) { if (env("--dry-run")) { return true; } abort( `task: ${this.name}: missing prerequisite path: ${prereq}`, ); } prereqs.push(prereq); } return outOfDate(this.name, prereqs); }}
export class TaskRegistry extends Map<string, Task> { lastDesc: string;
constructor() { super(); this.lastDesc = ""; }
get(name: string): Task { name = normalizeTaskName(name); if (!this.has(name)) { abort(`missing task: ${name}`); } return super.get(name)!; }
set(name: string, task: Task) { name = normalizeTaskName(name); if (this.has(name)) { abort(`task already exists: ${name}`); } return super.set(name, task); }
desc(description: string): void { this.lastDesc = description; }
register(name: string, prereqs: string[], action?: Action): void { this.set(name, new Task(name, this.lastDesc, prereqs, action)); this.lastDesc = ""; }
list(): string[] { let keys = Array.from(this.keys()); if (!env("--list-all")) { keys = keys.filter((k) => this.get(k).desc); } const maxLen = keys.reduce(function (a, b) { return a.length > b.length ? a : b; }).length; const result: string[] = []; for (const k of keys.sort()) { const task = this.get(k); const padding = " ".repeat(maxLen - k.length); let msg = k; if (k === env("--default-task")) { msg = underline(msg); } msg += padding; if (task.desc) { msg = `${green(bold(msg))} ${task.desc}`; } else { msg = green(msg); } if (env("--list-all")) { msg += ` ${yellow(`[${task.prereqs}]`)}`; } result.push(msg); } return result; }
private expand(names: string[]): Task[] { let result: Task[] = []; names = names.slice(); names.reverse(); for (const name of names) { if (isFileTask(name) && !this.has(name)) { continue; } const task = this.get(name); result.unshift(task); result = this.resolveDependencies(task.prereqs).concat(result); } return result; }
resolveDependencies(names: string[]): Task[] { names = names.map((name) => normalizeTaskName(name)); const result: Task[] = []; for (const task of this.expand(names)) { if (result.find((t) => t.name === task.name)) { continue; } result.push(task); } return result; }
checkForCycles(): void { const graph = new Graph(); for (const task of this.keys()) { graph.addNode(task, this.get(task).prereqs.filter((p) => this.has(p))); } graph.searchForCycles(); if (graph.errors.length > 0) { abort(graph.errors.join(", ")); } }
async run(...names: string[]) { this.checkForCycles(); const tasks = this.resolveDependencies(names).filter((t) => t.action); debug("run", `[${tasks.map((t) => `"${t.name}"`)}]`); for (const task of tasks) { if (isNormalTask(task.name)) { await this.execute(task.name); } else { await this.executeFileTask(task); } } }
async execute(names: string | string[]) { if (typeof names === "string") { names = [names]; } names = names.map((name) => normalizeTaskName(name)); if (env("--dry-run")) { log(yellow(`${names} skipped`) + " (dry run)"); return; } log(green(bold(`${names} started`))); const startTime = new Date().getTime(); const promises: Promise<any>[] = []; for (const name of names) { const task = this.get(name); if (!task.action) { continue; } if (task.action.constructor.name === "AsyncFunction") { promises.push(task.action()); } else { task.action(); } } await Promise.all(promises); const endTime = new Date().getTime(); log( green(bold(`${names} finished`)) + ` in ${((endTime - startTime) / 1000).toFixed(2)} seconds`, ); }
async executeFileTask(task: Task) { if (!env("--always-make") && !task.isOutOfDate()) { log(yellow(`${task.name} skipped`) + " (up to date)"); return; } const oldInfo = existsSync(task.name) ? Deno.statSync(task.name) : null; const savedAbortExits = env("--abort-exits"); env("--abort-exits", false); try { await this.execute(task.name); } catch (e) { env("--abort-exits", savedAbortExits); const newInfo = existsSync(task.name) ? Deno.statSync(task.name) : null; if (!oldInfo && newInfo) { Deno.removeSync(task.name); } else if ( newInfo && oldInfo && newInfo.modified! > oldInfo.modified! ) { Deno.utimeSync(task.name, oldInfo.accessed!, oldInfo.modified!); } if (e instanceof DrakeError) { abort(e.message); } else { throw e; } } finally { env("--abort-exits", savedAbortExits); } }}