@fynjs/run 1.0.0

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/lib/xqtree.js ADDED
@@ -0,0 +1,41 @@
1
+ import XQItem from "./xqitem.js";
2
+
3
+ class XQTree {
4
+ constructor() {
5
+ this.tree = {};
6
+ this._items = {};
7
+ }
8
+
9
+ create(options, parent) {
10
+ options = Object.assign({ parent }, options);
11
+
12
+ const x = new XQItem(options);
13
+ this._items[x.id] = x;
14
+
15
+ if (parent) {
16
+ parent.addChild(x);
17
+ x.level = parent.level !== undefined ? parent.level + 1 : 0;
18
+ } else {
19
+ this.tree[x.id] = x;
20
+ x.level = 0;
21
+ }
22
+
23
+ return x;
24
+ }
25
+
26
+ parent(qItem) {
27
+ const x = this._items[qItem.parentId];
28
+ return x;
29
+ }
30
+
31
+ parentName(qItem) {
32
+ const parent = this.parent(qItem);
33
+ return parent && parent.name;
34
+ }
35
+
36
+ item(id) {
37
+ return this._items[id];
38
+ }
39
+ }
40
+
41
+ export default XQTree;
@@ -0,0 +1,33 @@
1
+ import XRun from "./xrun.js";
2
+ import XReporterConsole from "../lib/reporters/console.js";
3
+ import XTaskSpec from "./xtask-spec.js";
4
+
5
+ function createXrunInstance() {
6
+ const xrun = new XRun({});
7
+ xrun[Symbol("reporter")] = new XReporterConsole(xrun);
8
+
9
+ xrun.load = xrun.load.bind(xrun);
10
+ xrun.run = xrun.run.bind(xrun);
11
+ xrun.asyncRun = xrun.asyncRun.bind(xrun);
12
+
13
+ xrun.XClap = XRun;
14
+ xrun.XRun = XRun;
15
+ xrun.XTaskSpec = XTaskSpec;
16
+ xrun.XReporterConsole = XReporterConsole;
17
+
18
+ return xrun;
19
+ }
20
+
21
+ const container = {
22
+ createXrunInstance,
23
+ _xrun: createXrunInstance(),
24
+ get xrun() {
25
+ return this._xrun || this.reset();
26
+ },
27
+ reset() {
28
+ return (this._xrun = createXrunInstance());
29
+ }
30
+ };
31
+
32
+ export { createXrunInstance };
33
+ export default container;
package/lib/xrun.d.ts ADDED
@@ -0,0 +1,89 @@
1
+ import { EventEmitter } from "events";
2
+
3
+ // Type for task execution options
4
+ interface TaskOptions {
5
+ flags?: string | string[];
6
+ [key: string]: any;
7
+ }
8
+
9
+ // Type for task specification
10
+ interface TaskSpec {
11
+ cmd: string | string[];
12
+ flags?: string | string[];
13
+ [key: string]: any;
14
+ }
15
+
16
+ // Type for environment update options
17
+ interface EnvUpdateOptions {
18
+ override?: boolean;
19
+ posix?: boolean;
20
+ }
21
+
22
+ // Type for namespace and tasks
23
+ interface TasksDefinition {
24
+ [key: string]: any;
25
+ }
26
+
27
+ // Type for XTaskSpec class
28
+ declare class XTaskSpec {
29
+ constructor(spec: TaskSpec);
30
+ }
31
+
32
+ // Type for XTasks class
33
+ declare class XTasks {
34
+ constructor(namespace: string | object, tasks?: TasksDefinition);
35
+ load(tasks: TasksDefinition): void;
36
+ load(namespace: string | object, tasks: TasksDefinition, priority?: number): void;
37
+ hasFinally(): boolean;
38
+ count(): number;
39
+ fullNames(): string[];
40
+ }
41
+
42
+ // Type for XQTree class
43
+ declare class XQTree {
44
+ create(options: { name: string; value?: any }): any;
45
+ }
46
+
47
+ // Type for the main XRun class
48
+ declare class XRun extends EventEmitter {
49
+ constructor(namespace?: string | object, tasks?: TasksDefinition);
50
+
51
+ // Properties
52
+ failed: Error | null;
53
+ stopOnError: boolean | "soft" | "full" | "";
54
+
55
+ // Methods
56
+ load(tasks: TasksDefinition): this;
57
+ load(namespace: string | object, tasks: TasksDefinition, priority?: number): this;
58
+ run(tasks: string | any[], done?: (err?: Error | Error[]) => void): this;
59
+ asyncRun(tasks: string | any[]): Promise<any>;
60
+ printTasks(): this;
61
+ countTasks(): number;
62
+ getNamespaces(): object;
63
+ fail(err: Error): this;
64
+ waitAllPending(done: () => void): this;
65
+ stop(): symbol;
66
+ exec(spec: string | string[] | TaskSpec, options?: string | string[] | TaskOptions): XTaskSpec;
67
+ updateEnv(envValues: object, options?: EnvUpdateOptions): void;
68
+ env(spec: object, options?: EnvUpdateOptions): void;
69
+ concurrent(...tasks: any[]): any[];
70
+ parallel(...tasks: any[]): any[];
71
+ serial(...tasks: any[]): any[];
72
+
73
+ // Internal methods
74
+ private _exitOnError(err: Error | Error[]): void;
75
+ private _showSimilarTasks(res: { name: string }): void;
76
+ private exit(code: number): void;
77
+ private killTaskChildren(): void;
78
+ private actStop(): void;
79
+ }
80
+
81
+ // Type for the xrun instance
82
+ declare const xrun: XRun & {
83
+ XClap: typeof XRun;
84
+ XRun: typeof XRun;
85
+ XTaskSpec: typeof XTaskSpec;
86
+ XReporterConsole: any;
87
+ };
88
+
89
+ export = xrun;
package/lib/xrun.js ADDED
@@ -0,0 +1,344 @@
1
+ import assert from "assert";
2
+ import defaults from "./defaults.js";
3
+ import chalk from "./chalk.js";
4
+ import XQtor from "./xqtor.js";
5
+ import XTasks from "./xtasks.js";
6
+ import XQTree from "./xqtree.js";
7
+ import logger from "./logger.js";
8
+ import EventEmitter from "events";
9
+ import printTasks from "./print-tasks/index.js";
10
+ import jaroWinkler from "jaro-winkler";
11
+ import XTaskSpec from "./xtask-spec.js";
12
+ import xsh from "xsh";
13
+ import updateEnv from "./util/update-env.js";
14
+ import myPkg from "../package.json" with { type: "json" };
15
+ import { CliContext } from "./cli-context.js";
16
+ import { exec } from "child_process";
17
+
18
+ // full - full stop, interrupting all pending async tasks
19
+ // soft - allow pending async tasks to complete, but no more new tasks
20
+ // "" - march on
21
+ const STOP_ON_ERROR = ["", "soft", "full"];
22
+
23
+ function _decorateTasks(type, name, ...tasks) {
24
+ assert(tasks.length > 0, `${name} no tasks passed`);
25
+ tasks = tasks.filter(x => x);
26
+ if (tasks.length === 1) {
27
+ tasks = tasks[0];
28
+ // user passed single argument that's not an array
29
+ // assume it's a single task where serial/concurrent is N/A
30
+ if (!Array.isArray(tasks)) return tasks;
31
+ }
32
+
33
+ return [type].concat(tasks);
34
+ }
35
+
36
+ function _concurrent(...tasks) {
37
+ return _decorateTasks(defaults.CONCURRENT_SYM, "xrun.concurrent", ...tasks);
38
+ }
39
+
40
+ class XRun extends EventEmitter {
41
+ constructor(namespace, tasks) {
42
+ super();
43
+ this._tasks = new XTasks(namespace, tasks);
44
+ this.failed = null;
45
+ this.xqTree = new XQTree();
46
+ this._logger = logger;
47
+ this._pending = 0;
48
+ this._taskChildren = {};
49
+ this._isStop = false;
50
+ this._cliContext = new CliContext({});
51
+ this.on("spawn-async", () => this._pending++);
52
+ this.on("done-async", () => this._pending--);
53
+ }
54
+
55
+ get stopOnError() {
56
+ return this._stopOnError;
57
+ }
58
+
59
+ set stopOnError(v) {
60
+ if (v === false) {
61
+ this._stopOnError = "";
62
+ } else if (v === true) {
63
+ this._stopOnError = "full";
64
+ } else {
65
+ assert(
66
+ STOP_ON_ERROR.indexOf(v) >= 0,
67
+ `stopOnError must be true or false, or one of ${STOP_ON_ERROR.map(JSON.stringify).join(
68
+ ", "
69
+ )}`
70
+ );
71
+ this._stopOnError = v;
72
+ }
73
+ }
74
+
75
+ // priority: lower value => lower priority when searching namespace for a task
76
+ load(namespace, tasks, priority = 1) {
77
+ this._tasks.load(namespace, tasks, priority);
78
+ return this;
79
+ }
80
+
81
+ addTaskChild(child, sym) {
82
+ this._taskChildren[sym] = child;
83
+ return this._taskChildren;
84
+ }
85
+
86
+ removeTaskChild(child, sym) {
87
+ delete this._taskChildren[sym];
88
+ return this._taskChildren;
89
+ }
90
+
91
+ /**
92
+ * Kill a child process in a cross-platform manner and mark it as stopped
93
+ * @param {import('child_process').ChildProcess|any} child - Child process to kill
94
+ */
95
+ killChildProcess(child) {
96
+ /* istanbul ignore next */
97
+ if (!child) return;
98
+ /* istanbul ignore next */
99
+ if (process.platform === "win32" && child.pid) {
100
+ try {
101
+ /* istanbul ignore next */
102
+ exec(`taskkill /T /F /PID ${child.pid}`);
103
+ } catch (err) {
104
+ /* istanbul ignore next */
105
+ try {
106
+ child.kill();
107
+ } catch (e) {
108
+ // ignore
109
+ }
110
+ }
111
+ } else {
112
+ try {
113
+ child.kill();
114
+ } catch (err) {
115
+ // ignore
116
+ }
117
+ }
118
+ child[defaults.STOP_SYM] = true;
119
+ }
120
+
121
+ /**
122
+ * Set the CLI context for this XRun instance
123
+ * @param {CliContext} cliContext - Command context object
124
+ * @returns {XRun} This instance for chaining
125
+ */
126
+ setCliContext(cliContext) {
127
+ this._cliContext = cliContext;
128
+ return this;
129
+ }
130
+
131
+ /**
132
+ * Get the current CLI context
133
+ * @returns {CliContext|null} Current CLI context
134
+ */
135
+ getCliContext() {
136
+ return this._cliContext;
137
+ }
138
+
139
+ asyncRun(tasks) {
140
+ return new Promise((resolve, reject) => {
141
+ this.run(tasks, (err, result) => {
142
+ if (err) reject(err);
143
+ else resolve(result);
144
+ });
145
+ });
146
+ }
147
+
148
+ run(tasks, done) {
149
+ if (this.stopOnError === undefined) {
150
+ this.stopOnError = true;
151
+ }
152
+ if (this._tasks.hasFinally() && this.stopOnError === "full") {
153
+ this.emit("warn-finally");
154
+ }
155
+ done = done || this._exitOnError.bind(this);
156
+ const xqtor = new XQtor({ tasks: this._tasks, done, xrun: this });
157
+
158
+ try {
159
+ if (typeof tasks === "string") {
160
+ this.emit("run", { name: tasks });
161
+ xqtor.pushItem(this.xqTree.create({ name: tasks }));
162
+ } else {
163
+ this.emit("run", { tasks: tasks });
164
+ xqtor.pushItem(this.xqTree.create({ name: "run", value: tasks }));
165
+ }
166
+ xqtor.next();
167
+ } catch (err) {
168
+ done([err]);
169
+ }
170
+ return this;
171
+ }
172
+
173
+ printTasks() {
174
+ printTasks(this._tasks);
175
+ return this;
176
+ }
177
+
178
+ countTasks() {
179
+ return this._tasks.count();
180
+ }
181
+
182
+ getNamespaces() {
183
+ return this._tasks._namespaces;
184
+ }
185
+
186
+ fail(err) {
187
+ if (!this.failed) {
188
+ this.failed = err;
189
+ } else {
190
+ this.failed.more = this.failed.more || [];
191
+ this.failed.more.push(err);
192
+ }
193
+ return this;
194
+ }
195
+
196
+ waitAllPending(done) {
197
+ const wait = () => {
198
+ if (this._pending === 0) {
199
+ return done();
200
+ }
201
+ setTimeout(wait, 10);
202
+ };
203
+ process.nextTick(wait);
204
+ return this;
205
+ }
206
+
207
+ _showSimilarTasks(res) {
208
+ const names = this._tasks.fullNames();
209
+ const distances = names
210
+ .map(name => {
211
+ name = name.split("/")[1];
212
+ if (name.startsWith(".")) return false;
213
+ const dis = jaroWinkler(res.name, name) * 100000;
214
+ return { dis, name };
215
+ })
216
+ .filter(x => x)
217
+ .sort((a, b) => b.dis - a.dis);
218
+ const similars = distances.slice(0, 5).map(x => chalk.cyan(x.name));
219
+ console.log(` Maybe try: ${similars.join(", ")}`);
220
+ }
221
+
222
+ _exitOnError(err) {
223
+ if (!err) {
224
+ return;
225
+ }
226
+
227
+ console.log(chalk.bold.red.inverse(" Execution Failed - Errors: "));
228
+ const errors = [].concat(err, err.more).filter(x => x);
229
+ errors.forEach((e, x) => {
230
+ const idx = chalk.bold.red.inverse(` ${x + 1} `);
231
+ if (!e.stack) {
232
+ console.log(idx, e);
233
+ return;
234
+ }
235
+ const lines = e.stack.split("\n");
236
+ console.log(idx, e.message);
237
+ if (e.name && e.name.indexOf("AssertionError") >= 0) return;
238
+ if (e.code === "TASK_NOT_FOUND") {
239
+ this._showSimilarTasks(e.res);
240
+ return;
241
+ }
242
+ if (lines.length < 2) return;
243
+ if (lines[1].indexOf("xsh/lib/exec.js") >= 0) return;
244
+ const cleaned = lines.filter(l => {
245
+ return (
246
+ // exclude stack for node.js internal modules and xrun's own files
247
+ !l.includes(`(internal/`) &&
248
+ !l.includes("/node_modules/xclap/lib/") &&
249
+ !l.includes(`/node_modules/${myPkg.name}/lib`)
250
+ );
251
+ });
252
+ // start at 1 to exclude the message that's the first line in stack output
253
+ for (let i = 1; i < cleaned.length; i++) {
254
+ console.log(
255
+ chalk.gray(
256
+ /* istanbul ignore next */
257
+ process.cwd().length > 2 ? xsh.pathCwd.replace(cleaned[i], ".", "g") : cleaned[i]
258
+ )
259
+ );
260
+ }
261
+ });
262
+ this.exit(1);
263
+ }
264
+
265
+ exit(code) {
266
+ if (this.stopOnError) {
267
+ // Check if there are any child processes to clean up
268
+ const hasChildren = Object.getOwnPropertySymbols(this._taskChildren).length > 0;
269
+
270
+ if (hasChildren) {
271
+ // Kill all tracked child processes before exiting
272
+ this.killTaskChildren();
273
+ // Give kill operations time to complete (especially on Windows with taskkill)
274
+ setTimeout(() => {
275
+ process.exit(code);
276
+ }, 100);
277
+ } else {
278
+ // No children to clean up, exit immediately
279
+ process.exit(code);
280
+ }
281
+ }
282
+ }
283
+
284
+ killTaskChildren() {
285
+ setTimeout(() => {
286
+ const children = this._taskChildren;
287
+ this._taskChildren = {};
288
+ const symbols = Object.getOwnPropertySymbols(children);
289
+ symbols.forEach(sym => {
290
+ const child = children[sym];
291
+ this.killChildProcess(child);
292
+ });
293
+ }, 10);
294
+ }
295
+
296
+ actStop() {
297
+ this.killTaskChildren();
298
+ this._isStop = true;
299
+ }
300
+
301
+ stop() {
302
+ return defaults.STOP_SYM;
303
+ }
304
+
305
+ exec(spec, options) {
306
+ if (Array.isArray(spec) || typeof spec === "string") {
307
+ if (typeof options === "string" || Array.isArray(options)) {
308
+ options = { flags: options };
309
+ }
310
+ return new XTaskSpec(Object.assign({ cmd: spec }, options));
311
+ } else if (typeof spec === "object") {
312
+ return new XTaskSpec(spec);
313
+ } else {
314
+ throw new Error(
315
+ `xrun.exec - unknown spec type ${typeof spec}: must be a string, array, or an object`
316
+ );
317
+ }
318
+ }
319
+
320
+ updateEnv(envValues, options = {}) {
321
+ return updateEnv(envValues, options.target || process.env, options.override);
322
+ }
323
+
324
+ env(spec, options) {
325
+ return new XTaskSpec(Object.assign({ ...options, type: "env", env: spec }));
326
+ }
327
+
328
+ // concurrent([task1, task2, ...]) or concurrent(task1, task2, ...)
329
+ concurrent(...tasks) {
330
+ return _concurrent(...tasks);
331
+ }
332
+
333
+ // alias for concurrent
334
+ parallel(...tasks) {
335
+ return _concurrent(...tasks);
336
+ }
337
+
338
+ // serial([task1, task2, ...]) or concurrent(task1, task2, ...)
339
+ serial(...tasks) {
340
+ return _decorateTasks(defaults.SERIAL_SYM, "xrun.serial", ...tasks);
341
+ }
342
+ }
343
+
344
+ export default XRun;
@@ -0,0 +1,63 @@
1
+ import { mkCmd } from "xsh";
2
+
3
+ class XTaskSpec {
4
+ constructor(spec) {
5
+ this.type = spec.type || "exec";
6
+
7
+ const cmd = spec.cmd || spec.command;
8
+ if (Array.isArray(cmd)) {
9
+ this.cmd = mkCmd(...cmd);
10
+ } else {
11
+ this.cmd = cmd;
12
+ }
13
+
14
+ // gather execOptions from spec
15
+ const execOptions = Object.assign({}, spec.execOptions);
16
+ const env = Object.assign({}, execOptions.env, spec.env);
17
+ if (Object.keys(env).length) {
18
+ execOptions.env = env;
19
+ }
20
+ this.options = execOptions;
21
+ if (this.type === "env") {
22
+ this.options.override = spec.override;
23
+ }
24
+ //
25
+ this.flags = spec.flags || {};
26
+ this.xrun = Object.assign({ delayRunMs: 0 }, spec.xrun || spec.xclap);
27
+ }
28
+
29
+ toString(trailing) {
30
+ const makeEnvStr = s => {
31
+ let envStr = "";
32
+ const env = this.options.env;
33
+ if (env) {
34
+ envStr = `${s}{${Object.keys(env)
35
+ .map(k => `${k}=${env[k]}`)
36
+ .join(";")}}`;
37
+ }
38
+ return envStr;
39
+ };
40
+ if (this.type === "exec") {
41
+ let flags = this.flags;
42
+
43
+ if (Array.isArray(flags)) {
44
+ flags = flags.join(",");
45
+ } else if (typeof flags === "object") {
46
+ flags = Object.keys(flags).join(",");
47
+ }
48
+
49
+ if (flags) {
50
+ flags = `(${flags})`;
51
+ }
52
+
53
+ const _t = (trailing && ` ${trailing}`) || "";
54
+ return `exec${flags}${makeEnvStr(" ")} '${this.cmd}${_t}'`;
55
+ } else if (this.type === "env") {
56
+ return `env${makeEnvStr("")}`;
57
+ }
58
+
59
+ return `XTaskSpec - Unknown type ${this.type}`;
60
+ }
61
+ }
62
+
63
+ export default XTaskSpec;
package/lib/xtasks.js ADDED
@@ -0,0 +1,137 @@
1
+ import assert from "assert";
2
+ import defaults from "./defaults.js";
3
+ import NSOrder from "./ns-order.js";
4
+
5
+ class XTasks {
6
+ constructor(namespace, tasks) {
7
+ this._tasks = { [defaults.NAMESPACE]: {} };
8
+ this._nsOrder = new NSOrder();
9
+ if (namespace) {
10
+ this.load(namespace, tasks);
11
+ } else {
12
+ this._namespaces = [defaults.NAMESPACE];
13
+ }
14
+ }
15
+
16
+ load(namespace, tasks, priority = 1) {
17
+ let overrides;
18
+ if (tasks === undefined) {
19
+ tasks = namespace;
20
+ namespace = defaults.NAMESPACE;
21
+ } else if (typeof namespace === "object") {
22
+ overrides = namespace.overrides;
23
+ namespace = namespace.namespace;
24
+ }
25
+
26
+ assert(tasks && typeof tasks === "object", "Invalid tasks");
27
+
28
+ if (this._tasks[namespace] === undefined) {
29
+ this._tasks[namespace] = {};
30
+ }
31
+
32
+ if (namespace !== defaults.NAMESPACE) {
33
+ this._nsOrder.add(namespace, overrides, priority);
34
+ }
35
+
36
+ this._namespaces = [defaults.NAMESPACE].concat(this._nsOrder._namespaces);
37
+
38
+ Object.assign(this._tasks[namespace], tasks);
39
+ }
40
+
41
+ count() {
42
+ return this._namespaces.reduce((x, ns) => {
43
+ return x + Object.keys(this._tasks[ns]).length;
44
+ }, 0);
45
+ }
46
+
47
+ names(ns) {
48
+ return (ns || this._namespaces).reduce((x, ns) => {
49
+ return x.concat(Object.keys(this._getNsTasks(ns)));
50
+ }, []);
51
+ }
52
+
53
+ fullNames(ns) {
54
+ return (ns || this._namespaces).reduce((x, ns) => {
55
+ const nsTasks = this._getNsTasks(ns);
56
+ return x.concat(
57
+ Object.keys(nsTasks).map(t =>
58
+ ns === defaults.NAMESPACE ? `${defaults.NS_SEP}${t}` : `${ns}${defaults.NS_SEP}${t}`
59
+ )
60
+ );
61
+ }, []);
62
+ }
63
+
64
+ lookup(name) {
65
+ const invalidName = "Empty task name is invalid";
66
+ assert(name, invalidName);
67
+ name = name.trim();
68
+
69
+ let optional = false;
70
+ // If there's a prefix ? then the execution is optional
71
+ if (name.startsWith("?")) {
72
+ optional = true;
73
+ name = name.substr(1).trim();
74
+ }
75
+ assert(name, invalidName);
76
+ let res = { name };
77
+ const nsSepIdx = name.indexOf(defaults.NS_SEP);
78
+ // no NS_SEP found
79
+ if (nsSepIdx < 0) {
80
+ res = this._searchNamespaces(name);
81
+ } else {
82
+ if (nsSepIdx === 0) {
83
+ res.ns = defaults.NAMESPACE;
84
+ res.name = name.substr(1);
85
+ } else {
86
+ res.ns = name.substring(0, nsSepIdx).trim();
87
+ assert(res.ns, `Invalid namespace in task name ${name}`);
88
+ res.name = name.substring(nsSepIdx + 1);
89
+ }
90
+ const nsTasks = this._getNsTasks(res.ns);
91
+ assert(res.name, `Empty task name from '${name}' is invalid`);
92
+ res.item = nsTasks[res.name];
93
+ }
94
+ if (!res.item) {
95
+ const opt = optional ? "Optional " : "";
96
+ const err = new Error(
97
+ `${opt}Task ${res.name}${res.ns ? " in namespace " + res.ns : ""} not found`
98
+ );
99
+ err.optional = optional;
100
+ err.res = res;
101
+ err.code = "TASK_NOT_FOUND";
102
+ throw err;
103
+ }
104
+ return res;
105
+ }
106
+
107
+ hasFinally() {
108
+ for (const ns of this._namespaces) {
109
+ const tasks = this._tasks[ns];
110
+ for (const name in tasks) {
111
+ if (tasks[name].hasOwnProperty("finally")) {
112
+ return true;
113
+ }
114
+ }
115
+ }
116
+ return false;
117
+ }
118
+
119
+ _getNsTasks(ns) {
120
+ const x = this._tasks[ns];
121
+ assert(x, `No task namespace ${ns} exist`);
122
+ return x;
123
+ }
124
+
125
+ _searchNamespaces(name) {
126
+ const ns = this._namespaces;
127
+ for (var i = 0; i < ns.length; i++) {
128
+ const x = this._tasks[ns[i]][name];
129
+ if (x) {
130
+ return { ns: ns[i], name, item: x, search: true };
131
+ }
132
+ }
133
+ return { name };
134
+ }
135
+ }
136
+
137
+ export default XTasks;