builderman 1.0.7 → 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 +51 -1
- package/dist/pipeline.js +158 -149
- package/dist/task.js +2 -4
- package/dist/types.d.ts +12 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
# **builderman**
|
|
2
2
|
|
|
3
|
-
####
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
shell: false,
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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 {};
|