@webpieces/nx-webpieces-rules 0.4.874 → 0.4.876
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/README.md +15 -7
- package/package.json +7 -7
- package/src/executors/generate/executor.js +4 -3
- package/src/executors/generate/executor.js.map +1 -1
- package/src/executors/visualize/executor.d.ts +8 -5
- package/src/executors/visualize/executor.js +10 -7
- package/src/executors/visualize/executor.js.map +1 -1
- package/src/lib/graph-color-modes.d.ts +3 -3
- package/src/lib/graph-color-modes.js +4 -4
- package/src/lib/graph-color-modes.js.map +1 -1
- package/src/lib/graph-impact.d.ts +168 -40
- package/src/lib/graph-impact.js +326 -74
- package/src/lib/graph-impact.js.map +1 -1
- package/src/lib/graph-legend.js +1 -1
- package/src/lib/graph-legend.js.map +1 -1
- package/src/lib/graph-page-shell.d.ts +6 -0
- package/src/lib/graph-page-shell.js +14 -3
- package/src/lib/graph-page-shell.js.map +1 -1
- package/src/lib/graph-page-styles.js +3 -0
- package/src/lib/graph-page-styles.js.map +1 -1
- package/src/lib/graph-visualizer.client.d.ts +24 -3
- package/src/lib/graph-visualizer.client.js +100 -15
- package/src/lib/graph-visualizer.client.js.map +1 -1
package/src/lib/graph-impact.js
CHANGED
|
@@ -1,44 +1,60 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Impact mode's data: what
|
|
3
|
+
* Impact mode's data: what one COMPARISON's build looks like, in three shades (plus untouched grey).
|
|
4
4
|
*
|
|
5
|
+
* There are two comparisons, and only two (#1163). Both end at the WORKING TREE, so uncommitted and
|
|
6
|
+
* untracked work counts in either:
|
|
7
|
+
*
|
|
8
|
+
* - BRANCH, "Changed on this branch": the fork point (`DiffScope.detectBase` in rules-config:
|
|
9
|
+
* `git merge-base HEAD origin/main`, falling back to `main`) → working tree.
|
|
10
|
+
* - COMMIT, "Last commit": `HEAD^` → working tree. To see what another commit changed, check it out
|
|
11
|
+
* (`git checkout <hash>`) and run `pnpm arch:visualize`: nx computes `--affected` against the code
|
|
12
|
+
* that is actually on disk, so nothing here ever jumps to another commit.
|
|
13
|
+
*
|
|
14
|
+
* Which of them exist is decided by `ImpactScopeResolver` from where HEAD is. On `main` (by BRANCH
|
|
15
|
+
* NAME, so a local main with unpushed commits still counts as main) and on a detached HEAD only the
|
|
16
|
+
* last commit exists — the fork point IS HEAD there, so the branch comparison would always be empty.
|
|
17
|
+
* On a feature branch with commits past its fork point both exist, the branch one first. On a FRESH
|
|
18
|
+
* feature branch (fork point = HEAD) only the branch one exists: falling through to "last commit"
|
|
19
|
+
* would show main's last PR as if it were the branch's own work.
|
|
20
|
+
*
|
|
21
|
+
* Per comparison:
|
|
5
22
|
* - BUILD INPUT is every transitive dependency of the affected set that is not itself affected:
|
|
6
23
|
* `ci` dependsOn `^build`, so they must exist as build output though their inputs did not change.
|
|
7
24
|
* nx restores them from cache when it has them and compiles them otherwise — which one happens is
|
|
8
25
|
* not knowable here, so nothing claims it. A pure downward walk over the graph's own edges.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
26
|
+
* - AFFECTED is `nx show projects --affected --base=<the comparison's base> --json`, with NO
|
|
27
|
+
* `--head`, so uncommitted and untracked work counts. nx already gets right everything a
|
|
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. This uses its public
|
|
30
|
+
* CLI surface only, never nx internals, and never NX_BASE/NX_HEAD, whose value depends on whoever
|
|
31
|
+
* launched the process.
|
|
15
32
|
* - TOUCHED is the subset of that set which OWNS a changed file, by nx's own file-to-project
|
|
16
33
|
* ownership (the project file map from `createProjectFileMapUsingProjectGraph`, public devkit API)
|
|
17
|
-
* — never a path-prefix guess.
|
|
18
|
-
* (the file is gone), so it shows
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* `git merge-base HEAD origin/main`, falling back to `main`) — never NX_BASE/NX_HEAD, whose value
|
|
22
|
-
* depends on whoever launched the process. The changed-file list is `DiffScope.getChangedFiles`, the
|
|
23
|
-
* same base→working-tree diff (with untracked files) every code rule uses.
|
|
34
|
+
* — never a path-prefix guess. The ownership map is computed ONCE and shared by every comparison.
|
|
35
|
+
* A project whose only change is a DELETED file is not in that map (the file is gone), so it shows
|
|
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.
|
|
24
38
|
*
|
|
25
|
-
* The answer changes per
|
|
26
|
-
* dependencies.json (both stay byte-stable
|
|
27
|
-
* `architecture/.impact/dependencies.impact.js`: a plain script setting one window global,
|
|
28
|
-
* with `<script src>` — the one way a page
|
|
29
|
-
* neighbouring file. The directory carries its own
|
|
30
|
-
* directory and everything in it, so a consumer repo
|
|
31
|
-
* "build left nothing uncommitted" check never sees it.
|
|
39
|
+
* The answer changes per checkout, so it never goes into the committed dependencies.html or
|
|
40
|
+
* dependencies.json (both stay byte-stable). It goes into a SIDECAR,
|
|
41
|
+
* `architecture/.impact/dependencies.impact.js`: a plain script setting one window global,
|
|
42
|
+
* `window.__WP_IMPACT__ = { scans, defaultKind }`, loaded with `<script src>` — the one way a page
|
|
43
|
+
* opened straight from disk (file://) can read a neighbouring file. The directory carries its own
|
|
44
|
+
* `.gitignore` containing `*`, which ignores the directory and everything in it, so a consumer repo
|
|
45
|
+
* needs no gitignore edit and the PR gate's "build left nothing uncommitted" check never sees it.
|
|
32
46
|
*
|
|
33
|
-
*
|
|
34
|
-
* branch
|
|
35
|
-
*
|
|
47
|
+
* `architecture:visualize` scans every comparison that exists. `architecture:generate` refreshes
|
|
48
|
+
* ONLY the branch comparison (`GENERATE_IMPACT_KINDS`) and never scans the last commit; on main or a
|
|
49
|
+
* detached HEAD that leaves nothing to write, so it removes the sidecar and the page says to run
|
|
50
|
+
* `pnpm arch:visualize`.
|
|
36
51
|
*
|
|
37
|
-
* Impact is a convenience. When nx or git cannot answer, NO sidecar is written (a
|
|
38
|
-
* removed) and the page disables Impact with a one-line reason. It never fails
|
|
52
|
+
* Impact is a convenience. When nx or git cannot answer any comparison, NO sidecar is written (a
|
|
53
|
+
* stale one is removed) and the page disables Impact with a one-line reason. It never fails
|
|
54
|
+
* generation.
|
|
39
55
|
*/
|
|
40
56
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
-
exports.ImpactRefresh = exports.ImpactSidecar = exports.NxImpactScanner = exports.ProjectFileOwners = exports.NxCli = exports.ImpactUnavailable = exports.ImpactReport = exports.IMPACT_SIDECAR_SRC = exports.IMPACT_SIDECAR_FILE = exports.IMPACT_SIDECAR_DIR = void 0;
|
|
57
|
+
exports.ImpactRefresh = exports.ImpactSidecar = exports.NxImpactScanner = exports.SharedOwners = exports.ProjectFileOwners = exports.NxCli = exports.ImpactScopeResolver = exports.ImpactGit = exports.ImpactPlan = exports.ImpactComparison = exports.ImpactUnavailable = exports.ImpactSidecarData = 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;
|
|
42
58
|
const tslib_1 = require("tslib");
|
|
43
59
|
const child_process_1 = require("child_process");
|
|
44
60
|
const fs = tslib_1.__importStar(require("fs"));
|
|
@@ -51,10 +67,30 @@ const toError_1 = require("../toError");
|
|
|
51
67
|
exports.IMPACT_SIDECAR_DIR = '.impact';
|
|
52
68
|
exports.IMPACT_SIDECAR_FILE = 'dependencies.impact.js';
|
|
53
69
|
exports.IMPACT_SIDECAR_SRC = `${exports.IMPACT_SIDECAR_DIR}/${exports.IMPACT_SIDECAR_FILE}`;
|
|
54
|
-
/**
|
|
70
|
+
/** Each nx call is capped; a slow or wedged nx disables that comparison instead of stalling generation. */
|
|
55
71
|
const NX_TIMEOUT_MS = 15000;
|
|
56
|
-
/**
|
|
72
|
+
/** The branch whose name means "there is no branch comparison here, only the last commit". */
|
|
73
|
+
const MAIN_BRANCH = 'main';
|
|
74
|
+
exports.NOTHING_CHANGED_ON_BRANCH = 'Nothing changed on this branch yet';
|
|
75
|
+
exports.ROOT_COMMIT_REASON = "HEAD is the repository's first commit; there is no previous commit to compare against";
|
|
76
|
+
exports.UNCOMMITTED_SUFFIX = ' + uncommitted changes';
|
|
77
|
+
/** Which of the two comparisons a scan answers. */
|
|
78
|
+
var ImpactKind;
|
|
79
|
+
(function (ImpactKind) {
|
|
80
|
+
/** Fork point → working tree: "Changed on this branch". */
|
|
81
|
+
ImpactKind["BRANCH"] = "branch";
|
|
82
|
+
/** HEAD^ → working tree: "Last commit". */
|
|
83
|
+
ImpactKind["COMMIT"] = "commit";
|
|
84
|
+
})(ImpactKind || (exports.ImpactKind = ImpactKind = {}));
|
|
85
|
+
/** `architecture:visualize` scans every comparison that exists. */
|
|
86
|
+
exports.VISUALIZE_IMPACT_KINDS = [ImpactKind.BRANCH, ImpactKind.COMMIT];
|
|
87
|
+
/** `architecture:generate` refreshes the branch comparison only and never scans the last commit. */
|
|
88
|
+
exports.GENERATE_IMPACT_KINDS = [ImpactKind.BRANCH];
|
|
89
|
+
/** One scan as the sidecar carries it; the page reads `window.__WP_IMPACT__.scans`. */
|
|
57
90
|
class ImpactReport {
|
|
91
|
+
kind;
|
|
92
|
+
label;
|
|
93
|
+
dirty;
|
|
58
94
|
available;
|
|
59
95
|
reason;
|
|
60
96
|
base;
|
|
@@ -64,7 +100,15 @@ class ImpactReport {
|
|
|
64
100
|
changedFiles;
|
|
65
101
|
dependencies;
|
|
66
102
|
globalFiles;
|
|
67
|
-
constructor(
|
|
103
|
+
constructor(kind,
|
|
104
|
+
/** What the page says this scan compares, e.g. "Changed on this branch (since abc1234)". */
|
|
105
|
+
label,
|
|
106
|
+
/** The working tree had uncommitted or untracked changes (they count in the scan). */
|
|
107
|
+
dirty, available,
|
|
108
|
+
/** Why the scan is unavailable; for an available but EMPTY scan, the note the page shows. */
|
|
109
|
+
reason,
|
|
110
|
+
/** The base's short sha ('' when there is none). */
|
|
111
|
+
base,
|
|
68
112
|
/** Affected AND owning a changed file (node ids). */
|
|
69
113
|
touched,
|
|
70
114
|
/** Affected but owning no changed file (node ids): their ci re-runs. */
|
|
@@ -88,6 +132,9 @@ class ImpactReport {
|
|
|
88
132
|
* legend names them. Empty when nx's file ownership was unavailable — then nothing is known.
|
|
89
133
|
*/
|
|
90
134
|
globalFiles) {
|
|
135
|
+
this.kind = kind;
|
|
136
|
+
this.label = label;
|
|
137
|
+
this.dirty = dirty;
|
|
91
138
|
this.available = available;
|
|
92
139
|
this.reason = reason;
|
|
93
140
|
this.base = base;
|
|
@@ -100,14 +147,138 @@ class ImpactReport {
|
|
|
100
147
|
}
|
|
101
148
|
}
|
|
102
149
|
exports.ImpactReport = ImpactReport;
|
|
103
|
-
/**
|
|
150
|
+
/** The sidecar's one global: every scan, and the one the page shows first. */
|
|
151
|
+
class ImpactSidecarData {
|
|
152
|
+
scans;
|
|
153
|
+
defaultKind;
|
|
154
|
+
constructor(scans, defaultKind) {
|
|
155
|
+
this.scans = scans;
|
|
156
|
+
this.defaultKind = defaultKind;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
exports.ImpactSidecarData = ImpactSidecarData;
|
|
160
|
+
/** A scan (or a whole plan) that could not produce an answer. */
|
|
104
161
|
class ImpactUnavailable {
|
|
105
162
|
reason;
|
|
106
|
-
|
|
163
|
+
label;
|
|
164
|
+
constructor(reason, label = 'Impact') {
|
|
107
165
|
this.reason = reason;
|
|
166
|
+
this.label = label;
|
|
108
167
|
}
|
|
109
168
|
}
|
|
110
169
|
exports.ImpactUnavailable = ImpactUnavailable;
|
|
170
|
+
/** One comparison to scan. `base` is a full sha; null when it does not exist (`reason` says why). */
|
|
171
|
+
class ImpactComparison {
|
|
172
|
+
kind;
|
|
173
|
+
base;
|
|
174
|
+
label;
|
|
175
|
+
dirty;
|
|
176
|
+
reason;
|
|
177
|
+
constructor(kind, base, label, dirty, reason) {
|
|
178
|
+
this.kind = kind;
|
|
179
|
+
this.base = base;
|
|
180
|
+
this.label = label;
|
|
181
|
+
this.dirty = dirty;
|
|
182
|
+
this.reason = reason;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
exports.ImpactComparison = ImpactComparison;
|
|
186
|
+
/** The comparisons that exist where HEAD is, and which one the page shows first. */
|
|
187
|
+
class ImpactPlan {
|
|
188
|
+
comparisons;
|
|
189
|
+
defaultKind;
|
|
190
|
+
constructor(comparisons, defaultKind) {
|
|
191
|
+
this.comparisons = comparisons;
|
|
192
|
+
this.defaultKind = defaultKind;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
exports.ImpactPlan = ImpactPlan;
|
|
196
|
+
/** The few git facts the resolver needs. Injected so specs never spawn git. */
|
|
197
|
+
class ImpactGit {
|
|
198
|
+
/** The checked-out branch's short name; null on a detached HEAD (or without git). */
|
|
199
|
+
branch(root) {
|
|
200
|
+
return this.run(root, ['symbolic-ref', '--short', '-q', 'HEAD']);
|
|
201
|
+
}
|
|
202
|
+
/** The full sha `ref` names; null when it does not resolve (e.g. `HEAD^` on the first commit). */
|
|
203
|
+
sha(root, ref) {
|
|
204
|
+
return this.run(root, ['rev-parse', '--verify', '-q', `${ref}^{commit}`]);
|
|
205
|
+
}
|
|
206
|
+
/** `git status --porcelain` is non-empty: uncommitted or untracked work. */
|
|
207
|
+
dirty(root) {
|
|
208
|
+
return (this.run(root, ['status', '--porcelain']) ?? '') !== '';
|
|
209
|
+
}
|
|
210
|
+
/** HEAD's subject line ('' when unknown). */
|
|
211
|
+
subject(root) {
|
|
212
|
+
return this.run(root, ['log', '-1', '--format=%s', 'HEAD']) ?? '';
|
|
213
|
+
}
|
|
214
|
+
/** Trimmed stdout, or null on any failure. */
|
|
215
|
+
run(root, args) {
|
|
216
|
+
const result = (0, child_process_1.spawnSync)('git', args, { cwd: root, encoding: 'utf8', stdio: 'pipe' });
|
|
217
|
+
if (result.error !== undefined || result.status !== 0)
|
|
218
|
+
return null;
|
|
219
|
+
return result.stdout.trim();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
exports.ImpactGit = ImpactGit;
|
|
223
|
+
/**
|
|
224
|
+
* Decides which comparisons exist from where HEAD is (#1163):
|
|
225
|
+
*
|
|
226
|
+
* | where | comparisons | default |
|
|
227
|
+
* |---|---|---|
|
|
228
|
+
* | `main` (by branch NAME) | last commit | last commit |
|
|
229
|
+
* | detached HEAD | last commit | last commit |
|
|
230
|
+
* | feature branch with commits past the fork point | branch, last commit | branch |
|
|
231
|
+
* | fresh feature branch (fork point = HEAD) | branch | branch |
|
|
232
|
+
*
|
|
233
|
+
* HEAD with no parent leaves the last commit unavailable, with `ROOT_COMMIT_REASON`.
|
|
234
|
+
*/
|
|
235
|
+
class ImpactScopeResolver {
|
|
236
|
+
git;
|
|
237
|
+
diffScope;
|
|
238
|
+
constructor(git = new ImpactGit(), diffScope = new rules_config_1.DiffScope()) {
|
|
239
|
+
this.git = git;
|
|
240
|
+
this.diffScope = diffScope;
|
|
241
|
+
}
|
|
242
|
+
resolve(root) {
|
|
243
|
+
const head = this.git.sha(root, 'HEAD');
|
|
244
|
+
if (head === null)
|
|
245
|
+
return new ImpactUnavailable('git cannot resolve HEAD (not a git checkout, or git is unavailable)');
|
|
246
|
+
const branch = this.git.branch(root);
|
|
247
|
+
const dirty = this.git.dirty(root);
|
|
248
|
+
const commit = this.lastCommit(root, dirty);
|
|
249
|
+
// On main the decision is the branch NAME, never merge-base: unpushed commits still mean "last commit".
|
|
250
|
+
if (branch === null || branch === MAIN_BRANCH)
|
|
251
|
+
return new ImpactPlan([commit], ImpactKind.COMMIT);
|
|
252
|
+
const fork = this.diffScope.detectBase(root);
|
|
253
|
+
if (fork === null) {
|
|
254
|
+
const reason = 'no fork point: neither origin/main nor main resolves';
|
|
255
|
+
return new ImpactPlan([new ImpactComparison(ImpactKind.BRANCH, null, 'Changed on this branch', dirty, reason)], ImpactKind.BRANCH);
|
|
256
|
+
}
|
|
257
|
+
const onBranch = new ImpactComparison(ImpactKind.BRANCH, fork, `Changed on this branch (since ${this.short(fork)})${dirty ? exports.UNCOMMITTED_SUFFIX : ''}`, dirty, '');
|
|
258
|
+
// A fresh branch has no commit of its own: "last commit" would be main's last PR, not its work.
|
|
259
|
+
if (this.sameCommit(fork, head))
|
|
260
|
+
return new ImpactPlan([onBranch], ImpactKind.BRANCH);
|
|
261
|
+
return new ImpactPlan([onBranch, commit], ImpactKind.BRANCH);
|
|
262
|
+
}
|
|
263
|
+
lastCommit(root, dirty) {
|
|
264
|
+
const parent = this.git.sha(root, 'HEAD^');
|
|
265
|
+
if (parent === null)
|
|
266
|
+
return new ImpactComparison(ImpactKind.COMMIT, null, 'Last commit', dirty, exports.ROOT_COMMIT_REASON);
|
|
267
|
+
const subject = this.git.subject(root);
|
|
268
|
+
const label = `Changed since ${this.short(parent)}${subject === '' ? '' : ` — ${subject}`}` +
|
|
269
|
+
(dirty ? exports.UNCOMMITTED_SUFFIX : '');
|
|
270
|
+
return new ImpactComparison(ImpactKind.COMMIT, parent, label, dirty, '');
|
|
271
|
+
}
|
|
272
|
+
/** detectBase may hand back a full or an abbreviated sha; compare on the shorter of the two. */
|
|
273
|
+
sameCommit(a, b) {
|
|
274
|
+
const length = Math.min(a.length, b.length);
|
|
275
|
+
return length > 0 && a.slice(0, length) === b.slice(0, length);
|
|
276
|
+
}
|
|
277
|
+
short(sha) {
|
|
278
|
+
return sha.slice(0, 7);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
exports.ImpactScopeResolver = ImpactScopeResolver;
|
|
111
282
|
/** Runs the workspace's own nx CLI. Injected so specs never spawn nx. */
|
|
112
283
|
class NxCli {
|
|
113
284
|
/** stdout, or null when nx cannot be found, exits non-zero or exceeds the timeout. */
|
|
@@ -153,6 +324,49 @@ class ProjectFileOwners {
|
|
|
153
324
|
}
|
|
154
325
|
}
|
|
155
326
|
exports.ProjectFileOwners = ProjectFileOwners;
|
|
327
|
+
/**
|
|
328
|
+
* nx's ownership map, computed at most ONCE however many comparisons ask for it. Failing is not
|
|
329
|
+
* fatal: the set then shows in one shade, all "affected".
|
|
330
|
+
*/
|
|
331
|
+
class SharedOwners {
|
|
332
|
+
ownership;
|
|
333
|
+
pending = null;
|
|
334
|
+
constructor(ownership) {
|
|
335
|
+
this.ownership = ownership;
|
|
336
|
+
}
|
|
337
|
+
get() {
|
|
338
|
+
if (this.pending === null)
|
|
339
|
+
this.pending = this.load();
|
|
340
|
+
return this.pending;
|
|
341
|
+
}
|
|
342
|
+
async load() {
|
|
343
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- the second shade is optional
|
|
344
|
+
try {
|
|
345
|
+
return await this.ownership.owners();
|
|
346
|
+
}
|
|
347
|
+
catch (err) {
|
|
348
|
+
const error = (0, toError_1.toError)(err);
|
|
349
|
+
console.warn(`⚠️ Impact: nx file ownership unavailable (${error.message}); showing one shade`);
|
|
350
|
+
return new Map();
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
exports.SharedOwners = SharedOwners;
|
|
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
|
+
}
|
|
156
370
|
class NxImpactScanner {
|
|
157
371
|
diffScope;
|
|
158
372
|
nx;
|
|
@@ -163,28 +377,48 @@ class NxImpactScanner {
|
|
|
163
377
|
this.nx = nx;
|
|
164
378
|
this.ownership = ownership;
|
|
165
379
|
}
|
|
166
|
-
|
|
167
|
-
|
|
380
|
+
/** Every comparison, in order, sharing one ownership map. */
|
|
381
|
+
async scanAll(workspaceRoot, graph, comparisons) {
|
|
382
|
+
const owners = new SharedOwners(this.ownership);
|
|
383
|
+
const results = [];
|
|
384
|
+
for (const comparison of comparisons)
|
|
385
|
+
results.push(await this.scan(workspaceRoot, graph, comparison, owners));
|
|
386
|
+
return results;
|
|
387
|
+
}
|
|
388
|
+
/** One comparison: nx affected from ITS base, with NO `--head`, so uncommitted work counts. */
|
|
389
|
+
async scan(workspaceRoot, graph, comparison, owners = new SharedOwners(this.ownership)) {
|
|
390
|
+
const base = comparison.base;
|
|
168
391
|
if (base === null)
|
|
169
|
-
return
|
|
392
|
+
return this.report(comparison, false, comparison.reason, '', 0, new ImpactSets());
|
|
170
393
|
const json = this.nx.run(workspaceRoot, ['show', 'projects', '--affected', `--base=${base}`, '--json'], NX_TIMEOUT_MS);
|
|
171
394
|
if (json === null)
|
|
172
|
-
return new ImpactUnavailable(`nx show projects --affected failed or took over ${NX_TIMEOUT_MS / 1000}s
|
|
395
|
+
return new ImpactUnavailable(`nx show projects --affected failed or took over ${NX_TIMEOUT_MS / 1000}s`, comparison.label);
|
|
173
396
|
const affectedSet = this.parse(json, graph);
|
|
174
397
|
if (affectedSet === null)
|
|
175
|
-
return new ImpactUnavailable('nx show projects --affected printed something other than a JSON list');
|
|
398
|
+
return new ImpactUnavailable('nx show projects --affected printed something other than a JSON list', comparison.label);
|
|
176
399
|
const options = new rules_config_1.ChangedFilesOptions();
|
|
177
400
|
options.tsOnly = false;
|
|
178
401
|
options.includeDeletions = true;
|
|
179
402
|
const files = this.diffScope.getChangedFiles(workspaceRoot, base, undefined, options);
|
|
180
403
|
const short = base.slice(0, 7);
|
|
181
404
|
if (affectedSet.length === 0)
|
|
182
|
-
return
|
|
183
|
-
const
|
|
184
|
-
const touched = this.touched(affectedSet, files,
|
|
405
|
+
return this.report(comparison, true, this.emptyNote(comparison, files.length, short), short, files.length, new ImpactSets());
|
|
406
|
+
const map = await owners.get();
|
|
407
|
+
const touched = this.touched(affectedSet, files, map);
|
|
185
408
|
const rest = affectedSet.filter((project) => !touched.includes(project));
|
|
186
|
-
const
|
|
187
|
-
return
|
|
409
|
+
const sets = new ImpactSets(this.ids(touched), this.ids(rest), this.ids(this.buildInputs(affectedSet, graph)), this.ids(this.buildInputs(touched, graph)), this.globalFiles(files, map));
|
|
410
|
+
return this.report(comparison, true, '', short, files.length, sets);
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* An empty scan is still an ANSWER (available, zero sets): a fresh branch with a clean tree has
|
|
414
|
+
* simply changed nothing yet, and the page says so instead of disabling Impact.
|
|
415
|
+
*/
|
|
416
|
+
emptyNote(comparison, changedFiles, short) {
|
|
417
|
+
if (changedFiles === 0 && comparison.kind === ImpactKind.BRANCH)
|
|
418
|
+
return exports.NOTHING_CHANGED_ON_BRANCH;
|
|
419
|
+
if (changedFiles === 0)
|
|
420
|
+
return `Nothing changed since ${short}`;
|
|
421
|
+
return `Nothing affected since ${short}`;
|
|
188
422
|
}
|
|
189
423
|
/** Changed files no project owns; none are claimed when ownership itself was unavailable. */
|
|
190
424
|
globalFiles(files, owners) {
|
|
@@ -243,17 +477,8 @@ class NxImpactScanner {
|
|
|
243
477
|
return null;
|
|
244
478
|
}
|
|
245
479
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- the second shade is optional
|
|
249
|
-
try {
|
|
250
|
-
return await this.ownership.owners();
|
|
251
|
-
}
|
|
252
|
-
catch (err) {
|
|
253
|
-
const error = (0, toError_1.toError)(err);
|
|
254
|
-
console.warn(`⚠️ Impact: nx file ownership unavailable (${error.message}); showing one shade`);
|
|
255
|
-
return new Map();
|
|
256
|
-
}
|
|
480
|
+
report(comparison, available, reason, base, changedFiles, sets) {
|
|
481
|
+
return new ImpactReport(comparison.kind, comparison.label, comparison.dirty, available, reason, base, sets.touched, sets.affected, sets.buildInputs, changedFiles, sets.dependencies, sets.globalFiles);
|
|
257
482
|
}
|
|
258
483
|
ids(projects) {
|
|
259
484
|
return projects.map((project) => this.names.getNodeId(project));
|
|
@@ -263,14 +488,14 @@ exports.NxImpactScanner = NxImpactScanner;
|
|
|
263
488
|
/** Writes (or removes) the sidecar, self-ignored, beside architecture/dependencies.html. */
|
|
264
489
|
class ImpactSidecar {
|
|
265
490
|
/** Never throws: a sidecar that cannot be written just leaves Impact disabled on the page. */
|
|
266
|
-
write(architectureDir,
|
|
491
|
+
write(architectureDir, data) {
|
|
267
492
|
const dir = path.join(architectureDir, exports.IMPACT_SIDECAR_DIR);
|
|
268
493
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- impact is optional; a write failure must never fail generation
|
|
269
494
|
try {
|
|
270
495
|
fs.mkdirSync(dir, { recursive: true });
|
|
271
|
-
fs.writeFileSync(path.join(dir, '.gitignore'), '# Written by architecture:generate and architecture:visualize. Per-
|
|
496
|
+
fs.writeFileSync(path.join(dir, '.gitignore'), '# Written by architecture:generate and architecture:visualize. Per-checkout data: never commit it.\n*\n', 'utf-8');
|
|
272
497
|
const file = path.join(dir, exports.IMPACT_SIDECAR_FILE);
|
|
273
|
-
fs.writeFileSync(file, this.script(
|
|
498
|
+
fs.writeFileSync(file, this.script(data), 'utf-8');
|
|
274
499
|
return file;
|
|
275
500
|
}
|
|
276
501
|
catch (err) {
|
|
@@ -279,48 +504,75 @@ class ImpactSidecar {
|
|
|
279
504
|
return null;
|
|
280
505
|
}
|
|
281
506
|
}
|
|
282
|
-
/** A stale sidecar from an earlier
|
|
507
|
+
/** A stale sidecar from an earlier checkout must never be shown as this one's impact. */
|
|
283
508
|
remove(architectureDir) {
|
|
284
509
|
fs.rmSync(path.join(architectureDir, exports.IMPACT_SIDECAR_DIR, exports.IMPACT_SIDECAR_FILE), { force: true });
|
|
285
510
|
}
|
|
286
|
-
script(
|
|
287
|
-
const json = JSON.stringify(
|
|
288
|
-
return `// Generated by architecture:generate or architecture:visualize for one
|
|
511
|
+
script(data) {
|
|
512
|
+
const json = JSON.stringify(data).replace(/</g, '\\u003c');
|
|
513
|
+
return `// Generated by architecture:generate or architecture:visualize for one checkout. Gitignored; do not commit.\nwindow.__WP_IMPACT__ = ${json};\n`;
|
|
289
514
|
}
|
|
290
515
|
}
|
|
291
516
|
exports.ImpactSidecar = ImpactSidecar;
|
|
292
|
-
/** The
|
|
517
|
+
/** The executors' one call: resolve, scan each comparison, write, and report one line per comparison. */
|
|
293
518
|
class ImpactRefresh {
|
|
294
519
|
scanner;
|
|
295
520
|
sidecar;
|
|
296
|
-
|
|
521
|
+
resolver;
|
|
522
|
+
constructor(scanner = new NxImpactScanner(), sidecar = new ImpactSidecar(), resolver = new ImpactScopeResolver()) {
|
|
297
523
|
this.scanner = scanner;
|
|
298
524
|
this.sidecar = sidecar;
|
|
525
|
+
this.resolver = resolver;
|
|
299
526
|
}
|
|
300
|
-
/**
|
|
301
|
-
|
|
527
|
+
/**
|
|
528
|
+
* Scans the comparisons in `kinds` that exist here (`VISUALIZE_IMPACT_KINDS` /
|
|
529
|
+
* `GENERATE_IMPACT_KINDS`). Never throws — Impact is optional, generation is not.
|
|
530
|
+
*/
|
|
531
|
+
async run(architectureDir, workspaceRoot, graph, kinds) {
|
|
302
532
|
const started = Date.now();
|
|
303
533
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- impact is optional; any failure becomes a reason, never a failed generation
|
|
304
534
|
try {
|
|
305
|
-
const
|
|
535
|
+
const plan = this.resolver.resolve(workspaceRoot);
|
|
536
|
+
if (plan instanceof ImpactUnavailable)
|
|
537
|
+
return this.none(architectureDir, plan.reason, started);
|
|
538
|
+
const comparisons = plan.comparisons.filter((comparison) => kinds.includes(comparison.kind));
|
|
539
|
+
if (comparisons.length === 0)
|
|
540
|
+
return this.none(architectureDir, 'no Changed-on-this-branch comparison on main or a detached HEAD; pnpm arch:visualize shows the last commit', started);
|
|
541
|
+
const results = await this.scanner.scanAll(workspaceRoot, graph, comparisons);
|
|
542
|
+
const lines = results.map((result) => this.line(result));
|
|
543
|
+
const scans = results.filter((result) => result instanceof ImpactReport);
|
|
306
544
|
const cost = `${Date.now() - started} ms`;
|
|
307
|
-
if (
|
|
545
|
+
if (scans.length === 0) {
|
|
308
546
|
this.sidecar.remove(architectureDir);
|
|
309
|
-
return `ℹ️ Impact disabled, no sidecar written
|
|
547
|
+
return [...lines, `ℹ️ Impact disabled, no sidecar written (${cost})`].join('\n');
|
|
310
548
|
}
|
|
311
|
-
const file = this.sidecar.write(architectureDir,
|
|
312
|
-
|
|
313
|
-
return `⚠️ Impact sidecar not written (${cost})`;
|
|
314
|
-
if (!report.available)
|
|
315
|
-
return `ℹ️ Impact disabled on the page: ${report.reason} (${cost})`;
|
|
316
|
-
return (`✅ Impact vs ${report.base}: ${report.touched.length} touched · ${report.affected.length} affected · ` +
|
|
317
|
-
`${report.buildInputs.length} build inputs, from ${report.changedFiles} changed file(s) (${cost}) → ${file}`);
|
|
549
|
+
const file = this.sidecar.write(architectureDir, new ImpactSidecarData(scans, this.defaultKind(scans, plan.defaultKind)));
|
|
550
|
+
return [...lines, file === null ? `⚠️ Impact sidecar not written (${cost})` : `→ ${file} (${cost})`].join('\n');
|
|
318
551
|
}
|
|
319
552
|
catch (err) {
|
|
320
553
|
const error = (0, toError_1.toError)(err);
|
|
321
554
|
return `⚠️ Impact skipped: ${error.message}`;
|
|
322
555
|
}
|
|
323
556
|
}
|
|
557
|
+
/** The plan's default when that scan is available, else the first available one. */
|
|
558
|
+
defaultKind(scans, preferred) {
|
|
559
|
+
const usable = scans.filter((scan) => scan.available);
|
|
560
|
+
if (usable.length === 0 || usable.some((scan) => scan.kind === preferred))
|
|
561
|
+
return preferred;
|
|
562
|
+
return usable[0].kind;
|
|
563
|
+
}
|
|
564
|
+
none(architectureDir, reason, started) {
|
|
565
|
+
this.sidecar.remove(architectureDir);
|
|
566
|
+
return `ℹ️ Impact disabled, no sidecar written: ${reason} (${Date.now() - started} ms)`;
|
|
567
|
+
}
|
|
568
|
+
line(result) {
|
|
569
|
+
if (result instanceof ImpactUnavailable || !result.available)
|
|
570
|
+
return `ℹ️ Impact · ${result.label}: unavailable — ${result.reason}`;
|
|
571
|
+
if (result.reason !== '')
|
|
572
|
+
return `✅ Impact · ${result.label}: ${result.reason}`;
|
|
573
|
+
return (`✅ Impact · ${result.label}: ${result.touched.length} touched · ${result.affected.length} affected · ` +
|
|
574
|
+
`${result.buildInputs.length} build inputs, from ${result.changedFiles} changed file(s)`);
|
|
575
|
+
}
|
|
324
576
|
}
|
|
325
577
|
exports.ImpactRefresh = ImpactRefresh;
|
|
326
578
|
//# sourceMappingURL=graph-impact.js.map
|