nx 23.3.0-beta.8 → 23.3.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/src/command-line/affected/affected.js +20 -1
  2. package/dist/src/command-line/affected/command-object.js +1 -1
  3. package/dist/src/command-line/reset/reset.js +7 -4
  4. package/dist/src/command-line/show/command-object.js +1 -1
  5. package/dist/src/command-line/show/projects.js +22 -0
  6. package/dist/src/command-line/yargs-utils/shared-options.d.ts +4 -0
  7. package/dist/src/command-line/yargs-utils/shared-options.js +16 -0
  8. package/dist/src/daemon/message-types/select-affected-tasks.d.ts +3 -0
  9. package/dist/src/daemon/server/handle-select-affected-tasks.js +1 -0
  10. package/dist/src/devkit-internals.d.ts +3 -1
  11. package/dist/src/devkit-internals.js +8 -3
  12. package/dist/src/hasher/hash-task.d.ts +1 -1
  13. package/dist/src/hasher/hash-task.js +1 -0
  14. package/dist/src/native/index.d.ts +56 -0
  15. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  16. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  17. package/dist/src/nx-cloud/utilities/client.d.ts +1 -1
  18. package/dist/src/nx-cloud/utilities/client.js +1 -1
  19. package/dist/src/plugins/js/utils/register.js +11 -3
  20. package/dist/src/project-graph/affected/affected-reasons.d.ts +116 -0
  21. package/dist/src/project-graph/affected/affected-reasons.js +459 -0
  22. package/dist/src/project-graph/affected/affected-tasks.d.ts +12 -0
  23. package/dist/src/project-graph/affected/affected-tasks.js +187 -11
  24. package/dist/src/project-graph/affected/print-explanation.d.ts +11 -0
  25. package/dist/src/project-graph/affected/print-explanation.js +44 -0
  26. package/dist/src/tasks-runner/default-tasks-runner.js +1 -1
  27. package/dist/src/tasks-runner/run-command.js +2 -1
  28. package/dist/src/tasks-runner/task-orchestrator.d.ts +7 -3
  29. package/dist/src/tasks-runner/task-orchestrator.js +9 -8
  30. package/dist/src/tasks-runner/tasks-runner.d.ts +2 -0
  31. package/dist/src/utils/command-line-utils.d.ts +1 -0
  32. package/dist/src/utils/create-cli-options.d.ts +6 -0
  33. package/dist/src/utils/create-cli-options.js +29 -0
  34. package/package.json +11 -11
@@ -0,0 +1,459 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.explainUnavailable = explainUnavailable;
4
+ exports.hydrateExplanation = hydrateExplanation;
5
+ exports.formatAffectedReason = formatAffectedReason;
6
+ exports.formatAffectedExplanation = formatAffectedExplanation;
7
+ exports.isExplaining = isExplaining;
8
+ /**
9
+ * Why `--explain` cannot run here, naming only what is missing or in the way.
10
+ * It explains task selection, so it needs what task selection needs.
11
+ */
12
+ function explainUnavailable(missing, conflicting = []) {
13
+ const sentences = [
14
+ missing.length && `--explain needs ${listed(missing)}.`,
15
+ conflicting.length &&
16
+ `--explain can't be combined with ${listed(conflicting)}.`,
17
+ ].filter(Boolean);
18
+ return sentences.length
19
+ ? sentences.join(' ')
20
+ : '--explain only works with nx affected and nx show projects --affected.';
21
+ }
22
+ /** The order input reasons are listed in, matching one task's own matches. */
23
+ const INPUT_KINDS = [
24
+ 'input-file',
25
+ 'npm-package',
26
+ 'moved-ecosystem',
27
+ 'external-dependencies',
28
+ 'project-configuration',
29
+ ];
30
+ /** Expands each task's `inputs` stand-in into its reasons. */
31
+ function hydrateExplanation({ inputs, affected, upstream, ...rest }) {
32
+ const hydrate = (reasons) => reasons.flatMap((reason) => reason.kind === 'inputs' ? mergeInputs(reason.inputs, inputs) : [reason]);
33
+ const all = (byTask) => Object.fromEntries(Object.entries(byTask).map(([id, reasons]) => [id, hydrate(reasons)]));
34
+ return { affected: all(affected), upstream: all(upstream), ...rest };
35
+ }
36
+ /**
37
+ * Several entries' reasons as one task's: grouped by kind, sorted, and with a
38
+ * reason two instructions both produced listed once.
39
+ */
40
+ function mergeInputs(indexes, inputs) {
41
+ // Each entry is already sorted and deduplicated.
42
+ if (indexes.length === 1) {
43
+ return inputs[indexes[0]];
44
+ }
45
+ const merged = indexes.flatMap((index) => inputs[index]);
46
+ const sortKey = (reason) => reason.kind === 'npm-package'
47
+ ? reason.package
48
+ : reason.kind === 'moved-ecosystem'
49
+ ? reason.ecosystem
50
+ : reason.kind === 'external-dependencies'
51
+ ? ''
52
+ : reason.file;
53
+ const result = [];
54
+ for (const kind of INPUT_KINDS) {
55
+ const seen = new Set();
56
+ const ofKind = merged
57
+ .filter((reason) => reason.kind === kind)
58
+ .sort((a, b) => compare(sortKey(a), sortKey(b)) || compare(a.pattern, b.pattern));
59
+ for (const reason of ofKind) {
60
+ const key = `${reason.file}\0${reason.pattern}\0${reason.package}\0${reason.ecosystem}`;
61
+ if (!seen.has(key)) {
62
+ seen.add(key);
63
+ result.push(reason);
64
+ }
65
+ }
66
+ }
67
+ return result;
68
+ }
69
+ /** Code-point order, as the native side sorts. */
70
+ function compare(a = '', b = '') {
71
+ return a < b ? -1 : a > b ? 1 : 0;
72
+ }
73
+ /**
74
+ * One line of `--explain` output, without the leading bullet. `many` is set
75
+ * when `file` names several files.
76
+ */
77
+ function formatAffectedReason(reason, many = false) {
78
+ const changes = many ? 'change' : 'changes';
79
+ switch (reason.kind) {
80
+ case 'deleted-project-configuration':
81
+ return `${reason.file} ${many ? 'are' : 'is'} deleted`;
82
+ case 'project-configuration':
83
+ return `project configuration in ${reason.file} ${changes}`;
84
+ case 'lockfile':
85
+ return `lockfile ${reason.file} ${changes}`;
86
+ case 'npm-package':
87
+ return `depends on ${reason.package}, whose version changes`;
88
+ case 'moved-ecosystem':
89
+ return `${reason.file ?? 'a dependency manifest'} ${changes} and can't be narrowed to packages, so every ${reason.ecosystem} package counts as changed`;
90
+ case 'input-file':
91
+ return reason.pattern
92
+ ? `${reason.file} (${reason.pattern})`
93
+ : reason.file;
94
+ case 'dependent-output':
95
+ return `reads the outputs of ${reason.producer}`;
96
+ case 'external-dependencies':
97
+ return `hashes every external dependency, and ${reason.file} ${changes}`;
98
+ case 'custom-hasher':
99
+ return `its executor uses a custom hasher, so it is always selected`;
100
+ }
101
+ }
102
+ /**
103
+ * Renders `--explain` output: one block per entity, its reasons beneath it.
104
+ *
105
+ * It follows the chain: what the change touched, what it reached through
106
+ * those, and last the selection itself, so the reader's own entries stay at
107
+ * the bottom however long the chain grows.
108
+ *
109
+ * An entity with no reason is still listed, with a line saying so, because a
110
+ * blank line is indistinguishable from a bug when you are troubleshooting.
111
+ */
112
+ function formatAffectedExplanation({ affected, upstream, touched: touchedNames, requested, moved = {}, }, heading, { verbose = false, styleTask = (id) => id, dim = (text) => text, bold = (text) => text, } = {}) {
113
+ const names = Object.keys(affected).sort();
114
+ if (!names.length) {
115
+ return `Nothing affected.`;
116
+ }
117
+ const reasonsOf = (name) => affected[name] ?? upstream[name] ?? [];
118
+ const touchedSet = new Set(touchedNames);
119
+ const touched = (name) => touchedSet.has(name);
120
+ // Out of the requested targets' tasks, so the title says what selection saved.
121
+ const n = names.length;
122
+ const title = requested
123
+ ? `${n} out of ${requested.total} ${listed(requested.targets)} ${requested.total === 1 ? 'task' : 'tasks'} ${n === 1 ? 'is' : 'are'} affected`
124
+ : `${heading} (${n})`;
125
+ const lines = [`${title}:`, ''];
126
+ // The tasks asked for are marked by count and listed first in each group,
127
+ // rather than split into a section of their own, so each cause shows once.
128
+ const requestedSet = new Set(names);
129
+ // Your tasks are counted per target, in the order the targets were given.
130
+ const targets = requested?.targets ?? [];
131
+ const targetOf = (name) => targets.find((target) => name.endsWith(`:${target}`) || name.includes(`:${target}:`)) ?? 'requested';
132
+ const breakdown = (group, more) => {
133
+ const byTarget = new Map([...targets, 'requested'].map((target) => [target, 0]));
134
+ let rest = 0;
135
+ for (const name of group) {
136
+ if (requestedSet.has(name)) {
137
+ const target = targetOf(name);
138
+ byTarget.set(target, byTarget.get(target) + 1);
139
+ }
140
+ else {
141
+ rest++;
142
+ }
143
+ }
144
+ const mine = [...byTarget.values()].reduce((sum, count) => sum + count, 0);
145
+ const parts = [...byTarget]
146
+ .filter(([, count]) => count)
147
+ .map(([target, count]) => `${count} ${more}${bold(target)} ${count === 1 ? 'task' : 'tasks'}`);
148
+ // Named as the run summary names them: the tasks yours depend on.
149
+ if (rest) {
150
+ const whose = mine
151
+ ? mine === 1
152
+ ? 'it depends on'
153
+ : 'they depend on'
154
+ : `the ${listed((targets.length ? targets : ['requested']).map(bold))} tasks depend on`;
155
+ parts.push(`${rest} ${mine ? '' : more}${rest === 1 ? 'task' : 'tasks'} ${whose}`);
156
+ }
157
+ return listed(parts);
158
+ };
159
+ const counted = (group) => breakdown(group, '');
160
+ // The tasks a list leaves out, as "N more build tasks and M tasks they depend on".
161
+ const otherCounted = (group) => breakdown(group, 'more ');
162
+ const requestedFirst = (group) => [
163
+ ...group.filter((name) => requestedSet.has(name)),
164
+ ...group.filter((name) => !requestedSet.has(name)),
165
+ ];
166
+ const shown = (name) => styleTask(name, requestedSet.has(name));
167
+ const detail = (name, indent) => {
168
+ lines.push(`${indent}${shown(name)}`);
169
+ const forName = reasonsOf(name);
170
+ if (!forName.length) {
171
+ lines.push(`${indent} - selected, but no reason is recorded`);
172
+ }
173
+ for (const line of reasonLines(forName, moved, dim)) {
174
+ lines.push(`${indent} - ${line}`);
175
+ }
176
+ };
177
+ // --verbose shows each task with its reasons; otherwise up to five names.
178
+ const list = (group, indent, detailed) => {
179
+ const ordered = requestedFirst(group);
180
+ if (detailed) {
181
+ ordered.forEach((name) => detail(name, indent));
182
+ return;
183
+ }
184
+ for (const name of ordered.slice(0, 5)) {
185
+ lines.push(`${indent}- ${shown(name)}`);
186
+ }
187
+ if (ordered.length > 5) {
188
+ lines.push(`${indent}- and ${otherCounted(ordered.slice(5))}`);
189
+ }
190
+ };
191
+ const hint = '. Pass --verbose to list each with its reasons.';
192
+ const all = [...new Set([...names, ...Object.keys(upstream)])].sort();
193
+ const touchedAll = all.filter(touched);
194
+ // Every task reading a changed output, so a touched one can be listed twice.
195
+ const reachedAll = all.filter((name) => !touched(name) || reasonsOf(name).some(isUpstreamReason));
196
+ // Touched tasks: a section for files, then one for packages, each grouped
197
+ // by what changed. A task touched by both is listed in each.
198
+ const kinds = [
199
+ { noun: 'file', groups: new Map() },
200
+ { noun: 'package', groups: new Map() },
201
+ ];
202
+ const unnamed = [];
203
+ for (const name of touchedAll) {
204
+ const changed = changedIn(reasonsOf(name), moved);
205
+ if (!changed.length)
206
+ unnamed.push(name);
207
+ for (const { noun, groups } of kinds) {
208
+ const ofKind = changed.filter((change) => change.isPackage === (noun === 'package'));
209
+ if (!ofKind.length)
210
+ continue;
211
+ // Keyed by the whole set: two sets can share a shortened heading.
212
+ const key = ofKind.map((change) => change.name).join('\0');
213
+ const group = groups.get(key) ?? {
214
+ heading: describeChanged(ofKind, noun),
215
+ names: ofKind.map((change) => change.name),
216
+ members: [],
217
+ };
218
+ group.members.push(name);
219
+ groups.set(key, group);
220
+ }
221
+ }
222
+ for (const { noun, groups } of kinds) {
223
+ if (!groups.size)
224
+ continue;
225
+ if (verbose) {
226
+ for (const group of groups.values())
227
+ group.heading = listed(group.names);
228
+ }
229
+ else {
230
+ disambiguate([...groups.values()], noun);
231
+ }
232
+ const members = [
233
+ ...new Set([...groups.values()].flatMap((group) => group.members)),
234
+ ];
235
+ const sorted = [...groups].sort(([a, x], [b, y]) => y.members.length - x.members.length ||
236
+ x.heading.localeCompare(y.heading) ||
237
+ a.localeCompare(b));
238
+ const changes = new Set(members.flatMap((name) => changedIn(reasonsOf(name), moved)
239
+ .filter((change) => change.isPackage === (noun === 'package'))
240
+ .map((change) => change.name)));
241
+ const header = `Changing ${changes.size} ${noun}${changes.size === 1 ? '' : 's'} touches ${counted(members)}`;
242
+ lines.push(verbose ? `${header}:` : `${header}${hint}`);
243
+ for (const [, { heading, members: inGroup }] of sorted) {
244
+ lines.push('', ` ${heading}:`);
245
+ list(inGroup, ' ', verbose);
246
+ }
247
+ lines.push('');
248
+ }
249
+ if (unnamed.length) {
250
+ const header = `${counted(unnamed)} ${unnamed.length === 1 ? 'is' : 'are'} touched with no changed file to name`;
251
+ lines.push(verbose ? `${header}:` : `${header}${hint}`);
252
+ list(unnamed, ' ', verbose);
253
+ lines.push('');
254
+ }
255
+ if (reachedAll.length) {
256
+ const header = `Touching those tasks changes outputs read by ${counted(reachedAll)}`;
257
+ lines.push(verbose ? `${header}:` : `${header}${hint}`);
258
+ list(reachedAll, ' ', verbose);
259
+ lines.push('');
260
+ }
261
+ lines.pop();
262
+ return lines.join('\n');
263
+ }
264
+ /**
265
+ * One line per reason, and per matched file with the patterns it matched.
266
+ * Producers read share a line that names them all. Moved packages name the
267
+ * first and count the rest: a dependency bump can move thousands. The JSON
268
+ * keeps them all.
269
+ */
270
+ function reasonLines(reasons, moved, dim = (text) => text) {
271
+ const externals = reasons.filter((reason) => reason.kind === 'external-dependencies');
272
+ const movedPackages = [
273
+ ...new Set(externals.flatMap((reason) => moved[reason.file] ?? [])),
274
+ ].sort();
275
+ // Hashing every external covers every package in an ecosystem that moved,
276
+ // unless the line names packages instead of files.
277
+ if (externals.length && !movedPackages.length) {
278
+ reasons = reasons.filter((reason) => reason.kind !== 'moved-ecosystem');
279
+ }
280
+ const grouped = new Map();
281
+ const keyOf = (reason) => {
282
+ switch (reason.kind) {
283
+ case 'input-file':
284
+ return `input-file\0${reason.file}`;
285
+ case 'npm-package':
286
+ case 'dependent-output':
287
+ case 'external-dependencies':
288
+ case 'project-configuration':
289
+ case 'deleted-project-configuration':
290
+ case 'lockfile':
291
+ return reason.kind;
292
+ case 'moved-ecosystem':
293
+ return `moved-ecosystem\0${reason.ecosystem}`;
294
+ default:
295
+ return undefined;
296
+ }
297
+ };
298
+ for (const reason of reasons) {
299
+ const key = keyOf(reason);
300
+ if (key) {
301
+ grouped.set(key, [...(grouped.get(key) ?? []), reason]);
302
+ }
303
+ }
304
+ const lines = [];
305
+ const done = new Set();
306
+ for (const reason of reasons) {
307
+ const key = keyOf(reason);
308
+ if (reason.kind === 'external-dependencies' && movedPackages.length) {
309
+ if (done.has(key))
310
+ continue;
311
+ done.add(key);
312
+ const [first] = movedPackages;
313
+ const others = movedPackages.length - 1;
314
+ lines.push(`hashes every external dependency, and ${first}${others
315
+ ? ` and ${others} other ${others === 1 ? 'package' : 'packages'}`
316
+ : ''} changed version`);
317
+ continue;
318
+ }
319
+ const group = key ? grouped.get(key) : undefined;
320
+ if (reason.kind === 'input-file') {
321
+ if (done.has(key))
322
+ continue;
323
+ done.add(key);
324
+ const patterns = group.map((r) => r.pattern).filter(Boolean);
325
+ lines.push(patterns.length
326
+ ? `${reason.file} ${dim(`(${patterns.join(', ')})`)}`
327
+ : reason.file);
328
+ continue;
329
+ }
330
+ if (!group || group.length === 1) {
331
+ lines.push(formatAffectedReason(reason));
332
+ continue;
333
+ }
334
+ if (done.has(key))
335
+ continue;
336
+ done.add(key);
337
+ const [first] = group;
338
+ const others = group.length - 1;
339
+ switch (first.kind) {
340
+ case 'dependent-output':
341
+ lines.push(`reads the outputs of ${listed(group.map((r) => r.producer).sort())}`);
342
+ break;
343
+ case 'npm-package':
344
+ lines.push(`depends on ${first.package} and ${others} other ${others === 1 ? 'package' : 'packages'}, whose versions change`);
345
+ break;
346
+ default: {
347
+ // The same reason for several files: name them in one.
348
+ const files = group.map((r) => r.file ?? '').sort();
349
+ lines.push(formatAffectedReason({ ...first, file: listed(files) }, true));
350
+ }
351
+ }
352
+ }
353
+ return lines;
354
+ }
355
+ /**
356
+ * Two sets of the same size and first name shorten to the same heading. Such
357
+ * a heading instead leads with the files that tell it apart, picked one at a
358
+ * time to rule out the most lookalikes, so it grows with the collisions rather
359
+ * than with the files.
360
+ */
361
+ function disambiguate(groups, noun) {
362
+ const others = (count) => count ? [`${count} other ${noun}${count === 1 ? '' : 's'}`] : [];
363
+ const byHeading = new Map();
364
+ for (const group of groups) {
365
+ byHeading.set(group.heading, [
366
+ ...(byHeading.get(group.heading) ?? []),
367
+ group,
368
+ ]);
369
+ }
370
+ const telling = new Map();
371
+ for (const alike of byHeading.values()) {
372
+ if (alike.length < 2)
373
+ continue;
374
+ for (const group of alike) {
375
+ const [, ...rest] = group.names;
376
+ const picked = [];
377
+ let lookalikes = alike
378
+ .filter((other) => other !== group)
379
+ .map((other) => new Set(other.names));
380
+ while (lookalikes.length) {
381
+ const ruledOut = (name) => lookalikes.filter((names) => !names.has(name)).length;
382
+ const pick = rest
383
+ .filter((name) => !picked.includes(name))
384
+ .reduce((best, name) => ruledOut(name) > ruledOut(best) ? name : best);
385
+ picked.push(pick);
386
+ lookalikes = lookalikes.filter((names) => names.has(pick));
387
+ }
388
+ telling.set(group, picked.sort());
389
+ }
390
+ }
391
+ const leading = new Map([...telling].map(([group, picked]) => [
392
+ group,
393
+ listed([...picked, ...others(group.names.length - picked.length)]),
394
+ ]));
395
+ const taken = new Map();
396
+ for (const group of groups) {
397
+ const heading = leading.get(group) ?? group.heading;
398
+ taken.set(heading, (taken.get(heading) ?? 0) + 1);
399
+ }
400
+ for (const [group, picked] of telling) {
401
+ // Leading with a telling file can, rarely, read like an unrelated group;
402
+ // the first name keeps it apart from everything outside its lookalikes.
403
+ group.heading =
404
+ taken.get(leading.get(group)) === 1
405
+ ? leading.get(group)
406
+ : listed([
407
+ group.names[0],
408
+ ...picked,
409
+ ...others(group.names.length - 1 - picked.length),
410
+ ]);
411
+ }
412
+ }
413
+ /**
414
+ * What a task's own reasons name as changed: a moved package by its name,
415
+ * anything else by its file.
416
+ */
417
+ function changedIn(reasons, moved) {
418
+ const changed = new Map();
419
+ const add = (name, isPackage) => {
420
+ if (name)
421
+ changed.set(name, { name, isPackage });
422
+ };
423
+ for (const reason of reasons) {
424
+ if (isUpstreamReason(reason))
425
+ continue;
426
+ const packages = reason.kind === 'external-dependencies' ? moved[reason.file] : undefined;
427
+ if (packages?.length) {
428
+ packages.forEach((name) => add(name, true));
429
+ }
430
+ else if (reason.kind === 'npm-package') {
431
+ add(reason.package, true);
432
+ }
433
+ else {
434
+ add(reason.file, false);
435
+ }
436
+ }
437
+ return [...changed.values()].sort((a, b) => a.name.localeCompare(b.name));
438
+ }
439
+ /** "a", "a and b", or "a and N other files". */
440
+ function describeChanged(changed, noun) {
441
+ if (changed.length <= 2) {
442
+ return changed.map((change) => change.name).join(' and ');
443
+ }
444
+ return `${changed[0].name} and ${changed.length - 1} other ${noun}s`;
445
+ }
446
+ /** A reason that names another entry rather than a change. */
447
+ function isUpstreamReason(reason) {
448
+ return reason.kind === 'dependent-output';
449
+ }
450
+ /** Whether `--explain` was asked for at all, in any of its forms. */
451
+ function isExplaining(value) {
452
+ return value !== undefined && value !== false;
453
+ }
454
+ /** "a", "a and b", or "a, b and c". */
455
+ function listed(items) {
456
+ return items.length <= 2
457
+ ? items.join(' and ')
458
+ : `${items.slice(0, -1).join(', ')} and ${items[items.length - 1]}`;
459
+ }
@@ -6,6 +6,7 @@ import { type UltracacheConfigurationOutcome } from '../../ultracache/store';
6
6
  import type { UltracacheConfigurationVersion } from '../../daemon/message-types/ultracache-configuration-version';
7
7
  import { FileChange } from '../file-utils';
8
8
  import type { NxArgs } from '../../utils/command-line-utils';
9
+ import { type AffectedExplanation, type InternedExplanation } from './affected-reasons';
9
10
  import { TaskPlanningContext } from '../../hasher/task-planning-context';
10
11
  import type { TaskSelection } from '../../tasks-runner/run-command';
11
12
  /**
@@ -26,6 +27,12 @@ export interface AffectedTasksResult {
26
27
  taskGraph: TaskGraph;
27
28
  /** What the run executes: the affected tasks and what they depend on. */
28
29
  taskSelection: TaskSelection;
30
+ /**
31
+ * Why each task is affected, plus the tasks outside the selection that
32
+ * carried the change to it, such as a `prebuild` under `-t build`. Only
33
+ * when `explain` was asked for.
34
+ */
35
+ explanation?: AffectedExplanation;
29
36
  }
30
37
  export interface ComputeAffectedTasksOptions {
31
38
  /** Fetched when absent. Unused when the daemon selects; it returns its own graph. */
@@ -45,6 +52,8 @@ export interface ComputeAffectedTasksOptions {
45
52
  /** This command's Ultracache configurations. Selection plans with them, as the run hashes with them. */
46
53
  ultracacheConfigurationOutcome?: UltracacheConfigurationOutcome | null;
47
54
  selectivelyHashTsConfig?: boolean;
55
+ /** Whether to say why each task is affected, for `--explain`. */
56
+ explain?: boolean;
48
57
  }
49
58
  export type FileChangeArgs = Pick<NxArgs, 'base' | 'head' | 'files'>;
50
59
  /**
@@ -64,6 +73,8 @@ export interface AffectedTasksRequest {
64
73
  ultracacheConfigurationsVersion?: UltracacheConfigurationVersion;
65
74
  /** The runner's `selectivelyHashTsConfig`, which decides what the tsconfig hash reads. */
66
75
  selectivelyHashTsConfig?: boolean;
76
+ /** Whether to return why each task is affected. Off, nothing is collected. */
77
+ explain?: boolean;
67
78
  }
68
79
  /**
69
80
  * Selects the tasks a change reaches, rather than the projects that own a
@@ -88,4 +99,5 @@ export declare function selectAffectedTasks(projectGraph: ProjectGraph, nxJson:
88
99
  affectedTaskIds: Set<string>;
89
100
  taskGraph: TaskGraph;
90
101
  taskSelection: TaskSelection;
102
+ explanation?: InternedExplanation;
91
103
  }>;