@webpieces/nx-webpieces-rules 0.4.919 → 0.4.921
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/package.json +7 -7
- package/src/executors/generate/executor.d.ts +7 -0
- package/src/executors/generate/executor.js +13 -5
- package/src/executors/generate/executor.js.map +1 -1
- package/src/executors/validate-architecture-unchanged/executor.d.ts +1 -20
- package/src/executors/validate-architecture-unchanged/executor.js +2 -45
- package/src/executors/validate-architecture-unchanged/executor.js.map +1 -1
- package/src/executors/visualize/executor.d.ts +8 -1
- package/src/executors/visualize/executor.js +11 -3
- package/src/executors/visualize/executor.js.map +1 -1
- package/src/executors/visualize-runtime/executor.js +2 -1
- package/src/executors/visualize-runtime/executor.js.map +1 -1
- package/src/executors/visualize-runtime-goal/executor.js +2 -1
- package/src/executors/visualize-runtime-goal/executor.js.map +1 -1
- package/src/lib/current-architecture.d.ts +26 -0
- package/src/lib/current-architecture.js +56 -0
- package/src/lib/current-architecture.js.map +1 -0
- package/src/lib/graph-color-modes.d.ts +29 -11
- package/src/lib/graph-color-modes.js +40 -14
- package/src/lib/graph-color-modes.js.map +1 -1
- package/src/lib/graph-edge-diff.d.ts +115 -0
- package/src/lib/graph-edge-diff.js +263 -0
- package/src/lib/graph-edge-diff.js.map +1 -0
- package/src/lib/graph-impact-reasons.d.ts +160 -0
- package/src/lib/graph-impact-reasons.js +412 -0
- package/src/lib/graph-impact-reasons.js.map +1 -0
- package/src/lib/graph-impact.d.ts +100 -48
- package/src/lib/graph-impact.js +176 -105
- package/src/lib/graph-impact.js.map +1 -1
- package/src/lib/graph-legend.d.ts +9 -2
- package/src/lib/graph-legend.js +31 -4
- package/src/lib/graph-legend.js.map +1 -1
- package/src/lib/graph-mode-state.client.d.ts +96 -2
- package/src/lib/graph-mode-state.client.js +343 -3
- package/src/lib/graph-mode-state.client.js.map +1 -1
- package/src/lib/graph-navigation.js +4 -2
- package/src/lib/graph-navigation.js.map +1 -1
- package/src/lib/graph-page-shell.d.ts +5 -3
- package/src/lib/graph-page-shell.js +7 -4
- package/src/lib/graph-page-shell.js.map +1 -1
- package/src/lib/graph-visualizer.client.d.ts +4 -0
- package/src/lib/graph-visualizer.client.js +36 -9
- package/src/lib/graph-visualizer.client.js.map +1 -1
- package/src/lib/runtime-html-page.js +2 -2
- package/src/lib/runtime-html-page.js.map +1 -1
- package/src/lib/runtime-viewer-adapter.js +1 -1
- package/src/lib/runtime-viewer-adapter.js.map +1 -1
- package/src/native-actions.js +1 -1
- package/src/native-actions.js.map +1 -1
package/src/lib/graph-impact.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Impact mode's data: what one COMPARISON's build looks like,
|
|
3
|
+
* Impact mode's data: what one COMPARISON's build looks like, and WHY each project is in it.
|
|
4
4
|
*
|
|
5
5
|
* There are two comparisons, and only two (#1163). Both end at the WORKING TREE, so uncommitted and
|
|
6
6
|
* untracked work counts in either:
|
|
@@ -26,15 +26,13 @@
|
|
|
26
26
|
* - AFFECTED is `nx show projects --affected --base=<the comparison's base> --json`, with NO
|
|
27
27
|
* `--head`, so uncommitted and untracked work counts. nx already gets right everything a
|
|
28
28
|
* hand-rolled diff gets wrong: lockfile and package.json changes resolved per project, root/global
|
|
29
|
-
* inputs, namedInputs, implicitDependencies, nested roots and deleted files.
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* as affected rather than touched. The changed-file list is `DiffScope.getChangedFiles`, the same
|
|
37
|
-
* base→working-tree diff (with untracked files) every code rule uses.
|
|
29
|
+
* inputs, namedInputs, implicitDependencies, nested roots and deleted files. It never reads
|
|
30
|
+
* NX_BASE/NX_HEAD, whose value depends on whoever launched the process.
|
|
31
|
+
* - WHY each affected project is in it (#1264) — changed, dependent (with the edge path), implicitly
|
|
32
|
+
* touched (with the target, glob and file) or root-file touched — comes from nx's OWN affected
|
|
33
|
+
* locators, run in-process against the workspace's own nx (graph-impact-reasons.ts). They are
|
|
34
|
+
* loaded ONCE and shared by every comparison. The changed-file list is `DiffScope.getChangedFiles`,
|
|
35
|
+
* the same base→working-tree diff (with untracked files) every code rule uses.
|
|
38
36
|
*
|
|
39
37
|
* The answer changes per checkout, so it never goes into the committed dependencies.html or
|
|
40
38
|
* dependencies.json (both stay byte-stable). It goes into a SIDECAR,
|
|
@@ -54,13 +52,15 @@
|
|
|
54
52
|
* generation.
|
|
55
53
|
*/
|
|
56
54
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
57
|
-
exports.ImpactRefresh = exports.ImpactSidecar = exports.NxImpactScanner = exports.
|
|
55
|
+
exports.ImpactRefresh = exports.ImpactSidecar = exports.NxImpactScanner = exports.SharedContext = exports.AffectedContextLoader = exports.AffectedContext = exports.NxCli = exports.ImpactScopeResolver = exports.ImpactGit = exports.ImpactPlan = exports.ImpactComparison = exports.ImpactUnavailable = exports.ImpactSidecarData = exports.ImpactCounts = exports.ImpactReport = exports.GENERATE_IMPACT_KINDS = exports.VISUALIZE_IMPACT_KINDS = exports.ImpactKind = exports.UNCOMMITTED_SUFFIX = exports.ROOT_COMMIT_REASON = exports.NOTHING_CHANGED_ON_BRANCH = exports.IMPACT_SIDECAR_SRC = exports.IMPACT_SIDECAR_FILE = exports.IMPACT_SIDECAR_DIR = void 0;
|
|
58
56
|
const tslib_1 = require("tslib");
|
|
59
57
|
const child_process_1 = require("child_process");
|
|
60
58
|
const fs = tslib_1.__importStar(require("fs"));
|
|
61
59
|
const path = tslib_1.__importStar(require("path"));
|
|
62
60
|
const devkit_1 = require("@nx/devkit");
|
|
63
61
|
const rules_config_1 = require("@webpieces/rules-config");
|
|
62
|
+
const graph_impact_reasons_1 = require("./graph-impact-reasons");
|
|
63
|
+
const graph_edge_diff_1 = require("./graph-edge-diff");
|
|
64
64
|
const graph_names_1 = require("./graph-names");
|
|
65
65
|
const toError_1 = require("../toError");
|
|
66
66
|
/** Relative to the architecture/ directory the page lives in; the page's `<script src>`. */
|
|
@@ -95,11 +95,15 @@ class ImpactReport {
|
|
|
95
95
|
reason;
|
|
96
96
|
base;
|
|
97
97
|
touched;
|
|
98
|
-
|
|
98
|
+
dependents;
|
|
99
|
+
implicit;
|
|
100
|
+
rootFile;
|
|
101
|
+
unexplained;
|
|
99
102
|
buildInputs;
|
|
100
103
|
changedFiles;
|
|
101
104
|
dependencies;
|
|
102
105
|
globalFiles;
|
|
106
|
+
reasons;
|
|
103
107
|
constructor(kind,
|
|
104
108
|
/** What the page says this scan compares, e.g. "Changed on this branch (since abc1234)". */
|
|
105
109
|
label,
|
|
@@ -109,10 +113,16 @@ class ImpactReport {
|
|
|
109
113
|
reason,
|
|
110
114
|
/** The base's short sha ('' when there is none). */
|
|
111
115
|
base,
|
|
112
|
-
/** Affected AND owning a changed file (node ids). */
|
|
116
|
+
/** Affected AND owning a changed file (node ids): "changed". */
|
|
113
117
|
touched,
|
|
114
|
-
/** Affected
|
|
115
|
-
|
|
118
|
+
/** Affected through dependency edges from a changed or touched project (node ids). */
|
|
119
|
+
dependents,
|
|
120
|
+
/** Affected because a `{workspaceRoot}` target input matched a changed file (node ids). */
|
|
121
|
+
implicit,
|
|
122
|
+
/** Affected by a lockfile, package.json, tsconfig.base.json, nx.json or deleted-config change (node ids). */
|
|
123
|
+
rootFile,
|
|
124
|
+
/** Affected, and nothing here could say why (node ids). Never shown as a dependent. */
|
|
125
|
+
unexplained,
|
|
116
126
|
/**
|
|
117
127
|
* Transitive dependencies of the affected set that are NOT affected (node ids). `ci` dependsOn
|
|
118
128
|
* `^build`, so they must exist as build output although their inputs did not change: nx
|
|
@@ -122,16 +132,16 @@ class ImpactReport {
|
|
|
122
132
|
buildInputs, changedFiles,
|
|
123
133
|
/**
|
|
124
134
|
* Transitive dependencies of the TOUCHED set that are not touched themselves (node ids): the
|
|
125
|
-
* Filter popover's "Changed + dependencies". ("Changed + dependents" is
|
|
126
|
-
* which nx already computed as the dependents whose tests re-run.)
|
|
135
|
+
* Filter popover's "Changed + dependencies". ("Changed + dependents" is every affected project.)
|
|
127
136
|
*/
|
|
128
137
|
dependencies,
|
|
129
138
|
/**
|
|
130
139
|
* Changed files no project owns (repo-relative, sorted): workspace-global inputs such as
|
|
131
|
-
* pnpm-lock.yaml.
|
|
132
|
-
* legend names them. Empty when nx's file ownership was unavailable — then nothing is known.
|
|
140
|
+
* pnpm-lock.yaml. Empty when nx's locators were unavailable — then nothing is known.
|
|
133
141
|
*/
|
|
134
|
-
globalFiles
|
|
142
|
+
globalFiles,
|
|
143
|
+
/** WHY each affected project is in the build (graph-impact-reasons.ts): one per affected node. */
|
|
144
|
+
reasons) {
|
|
135
145
|
this.kind = kind;
|
|
136
146
|
this.label = label;
|
|
137
147
|
this.dirty = dirty;
|
|
@@ -139,21 +149,56 @@ class ImpactReport {
|
|
|
139
149
|
this.reason = reason;
|
|
140
150
|
this.base = base;
|
|
141
151
|
this.touched = touched;
|
|
142
|
-
this.
|
|
152
|
+
this.dependents = dependents;
|
|
153
|
+
this.implicit = implicit;
|
|
154
|
+
this.rootFile = rootFile;
|
|
155
|
+
this.unexplained = unexplained;
|
|
143
156
|
this.buildInputs = buildInputs;
|
|
144
157
|
this.changedFiles = changedFiles;
|
|
145
158
|
this.dependencies = dependencies;
|
|
146
159
|
this.globalFiles = globalFiles;
|
|
160
|
+
this.reasons = reasons;
|
|
147
161
|
}
|
|
148
162
|
}
|
|
149
163
|
exports.ImpactReport = ImpactReport;
|
|
164
|
+
/** The one-line count split, shared by the executor's console line (the page builds the same line). */
|
|
165
|
+
class ImpactCounts {
|
|
166
|
+
/** `8 changed · 3 dependents · 22 implicitly touched (runtime-wiring-check inputs) · 47 dependencies`. */
|
|
167
|
+
line(report) {
|
|
168
|
+
const parts = [`${report.touched.length} changed`, `${report.dependents.length} dependents`];
|
|
169
|
+
if (report.implicit.length > 0)
|
|
170
|
+
parts.push(`${report.implicit.length} implicitly touched${this.targets(report)}`);
|
|
171
|
+
if (report.rootFile.length > 0)
|
|
172
|
+
parts.push(`${report.rootFile.length} root-file touched`);
|
|
173
|
+
if (report.unexplained.length > 0)
|
|
174
|
+
parts.push(`${report.unexplained.length} unexplained`);
|
|
175
|
+
parts.push(`${report.buildInputs.length} dependencies`);
|
|
176
|
+
return parts.join(' · ');
|
|
177
|
+
}
|
|
178
|
+
targets(report) {
|
|
179
|
+
const targets = new Set();
|
|
180
|
+
for (const reason of report.reasons)
|
|
181
|
+
if (reason.kind === graph_impact_reasons_1.AffectedClass.IMPLICIT && reason.target !== '')
|
|
182
|
+
targets.add(reason.target);
|
|
183
|
+
return targets.size === 0 ? '' : ` (${[...targets].sort().join(', ')} inputs)`;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
exports.ImpactCounts = ImpactCounts;
|
|
150
187
|
/** The sidecar's one global: every scan, and the one the page shows first. */
|
|
151
188
|
class ImpactSidecarData {
|
|
152
189
|
scans;
|
|
153
190
|
defaultKind;
|
|
154
|
-
|
|
191
|
+
edges;
|
|
192
|
+
constructor(scans, defaultKind,
|
|
193
|
+
/**
|
|
194
|
+
* The edges this branch gained or lost since its FORK POINT, the live graph against the fork
|
|
195
|
+
* point's committed one (graph-edge-diff.ts, #1264). One per sidecar: it does not depend on
|
|
196
|
+
* which scan is shown.
|
|
197
|
+
*/
|
|
198
|
+
edges) {
|
|
155
199
|
this.scans = scans;
|
|
156
200
|
this.defaultKind = defaultKind;
|
|
201
|
+
this.edges = edges;
|
|
157
202
|
}
|
|
158
203
|
}
|
|
159
204
|
exports.ImpactSidecarData = ImpactSidecarData;
|
|
@@ -311,28 +356,50 @@ class NxCli {
|
|
|
311
356
|
}
|
|
312
357
|
}
|
|
313
358
|
exports.NxCli = NxCli;
|
|
314
|
-
/**
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
359
|
+
/**
|
|
360
|
+
* The workspace's nx facts every comparison classifies against: its project graph (with npm nodes
|
|
361
|
+
* and every edge), nx.json, package.json, and nx's own affected locators. Subclassed by specs.
|
|
362
|
+
*/
|
|
363
|
+
class AffectedContext {
|
|
364
|
+
graph;
|
|
365
|
+
nxJson;
|
|
366
|
+
packageJson;
|
|
367
|
+
classifier;
|
|
368
|
+
nx;
|
|
369
|
+
constructor(graph, nxJson, packageJson, classifier, nx) {
|
|
370
|
+
this.graph = graph;
|
|
371
|
+
this.nxJson = nxJson;
|
|
372
|
+
this.packageJson = packageJson;
|
|
373
|
+
this.classifier = classifier;
|
|
374
|
+
this.nx = nx;
|
|
375
|
+
}
|
|
376
|
+
/** nx's FileChange list for `files`, base → working tree. */
|
|
377
|
+
fileChanges(files, base) {
|
|
378
|
+
if (this.nx === null)
|
|
379
|
+
throw new Error('no workspace nx to read file changes with');
|
|
380
|
+
return this.nx.fileChanges(files, base);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
exports.AffectedContext = AffectedContext;
|
|
384
|
+
/** Loads the workspace's nx facts. Injected so specs never load the real project graph. */
|
|
385
|
+
class AffectedContextLoader {
|
|
386
|
+
async load(workspaceRoot) {
|
|
387
|
+
const nx = new graph_impact_reasons_1.WorkspaceNx(workspaceRoot);
|
|
388
|
+
return new AffectedContext(await (0, devkit_1.createProjectGraphAsync)(), nx.nxJson(), nx.packageJson(), new graph_impact_reasons_1.AffectedClassifier(nx.locators()), nx);
|
|
324
389
|
}
|
|
325
390
|
}
|
|
326
|
-
exports.
|
|
391
|
+
exports.AffectedContextLoader = AffectedContextLoader;
|
|
327
392
|
/**
|
|
328
|
-
*
|
|
329
|
-
*
|
|
393
|
+
* The context, loaded at most ONCE however many comparisons ask for it. Failing is not fatal: every
|
|
394
|
+
* affected project is then "unexplained", with this error as its reason — never a dependent.
|
|
330
395
|
*/
|
|
331
|
-
class
|
|
332
|
-
|
|
396
|
+
class SharedContext {
|
|
397
|
+
loader;
|
|
398
|
+
workspaceRoot;
|
|
333
399
|
pending = null;
|
|
334
|
-
constructor(
|
|
335
|
-
this.
|
|
400
|
+
constructor(loader, workspaceRoot) {
|
|
401
|
+
this.loader = loader;
|
|
402
|
+
this.workspaceRoot = workspaceRoot;
|
|
336
403
|
}
|
|
337
404
|
get() {
|
|
338
405
|
if (this.pending === null)
|
|
@@ -340,60 +407,45 @@ class SharedOwners {
|
|
|
340
407
|
return this.pending;
|
|
341
408
|
}
|
|
342
409
|
async load() {
|
|
343
|
-
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- the
|
|
410
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- the reasons are optional; the affected set still shows
|
|
344
411
|
try {
|
|
345
|
-
return await this.
|
|
412
|
+
return await this.loader.load(this.workspaceRoot);
|
|
346
413
|
}
|
|
347
414
|
catch (err) {
|
|
348
415
|
const error = (0, toError_1.toError)(err);
|
|
349
|
-
console.warn(`⚠️ Impact: nx
|
|
350
|
-
return
|
|
416
|
+
console.warn(`⚠️ Impact: nx's affected locators unavailable (${error.message}); reasons unknown`);
|
|
417
|
+
return error;
|
|
351
418
|
}
|
|
352
419
|
}
|
|
353
420
|
}
|
|
354
|
-
exports.
|
|
355
|
-
/** The sets one scan found; an empty one is still an answer. */
|
|
356
|
-
class ImpactSets {
|
|
357
|
-
touched;
|
|
358
|
-
affected;
|
|
359
|
-
buildInputs;
|
|
360
|
-
dependencies;
|
|
361
|
-
globalFiles;
|
|
362
|
-
constructor(touched = [], affected = [], buildInputs = [], dependencies = [], globalFiles = []) {
|
|
363
|
-
this.touched = touched;
|
|
364
|
-
this.affected = affected;
|
|
365
|
-
this.buildInputs = buildInputs;
|
|
366
|
-
this.dependencies = dependencies;
|
|
367
|
-
this.globalFiles = globalFiles;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
421
|
+
exports.SharedContext = SharedContext;
|
|
370
422
|
class NxImpactScanner {
|
|
371
423
|
diffScope;
|
|
372
424
|
nx;
|
|
373
|
-
|
|
425
|
+
loader;
|
|
374
426
|
names = new graph_names_1.GraphNames();
|
|
375
|
-
constructor(diffScope = new rules_config_1.DiffScope(), nx = new NxCli(),
|
|
427
|
+
constructor(diffScope = new rules_config_1.DiffScope(), nx = new NxCli(), loader = new AffectedContextLoader()) {
|
|
376
428
|
this.diffScope = diffScope;
|
|
377
429
|
this.nx = nx;
|
|
378
|
-
this.
|
|
430
|
+
this.loader = loader;
|
|
379
431
|
}
|
|
380
|
-
/** Every comparison, in order, sharing one
|
|
432
|
+
/** Every comparison, in order, sharing one nx context. */
|
|
381
433
|
async scanAll(workspaceRoot, graph, comparisons) {
|
|
382
|
-
const
|
|
434
|
+
const context = new SharedContext(this.loader, workspaceRoot);
|
|
383
435
|
const results = [];
|
|
384
436
|
for (const comparison of comparisons)
|
|
385
|
-
results.push(await this.scan(workspaceRoot, graph, comparison,
|
|
437
|
+
results.push(await this.scan(workspaceRoot, graph, comparison, context));
|
|
386
438
|
return results;
|
|
387
439
|
}
|
|
388
440
|
/** One comparison: nx affected from ITS base, with NO `--head`, so uncommitted work counts. */
|
|
389
|
-
async scan(workspaceRoot, graph, comparison,
|
|
441
|
+
async scan(workspaceRoot, graph, comparison, context = new SharedContext(this.loader, workspaceRoot)) {
|
|
390
442
|
const base = comparison.base;
|
|
391
443
|
if (base === null)
|
|
392
|
-
return this.
|
|
444
|
+
return this.empty(comparison, false, comparison.reason, '', 0);
|
|
393
445
|
const json = this.nx.run(workspaceRoot, ['show', 'projects', '--affected', `--base=${base}`, '--json'], NX_TIMEOUT_MS);
|
|
394
446
|
if (json === null)
|
|
395
447
|
return new ImpactUnavailable(`nx show projects --affected failed or took over ${NX_TIMEOUT_MS / 1000}s`, comparison.label);
|
|
396
|
-
const affectedSet = this.parse(json
|
|
448
|
+
const affectedSet = this.parse(json);
|
|
397
449
|
if (affectedSet === null)
|
|
398
450
|
return new ImpactUnavailable('nx show projects --affected printed something other than a JSON list', comparison.label);
|
|
399
451
|
const options = new rules_config_1.ChangedFilesOptions();
|
|
@@ -401,13 +453,26 @@ class NxImpactScanner {
|
|
|
401
453
|
options.includeDeletions = true;
|
|
402
454
|
const files = this.diffScope.getChangedFiles(workspaceRoot, base, undefined, options);
|
|
403
455
|
const short = base.slice(0, 7);
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
456
|
+
const drawn = affectedSet.filter((project) => graph[project] !== undefined);
|
|
457
|
+
if (drawn.length === 0)
|
|
458
|
+
return this.empty(comparison, true, this.emptyNote(comparison, files.length, short), short, files.length);
|
|
459
|
+
const classification = await this.classify(await context.get(), files, base, affectedSet);
|
|
460
|
+
return this.report(comparison, short, files.length, graph, classification);
|
|
461
|
+
}
|
|
462
|
+
/** nx's locators' answer; any failure makes every affected project "unexplained", with the reason. */
|
|
463
|
+
async classify(context, files, base, affected) {
|
|
464
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- the reasons are optional; a locator failure leaves them unknown
|
|
465
|
+
try {
|
|
466
|
+
if (context instanceof Error)
|
|
467
|
+
throw context;
|
|
468
|
+
const changes = context.fileChanges(files, base);
|
|
469
|
+
return await context.classifier.classify(new graph_impact_reasons_1.AffectedInputs(context.graph, context.nxJson, context.packageJson, changes, affected));
|
|
470
|
+
}
|
|
471
|
+
catch (err) {
|
|
472
|
+
const error = (0, toError_1.toError)(err);
|
|
473
|
+
const why = `nx's affected locators could not run here (${error.message}), so why nx marked it is unknown`;
|
|
474
|
+
return new graph_impact_reasons_1.AffectedClassification([...affected].sort().map((project) => new graph_impact_reasons_1.AffectedReason(project, graph_impact_reasons_1.AffectedClass.UNEXPLAINED, [], [], '', '', why)), []);
|
|
475
|
+
}
|
|
411
476
|
}
|
|
412
477
|
/**
|
|
413
478
|
* An empty scan is still an ANSWER (available, zero sets): a fresh branch with a clean tree has
|
|
@@ -420,12 +485,6 @@ class NxImpactScanner {
|
|
|
420
485
|
return `Nothing changed since ${short}`;
|
|
421
486
|
return `Nothing affected since ${short}`;
|
|
422
487
|
}
|
|
423
|
-
/** Changed files no project owns; none are claimed when ownership itself was unavailable. */
|
|
424
|
-
globalFiles(files, owners) {
|
|
425
|
-
if (owners.size === 0)
|
|
426
|
-
return [];
|
|
427
|
-
return files.filter((file) => !owners.has(file)).sort();
|
|
428
|
-
}
|
|
429
488
|
/**
|
|
430
489
|
* Every transitive dependency of `roots` that is not itself one of them: a pure downward walk over
|
|
431
490
|
* the graph's own dependsOn edges, no nx call. Of the affected set these are the build inputs; of
|
|
@@ -449,27 +508,15 @@ class NxImpactScanner {
|
|
|
449
508
|
}
|
|
450
509
|
return inputs.sort();
|
|
451
510
|
}
|
|
452
|
-
/**
|
|
453
|
-
|
|
454
|
-
const owning = new Set();
|
|
455
|
-
for (const file of files) {
|
|
456
|
-
const owner = owners.get(file);
|
|
457
|
-
if (owner !== undefined)
|
|
458
|
-
owning.add(owner);
|
|
459
|
-
}
|
|
460
|
-
return affected.filter((project) => owning.has(project));
|
|
461
|
-
}
|
|
462
|
-
/** The JSON list, narrowed to projects the graph draws; null when it is not a list of names. */
|
|
463
|
-
parse(json, graph) {
|
|
511
|
+
/** The JSON list of project names, sorted; null when it is not a list of names. */
|
|
512
|
+
parse(json) {
|
|
464
513
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- untrusted CLI output; anything unparseable is reported as a reason
|
|
465
514
|
try {
|
|
466
515
|
// webpieces-disable no-any-unknown -- CLI JSON is untrusted until narrowed below
|
|
467
516
|
const parsed = JSON.parse(json);
|
|
468
517
|
if (!Array.isArray(parsed))
|
|
469
518
|
return null;
|
|
470
|
-
return parsed
|
|
471
|
-
.filter((name) => typeof name === 'string' && graph[name] !== undefined)
|
|
472
|
-
.sort();
|
|
519
|
+
return parsed.filter((name) => typeof name === 'string').sort();
|
|
473
520
|
}
|
|
474
521
|
catch (err) {
|
|
475
522
|
const error = (0, toError_1.toError)(err);
|
|
@@ -477,8 +524,15 @@ class NxImpactScanner {
|
|
|
477
524
|
return null;
|
|
478
525
|
}
|
|
479
526
|
}
|
|
480
|
-
|
|
481
|
-
|
|
527
|
+
/** The drawn projects only: a reason for a project the page does not draw has nowhere to show. */
|
|
528
|
+
report(comparison, short, changedFiles, graph, classification) {
|
|
529
|
+
const drawn = (projects) => projects.filter((project) => graph[project] !== undefined);
|
|
530
|
+
const touched = drawn(classification.of(graph_impact_reasons_1.AffectedClass.CHANGED));
|
|
531
|
+
const affected = drawn(classification.reasons.map((reason) => reason.project));
|
|
532
|
+
return new ImpactReport(comparison.kind, comparison.label, comparison.dirty, true, '', short, this.ids(touched), this.ids(drawn(classification.of(graph_impact_reasons_1.AffectedClass.DEPENDENT))), this.ids(drawn(classification.of(graph_impact_reasons_1.AffectedClass.IMPLICIT))), this.ids(drawn(classification.of(graph_impact_reasons_1.AffectedClass.ROOT_FILE))), this.ids(drawn(classification.of(graph_impact_reasons_1.AffectedClass.UNEXPLAINED))), this.ids(this.buildInputs(affected, graph)), changedFiles, this.ids(this.buildInputs(touched, graph)), classification.globalFiles, classification.reasons.filter((reason) => graph[reason.project] !== undefined));
|
|
533
|
+
}
|
|
534
|
+
empty(comparison, available, reason, base, changedFiles) {
|
|
535
|
+
return new ImpactReport(comparison.kind, comparison.label, comparison.dirty, available, reason, base, [], [], [], [], [], [], changedFiles, [], [], []);
|
|
482
536
|
}
|
|
483
537
|
ids(projects) {
|
|
484
538
|
return projects.map((project) => this.names.getNodeId(project));
|
|
@@ -519,16 +573,19 @@ class ImpactRefresh {
|
|
|
519
573
|
scanner;
|
|
520
574
|
sidecar;
|
|
521
575
|
resolver;
|
|
522
|
-
|
|
576
|
+
edges;
|
|
577
|
+
constructor(scanner = new NxImpactScanner(), sidecar = new ImpactSidecar(), resolver = new ImpactScopeResolver(), edges = new graph_edge_diff_1.EdgeChangeScanner()) {
|
|
523
578
|
this.scanner = scanner;
|
|
524
579
|
this.sidecar = sidecar;
|
|
525
580
|
this.resolver = resolver;
|
|
581
|
+
this.edges = edges;
|
|
526
582
|
}
|
|
527
583
|
/**
|
|
528
584
|
* Scans the comparisons in `kinds` that exist here (`VISUALIZE_IMPACT_KINDS` /
|
|
529
|
-
* `GENERATE_IMPACT_KINDS`)
|
|
585
|
+
* `GENERATE_IMPACT_KINDS`), and the edge changes since the fork point against the LIVE graphs
|
|
586
|
+
* `live` computes. Never throws — Impact is optional, generation is not.
|
|
530
587
|
*/
|
|
531
|
-
async run(architectureDir, workspaceRoot, graph, kinds) {
|
|
588
|
+
async run(architectureDir, workspaceRoot, graph, kinds, live) {
|
|
532
589
|
const started = Date.now();
|
|
533
590
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- impact is optional; any failure becomes a reason, never a failed generation
|
|
534
591
|
try {
|
|
@@ -546,8 +603,12 @@ class ImpactRefresh {
|
|
|
546
603
|
this.sidecar.remove(architectureDir);
|
|
547
604
|
return [...lines, `ℹ️ Impact disabled, no sidecar written (${cost})`].join('\n');
|
|
548
605
|
}
|
|
549
|
-
const
|
|
550
|
-
|
|
606
|
+
const fork = plan.comparisons.find((comparison) => comparison.kind === ImpactKind.BRANCH);
|
|
607
|
+
const edges = await this.edges.scan(workspaceRoot, fork?.base ?? null, live);
|
|
608
|
+
lines.push(this.edgeLine(edges));
|
|
609
|
+
const data = new ImpactSidecarData(scans, this.defaultKind(scans, plan.defaultKind), edges);
|
|
610
|
+
const file = this.sidecar.write(architectureDir, data);
|
|
611
|
+
return [...lines, file === null ? `⚠️ Impact sidecar not written (${Date.now() - started} ms)` : `→ ${file} (${Date.now() - started} ms)`].join('\n');
|
|
551
612
|
}
|
|
552
613
|
catch (err) {
|
|
553
614
|
const error = (0, toError_1.toError)(err);
|
|
@@ -555,6 +616,17 @@ class ImpactRefresh {
|
|
|
555
616
|
return `⚠️ Impact skipped: ${error.message}`;
|
|
556
617
|
}
|
|
557
618
|
}
|
|
619
|
+
/** `✅ Edges since abc1234: dependencies 1 gained · 1 lost; runtime 0 gained · 0 lost`, or why not. */
|
|
620
|
+
edgeLine(edges) {
|
|
621
|
+
const half = (name, reason, changes) => {
|
|
622
|
+
if (reason !== '')
|
|
623
|
+
return `${name}: ${reason}`;
|
|
624
|
+
const gained = changes.filter((change) => change.change === graph_edge_diff_1.EdgeChangeKind.GAINED).length;
|
|
625
|
+
return `${name} ${gained} gained · ${changes.length - gained} lost`;
|
|
626
|
+
};
|
|
627
|
+
const head = edges.base === '' ? 'ℹ️ Edges' : `✅ Edges since ${edges.base}`;
|
|
628
|
+
return `${head}: ${half('dependencies', edges.dependencyReason, edges.dependency)}; ${half('runtime', edges.runtimeReason, edges.runtime)}`;
|
|
629
|
+
}
|
|
558
630
|
/** The plan's default when that scan is available, else the first available one. */
|
|
559
631
|
defaultKind(scans, preferred) {
|
|
560
632
|
const usable = scans.filter((scan) => scan.available);
|
|
@@ -571,8 +643,7 @@ class ImpactRefresh {
|
|
|
571
643
|
return `ℹ️ Impact · ${result.label}: unavailable — ${result.reason}`;
|
|
572
644
|
if (result.reason !== '')
|
|
573
645
|
return `✅ Impact · ${result.label}: ${result.reason}`;
|
|
574
|
-
return
|
|
575
|
-
`${result.buildInputs.length} dependencies, from ${result.changedFiles} changed file(s)`);
|
|
646
|
+
return `✅ Impact · ${result.label}: ${new ImpactCounts().line(result)}, from ${result.changedFiles} changed file(s)`;
|
|
576
647
|
}
|
|
577
648
|
}
|
|
578
649
|
exports.ImpactRefresh = ImpactRefresh;
|