UNPKG

nadle

Version:

A modern, type-safe task runner for Node.js inspired by the awesome Gradle build tool

1,267 lines (1,243 loc) 39.8 kB
// package.json var package_default = { name: "nadle", version: "0.3.5", description: "A modern, type-safe task runner for Node.js inspired by the awesome Gradle build tool", type: "module", scripts: { build: 'pnpm "/build:/"', "build:tsup": "tsup", "build:ts": "tsc -p tsconfig.build.json", start: "pnpm run build:ts --watch --noEmit false", test: "vitest" }, files: [ "lib", "nadle.mjs", "CHANGELOG.md" ], main: "lib/index.js", bin: "nadle.mjs", types: "lib/index.d.ts", dependencies: { consola: "^3.4.2", execa: "^9.6.0", "fastest-levenshtein": "^1.0.16", "find-up": "^7.0.0", glob: "^11.0.2", jiti: "^2.4.2", "lodash-es": "^4.17.21", rimraf: "^6.0.1", "std-env": "^3.9.0", tinypool: "^1.1.0", tinyrainbow: "^2.0.0", yargs: "^18.0.0" }, devDependencies: { "@size-limit/file": "^11.2.0", "@types/lodash-es": "^4.17.12", "@types/node": "^20.17.57", "@types/yargs": "^17.0.33", fixturify: "^3.0.0", knip: "^5.60.2", "size-limit": "^11.2.0", "strip-ansi": "^7.1.0", tsup: "^8.5.0", typescript: "^5.8.2", vitest: "3.2.2" }, engines: { node: ">=20" }, author: { name: "Nam Hoang Le", email: "namhoangle.dev@gmail.com" }, repository: { type: "git", url: "https://github.com/nam-hle/nadle.git", directory: "packages/nadle" }, keywords: [ "nodejs", "task-runner", "build-tool", "typescript", "cli", "automation", "gradle-inspired", "task-orchestration", "type-safe", "developer-tools", "lightweight" ], homepage: "https://nadle.dev", bugs: "https://github.com/nam-hle/nadle/issues", "size-limit": [ { path: "lib/**", limit: "40 KB", name: "bundled" } ] }; // src/version.ts var VERSION = package_default.version; // src/core/types.ts var TaskStatus = /* @__PURE__ */ ((TaskStatus2) => { TaskStatus2["Registered"] = "registered"; TaskStatus2["Scheduled"] = "scheduled"; TaskStatus2["Running"] = "running"; TaskStatus2["Finished"] = "finished"; TaskStatus2["Failed"] = "failed"; return TaskStatus2; })(TaskStatus || {}); // src/core/task-registry.ts import c from "tinyrainbow"; var TaskRegistry = class { constructor() { this.registry = /* @__PURE__ */ new Map(); } register(name, task) { this.registry.set(name, task); } has(name) { return this.registry.has(name); } getAll() { return [...this.registry.values()]; } findByName(taskName) { return this.registry.get(taskName); } getByName(taskName) { const task = this.findByName(taskName); if (!task) { throw new Error(`Task ${c.bold(taskName)} not found`); } return task; } onTaskStart(name) { const task = this.getByName(name); task.status = "running" /* Running */; task.result.startTime = Date.now(); } onTaskFinish(name) { const task = this.getByName(name); task.status = "finished" /* Finished */; task.result.duration = Date.now() - (task.result.startTime ?? 0); } onTaskFailed(name) { const task = this.getByName(name); task.status = "failed" /* Failed */; task.result.duration = Date.now() - (task.result.startTime ?? 0); } onTasksScheduled(names) { for (const name of names) { this.getByName(name).status = "scheduled" /* Scheduled */; } } }; var taskRegistry = new TaskRegistry(); // src/core/logger.ts import c2 from "tinyrainbow"; // src/core/file-logger.ts import * as Path from "path"; import { inspect } from "util"; import * as Fs from "fs/promises"; var logItems = []; var ESC = "\\x1B["; var CLEAR_LINE = `${ESC}K`; var MOVE_CURSOR_ONE_ROW_UP = `${ESC}1A`; var SYNC_START = `${ESC}?2026h`; var SYNC_END = `${ESC}?2026l`; var ERASE_DOWN = `${ESC}J`; var ERASE_SCROLLBACK = `${ESC}3J`; var CURSOR_TO_START = `${ESC}1;1H`; var HIDE_CURSOR = `${ESC}?25l`; var SHOW_CURSOR = `${ESC}?25h`; var filePath = Path.join(import.meta.dirname, "..", "..", "logs", `${(/* @__PURE__ */ new Date()).toISOString()}.txt`); var FileLogger = class { constructor(namespace, options) { this.namespace = namespace; this.options = options; } log(subspace, ...args) { if (!this.options?.silent) { logItems.push({ args, subspace, namespace: this.namespace }); } } }; // src/core/consola-reporters.ts import { isCI, isTest } from "std-env"; import { LogLevels, createConsola } from "consola"; var [BasicReporter] = createConsola({ fancy: false }).options.reporters; var [FancyReporter] = createConsola({ fancy: true }).options.reporters; var CIReporter = class { log(logObj, ctx) { BasicReporter.log({ ...logObj, type: "" }, ctx); } }; function createConsolaReporters() { if (isTest) { return [BasicReporter]; } if (isCI) { return [new CIReporter()]; } return [FancyReporter]; } function createNadleConsola(options) { return createConsola({ formatOptions: { date: false }, level: LogLevels[options.logLevel], reporters: createConsolaReporters() }); } // src/core/constants.ts var UnnamedGroup = "Unnamed"; var ESC2 = "\x1B["; var ERASE_DOWN2 = `${ESC2}J`; var ERASE_SCROLLBACK2 = `${ESC2}3J`; var CURSOR_TO_START2 = `${ESC2}1;1H`; var HIDE_CURSOR2 = `${ESC2}?25l`; var CLEAR_SCREEN = "\x1Bc"; var CROSS = "\xD7"; var CHECK = "\u2713"; var RIGHT_ARROW = "\u2192"; var VERTICAL_BAR = "|"; var DEFAULT_CONFIG_FILE_NAME = "nadle.config"; // src/core/logger.ts var SupportLogLevels = ["error", "log", "info", "debug"]; var l = new FileLogger("logger"); var Logger = class { constructor(options) { this.outputStream = process.stdout; this.errorStream = process.stderr; this.options = { logLevel: "log", isWorkerThread: false, ...options }; this.consola = createNadleConsola(this.options); if (this.outputStream.isTTY) { this.outputStream.write(HIDE_CURSOR2); } if (!this.options.isWorkerThread) { const { stderr, stdout, ...consoleOptions } = this.consola.options; this.info(`Initialized logger with consola options:`, consoleOptions); } } /* eslint-disable @typescript-eslint/no-explicit-any */ log(message, ...args) { l.log("log", message, ...args); this._clearScreen(); this.consola.log(message, ...args); } error(message, ...args) { l.log("error", message, ...args); this._clearScreen(); this.consola.error(message, ...args); } warn(message, ...args) { l.log("warn", message, ...args); this._clearScreen(); this.consola.warn(message, ...args); } info(message, ...args) { l.log("info", message, ...args); this._clearScreen(); this.consola.info(message, ...args); } debug(message, ...args) { l.log("debug", message, ...args); this._clearScreen(); if (typeof message === "string") { this.consola.debug(message, ...args); return; } const { tag, ...rest } = message; if (Object.keys(rest).length === 0) { this.consola.debug(c2.yellow(tag), ...args); return; } this.consola.debug(rest, c2.yellow(tag), ...args); } /* eslint-enable @typescript-eslint/no-explicit-any */ clearFullScreen(message = "") { l.log("clearFullScreen"); if (message) { this.consola.log(`${CLEAR_SCREEN}${ERASE_SCROLLBACK2}${message}`); } else { this.outputStream.write(`${CLEAR_SCREEN}${ERASE_SCROLLBACK2}`); } } clearScreen(message, force = false) { l.log("clearScreen", { message }); this._clearScreenPending = message; if (force) { this._clearScreen(); } } _clearScreen() { l.log("_clearScreen"); if (this._clearScreenPending == null) { return; } const log = this._clearScreenPending; this._clearScreenPending = void 0; this.consola.log(`${CURSOR_TO_START2}${ERASE_DOWN2}${log}`); } getColumns() { return "columns" in this.outputStream ? this.outputStream.columns : 80; } }; // src/core/reporter.ts import { fileURLToPath } from "url"; import c3 from "tinyrainbow"; import { isCI as isCI2, isTest as isTest2 } from "std-env"; // src/core/utils.ts import Path2 from "path"; function noop() { } function capitalize(str) { if (str.length === 0) { return str; } return str.charAt(0).toUpperCase() + str.slice(1); } function formatTime(time) { if (time > 1e3) { return `${Math.round(time / 1e3)}s`; } return `${Math.round(time)}ms`; } function formatTimeString(date) { return date.toTimeString().split(" ")[0]; } function normalizeGlobPath(path) { if (path.startsWith(".")) { return path; } return `.${Path2.sep}${path}`; } function clamp(value, min, max) { return Math.min(Math.max(value, min), max); } // src/core/renderers/normal-renderer.ts var NormalRenderer = class { start() { noop(); } finish() { noop(); } schedule() { noop(); } }; // src/core/renderers/summary-renderer.ts import { stripVTControlCharacters } from "util"; var DEFAULT_RENDER_INTERVAL_MS = 200; var ESC3 = "\x1B["; var CLEAR_LINE2 = `${ESC3}K`; var MOVE_CURSOR_ONE_ROW_UP2 = `${ESC3}1A`; var SYNC_START2 = `${ESC3}?2026h`; var SYNC_END2 = `${ESC3}?2026l`; var l2 = new FileLogger("WindowRenderer"); var SummaryRenderer = class { constructor(options) { this.buffer = []; this.renderInterval = void 0; this.renderScheduled = false; this.windowHeight = 0; this.finished = false; this.cleanups = []; this.options = { interval: DEFAULT_RENDER_INTERVAL_MS, ...options }; this.streams = { error: options.logger.errorStream.write.bind(options.logger.errorStream), output: options.logger.outputStream.write.bind(options.logger.outputStream) }; this.cleanups.push(this.interceptStream(process.stdout, "output"), this.interceptStream(process.stderr, "error")); this.start(); } start() { l2.log("start"); this.finished = false; this.renderInterval = setInterval(() => this.schedule(), this.options.interval).unref(); } stop() { l2.log("stop"); this.cleanups.splice(0).forEach((fn) => fn()); clearInterval(this.renderInterval); } /** * Write all buffered output and stop buffering. * All intercepted writes are forwarded to actual write after this. */ finish() { l2.log("finish"); this.finished = true; this.flushBuffer(); clearInterval(this.renderInterval); } /** * Queue new render update */ schedule() { if (!this.renderScheduled) { this.renderScheduled = true; this.flushBuffer(); setTimeout(() => { this.renderScheduled = false; }, 100).unref(); } } flushBuffer() { if (this.buffer.length === 0) { return this.render(); } let current; for (const next of this.buffer.splice(0)) { if (!current) { current = next; continue; } if (current.type !== next.type) { this.render(current.message, current.type); current = next; continue; } current.message += next.message; } if (current) { this.render(current.message, current.type); } } render(message, type = "output") { l2.log("render", { message }); if (this.finished) { this.clearWindow(); return this.write(message ?? "", type); } const win = this.windowHeight; const windowContent = this.options.getWindow(); const rowCount = getRenderedRowCount(windowContent, this.options.logger.getColumns()); let padding = this.windowHeight - rowCount; if (padding > 0 && message) { padding -= getRenderedRowCount([message], this.options.logger.getColumns()); } l2.log("render", { padding, rowCount, windowHeight: win, col: this.options.logger.getColumns() }); this.write(SYNC_START2); this.clearWindow(); if (message) { this.write(message, type); } if (padding > 0) { this.write("\n".repeat(padding)); } this.write(windowContent.join("\n")); this.write(SYNC_END2); this.windowHeight = rowCount + Math.max(0, padding); } clearWindow() { l2.log("clearWindow", `this.windowHeight = ${this.windowHeight}`); if (this.windowHeight === 0) { return; } this.write(CLEAR_LINE2); for (let i = 1; i < this.windowHeight; i++) { this.write(`${MOVE_CURSOR_ONE_ROW_UP2}${CLEAR_LINE2}`); } this.windowHeight = 0; } interceptStream(stream, type) { const original = stream.write; stream.write = (chunk, _, callback) => { if (chunk) { if (this.finished) { this.write(chunk.toString(), type); } else { this.buffer.push({ type, message: chunk.toString() }); } } callback?.(); }; return function restore() { stream.write = original; }; } write(message, type = "output") { l2.log("write", { message }); this.streams[type](message); } }; function getRenderedRowCount(rows, columns) { let count = 0; for (const row of rows) { const text = stripVTControlCharacters(row); count += Math.max(1, Math.ceil(text.length / columns)); } return count; } // src/core/reporter.ts var DURATION_UPDATE_INTERVAL_MS = 100; var DefaultReporter = class { constructor(nadle) { this.nadle = nadle; this.taskStat = { failed: 0, running: 0, finished: 0, scheduled: 0 }; this.threadIdPerWorker = {}; this.startTime = ""; this.currentTime = 0; this.duration = 0; this.durationInterval = void 0; this.renderer = this.nadle.options.showSummary ? new SummaryRenderer({ logger: this.nadle.logger, getWindow: () => this.createSummary() }) : new NormalRenderer(); } printLabel(label) { return c3.dim(label.padEnd(11, " ")); } createSummary() { const summary = [""]; const stats = [ c3.cyanBright(`${this.taskStat.scheduled - this.taskStat.running - this.taskStat.finished - this.taskStat.failed} pending`), c3.yellow(`${this.taskStat.running} running`), c3.green(`${this.taskStat.finished} finished`) ].join(` ${c3.gray(VERTICAL_BAR)} `); summary.push([this.printLabel("Tasks"), stats, c3.dim(`(${this.taskStat.scheduled} scheduled)`)].join(" ")); summary.push([this.printLabel("Duration"), formatTime(this.duration)].join(" ")); summary.push(...this.printRunningTasks()); summary.push(""); return summary; } printRunningTasks() { const lines = Array.from({ length: this.nadle.options.maxWorkers }, () => ` ${c3.yellow(">")} ${c3.dim("IDLE")}`); for (const runningTask of this.nadle.registry.getAll().filter((task) => task.status === "running" /* Running */)) { lines[this.threadIdPerWorker[runningTask.name] - 1] = ` ${c3.yellow(">")} :${c3.bold(runningTask.name)}`; } return lines; } onInit() { const { minWorkers, maxWorkers } = this.nadle.options; if (!this.nadle.options.isWorkerThread) { this.nadle.logger.log(c3.bold(c3.cyan(`\u{1F6E0}\uFE0F Welcome to Nadle v${this.nadle.version}!`))); this.nadle.logger.info(`Using Nadle from: ${fileURLToPath(import.meta.resolve("nadle"))}`); this.nadle.logger.log(c3.dim(`Loading configuration file from: ${this.nadle.options.configPath}`)); this.nadle.logger.log( c3.dim(`Using ${minWorkers === maxWorkers ? minWorkers : `${minWorkers}\u2013${maxWorkers}`} worker${maxWorkers > 1 ? "s" : ""} for task execution`) ); this.nadle.logger.info("Resolved options:", this.nadle.options); this.nadle.logger.info("Detected environments:", { CI: isCI2, TEST: isTest2 }); } this.renderer.start(); } async onTaskStart(task, threadId) { this.nadle.logger.log(`${c3.yellow(">")} Task ${c3.bold(task.name)} started `); this.taskStat = { ...this.taskStat, running: ++this.taskStat.running }; this.threadIdPerWorker = { ...this.threadIdPerWorker, [task.name]: threadId }; this.renderer.schedule(); } async onTaskFinish(task) { this.nadle.logger.log(` ${c3.green(CHECK)} Task ${c3.bold(task.name)} done in ${formatTime(task.result.duration ?? 0)}`); this.taskStat = { ...this.taskStat, running: --this.taskStat.running, finished: ++this.taskStat.finished }; this.renderer.schedule(); } async onTaskFailed(task) { this.nadle.logger.log(` ${c3.red(CROSS)} Task ${c3.bold(task.name)} failed in ${formatTime(task.result.duration ?? 0)}`); this.taskStat = { ...this.taskStat, failed: ++this.taskStat.failed, running: --this.taskStat.running }; this.renderer.schedule(); } async onTasksScheduled(tasks) { this.nadle.logger.info(`Scheduled tasks: ${tasks.join(", ")}`); this.taskStat = { ...this.taskStat, scheduled: tasks.length }; this.renderer.schedule(); } async onExecutionStart() { this.nadle.logger.info("Execution started"); this.startTimers(); this.renderer.start(); } async onExecutionFinish() { this.nadle.logger.info("Execution finished"); this.renderer.finish(); clearInterval(this.durationInterval); const finishedTasks = `${c3.bold(this.taskStat.finished)} task${this.taskStat.finished > 1 ? "s" : ""}`; this.nadle.logger.log(` ${c3.bold(c3.green("RUN SUCCESSFUL"))} in ${c3.bold(formatTime(this.duration))} ${c3.dim(`(${finishedTasks} executed)`)}`); } async onExecutionFailed(error) { this.nadle.logger.info("Execution failed"); this.renderer.finish(); clearInterval(this.durationInterval); const finishedTasks = `${c3.bold(this.taskStat.finished)} task${this.taskStat.finished > 1 ? "s" : ""}`; const failedTasks = `${c3.bold(this.taskStat.failed)} task${this.taskStat.failed > 1 ? "s" : ""}`; this.nadle.logger.log( ` ${c3.bold(c3.red("RUN FAILED"))} in ${c3.bold(formatTime(this.duration))} ${c3.dim(`(${finishedTasks} executed, ${failedTasks} failed)`)}` ); if (!this.nadle.options.stacktrace) { this.nadle.logger.log( ` For more details, re-run the command with the ${c3.yellow("--stacktrace")} option to display the full error and help identify the root cause.` ); } else { this.nadle.logger.error(error instanceof Error ? error.stack : error); } } startTimers() { const start = performance.now(); this.startTime = formatTimeString(/* @__PURE__ */ new Date()); this.durationInterval = setInterval(() => { this.currentTime = performance.now(); this.duration = this.currentTime - start; }, DURATION_UPDATE_INTERVAL_MS).unref(); } }; // src/core/resolve-task.ts import { words } from "lodash-es"; import { distance } from "fastest-levenshtein"; var InitialsMatcher = { name: "InitialsMatcher", match(input, pattern) { return input.toLowerCase() === getInitials(pattern); } }; var FUZZY_MATCH_THRESHOLD = 2; var MAX_SUGGESTION_DISTANCE = 3; var fuzzyMatcher = { name: "FuzzyMatcher", match(input, pattern) { return distance(input.toLowerCase(), pattern.toLowerCase()) <= FUZZY_MATCH_THRESHOLD; } }; function match(input, allTasks, matcher) { return allTasks.filter((task) => matcher.match(input, task)); } function resolveTask(input, allTasks) { if (allTasks.includes(input)) { return { result: input }; } const initialsMatches = match(input, allTasks, InitialsMatcher); if (initialsMatches.length === 1) { return { result: initialsMatches[0] }; } const fuzzyMatches = match(input, allTasks, fuzzyMatcher); if (fuzzyMatches.length === 1) { return { result: fuzzyMatches[0] }; } const suggestedTasks = allTasks.flatMap((name) => { const score = distance(input, name); if (score <= MAX_SUGGESTION_DISTANCE) { return { name, score }; } return []; }).sort((a, b) => a.score - b.score).map(({ name }) => name); return { result: void 0, suggestions: suggestedTasks }; } function getInitials(name) { return words(name).map((w) => w[0]).join("").toLowerCase(); } function formatSuggestions(names) { if (names.length === 0) { return ""; } let tasksList; if (names.length === 1) { tasksList = names[0]; } else if (names.length === 2) { tasksList = `${names[0]} or ${names[1]}`; } else { tasksList = `${names.slice(0, -1).join(", ")}, or ${names.at(-1)}`; } return ` Did you mean ${tasksList}?`; } // src/core/options/options-resolver.ts import Fs2 from "fs"; import Os from "os"; import Path3 from "path"; import Process from "process"; import { isCI as isCI3 } from "std-env"; import { findUpSync } from "find-up"; var OptionsResolver = class _OptionsResolver { constructor(cliOptions) { this.cliOptions = cliOptions; this.defaultOptions = { parallel: false, logLevel: "log", showConfig: false, showSummary: !isCI3, isWorkerThread: false }; this.#options = this.resolve(); } static { this.SUPPORT_EXTENSIONS = ["js", "mjs", "ts", "mts"]; } #options; get options() { return this.#options; } addConfigFileOptions(configFileOptions) { this.#options = this.resolve(configFileOptions); } resolve(configFileOptions) { const baseOptions = { ...this.defaultOptions, ...configFileOptions, ...this.cliOptions }; const maxWorkers = this.resolveWorkers(baseOptions.maxWorkers); const minWorkers = Math.min(this.resolveWorkers(baseOptions.minWorkers), maxWorkers); return { ...baseOptions, minWorkers, maxWorkers, configPath: this.resolveConfigPath(baseOptions.configPath) }; } resolveConfigPath(configPath) { const cwd = Process.cwd(); if (configPath !== void 0) { const resolvedConfigPath = Path3.resolve(cwd, configPath); if (!Fs2.existsSync(resolvedConfigPath)) { throw new Error(`Config file not found at ${resolvedConfigPath}. Please check the path.`); } return resolvedConfigPath; } const resolveConfigPath = findUpSync(_OptionsResolver.SUPPORT_EXTENSIONS.map((ext) => `${DEFAULT_CONFIG_FILE_NAME}.${ext}`)); if (!resolveConfigPath) { throw new Error( `No ${DEFAULT_CONFIG_FILE_NAME}.{${_OptionsResolver.SUPPORT_EXTENSIONS.join(",")}} found in ${Process.cwd()} directory or parent directories. Please use --config to specify a custom path.` ); } return resolveConfigPath; } resolveWorkers(configValue) { let result; if (configValue === void 0) { result = this.availableWorkers - 1; } else if (typeof configValue === "number") { result = configValue; } else if (typeof configValue === "string") { result = Math.round(Number.parseInt(configValue) / 100 * this.availableWorkers); } else { throw new Error(`Invalid worker value: ${configValue}`); } return clamp(result, 1, this.availableWorkers); } get availableWorkers() { if (process.env.NADLE_MAX_WORKERS) { return Number(process.env.NADLE_MAX_WORKERS); } return Os.availableParallelism(); } }; // src/core/options/cli-options.ts function resolveCLIOptions(argv) { let resolvedOptions = {}; const aliases = Object.values(CLIOptions).map(({ options }) => options.alias).filter(Boolean); for (const [key, value] of Object.entries(argv)) { if (aliases.includes(key) || key.includes("-") || key === "_" || key === "$0" || key === "tasks") { continue; } if ((key === "minWorkers" || key === "maxWorkers") && value === void 0) { continue; } if (key === "config") { if (value !== void 0) { resolvedOptions = { ...resolvedOptions, configPath: value }; } continue; } resolvedOptions = { ...resolvedOptions, [key]: value }; } return resolvedOptions; } var CLIOptions = { configPath: { key: "config", options: { alias: "c", type: "string", description: "Path to config file", defaultDescription: `<cwd>/build.nadle.{${OptionsResolver.SUPPORT_EXTENSIONS.join(",")}}` } }, list: { key: "list", options: { alias: "l", default: false, type: "boolean", description: "List all available tasks" } }, parallel: { key: "parallel", options: { default: false, type: "boolean", description: "Run all specified tasks in parallel regardless of their order, while still respecting task dependencies." } }, dryRun: { key: "dry-run", options: { alias: "m", default: false, type: "boolean", description: "Run specified tasks in dry run mode" } }, stacktrace: { key: "stacktrace", options: { default: false, type: "boolean", description: "Print stacktrace on error" } }, showConfig: { key: "show-config", options: { default: false, type: "boolean", description: "Print the resolved configuration" } }, showSummary: { key: "show-summary", options: { hidden: true, type: "boolean" } }, logLevel: { key: "log-level", options: { type: "string", defaultDescription: "log", choices: SupportLogLevels, describe: "Set the logging level" } }, minWorkers: { key: "min-workers", options: { type: "string", describe: "Minimum number of workers (integer or percentage)", defaultDescription: "Os.availableParallelism() - 1", coerce: createWorkerCoercer("min") } }, maxWorkers: { key: "max-workers", options: { type: "string", describe: "Maximum number of workers (integer or percentage)", defaultDescription: "Os.availableParallelism() - 1", coerce: createWorkerCoercer("max") } } }; function createWorkerCoercer(type) { return (workers) => { if (workers === void 0) { return void 0; } if (/^\d+$/.test(workers)) { return Number(workers); } if (/^\d+(\.\d+)?%$/.test(workers)) { return workers; } throw new Error(`Invalid value for --${type}-workers. It should be an integer or a percentage (e.g., 50%).`); }; } // src/core/options/options-registry.ts var OptionsRegistry = class { constructor() { this.options = {}; } add(options) { this.options = options; } get() { return this.options; } }; var optionRegistry = new OptionsRegistry(); // src/core/nadle.ts import process2 from "process"; import { pathToFileURL } from "url"; import c5 from "tinyrainbow"; import { createJiti } from "jiti"; // src/core/task-pool.ts import TinyPool from "tinypool"; var TaskPool = class { constructor(nadle, getNextReadyTasks) { this.nadle = nadle; this.getNextReadyTasks = getNextReadyTasks; this.pool = new TinyPool({ concurrentTasksPerWorker: 1, minThreads: this.nadle.options.minWorkers, maxThreads: this.nadle.options.maxWorkers, filename: new URL("./worker.js", import.meta.url).href }); } async run() { try { await Promise.all(Array.from(this.getNextReadyTasks()).map((taskName) => this.pushTask(taskName))); } finally { await this.pool.destroy(); } } async pushTask(taskName) { const task = this.nadle.registry.getByName(taskName); try { const { port2: poolPort, port1: workerPort } = new MessageChannel(); poolPort.on("message", async (msg) => { if (msg.type === "start") { await this.nadle.onTaskStart(task, msg.threadId); } }); const workerParams = { name: task.name, port: workerPort, env: process.env, options: { ...this.nadle.options, showSummary: false, isWorkerThread: true } }; await this.pool.run(workerParams, { transferList: [workerPort] }); await this.nadle.onTaskFinish(task); } catch (error) { await this.nadle.onTaskFailed(task); throw error; } await Promise.all(Array.from(this.getNextReadyTasks(taskName)).map((taskName2) => this.pushTask(taskName2))); } }; // src/core/task-scheduler.ts import c4 from "tinyrainbow"; // src/core/ensure-map.ts var EnsureMap = class extends Map { constructor(initializer) { super(); this.initializer = initializer; } get(key) { if (this.has(key)) { return super.get(key); } const value = this.initializer(); this.set(key, value); return value; } update(key, updater) { const value = this.get(key); const updatedValue = updater(value); this.set(key, updatedValue); return updatedValue; } }; // src/core/task-scheduler.ts var TaskScheduler = class { constructor(context, taskNames) { this.context = context; this.taskNames = taskNames; // Map between a task and the set of tasks that depend on it this.dependentsGraph = new EnsureMap(() => /* @__PURE__ */ new Set()); // Map between a task and the set of tasks that it depends on this.dependencyGraph = new EnsureMap(() => /* @__PURE__ */ new Set()); // Map between a task and the set of tasks that it depends on transitively this.transitiveDependencyGraph = new EnsureMap(() => /* @__PURE__ */ new Set()); // Map between a tasks and its indegree this.indegree = new EnsureMap(() => 0); // Set of tasks that are ready to be executed this.readyTasks = /* @__PURE__ */ new Set(); // The main task listed in the tasks argument need to be focused on this.mainTask = void 0; taskNames.forEach((taskName) => this.analyze(taskName)); taskNames.forEach((taskName) => this.detectCycle(taskName, [taskName])); this.context.nadle.logger.debug({ tag: "Scheduler" }, `transitiveDependencyGraph`, this.transitiveDependencyGraph); this.context.nadle.logger.debug({ tag: "Scheduler" }, `dependencyGraph`, this.dependencyGraph); if (!context.nadle.options.parallel) { this.mainTask = taskNames[0]; } } detectCycle(taskName, paths) { for (const dependency of this.dependencyGraph.get(taskName)) { const startTaskIndex = paths.indexOf(dependency); if (startTaskIndex !== -1) { const cyclePath = [...paths.slice(startTaskIndex), dependency].map(c4.bold).join(` ${RIGHT_ARROW} `); this.context.nadle.logger.error(`Cycle detected: ${cyclePath}`); throw new Error(`Cycle detected: ${cyclePath}`); } this.detectCycle(dependency, [...paths, dependency]); } } analyze(taskName) { if (this.dependencyGraph.has(taskName)) { return; } const task = this.context.nadle.registry.getByName(taskName); const dependencies = new Set(task.configResolver({ context: this.context }).dependsOn ?? []); this.dependencyGraph.set(taskName, dependencies); this.indegree.set(taskName, dependencies.size); this.transitiveDependencyGraph.set(taskName, new Set(dependencies)); for (const dependency of dependencies) { this.dependentsGraph.update(dependency, (oldDependents) => oldDependents.add(taskName)); this.analyze(dependency); this.transitiveDependencyGraph.update(taskName, (current) => { this.transitiveDependencyGraph.get(dependency).forEach((d) => current.add(d)); return current; }); } } get scheduledTask() { return Array.from(this.dependencyGraph.keys()); } getIndegreeEntries() { const runningRootTask = this.mainTask; if (!runningRootTask) { return this.indegree.entries(); } return new Map( Array.from(this.indegree.entries()).filter( ([taskName]) => taskName === runningRootTask || this.transitiveDependencyGraph.get(runningRootTask)?.has(taskName) ) ); } isBelongToRootTaskTree(taskName) { if (!this.mainTask) { return true; } if (taskName === this.mainTask) { return true; } return this.transitiveDependencyGraph.get(this.mainTask).has(taskName); } getReadyTasks(doneTask) { this.context.nadle.logger.debug({ tag: "Scheduler" }, `runningRoot = ${this.mainTask}, finishedTaskName = ${doneTask}`); if (doneTask === void 0) { return this.getInitialReadyTasks(); } const nextReadyTasks = /* @__PURE__ */ new Set(); for (const dependentTask of this.dependentsGraph.get(doneTask)) { if (this.readyTasks.has(dependentTask)) { continue; } let indegree = this.indegree.get(dependentTask); if (indegree === 0) { const message = `Incorrect state. Expect ${dependentTask} to have indegree > 0 because it depends on the running task ${doneTask}`; this.context.nadle.logger.error(message); throw new Error(message); } this.indegree.set(dependentTask, --indegree); if (indegree === 0 && this.isBelongToRootTaskTree(dependentTask)) { nextReadyTasks.add(dependentTask); this.readyTasks.add(dependentTask); } } if (doneTask === this.mainTask) { const nextMainTask = this.moveToNextMainTask(); if (!nextMainTask) { return /* @__PURE__ */ new Set(); } return this.getReadyTasks(); } this.context.nadle.logger.debug({ tag: "Scheduler" }, `Next tasks = ${Array.from(nextReadyTasks).join(",")}`); return nextReadyTasks; } getInitialReadyTasks() { const nextReadyTasks = /* @__PURE__ */ new Set(); for (const [taskName, indegree] of this.getIndegreeEntries()) { this.context.nadle.logger.debug({ tag: "Scheduler" }, `taskName = ${taskName}, indegree = ${indegree}`); if (indegree === 0 && !this.readyTasks.has(taskName)) { nextReadyTasks.add(taskName); this.readyTasks.add(taskName); } } this.context.nadle.logger.debug({ tag: "Scheduler" }, `Next tasks = ${Array.from(nextReadyTasks).join(",")}`); return nextReadyTasks; } moveToNextMainTask() { this.mainTask = this.mainTask !== void 0 ? this.taskNames[this.taskNames.indexOf(this.mainTask) + 1] : void 0; return this.mainTask; } getOrderedTasks(task) { const readyTasks = Array.from(this.getReadyTasks(task)); for (const readyTask of readyTasks) { readyTasks.push(...this.getOrderedTasks(readyTask)); } return readyTasks; } }; // src/core/nadle.ts var Nadle = class { constructor(options) { this.version = VERSION; this.registry = taskRegistry; this.optionsResolver = new OptionsResolver(options); this.logger = new Logger(options); this.reporter = new DefaultReporter(this); this.reporter.onInit?.(); } async execute(tasks) { await this.registerTask(); this.optionsResolver.addConfigFileOptions(optionRegistry.get()); try { const resolvedTasks = this.resolveTasks(tasks); this.reporter.onExecutionStart?.(); if (this.options.showConfig) { this.showConfig(); } else if (this.options.list) { this.listTasks(); } else if (this.options.dryRun) { this.dryRunTasks(resolvedTasks); } else { await this.runTasks(resolvedTasks); } } catch (error) { this.reporter.onExecutionFailed?.(error); process2.exit(error.errorCode || 1); } this.reporter.onExecutionFinish?.(); } get options() { return this.optionsResolver.options; } async runTasks(tasks) { if (tasks.length === 0) { this.printNoTasksFound(); return; } const scheduler = new TaskScheduler({ nadle: this }, tasks); await this.onTasksScheduled(scheduler.scheduledTask); await new TaskPool(this, (taskName) => scheduler.getReadyTasks(taskName)).run(); } dryRunTasks(tasks) { if (tasks.length === 0) { this.printNoTasksFound(); return; } const orderedTasks = new TaskScheduler({ nadle: this }, tasks).getOrderedTasks(); this.logger.log(c5.bold("Execution plan:")); for (const task of orderedTasks) { this.logger.log(`${c5.yellow(">")} Task ${c5.bold(task)}`); } } listTasks() { if (taskRegistry.getAll().length === 0) { this.logger.log("No tasks found"); return; } const groups = this.computeTaskGroups(); for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) { const [groupName, tasks] = groups[groupIndex]; const label = `${capitalize(groupName)} tasks`; this.logger.log(c5.bold(label)); this.logger.log(c5.bold("-".repeat(label.length))); for (const task of tasks) { const { description, name: taskName } = task; if (description) { this.logger.log(c5.bold(c5.green(taskName)) + c5.yellow(` - ${description}`)); } else { this.logger.log(c5.green(taskName)); } } if (groupIndex < groups.length - 1) { this.logger.log(""); } } } showConfig() { this.logger.log(JSON.stringify(this.options, null, 2)); } resolveTasks(tasks) { const allTasks = this.registry.getAll().map(({ name }) => name); const resolveTaskPairs = []; const resolvedTasks = tasks.map((task) => { const resolvedTask = resolveTask(task, allTasks); if (resolvedTask.result === void 0) { const message = `Task ${c5.yellow(c5.bold(task))} not found.${formatSuggestions(resolvedTask.suggestions.map((task2) => c5.yellow(c5.bold(task2))))}`; this.logger.error(message); throw new Error(message); } if (resolvedTask.result !== task) { resolveTaskPairs.push({ original: task, resolved: resolvedTask.result }); } return resolvedTask.result; }); if (resolveTaskPairs.length > 0) { const maxOriginTaskLength = Math.max(...resolveTaskPairs.map(({ original }) => original?.length ?? 0)); const message = [ `Resolved tasks: `, ...resolveTaskPairs.map( ({ resolved, original }) => `${" ".repeat(4)}${c5.yellow(c5.bold(original?.padEnd(maxOriginTaskLength, " ")))} ${RIGHT_ARROW} ${c5.green(c5.bold(resolved))} ` ) ].join(""); this.logger.log(message); } this.logger.info(`Resolved tasks: [ ${resolvedTasks.join(", ")} ]`); return resolvedTasks; } computeTaskGroups() { const tasksByGroup = {}; for (const task of this.registry.getAll()) { const { description, group = UnnamedGroup } = task.configResolver({ context: { nadle: this } }); tasksByGroup[group] ??= []; tasksByGroup[group].push({ ...task, description }); } return Object.entries(tasksByGroup).sort(([firstGroupName], [secondGroupName]) => { if (firstGroupName === UnnamedGroup) { return 1; } if (secondGroupName === UnnamedGroup) { return -1; } return firstGroupName.localeCompare(secondGroupName); }).map(([groupName, tasks]) => { return [groupName, tasks.sort((firstTask, secondTask) => firstTask.name.localeCompare(secondTask.name))]; }); } printNoTasksFound() { this.logger.log("No tasks were specified. Please specify one or more tasks to execute, or use the --list option to view available tasks."); } async registerTask() { const configFile = this.options.configPath; const jiti = createJiti(import.meta.url, { interopDefault: true, extensions: OptionsResolver.SUPPORT_EXTENSIONS.map((ext) => `.${ext}`) }); await jiti.import(pathToFileURL(configFile).toString()); } async onTaskStart(task, threadId) { this.registry.onTaskStart(task.name); await this.reporter.onTaskStart?.(task, threadId); } async onTaskFinish(task) { this.registry.onTaskFinish(task.name); await this.reporter.onTaskFinish?.(task); } async onTaskFailed(task) { this.registry.onTaskFailed(task.name); await this.reporter.onTaskFailed?.(task); } async onTasksScheduled(tasks) { this.registry.onTasksScheduled(tasks); await this.reporter.onTasksScheduled?.(tasks); } }; export { VERSION, TaskStatus, TaskRegistry, taskRegistry, SupportLogLevels, Logger, normalizeGlobPath, DefaultReporter, resolveTask, formatSuggestions, resolveCLIOptions, CLIOptions, optionRegistry, Nadle }; //# sourceMappingURL=chunk-U7OSCGJU.js.map