rman 2.7.0 → 2.9.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/builtins/platforms/node/node-manifest.provider.d.ts +10 -0
- package/builtins/platforms/node/node-manifest.provider.js +32 -5
- package/builtins/platforms/node/services/clean.service.js +104 -23
- package/cli.js +31 -13
- package/constants.js +1 -1
- package/core/classes/repository.d.ts +48 -0
- package/core/classes/repository.js +139 -8
- package/core/command-builder.d.ts +9 -1
- package/core/command-builder.js +24 -11
- package/core/config/config-paths.js +24 -0
- package/core/interfaces/custom-command.d.ts +39 -0
- package/core/interfaces/run-step.d.ts +50 -3
- package/index.d.ts +7 -1
- package/index.js +11 -0
- package/interfaces/rman-config.interface.d.ts +21 -3
- package/package.json +1 -1
- package/services/run.service.d.ts +131 -2
- package/services/run.service.js +462 -148
- package/services/version.service.d.ts +2 -0
- package/services/version.service.js +7 -2
- package/utils/progress-panel.js +98 -16
- package/utils/run-bin.d.ts +4 -0
- package/utils/run-bin.js +14 -2
- package/utils/run-options.d.ts +44 -0
- package/utils/run-options.js +55 -0
package/services/run.service.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
1
2
|
import os from 'node:os';
|
|
2
3
|
import { inspect } from 'node:util';
|
|
3
4
|
import colors from 'ansi-colors';
|
|
@@ -41,24 +42,133 @@ export class RunService extends Service {
|
|
|
41
42
|
const status = await repository.listStatus({ hash: changedSince });
|
|
42
43
|
packages = packages.filter(p => status[p.name] !== 'clean');
|
|
43
44
|
}
|
|
44
|
-
const concurrency = options.parallel
|
|
45
|
-
? 1
|
|
46
|
-
: typeof options.parallel === 'number'
|
|
47
|
-
? options.parallel
|
|
48
|
-
: options.parallel === true
|
|
49
|
-
? os.cpus().length
|
|
50
|
-
: resolveNumber(undefined, repository.rootPackage, script, 'concurrency', os.cpus().length);
|
|
45
|
+
const concurrency = resolveConcurrency(options.parallel, resolveNumber(undefined, repository.rootPackage, script, 'concurrency', os.cpus().length));
|
|
51
46
|
const progress = resolveBool(options.progress, repository.rootPackage, script, 'progress', true);
|
|
52
47
|
const panel = new ProgressPanel(`RUN ${script}`, !!process.stdout.isTTY && progress);
|
|
48
|
+
/** Shared across all `if: changed[ = hash]` evaluations so the same reference is only `git`-queried once. */
|
|
49
|
+
const ifStatusCache = new Map();
|
|
50
|
+
/** Repo-wide bookend: root's own pre/post hooks run once each, exclusively, around every package
|
|
51
|
+
* (unless root itself opts out via `run.<script>.skip`, fails its own `run.<script>.if`, or the
|
|
52
|
+
* run is scoped to a single package by `cwdScope` - a repo-wide bookend has no place there).
|
|
53
|
+
*
|
|
54
|
+
* Only in a monorepo. Without one the root *is* the single package, already in the loop below
|
|
55
|
+
* with the same hooks and the same directory - a bookend would simply run each of them a
|
|
56
|
+
* second time. */
|
|
57
|
+
/** Short-circuited deliberately: a root already out of the run for a structural reason must not
|
|
58
|
+
* have its `if` evaluated, now that evaluating one can mean calling the repository's own code. */
|
|
59
|
+
const rootSkipped = !!cwdScope ||
|
|
60
|
+
!repository.monorepo ||
|
|
61
|
+
rootCfg.skip === true ||
|
|
62
|
+
!(await passesIf(repository, repository.rootPackage, rootCfg.if, repository.dirname, ifStatusCache));
|
|
63
|
+
const rootSteps = rootSkipped ? [] : getScriptSteps(repository.rootPackage, script);
|
|
64
|
+
/** Filtered on the slot, not on `'pre' + script`: the step labels are `before`/`exec`/`after`
|
|
65
|
+
* now - the same words the config uses - rather than npm's `pre<script>` naming, which moved
|
|
66
|
+
* out with the package.json source. */
|
|
67
|
+
const bookend = (slot, name) => {
|
|
68
|
+
const steps = rootSteps.filter(s => s.name === slot);
|
|
69
|
+
if (!steps.length)
|
|
70
|
+
return undefined;
|
|
71
|
+
return {
|
|
72
|
+
pkg: repository.rootPackage,
|
|
73
|
+
name,
|
|
74
|
+
label: 'root',
|
|
75
|
+
steps,
|
|
76
|
+
cwd: repository.dirname,
|
|
77
|
+
topo: false,
|
|
78
|
+
bail: resolveBail(options.bail, repository.rootPackage, script, true),
|
|
79
|
+
logLevel: resolveLogLevel(options.logLevel, repository.rootPackage, script, logLevelDefault),
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
const before = bookend('before', 'root (pre)');
|
|
83
|
+
const after = bookend('after', 'root (post)');
|
|
84
|
+
const plans = [];
|
|
85
|
+
for (const pkg of packages) {
|
|
86
|
+
const pkgCfg = RunService.getConfig(pkg, script);
|
|
87
|
+
if (pkgCfg.skip === true)
|
|
88
|
+
continue;
|
|
89
|
+
if (!(await passesIf(repository, pkg, pkgCfg.if, pkg.dirname, ifStatusCache)))
|
|
90
|
+
continue;
|
|
91
|
+
const steps = getScriptSteps(pkg, script);
|
|
92
|
+
if (!steps.length)
|
|
93
|
+
continue;
|
|
94
|
+
plans.push({
|
|
95
|
+
pkg,
|
|
96
|
+
name: pkg.name,
|
|
97
|
+
label: pkg.name,
|
|
98
|
+
steps,
|
|
99
|
+
cwd: pkg.dirname,
|
|
100
|
+
topo: resolveBool(options.topo, pkg, script, 'topo', topo),
|
|
101
|
+
bail: resolveBail(options.bail, pkg, script, true),
|
|
102
|
+
logLevel: resolveLogLevel(options.logLevel, pkg, script, logLevelDefault),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
if (!plans.length && !before && !after) {
|
|
106
|
+
/**
|
|
107
|
+
* Two different nothings, and only one of them is fine.
|
|
108
|
+
*
|
|
109
|
+
* Nobody in the repository defines this script at all: the name is a mistake - a typo, or a
|
|
110
|
+
* script that used to exist - and `npm run` fails on exactly this. Staying silent is how
|
|
111
|
+
* `rman run qc` sat in a CI pipeline for months reporting success while running nothing, with
|
|
112
|
+
* `qc` defined only on the root (whose own scripts a monorepo never runs, only its
|
|
113
|
+
* `pre`/`post` bookends).
|
|
114
|
+
*
|
|
115
|
+
* Everything was filtered out instead - `--scope`, `--changed`, `run.<script>.skip`, an
|
|
116
|
+
* `if:` that didn't match: zero is the correct answer to what was asked, and asking "build
|
|
117
|
+
* only what changed" when nothing changed must not fail a pipeline.
|
|
118
|
+
*/
|
|
119
|
+
/** `getPackages()` and nothing else - in a monorepo that excludes the root, which is the
|
|
120
|
+
* point: the root contributes only `pre`/`post` bookends, never the script itself, so a
|
|
121
|
+
* `qc` defined *only* there is exactly the mistake above rather than an excuse for it. (And
|
|
122
|
+
* had the root contributed a bookend, there would be a plan.) In a single-package
|
|
123
|
+
* repository the root *is* the one package, and is covered. */
|
|
124
|
+
const definedSomewhere = repository.getPackages().some(pkg => getScriptSteps(pkg, script).length > 0);
|
|
125
|
+
if (definedSomewhere) {
|
|
126
|
+
console.log(colors.gray(`Nothing to run - every package was filtered out of "${script}".`));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const message = `No package defines a "${script}" script.`;
|
|
130
|
+
console.log(colors.red(message));
|
|
131
|
+
const err = new Error(message);
|
|
132
|
+
err.logged = true;
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
await this.schedule({ panel, concurrency, commandName, failureLabel: `"${script}"`, before, plans, after });
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* **The scheduler, shared by every driver that runs work per package.**
|
|
139
|
+
*
|
|
140
|
+
* `runScript` is one driver and `forEachPackage` is the other; both hand over a plan per package
|
|
141
|
+
* and get the same machinery, which is the point of this being a method rather than a shape each
|
|
142
|
+
* one builds for itself. Four things have had to be fixed in it this month alone - the status
|
|
143
|
+
* region handover, the depth-counted console patch, the per-row command, the failed list - and a
|
|
144
|
+
* second copy would have needed each fix twice, with nothing reporting the one that was missed.
|
|
145
|
+
*
|
|
146
|
+
* Throws when any package failed, with `logged` set: the panel has already printed the recap.
|
|
147
|
+
*/
|
|
148
|
+
async schedule(options) {
|
|
149
|
+
const { panel, concurrency, commandName } = options;
|
|
53
150
|
/** Set once the aggregate Task exists, so a package's own failure can trigger a manual
|
|
54
151
|
* abort using *its own* resolved bail setting (see `runSteps` below) - power-tasks' own
|
|
55
152
|
* `bail` is a single blanket policy for the whole batch, it can't vary per package. */
|
|
56
153
|
let rootTask;
|
|
57
|
-
const runSteps = async (ctx,
|
|
58
|
-
|
|
59
|
-
|
|
154
|
+
const runSteps = async (ctx, plan, steps,
|
|
155
|
+
/**
|
|
156
|
+
* Which slice of the package's steps this call runs, when a `topo` step split it in two.
|
|
157
|
+
* `from` keeps `stepIndex` counting across the pair, so one panel row spans both tasks and
|
|
158
|
+
* still reports `(4/9)` rather than restarting; `more` says another call is coming, so this
|
|
159
|
+
* one must not mark the package finished.
|
|
160
|
+
*/
|
|
161
|
+
part) => {
|
|
162
|
+
const { pkg, cwd, bail: pkgBail, logLevel: pkgLogLevel, label: pkgLabel } = plan;
|
|
163
|
+
const from = part?.from ?? 0;
|
|
164
|
+
/** Only the first part starts the clock: the row's elapsed time is the package's, and a
|
|
165
|
+
* package waiting at a `topo` barrier is waiting as part of its own run. */
|
|
166
|
+
if (!from) {
|
|
167
|
+
ctx.status = 'running';
|
|
168
|
+
ctx.startedAt = Date.now();
|
|
169
|
+
}
|
|
60
170
|
try {
|
|
61
|
-
for (let i =
|
|
171
|
+
for (let i = from; i < steps.length; i++) {
|
|
62
172
|
const step = steps[i];
|
|
63
173
|
ctx.currentStep = step.name;
|
|
64
174
|
/** The command for a shell step, the function's own name for a JS one - and a bare
|
|
@@ -119,6 +229,15 @@ export class RunService extends Service {
|
|
|
119
229
|
throw stepError;
|
|
120
230
|
}
|
|
121
231
|
}
|
|
232
|
+
/** Another part is coming, so the package is not done - and it is about to sit at the
|
|
233
|
+
* barrier, which the row says out loud rather than leaving the last step's command on
|
|
234
|
+
* screen as though it were still running. (The setter clears the row's last output line,
|
|
235
|
+
* which would otherwise read as that step's.) */
|
|
236
|
+
if (part?.more) {
|
|
237
|
+
ctx.currentStep = 'waiting';
|
|
238
|
+
ctx.currentCommand = 'waiting for dependencies';
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
122
241
|
ctx.status = 'success';
|
|
123
242
|
}
|
|
124
243
|
catch (e) {
|
|
@@ -128,114 +247,79 @@ export class RunService extends Service {
|
|
|
128
247
|
throw e;
|
|
129
248
|
}
|
|
130
249
|
finally {
|
|
131
|
-
ctx.
|
|
250
|
+
if (!part?.more || ctx.status === 'failed')
|
|
251
|
+
ctx.finishedAt = Date.now();
|
|
132
252
|
}
|
|
133
253
|
};
|
|
134
254
|
const children = [];
|
|
135
|
-
/**
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
* second time. */
|
|
144
|
-
/** Short-circuited deliberately: a root already out of the run for a structural reason must not
|
|
145
|
-
* have its `if` evaluated, now that evaluating one can mean calling the repository's own code. */
|
|
146
|
-
const rootSkipped = !!cwdScope ||
|
|
147
|
-
!repository.monorepo ||
|
|
148
|
-
rootCfg.skip === true ||
|
|
149
|
-
!(await passesIf(repository, repository.rootPackage, rootCfg.if, repository.dirname, ifStatusCache));
|
|
150
|
-
const rootSteps = rootSkipped ? [] : getScriptSteps(repository.rootPackage, script);
|
|
151
|
-
/** Filtered on the slot, not on `'pre' + script`: the step labels are `before`/`exec`/`after`
|
|
152
|
-
* now - the same words the config uses - rather than npm's `pre<script>` naming, which moved
|
|
153
|
-
* out with the package.json source. */
|
|
154
|
-
const rootPre = rootSteps.filter(s => s.name === 'before');
|
|
155
|
-
const rootPost = rootSteps.filter(s => s.name === 'after');
|
|
156
|
-
const rootPreName = 'root (pre)';
|
|
157
|
-
const rootPostName = 'root (post)';
|
|
158
|
-
if (rootPre.length) {
|
|
159
|
-
const ctx = panel.addItem(rootPreName, rootPre.length);
|
|
160
|
-
const pkgBail = resolveBail(options.bail, repository.rootPackage, script, true);
|
|
161
|
-
const pkgLogLevel = resolveLogLevel(options.logLevel, repository.rootPackage, script, logLevelDefault);
|
|
162
|
-
children.push(new Task(() => runSteps(ctx, repository.rootPackage, 'root', rootPre, repository.dirname, pkgBail, pkgLogLevel), {
|
|
163
|
-
name: ctx.name,
|
|
164
|
-
exclusive: true,
|
|
165
|
-
}));
|
|
255
|
+
/** Row and task names are the plan's, and a package's is its package name - which is what lets
|
|
256
|
+
* the dependency edges below be handed to power-tasks as names. Mapped rather than assumed, so
|
|
257
|
+
* a driver labelling its rows differently does not silently lose its ordering. */
|
|
258
|
+
const taskOf = new Map(options.plans.map(plan => [plan.pkg.name, plan.name]));
|
|
259
|
+
if (options.before) {
|
|
260
|
+
const plan = options.before;
|
|
261
|
+
const ctx = panel.addItem(plan.name, plan.steps.length);
|
|
262
|
+
children.push(new Task(() => runSteps(ctx, plan, plan.steps), { name: plan.name, exclusive: true }));
|
|
166
263
|
}
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
if (pkgCfg.skip === true)
|
|
171
|
-
continue;
|
|
172
|
-
if (!(await passesIf(repository, pkg, pkgCfg.if, pkg.dirname, ifStatusCache)))
|
|
173
|
-
continue;
|
|
174
|
-
const steps = getScriptSteps(pkg, script);
|
|
175
|
-
if (steps.length)
|
|
176
|
-
stepsByPackage.set(pkg.name, steps);
|
|
177
|
-
}
|
|
178
|
-
for (const pkg of packages) {
|
|
179
|
-
const steps = stepsByPackage.get(pkg.name);
|
|
180
|
-
if (!steps)
|
|
181
|
-
continue;
|
|
182
|
-
const ctx = panel.addItem(pkg.name, steps.length);
|
|
183
|
-
const pkgTopo = resolveBool(options.topo, pkg, script, 'topo', topo);
|
|
184
|
-
const pkgBail = resolveBail(options.bail, pkg, script, true);
|
|
185
|
-
const pkgLogLevel = resolveLogLevel(options.logLevel, pkg, script, logLevelDefault);
|
|
264
|
+
for (const plan of options.plans) {
|
|
265
|
+
const ctx = panel.addItem(plan.name, plan.steps.length);
|
|
266
|
+
const steps = plan.steps;
|
|
186
267
|
/** power-tasks identifies a task by its name string, so the graph is handed over as names -
|
|
187
268
|
* the references are what rman reasons with, the names are what the scheduler wants. */
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
children.push(new Task(() => runSteps(ctx, pkg, pkg.name, steps, pkg.dirname, pkgBail, pkgLogLevel), {
|
|
192
|
-
name: ctx.name,
|
|
193
|
-
dependencies,
|
|
194
|
-
}));
|
|
195
|
-
}
|
|
196
|
-
if (rootPost.length) {
|
|
197
|
-
const ctx = panel.addItem(rootPostName, rootPost.length);
|
|
198
|
-
const pkgBail = resolveBail(options.bail, repository.rootPackage, script, true);
|
|
199
|
-
const pkgLogLevel = resolveLogLevel(options.logLevel, repository.rootPackage, script, logLevelDefault);
|
|
200
|
-
children.push(new Task(() => runSteps(ctx, repository.rootPackage, 'root', rootPost, repository.dirname, pkgBail, pkgLogLevel), {
|
|
201
|
-
name: ctx.name,
|
|
202
|
-
exclusive: true,
|
|
203
|
-
/** Must wait for every package task to finish, not just be "exclusive" once it starts. */
|
|
204
|
-
dependencies: [...stepsByPackage.keys()],
|
|
205
|
-
}));
|
|
206
|
-
}
|
|
207
|
-
if (!children.length) {
|
|
269
|
+
const waitsFor = plan.topo
|
|
270
|
+
? plan.pkg.dependencies.map(d => taskOf.get(d.name)).filter((name) => !!name)
|
|
271
|
+
: [];
|
|
208
272
|
/**
|
|
209
|
-
*
|
|
273
|
+
* **Where this package starts waiting for its dependencies**, or `-1` for not at all.
|
|
210
274
|
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
* `pre`/`post` bookends).
|
|
275
|
+
* A script whose steps mention `topo` at all is taken as the whole statement: the first step
|
|
276
|
+
* saying `true` is where the package blocks, and a script where every step says `false` does
|
|
277
|
+
* not wait. Only where **no** step mentions it does `run.<script>.topo` decide, and then the
|
|
278
|
+
* wait is before the first step - where it has always been.
|
|
216
279
|
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
280
|
+
* The two halves cannot be collapsed into one `findIndex`: "no step marked true" and "no step
|
|
281
|
+
* marked at all" are different answers (no wait against the old wait at step 0), and reading
|
|
282
|
+
* `-1` as either one alone gets the other wrong.
|
|
220
283
|
*/
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
284
|
+
const barrier = !plan.topo ? -1 : steps.some(s => s.topo !== undefined) ? steps.findIndex(s => s.topo) : 0;
|
|
285
|
+
/**
|
|
286
|
+
* **Split only when the wait is in the middle of the package and there is something to wait
|
|
287
|
+
* for.** A barrier at step 0 is one task, as it always was; a package with no in-run
|
|
288
|
+
* dependencies has nothing to wait for wherever its barrier sits, and a second task would buy
|
|
289
|
+
* an empty wait and a second panel transition for it.
|
|
290
|
+
*/
|
|
291
|
+
if (barrier > 0 && waitsFor.length) {
|
|
292
|
+
/** A name of its own, because power-tasks addresses tasks by name and the plan's name has to
|
|
293
|
+
* stay the *whole* package: a dependency waiting on it, and the root's post bookend
|
|
294
|
+
* waiting on all of them, both mean "everything this package does". */
|
|
295
|
+
const freeName = `${plan.name} (pre-topo)`;
|
|
296
|
+
children.push(new Task(() => runSteps(ctx, plan, steps.slice(0, barrier), { from: 0, more: true }), {
|
|
297
|
+
name: freeName,
|
|
298
|
+
dependencies: options.before ? [options.before.name] : [],
|
|
299
|
+
}));
|
|
300
|
+
children.push(new Task(() => runSteps(ctx, plan, steps, { from: barrier }), {
|
|
301
|
+
name: plan.name,
|
|
302
|
+
dependencies: [freeName, ...waitsFor],
|
|
303
|
+
}));
|
|
304
|
+
continue;
|
|
230
305
|
}
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
306
|
+
const dependencies = barrier < 0 ? [] : [...waitsFor];
|
|
307
|
+
if (options.before)
|
|
308
|
+
dependencies.push(options.before.name);
|
|
309
|
+
children.push(new Task(() => runSteps(ctx, plan, steps), { name: plan.name, dependencies }));
|
|
310
|
+
}
|
|
311
|
+
if (options.after) {
|
|
312
|
+
const plan = options.after;
|
|
313
|
+
const ctx = panel.addItem(plan.name, plan.steps.length);
|
|
314
|
+
children.push(new Task(() => runSteps(ctx, plan, plan.steps), {
|
|
315
|
+
name: plan.name,
|
|
316
|
+
exclusive: true,
|
|
317
|
+
/** Must wait for every package task to finish, not just be "exclusive" once it starts. */
|
|
318
|
+
dependencies: [...taskOf.values()],
|
|
319
|
+
}));
|
|
236
320
|
}
|
|
237
|
-
panel.detail = repository.name;
|
|
238
|
-
panel.start(repository.app.statusRegion);
|
|
321
|
+
panel.detail = this.repository.name;
|
|
322
|
+
panel.start(this.repository.app.statusRegion);
|
|
239
323
|
try {
|
|
240
324
|
/** bail:false here - each package's own resolved bail setting decides whether to call
|
|
241
325
|
* `rootTask.abort()` itself (see `runSteps`), since power-tasks' own `bail` can't vary per package. */
|
|
@@ -256,17 +340,121 @@ export class RunService extends Service {
|
|
|
256
340
|
panel.stop();
|
|
257
341
|
}
|
|
258
342
|
const summary = panel.printSummary();
|
|
343
|
+
/** The tallies, never `rootTask.toPromise()`'s own outcome: with a sibling still in flight at
|
|
344
|
+
* the moment one package failed, that promise *resolves*, and this command used to exit 0 on a
|
|
345
|
+
* run it had just reported as failed - non-deterministically, since it came down to which
|
|
346
|
+
* packages happened to still be running (measured: `1 0 1 1 0` across five identical runs).
|
|
347
|
+
* A single failed package must fail the command, every time. */
|
|
259
348
|
/** The tallies, never `rootTask.toPromise()`'s own outcome: with a sibling still in flight at
|
|
260
349
|
* the moment one package failed, that promise *resolves*, and this command used to exit 0 on a
|
|
261
350
|
* run it had just reported as failed - non-deterministically, since it came down to which
|
|
262
351
|
* packages happened to still be running (measured: `1 0 1 1 0` across five identical runs).
|
|
263
352
|
* A single failed package must fail the command, every time. */
|
|
264
353
|
if (summary.failedCount > 0) {
|
|
265
|
-
const err = new Error(
|
|
354
|
+
const err = new Error(`${options.failureLabel} failed`);
|
|
266
355
|
err.logged = true;
|
|
267
356
|
throw err;
|
|
268
357
|
}
|
|
269
358
|
}
|
|
359
|
+
/**
|
|
360
|
+
* Runs `fn` once per package, under the same scheduler `run` uses - concurrency, `--bail`,
|
|
361
|
+
* dependency order, the progress panel, and `console` routed to the right row.
|
|
362
|
+
*
|
|
363
|
+
* **This is what a command reaches for instead of a `for` loop.** `check` was that loop: `await
|
|
364
|
+
* runBin(...)` per package, which ignores `--parallel` outright, draws no panel, and had
|
|
365
|
+
* re-implemented `--bail`, the package filters, the two-endings rule and a summary line - about
|
|
366
|
+
* sixty lines of this service, already drifted (its own tally counted packages a bail had skipped
|
|
367
|
+
* as successes, a bug fixed here separately). Measured on `panates/opra`: nineteen packages at
|
|
368
|
+
* 0.4-0.9s each, so eleven seconds of wall clock where the work is entirely independent.
|
|
369
|
+
*
|
|
370
|
+
* `fn` is handed exactly what a **function step** gets - `pkg`, `cwd`, a `runBin` already bound to
|
|
371
|
+
* that package's directory and this run's log level, and a `logger`. That is the answer to the
|
|
372
|
+
* complaint this exists for: an imported `runBin` knows neither, and a command reaching for one
|
|
373
|
+
* had to thread `cwd`, `app` and `logLevel` through by hand at every call.
|
|
374
|
+
*
|
|
375
|
+
* Ordering is **off** unless `topo` asks for it: a command sweeping packages with an independent
|
|
376
|
+
* tool is the common case, and a wait nobody asked for is the more expensive mistake here.
|
|
377
|
+
*/
|
|
378
|
+
async forEachPackage(packages, fn, options = {}) {
|
|
379
|
+
const repository = this.repository;
|
|
380
|
+
const label = options.label ?? 'each';
|
|
381
|
+
const logLevel = options.logLevel ?? resolveRootLogLevel(repository);
|
|
382
|
+
const concurrency = resolveConcurrency(options.parallel, os.cpus().length);
|
|
383
|
+
const panel = new ProgressPanel(label.toUpperCase(), !!process.stdout.isTTY && options.progress !== false);
|
|
384
|
+
/** Nothing to do is not a failure, and the caller is the one that can tell the two endings
|
|
385
|
+
* apart - it knows why its list is empty. `check` says so itself and returns. */
|
|
386
|
+
if (!packages.length)
|
|
387
|
+
return;
|
|
388
|
+
/**
|
|
389
|
+
* **The step's label is the function's own name, and empty for an anonymous one** - never the
|
|
390
|
+
* run's label repeated. A sweep has one step whose slot name is already the label, so falling
|
|
391
|
+
* back to it printed the same word three times on the panel-off line:
|
|
392
|
+
* `info check @opra/testing ┆ check success ┆ check`. The panel path never had it - a row shows
|
|
393
|
+
* whatever `runBin` spawned (`dpdm -T ...`) as soon as there is one.
|
|
394
|
+
*/
|
|
395
|
+
const steps = [{ name: label, label: fn.name, run: fn }];
|
|
396
|
+
const plans = packages.map(pkg => ({
|
|
397
|
+
pkg,
|
|
398
|
+
name: pkg.name,
|
|
399
|
+
label: pkg.name,
|
|
400
|
+
/** One step object per package, not one shared: `ScriptStep` is read for `topo` and the row's
|
|
401
|
+
* own labels, and a shared object would be fine today and a race the day a driver writes to
|
|
402
|
+
* it. */
|
|
403
|
+
steps: [...steps],
|
|
404
|
+
cwd: pkg.dirname,
|
|
405
|
+
topo: options.topo === true,
|
|
406
|
+
bail: options.bail !== false,
|
|
407
|
+
logLevel,
|
|
408
|
+
}));
|
|
409
|
+
await this.schedule({
|
|
410
|
+
panel,
|
|
411
|
+
concurrency,
|
|
412
|
+
commandName: label,
|
|
413
|
+
failureLabel: `"${label}"`,
|
|
414
|
+
plans,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Runs `tasks` with at most `parallel` of them in flight - `Promise.all` under the repository's
|
|
419
|
+
* own concurrency rule.
|
|
420
|
+
*
|
|
421
|
+
* **The low-level half of `forEachPackage`, for work that is not per package** - sharding a file
|
|
422
|
+
* list across cores, say. It buys the concurrency limit and nothing else: a bare list of functions
|
|
423
|
+
* carries no name, so there is no panel row to label, nothing to name in a failure, and no
|
|
424
|
+
* dependency to order by. That is why it is the second thing to reach for, not the first.
|
|
425
|
+
*
|
|
426
|
+
* **Settles the tasks already in flight before rejecting.** A plain `Promise.all` rejects on the
|
|
427
|
+
* first failure while its siblings keep running - with child processes behind them, that leaves
|
|
428
|
+
* output arriving after the command has reported and exited.
|
|
429
|
+
*/
|
|
430
|
+
async parallel(tasks, options = {}) {
|
|
431
|
+
const limit = Math.max(1, resolveConcurrency(options.parallel, os.cpus().length));
|
|
432
|
+
const results = new Array(tasks.length);
|
|
433
|
+
const running = new Set();
|
|
434
|
+
let failure;
|
|
435
|
+
let failed = false;
|
|
436
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
437
|
+
if (failed)
|
|
438
|
+
break;
|
|
439
|
+
const index = i;
|
|
440
|
+
const started = (async () => {
|
|
441
|
+
results[index] = await tasks[index]();
|
|
442
|
+
})().catch(error => {
|
|
443
|
+
if (!failed) {
|
|
444
|
+
failed = true;
|
|
445
|
+
failure = error;
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
running.add(started);
|
|
449
|
+
void started.finally(() => running.delete(started));
|
|
450
|
+
if (running.size >= limit)
|
|
451
|
+
await Promise.race(running);
|
|
452
|
+
}
|
|
453
|
+
await Promise.allSettled(running);
|
|
454
|
+
if (failed)
|
|
455
|
+
throw failure;
|
|
456
|
+
return results;
|
|
457
|
+
}
|
|
270
458
|
}
|
|
271
459
|
(function (RunService) {
|
|
272
460
|
/**
|
|
@@ -310,11 +498,15 @@ export class RunService extends Service {
|
|
|
310
498
|
async function runLifecycleSlot(pkg, script, slot, fallback) {
|
|
311
499
|
const values = slotValues(slot, contributedSlots(pkg, script)?.[slot] ?? [], fallback ?? [], false);
|
|
312
500
|
for (const value of values) {
|
|
313
|
-
|
|
314
|
-
|
|
501
|
+
/** Through `toStep` rather than reading the value's own shape, so the object form
|
|
502
|
+
* (`{ command }` / `{ run }`) reaches here too and only one place knows the three spellings.
|
|
503
|
+
* `topo` cannot be set on this path - `normalizeScriptValue` refuses it for a version hook. */
|
|
504
|
+
const step = toStep(slot, value);
|
|
505
|
+
if (step.run) {
|
|
506
|
+
await step.run(createStepContext(pkg, pkg.dirname));
|
|
315
507
|
continue;
|
|
316
508
|
}
|
|
317
|
-
await exec(
|
|
509
|
+
await exec(step.command, { cwd: pkg.dirname, stdio: 'inherit', app: pkg.repository.app });
|
|
318
510
|
}
|
|
319
511
|
}
|
|
320
512
|
RunService.runLifecycleSlot = runLifecycleSlot;
|
|
@@ -348,7 +540,7 @@ export class RunService extends Service {
|
|
|
348
540
|
* Exported, and the only implementation: `VersionService` used to carry a second one that behaved
|
|
349
541
|
* differently, which is how `version.<slot>` came to join its array with `' && '`.
|
|
350
542
|
*/
|
|
351
|
-
function normalizeScriptValue(value, at) {
|
|
543
|
+
function normalizeScriptValue(value, at, options) {
|
|
352
544
|
const items = Array.isArray(value) ? value : [value];
|
|
353
545
|
const steps = [];
|
|
354
546
|
for (let i = 0; i < items.length; i++) {
|
|
@@ -357,17 +549,75 @@ export class RunService extends Service {
|
|
|
357
549
|
* declaring a slot some packages don't use has always behaved. */
|
|
358
550
|
if (item === undefined || item === null || item === '')
|
|
359
551
|
continue;
|
|
552
|
+
const where = Array.isArray(value) ? `${at}[${i}]` : at;
|
|
360
553
|
if (typeof item === 'string' || typeof item === 'function') {
|
|
361
554
|
steps.push(item);
|
|
362
555
|
continue;
|
|
363
556
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
557
|
+
if (item && typeof item === 'object' && !Array.isArray(item)) {
|
|
558
|
+
steps.push(assertStepObject(item, where, options?.topo !== false));
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
throw new Error(`"${where}" must be a shell command, a function, or { command | run, topo }, ` +
|
|
562
|
+
`but it is ${describeValue(item)}.\n` +
|
|
563
|
+
` A list of any of them runs them in sequence.`);
|
|
367
564
|
}
|
|
368
565
|
return steps;
|
|
369
566
|
}
|
|
370
567
|
RunService.normalizeScriptValue = normalizeScriptValue;
|
|
568
|
+
/**
|
|
569
|
+
* Runs `fn` with `console` captured into `onLine`, and leaves the real `console` behind when the
|
|
570
|
+
* last concurrent step is done.
|
|
571
|
+
*/
|
|
572
|
+
/* **One patch for the whole run, routed per step - and the obvious alternative is broken under
|
|
573
|
+
* concurrency.** Patching per step and restoring "the original" works for one step and corrupts the
|
|
574
|
+
* console for every run with two:
|
|
575
|
+
*
|
|
576
|
+
* A patches -> A's "original" is the real console
|
|
577
|
+
* B patches -> B's "original" is *A's patch*
|
|
578
|
+
* A restores -> console is real again
|
|
579
|
+
* B restores -> console is A's patch, for the rest of the process
|
|
580
|
+
*
|
|
581
|
+
* Measured on a twenty-package build with sixteen running at once: afterwards `printSummary`'s own
|
|
582
|
+
* `console.log` calls went into a finished package's log array, so the run printed **no recap and
|
|
583
|
+
* no failure logs at all** - every error silently swallowed, which is the opposite of what the
|
|
584
|
+
* capture is for. The depth counter is what restores the pristine console rather than whatever was
|
|
585
|
+
* installed when this step started.
|
|
586
|
+
*
|
|
587
|
+
* `AsyncLocalStorage` is what makes one shared patch route to the right step: the steps interleave
|
|
588
|
+
* on the event loop, so the call stack cannot say which package a `console.log` belongs to, and
|
|
589
|
+
* `await` inside an author's function is exactly what it propagates through. A log from outside any
|
|
590
|
+
* step - a timer a step left running - finds no store and goes to the real console, which is right.
|
|
591
|
+
*/
|
|
592
|
+
function withCapturedConsole(onLine, fn) {
|
|
593
|
+
const console_ = globalThis.console;
|
|
594
|
+
if (consoleDepth++ === 0) {
|
|
595
|
+
const pristine = {};
|
|
596
|
+
for (const method of CAPTURED_CONSOLE) {
|
|
597
|
+
pristine[method] = console_[method];
|
|
598
|
+
console_[method] = (...args) => {
|
|
599
|
+
const sink = consoleSink.getStore();
|
|
600
|
+
if (!sink)
|
|
601
|
+
return pristine[method](...args);
|
|
602
|
+
/** Split, because one `console.log` may carry several lines and the panel's log is a list
|
|
603
|
+
* of them - a multi-line entry would render as one unreadable row. */
|
|
604
|
+
for (const line of format(args).split('\n'))
|
|
605
|
+
sink(line);
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
pristineConsole = pristine;
|
|
609
|
+
}
|
|
610
|
+
/** `Promise.resolve`, because a step may be a plain function - `RunStepFn` returns
|
|
611
|
+
* `void | Promise<void>` and only the promise form can be awaited. */
|
|
612
|
+
return Promise.resolve(consoleSink.run(onLine, fn)).finally(() => {
|
|
613
|
+
if (--consoleDepth > 0 || !pristineConsole)
|
|
614
|
+
return;
|
|
615
|
+
for (const method of CAPTURED_CONSOLE)
|
|
616
|
+
console_[method] = pristineConsole[method];
|
|
617
|
+
pristineConsole = undefined;
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
RunService.withCapturedConsole = withCapturedConsole;
|
|
371
621
|
/**
|
|
372
622
|
* `onCommand` is told what the step is spawning, for as long as it runs - the argv while a child
|
|
373
623
|
* is up, `undefined` when it returns.
|
|
@@ -376,7 +626,7 @@ export class RunService extends Service {
|
|
|
376
626
|
* whole of a build, because that is all the slot knows; what a reader wants is the `tsc -b
|
|
377
627
|
* <tsconfig>` it is sitting in. A function step is the shape every shared config uses, so this is
|
|
378
628
|
* the common case rather than an edge one. */
|
|
379
|
-
function createStepContext(pkg, cwd, onCommand) {
|
|
629
|
+
function createStepContext(pkg, cwd, onCommand, onLine) {
|
|
380
630
|
const logLevel = resolveRootLogLevel(pkg.repository);
|
|
381
631
|
return {
|
|
382
632
|
pkg,
|
|
@@ -384,14 +634,17 @@ export class RunService extends Service {
|
|
|
384
634
|
cwd,
|
|
385
635
|
runBin: async (bin, argv, opts) => {
|
|
386
636
|
onCommand?.([bin, ...argv].join(' '));
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
637
|
+
const result = await runBin(bin, argv, { cwd, logLevel, app: pkg.repository.app, onLine, ...opts });
|
|
638
|
+
/**
|
|
639
|
+
* **Handed back on success only, so a failure leaves the command that failed on the row.**
|
|
640
|
+
*/
|
|
641
|
+
/* A rejection skips this line, which is the whole point: the step is over, its row becomes
|
|
642
|
+
* a failed one, and `buildWithTsc()` is a worse thing to read there than the
|
|
643
|
+
* `tsc -b <tsconfig>` that actually exited non-zero. On the way out of a *successful* call
|
|
644
|
+
* the name does have to come back, or a step that spawns once would wear that command for
|
|
645
|
+
* the rest of its run. */
|
|
646
|
+
onCommand?.(undefined);
|
|
647
|
+
return result;
|
|
395
648
|
},
|
|
396
649
|
logger: new Logger(logLevel),
|
|
397
650
|
};
|
|
@@ -483,7 +736,7 @@ function printLegacyExecutingLine(commandName, pkgLabel, step, level) {
|
|
|
483
736
|
if (LOG_LEVELS.indexOf(level) < LOG_LEVELS.indexOf('verbose'))
|
|
484
737
|
return;
|
|
485
738
|
const sep = colors.gray('┆');
|
|
486
|
-
console.log(colors.magenta('verbose'), commandName, colors.cyan(pkgLabel), sep, colors.cyanBright.bold(step.name), colors.cyanBright.bold('executing'),
|
|
739
|
+
console.log(colors.magenta('verbose'), commandName, colors.cyan(pkgLabel), sep, colors.cyanBright.bold(step.name), colors.cyanBright.bold('executing'), ...describing(step));
|
|
487
740
|
}
|
|
488
741
|
function printLegacyStepLine(commandName, pkgLabel, step, durationMs, level, error) {
|
|
489
742
|
if (!error && LOG_LEVELS.indexOf(level) < LOG_LEVELS.indexOf('info'))
|
|
@@ -493,7 +746,13 @@ function printLegacyStepLine(commandName, pkgLabel, step, durationMs, level, err
|
|
|
493
746
|
const sep = colors.gray('┆');
|
|
494
747
|
const levelLabel = error ? colors.red('error') : colors.green('info');
|
|
495
748
|
const status = error ? colors.red.bold('failed') : colors.green.bold('success');
|
|
496
|
-
console.log(levelLabel, commandName, colors.cyan(pkgLabel), sep, colors.cyanBright.bold(step.name), status,
|
|
749
|
+
console.log(levelLabel, commandName, colors.cyan(pkgLabel), sep, colors.cyanBright.bold(step.name), status, ...describing(step), colors.yellow(` (${durationMs} ms)`));
|
|
750
|
+
}
|
|
751
|
+
/** The `┆ <what it runs>` tail of a legacy log line, **or nothing at all** when the step has no
|
|
752
|
+
* label of its own - an anonymous function handed to `forEachPackage`, where the slot name already
|
|
753
|
+
* said everything there is to say and a separator with nothing after it is just noise. */
|
|
754
|
+
function describing(step) {
|
|
755
|
+
return step.label ? [colors.gray('┆'), step.label] : [];
|
|
497
756
|
}
|
|
498
757
|
function describeValue(value) {
|
|
499
758
|
if (Array.isArray(value))
|
|
@@ -506,10 +765,54 @@ function describeValue(value) {
|
|
|
506
765
|
* `function copyDocs()` and `const copyDocs = () => {}` both read as `copyDocs` - falling back to
|
|
507
766
|
* the slot's own word for one passed inline, which has no name at all. */
|
|
508
767
|
function toStep(slot, value) {
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
768
|
+
const topo = typeof value === 'object' ? value.topo : undefined;
|
|
769
|
+
const step = typeof value === 'object' ? value.command : value;
|
|
770
|
+
if (typeof step === 'function')
|
|
771
|
+
return { name: slot, label: step.name || `${slot} (js)`, run: step, topo };
|
|
772
|
+
return { name: slot, label: step, command: step, topo };
|
|
512
773
|
}
|
|
774
|
+
/**
|
|
775
|
+
* Checks a step written as an object and hands it back - a `command` that is a shell command or a
|
|
776
|
+
* function, and a boolean `topo` where ordering means anything.
|
|
777
|
+
*
|
|
778
|
+
* **An unknown key is refused, which is most of what this is for.** The object form exists to carry
|
|
779
|
+
* one marker beside the step, so anything else in it is a mistake with no effect - and the mistake
|
|
780
|
+
* is a plausible one: `{ topo: true, script: 'tsc -b' }` reads perfectly well and names the key rman
|
|
781
|
+
* uses for the *lifecycle* (`run.<script>`) rather than the step. Ignored, that step would silently
|
|
782
|
+
* run nothing while the run reported success, which is the failure `normalizeScriptValue` was
|
|
783
|
+
* already rewritten once to stop.
|
|
784
|
+
*
|
|
785
|
+
* **`command` takes a function as well as a string**, because `run.<script>.exec` already does: a
|
|
786
|
+
* second key for the function case would be two spellings of one thing plus a rule about which to
|
|
787
|
+
* use. The first draft of this did have one (`run`), and the config that drove the feature -
|
|
788
|
+
* `@panates/rman-preset`'s build hook - reached for `command` without being asked.
|
|
789
|
+
*
|
|
790
|
+
* `allowTopo` is false for `version`'s hooks: they belong to one package's version write, with no
|
|
791
|
+
* package graph to wait on, so the key means nothing there and says so rather than being dropped.
|
|
792
|
+
*/
|
|
793
|
+
function assertStepObject(item, where, allowTopo) {
|
|
794
|
+
for (const key of Object.keys(item)) {
|
|
795
|
+
if (!STEP_OBJECT_KEYS.includes(key)) {
|
|
796
|
+
throw new Error(`"${where}" has an unknown key "${key}". A step object takes ${STEP_OBJECT_KEYS.join(', ')}.\n` +
|
|
797
|
+
` The step itself goes in "command" - "script" is the name of the lifecycle, not the step.`);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
if (typeof item.command !== 'function' && (typeof item.command !== 'string' || item.command === '')) {
|
|
801
|
+
throw new Error(`"${where}" must set "command" to a shell command or a function, but it is ` + `${describeValue(item.command)}.`);
|
|
802
|
+
}
|
|
803
|
+
if (item.topo !== undefined) {
|
|
804
|
+
if (!allowTopo) {
|
|
805
|
+
throw new Error(`"${where}" cannot set "topo": it is a version hook, which runs for one package and has no\n` +
|
|
806
|
+
` dependency order to wait for. The key belongs to run.<script> steps.`);
|
|
807
|
+
}
|
|
808
|
+
if (typeof item.topo !== 'boolean') {
|
|
809
|
+
throw new Error(`"${where}" must set "topo" to true or false, but it is ${describeValue(item.topo)}.`);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return { command: item.command, topo: item.topo };
|
|
813
|
+
}
|
|
814
|
+
/** What a step object may hold - the step itself, and the marker beside it. */
|
|
815
|
+
const STEP_OBJECT_KEYS = ['command', 'topo'];
|
|
513
816
|
/**
|
|
514
817
|
* Runs a function step.
|
|
515
818
|
*
|
|
@@ -536,7 +839,7 @@ function toStep(slot, value) {
|
|
|
536
839
|
* stderr with it off, ahead of the `failed` line - the order a shell step already produces.
|
|
537
840
|
*/
|
|
538
841
|
async function runFunctionStep(run, pkg, cwd, onLine, onCommand) {
|
|
539
|
-
const context = RunService.createStepContext(pkg, cwd, onCommand);
|
|
842
|
+
const context = RunService.createStepContext(pkg, cwd, onCommand, onLine);
|
|
540
843
|
if (!onLine) {
|
|
541
844
|
try {
|
|
542
845
|
await run(context);
|
|
@@ -547,32 +850,21 @@ async function runFunctionStep(run, pkg, cwd, onLine, onCommand) {
|
|
|
547
850
|
}
|
|
548
851
|
return;
|
|
549
852
|
}
|
|
550
|
-
const console_ = globalThis.console;
|
|
551
|
-
const original = {};
|
|
552
|
-
for (const method of CAPTURED_CONSOLE) {
|
|
553
|
-
original[method] = console_[method];
|
|
554
|
-
console_[method] = (...args) => {
|
|
555
|
-
/** Split, because one `console.log` may carry several lines and the panel's log is a list of
|
|
556
|
-
* them - a multi-line entry would render as one unreadable row. */
|
|
557
|
-
for (const line of format(args).split('\n'))
|
|
558
|
-
onLine(line);
|
|
559
|
-
};
|
|
560
|
-
}
|
|
561
853
|
try {
|
|
562
|
-
await run(context);
|
|
854
|
+
await RunService.withCapturedConsole(onLine, () => run(context));
|
|
563
855
|
}
|
|
564
856
|
catch (e) {
|
|
565
|
-
/**
|
|
566
|
-
*
|
|
857
|
+
/** Straight to `onLine`, so the message lands in this step's log rather than over the panel it
|
|
858
|
+
* is drawn inside. */
|
|
567
859
|
for (const line of messageOf(e).split('\n'))
|
|
568
860
|
onLine(line);
|
|
569
861
|
throw e;
|
|
570
862
|
}
|
|
571
|
-
finally {
|
|
572
|
-
for (const method of CAPTURED_CONSOLE)
|
|
573
|
-
console_[method] = original[method];
|
|
574
|
-
}
|
|
575
863
|
}
|
|
864
|
+
/** Which step a captured `console` call belongs to - see `withCapturedConsole`. */
|
|
865
|
+
const consoleSink = new AsyncLocalStorage();
|
|
866
|
+
let consoleDepth = 0;
|
|
867
|
+
let pristineConsole;
|
|
576
868
|
/** What a thrown value has to say for itself. A step may throw anything, and `String(undefined)`
|
|
577
869
|
* reading as `undefined` in a run log is worse than saying nothing was said. */
|
|
578
870
|
function messageOf(error) {
|
|
@@ -756,6 +1048,28 @@ export function resolveNumber(cliValue, pkg, script, key, fallback) {
|
|
|
756
1048
|
const v = RunService.getConfig(pkg, script)[key];
|
|
757
1049
|
return typeof v === 'number' ? v : fallback;
|
|
758
1050
|
}
|
|
1051
|
+
/**
|
|
1052
|
+
* How many packages run at once: what the flag said, or `whenUnset` where it said nothing.
|
|
1053
|
+
*
|
|
1054
|
+
* **`--parallel` and `concurrency` are deliberately different names**, so this is where the two
|
|
1055
|
+
* meet: the flag is `boolean | number` (omit/`true` = CPU count, `false` = serially), the key is the
|
|
1056
|
+
* number it resolves to. It was written out at `runScript`'s top and two more callers need the same
|
|
1057
|
+
* answer - three copies of a four-branch ternary is how a flag comes to mean something slightly
|
|
1058
|
+
* different depending on which command you typed.
|
|
1059
|
+
*
|
|
1060
|
+
* **Where the unset case comes from is the caller's**, and deliberately not read here: `run`'s is
|
|
1061
|
+
* `run.<script>.concurrency`, while a command calling `forEachPackage` owns a key of its own
|
|
1062
|
+
* (`check.concurrency`) that this function has no business guessing the name of.
|
|
1063
|
+
*/
|
|
1064
|
+
export function resolveConcurrency(parallel, whenUnset) {
|
|
1065
|
+
if (parallel === false)
|
|
1066
|
+
return 1;
|
|
1067
|
+
if (typeof parallel === 'number')
|
|
1068
|
+
return parallel;
|
|
1069
|
+
if (parallel === true)
|
|
1070
|
+
return os.cpus().length;
|
|
1071
|
+
return whenUnset;
|
|
1072
|
+
}
|
|
759
1073
|
export function resolveLogLevel(cliValue, pkg, script, fallback) {
|
|
760
1074
|
if (cliValue !== undefined)
|
|
761
1075
|
return cliValue;
|