builderman 1.0.1 → 1.0.3
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/dist/index.d.ts +10 -9
- package/dist/index.js +96 -72
- package/package.json +9 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
|
+
export interface Commands {
|
|
2
|
+
dev: string;
|
|
3
|
+
build: string;
|
|
4
|
+
}
|
|
1
5
|
export interface TaskConfig {
|
|
2
6
|
name: string;
|
|
3
|
-
|
|
7
|
+
commands: Commands;
|
|
4
8
|
cwd: string;
|
|
5
|
-
|
|
9
|
+
readyOn?: (output: string) => boolean;
|
|
10
|
+
dependsOn?: Dependency[];
|
|
6
11
|
}
|
|
12
|
+
export type Dependency = Promise<void> | (() => Promise<void>);
|
|
7
13
|
export interface Task extends TaskConfig {
|
|
8
|
-
|
|
14
|
+
readyOrComplete(): Promise<void>;
|
|
9
15
|
}
|
|
10
16
|
export interface Pipeline {
|
|
11
17
|
run(): Promise<void>;
|
|
12
18
|
}
|
|
13
|
-
/**
|
|
14
|
-
* Emits an event that can be received by the parent process.
|
|
15
|
-
* Events are written to stdout in a parseable format.
|
|
16
|
-
*/
|
|
17
|
-
export declare function emit(event: string): void;
|
|
18
19
|
/**
|
|
19
20
|
* Creates a task configuration.
|
|
20
21
|
*/
|
|
21
22
|
export declare function task(config: TaskConfig): Task;
|
|
22
23
|
/**
|
|
23
|
-
* Creates a pipeline that manages task execution with
|
|
24
|
+
* Creates a pipeline that manages task execution with dependency-based coordination.
|
|
24
25
|
*/
|
|
25
26
|
export declare function pipeline(tasks: Task[]): Pipeline;
|
package/dist/index.js
CHANGED
|
@@ -1,70 +1,95 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
const EVENT_PREFIX = "__BUILDERMAN_EVENT__:";
|
|
5
|
-
/**
|
|
6
|
-
* Emits an event that can be received by the parent process.
|
|
7
|
-
* Events are written to stdout in a parseable format.
|
|
8
|
-
*/
|
|
9
|
-
export function emit(event) {
|
|
10
|
-
const eventData = JSON.stringify({ event });
|
|
11
|
-
console.log(`${EVENT_PREFIX}${eventData}`);
|
|
12
|
-
}
|
|
13
4
|
/**
|
|
14
5
|
* Creates a task configuration.
|
|
15
6
|
*/
|
|
16
7
|
export function task(config) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
8
|
+
let resolveReady = null;
|
|
9
|
+
const readyPromise = new Promise((resolve) => {
|
|
10
|
+
resolveReady = resolve;
|
|
11
|
+
});
|
|
12
|
+
let isReady = false;
|
|
13
|
+
let isComplete = false;
|
|
14
|
+
const task = {
|
|
15
|
+
...config,
|
|
16
|
+
dependsOn: config.dependsOn || [],
|
|
17
|
+
readyOrComplete() {
|
|
18
|
+
return readyPromise;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
task._markReady = () => {
|
|
22
|
+
if (!isReady && resolveReady) {
|
|
23
|
+
isReady = true;
|
|
24
|
+
resolveReady();
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
task._markComplete = () => {
|
|
28
|
+
if (!isComplete) {
|
|
29
|
+
isComplete = true;
|
|
30
|
+
if (!isReady && resolveReady) {
|
|
31
|
+
isReady = true;
|
|
32
|
+
resolveReady();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
22
35
|
};
|
|
36
|
+
task._isReady = () => isReady;
|
|
37
|
+
task._isComplete = () => isComplete;
|
|
38
|
+
return task;
|
|
23
39
|
}
|
|
24
40
|
/**
|
|
25
|
-
* Creates a pipeline that manages task execution with
|
|
41
|
+
* Creates a pipeline that manages task execution with dependency-based coordination.
|
|
26
42
|
*/
|
|
27
43
|
export function pipeline(tasks) {
|
|
28
44
|
return {
|
|
29
45
|
async run() {
|
|
30
46
|
const eventEmitter = new EventEmitter();
|
|
31
|
-
const emittedEvents = new Set();
|
|
32
47
|
const runningTasks = new Map();
|
|
33
48
|
const completedTasks = new Set();
|
|
34
|
-
const
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
const readyTasks = new Set();
|
|
50
|
+
// Determine which command to use based on NODE_ENV
|
|
51
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
52
|
+
const getCommand = (task) => {
|
|
53
|
+
return isProduction ? task.commands.build : task.commands.dev;
|
|
54
|
+
};
|
|
55
|
+
// Function to check if a task's dependencies are satisfied
|
|
56
|
+
const canStart = async (task) => {
|
|
41
57
|
if (runningTasks.has(task.name) || completedTasks.has(task.name)) {
|
|
42
58
|
return false;
|
|
43
59
|
}
|
|
44
|
-
|
|
60
|
+
if (!task.dependsOn || task.dependsOn.length === 0) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
// Wait for all dependencies
|
|
64
|
+
for (const dep of task.dependsOn) {
|
|
65
|
+
if (typeof dep === "function") {
|
|
66
|
+
await dep();
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
await dep;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
45
73
|
};
|
|
46
74
|
// Function to start a task
|
|
47
|
-
const startTask = (task) => {
|
|
75
|
+
const startTask = async (task) => {
|
|
48
76
|
if (runningTasks.has(task.name)) {
|
|
49
77
|
return;
|
|
50
78
|
}
|
|
79
|
+
const command = getCommand(task);
|
|
51
80
|
// Ensure node_modules/.bin is in PATH for local dependencies
|
|
52
|
-
// Resolve cwd relative to current working directory
|
|
53
81
|
const taskCwd = path.isAbsolute(task.cwd)
|
|
54
82
|
? task.cwd
|
|
55
83
|
: path.resolve(process.cwd(), task.cwd);
|
|
56
84
|
const localBinPath = path.join(taskCwd, "node_modules", ".bin");
|
|
57
|
-
// Build PATH with local node_modules/.bin first
|
|
85
|
+
// Build PATH with local node_modules/.bin first
|
|
58
86
|
const existingPath = process.env.PATH || process.env.Path || "";
|
|
59
87
|
const pathSeparator = process.platform === "win32" ? ";" : ":";
|
|
60
|
-
// Collect all potential bin paths (local first, then root, then existing)
|
|
61
88
|
const binPaths = [localBinPath];
|
|
62
|
-
// Also add root node_modules/.bin if different from local
|
|
63
89
|
const rootBinPath = path.join(process.cwd(), "node_modules", ".bin");
|
|
64
90
|
if (rootBinPath !== localBinPath) {
|
|
65
91
|
binPaths.push(rootBinPath);
|
|
66
92
|
}
|
|
67
|
-
// Add existing PATH
|
|
68
93
|
if (existingPath) {
|
|
69
94
|
binPaths.push(existingPath);
|
|
70
95
|
}
|
|
@@ -72,41 +97,50 @@ export function pipeline(tasks) {
|
|
|
72
97
|
const env = {
|
|
73
98
|
...process.env,
|
|
74
99
|
PATH: newPath,
|
|
75
|
-
Path: newPath,
|
|
100
|
+
Path: newPath,
|
|
76
101
|
};
|
|
77
|
-
|
|
78
|
-
const child = spawn(task.command, {
|
|
102
|
+
const child = spawn(command, {
|
|
79
103
|
cwd: task.cwd,
|
|
80
104
|
stdio: ["inherit", "pipe", "pipe"],
|
|
81
105
|
shell: true,
|
|
82
106
|
env,
|
|
83
107
|
});
|
|
108
|
+
// Handle spawn errors
|
|
109
|
+
child.on("error", (error) => {
|
|
110
|
+
console.error(`[${task.name}] Failed to start:`, error.message);
|
|
111
|
+
runningTasks.delete(task.name);
|
|
112
|
+
completedTasks.add(task.name);
|
|
113
|
+
task._markComplete();
|
|
114
|
+
process.exitCode = 1;
|
|
115
|
+
eventEmitter.emit("taskCompleted", task.name);
|
|
116
|
+
});
|
|
84
117
|
runningTasks.set(task.name, child);
|
|
85
|
-
//
|
|
118
|
+
// If task doesn't have readyOn, mark as ready immediately
|
|
119
|
+
if (!task.readyOn) {
|
|
120
|
+
readyTasks.add(task.name);
|
|
121
|
+
task._markReady();
|
|
122
|
+
eventEmitter.emit("taskReady", task.name);
|
|
123
|
+
}
|
|
124
|
+
// Monitor stdout for readiness
|
|
86
125
|
let stdoutBuffer = "";
|
|
126
|
+
let allOutput = "";
|
|
87
127
|
child.stdout?.on("data", (data) => {
|
|
88
|
-
|
|
128
|
+
const chunk = data.toString();
|
|
129
|
+
allOutput += chunk;
|
|
130
|
+
stdoutBuffer += chunk;
|
|
89
131
|
const lines = stdoutBuffer.split("\n");
|
|
90
132
|
stdoutBuffer = lines.pop() || "";
|
|
91
133
|
for (const line of lines) {
|
|
92
|
-
if
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
eventEmitter.emit("event", eventName);
|
|
99
|
-
}
|
|
134
|
+
// Check if task is ready based on readyOn callback
|
|
135
|
+
if (task.readyOn && !readyTasks.has(task.name)) {
|
|
136
|
+
if (task.readyOn(allOutput)) {
|
|
137
|
+
readyTasks.add(task.name);
|
|
138
|
+
task._markReady();
|
|
139
|
+
eventEmitter.emit("taskReady", task.name);
|
|
100
140
|
}
|
|
101
|
-
catch (e) {
|
|
102
|
-
// Ignore parse errors
|
|
103
|
-
}
|
|
104
|
-
// Don't forward event lines to stdout
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
// Forward stdout to parent
|
|
108
|
-
process.stdout.write(line + "\n");
|
|
109
141
|
}
|
|
142
|
+
// Forward stdout to parent
|
|
143
|
+
process.stdout.write(line + "\n");
|
|
110
144
|
}
|
|
111
145
|
});
|
|
112
146
|
// Forward any remaining buffer on end
|
|
@@ -123,34 +157,26 @@ export function pipeline(tasks) {
|
|
|
123
157
|
child.on("exit", (code) => {
|
|
124
158
|
runningTasks.delete(task.name);
|
|
125
159
|
completedTasks.add(task.name);
|
|
160
|
+
task._markComplete();
|
|
126
161
|
if (code !== 0) {
|
|
127
162
|
process.exitCode = code || 1;
|
|
128
163
|
}
|
|
129
|
-
// Check if any waiting tasks can now start
|
|
130
164
|
eventEmitter.emit("taskCompleted", task.name);
|
|
131
165
|
});
|
|
132
166
|
};
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
for (const task of tasks) {
|
|
136
|
-
if (canStart(task)) {
|
|
137
|
-
startTask(task);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
eventEmitter.on("taskCompleted", () => {
|
|
167
|
+
// Function to try starting tasks when dependencies are ready
|
|
168
|
+
const tryStartTasks = async () => {
|
|
142
169
|
for (const task of tasks) {
|
|
143
|
-
if (canStart(task)) {
|
|
144
|
-
startTask(task);
|
|
170
|
+
if (await canStart(task)) {
|
|
171
|
+
await startTask(task);
|
|
145
172
|
}
|
|
146
173
|
}
|
|
147
|
-
}
|
|
174
|
+
};
|
|
175
|
+
// Listen for task readiness and completion to start dependent tasks
|
|
176
|
+
eventEmitter.on("taskReady", tryStartTasks);
|
|
177
|
+
eventEmitter.on("taskCompleted", tryStartTasks);
|
|
148
178
|
// Start tasks that don't have dependencies
|
|
149
|
-
|
|
150
|
-
if (canStart(task)) {
|
|
151
|
-
startTask(task);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
179
|
+
await tryStartTasks();
|
|
154
180
|
// Wait for all tasks to complete
|
|
155
181
|
return new Promise((resolve, reject) => {
|
|
156
182
|
const checkCompletion = () => {
|
|
@@ -159,17 +185,15 @@ export function pipeline(tasks) {
|
|
|
159
185
|
}
|
|
160
186
|
};
|
|
161
187
|
eventEmitter.on("taskCompleted", checkCompletion);
|
|
162
|
-
checkCompletion();
|
|
188
|
+
checkCompletion();
|
|
163
189
|
// Handle process termination
|
|
164
190
|
process.on("SIGINT", () => {
|
|
165
|
-
// Kill all running tasks
|
|
166
191
|
for (const child of runningTasks.values()) {
|
|
167
192
|
child.kill("SIGINT");
|
|
168
193
|
}
|
|
169
194
|
reject(new Error("Process interrupted"));
|
|
170
195
|
});
|
|
171
196
|
process.on("SIGTERM", () => {
|
|
172
|
-
// Kill all running tasks
|
|
173
197
|
for (const child of runningTasks.values()) {
|
|
174
198
|
child.kill("SIGTERM");
|
|
175
199
|
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "builderman",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "Simple task runner for building and developing projects.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/LankyMoose/builderman"
|
|
10
10
|
},
|
|
11
|
-
"keywords": [
|
|
11
|
+
"keywords": [
|
|
12
|
+
"task",
|
|
13
|
+
"runner",
|
|
14
|
+
"build",
|
|
15
|
+
"development",
|
|
16
|
+
"pipeline"
|
|
17
|
+
],
|
|
12
18
|
"author": "LankyMoose",
|
|
13
19
|
"license": "ISC",
|
|
14
20
|
"devDependencies": {
|