@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/xqtor.js ADDED
@@ -0,0 +1,814 @@
1
+ import { createRequire } from "node:module";
2
+ import assert from "assert";
3
+ import defaults from "./defaults.js";
4
+ import Insync from "insync";
5
+ import XQItem from "./xqitem.js";
6
+ import { exec as exec } from "xsh";
7
+ import parseArray from "./util/parse-array.js";
8
+ import childProc from "child_process";
9
+ import updateEnv from "./util/update-env.js";
10
+ import { NixClap } from "@fynjs/cli-args";
11
+
12
+ //
13
+ // Kept lazy rather than a static import: a static import is hoisted to module load, which is
14
+ // exactly what the call site below avoids so fynpo bootstrap can build unwrap-npm-cmd first.
15
+ //
16
+ const lazyRequire = createRequire(import.meta.url);
17
+
18
+ const STAGE_FINALLY = "finally";
19
+
20
+ const isSerial = x => defaults.SERIAL_SIG.includes(x[0]);
21
+ const isConcurrent = x => defaults.CONCURRENT_SIG.includes(x[0]);
22
+
23
+ const isReadableStream = x => Boolean(x && x.pipe && x.on && x._readableState);
24
+
25
+ /*
26
+ * Task executor (XQtor) - After all the fuss, it comes down to this -- the core of xrun
27
+ * that handles the execution of tasks.
28
+ *
29
+ * Tasks are added to a stack using an array.
30
+ *
31
+ * - Each concurrent tasks array is executed with a new XQtor and Insync.parallel.
32
+ * - Serial tasks are all added to the top of the "stack" in reverse order
33
+ * - Some items are actions that cause more items to be created and pushed
34
+ * into the stack. Like looking up the task of a task name.
35
+ * - As each task is completed, the XQtor's next is invoked to pop items
36
+ * from the stack for processing.
37
+ * - A mark item is added to the stack before each task so when a mark
38
+ * is seen, error is checked and done event is emitted.
39
+ *
40
+ */
41
+ class XQtor {
42
+ constructor(options) {
43
+ this._tasks = options.tasks;
44
+ this._done = options.done;
45
+ this._xrun = options.xrun;
46
+ this.xqItems = [];
47
+ }
48
+
49
+ next(err, xqId) {
50
+ process.nextTick(() => this._next(err, xqId), 0);
51
+ }
52
+
53
+ _next(err, xqId) {
54
+ if (err) {
55
+ const qItem = this._xrun.xqTree.item(xqId);
56
+ qItem.err = err;
57
+ this._xrun.fail(err);
58
+ }
59
+
60
+ if (this.xqItems.length > 0) {
61
+ this.execute();
62
+ } else {
63
+ this._done(this._xrun.failed);
64
+ }
65
+ }
66
+
67
+ execute() {
68
+ assert(this.xqItems.length > 0, "no next task");
69
+
70
+ const qItem = this.popItem();
71
+
72
+ if (qItem.mark) {
73
+ return this._processMark(qItem);
74
+ }
75
+
76
+ if (this._xrun._isStop || (this._xrun.failed && !qItem.isFinally && this._xrun.stopOnError)) {
77
+ return this.next(null, qItem.id);
78
+ }
79
+
80
+ const value = qItem.value();
81
+ if (value === defaults.STOP_SYM) {
82
+ this._xrun.actStop();
83
+ return this.next(null, qItem.id);
84
+ }
85
+
86
+ const vtype = value.constructor.name;
87
+ if (vtype === "XTaskSpec") {
88
+ if (value.type === "exec") {
89
+ return this._shellXer(qItem, value, true);
90
+ } else if (value.type === "env") {
91
+ return this._envXer(qItem, value);
92
+ }
93
+ this._xrun.fail(
94
+ new Error(`Unable to process XTaskSpec type ${value.type} for task ${qItem.name}`)
95
+ );
96
+ } else if (vtype === "String") {
97
+ if (this._isAnonShell(value)) {
98
+ return this._shellXer(qItem, this._parseAnonShell(value), true);
99
+ }
100
+
101
+ return this._processLookup(qItem);
102
+ } else if (vtype === "Function" || vtype === "AsyncFunction") {
103
+ return this._functionXer(qItem, value);
104
+ } else if (vtype === "Array") {
105
+ return this._processArray(qItem, value);
106
+ } else if (value.item) {
107
+ const dep = value.item.dep;
108
+ if (dep && !qItem.xqDep) {
109
+ return this._processDep(qItem, dep);
110
+ } else {
111
+ return this._processTaskObject(qItem);
112
+ }
113
+ } else {
114
+ this._xrun.fail(
115
+ new Error(`Unable to process task ${qItem.name} \
116
+ because value type ${vtype} is unknown and no value.item`)
117
+ );
118
+ }
119
+
120
+ return this.next(null, qItem.id);
121
+ }
122
+
123
+ pushMarkItem(qItem, callback) {
124
+ const x = this._makeXqMarkItem(qItem, callback);
125
+ this.pushItem(x);
126
+ return x;
127
+ }
128
+
129
+ pushItem(qItem) {
130
+ this.xqItems.push(qItem);
131
+ }
132
+
133
+ popItem() {
134
+ return this.xqItems.pop();
135
+ }
136
+
137
+ _isAnonShell(value) {
138
+ let sig;
139
+ if ((sig = defaults.ANON_SHELL_OPT_SIG.find(x => value.startsWith(x)))) {
140
+ return { sig, opt: true };
141
+ }
142
+ if ((sig = defaults.ANON_SHELL_SIG.find(x => value.startsWith(x)))) {
143
+ return { sig, opt: false };
144
+ }
145
+ return false;
146
+ }
147
+
148
+ _parseShellFlags(flags, value) {
149
+ const unknowns = [];
150
+
151
+ flags = flags.reduce((a, o) => {
152
+ const f = o.trim().toLowerCase();
153
+ if (defaults.SHELL_FLAGS.indexOf(f) < 0) {
154
+ unknowns.push(o);
155
+ } else {
156
+ a[f] = true;
157
+ }
158
+ return a;
159
+ }, {});
160
+
161
+ if (unknowns.length) {
162
+ throw new Error(`Unknown flag ${unknowns.join(",")} in shell task: ${value}`);
163
+ }
164
+
165
+ return flags;
166
+ }
167
+
168
+ _parseAnonShell(value) {
169
+ if (typeof value !== "string") return value;
170
+
171
+ let cmd = value;
172
+ let flags = {};
173
+ let error;
174
+ const anon = this._isAnonShell(value);
175
+ if (anon) {
176
+ const { sig, opt } = anon;
177
+ // support options like ~(tty,spawn,sync)$
178
+
179
+ if (opt) {
180
+ let ix;
181
+ const closeSig = defaults.ANON_SHELL_OPT_CLOSE_SIG.find(x => {
182
+ ix = value.indexOf(x, sig.length);
183
+ return ix > sig.length;
184
+ });
185
+
186
+ if (closeSig) {
187
+ const so = value.substring(sig.length, ix);
188
+ try {
189
+ flags = this._parseShellFlags(so.split(","), value);
190
+ } catch (err) {
191
+ error = err;
192
+ }
193
+ cmd = value.substr(ix + closeSig.length);
194
+ } else {
195
+ error = new Error(
196
+ `Missing ${defaults.ANON_SHELL_OPT_CLOSE_SIG[0]} in shell task: ${value}`
197
+ );
198
+ }
199
+ } else {
200
+ cmd = value.substr(sig.length);
201
+ }
202
+ }
203
+
204
+ return { flags, cmd, error };
205
+ }
206
+
207
+ _isStrArray(value) {
208
+ return value.startsWith(defaults.STR_ARRAY_SIG);
209
+ }
210
+
211
+ _parseStrArray(value) {
212
+ if (this._isStrArray(value)) {
213
+ return parseArray(value.substr(defaults.STR_ARRAY_SIG.length - 1));
214
+ }
215
+ return undefined;
216
+ }
217
+
218
+ _processLookup(qItem) {
219
+ this._emit("execute", {
220
+ type: "lookup",
221
+ qItem
222
+ });
223
+ // lookup is sync; should not insert mark for it
224
+ try {
225
+ const found = qItem.lookup(this._tasks);
226
+ this.pushItem(qItem);
227
+ this.next(null, qItem.id);
228
+ if (found.search) {
229
+ this._xrun.emit("search", { qItem, found });
230
+ qItem.setNamespace(found.ns);
231
+ }
232
+ } catch (error) {
233
+ if (error.optional) {
234
+ this._xrun.emit("not-found", error);
235
+ this.next();
236
+ } else {
237
+ this.next(error, qItem.id);
238
+ }
239
+ }
240
+ }
241
+
242
+ _processDep(qItem, dep) {
243
+ this._emit("dep", {
244
+ qItem
245
+ });
246
+ qItem.xqDep = true; // item's dep has been processed
247
+ this.pushItem(qItem);
248
+ const di = this._createQItem(
249
+ {
250
+ name: qItem.name,
251
+ value: { top: true, depDee: qItem, item: { task: dep } }
252
+ },
253
+ this._xrun.xqTree.parent(qItem)
254
+ );
255
+
256
+ this.pushItem(di);
257
+
258
+ this.next(null, qItem.id);
259
+ }
260
+
261
+ _processTaskObject(qItem) {
262
+ const value = qItem.value();
263
+ const stage = qItem.stage || "task";
264
+ const itemTask = value.item[stage] || value.item;
265
+ const type = itemTask.constructor.name;
266
+ if (type === "Array") {
267
+ return this._processArray(qItem, itemTask, value.top);
268
+ } else if (type === "Function" || type === "AsyncFunction") {
269
+ return this._functionXer(qItem, itemTask);
270
+ } else if (type === "String") {
271
+ const parsedArray = this._parseStrArray(itemTask);
272
+ if (parsedArray) return this._processArray(qItem, parsedArray, value.top);
273
+ return this._shellXer(qItem, itemTask);
274
+ } else if (type === "XTaskSpec") {
275
+ if (itemTask.type === "exec") {
276
+ return this._shellXer(qItem, itemTask);
277
+ } else if (itemTask.type === "env") {
278
+ return this._envXer(qItem, itemTask);
279
+ }
280
+ this._xrun.fail(
281
+ new Error(`Unable to process XTaskSpec type ${itemTask.type} for task ${qItem.name}`)
282
+ );
283
+ } else {
284
+ this._xrun.fail(new Error(`Task ${qItem.name} has unrecognize task value type ${type}`));
285
+ }
286
+
287
+ return this.next(null, qItem.id);
288
+ }
289
+
290
+ _processArray(qItem, tasks, top) {
291
+ // check first element for concurrent signature
292
+ // top level task arrays are automatically serial
293
+ // or check first element for serial signature
294
+ if (isConcurrent(tasks) || (!top && !isSerial(tasks))) {
295
+ this._concurrentArrayXer(qItem, tasks);
296
+ } else {
297
+ this._serialArrayXer(qItem, tasks);
298
+ }
299
+ }
300
+
301
+ _serialArrayXer(qItem, tasks) {
302
+ if (isSerial(tasks)) {
303
+ tasks = tasks.slice(1);
304
+ }
305
+
306
+ this._emit("execute", {
307
+ type: "serial-arr",
308
+ qItem,
309
+ array: tasks
310
+ });
311
+
312
+ this.pushMarkItem(qItem);
313
+ this.xqItems = this.xqItems.concat(
314
+ tasks
315
+ .map(value => {
316
+ return this._createQItem(
317
+ this._resolveValueToQItemOptions(qItem.ns, `${qItem.name}.S`, value, "serial_child"),
318
+ qItem
319
+ );
320
+ })
321
+ .reverse()
322
+ );
323
+ this.next(null, qItem.id);
324
+ }
325
+
326
+ _resolveValueToQItemOptions(ns, name, value, type) {
327
+ if (typeof value === "string" && !this._isAnonShell(value)) {
328
+ name = value;
329
+ value = undefined;
330
+ }
331
+
332
+ const anon = typeof value === "function";
333
+ return {
334
+ ns,
335
+ name,
336
+ value,
337
+ anon,
338
+ type
339
+ };
340
+ }
341
+
342
+ _concurrentArrayXer(qItem, tasks) {
343
+ if (isConcurrent(tasks)) {
344
+ tasks = tasks.slice(1);
345
+ }
346
+
347
+ this._emit("execute", {
348
+ type: "concurrent-arr",
349
+ qItem,
350
+ array: tasks
351
+ });
352
+
353
+ this.pushMarkItem(qItem);
354
+ Insync.parallel(
355
+ tasks.map(value => cb => {
356
+ this._emit("spawn-async", { name: qItem.name });
357
+ const xqtor = new XQtor({
358
+ tasks: this._tasks,
359
+ xrun: this._xrun,
360
+ done: () => {
361
+ this._emit("done-async");
362
+ process.nextTick(() => cb());
363
+ }
364
+ });
365
+ const options = this._resolveValueToQItemOptions(
366
+ qItem.ns,
367
+ `${qItem.name}.C`,
368
+ value,
369
+ "concurrent_child"
370
+ );
371
+ const item = this._createQItem(options, qItem);
372
+ if (options.value === undefined) {
373
+ xqtor._processLookup(item);
374
+ } else {
375
+ xqtor.pushItem(item);
376
+ xqtor.next();
377
+ }
378
+ }),
379
+ () => this.next()
380
+ );
381
+ }
382
+
383
+ _envXer(qItem, cmdVal) {
384
+ this._emit("execute", {
385
+ type: "env",
386
+ qItem,
387
+ cmdVal
388
+ });
389
+
390
+ updateEnv(cmdVal.options.env, process.env, cmdVal.options.override);
391
+ return this.next();
392
+ }
393
+
394
+ _shellXer(qItem, cmdVal, anon) {
395
+ cmdVal = this._parseAnonShell(cmdVal);
396
+
397
+ if (cmdVal.error) {
398
+ setTimeout(() => this.next(cmdVal.error, qItem.id), 0);
399
+ return;
400
+ }
401
+
402
+ const { cmd } = cmdVal;
403
+ const xrunOptions = Object.assign({ delayRunMs: 0 }, cmdVal.xrun || cmdVal.xclap);
404
+ const options = Object.assign({}, cmdVal.options);
405
+
406
+ let { flags } = cmdVal;
407
+
408
+ if (typeof flags === "string") {
409
+ flags = this._parseShellFlags(flags.split(","), `XTaskSpec "${flags}"`);
410
+ } else if (Array.isArray(flags)) {
411
+ flags = this._parseShellFlags(flags);
412
+ }
413
+
414
+ const item = qItem.value().item || {};
415
+
416
+ Object.assign(flags, item.flags);
417
+
418
+ // Access CLI context through runner
419
+ const cliContext = this._xrun.getCliContext();
420
+ const itemArgv = qItem.argv;
421
+ const cliArgv = cliContext.getTaskArgv(qItem.name);
422
+ const remainingArgv =
423
+ (cliContext.isLastTask(qItem.name) && cliContext.getRemainingArgs()) || [];
424
+
425
+ // Build command with task-specific argv
426
+ const cmd2 = []
427
+ .concat(itemArgv.slice(1), cliArgv.slice(1), remainingArgv)
428
+ .map(x => (x && x.includes(" ") ? `"${x}"` : x))
429
+ .join(" ");
430
+
431
+ setTimeout(() => {
432
+ const execData = {
433
+ type: "shell",
434
+ anon,
435
+ qItem,
436
+ cmd: cmd + ((cmd2 && " " + cmd2) || ""),
437
+ cmd2,
438
+ flags,
439
+ cmdVal,
440
+ options,
441
+ item
442
+ };
443
+
444
+ this._emit("execute", execData);
445
+ this._doShellXer(execData);
446
+ }, xrunOptions.delayRunMs);
447
+ }
448
+
449
+ _doShellXer({ /*anon,*/ qItem, cmd, flags, /*cmdVal,*/ options, item }) {
450
+ const itemOptions = Object.assign({}, item.options);
451
+
452
+ this.pushMarkItem(qItem);
453
+ const env = Object.assign(
454
+ flags.noenv ? {} : Object.assign({}, process.env),
455
+ itemOptions.env,
456
+ options.env
457
+ );
458
+
459
+ if (qItem.err) {
460
+ env.XCLAP_ERR = env.XRUN_ERR = qItem.err.message || "true";
461
+ }
462
+
463
+ if (this._xrun.failed || qItem.err) {
464
+ env.XCLAP_FAILED = env.XRUN_FAILED = "true";
465
+ }
466
+
467
+ let child;
468
+
469
+ const watch = { finish: false };
470
+ const done = err => {
471
+ // even if there's error, but if it's due to child being terminated with
472
+ // SIGTERM, then treat that as a normal exit.
473
+ if (this._isChildSigTerm(err, child) || this._isChildTerminated(child)) {
474
+ err = null;
475
+ }
476
+ //
477
+ // A spawned child can report both `error` and `close`, so `done` can be called twice.
478
+ // Guarding it is the point; reproducing that race on demand in a test is not something
479
+ // this suite can do deterministically.
480
+ //
481
+ /* istanbul ignore next */
482
+ if (watch.finish) return 0;
483
+ watch.finish = true;
484
+ return this.next(err, qItem.id);
485
+ };
486
+
487
+ let { tty, spawn, sync, npm } = flags;
488
+ if (npm) {
489
+ tty = spawn = true;
490
+ }
491
+
492
+ // Lazy require unwrap-npm-cmd to avoid requiring it at module load time
493
+ // This ensures it's built by fynpo bootstrap before it's required
494
+ const { unwrapNpmCmd } = lazyRequire("unwrap-npm-cmd");
495
+ const cmd2 = unwrapNpmCmd(cmd, { path: env.PATH });
496
+
497
+ if (tty || spawn) {
498
+ const spawnOpts = { shell: true };
499
+ Object.assign(spawnOpts, itemOptions, options, { env });
500
+ if (tty) spawnOpts.stdio = "inherit";
501
+
502
+ if (sync) {
503
+ child = childProc.spawnSync(cmd2, spawnOpts);
504
+ if (child.error) {
505
+ done(child.error);
506
+ } else if (child.status === 0) {
507
+ done();
508
+ } else {
509
+ done(new Error(`cmd "${cmd}" exit code ${child.status}`));
510
+ }
511
+ } else {
512
+ child = childProc.spawn(cmd2, spawnOpts);
513
+ child.on("close", (code, signal) => {
514
+ if (code === 0 || signal === "SIGTERM") {
515
+ return done();
516
+ }
517
+ return done(new Error(`cmd "${cmd}" exit code ${code}`));
518
+ });
519
+ }
520
+ } else {
521
+ child = exec(Object.assign({ silent: false }, itemOptions, options, { env }), cmd2, done);
522
+ }
523
+
524
+ if (!watch.finish) {
525
+ this._handleChildTask(child, cmd);
526
+ watch.cancel = () => {
527
+ this._xrun.killChildProcess(child);
528
+ done();
529
+ };
530
+ this._watchFailure(watch);
531
+ }
532
+ }
533
+
534
+ _handleChildTask(child, cmd) {
535
+ if (child && child instanceof childProc.ChildProcess) {
536
+ const sym = Symbol(cmd);
537
+ this._xrun.addTaskChild(child, sym);
538
+ child.on("exit", () => {
539
+ this._xrun.removeTaskChild(child, sym);
540
+ });
541
+ return child;
542
+ }
543
+ }
544
+
545
+ _processMoreFromFn(qItem, value, callback) {
546
+ let tof = typeof value;
547
+
548
+ if (tof === "string") {
549
+ const parsedArray = this._parseStrArray(value);
550
+ if (parsedArray) value = parsedArray;
551
+ tof = typeof value;
552
+ }
553
+
554
+ if (
555
+ !value ||
556
+ (tof !== "string" &&
557
+ tof !== "function" &&
558
+ !Array.isArray(value) &&
559
+ value.constructor.name !== "XTaskSpec" &&
560
+ value !== defaults.STOP_SYM)
561
+ ) {
562
+ return;
563
+ }
564
+
565
+ const qi = this._createQItem(
566
+ this._resolveValueToQItemOptions(qItem.ns, `${qItem.name}.fR`, value, "func_returned_child"),
567
+ qItem
568
+ );
569
+ if (callback) {
570
+ assert(
571
+ typeof callback === "function",
572
+ `${qItem.name} callback from function calling run is not a function`
573
+ );
574
+ this.pushMarkItem(qItem, callback);
575
+ }
576
+ this.pushItem(qi);
577
+ }
578
+
579
+ _isChildSigTerm(err, child) {
580
+ return err && err.code === null && child && child.signalCode === "SIGTERM";
581
+ }
582
+
583
+ _isChildTerminated(child) {
584
+ return child && child[defaults.STOP_SYM];
585
+ }
586
+
587
+ _handleStream(val) {
588
+ if (!isReadableStream(val)) {
589
+ return val;
590
+ }
591
+
592
+ return new Promise((resolve, reject) => {
593
+ const handle = err => {
594
+ val.removeListener("end", handle);
595
+ val.removeListener("error", handle);
596
+ return err ? reject(err) : resolve();
597
+ };
598
+
599
+ val.once("close", handle);
600
+ val.once("error", handle);
601
+ });
602
+ }
603
+
604
+ _functionXer(qItem, fn) {
605
+ this._emit("execute", {
606
+ type: "function",
607
+ anon: qItem.anon,
608
+ qItem
609
+ });
610
+
611
+ const watch = { finish: false, qItem };
612
+ const done = (err, value) => {
613
+ if (watch.finish) return;
614
+ watch.finish = true;
615
+ if (value) {
616
+ this._processMoreFromFn(qItem, value);
617
+ }
618
+ this.next(err, qItem.id);
619
+ };
620
+
621
+ watch.cancel = done;
622
+
623
+ try {
624
+ this.pushMarkItem(qItem);
625
+ const run = (value, cb) => this._processMoreFromFn(qItem, value, cb);
626
+ const task = qItem.value();
627
+
628
+ // Access CLI context through runner
629
+ const cliContext = this._xrun.getCliContext();
630
+ let cliCmd = cliContext.getTaskCommand(qItem.name);
631
+
632
+ const cliParser = task.item?.cliParser || task.cliParser || {};
633
+ const itemArgv = qItem.argv;
634
+ const cliArgv = cliCmd.argv || [];
635
+
636
+ if (itemArgv.length > 1 || (cliParser && cliArgv.length > 1)) {
637
+ const argv = [qItem.name].concat(itemArgv.slice(1), cliArgv.slice(1));
638
+ const config = {
639
+ name: qItem.name,
640
+ allowUnknownOption:
641
+ cliParser.allowUnknownOption !== undefined ? cliParser.allowUnknownOption : true,
642
+ allowUnknownCommand: false,
643
+ noDefaultHandlers: true
644
+ };
645
+
646
+ const argp = new NixClap(config)
647
+ .init(cliParser.options || {}, cliParser.commands || {})
648
+ .parse(argv, 1);
649
+
650
+ if (argp.errorNodes.length > 0) {
651
+ const unknownOptions = argp.errorNodes.reduce((acc, node) => {
652
+ acc.push(
653
+ ...node.errors
654
+ .filter(e => e.message.includes("unknown CLI option"))
655
+ .map(e => e.data.name)
656
+ );
657
+ return acc;
658
+ }, []);
659
+
660
+ if (unknownOptions.length > 0) {
661
+ return done(
662
+ new Error(`Unknown options for task ${qItem.name}: ${unknownOptions.join(", ")}`)
663
+ );
664
+ }
665
+ }
666
+
667
+ cliCmd = argp.command;
668
+ }
669
+
670
+ const argv = cliCmd.argv || [];
671
+
672
+ const context = {
673
+ run,
674
+ argv,
675
+ cliCmd,
676
+ argp: cliCmd.jsonMeta,
677
+ args: cliCmd.argsList,
678
+ argOpts: cliCmd.opts,
679
+ err: qItem.err,
680
+ failed: this._xrun.failed
681
+ };
682
+
683
+ if (fn.length > 1) {
684
+ // takes two params, pass in context and done callback
685
+ this._watchFailure(watch);
686
+ return fn.call(context, context, done);
687
+ }
688
+
689
+ if (fn.constructor.name !== "AsyncFunction" && fn.length > 0) {
690
+ // non async function that takes 1 param, check if it wants the context
691
+ // or a callback.
692
+ const fnStr = fn.toString().replace(/\s/g, "");
693
+ const matchParam = x => {
694
+ return (
695
+ fnStr.startsWith(`${x}=>`) ||
696
+ fnStr.match(new RegExp(`\\(${x}[^\\)]*\\)=>`)) ||
697
+ fnStr.match(new RegExp(`function[^\\(]*\\(${x}[^\\)]*\\){`)) ||
698
+ fnStr.match(new RegExp(`[^(]*\\(${x}[^\\)]*\\){`))
699
+ );
700
+ };
701
+
702
+ const takeContext = ["ctx", "context"].find(matchParam);
703
+
704
+ if (!takeContext) {
705
+ // non async task function takes callback, expect async behavior
706
+ this._watchFailure(watch);
707
+ return fn.call(context, done);
708
+ }
709
+
710
+ // function takes context as first param
711
+ return done(null, fn.call(context, context));
712
+ }
713
+
714
+ const x = this._handleStream(fn.call(context, context));
715
+
716
+ // handling a exec/spawn child from a function
717
+ const cmd = `ChildProcess of function task ${qItem.name}`;
718
+ const child = this._handleChildTask(x && x.child, cmd) || this._handleChildTask(x, cmd);
719
+
720
+ if (!x || typeof x.then !== "function") {
721
+ // assume no async behavior in task function
722
+ return done(null, x);
723
+ }
724
+
725
+ // x.then is a function, assume task function returned a promise
726
+ this._watchFailure(watch);
727
+ return x.then(
728
+ v => done(null, v),
729
+ err => {
730
+ // treat child process that got SIGTERM as exited normally
731
+ if (this._isChildSigTerm(err, child)) {
732
+ done();
733
+ } else {
734
+ done(err);
735
+ }
736
+ }
737
+ );
738
+ } catch (err) {
739
+ return done(err);
740
+ }
741
+ }
742
+
743
+ _watchFailure(watch) {
744
+ if (this._xrun.stopOnError !== "full") return undefined;
745
+
746
+ return setTimeout(() => {
747
+ if (watch.finish) return 0;
748
+ if (this._xrun.failed) {
749
+ this._xrun.emit("fail-cancel", watch);
750
+ return watch.cancel();
751
+ }
752
+ return this._watchFailure(watch);
753
+ }, 50).unref();
754
+ }
755
+
756
+ _processMark(qItem) {
757
+ const value = qItem.value();
758
+ const xqItem = this._xrun.xqTree.item(value.xqId);
759
+ const endTime = Date.now();
760
+ const data = {
761
+ qItem,
762
+ xqItem,
763
+ elapse: endTime - value.startTime,
764
+ startTime: value.startTime,
765
+ endTime,
766
+ hrStartTime: value.hrStartTime,
767
+ hrElapse: process.hrtime(value.hrStartTime)
768
+ };
769
+ if (value.callback) {
770
+ value.callback(this._xrun.failed, data);
771
+ } else {
772
+ this._emit("done-item", data);
773
+ }
774
+
775
+ const xqValue = xqItem.value();
776
+
777
+ if (xqValue.item && xqValue.item[STAGE_FINALLY] && !xqItem.isFinally) {
778
+ xqItem.stage = STAGE_FINALLY;
779
+ xqItem.isFinally = true;
780
+ this.pushItem(xqItem);
781
+ }
782
+
783
+ this.next();
784
+ }
785
+
786
+ _makeXqMarkItem(qItem, callback) {
787
+ // do not add marking item to tree
788
+ const mark = new XQItem({
789
+ name: `mark_${qItem.name}`,
790
+ value: {
791
+ startTime: Date.now(),
792
+ hrStartTime: process.hrtime(),
793
+ callback,
794
+ xqId: qItem.id
795
+ }
796
+ });
797
+ mark.mark = true;
798
+ return mark;
799
+ }
800
+
801
+ _emit(event, data) {
802
+ try {
803
+ this._xrun.emit(event, data);
804
+ } catch (err) {
805
+ this._xrun.fail(err);
806
+ }
807
+ }
808
+
809
+ _createQItem(options, parent) {
810
+ return this._xrun.xqTree.create(options, parent);
811
+ }
812
+ }
813
+
814
+ export default XQtor;