@webpieces/nx-webpieces-rules 0.4.919 → 0.4.920
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/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-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 +79 -44
- package/src/lib/graph-impact.js +143 -99
- package/src/lib/graph-impact.js.map +1 -1
- package/src/lib/graph-legend.d.ts +5 -2
- package/src/lib/graph-legend.js +17 -3
- package/src/lib/graph-legend.js.map +1 -1
- package/src/lib/graph-mode-state.client.d.ts +32 -2
- package/src/lib/graph-mode-state.client.js +137 -3
- package/src/lib/graph-mode-state.client.js.map +1 -1
- package/src/lib/graph-page-shell.d.ts +2 -2
- package/src/lib/graph-page-shell.js +4 -4
- package/src/lib/graph-page-shell.js.map +1 -1
- package/src/lib/graph-visualizer.client.d.ts +3 -0
- package/src/lib/graph-visualizer.client.js +20 -6
- package/src/lib/graph-visualizer.client.js.map +1 -1
- package/src/lib/runtime-viewer-adapter.js +1 -1
- package/src/lib/runtime-viewer-adapter.js.map +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Impact mode's data: what one COMPARISON's build looks like,
|
|
2
|
+
* Impact mode's data: what one COMPARISON's build looks like, and WHY each project is in it.
|
|
3
3
|
*
|
|
4
4
|
* There are two comparisons, and only two (#1163). Both end at the WORKING TREE, so uncommitted and
|
|
5
5
|
* untracked work counts in either:
|
|
@@ -25,15 +25,13 @@
|
|
|
25
25
|
* - AFFECTED is `nx show projects --affected --base=<the comparison's base> --json`, with NO
|
|
26
26
|
* `--head`, so uncommitted and untracked work counts. nx already gets right everything a
|
|
27
27
|
* hand-rolled diff gets wrong: lockfile and package.json changes resolved per project, root/global
|
|
28
|
-
* inputs, namedInputs, implicitDependencies, nested roots and deleted files.
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* as affected rather than touched. The changed-file list is `DiffScope.getChangedFiles`, the same
|
|
36
|
-
* base→working-tree diff (with untracked files) every code rule uses.
|
|
28
|
+
* inputs, namedInputs, implicitDependencies, nested roots and deleted files. It never reads
|
|
29
|
+
* NX_BASE/NX_HEAD, whose value depends on whoever launched the process.
|
|
30
|
+
* - WHY each affected project is in it (#1264) — changed, dependent (with the edge path), implicitly
|
|
31
|
+
* touched (with the target, glob and file) or root-file touched — comes from nx's OWN affected
|
|
32
|
+
* locators, run in-process against the workspace's own nx (graph-impact-reasons.ts). They are
|
|
33
|
+
* loaded ONCE and shared by every comparison. The changed-file list is `DiffScope.getChangedFiles`,
|
|
34
|
+
* the same base→working-tree diff (with untracked files) every code rule uses.
|
|
37
35
|
*
|
|
38
36
|
* The answer changes per checkout, so it never goes into the committed dependencies.html or
|
|
39
37
|
* dependencies.json (both stay byte-stable). It goes into a SIDECAR,
|
|
@@ -52,7 +50,11 @@
|
|
|
52
50
|
* stale one is removed) and the page disables Impact with a one-line reason. It never fails
|
|
53
51
|
* generation.
|
|
54
52
|
*/
|
|
53
|
+
import type { NxJsonConfiguration, ProjectGraph } from '@nx/devkit';
|
|
54
|
+
import type { FileChange } from 'nx/src/project-graph/file-utils';
|
|
55
|
+
import type { PackageJson } from 'nx/src/utils/package-json';
|
|
55
56
|
import { DiffScope } from '@webpieces/rules-config';
|
|
57
|
+
import { AffectedClassification, AffectedClassifier, AffectedReason, WorkspaceNx } from './graph-impact-reasons';
|
|
56
58
|
import type { EnhancedGraph } from './graph-sorter';
|
|
57
59
|
/** Relative to the architecture/ directory the page lives in; the page's `<script src>`. */
|
|
58
60
|
export declare const IMPACT_SIDECAR_DIR = ".impact";
|
|
@@ -84,10 +86,16 @@ export declare class ImpactReport {
|
|
|
84
86
|
readonly reason: string;
|
|
85
87
|
/** The base's short sha ('' when there is none). */
|
|
86
88
|
readonly base: string;
|
|
87
|
-
/** Affected AND owning a changed file (node ids). */
|
|
89
|
+
/** Affected AND owning a changed file (node ids): "changed". */
|
|
88
90
|
readonly touched: string[];
|
|
89
|
-
/** Affected
|
|
90
|
-
readonly
|
|
91
|
+
/** Affected through dependency edges from a changed or touched project (node ids). */
|
|
92
|
+
readonly dependents: string[];
|
|
93
|
+
/** Affected because a `{workspaceRoot}` target input matched a changed file (node ids). */
|
|
94
|
+
readonly implicit: string[];
|
|
95
|
+
/** Affected by a lockfile, package.json, tsconfig.base.json, nx.json or deleted-config change (node ids). */
|
|
96
|
+
readonly rootFile: string[];
|
|
97
|
+
/** Affected, and nothing here could say why (node ids). Never shown as a dependent. */
|
|
98
|
+
readonly unexplained: string[];
|
|
91
99
|
/**
|
|
92
100
|
* Transitive dependencies of the affected set that are NOT affected (node ids). `ci` dependsOn
|
|
93
101
|
* `^build`, so they must exist as build output although their inputs did not change: nx
|
|
@@ -98,16 +106,16 @@ export declare class ImpactReport {
|
|
|
98
106
|
readonly changedFiles: number;
|
|
99
107
|
/**
|
|
100
108
|
* Transitive dependencies of the TOUCHED set that are not touched themselves (node ids): the
|
|
101
|
-
* Filter popover's "Changed + dependencies". ("Changed + dependents" is
|
|
102
|
-
* which nx already computed as the dependents whose tests re-run.)
|
|
109
|
+
* Filter popover's "Changed + dependencies". ("Changed + dependents" is every affected project.)
|
|
103
110
|
*/
|
|
104
111
|
readonly dependencies: string[];
|
|
105
112
|
/**
|
|
106
113
|
* Changed files no project owns (repo-relative, sorted): workspace-global inputs such as
|
|
107
|
-
* pnpm-lock.yaml.
|
|
108
|
-
* legend names them. Empty when nx's file ownership was unavailable — then nothing is known.
|
|
114
|
+
* pnpm-lock.yaml. Empty when nx's locators were unavailable — then nothing is known.
|
|
109
115
|
*/
|
|
110
116
|
readonly globalFiles: string[];
|
|
117
|
+
/** WHY each affected project is in the build (graph-impact-reasons.ts): one per affected node. */
|
|
118
|
+
readonly reasons: AffectedReason[];
|
|
111
119
|
constructor(kind: ImpactKind,
|
|
112
120
|
/** What the page says this scan compares, e.g. "Changed on this branch (since abc1234)". */
|
|
113
121
|
label: string,
|
|
@@ -117,10 +125,16 @@ export declare class ImpactReport {
|
|
|
117
125
|
reason: string,
|
|
118
126
|
/** The base's short sha ('' when there is none). */
|
|
119
127
|
base: string,
|
|
120
|
-
/** Affected AND owning a changed file (node ids). */
|
|
128
|
+
/** Affected AND owning a changed file (node ids): "changed". */
|
|
121
129
|
touched: string[],
|
|
122
|
-
/** Affected
|
|
123
|
-
|
|
130
|
+
/** Affected through dependency edges from a changed or touched project (node ids). */
|
|
131
|
+
dependents: string[],
|
|
132
|
+
/** Affected because a `{workspaceRoot}` target input matched a changed file (node ids). */
|
|
133
|
+
implicit: string[],
|
|
134
|
+
/** Affected by a lockfile, package.json, tsconfig.base.json, nx.json or deleted-config change (node ids). */
|
|
135
|
+
rootFile: string[],
|
|
136
|
+
/** Affected, and nothing here could say why (node ids). Never shown as a dependent. */
|
|
137
|
+
unexplained: string[],
|
|
124
138
|
/**
|
|
125
139
|
* Transitive dependencies of the affected set that are NOT affected (node ids). `ci` dependsOn
|
|
126
140
|
* `^build`, so they must exist as build output although their inputs did not change: nx
|
|
@@ -130,16 +144,22 @@ export declare class ImpactReport {
|
|
|
130
144
|
buildInputs: string[], changedFiles: number,
|
|
131
145
|
/**
|
|
132
146
|
* Transitive dependencies of the TOUCHED set that are not touched themselves (node ids): the
|
|
133
|
-
* Filter popover's "Changed + dependencies". ("Changed + dependents" is
|
|
134
|
-
* which nx already computed as the dependents whose tests re-run.)
|
|
147
|
+
* Filter popover's "Changed + dependencies". ("Changed + dependents" is every affected project.)
|
|
135
148
|
*/
|
|
136
149
|
dependencies: string[],
|
|
137
150
|
/**
|
|
138
151
|
* Changed files no project owns (repo-relative, sorted): workspace-global inputs such as
|
|
139
|
-
* pnpm-lock.yaml.
|
|
140
|
-
* legend names them. Empty when nx's file ownership was unavailable — then nothing is known.
|
|
152
|
+
* pnpm-lock.yaml. Empty when nx's locators were unavailable — then nothing is known.
|
|
141
153
|
*/
|
|
142
|
-
globalFiles: string[]
|
|
154
|
+
globalFiles: string[],
|
|
155
|
+
/** WHY each affected project is in the build (graph-impact-reasons.ts): one per affected node. */
|
|
156
|
+
reasons: AffectedReason[]);
|
|
157
|
+
}
|
|
158
|
+
/** The one-line count split, shared by the executor's console line (the page builds the same line). */
|
|
159
|
+
export declare class ImpactCounts {
|
|
160
|
+
/** `8 changed · 3 dependents · 22 implicitly touched (runtime-wiring-check inputs) · 47 dependencies`. */
|
|
161
|
+
line(report: ImpactReport): string;
|
|
162
|
+
private targets;
|
|
143
163
|
}
|
|
144
164
|
/** The sidecar's one global: every scan, and the one the page shows first. */
|
|
145
165
|
export declare class ImpactSidecarData {
|
|
@@ -210,49 +230,64 @@ export declare class NxCli {
|
|
|
210
230
|
/** Resolved the way node would from the workspace, so a linked worktree finds its main tree's nx. */
|
|
211
231
|
private binary;
|
|
212
232
|
}
|
|
213
|
-
/**
|
|
214
|
-
|
|
215
|
-
|
|
233
|
+
/**
|
|
234
|
+
* The workspace's nx facts every comparison classifies against: its project graph (with npm nodes
|
|
235
|
+
* and every edge), nx.json, package.json, and nx's own affected locators. Subclassed by specs.
|
|
236
|
+
*/
|
|
237
|
+
export declare class AffectedContext {
|
|
238
|
+
readonly graph: ProjectGraph;
|
|
239
|
+
readonly nxJson: NxJsonConfiguration;
|
|
240
|
+
readonly packageJson: PackageJson;
|
|
241
|
+
readonly classifier: AffectedClassifier;
|
|
242
|
+
private readonly nx;
|
|
243
|
+
constructor(graph: ProjectGraph, nxJson: NxJsonConfiguration, packageJson: PackageJson, classifier: AffectedClassifier, nx: WorkspaceNx | null);
|
|
244
|
+
/** nx's FileChange list for `files`, base → working tree. */
|
|
245
|
+
fileChanges(files: string[], base: string): FileChange[];
|
|
246
|
+
}
|
|
247
|
+
/** Loads the workspace's nx facts. Injected so specs never load the real project graph. */
|
|
248
|
+
export declare class AffectedContextLoader {
|
|
249
|
+
load(workspaceRoot: string): Promise<AffectedContext>;
|
|
216
250
|
}
|
|
217
251
|
/**
|
|
218
|
-
*
|
|
219
|
-
*
|
|
252
|
+
* The context, loaded at most ONCE however many comparisons ask for it. Failing is not fatal: every
|
|
253
|
+
* affected project is then "unexplained", with this error as its reason — never a dependent.
|
|
220
254
|
*/
|
|
221
|
-
export declare class
|
|
222
|
-
private readonly
|
|
255
|
+
export declare class SharedContext {
|
|
256
|
+
private readonly loader;
|
|
257
|
+
private readonly workspaceRoot;
|
|
223
258
|
private pending;
|
|
224
|
-
constructor(
|
|
225
|
-
get(): Promise<
|
|
259
|
+
constructor(loader: AffectedContextLoader, workspaceRoot: string);
|
|
260
|
+
get(): Promise<AffectedContext | Error>;
|
|
226
261
|
private load;
|
|
227
262
|
}
|
|
228
263
|
export declare class NxImpactScanner {
|
|
229
264
|
private readonly diffScope;
|
|
230
265
|
private readonly nx;
|
|
231
|
-
private readonly
|
|
266
|
+
private readonly loader;
|
|
232
267
|
private readonly names;
|
|
233
|
-
constructor(diffScope?: DiffScope, nx?: NxCli,
|
|
234
|
-
/** Every comparison, in order, sharing one
|
|
268
|
+
constructor(diffScope?: DiffScope, nx?: NxCli, loader?: AffectedContextLoader);
|
|
269
|
+
/** Every comparison, in order, sharing one nx context. */
|
|
235
270
|
scanAll(workspaceRoot: string, graph: EnhancedGraph, comparisons: ImpactComparison[]): Promise<(ImpactReport | ImpactUnavailable)[]>;
|
|
236
271
|
/** One comparison: nx affected from ITS base, with NO `--head`, so uncommitted work counts. */
|
|
237
|
-
scan(workspaceRoot: string, graph: EnhancedGraph, comparison: ImpactComparison,
|
|
272
|
+
scan(workspaceRoot: string, graph: EnhancedGraph, comparison: ImpactComparison, context?: SharedContext): Promise<ImpactReport | ImpactUnavailable>;
|
|
273
|
+
/** nx's locators' answer; any failure makes every affected project "unexplained", with the reason. */
|
|
274
|
+
classify(context: AffectedContext | Error, files: string[], base: string, affected: string[]): Promise<AffectedClassification>;
|
|
238
275
|
/**
|
|
239
276
|
* An empty scan is still an ANSWER (available, zero sets): a fresh branch with a clean tree has
|
|
240
277
|
* simply changed nothing yet, and the page says so instead of disabling Impact.
|
|
241
278
|
*/
|
|
242
279
|
emptyNote(comparison: ImpactComparison, changedFiles: number, short: string): string;
|
|
243
|
-
/** Changed files no project owns; none are claimed when ownership itself was unavailable. */
|
|
244
|
-
globalFiles(files: string[], owners: Map<string, string>): string[];
|
|
245
280
|
/**
|
|
246
281
|
* Every transitive dependency of `roots` that is not itself one of them: a pure downward walk over
|
|
247
282
|
* the graph's own dependsOn edges, no nx call. Of the affected set these are the build inputs; of
|
|
248
283
|
* the touched set, "Changed + dependencies".
|
|
249
284
|
*/
|
|
250
285
|
buildInputs(roots: string[], graph: EnhancedGraph): string[];
|
|
251
|
-
/**
|
|
252
|
-
|
|
253
|
-
/** The
|
|
254
|
-
parse(json: string, graph: EnhancedGraph): string[] | null;
|
|
286
|
+
/** The JSON list of project names, sorted; null when it is not a list of names. */
|
|
287
|
+
parse(json: string): string[] | null;
|
|
288
|
+
/** The drawn projects only: a reason for a project the page does not draw has nowhere to show. */
|
|
255
289
|
private report;
|
|
290
|
+
private empty;
|
|
256
291
|
private ids;
|
|
257
292
|
}
|
|
258
293
|
/** Writes (or removes) the sidecar, self-ignored, beside architecture/dependencies.html. */
|
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,14 @@
|
|
|
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");
|
|
64
63
|
const graph_names_1 = require("./graph-names");
|
|
65
64
|
const toError_1 = require("../toError");
|
|
66
65
|
/** Relative to the architecture/ directory the page lives in; the page's `<script src>`. */
|
|
@@ -95,11 +94,15 @@ class ImpactReport {
|
|
|
95
94
|
reason;
|
|
96
95
|
base;
|
|
97
96
|
touched;
|
|
98
|
-
|
|
97
|
+
dependents;
|
|
98
|
+
implicit;
|
|
99
|
+
rootFile;
|
|
100
|
+
unexplained;
|
|
99
101
|
buildInputs;
|
|
100
102
|
changedFiles;
|
|
101
103
|
dependencies;
|
|
102
104
|
globalFiles;
|
|
105
|
+
reasons;
|
|
103
106
|
constructor(kind,
|
|
104
107
|
/** What the page says this scan compares, e.g. "Changed on this branch (since abc1234)". */
|
|
105
108
|
label,
|
|
@@ -109,10 +112,16 @@ class ImpactReport {
|
|
|
109
112
|
reason,
|
|
110
113
|
/** The base's short sha ('' when there is none). */
|
|
111
114
|
base,
|
|
112
|
-
/** Affected AND owning a changed file (node ids). */
|
|
115
|
+
/** Affected AND owning a changed file (node ids): "changed". */
|
|
113
116
|
touched,
|
|
114
|
-
/** Affected
|
|
115
|
-
|
|
117
|
+
/** Affected through dependency edges from a changed or touched project (node ids). */
|
|
118
|
+
dependents,
|
|
119
|
+
/** Affected because a `{workspaceRoot}` target input matched a changed file (node ids). */
|
|
120
|
+
implicit,
|
|
121
|
+
/** Affected by a lockfile, package.json, tsconfig.base.json, nx.json or deleted-config change (node ids). */
|
|
122
|
+
rootFile,
|
|
123
|
+
/** Affected, and nothing here could say why (node ids). Never shown as a dependent. */
|
|
124
|
+
unexplained,
|
|
116
125
|
/**
|
|
117
126
|
* Transitive dependencies of the affected set that are NOT affected (node ids). `ci` dependsOn
|
|
118
127
|
* `^build`, so they must exist as build output although their inputs did not change: nx
|
|
@@ -122,16 +131,16 @@ class ImpactReport {
|
|
|
122
131
|
buildInputs, changedFiles,
|
|
123
132
|
/**
|
|
124
133
|
* 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.)
|
|
134
|
+
* Filter popover's "Changed + dependencies". ("Changed + dependents" is every affected project.)
|
|
127
135
|
*/
|
|
128
136
|
dependencies,
|
|
129
137
|
/**
|
|
130
138
|
* 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.
|
|
139
|
+
* pnpm-lock.yaml. Empty when nx's locators were unavailable — then nothing is known.
|
|
133
140
|
*/
|
|
134
|
-
globalFiles
|
|
141
|
+
globalFiles,
|
|
142
|
+
/** WHY each affected project is in the build (graph-impact-reasons.ts): one per affected node. */
|
|
143
|
+
reasons) {
|
|
135
144
|
this.kind = kind;
|
|
136
145
|
this.label = label;
|
|
137
146
|
this.dirty = dirty;
|
|
@@ -139,14 +148,41 @@ class ImpactReport {
|
|
|
139
148
|
this.reason = reason;
|
|
140
149
|
this.base = base;
|
|
141
150
|
this.touched = touched;
|
|
142
|
-
this.
|
|
151
|
+
this.dependents = dependents;
|
|
152
|
+
this.implicit = implicit;
|
|
153
|
+
this.rootFile = rootFile;
|
|
154
|
+
this.unexplained = unexplained;
|
|
143
155
|
this.buildInputs = buildInputs;
|
|
144
156
|
this.changedFiles = changedFiles;
|
|
145
157
|
this.dependencies = dependencies;
|
|
146
158
|
this.globalFiles = globalFiles;
|
|
159
|
+
this.reasons = reasons;
|
|
147
160
|
}
|
|
148
161
|
}
|
|
149
162
|
exports.ImpactReport = ImpactReport;
|
|
163
|
+
/** The one-line count split, shared by the executor's console line (the page builds the same line). */
|
|
164
|
+
class ImpactCounts {
|
|
165
|
+
/** `8 changed · 3 dependents · 22 implicitly touched (runtime-wiring-check inputs) · 47 dependencies`. */
|
|
166
|
+
line(report) {
|
|
167
|
+
const parts = [`${report.touched.length} changed`, `${report.dependents.length} dependents`];
|
|
168
|
+
if (report.implicit.length > 0)
|
|
169
|
+
parts.push(`${report.implicit.length} implicitly touched${this.targets(report)}`);
|
|
170
|
+
if (report.rootFile.length > 0)
|
|
171
|
+
parts.push(`${report.rootFile.length} root-file touched`);
|
|
172
|
+
if (report.unexplained.length > 0)
|
|
173
|
+
parts.push(`${report.unexplained.length} unexplained`);
|
|
174
|
+
parts.push(`${report.buildInputs.length} dependencies`);
|
|
175
|
+
return parts.join(' · ');
|
|
176
|
+
}
|
|
177
|
+
targets(report) {
|
|
178
|
+
const targets = new Set();
|
|
179
|
+
for (const reason of report.reasons)
|
|
180
|
+
if (reason.kind === graph_impact_reasons_1.AffectedClass.IMPLICIT && reason.target !== '')
|
|
181
|
+
targets.add(reason.target);
|
|
182
|
+
return targets.size === 0 ? '' : ` (${[...targets].sort().join(', ')} inputs)`;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
exports.ImpactCounts = ImpactCounts;
|
|
150
186
|
/** The sidecar's one global: every scan, and the one the page shows first. */
|
|
151
187
|
class ImpactSidecarData {
|
|
152
188
|
scans;
|
|
@@ -311,28 +347,50 @@ class NxCli {
|
|
|
311
347
|
}
|
|
312
348
|
}
|
|
313
349
|
exports.NxCli = NxCli;
|
|
314
|
-
/**
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
350
|
+
/**
|
|
351
|
+
* The workspace's nx facts every comparison classifies against: its project graph (with npm nodes
|
|
352
|
+
* and every edge), nx.json, package.json, and nx's own affected locators. Subclassed by specs.
|
|
353
|
+
*/
|
|
354
|
+
class AffectedContext {
|
|
355
|
+
graph;
|
|
356
|
+
nxJson;
|
|
357
|
+
packageJson;
|
|
358
|
+
classifier;
|
|
359
|
+
nx;
|
|
360
|
+
constructor(graph, nxJson, packageJson, classifier, nx) {
|
|
361
|
+
this.graph = graph;
|
|
362
|
+
this.nxJson = nxJson;
|
|
363
|
+
this.packageJson = packageJson;
|
|
364
|
+
this.classifier = classifier;
|
|
365
|
+
this.nx = nx;
|
|
366
|
+
}
|
|
367
|
+
/** nx's FileChange list for `files`, base → working tree. */
|
|
368
|
+
fileChanges(files, base) {
|
|
369
|
+
if (this.nx === null)
|
|
370
|
+
throw new Error('no workspace nx to read file changes with');
|
|
371
|
+
return this.nx.fileChanges(files, base);
|
|
324
372
|
}
|
|
325
373
|
}
|
|
326
|
-
exports.
|
|
374
|
+
exports.AffectedContext = AffectedContext;
|
|
375
|
+
/** Loads the workspace's nx facts. Injected so specs never load the real project graph. */
|
|
376
|
+
class AffectedContextLoader {
|
|
377
|
+
async load(workspaceRoot) {
|
|
378
|
+
const nx = new graph_impact_reasons_1.WorkspaceNx(workspaceRoot);
|
|
379
|
+
return new AffectedContext(await (0, devkit_1.createProjectGraphAsync)(), nx.nxJson(), nx.packageJson(), new graph_impact_reasons_1.AffectedClassifier(nx.locators()), nx);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
exports.AffectedContextLoader = AffectedContextLoader;
|
|
327
383
|
/**
|
|
328
|
-
*
|
|
329
|
-
*
|
|
384
|
+
* The context, loaded at most ONCE however many comparisons ask for it. Failing is not fatal: every
|
|
385
|
+
* affected project is then "unexplained", with this error as its reason — never a dependent.
|
|
330
386
|
*/
|
|
331
|
-
class
|
|
332
|
-
|
|
387
|
+
class SharedContext {
|
|
388
|
+
loader;
|
|
389
|
+
workspaceRoot;
|
|
333
390
|
pending = null;
|
|
334
|
-
constructor(
|
|
335
|
-
this.
|
|
391
|
+
constructor(loader, workspaceRoot) {
|
|
392
|
+
this.loader = loader;
|
|
393
|
+
this.workspaceRoot = workspaceRoot;
|
|
336
394
|
}
|
|
337
395
|
get() {
|
|
338
396
|
if (this.pending === null)
|
|
@@ -340,60 +398,45 @@ class SharedOwners {
|
|
|
340
398
|
return this.pending;
|
|
341
399
|
}
|
|
342
400
|
async load() {
|
|
343
|
-
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- the
|
|
401
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- the reasons are optional; the affected set still shows
|
|
344
402
|
try {
|
|
345
|
-
return await this.
|
|
403
|
+
return await this.loader.load(this.workspaceRoot);
|
|
346
404
|
}
|
|
347
405
|
catch (err) {
|
|
348
406
|
const error = (0, toError_1.toError)(err);
|
|
349
|
-
console.warn(`⚠️ Impact: nx
|
|
350
|
-
return
|
|
407
|
+
console.warn(`⚠️ Impact: nx's affected locators unavailable (${error.message}); reasons unknown`);
|
|
408
|
+
return error;
|
|
351
409
|
}
|
|
352
410
|
}
|
|
353
411
|
}
|
|
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
|
-
}
|
|
412
|
+
exports.SharedContext = SharedContext;
|
|
370
413
|
class NxImpactScanner {
|
|
371
414
|
diffScope;
|
|
372
415
|
nx;
|
|
373
|
-
|
|
416
|
+
loader;
|
|
374
417
|
names = new graph_names_1.GraphNames();
|
|
375
|
-
constructor(diffScope = new rules_config_1.DiffScope(), nx = new NxCli(),
|
|
418
|
+
constructor(diffScope = new rules_config_1.DiffScope(), nx = new NxCli(), loader = new AffectedContextLoader()) {
|
|
376
419
|
this.diffScope = diffScope;
|
|
377
420
|
this.nx = nx;
|
|
378
|
-
this.
|
|
421
|
+
this.loader = loader;
|
|
379
422
|
}
|
|
380
|
-
/** Every comparison, in order, sharing one
|
|
423
|
+
/** Every comparison, in order, sharing one nx context. */
|
|
381
424
|
async scanAll(workspaceRoot, graph, comparisons) {
|
|
382
|
-
const
|
|
425
|
+
const context = new SharedContext(this.loader, workspaceRoot);
|
|
383
426
|
const results = [];
|
|
384
427
|
for (const comparison of comparisons)
|
|
385
|
-
results.push(await this.scan(workspaceRoot, graph, comparison,
|
|
428
|
+
results.push(await this.scan(workspaceRoot, graph, comparison, context));
|
|
386
429
|
return results;
|
|
387
430
|
}
|
|
388
431
|
/** One comparison: nx affected from ITS base, with NO `--head`, so uncommitted work counts. */
|
|
389
|
-
async scan(workspaceRoot, graph, comparison,
|
|
432
|
+
async scan(workspaceRoot, graph, comparison, context = new SharedContext(this.loader, workspaceRoot)) {
|
|
390
433
|
const base = comparison.base;
|
|
391
434
|
if (base === null)
|
|
392
|
-
return this.
|
|
435
|
+
return this.empty(comparison, false, comparison.reason, '', 0);
|
|
393
436
|
const json = this.nx.run(workspaceRoot, ['show', 'projects', '--affected', `--base=${base}`, '--json'], NX_TIMEOUT_MS);
|
|
394
437
|
if (json === null)
|
|
395
438
|
return new ImpactUnavailable(`nx show projects --affected failed or took over ${NX_TIMEOUT_MS / 1000}s`, comparison.label);
|
|
396
|
-
const affectedSet = this.parse(json
|
|
439
|
+
const affectedSet = this.parse(json);
|
|
397
440
|
if (affectedSet === null)
|
|
398
441
|
return new ImpactUnavailable('nx show projects --affected printed something other than a JSON list', comparison.label);
|
|
399
442
|
const options = new rules_config_1.ChangedFilesOptions();
|
|
@@ -401,13 +444,26 @@ class NxImpactScanner {
|
|
|
401
444
|
options.includeDeletions = true;
|
|
402
445
|
const files = this.diffScope.getChangedFiles(workspaceRoot, base, undefined, options);
|
|
403
446
|
const short = base.slice(0, 7);
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
447
|
+
const drawn = affectedSet.filter((project) => graph[project] !== undefined);
|
|
448
|
+
if (drawn.length === 0)
|
|
449
|
+
return this.empty(comparison, true, this.emptyNote(comparison, files.length, short), short, files.length);
|
|
450
|
+
const classification = await this.classify(await context.get(), files, base, affectedSet);
|
|
451
|
+
return this.report(comparison, short, files.length, graph, classification);
|
|
452
|
+
}
|
|
453
|
+
/** nx's locators' answer; any failure makes every affected project "unexplained", with the reason. */
|
|
454
|
+
async classify(context, files, base, affected) {
|
|
455
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- the reasons are optional; a locator failure leaves them unknown
|
|
456
|
+
try {
|
|
457
|
+
if (context instanceof Error)
|
|
458
|
+
throw context;
|
|
459
|
+
const changes = context.fileChanges(files, base);
|
|
460
|
+
return await context.classifier.classify(new graph_impact_reasons_1.AffectedInputs(context.graph, context.nxJson, context.packageJson, changes, affected));
|
|
461
|
+
}
|
|
462
|
+
catch (err) {
|
|
463
|
+
const error = (0, toError_1.toError)(err);
|
|
464
|
+
const why = `nx's affected locators could not run here (${error.message}), so why nx marked it is unknown`;
|
|
465
|
+
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)), []);
|
|
466
|
+
}
|
|
411
467
|
}
|
|
412
468
|
/**
|
|
413
469
|
* An empty scan is still an ANSWER (available, zero sets): a fresh branch with a clean tree has
|
|
@@ -420,12 +476,6 @@ class NxImpactScanner {
|
|
|
420
476
|
return `Nothing changed since ${short}`;
|
|
421
477
|
return `Nothing affected since ${short}`;
|
|
422
478
|
}
|
|
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
479
|
/**
|
|
430
480
|
* Every transitive dependency of `roots` that is not itself one of them: a pure downward walk over
|
|
431
481
|
* the graph's own dependsOn edges, no nx call. Of the affected set these are the build inputs; of
|
|
@@ -449,27 +499,15 @@ class NxImpactScanner {
|
|
|
449
499
|
}
|
|
450
500
|
return inputs.sort();
|
|
451
501
|
}
|
|
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) {
|
|
502
|
+
/** The JSON list of project names, sorted; null when it is not a list of names. */
|
|
503
|
+
parse(json) {
|
|
464
504
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- untrusted CLI output; anything unparseable is reported as a reason
|
|
465
505
|
try {
|
|
466
506
|
// webpieces-disable no-any-unknown -- CLI JSON is untrusted until narrowed below
|
|
467
507
|
const parsed = JSON.parse(json);
|
|
468
508
|
if (!Array.isArray(parsed))
|
|
469
509
|
return null;
|
|
470
|
-
return parsed
|
|
471
|
-
.filter((name) => typeof name === 'string' && graph[name] !== undefined)
|
|
472
|
-
.sort();
|
|
510
|
+
return parsed.filter((name) => typeof name === 'string').sort();
|
|
473
511
|
}
|
|
474
512
|
catch (err) {
|
|
475
513
|
const error = (0, toError_1.toError)(err);
|
|
@@ -477,8 +515,15 @@ class NxImpactScanner {
|
|
|
477
515
|
return null;
|
|
478
516
|
}
|
|
479
517
|
}
|
|
480
|
-
|
|
481
|
-
|
|
518
|
+
/** The drawn projects only: a reason for a project the page does not draw has nowhere to show. */
|
|
519
|
+
report(comparison, short, changedFiles, graph, classification) {
|
|
520
|
+
const drawn = (projects) => projects.filter((project) => graph[project] !== undefined);
|
|
521
|
+
const touched = drawn(classification.of(graph_impact_reasons_1.AffectedClass.CHANGED));
|
|
522
|
+
const affected = drawn(classification.reasons.map((reason) => reason.project));
|
|
523
|
+
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));
|
|
524
|
+
}
|
|
525
|
+
empty(comparison, available, reason, base, changedFiles) {
|
|
526
|
+
return new ImpactReport(comparison.kind, comparison.label, comparison.dirty, available, reason, base, [], [], [], [], [], [], changedFiles, [], [], []);
|
|
482
527
|
}
|
|
483
528
|
ids(projects) {
|
|
484
529
|
return projects.map((project) => this.names.getNodeId(project));
|
|
@@ -571,8 +616,7 @@ class ImpactRefresh {
|
|
|
571
616
|
return `ℹ️ Impact · ${result.label}: unavailable — ${result.reason}`;
|
|
572
617
|
if (result.reason !== '')
|
|
573
618
|
return `✅ Impact · ${result.label}: ${result.reason}`;
|
|
574
|
-
return
|
|
575
|
-
`${result.buildInputs.length} dependencies, from ${result.changedFiles} changed file(s)`);
|
|
619
|
+
return `✅ Impact · ${result.label}: ${new ImpactCounts().line(result)}, from ${result.changedFiles} changed file(s)`;
|
|
576
620
|
}
|
|
577
621
|
}
|
|
578
622
|
exports.ImpactRefresh = ImpactRefresh;
|