UNPKG

nadle

Version:

A type-safe, Gradle-inspired task runner for Node.js. Sharp tasks. Fast builds.

1,407 lines (1,368 loc) 100 kB
#!/usr/bin/env node import { CHECK, CLEAR_SCREEN, COLON, CROSS, CURSOR_TO_START, CURVE_ARROW, ConfigurationError, CyclicDependencyError, DASH, DEFAULT_CACHE_DIR_NAME, ERASE_DOWN, ERASE_SCROLLBACK, HIDE_CURSOR, LINES, LogLevels, MaybeArray, Messages, NadleError, RIGHT_ARROW, TaskExecutionError, TaskNotFoundError, UNDERSCORE, VERTICAL_BAR, bindObject, capitalize, clamp, createNadleConsola, createPlainReporters, formatTime, highlight, isPathExists, noop, runWithInstance } from "./chunk-U2TDD2B5.js"; // packages/nadle/src/core/engine/worker.ts import Path5 from "path"; import WorkerThreads2 from "worker_threads"; import c7 from "tinyrainbow"; import { getWorkspaceById as getWorkspaceById3 } from "@nadle/project-resolver"; // packages/nadle/src/core/nadle.ts import Process2 from "process"; import { getWorkspaceById as getWorkspaceById2, ROOT_WORKSPACE_ID as ROOT_WORKSPACE_ID3, isRootWorkspaceId as isRootWorkspaceId4 } from "@nadle/project-resolver"; // packages/nadle/src/core/handlers/list-handler.ts import c from "tinyrainbow"; import { groupBy } from "lodash-es"; import { isRootWorkspaceId } from "@nadle/project-resolver"; // packages/nadle/src/core/handlers/base-handler.ts var BaseHandler = class { constructor(context) { this.context = context; } }; // packages/nadle/src/core/utilities/comparator.ts function combineComparators(comparators) { return (a, b) => { for (const compare of comparators) { const result = compare(a, b); if (result !== 0) { return result; } } return 0; }; } // packages/nadle/src/core/handlers/list-handler.ts var ListHandler = class _ListHandler extends BaseHandler { constructor() { super(...arguments); this.name = "list"; this.description = "Lists all registered tasks with their groups and descriptions."; } static { this.UncategorizedGroup = "Uncategorized"; } canHandle() { return this.context.options.list; } handle() { if (this.context.taskRegistry.tasks.length === 0) { this.context.logger.log("No tasks found"); return; } const groups = this.computeGroups(); for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) { const [groupName, tasks] = groups[groupIndex]; const label = `${capitalize(groupName)} tasks`; this.context.logger.log(c.bold(label)); this.context.logger.log(c.bold(DASH.repeat(label.length))); for (const task of tasks) { const { label: label2, description } = task; if (description) { this.context.logger.log(c.bold(c.green(label2)) + c.yellow(` - ${description}`)); } else { this.context.logger.log(c.green(label2)); } } if (groupIndex < groups.length - 1) { this.context.logger.log(""); } } } computeGroups() { const tasks = this.context.taskRegistry.tasks.map((task) => { const { description, group = _ListHandler.UncategorizedGroup } = task.configResolver(); return { ...task, group, description }; }); const groupComparator = combineComparators([ ([a], [b]) => Number(a === _ListHandler.UncategorizedGroup) - Number(b === _ListHandler.UncategorizedGroup), ([a], [b]) => a.localeCompare(b) ]); const taskComparator = combineComparators([ (a, b) => Number(isRootWorkspaceId(b.workspaceId)) - Number(isRootWorkspaceId(a.workspaceId)), (a, b) => a.label.localeCompare(b.label) ]); return Object.entries(groupBy(tasks, "group")).sort(groupComparator).map(([groupName, tasks2]) => [groupName, tasks2.sort(taskComparator)]); } }; // packages/nadle/src/core/handlers/dry-run-handler.ts import c2 from "tinyrainbow"; // packages/nadle/src/core/interfaces/resolved-task.ts var ResolvedTask; ((ResolvedTask2) => { ResolvedTask2.getId = (task) => task.taskId; })(ResolvedTask || (ResolvedTask = {})); // packages/nadle/src/core/handlers/dry-run-handler.ts var DryRunHandler = class extends BaseHandler { constructor() { super(...arguments); this.name = "dry-run"; this.description = "Prints the execution plan without running tasks."; } canHandle() { return this.context.options.dryRun; } handle() { if (this.context.options.tasks.length === 0) { this.context.logger.log(Messages.NoTasksFound()); return; } const scheduler = this.context.taskScheduler.init(); const taskIds = scheduler.getExecutionPlan(); this.context.logger.log(c2.bold("Execution plan:")); const { passthroughArgs } = this.context.options; const requestedTaskIds = new Set(this.context.options.tasks.map(ResolvedTask.getId)); for (const taskId of taskIds) { const label = this.context.taskRegistry.getTaskById(taskId).label; const implicitDeps = scheduler.getImplicitDeps(taskId); const argsSuffix = passthroughArgs.length > 0 && requestedTaskIds.has(taskId) ? c2.dim(` (args: ${passthroughArgs.join(" ")})`) : ""; const suffix = implicitDeps.length > 0 ? c2.dim(` (after ${implicitDeps.map((d) => this.context.taskRegistry.getTaskById(d).label).join(", ")} \u2014 implicit)`) : ""; this.context.logger.log(`${c2.yellow(">")} Task ${c2.bold(label)}${argsSuffix}${suffix}`); } } }; // packages/nadle/src/core/engine/executor.ts import WorkerThreads from "worker_threads"; import TinyPool from "tinypool"; var PoolExecutor = class { constructor(params) { this.pool = new TinyPool({ concurrentTasksPerWorker: 1, minThreads: params.minThreads, maxThreads: params.maxThreads, filename: new URL("./worker.js", import.meta.url).href }); } async run(params, notify) { const { port2: poolPort, port1: workerPort } = new WorkerThreads.MessageChannel(); let resolveMessageReceived; const messageReceived = new Promise((resolve) => { resolveMessageReceived = resolve; }); poolPort.on("message", async (message) => { try { await notify(message); } finally { resolveMessageReceived(); } }); try { const outputsFingerprint = await this.pool.run({ ...params, port: workerPort }, { transferList: [workerPort] }); await messageReceived; return outputsFingerprint; } finally { poolPort.close(); } } async destroy() { await this.pool.destroy(); } }; var InlineExecutor = class { constructor(nadle) { this.nadle = nadle; // Serializes task execution: the scheduler dispatches ready tasks concurrently, // but maxWorkers === 1 must run them strictly one at a time, matching the // single-thread pool. Each run() chains onto the previous one's completion. this.tail = Promise.resolve(); } run(params, notify) { const result = this.tail.then(() => runTask(this.nadle, params, notify)); this.tail = result.catch(() => void 0); return result; } async destroy() { } }; // packages/nadle/src/core/engine/task-pool.ts var TERMINATING_WORKER_ERROR = "Terminating worker thread"; function toTaskExecutionError(error, label) { if (error instanceof NadleError) { return error; } const message = error instanceof Error ? error.message : String(error); return new TaskExecutionError(`Task ${label} failed: ${message}`, { cause: error }); } var TaskPool = class { constructor(context, getNextReadyTasks) { this.context = context; this.getNextReadyTasks = getNextReadyTasks; this.outputFingerprints = /* @__PURE__ */ new Map(); const { minWorkers, maxWorkers } = this.context.options; this.executor = maxWorkers === 1 ? new InlineExecutor(this.context) : new PoolExecutor({ minThreads: minWorkers, maxThreads: maxWorkers }); } async run() { try { await Promise.all(Array.from(this.getNextReadyTasks()).map((taskId) => this.pushTask(taskId))); } finally { await this.executor.destroy(); } } async pushTask(taskId) { const task = this.context.taskRegistry.getTaskById(taskId); try { const { executeType, outputsFingerprint } = await this.executeWorker(taskId); if (outputsFingerprint) { this.outputFingerprints.set(taskId, outputsFingerprint); } if (executeType === "execute") { await this.context.eventEmitter.onTaskFinish(task); } else if (executeType === "up-to-date") { await this.context.eventEmitter.onTaskUpToDate(task); } else if (executeType === "from-cache") { await this.context.eventEmitter.onTaskRestoreFromCache(task); } else { throw new Error(`Unknown execute type: ${executeType}`); } } catch (error) { if (error instanceof Error && error.message === TERMINATING_WORKER_ERROR && this.context.executionTracker.getTaskStatus(task.id) === "running" /* Running */) { await this.context.eventEmitter.onTaskCanceled(task); return; } await this.context.eventEmitter.onTaskFailed(task); throw toTaskExecutionError(error, task.label); } await Promise.all(Array.from(this.getNextReadyTasks(taskId)).map((readyTaskId) => this.pushTask(readyTaskId))); } async executeWorker(taskId) { const task = this.context.taskRegistry.getTaskById(taskId); let executeType = "execute"; const notify = async (message) => { if (message.type === "start") { await this.context.eventEmitter.onTaskStart(task, message.threadId); } else if (message.type === "up-to-date") { executeType = "up-to-date"; } else if (message.type === "from-cache") { executeType = "from-cache"; } }; const workerParams = { taskId: task.id, env: process.env, options: { ...this.context.options, footer: false }, dependencyFingerprints: this.collectDependencyFingerprints(taskId) }; const outputsFingerprint = await this.executor.run(workerParams, notify); return { executeType, outputsFingerprint }; } collectDependencyFingerprints(taskId) { const deps = this.context.taskScheduler.getDirectDependencies(taskId); const fingerprints = {}; for (const depId of deps) { const fp = this.outputFingerprints.get(depId); if (fp) { fingerprints[depId] = fp; } } return fingerprints; } }; // packages/nadle/src/core/views/tasks-selection.tsx import React2, { useState } from "react"; import { Box, Text as Text3, render, useApp, useInput } from "ink"; // packages/nadle/src/core/views/visible-task.tsx import { Text } from "ink"; import { jsx, jsxs } from "react/jsx-runtime"; var VisibleTask = ({ task }) => { const { label, selected, pointing, TaskLabel, description } = task; return /* @__PURE__ */ jsxs(Text, { color: pointing ? PRIMARY_COLOR : void 0, children: [ pointing ? ">" : " ", " ", `[${selected ? "x" : " "}]`, " ", /* @__PURE__ */ jsx(TaskLabel, {}), " ", description ? /* @__PURE__ */ jsx(Text, { color: SECONDARY_COLOR, children: description }) : "" ] }, label); }; // packages/nadle/src/core/views/use-searching.tsx import React from "react"; import { Text as Text2 } from "ink"; import fuzzySort from "fuzzysort"; import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime"; var VISIBLE_TASKS_LIMIT = 5; function useSearching(options) { const { tasks, cursor, searchText, selectedTasks } = options; return React.useMemo(() => { if (!searchText) { const visibleTasks = tasks.slice(0, VISIBLE_TASKS_LIMIT); const maxLength2 = Math.max(...visibleTasks.map((task) => task.label.length)); return visibleTasks.map((task) => { return { ...task, pointing: cursor === tasks.indexOf(task), selected: selectedTasks.includes(task.id), TaskLabel: () => task.label.padEnd(maxLength2, " ") }; }); } const filteredTasks = fuzzySort.go(searchText, tasks, { key: "label", limit: VISIBLE_TASKS_LIMIT }); const maxLength = Math.max(...filteredTasks.map((task) => task.obj.label.length)); return filteredTasks.map((task) => { return { ...task.obj, selected: selectedTasks.includes(task.obj.id), pointing: cursor === filteredTasks.indexOf(task), TaskLabel: () => /* @__PURE__ */ jsx2(HighlightedTaskLabel, { task, marginRight: maxLength - task.obj.label.length }) }; }); }, [cursor, searchText, selectedTasks, tasks]); } var HighlightedTaskLabel = (props) => { const { task, marginRight } = props; return /* @__PURE__ */ jsxs2(Fragment, { children: [ task.highlight((char, charIndex) => /* @__PURE__ */ jsx2(Text2, { color: HIGHLIGHT_COLOR, children: char }, charIndex)).map((part, i) => typeof part === "string" ? /* @__PURE__ */ jsx2(Text2, { children: part }, i) : /* @__PURE__ */ jsx2(React.Fragment, { children: part }, i)), " ".repeat(marginRight) ] }); }; // packages/nadle/src/core/views/input-handlers.ts var commandHandler = { canHandle: ({ context }) => context.isCommandMode, handle: ({ key, input, context }) => { const { exit, onSubmit, commandBuffer, setSearchText, setCommandBuffer, setIsCommandMode } = context; const exitCommandMode = () => { setIsCommandMode(false); setCommandBuffer(""); }; if (key.escape) { return exitCommandMode(); } if (key.return) { if (commandBuffer === "q") { exit(); onSubmit([]); } if (commandBuffer === "l") { setSearchText(""); } return exitCommandMode(); } setCommandBuffer((prev) => prev + input); } }; var semiColonHandler = { canHandle: ({ input }) => input === ":", handle: ({ context: { setIsCommandMode, setCommandBuffer } }) => { setIsCommandMode(true); setCommandBuffer(""); } }; var arrowsHandler = { canHandle: ({ key }) => key.upArrow || key.downArrow, handle: ({ key, context: { setCursor, searchingTasks } }) => { if (key.upArrow) { setCursor((cursor) => (cursor - 1 + searchingTasks.length) % searchingTasks.length); } if (key.downArrow) { setCursor((cursor) => (cursor + 1 + searchingTasks.length) % searchingTasks.length); } } }; var enterHandler = { canHandle: ({ key }) => key.return, handle: ({ context: { exit, onSubmit, selectedTasks } }) => { exit(); onSubmit(Array.from(selectedTasks)); } }; var escapeHandler = { canHandle: ({ key }) => key.escape, handle: ({ context: { exit } }) => { exit(); } }; var deleteHandler = { canHandle: ({ key }) => key.backspace || key.delete, handle: ({ context: { setSearchText } }) => { setSearchText((searchText) => searchText.slice(0, -1)); } }; var spaceHandler = { canHandle: ({ input }) => input === " ", handle: ({ context: { cursor, searchingTasks, setSelectedTasks } }) => { const { id } = searchingTasks[cursor]; setSelectedTasks((tasks) => tasks.includes(id) ? tasks.filter((task) => task !== id) : [...tasks, id]); } }; var fallbackHandler = { canHandle: () => true, handle: ({ input, context: { setSearchText } }) => { setSearchText((currentSearchText) => currentSearchText + input); } }; var inputHandlers = [ commandHandler, semiColonHandler, arrowsHandler, enterHandler, escapeHandler, deleteHandler, spaceHandler, fallbackHandler ]; // packages/nadle/src/core/views/tasks-selection.tsx import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime"; function renderTaskSelection(tasks) { return new Promise((resolve) => { render(/* @__PURE__ */ jsx3(TasksSelection, { onSubmit: resolve, tasks })); }); } var PRIMARY_COLOR = "cyan"; var SECONDARY_COLOR = "yellow"; var HIGHLIGHT_COLOR = "cyanBright"; var TasksSelection = ({ tasks, onSubmit }) => { const [cursor, setCursor] = useState(0); const [selectedTasks, setSelectedTasks] = useState([]); const [searchText, setSearchText] = useState(""); const [isCommandMode, setIsCommandMode] = useState(false); const [commandBuffer, setCommandBuffer] = useState(""); const searchingTasks = useSearching({ tasks, cursor, searchText, selectedTasks }); const { exit } = useApp(); const context = React2.useMemo(() => { return { exit, cursor, onSubmit, setCursor, isCommandMode, commandBuffer, setSearchText, selectedTasks, searchingTasks, setSelectedTasks, setCommandBuffer, setIsCommandMode }; }, [commandBuffer, cursor, exit, isCommandMode, onSubmit, searchingTasks, selectedTasks]); useInput((input, key) => { const handleParams = { key, input, context }; for (const { handle, canHandle } of inputHandlers) { if (canHandle(handleParams)) { handle(handleParams); return; } } }); return /* @__PURE__ */ jsx3( TasksSelectionView, { commandBuffer, isCommandMode, searchText, searchingTasks, selectedTasks, tasks } ); }; var TasksSelectionView = (props) => { const { tasks, searchText, selectedTasks, isCommandMode, commandBuffer, searchingTasks } = props; return /* @__PURE__ */ jsxs3(Box, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: PRIMARY_COLOR, children: [ /* @__PURE__ */ jsxs3(Text3, { color: PRIMARY_COLOR, children: [ "Select tasks to run (type to search): ", searchText ] }), /* @__PURE__ */ jsx3(Box, { flexDirection: "column", marginTop: 1, children: searchText && searchingTasks.length === 0 ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No matched tasks" }) : searchingTasks.map((task) => /* @__PURE__ */ jsx3(VisibleTask, { task }, task.label)) }), /* @__PURE__ */ jsx3(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs3(Text3, { color: PRIMARY_COLOR, children: [ "Selected tasks: ", selectedTasks.map((selectedTask) => tasks.find((task) => task.id === selectedTask)?.label ?? "").join(", ") ] }) }), /* @__PURE__ */ jsx3(Box, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "[\u2191\u2193]: Navigate [Space]: Toggle [Enter]: Run [:q]: Quit [:l]: Clear" }) }), isCommandMode && /* @__PURE__ */ jsx3(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs3(Text3, { color: PRIMARY_COLOR, children: [ "Command: :", commandBuffer ] }) }) ] }); }; // packages/nadle/src/core/handlers/execute-handler.ts var ExecuteHandler = class extends BaseHandler { constructor() { super(...arguments); this.name = "execute"; this.description = "Executes the specified tasks."; } canHandle() { return true; } async handle() { let chosenTasks = this.context.options.tasks.map(ResolvedTask.getId); if (chosenTasks.length === 0) { this.context.updateState((state) => ({ ...state, selectingTasks: true })); chosenTasks = await renderTaskSelection( this.context.taskRegistry.tasks.map(({ id, label, configResolver }) => { return { id, label, description: configResolver().description }; }) ); this.context.updateState((state) => ({ ...state, selectingTasks: false })); if (chosenTasks.length === 0) { this.context.logger.log(Messages.NoTasksFound()); return; } } const { passthroughArgs } = this.context.options; if (passthroughArgs.length > 0 && chosenTasks.length > 1) { this.context.logger.log(Messages.PassthroughArgsNotice([...passthroughArgs], chosenTasks)); } const scheduler = this.context.taskScheduler.init(chosenTasks); await this.context.eventEmitter.onTasksScheduled(scheduler.scheduledTask.map((taskId) => this.context.taskRegistry.getTaskById(taskId))); await new TaskPool(this.context, (taskId) => scheduler.getReadyTasks(taskId)).run(); } }; // packages/nadle/src/core/handlers/clean-cache-handler.ts import Fs from "fs/promises"; var CleanCacheHandler = class extends BaseHandler { constructor() { super(...arguments); this.name = "clean-cache"; this.description = "Cleans the cache directory."; } canHandle() { return this.context.options.cleanCache; } async handle() { try { this.context.logger.log(`Cleaning cache at ${this.context.options.cacheDir}...`); await Fs.rm(this.context.options.cacheDir, { force: true, recursive: true }); } catch (error) { this.context.logger.error(`Failed to clean cache at ${this.context.options.cacheDir}:`, error); throw error; } } }; // packages/nadle/src/core/handlers/show-config-handler.ts import { get } from "lodash-es"; // packages/nadle/src/core/utilities/stringify.ts function stringify(value) { return JSON.stringify(value, null, 2); } // packages/nadle/src/core/handlers/show-config-handler.ts var ShowConfigHandler = class extends BaseHandler { constructor() { super(...arguments); this.name = "show-config"; this.description = "Shows the current Nadle configuration."; } canHandle() { return this.context.options.showConfig; } handle() { const { configKey } = this.context.options; if (!configKey) { this.context.logger.log(stringify(this.context.options)); return; } this.context.logger.log(stringify(get(this.context.options, configKey))); } }; // packages/nadle/src/core/handlers/list-workspace-handler.ts import c3 from "tinyrainbow"; import { getAllWorkspaces, isRootWorkspaceId as isRootWorkspaceId2 } from "@nadle/project-resolver"; // packages/nadle/src/core/utilities/create-tree.ts var { VERTICAL, UP_RIGHT, HORIZONTAL, VERTICAL_RIGHT } = LINES; function createTree(node, getChildren, getLabel) { const lines = [getLabel(node)]; const children = getChildren(node); for (let childIndex = 0; childIndex < children.length; childIndex++) { for (const childLine of createTree(children[childIndex], getChildren, getLabel)) { if (childLine.startsWith(VERTICAL) || childLine.startsWith(UP_RIGHT) || childLine.startsWith(VERTICAL_RIGHT)) { lines.push(`${VERTICAL} ${childLine}`); } else { lines.push(`${childIndex === children.length - 1 ? UP_RIGHT : VERTICAL_RIGHT}${HORIZONTAL}${HORIZONTAL} ${childLine}`); } } } return lines; } // packages/nadle/src/core/utilities/string-builder.ts var StringBuilder = class { constructor(separator = " ") { this.separator = separator; this.items = []; } add(item) { if (item === false) { return this; } this.items.push(item); return this; } addIf(condition, item) { if (condition) { return this.add(item); } return this; } build() { if (this.items.length === 0) { return ""; } return this.items.join(this.separator); } }; // packages/nadle/src/core/handlers/list-workspace-handler.ts var ListWorkspacesHandler = class extends BaseHandler { constructor() { super(...arguments); this.name = "listWorkspaces"; this.description = "Lists all available workspaces."; } canHandle() { return this.context.options.listWorkspaces; } handle() { const workspaces = getAllWorkspaces(this.context.options.project); const parentWorkspaceMap = this.computeParentWorkspaceMap(workspaces); const childrenWorkspaceMap = Object.fromEntries( workspaces.map(({ id }) => [id, workspaces.filter((workspace) => parentWorkspaceMap[workspace.id]?.id === id)]) ); this.context.logger.log(c3.bold("Available workspaces:\n")); const tree = createTree( this.context.options.project.rootWorkspace, (workspace) => childrenWorkspaceMap[workspace.id], (workspace) => new StringBuilder().add(isRootWorkspaceId2(workspace.id) ? "Root workspace" : "Workspace").add(highlight(workspace.id)).addIf(workspace.label !== workspace.id && workspace.label !== "", c3.dim(`(alias: ${workspace.label})`)).build() ); this.context.logger.log(tree.join("\n")); } computeParentWorkspaceMap(workspaces) { const parentWorkspaceMap = {}; for (const childWorkspace of workspaces) { if (isRootWorkspaceId2(childWorkspace.id)) { parentWorkspaceMap[childWorkspace.id] = null; continue; } let currentParent = null; for (const parent of workspaces) { if (parent.id === childWorkspace.id || !this.isParentWorkspace(parent, childWorkspace)) { continue; } if (!currentParent || parent.relativePath.length > currentParent.relativePath.length) { currentParent = parent; } } parentWorkspaceMap[childWorkspace.id] = currentParent; } return parentWorkspaceMap; } isParentWorkspace(parentWorkspace, childWorkspace) { if (isRootWorkspaceId2(parentWorkspace.id)) { return true; } if (parentWorkspace.id === childWorkspace.id) { return false; } return childWorkspace.relativePath.startsWith(`${parentWorkspace.relativePath}/`); } }; // packages/nadle/src/core/handlers/index.ts var Handlers = [ ListHandler, ListWorkspacesHandler, CleanCacheHandler, DryRunHandler, ShowConfigHandler, ExecuteHandler ]; // packages/nadle/src/core/models/event-emitter.ts var EventEmitter = class { constructor(listeners) { this.listeners = listeners; } addListener(listener) { this.listeners.push(listener); } async emit(event, ...args) { for (const listener of this.listeners) { await listener[event]?.(...args); } } async onInitialize() { await this.emit("onInitialize"); return this; } async onExecutionFailed(error) { await this.emit("onExecutionFailed", error); } async onExecutionFinish() { await this.emit("onExecutionFinish"); } async onExecutionStart() { await this.emit("onExecutionStart"); } async onTaskCanceled(task) { await this.emit("onTaskCanceled", task); } async onTaskFailed(task) { await this.emit("onTaskFailed", task); } async onTaskFinish(task) { await this.emit("onTaskFinish", task); } async onTaskRestoreFromCache(task) { await this.emit("onTaskRestoreFromCache", task); } async onTaskStart(task, threadId2) { await this.emit("onTaskStart", task, threadId2); } async onTaskUpToDate(task) { await this.emit("onTaskUpToDate", task); } async onTasksScheduled(tasks) { await this.emit("onTasksScheduled", tasks); } }; // packages/nadle/src/core/reporting/reporter.ts import Url from "url"; import c5 from "tinyrainbow"; import { isCI, isTest } from "std-env"; // packages/nadle/src/core/reporting/renderers/footer-renderer.ts import Util from "util"; // packages/nadle/src/core/utilities/file-logger.ts var logItems = []; 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 }); } } }; // packages/nadle/src/core/reporting/renderers/footer-renderer.ts var DEFAULT_RENDER_INTERVAL_MS = 100; 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 l = new FileLogger("WindowRenderer"); var FooterRenderer = 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() { l.log("start"); this.finished = false; this.renderInterval = setInterval(() => this.schedule(), this.options.interval).unref(); } stop() { l.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() { l.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") { l.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()); } l.log("render", { padding, rowCount, windowHeight: win, col: this.options.logger.getColumns() }); this.write(SYNC_START); this.clearWindow(); if (message) { this.write(message, type); } if (padding > 0) { this.write("\n".repeat(padding)); } this.write(windowContent.join("\n")); this.write(SYNC_END); this.windowHeight = rowCount + Math.max(0, padding); } clearWindow() { l.log("clearWindow", `this.windowHeight = ${this.windowHeight}`); if (this.windowHeight === 0) { return; } this.write(CLEAR_LINE); for (let i = 1; i < this.windowHeight; i++) { this.write(`${MOVE_CURSOR_ONE_ROW_UP}${CLEAR_LINE}`); } 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") { l.log("write", { message }); this.streams[type](message); } }; function getRenderedRowCount(rows, columns) { let count = 0; for (const row of rows) { const text = Util.stripVTControlCharacters(row); count += Math.max(1, Math.ceil(text.length / columns)); } return count; } // packages/nadle/src/core/reporting/profiling-summary.tsx import c4 from "tinyrainbow"; var TASK_LIMIT = 5; function renderProfilingSummary({ tasks, totalDuration }) { if (tasks.length === 0) { return ""; } const headerRow = { label: "Task", duration: "Duration", percentage: "Percentage" }; const bodyRows = []; for (const task of tasks.sort((task1, task2) => task2.duration - task1.duration).slice(0, TASK_LIMIT)) { bodyRows.push({ label: task.label, duration: formatTime(task.duration), percentage: (task.duration / totalDuration * 100).toFixed(1) + "%" }); } const columnWidths = { label: 0, duration: 0, percentage: 0 }; for (const row of [headerRow, ...bodyRows]) { columnWidths.label = Math.max(columnWidths.label, row.label.length); columnWidths.duration = Math.max(columnWidths.duration, row.duration.length); columnWidths.percentage = Math.max(columnWidths.percentage, row.percentage.length); } const widths = Object.values(columnWidths).map((width) => width + 2); return [ "", c4.bold(c4.green("Profiling Summary")), renderBorder("top", widths), renderRow([headerRow.label, headerRow.duration, headerRow.percentage], widths, ["left", "right", "right"]), renderBorder("join", widths), ...bodyRows.map((row) => renderRow([row.label, row.duration, row.percentage], widths, ["left", "right", "right"])), renderBorder("bottom", widths) ].join("\n"); } var toPad = (alignment) => alignment === "left" ? "padEnd" : "padStart"; var renderRow = (cells, lengths, alignments) => CHARS.bodyLeft + cells.map((cell, index) => ` ${cell[toPad(alignments[index])](lengths[index] - 2, " ")} `).join(CHARS.bodyJoin) + CHARS.bodyRight; var renderBorder = (type, lengths) => CHARS[`${type}Left`] + lengths.map((length) => CHARS[`${type}Body`].repeat(length)).join(CHARS[`${type}Join`]) + CHARS[`${type}Right`]; var CHARS = { topBody: "\u2500", topJoin: "\u252C", topLeft: "\u250C", topRight: "\u2510", bottomBody: "\u2500", bottomJoin: "\u2534", bottomLeft: "\u2514", bottomRight: "\u2518", bodyLeft: "\u2502", bodyJoin: "\u2502", bodyRight: "\u2502", headerJoin: "\u252C", joinBody: "\u2500", joinLeft: "\u251C", joinJoin: "\u253C", joinRight: "\u2524" }; // packages/nadle/src/core/reporting/renderers/default-renderer.ts var DefaultRenderer = class { start() { noop(); } finish() { noop(); } schedule() { noop(); } }; // packages/nadle/src/core/reporting/reporter.ts var DefaultReporter = class { constructor(context) { this.context = context; this.renderer = new DefaultRenderer(); this.tracker = this.context.executionTracker; } onInitialize() { this.renderer = this.context.options.footer ? new FooterRenderer({ logger: this.context.logger, getWindow: () => this.createFooter() }) : new DefaultRenderer(); return this; } printLabel(label) { return c5.dim(label.padEnd(11, " ")); } createFooter() { const footer = [""]; if (this.context.options.tasks.length === 0 && this.context.state.selectingTasks) { return footer; } const doneTask = this.stats["finished" /* Finished */] + this.stats["from-cache" /* FromCache */] + this.stats["up-to-date" /* UpToDate */]; const stats = [ c5.cyanBright(`${this.stats["scheduled" /* Scheduled */] - this.stats["running" /* Running */] - this.stats["failed" /* Failed */] - doneTask} pending`), c5.yellow(`${this.stats["running" /* Running */]} running`), c5.green(`${this.stats["finished" /* Finished */]} done`) ].join(` ${c5.gray(VERTICAL_BAR)} `); footer.push([this.printLabel("Tasks"), stats, c5.dim(`(${this.stats["scheduled" /* Scheduled */]} scheduled)`)].join(" ")); footer.push([this.printLabel("Duration"), formatTime(this.duration)].join(" ")); footer.push(...this.printRunningTasks()); footer.push(""); return footer; } printRunningTasks() { const lines = Array.from({ length: this.context.options.maxWorkers }, () => ` ${c5.yellow(">")} ${c5.dim("IDLE")}`); let maxThreadId = 0; for (const { label, threadId: threadId2 } of this.tracker.getTaskStateByStatus("running" /* Running */)) { if (threadId2 === null) { throw new Error(`Task ${label} is running but has no worker ID assigned.`); } lines[threadId2 - 1] = ` ${c5.yellow(">")} ${c5.bold(label)}`; maxThreadId = Math.max(maxThreadId, threadId2); } return lines.slice(0, maxThreadId); } async onTaskStart(task) { if (!task.empty) { this.context.logger.log(`${c5.yellow(">")} Task ${c5.bold(task.label)} ${c5.yellow("STARTED")} `); } this.renderer.schedule(); } async onTaskFinish(task) { const prefix = task.empty ? "" : "\n"; this.context.logger.log( `${prefix}${c5.green(CHECK)} Task ${c5.bold(task.label)} ${c5.green("DONE")} ${c5.dim(formatTime(this.tracker.getTaskState(task.id).duration ?? 0))}` ); this.renderer.schedule(); } async onTaskUpToDate(task) { this.context.logger.log(` ${c5.green(DASH)} Task ${c5.bold(task.label)} ${c5.green("UP-TO-DATE")}`); this.renderer.schedule(); } async onTaskRestoreFromCache(task) { this.context.logger.log(` ${c5.green(CURVE_ARROW)} Task ${c5.bold(task.label)} ${c5.green("FROM-CACHE")}`); this.renderer.schedule(); } async onTaskFailed(task) { this.context.logger.log( ` ${c5.red(CROSS)} Task ${c5.bold(task.label)} ${c5.red("FAILED")} ${formatTime(this.tracker.getTaskState(task.id).duration ?? 0)}` ); this.renderer.schedule(); } async onTaskCanceled(task) { this.context.logger.log(` ${c5.yellow(CROSS)} Task ${c5.bold(task.label)} ${c5.yellow("CANCELED")}`); this.renderer.schedule(); } async onTasksScheduled(tasks) { this.context.logger.info(`Scheduled tasks: ${tasks.map((task) => task.id).join(", ")}`); this.renderer.schedule(); } onExecutionStart() { this.renderer.start(); const { project, minWorkers, maxWorkers } = this.context.options; const workspaceConfigFileCount = project.workspaces.flatMap((workspace) => workspace.configFilePath ?? []).length; if (!this.context.options.showConfig) { this.context.logger.log(c5.bold(c5.cyan(`\u25B6 Welcome to Nadle v${Nadle.version}!`))); this.context.logger.log(`Using Nadle from ${Url.fileURLToPath(import.meta.resolve("nadle"))}`); this.context.logger.log( `Loaded configuration from ${project.rootWorkspace.configFilePath}${workspaceConfigFileCount > 0 ? ` and ${workspaceConfigFileCount} other(s) files` : ""} ` ); } this.context.logger.info( `Using ${minWorkers === maxWorkers ? minWorkers : `${minWorkers}\u2013${maxWorkers}`} worker${maxWorkers > 1 ? "s" : ""} for task execution` ); this.context.logger.info(`Project directory: ${project.rootWorkspace.absolutePath}`); this.context.logger.info("Resolved options:", stringify(this.context.options)); this.context.logger.info("Detected environments:", { CI: isCI, TEST: isTest }); this.printResolvedTasks(); this.context.logger.info("Execution started"); } printResolvedTasks() { const resolvedTasks = [...this.context.options.tasks, ...this.context.options.excludedTasks].filter(({ corrected }) => corrected); if (resolvedTasks.length === 0) { return; } const maxOriginTaskLength = Math.max(...resolvedTasks.map(({ rawInput }) => rawInput.length)); const message = [ `Resolved tasks:`, ...resolvedTasks.flatMap(({ taskId, rawInput, corrected }) => { if (!corrected) { return []; } return `${" ".repeat(4)}${highlight(rawInput.padEnd(maxOriginTaskLength, " "))} ${RIGHT_ARROW} ${c5.green(c5.bold(taskId))}`; }) ].join("\n"); this.context.logger.log(message + "\n"); } async onExecutionFinish() { this.renderer.finish(); this.context.logger.info("Execution finished"); if (this.context.options.showConfig) { return; } if (this.context.options.summary) { this.context.logger.log( renderProfilingSummary({ totalDuration: this.duration, tasks: this.context.taskRegistry.tasks.flatMap((task) => { const { status, duration } = this.tracker.getTaskState(task.id); if (status !== "finished" /* Finished */) { return []; } return { label: task.label, duration: duration ?? 0 }; }) }) ); } const print = (count) => `${c5.bold(count)} task${count > 1 ? "s" : ""}`; this.context.logger.log(` ${c5.bold(c5.green("RUN SUCCESSFUL"))} in ${c5.bold(formatTime(this.duration))}`); this.context.logger.log( new StringBuilder(", ").add(`${print(this.stats["finished" /* Finished */])} executed`).add(this.stats["up-to-date" /* UpToDate */] > 0 && `${print(this.stats["up-to-date" /* UpToDate */])} up-to-date`).add(this.stats["from-cache" /* FromCache */] > 0 && `${print(this.stats["from-cache" /* FromCache */])} restored from cache`).build() ); } async onExecutionFailed(error) { this.renderer.finish(); this.context.logger.info("Execution failed"); const finishedTasks = `${c5.bold(this.stats["finished" /* Finished */])} task${this.stats["finished" /* Finished */] > 1 ? "s" : ""}`; const failedTasks = `${c5.bold(this.stats["failed" /* Failed */])} task${this.stats["failed" /* Failed */] > 1 ? "s" : ""}`; this.context.logger.log( ` ${c5.bold(c5.red("RUN FAILED"))} in ${c5.bold(formatTime(this.duration))} ${c5.dim(`(${finishedTasks} executed, ${failedTasks} failed)`)}` ); if (!this.context.options.stacktrace) { this.context.logger.log( ` For more details, re-run the command with the ${c5.yellow("--stacktrace")} option to display the full error and help identify the root cause.` ); } else { this.context.logger.error(error instanceof Error ? error.stack ?? error.message : String(error)); } } get stats() { return this.tracker.taskStats; } get duration() { return this.tracker.duration; } }; // packages/nadle/src/core/utilities/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; } }; // packages/nadle/src/core/engine/implicit-dependency-resolver.ts function resolveImplicitDependencies(taskName, workspaceId, deps) { const implicitDeps = /* @__PURE__ */ new Set(); const upstreamWorkspaceIds = deps.getWorkspaceDependencies(workspaceId); for (const upstreamWorkspaceId of upstreamWorkspaceIds) { const sameNameTasks = deps.getTasksByName(taskName); const upstreamTask = sameNameTasks.find((task) => task.workspaceId === upstreamWorkspaceId); if (!upstreamTask || deps.excludedTaskIds.has(upstreamTask.id)) { continue; } deps.logger.debug({ tag: "Scheduler" }, `Implicit dependency: ${workspaceId}:${taskName} \u2192 ${upstreamTask.id}`); implicitDeps.add(upstreamTask.id); } return implicitDeps; } // packages/nadle/src/core/engine/task-scheduler.ts var TaskScheduler = class { constructor(deps) { this.deps = deps; this.dependentsGraph = new EnsureMap(() => /* @__PURE__ */ new Set()); this.dependencyGraph = new EnsureMap(() => /* @__PURE__ */ new Set()); this.transitiveDependencyGraph = new EnsureMap(() => /* @__PURE__ */ new Set()); this.indegree = new EnsureMap(() => 0); this.readyTasks = /* @__PURE__ */ new Set(); this.implicitEdges = /* @__PURE__ */ new Set(); this.rootAggregationDeps = /* @__PURE__ */ new Map(); this.taskIds = []; this.excludedTaskIds = /* @__PURE__ */ new Set(); this.mainTaskId = void 0; } init(taskIds = this.deps.options.tasks.map(({ taskId }) => taskId)) { this.taskIds = this.expandWorkspaceTasks(taskIds); this.excludedTaskIds = new Set(this.deps.options.excludedTasks.map(ResolvedTask.getId)); this.taskIds.forEach((taskId) => this.analyze(taskId)); this.taskIds.forEach((taskId) => this.detectCycle(taskId, [taskId])); this.deps.logger.debug({ tag: "Scheduler" }, `transitiveDependencyGraph`, this.transitiveDependencyGraph); this.deps.logger.debug({ tag: "Scheduler" }, `dependencyGraph`, this.dependencyGraph); if (!this.deps.options.parallel) { this.mainTaskId = this.taskIds[0]; } return this; } expandWorkspaceTasks(taskIds) { const expandedTaskIds = []; for (const taskId of taskIds) { expandedTaskIds.push(taskId); const { name, workspaceId } = this.deps.getTaskById(taskId); if (!this.deps.isRootWorkspace(workspaceId)) { continue; } const childTaskIds = /* @__PURE__ */ new Set(); for (const sameNameTask of this.deps.getTasksByName(name)) { if (!taskIds.includes(sameNameTask.id)) { expandedTaskIds.push(sameNameTask.id); childTaskIds.add(sameNameTask.id); } } if (childTaskIds.size > 0 && this.deps.options.implicitDependencies) { this.rootAggregationDeps.set(taskId, childTaskIds); } } return expandedTaskIds; } detectCycle(taskId, paths) { for (const dependency of this.dependencyGraph.get(taskId)) { const startIndex = paths.indexOf(dependency); if (startIndex !== -1) { const message = Messages.CycleDetected([...paths.slice(startIndex), dependency].map(highlight).join(` ${RIGHT_ARROW} `)); this.deps.logger.error(message); throw new CyclicDependencyError(message); } this.detectCycle(dependency, [...paths, dependency]); } } analyze(taskId) { if (this.dependencyGraph.has(taskId)) { return; } const { workspaceId, configResolver } = this.deps.getTaskById(taskId); const dependencies = new Set( MaybeArray.toArray(configResolver().dependsOn ?? []).map((input) => this.deps.parseTaskRef(input, workspaceId)).filter((id) => !this.excludedTaskIds.has(id)) ); if (this.deps.options.implicitDependencies) { this.addImplicitDeps(taskId, dependencies); } this.dependencyGraph.set(taskId, dependencies); this.indegree.set(taskId, dependencies.size); this.transitiveDependencyGraph.set(taskId, new Set(dependencies)); for (const dependency of dependencies) { this.dependentsGraph.update(dependency, (s) => s.add(taskId)); this.analyze(dependency); this.transitiveDependencyGraph.update(taskId, (current) => { this.transitiveDependencyGraph.get(dependency).forEach((d) => current.add(d)); return current; }); } } addImplicitDeps(taskId, dependencies) { const { name, workspaceId } = this.deps.getTaskById(taskId); const resolverDeps = { logger: this.deps.logger, excludedTaskIds: this.excludedTaskIds, getTasksByName: (n) => this.deps.getTasksByName(n), getWorkspaceDependencies: (id) => this.deps.getWorkspaceDependencies(id) }; for (const dep of resolveImplicitDependencies(name, workspaceId, resolverDeps)) { if (!dependencies.has(dep)) { this.implicitEdges.add(`${dep}->${taskId}`); } dependencies.add(dep); } for (const childId of this.rootAggregationDeps.get(taskId) ?? []) { if (!this.excludedTaskIds.has(childId)) { if (!dependencies.has(childId)) { this.implicitEdges.add(`${childId}->${taskId}`); } dependencies.add(childId); } } } get scheduledTask() { return Array.from(this.dependencyGraph.keys()); } getDirectDependencies(taskId) { return this.dependencyGraph.get(taskId); } getImplicitDeps(taskId) { return [...this.dependencyGraph.get(taskId)].filter((dep) => this.implicitEdges.has(`${dep}->${taskId}`)); } getIndegreeEntries() { if (!this.mainTaskId) { return this.indegree.entries(); } const rootId = this.mainTaskId; return new Map( Array.from(this.indegree.entries()).filter(([taskId]) => taskId === rootId || this.transitiveDependencyGraph.get(rootId)?.has(taskId)) ); } isBelongToRootTaskTree(taskId) { if (!this.mainTaskId || taskId === this.mainTaskId) { return true; } return this.transitiveDependencyGraph.get(this.mainTaskId).has(taskId); } getReadyTasks(doneTaskId) { this.deps.logger.debug({ tag: "Scheduler" }, `runningRoot = ${this.mainTaskId}, doneTaskId = ${doneTaskId}`); if (doneTaskId === void 0) { return this.getInitialReadyTasks(); } const nextReadyTasks = /* @__PURE__ */ new Set(); for (const dependentTask of this.dependentsGraph.get(doneTaskId)) { if (this.readyTasks.has(dependentTask)) { continue; } let indegree = this.indegree.get(dependentTask); if (indegree === 0) { throw new Error(`Incorrect state. Expect ${dependentTask} to have indegree > 0`); } this.indegree.set(dependentTask, --indegree); if (indegree === 0 && this.isBelongToRootTaskTree(dependentTask)) { nextReadyTasks.add(dependentTask); this.readyTasks.add(dependentTask); } } if (doneTaskId === this.mainTaskId) { if (!this.moveToNextMainTask()) { return /* @__PURE__ */ new Set(); } return this.getReadyTasks(); } this.deps.logger.debug({ tag: "Scheduler" }, `Next tasks = ${Array.from(nextReadyTasks).join(",")}`); return nextReadyTasks; } getInitialReadyTasks() { const nextReadyTasks = /* @__PURE__ */ new Set(); for (const [taskId, indegree] of this.getIndegreeEntries()) { this.deps.logger.debug({ tag: "Scheduler" }, `taskId = ${taskId}, indegree = ${indegree}`); if (indegree === 0 && !this.readyTasks.has(taskId)) { nextReadyTasks.add(taskId); this.readyTasks.add(taskId); } } this.deps.logger.debug({ tag: "Scheduler" }, `Next tasks = ${Array.from(nextReadyTasks).join(",")}`); return nextReadyTasks; } moveToNextMainTask() { const nextIndex = this.mainTaskId !== void 0 ? this.taskIds.indexOf(this.mainTaskId) + 1 : -1; this.mainTaskId = nextIndex >= 0 ? this.taskIds[nextIndex] : void 0; return this.mainTaskId; } getExecutionPlan(taskId) { const readyTaskIds = Array.from(this.getReadyTasks(taskId)); for (const readyTaskId of readyTaskIds) { readyTaskIds.push(...this.getExecutionPlan(readyTaskId)); } return readyTaskIds; } }; // packages/nadle/src/core/reporting/agent-reporter.ts var AgentReporter = class { constructor(context) { this.context = context; this.tracker = this.context.executionTracker;