builderman 1.0.8 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,55 @@
1
1
  # **builderman**
2
2
 
3
- #### _An awesome new package._
3
+ #### _A simple task runner for building and developing projects._
4
4
 
5
5
  <br />
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install builderman
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { task, pipeline } from "builderman"
17
+
18
+ const task1 = task({
19
+ name: "lib:build",
20
+ commands: {
21
+ dev: "tsc --watch",
22
+ build: "tsc",
23
+ },
24
+ cwd: "packages/lib",
25
+ isReady: (stdout) => {
26
+ // mark this this task as ready when the process is watching for file changes
27
+ return stdout.includes("Watching for file changes.")
28
+ },
29
+ })
30
+
31
+ const task2 = task({
32
+ name: "consumer:dev",
33
+ commands: {
34
+ dev: "npm run dev",
35
+ build: "npm run build",
36
+ },
37
+ cwd: "packages/consumer",
38
+ dependencies: [task1],
39
+ })
40
+
41
+ await pipeline([task1, task2]).run({
42
+ onTaskError: (taskName, error) => {
43
+ console.error(`[${taskName}] Error: ${error.message}`)
44
+ },
45
+ onTaskComplete: (taskName) => {
46
+ console.log(`[${taskName}] Complete!`)
47
+ },
48
+ onPipelineComplete: () => {
49
+ console.log("All tasks complete! 🎉")
50
+ },
51
+ onPipelineError: (error) => {
52
+ console.error(`Pipeline error: ${error.message}`)
53
+ },
54
+ })
55
+ ```
package/dist/pipeline.js CHANGED
@@ -1,173 +1,182 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { EventEmitter } from "node:events";
3
3
  import * as path from "node:path";
4
+ import * as fs from "node:fs";
4
5
  import { $TASK_INTERNAL } from "./constants.js";
5
6
  /**
6
7
  * Creates a pipeline that manages task execution with dependency-based coordination.
7
8
  */
8
9
  export function pipeline(tasks) {
9
10
  return {
10
- async run() {
11
- const eventEmitter = new EventEmitter();
12
- const runningTasks = new Map();
13
- const completedTasks = new Set();
14
- const readyTasks = new Set();
15
- // Determine which command to use based on NODE_ENV
16
- const isProduction = process.env.NODE_ENV === "production";
17
- const getCommand = (task) => {
18
- return isProduction
19
- ? task[$TASK_INTERNAL].commands.build
20
- : task[$TASK_INTERNAL].commands.dev;
21
- };
22
- // Function to check if a task's dependencies are satisfied
23
- const canStart = async (task) => {
24
- if (runningTasks.has(task.name) || completedTasks.has(task.name)) {
25
- return false;
26
- }
27
- const { dependsOn: dependencies } = task[$TASK_INTERNAL];
28
- if (!dependencies || dependencies.length === 0) {
29
- return true;
30
- }
31
- // Wait for all dependencies
32
- for (const dep of dependencies) {
33
- if (typeof dep === "function") {
34
- await dep();
11
+ run(config) {
12
+ return new Promise((resolvePipeline, rejectPipeline) => {
13
+ let hasFailed = false;
14
+ // Function to fail the pipeline and kill all running tasks
15
+ const failPipeline = (error) => {
16
+ if (hasFailed)
17
+ return;
18
+ hasFailed = true;
19
+ // Kill all running tasks
20
+ for (const child of runningTasks.values()) {
21
+ try {
22
+ child.kill("SIGTERM");
23
+ }
24
+ catch (e) {
25
+ // Ignore errors when killing
26
+ }
35
27
  }
36
- else {
37
- await dep;
28
+ rejectPipeline(error);
29
+ };
30
+ const eventEmitter = new EventEmitter();
31
+ const runningTasks = new Map();
32
+ const completedTasks = new Set();
33
+ const readyTasks = new Set();
34
+ const isProduction = process.env.NODE_ENV === "production";
35
+ const getCommand = (task) => {
36
+ return isProduction
37
+ ? task[$TASK_INTERNAL].commands.build
38
+ : task[$TASK_INTERNAL].commands.dev;
39
+ };
40
+ const canStart = async (task) => {
41
+ if (runningTasks.has(task.name) || completedTasks.has(task.name)) {
42
+ return false;
38
43
  }
39
- }
40
- return true;
41
- };
42
- // Function to start a task
43
- const startTask = async (task) => {
44
- if (runningTasks.has(task.name)) {
45
- return;
46
- }
47
- const command = getCommand(task);
48
- const { cwd, readyOn, markComplete, markReady } = task[$TASK_INTERNAL];
49
- // Ensure node_modules/.bin is in PATH for local dependencies
50
- const taskCwd = path.isAbsolute(cwd)
51
- ? cwd
52
- : path.resolve(process.cwd(), cwd);
53
- const localBinPath = path.join(taskCwd, "node_modules", ".bin");
54
- // Build PATH with local node_modules/.bin first
55
- const existingPath = process.env.PATH || process.env.Path || "";
56
- const pathSeparator = process.platform === "win32" ? ";" : ":";
57
- const binPaths = [localBinPath];
58
- const rootBinPath = path.join(process.cwd(), "node_modules", ".bin");
59
- if (rootBinPath !== localBinPath) {
60
- binPaths.push(rootBinPath);
61
- }
62
- if (existingPath) {
63
- binPaths.push(existingPath);
64
- }
65
- const newPath = binPaths.join(pathSeparator);
66
- const env = {
67
- ...process.env,
68
- PATH: newPath,
69
- Path: newPath,
44
+ const { dependencies } = task[$TASK_INTERNAL];
45
+ if (!dependencies || dependencies.length === 0) {
46
+ return true;
47
+ }
48
+ await Promise.all(dependencies.map((task) => task[$TASK_INTERNAL].readyPromise));
49
+ return true;
70
50
  };
71
- const child = spawn(command, {
72
- cwd,
73
- stdio: ["inherit", "pipe", "pipe"],
74
- shell: true,
75
- env,
76
- });
77
- // Handle spawn errors
78
- child.on("error", (error) => {
79
- console.error(`[${task.name}] Failed to start:`, error.message);
80
- runningTasks.delete(task.name);
81
- completedTasks.add(task.name);
82
- markComplete();
83
- process.exitCode = 1;
84
- eventEmitter.emit("taskCompleted", task.name);
85
- });
86
- runningTasks.set(task.name, child);
87
- // If task doesn't have readyOn, mark as ready immediately
88
- if (!readyOn) {
89
- readyTasks.add(task.name);
90
- markReady();
91
- eventEmitter.emit("taskReady", task.name);
92
- }
93
- // Monitor stdout for readiness
94
- let stdoutBuffer = "";
95
- let allOutput = "";
96
- child.stdout?.on("data", (data) => {
97
- const chunk = data.toString();
98
- allOutput += chunk;
99
- stdoutBuffer += chunk;
100
- const lines = stdoutBuffer.split("\n");
101
- stdoutBuffer = lines.pop() || "";
102
- for (const line of lines) {
103
- // Check if task is ready based on readyOn callback
104
- if (readyOn && !readyTasks.has(task.name)) {
105
- if (readyOn(allOutput)) {
106
- readyTasks.add(task.name);
107
- markReady();
108
- eventEmitter.emit("taskReady", task.name);
109
- }
110
- }
111
- // Forward stdout to parent
112
- process.stdout.write(line + "\n");
51
+ const startTask = (task) => {
52
+ if (runningTasks.has(task.name)) {
53
+ return;
113
54
  }
114
- });
115
- // Forward any remaining buffer on end
116
- child.stdout?.on("end", () => {
117
- if (stdoutBuffer) {
118
- process.stdout.write(stdoutBuffer);
55
+ const command = getCommand(task);
56
+ const { cwd, isReady: getIsReady, markComplete, markReady, } = task[$TASK_INTERNAL];
57
+ // Ensure node_modules/.bin is in PATH for local dependencies
58
+ const taskCwd = path.isAbsolute(cwd)
59
+ ? cwd
60
+ : path.resolve(process.cwd(), cwd);
61
+ const localBinPath = path.join(taskCwd, "node_modules", ".bin");
62
+ // Build PATH with local node_modules/.bin first
63
+ const existingPath = process.env.PATH || process.env.Path || "";
64
+ const pathSeparator = process.platform === "win32" ? ";" : ":";
65
+ const binPaths = [localBinPath];
66
+ const rootBinPath = path.join(process.cwd(), "node_modules", ".bin");
67
+ if (rootBinPath !== localBinPath) {
68
+ binPaths.push(rootBinPath);
119
69
  }
120
- });
121
- // Forward stderr
122
- child.stderr?.on("data", (data) => {
123
- process.stderr.write(data);
124
- });
125
- // Handle task completion
126
- child.on("exit", (code) => {
127
- runningTasks.delete(task.name);
128
- completedTasks.add(task.name);
129
- markComplete();
130
- if (code !== 0) {
131
- process.exitCode = code || 1;
70
+ if (existingPath) {
71
+ binPaths.push(existingPath);
132
72
  }
133
- eventEmitter.emit("taskCompleted", task.name);
134
- });
135
- };
136
- // Function to try starting tasks when dependencies are ready
137
- const tryStartTasks = async () => {
138
- for (const task of tasks) {
139
- if (await canStart(task)) {
140
- await startTask(task);
73
+ const newPath = binPaths.join(pathSeparator);
74
+ const env = {
75
+ ...process.env,
76
+ PATH: newPath,
77
+ Path: newPath,
78
+ };
79
+ // Validate that the cwd exists
80
+ if (!fs.existsSync(taskCwd)) {
81
+ const error = new Error(`[${task.name}] Working directory does not exist: ${taskCwd}`);
82
+ config.onTaskError?.(task.name, error);
83
+ failPipeline(error);
84
+ return;
141
85
  }
142
- }
143
- };
144
- // Listen for task readiness and completion to start dependent tasks
145
- eventEmitter.on("taskReady", tryStartTasks);
146
- eventEmitter.on("taskCompleted", tryStartTasks);
147
- // Start tasks that don't have dependencies
148
- await tryStartTasks();
149
- // Wait for all tasks to complete
150
- return new Promise((resolve, reject) => {
151
- const checkCompletion = () => {
152
- if (runningTasks.size === 0) {
153
- resolve();
86
+ // Use the resolved absolute path for cwd
87
+ const child = spawn(command, {
88
+ cwd: taskCwd,
89
+ stdio: ["inherit", "pipe", "pipe"],
90
+ shell: true,
91
+ env,
92
+ });
93
+ // Handle spawn errors
94
+ child.on("error", (error) => {
95
+ const errorMsg = `[${task.name}] Failed to start: ${error.message}\n Command: ${command}\n CWD: ${taskCwd}`;
96
+ const e = new Error(errorMsg);
97
+ config.onTaskError?.(task.name, e);
98
+ failPipeline(e);
99
+ });
100
+ runningTasks.set(task.name, child);
101
+ // If task doesn't have getIsReady, mark as ready immediately
102
+ if (!getIsReady) {
103
+ readyTasks.add(task.name);
104
+ markReady();
105
+ eventEmitter.emit("taskReady", task.name);
154
106
  }
107
+ // Monitor stdout for readiness
108
+ let stdoutBuffer = "";
109
+ let allOutput = "";
110
+ child.stdout?.on("data", (data) => {
111
+ const chunk = data.toString();
112
+ allOutput += chunk;
113
+ stdoutBuffer += chunk;
114
+ const lines = stdoutBuffer.split("\n");
115
+ stdoutBuffer = lines.pop() || "";
116
+ for (const line of lines) {
117
+ // Check if task is ready based on readyOn callback
118
+ if (getIsReady && !readyTasks.has(task.name)) {
119
+ if (getIsReady(allOutput)) {
120
+ readyTasks.add(task.name);
121
+ markReady();
122
+ eventEmitter.emit("taskReady", task.name);
123
+ }
124
+ }
125
+ // Forward stdout to parent
126
+ process.stdout.write(line + "\n");
127
+ }
128
+ });
129
+ // Forward any remaining buffer on end
130
+ child.stdout?.on("end", () => {
131
+ if (stdoutBuffer) {
132
+ process.stdout.write(stdoutBuffer);
133
+ }
134
+ });
135
+ // Forward stderr
136
+ child.stderr?.on("data", (data) => {
137
+ process.stderr.write(data);
138
+ });
139
+ // Handle task completion
140
+ child.on("exit", (code) => {
141
+ runningTasks.delete(task.name);
142
+ completedTasks.add(task.name);
143
+ if (code !== 0) {
144
+ const error = new Error(`[${task.name}] Task failed with exit code ${code || 1}`);
145
+ config.onTaskError?.(task.name, error);
146
+ failPipeline(error);
147
+ }
148
+ else {
149
+ markComplete();
150
+ eventEmitter.emit("taskCompleted", task.name);
151
+ config.onTaskComplete?.(task.name);
152
+ }
153
+ });
155
154
  };
156
- eventEmitter.on("taskCompleted", checkCompletion);
157
- checkCompletion();
158
- // Handle process termination
159
- process.on("SIGINT", () => {
160
- for (const child of runningTasks.values()) {
161
- child.kill("SIGINT");
155
+ const tryStartTasks = async () => {
156
+ for (const task of tasks) {
157
+ if (await canStart(task)) {
158
+ startTask(task);
159
+ }
162
160
  }
163
- reject(new Error("Process interrupted"));
164
- });
165
- process.on("SIGTERM", () => {
166
- for (const child of runningTasks.values()) {
167
- child.kill("SIGTERM");
161
+ };
162
+ // Function to check completion (only resolve if no failures)
163
+ const checkCompletion = () => {
164
+ if (completedTasks.size === tasks.length && !hasFailed) {
165
+ config.onPipelineComplete?.();
166
+ resolvePipeline();
168
167
  }
169
- reject(new Error("Process terminated"));
168
+ };
169
+ ["SIGINT", "SIGBREAK", "SIGTERM", "SIGQUIT"].forEach((signal) => {
170
+ process.once(signal, () => {
171
+ const e = new Error(`Received ${signal} signal during pipeline execution`);
172
+ config.onPipelineError?.(e);
173
+ failPipeline(e);
174
+ });
170
175
  });
176
+ eventEmitter.on("taskReady", tryStartTasks);
177
+ eventEmitter.on("taskCompleted", tryStartTasks);
178
+ eventEmitter.on("taskCompleted", checkCompletion);
179
+ tryStartTasks().catch(failPipeline);
171
180
  });
172
181
  },
173
182
  };
package/dist/task.js CHANGED
@@ -11,12 +11,10 @@ export function task(config) {
11
11
  let isComplete = false;
12
12
  return {
13
13
  name: config.name,
14
- readyOrComplete() {
15
- return readyPromise;
16
- },
17
14
  [$TASK_INTERNAL]: {
18
15
  ...config,
19
- dependsOn: config.dependsOn || [],
16
+ readyPromise,
17
+ dependencies: config.dependencies || [],
20
18
  isReady: () => isReady,
21
19
  isComplete: () => isComplete,
22
20
  markReady: () => {
package/dist/types.d.ts CHANGED
@@ -7,23 +7,28 @@ export interface TaskConfig {
7
7
  name: string;
8
8
  commands: Commands;
9
9
  cwd: string;
10
- readyOn?: (output: string) => boolean;
11
- dependsOn?: Dependency[];
10
+ isReady?: (stdout: string) => boolean;
11
+ dependencies?: Task[];
12
12
  }
13
- export type Dependency = Promise<void> | (() => Promise<void>);
14
13
  interface TaskInternal extends TaskConfig {
15
- dependsOn: Dependency[];
16
- isReady: () => boolean;
14
+ readyPromise: Promise<void>;
15
+ dependencies: Task[];
16
+ isReady: (stdout: string) => boolean;
17
17
  isComplete: () => boolean;
18
18
  markReady: () => void;
19
19
  markComplete: () => void;
20
20
  }
21
21
  export interface Task {
22
22
  name: string;
23
- readyOrComplete(): Promise<void>;
24
23
  [$TASK_INTERNAL]: TaskInternal;
25
24
  }
25
+ export interface PipelineRunConfig {
26
+ onTaskError?: (taskName: string, error: Error) => void;
27
+ onTaskComplete?: (taskName: string) => void;
28
+ onPipelineError?: (error: Error) => void;
29
+ onPipelineComplete?: () => void;
30
+ }
26
31
  export interface Pipeline {
27
- run(): Promise<void>;
32
+ run(config: PipelineRunConfig): Promise<void>;
28
33
  }
29
34
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "builderman",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "Simple task runner for building and developing projects.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",