@ecoma-io/archkeep 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/cli.mjs +126 -4
- package/commands.mjs +6 -0
- package/lsp.mjs +15 -2
- package/package.json +6 -2
- package/src/analysis/analyze.mjs +15 -0
- package/src/analysis/contract.md +36 -18
- package/src/analysis/csharp.mjs +485 -0
- package/src/analysis/dotnet/csproj.mjs +380 -0
- package/src/analysis/dotnet/mask.mjs +178 -0
- package/src/analysis/dotnet/namespaces.mjs +172 -0
- package/src/analysis/dotnet/resolve.mjs +89 -0
- package/src/analysis/go.mjs +289 -5
- package/src/analysis/java.mjs +329 -0
- package/src/analysis/jvm/gradle.mjs +545 -0
- package/src/analysis/jvm/mask.mjs +170 -0
- package/src/analysis/jvm/maven.mjs +612 -0
- package/src/analysis/jvm/packages.mjs +209 -0
- package/src/analysis/jvm/resolve.mjs +139 -0
- package/src/analysis/kotlin.mjs +210 -0
- package/src/analysis/manifest-util.mjs +30 -0
- package/src/analysis/python.mjs +3 -2
- package/src/analysis/registry.mjs +11 -0
- package/src/analysis/rust.mjs +171 -17
- package/src/analysis/source-util.mjs +155 -6
- package/src/analysis/typescript.mjs +9 -2
- package/src/commands/context.mjs +84 -14
- package/src/commands/provenance.mjs +7 -44
- package/src/commands/rules.mjs +775 -0
- package/src/governance/profile-registry.mjs +0 -1
- package/src/graph/create-dependencies.mjs +138 -15
- package/src/lsp/diagnose.mjs +1 -1
- package/src/lsp/server.mjs +97 -1
- package/src/lsp/workspace-index.mjs +106 -15
- package/src/options.mjs +30 -7
- package/src/process.mjs +10 -1
- package/src/providers/moon.mjs +287 -36
- package/src/providers/native/differential.fixtures.mjs +32 -6
- package/src/providers/native/discover.mjs +83 -4
- package/src/providers/native/graph.mjs +58 -0
- package/src/providers/native/model.mjs +59 -1
- package/src/rules/index.mjs +21 -6
- package/src/rules/reachability.mjs +2 -0
- package/src/rules/tags.mjs +7 -5
- package/src/rules/topology.mjs +5 -3
- package/src/workspace.mjs +115 -23
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The graph layer: cross-project EDGES for Go, Rust, and
|
|
2
|
+
* The graph layer: cross-project EDGES for Go, Rust, Python, Java, and C#/\.NET, in the shape
|
|
3
3
|
* Nx's `createDependencies` hook returns. Nothing else — nodes still come from
|
|
4
4
|
* each project's hand-written `project.json`, and targets are never inferred
|
|
5
5
|
* (`packages/archkeep/AGENTS.md`).
|
|
@@ -14,33 +14,94 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Each resolver reads tracked manifests and sources statically (regex for Go
|
|
16
16
|
* imports, smol-toml for Cargo/pyproject manifests) so the graph computes
|
|
17
|
-
* without any language toolchain installed. A workspace with no Go/Rust/Python
|
|
18
|
-
* projects pays nothing: every resolver keys off
|
|
19
|
-
*
|
|
20
|
-
* returning — the Python one does, for a declared path dependency it cannot
|
|
21
|
-
* attribute to any project (`../analysis/python.mjs` header) — and the throw
|
|
22
|
-
* is deliberate: edges and an error are the only two outputs this hook has,
|
|
23
|
-
* and an edge quietly missing from the graph is the failure mode this plugin
|
|
24
|
-
* exists to close.
|
|
17
|
+
* without any language toolchain installed. A workspace with no Go/Rust/Python/
|
|
18
|
+
* Java/C# projects pays nothing: every resolver keys off what it reads existing in
|
|
19
|
+
* the project's tracked files.
|
|
25
20
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
21
|
+
* ## The hook's failure posture (#364)
|
|
22
|
+
*
|
|
23
|
+
* Edges and a throw are the only two outputs this hook has, and every
|
|
24
|
+
* manifest reader and name index throws for the states its funnel
|
|
25
|
+
* classifies as could-not-complete — the same failure lists the CLI turns
|
|
26
|
+
* into exit 3: the Maven, Gradle and .csproj models, the JVM package index,
|
|
27
|
+
* the C# namespace index, the Go module map and the Rust crate map
|
|
28
|
+
* (`../analysis/source-util.mjs`'s `refuseUnreadTree`).
|
|
29
|
+
* Nx wraps a throwing hook's error and fails the whole graph computation —
|
|
30
|
+
* measured against nx 23.1.1, which also has a dedicated `ProcessDependenciesError`
|
|
31
|
+
* for the event and whose daemon refuses with an error rather than serving a
|
|
32
|
+
* stale graph — so the throw is a designed-for channel, and `nx affected`
|
|
33
|
+
* fails loudly on a broken reactor instead of under-selecting on it.
|
|
34
|
+
*
|
|
35
|
+
* The boundary: manifests and workspace-wide name indexes throw — one
|
|
36
|
+
* unreadable entry corrupts resolution for every project that names that
|
|
37
|
+
* identity, arbitrarily far from the file, so the failure cannot be
|
|
38
|
+
* attributed to the file's own edges — while per-source import reads keep
|
|
39
|
+
* the null-read posture the hook's `readFile` states below (a dropped source
|
|
40
|
+
* loses only its own edges; a dropped manifest or index entry loses
|
|
41
|
+
* everyone's). Python's dangling-path throw is the precedent that predates
|
|
42
|
+
* the rule; its malformed-TOML tolerance stays the documented exception its
|
|
43
|
+
* own header pins (`../analysis/python.mjs`).
|
|
44
|
+
*
|
|
45
|
+
* Resolver contract (see `../analysis/*.mjs`): every resolver returns raw Nx
|
|
46
|
+
* edges — { source, target, sourceFile, type } and nothing else. Go, Rust and
|
|
47
|
+
* Python take `resolve(projects, filesOf, readFile)`; the C# and JVM halves
|
|
48
|
+
* instead take ONE workspace-shaped object (`{ projects, filesOf, readFile }`),
|
|
49
|
+
* because everything they read is `perWorkspace`-memoized on that object — the
|
|
50
|
+
* C# namespace index, the JVM package index, the Maven and Gradle models — and
|
|
51
|
+
* a resolver destructuring its own arguments would have to build a fresh
|
|
52
|
+
* object per call, defeating the memo (#363: three builds of the same index
|
|
53
|
+
* per run).
|
|
28
54
|
*/
|
|
29
55
|
import { readFileSync } from "node:fs";
|
|
30
56
|
import { join } from "node:path";
|
|
31
57
|
|
|
32
58
|
import { containmentViolation } from "../containment.mjs";
|
|
59
|
+
import { resolveCsharpDependencies } from "../analysis/csharp.mjs";
|
|
60
|
+
import { resolveCsprojDependencies, dotnetManifestFailures } from "../analysis/dotnet/csproj.mjs";
|
|
33
61
|
import { resolveGoDependencies } from "../analysis/go.mjs";
|
|
62
|
+
import { resolveJavaDependencies } from "../analysis/java.mjs";
|
|
63
|
+
import { resolveKotlinDependencies } from "../analysis/kotlin.mjs";
|
|
64
|
+
import { resolveMavenDependencies, mavenManifestFailures } from "../analysis/jvm/maven.mjs";
|
|
65
|
+
import { resolveGradleDependencies, gradleManifestFailures } from "../analysis/jvm/gradle.mjs";
|
|
66
|
+
import { dotnetIndexFailures } from "../analysis/dotnet/namespaces.mjs";
|
|
34
67
|
import { resolvePythonDependencies } from "../analysis/python.mjs";
|
|
35
68
|
import { resolveRustDependencies } from "../analysis/rust.mjs";
|
|
36
69
|
import { resolveOptions } from "../options.mjs";
|
|
37
70
|
|
|
38
71
|
/** Pure core over an abstract workspace; injectable for tests. */
|
|
39
72
|
export function resolvePolyglotDependencies(projects, filesOf, readFile) {
|
|
73
|
+
// The C# and JVM halves read the same tree through ONE workspace-shaped
|
|
74
|
+
// object: their memoized reads (namespace index, package index, Maven and
|
|
75
|
+
// Gradle models) all key on the object itself, so every resolver handed the
|
|
76
|
+
// same one shares a single build of each — and the memoized read behind the
|
|
77
|
+
// object means no file's content is fetched twice on one graph computation.
|
|
78
|
+
// The positional resolvers (Go, Rust, Python) hold no such memo, so they
|
|
79
|
+
// keep taking the three values directly.
|
|
80
|
+
const reads = new Map();
|
|
81
|
+
const sharedWorkspace = {
|
|
82
|
+
projects,
|
|
83
|
+
filesOf,
|
|
84
|
+
readFile: (path) => {
|
|
85
|
+
if (!reads.has(path)) reads.set(path, readFile(path));
|
|
86
|
+
return reads.get(path);
|
|
87
|
+
},
|
|
88
|
+
};
|
|
40
89
|
const deps = [
|
|
41
90
|
...resolveGoDependencies(projects, filesOf, readFile),
|
|
42
91
|
...resolveRustDependencies(projects, filesOf, readFile),
|
|
43
92
|
...resolvePythonDependencies(projects, filesOf, readFile),
|
|
93
|
+
...resolveJavaDependencies(sharedWorkspace),
|
|
94
|
+
...resolveKotlinDependencies(sharedWorkspace),
|
|
95
|
+
...resolveCsharpDependencies(sharedWorkspace),
|
|
96
|
+
// Manifest edges for .csproj trees: ProjectReference resolution,
|
|
97
|
+
// independent of (and complementary to) the source-track edges above.
|
|
98
|
+
...resolveCsprojDependencies(sharedWorkspace),
|
|
99
|
+
// Manifest edges for Maven/Gradle trees: the identity-anchor half of JVM
|
|
100
|
+
// support, independent of (and complementary to) the import edges above
|
|
101
|
+
// — a declared-but-unused dependency and an undeclared-but-imported one
|
|
102
|
+
// are both findings.
|
|
103
|
+
...resolveMavenDependencies(sharedWorkspace),
|
|
104
|
+
...resolveGradleDependencies(sharedWorkspace),
|
|
44
105
|
];
|
|
45
106
|
// One edge per (source, target, sourceFile) — a Go project importing a
|
|
46
107
|
// sibling from ten files yields ten sourceFile-attributed edges upstream
|
|
@@ -54,6 +115,64 @@ export function resolvePolyglotDependencies(projects, filesOf, readFile) {
|
|
|
54
115
|
});
|
|
55
116
|
}
|
|
56
117
|
|
|
118
|
+
/**
|
|
119
|
+
* The manifest-track edges in one list — `<ProjectReference>` (plus
|
|
120
|
+
* `<Using Include>`) for .NET trees, Maven coordinate matching and Gradle
|
|
121
|
+
* `project(":x")` for JVM ones. `resolvePolyglotDependencies` above already
|
|
122
|
+
* folds these into the Nx plugin hook's answer; the CLI's native and Moon
|
|
123
|
+
* branches and the language server's index build their graphs from import
|
|
124
|
+
* sites alone, with no plugin host to call the hook, so they fold this list
|
|
125
|
+
* themselves (`../providers/native/graph.mjs`'s `mergeDeclaredEdges`) — the
|
|
126
|
+
* two-track contract of `../../../../docs/adr/0006-dotnet-language-integration.md`
|
|
127
|
+
* (Decision 3) and `../../../../docs/adr/0005-jvm-language-integration.md`
|
|
128
|
+
* (Decision 4) names no face it does not hold on.
|
|
129
|
+
*
|
|
130
|
+
* Each resolver refuses — throws — on exactly the failure list its
|
|
131
|
+
* `*ManifestFailures` twin reports from the same memoized model
|
|
132
|
+
* (`../../analysis/source-util.mjs`'s `perWorkspace` memoizes on the
|
|
133
|
+
* workspace object, so twin calls share one build): a caller that has found
|
|
134
|
+
* `mavenManifestFailures`/`gradleManifestFailures`/`dotnetManifestFailures`/
|
|
135
|
+
* `dotnetIndexFailures` empty on THIS workspace cannot hit the throw.
|
|
136
|
+
*
|
|
137
|
+
* @param {object} workspace The shared workspace-shaped object.
|
|
138
|
+
* @returns {{source: string, target: string, sourceFile: string, type: string}[]}
|
|
139
|
+
*/
|
|
140
|
+
export function resolveDeclaredManifestEdges(workspace) {
|
|
141
|
+
return [
|
|
142
|
+
...resolveCsprojDependencies(workspace),
|
|
143
|
+
...resolveMavenDependencies(workspace),
|
|
144
|
+
...resolveGradleDependencies(workspace),
|
|
145
|
+
];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The could-not-complete failures behind `resolveDeclaredManifestEdges`'s
|
|
150
|
+
* refusal — the four manifest and index failure lists folded into one. A
|
|
151
|
+
* caller uses this list twice: folded into its own whole-file failure
|
|
152
|
+
* reporting, and as the no-throw guard for the edge call — each resolver
|
|
153
|
+
* refuses on exactly the failures its `*ManifestFailures` twin reports from
|
|
154
|
+
* the same memoized model, so an empty list here makes
|
|
155
|
+
* `resolveDeclaredManifestEdges` unable to throw on this workspace. Both
|
|
156
|
+
* halves of that sentence live beside each other here so the identity cannot
|
|
157
|
+
* drift between a caller that forgets to report and one that skips the guard.
|
|
158
|
+
*
|
|
159
|
+
* `jvmIndexFailures` and `pythonUnmodelledFailures` are deliberately absent:
|
|
160
|
+
* no resolver here reads the JVM package index or refuses on Python's
|
|
161
|
+
* unmodelled posture, so neither is a condition of the no-throw guarantee —
|
|
162
|
+
* callers keep those lists flowing through their own funnels.
|
|
163
|
+
*
|
|
164
|
+
* @param {object} workspace The shared workspace-shaped object.
|
|
165
|
+
* @returns {{sourceFile: string, reason: string}[]}
|
|
166
|
+
*/
|
|
167
|
+
export function resolveDeclaredManifestFailures(workspace) {
|
|
168
|
+
return [
|
|
169
|
+
...mavenManifestFailures(workspace),
|
|
170
|
+
...gradleManifestFailures(workspace),
|
|
171
|
+
...dotnetManifestFailures(workspace),
|
|
172
|
+
...dotnetIndexFailures(workspace),
|
|
173
|
+
];
|
|
174
|
+
}
|
|
175
|
+
|
|
57
176
|
/**
|
|
58
177
|
* The Nx hook.
|
|
59
178
|
*
|
|
@@ -81,10 +200,14 @@ export const createDependencies = (options, context) => {
|
|
|
81
200
|
// attacker-supplied the moment a PR adds a tracked path. A tracked symlink
|
|
82
201
|
// whose realpath leaves the workspace would draw a dependency edge from
|
|
83
202
|
// outside bytes into `nx affected`'s graph; refusing (null) drops the
|
|
84
|
-
// read
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
203
|
+
// read. For a SOURCE file an import walk reads, that is the end of it:
|
|
204
|
+
// null read = no edge, the file's own edges and nothing wider. For a
|
|
205
|
+
// MANIFEST or an index-feeding file (a pom, a settings or build file, a
|
|
206
|
+
// .csproj, a .java/.kt/.cs a name index reads), the reader records the
|
|
207
|
+
// null read as a could-not-complete failure and its resolver throws on
|
|
208
|
+
// it — the posture this file's header owns (#364). A plugin that never
|
|
209
|
+
// resolves outside bytes stays silent-green only when the bytes are
|
|
210
|
+
// really inside (`../containment.mjs`).
|
|
88
211
|
if (containmentViolation(context.workspaceRoot, abs) !== null) return null;
|
|
89
212
|
try {
|
|
90
213
|
return readFileSync(abs, "utf8");
|
package/src/lsp/diagnose.mjs
CHANGED
|
@@ -63,7 +63,7 @@ import { indexGaps } from "./workspace-index.mjs";
|
|
|
63
63
|
* @param {string} request.text Its current contents — the editor's buffer, not
|
|
64
64
|
* what is on disk. Diagnosing the saved file would answer a question nobody
|
|
65
65
|
* asked while the developer is looking at their unsaved edit.
|
|
66
|
-
* @param {{workspace: object, graph: object, skippedProjects?: object[], fileFailures?: object[], importSites?: object[], nativeMarker?: boolean, nativeModelFailure?: string|null, moonModelFailure?: string|null, nxModelFailure?: string|null, workspaceLayoutFailure?: string|null}} request.index
|
|
66
|
+
* @param {{workspace: object, graph: object, skippedProjects?: object[], fileFailures?: object[], importSites?: object[], duplicateProjects?: object[], nativeMarker?: boolean, nativeModelFailure?: string|null, moonModelFailure?: string|null, nxModelFailure?: string|null, workspaceLayoutFailure?: string|null}} request.index
|
|
67
67
|
* From `./workspace-index.mjs`. `importSites` is the whole tree's retained
|
|
68
68
|
* analysis output — the evidence half of the run below; absent (an index
|
|
69
69
|
* built before it existed) reads as none, which degrades evidence, never a
|
package/src/lsp/server.mjs
CHANGED
|
@@ -43,7 +43,11 @@ import {
|
|
|
43
43
|
readMoonOptions,
|
|
44
44
|
readPluginOptions,
|
|
45
45
|
} from "../options.mjs";
|
|
46
|
-
import {
|
|
46
|
+
import {
|
|
47
|
+
MOON_ALT_WORKSPACE_MARKER,
|
|
48
|
+
MOON_WORKSPACE_MARKER,
|
|
49
|
+
moonMarkerAt,
|
|
50
|
+
} from "../providers/moon.mjs";
|
|
47
51
|
import { ARCHKEEP_MODEL_FILE, loadNativeModel } from "../providers/native/model.mjs";
|
|
48
52
|
|
|
49
53
|
import { readBoundaryConfig } from "./boundary-config.mjs";
|
|
@@ -82,6 +86,88 @@ const MFE_CONFIG_FILES = Object.freeze([
|
|
|
82
86
|
"module-federation.config.ts",
|
|
83
87
|
]);
|
|
84
88
|
|
|
89
|
+
/**
|
|
90
|
+
* The manifests the non-TypeScript halves of the project graph are read from —
|
|
91
|
+
* the finite names each provider decides projects and edges by.
|
|
92
|
+
*
|
|
93
|
+
* This list carried none of them, and that was the defect #410 reports: the
|
|
94
|
+
* watched set described the TypeScript-side configuration only, while on a Go,
|
|
95
|
+
* Rust, Python, JVM or Moon workspace the graph itself is read from manifests.
|
|
96
|
+
* `textDocument/didSave` invalidates the whole session only when
|
|
97
|
+
* `touchesWatchedFile` matches this list, so saving an edited `go.mod`
|
|
98
|
+
* republished the manifest alone and left every open verdict answering from
|
|
99
|
+
* the graph as it was at index time — a module added to the workspace stayed
|
|
100
|
+
* invisible, the rules judged against a graph that no longer matched disk, and
|
|
101
|
+
* the affected files went on publishing `[]`, which reads as "no violation"
|
|
102
|
+
* (`../../../../AGENTS.md`). Only an unrelated watched edit or an editor
|
|
103
|
+
* restart rebuilt the graph.
|
|
104
|
+
*
|
|
105
|
+
* What reads each entry:
|
|
106
|
+
*
|
|
107
|
+
* - `go.mod` — the module path `../analysis/go.mjs`'s `resolveGoDependencies`
|
|
108
|
+
* resolves Go imports against, and the anchor
|
|
109
|
+
* `../providers/native/model.mjs`'s `DEFAULT_MANIFEST_NAMES` infers Go
|
|
110
|
+
* projects from. Renaming a module in the editor re-points or drops every
|
|
111
|
+
* edge into that project at the next index build; this entry is what makes
|
|
112
|
+
* the next index build happen.
|
|
113
|
+
* - `go.work` — the use list deciding which Go modules the workspace's own
|
|
114
|
+
* tooling builds (`../go-work.mjs`'s parser): the same module set the Go
|
|
115
|
+
* analysis models, so a save of it is a graph-shaping edit on the same terms
|
|
116
|
+
* as a `go.mod`.
|
|
117
|
+
* - `Cargo.toml` — crate identity, `[workspace]` membership and dependency
|
|
118
|
+
* declarations (`../analysis/rust.mjs`). One basename covers the root
|
|
119
|
+
* manifest and every member's, the reach `**\/package.json` already has.
|
|
120
|
+
* - `pyproject.toml` — the package names and `sources` tables
|
|
121
|
+
* `../analysis/python.mjs` resolves Python imports against.
|
|
122
|
+
* - `pom.xml` — Maven identity and dependency edges
|
|
123
|
+
* (`../analysis/jvm/maven.mjs`), and a project anchor in
|
|
124
|
+
* `DEFAULT_MANIFEST_NAMES`.
|
|
125
|
+
* - `settings.gradle` / `settings.gradle.kts` — the reactor structure
|
|
126
|
+
* `../analysis/jvm/gradle.mjs` reads Gradle identity from.
|
|
127
|
+
* - `build.gradle` / `build.gradle.kts` — the dependency declarations the same
|
|
128
|
+
* module reads Gradle EDGES from, which is why they sit beside the settings
|
|
129
|
+
* file that anchors the reactor rather than instead of it.
|
|
130
|
+
* - `moon.yml` and the two `workspace.yml` spellings — the Moon provider
|
|
131
|
+
* derives its whole graph from `moon project-graph --json`
|
|
132
|
+
* (`../providers/moon.mjs`'s `readProjectGraph`), which reads the tree's
|
|
133
|
+
* `moon.yml` files for ids, tags and `dependsOn`, under the workspace whose
|
|
134
|
+
* marker the two imported constants name. They are imported rather than
|
|
135
|
+
* spelled out so the watched set follows Moon's own directory conventions
|
|
136
|
+
* instead of a second copy of them.
|
|
137
|
+
*
|
|
138
|
+
* `*.csproj` is deliberately absent although `DEFAULT_MANIFEST_NAMES` carries
|
|
139
|
+
* it: that entry is a glob PATTERN, and the reach this list's entries get —
|
|
140
|
+
* `touchesWatchedFile`'s workspace-relative suffix comparison, mirrored by the
|
|
141
|
+
* `**\/<entry>` glob `registerFileWatchers` registers — covers exact names and
|
|
142
|
+
* directory-prefixed names only. A pattern entry would register `**\/*.csproj`,
|
|
143
|
+
* a watcher that fires, while the suffix match could never answer it: a
|
|
144
|
+
* notification path that looks covered and is not, which is the silent
|
|
145
|
+
* direction in the one list whose job is noticing. The list carries only names
|
|
146
|
+
* it can fire on.
|
|
147
|
+
*
|
|
148
|
+
* The entries are unconditional rather than provider-selected, for the same
|
|
149
|
+
* reason `archkeep.json` is: which manifests shape a root's graph can move
|
|
150
|
+
* under a running session — a `go.mod` appearing beside `nx.json` — and a
|
|
151
|
+
* watch list fixed to the provider the session started with is this same
|
|
152
|
+
* stale-graph hole one step out. The cost of an entry a TypeScript-only
|
|
153
|
+
* workspace never matches is one glob; the failure it buys out is a graph the
|
|
154
|
+
* editor never re-reads.
|
|
155
|
+
*/
|
|
156
|
+
const POLYGLOT_GRAPH_MANIFESTS = Object.freeze([
|
|
157
|
+
"go.mod",
|
|
158
|
+
"go.work",
|
|
159
|
+
"Cargo.toml",
|
|
160
|
+
"pyproject.toml",
|
|
161
|
+
"pom.xml",
|
|
162
|
+
"settings.gradle",
|
|
163
|
+
"settings.gradle.kts",
|
|
164
|
+
"build.gradle",
|
|
165
|
+
"build.gradle.kts",
|
|
166
|
+
"moon.yml",
|
|
167
|
+
MOON_WORKSPACE_MARKER,
|
|
168
|
+
MOON_ALT_WORKSPACE_MARKER,
|
|
169
|
+
]);
|
|
170
|
+
|
|
85
171
|
/**
|
|
86
172
|
* The files whose content changes the verdict for files that did not change,
|
|
87
173
|
* given the options this session resolved.
|
|
@@ -150,6 +236,15 @@ const MFE_CONFIG_FILES = Object.freeze([
|
|
|
150
236
|
* takes a project's NAME when its `project.json` states none, so an edit to it
|
|
151
237
|
* can move a project in the graph and not only waive something in it.
|
|
152
238
|
*
|
|
239
|
+
* The polyglot graph manifests are here for that same argument one level out,
|
|
240
|
+
* at the place a project comes into existence at all: on a Go, Rust, Python,
|
|
241
|
+
* JVM or Moon root the graph itself is read from manifests this list used to
|
|
242
|
+
* carry none of, so saving an edited one republished the manifest alone and
|
|
243
|
+
* left every verdict answering from the graph as it was at index time. Which
|
|
244
|
+
* names, and which module reads each, is `POLYGLOT_GRAPH_MANIFESTS`'s own
|
|
245
|
+
* comment — the roster is not restated here, because two rosters are two
|
|
246
|
+
* chances to drift.
|
|
247
|
+
*
|
|
153
248
|
* Those three are PER-PROJECT paths rather than workspace-root singletons, and
|
|
154
249
|
* they need no new mechanism for it: `registerFileWatchers` gives every entry
|
|
155
250
|
* the glob `**\/<entry>` and `touchesWatchedFile` matches the same reach — an
|
|
@@ -209,6 +304,7 @@ export function watchedFilesFor(options, { unresolved = false } = {}) {
|
|
|
209
304
|
ARCHKEEP_MODEL_FILE,
|
|
210
305
|
PACKAGE_MANIFEST_FILE,
|
|
211
306
|
...MFE_CONFIG_FILES,
|
|
307
|
+
...POLYGLOT_GRAPH_MANIFESTS,
|
|
212
308
|
]);
|
|
213
309
|
}
|
|
214
310
|
|
|
@@ -126,7 +126,11 @@ import {
|
|
|
126
126
|
createWorkspace,
|
|
127
127
|
listTrackedFiles,
|
|
128
128
|
} from "../workspace.mjs";
|
|
129
|
-
import { buildDependencies } from "../providers/native/graph.mjs";
|
|
129
|
+
import { buildDependencies, mergeDeclaredEdges } from "../providers/native/graph.mjs";
|
|
130
|
+
import {
|
|
131
|
+
resolveDeclaredManifestEdges,
|
|
132
|
+
resolveDeclaredManifestFailures,
|
|
133
|
+
} from "../graph/create-dependencies.mjs";
|
|
130
134
|
import { nodeTypeOf, PROJECT_CONFIG_FILE } from "../providers/native/discover.mjs";
|
|
131
135
|
import { ARCHKEEP_MODEL_FILE } from "../providers/native/model.mjs";
|
|
132
136
|
import { nativeProvider } from "../providers/native/index.mjs";
|
|
@@ -249,7 +253,14 @@ export function discoverProjects({ files, readFile }) {
|
|
|
249
253
|
* reads it unguarded and an absent list is not the same fact as an empty one.
|
|
250
254
|
*
|
|
251
255
|
* @param {{name: string, root: string, config: object}[]} projects
|
|
252
|
-
* @returns {Record<string, object
|
|
256
|
+
* @returns {{nodes: Record<string, object>, duplicateProjects: {name: string, roots: string[]}[]}}
|
|
257
|
+
* `duplicateProjects` names every name two or more projects resolved to and
|
|
258
|
+
* every root that claimed it (#375): a silent `nodes[name] = …` overwrite
|
|
259
|
+
* drops the shadowed project from the graph, its files match no root, and
|
|
260
|
+
* the editor publishes no diagnostics for real boundary crossings — the
|
|
261
|
+
* exact silent direction `../../../../AGENTS.md`'s invariant refuses. The
|
|
262
|
+
* first project still wins in `nodes` (the index stays usable); the caller
|
|
263
|
+
* publishes the collision through `indexGaps`.
|
|
253
264
|
*/
|
|
254
265
|
export function buildNodes(projects) {
|
|
255
266
|
// Null-prototype for the same reason `../providers/native/graph.mjs` and
|
|
@@ -264,14 +275,36 @@ export function buildNodes(projects) {
|
|
|
264
275
|
// has no inherited `__proto__` accessor to collide with, so the name behaves
|
|
265
276
|
// like every other project name: a real, own, enumerable entry.
|
|
266
277
|
const nodes = Object.create(null);
|
|
278
|
+
/** @type {Map<string, string>} name → root of the first project that claimed it. */
|
|
279
|
+
const seenNames = new Map();
|
|
280
|
+
/** @type {Map<string, string[]>} name → every root that resolved to it, for names claimed twice or more. */
|
|
281
|
+
const duplicateMap = new Map();
|
|
282
|
+
|
|
267
283
|
for (const { name, root, config } of projects) {
|
|
284
|
+
if (seenNames.has(name)) {
|
|
285
|
+
// Duplicate name detected — record it for loud reporting
|
|
286
|
+
if (!duplicateMap.has(name)) {
|
|
287
|
+
duplicateMap.set(name, [seenNames.get(name)]);
|
|
288
|
+
}
|
|
289
|
+
duplicateMap.get(name).push(root);
|
|
290
|
+
// Skip adding the duplicate to nodes — first project wins
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
seenNames.set(name, root);
|
|
268
294
|
nodes[name] = {
|
|
269
295
|
name,
|
|
270
296
|
type: nodeTypeOf(name, config.projectType),
|
|
271
297
|
data: { ...config, root, tags: config.tags ?? [] },
|
|
272
298
|
};
|
|
273
299
|
}
|
|
274
|
-
|
|
300
|
+
|
|
301
|
+
// Convert the duplicate map to the expected output format
|
|
302
|
+
const duplicateProjects = [];
|
|
303
|
+
for (const [name, roots] of duplicateMap.entries()) {
|
|
304
|
+
duplicateProjects.push({ name, roots });
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return { nodes, duplicateProjects };
|
|
275
308
|
}
|
|
276
309
|
|
|
277
310
|
/**
|
|
@@ -293,10 +326,12 @@ export function buildNodes(projects) {
|
|
|
293
326
|
* filesystem existence (the marker is a directory, which `readFileAt`
|
|
294
327
|
* cannot answer) and the provider's one-call graph reader, both injectable
|
|
295
328
|
* for the same reason `listFiles` is.
|
|
296
|
-
* @returns {{root: string, files: string[], workspace: object, graph: object, skippedProjects: object[], fileFailures: object[], importSites: object[], nativeMarker: boolean, nativeModelFailure: string|null, moonModelFailure: string|null, nxModelFailure: string|null, workspaceLayoutFailure: string|null}}
|
|
329
|
+
* @returns {{root: string, files: string[], workspace: object, graph: object, skippedProjects: object[], fileFailures: object[], importSites: object[], duplicateProjects: {name: string, roots: string[]}[], nativeMarker: boolean, nativeModelFailure: string|null, moonModelFailure: string|null, nxModelFailure: string|null, workspaceLayoutFailure: string|null}}
|
|
297
330
|
* `importSites` is the whole tree's analysis output, retained past the graph
|
|
298
331
|
* build for the rule engine's evidence index — see this module header's "Why
|
|
299
|
-
* the whole tree's import sites stay on the index".
|
|
332
|
+
* the whole tree's import sites stay on the index". `duplicateProjects` is
|
|
333
|
+
* the Nx-shaped branch's own (#375): empty everywhere but that branch, which
|
|
334
|
+
* is the only one that resolves names from `project.json` files itself.
|
|
300
335
|
* @throws {Error} when the file list cannot be obtained. Loud on purpose: an
|
|
301
336
|
* index built from no files would put every file in no project, and a file in
|
|
302
337
|
* no project has no boundary to cross — a clean report, produced by not
|
|
@@ -359,7 +394,7 @@ export function buildWorkspaceIndex({
|
|
|
359
394
|
}
|
|
360
395
|
|
|
361
396
|
const { projects, skipped } = discoverProjects({ files, readFile });
|
|
362
|
-
const nodes = buildNodes(projects);
|
|
397
|
+
const { nodes, duplicateProjects } = buildNodes(projects);
|
|
363
398
|
// The same Module Federation fact the CLI path computes, from the same
|
|
364
399
|
// predicate (`../workspace.mjs` → `annotateMFERemotes`): a CLI verdict and an
|
|
365
400
|
// editor verdict on the same import must match, and the field failing closed
|
|
@@ -394,6 +429,18 @@ export function buildWorkspaceIndex({
|
|
|
394
429
|
|
|
395
430
|
const { importSites, fileFailures } = analyzeTrackedFiles({ files, workspace });
|
|
396
431
|
|
|
432
|
+
// The manifest track — Maven, Gradle, csproj — draws its edges here too.
|
|
433
|
+
// The CLI's Nx face gets them from the polyglot plugin inside Nx's own
|
|
434
|
+
// graph computation; this index has no plugin host, so it folds the same
|
|
435
|
+
// resolvers' records itself (`../graph/create-dependencies.mjs`), the way
|
|
436
|
+
// the native and Moon branches below do. The failure list doubles as the
|
|
437
|
+
// no-throw guard (that module's `resolveDeclaredManifestFailures` — each
|
|
438
|
+
// resolver refuses on exactly the failures its `*ManifestFailures` twin
|
|
439
|
+
// reports, same memoized model) and joins `fileFailures`, so a manifest
|
|
440
|
+
// the index cannot read is a named gap on every document rather than
|
|
441
|
+
// silently missing edges.
|
|
442
|
+
const manifestRefusalFailures = resolveDeclaredManifestFailures(workspace);
|
|
443
|
+
|
|
397
444
|
// `nx.json`'s `workspaceLayout` reaches the rule engine here the same way
|
|
398
445
|
// `../providers/nx.mjs`'s `readProjectGraph` merges it onto the graph it
|
|
399
446
|
// returns to `cli.mjs` — see that function's own doc for why a merge step
|
|
@@ -454,21 +501,27 @@ export function buildWorkspaceIndex({
|
|
|
454
501
|
`workspace, whose projects are declared in package.json, yields none`
|
|
455
502
|
: null;
|
|
456
503
|
|
|
504
|
+
const graph = {
|
|
505
|
+
nodes,
|
|
506
|
+
dependencies: buildDependencies({ importSites, nodes, projectOf }),
|
|
507
|
+
...(workspaceLayout === undefined ? {} : { workspaceLayout }),
|
|
508
|
+
};
|
|
509
|
+
if (manifestRefusalFailures.length === 0) {
|
|
510
|
+
mergeDeclaredEdges(graph, resolveDeclaredManifestEdges(workspace));
|
|
511
|
+
}
|
|
512
|
+
|
|
457
513
|
return {
|
|
458
514
|
root,
|
|
459
515
|
files,
|
|
460
516
|
workspace,
|
|
461
|
-
graph
|
|
462
|
-
nodes,
|
|
463
|
-
dependencies: buildDependencies({ importSites, nodes, projectOf }),
|
|
464
|
-
...(workspaceLayout === undefined ? {} : { workspaceLayout }),
|
|
465
|
-
},
|
|
517
|
+
graph,
|
|
466
518
|
skippedProjects: skipped,
|
|
467
|
-
fileFailures,
|
|
519
|
+
fileFailures: [...fileFailures, ...manifestRefusalFailures],
|
|
468
520
|
// Retained past the graph build — the evidence half of `evaluate()`'s
|
|
469
521
|
// input (`./diagnose.mjs` composes its run from these). See this module
|
|
470
522
|
// header's "Why the whole tree's import sites stay on the index".
|
|
471
523
|
importSites,
|
|
524
|
+
duplicateProjects,
|
|
472
525
|
nativeMarker: false,
|
|
473
526
|
nativeModelFailure: null,
|
|
474
527
|
moonModelFailure: null,
|
|
@@ -570,6 +623,7 @@ function buildNativeWorkspaceIndex({ root, files, readFile, tsConfig }) {
|
|
|
570
623
|
// was read — and an empty list is the honest answer, not a missing field
|
|
571
624
|
// every consumer would have to guard against.
|
|
572
625
|
importSites: [],
|
|
626
|
+
duplicateProjects: [],
|
|
573
627
|
nativeMarker: true,
|
|
574
628
|
nativeModelFailure: cause?.message ?? String(cause),
|
|
575
629
|
moonModelFailure: null,
|
|
@@ -605,9 +659,26 @@ function buildNativeWorkspaceIndex({ root, files, readFile, tsConfig }) {
|
|
|
605
659
|
// reports the same way: an unowned file is analyzed by nothing and judged
|
|
606
660
|
// by nothing, which is exactly the hole `../providers/native/coverage.mjs`'s
|
|
607
661
|
// own header names.
|
|
608
|
-
const
|
|
662
|
+
const manifestRefusalFailures = resolveDeclaredManifestFailures(workspace);
|
|
663
|
+
const fileFailures = [
|
|
664
|
+
...discovered.failures,
|
|
665
|
+
...analysisFailures,
|
|
666
|
+
// The manifest track's own could-not-complete failures, in the same
|
|
667
|
+
// whole-file shape — an unreadable pom/build.gradle/.csproj is a file
|
|
668
|
+
// this index could not analyze, exactly like the ones above.
|
|
669
|
+
...manifestRefusalFailures,
|
|
670
|
+
];
|
|
609
671
|
|
|
610
672
|
const graph = nativeProvider.buildGraph({ discovered, importSites });
|
|
673
|
+
// The manifest track's edges fold in after the import sites — no plugin
|
|
674
|
+
// host exists on this branch to draw them (`../graph/create-dependencies.mjs`
|
|
675
|
+
// names the faces that route through the Nx hook instead). The failure list
|
|
676
|
+
// above doubles as the no-throw guard: each resolver refuses on exactly the
|
|
677
|
+
// failures its `*ManifestFailures` twin reports from the same memoized model.
|
|
678
|
+
if (manifestRefusalFailures.length === 0) {
|
|
679
|
+
mergeDeclaredEdges(graph, resolveDeclaredManifestEdges(workspace));
|
|
680
|
+
}
|
|
681
|
+
|
|
611
682
|
// The same Module Federation and `package.json` facts the Nx branch and
|
|
612
683
|
// `../../cli.mjs`'s native branch both compute, from the same shared
|
|
613
684
|
// functions (`../workspace.mjs`) — a CLI verdict and an editor verdict on
|
|
@@ -626,6 +697,7 @@ function buildNativeWorkspaceIndex({ root, files, readFile, tsConfig }) {
|
|
|
626
697
|
// input (`./diagnose.mjs` composes its run from these), on this branch
|
|
627
698
|
// exactly as on the Nx-shaped one.
|
|
628
699
|
importSites,
|
|
700
|
+
duplicateProjects: [],
|
|
629
701
|
nativeMarker: true,
|
|
630
702
|
nativeModelFailure: null,
|
|
631
703
|
moonModelFailure: null,
|
|
@@ -673,6 +745,7 @@ function buildMoonWorkspaceIndex({ root, files, readFile, tsConfig, readGraph })
|
|
|
673
745
|
// empty list is the honest answer, not a missing field every consumer
|
|
674
746
|
// would have to guard against.
|
|
675
747
|
importSites: [],
|
|
748
|
+
duplicateProjects: [],
|
|
676
749
|
nativeMarker: false,
|
|
677
750
|
nativeModelFailure: null,
|
|
678
751
|
moonModelFailure: cause?.message ?? String(cause),
|
|
@@ -704,17 +777,29 @@ function buildMoonWorkspaceIndex({ root, files, readFile, tsConfig, readGraph })
|
|
|
704
777
|
projectOf: (file) => projectOfFile.get(file),
|
|
705
778
|
});
|
|
706
779
|
|
|
780
|
+
// The manifest track folds in after the import sites, for the same
|
|
781
|
+
// no-face-qualifier reason the CLI's Moon branch states at its own fold —
|
|
782
|
+
// Moon has no plugin hook to draw declared edges either. The failure list
|
|
783
|
+
// doubles as the no-throw guard and joins `fileFailures`, so an
|
|
784
|
+
// unreadable pom/build.gradle/.csproj is a named gap rather than silently
|
|
785
|
+
// missing edges.
|
|
786
|
+
const manifestRefusalFailures = resolveDeclaredManifestFailures(workspace);
|
|
787
|
+
if (manifestRefusalFailures.length === 0) {
|
|
788
|
+
mergeDeclaredEdges(graph, resolveDeclaredManifestEdges(workspace));
|
|
789
|
+
}
|
|
790
|
+
|
|
707
791
|
return {
|
|
708
792
|
root,
|
|
709
793
|
files,
|
|
710
794
|
workspace,
|
|
711
795
|
graph,
|
|
712
796
|
skippedProjects: [],
|
|
713
|
-
fileFailures,
|
|
797
|
+
fileFailures: [...fileFailures, ...manifestRefusalFailures],
|
|
714
798
|
// Retained past the graph build — the evidence half of `evaluate()`'s
|
|
715
799
|
// input (`./diagnose.mjs` composes its run from these), on this branch
|
|
716
800
|
// exactly as on the other two.
|
|
717
801
|
importSites,
|
|
802
|
+
duplicateProjects: [],
|
|
718
803
|
nativeMarker: false,
|
|
719
804
|
nativeModelFailure: null,
|
|
720
805
|
moonModelFailure: null,
|
|
@@ -799,12 +884,13 @@ function buildMoonWorkspaceIndex({ root, files, readFile, tsConfig, readGraph })
|
|
|
799
884
|
*
|
|
800
885
|
* Each sentence names a path, so the diagnostic says which file to open.
|
|
801
886
|
*
|
|
802
|
-
* @param {{skippedProjects?: {file: string, reason: string}[], fileFailures?: {sourceFile: string, reason: string}[], nativeModelFailure?: string|null, moonModelFailure?: string|null, nxModelFailure?: string|null, workspaceLayoutFailure?: string|null}} index
|
|
887
|
+
* @param {{skippedProjects?: {file: string, reason: string}[], fileFailures?: {sourceFile: string, reason: string}[], duplicateProjects?: {name: string, roots: string[]}[], nativeModelFailure?: string|null, moonModelFailure?: string|null, nxModelFailure?: string|null, workspaceLayoutFailure?: string|null}} index
|
|
803
888
|
* @returns {string[]}
|
|
804
889
|
*/
|
|
805
890
|
export function indexGaps({
|
|
806
891
|
skippedProjects = [],
|
|
807
892
|
fileFailures = [],
|
|
893
|
+
duplicateProjects = [],
|
|
808
894
|
nativeModelFailure = null,
|
|
809
895
|
moonModelFailure = null,
|
|
810
896
|
nxModelFailure = null,
|
|
@@ -840,6 +926,11 @@ export function indexGaps({
|
|
|
840
926
|
`so imports across a non-default apps/libs boundary are judged against the default layout ` +
|
|
841
927
|
`instead of the one this workspace declared`,
|
|
842
928
|
]),
|
|
929
|
+
...duplicateProjects.map(
|
|
930
|
+
({ name, roots }) =>
|
|
931
|
+
`multiple projects resolve to the name '${name}': ${roots.map((r) => `'${r}'`).join(" and ")} — ` +
|
|
932
|
+
`only the first is indexed; files in the shadowed projects will show no diagnostics`,
|
|
933
|
+
),
|
|
843
934
|
...skippedProjects.map(
|
|
844
935
|
({ file, reason }) =>
|
|
845
936
|
`${file} ${firstLine(reason)}, so that project is missing from the graph entirely`,
|
package/src/options.mjs
CHANGED
|
@@ -182,12 +182,20 @@ export function resolveOptions(rawOptions) {
|
|
|
182
182
|
* This plugin's own entry in an `nx.json` `plugins` array, in either form Nx
|
|
183
183
|
* accepts: a bare string, or `{plugin, options}`.
|
|
184
184
|
*
|
|
185
|
-
* Matched
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
185
|
+
* Matched by exact package specifier or relative/aliased path, never by bare
|
|
186
|
+
* suffix. The three legitimate spellings this plugin recognizes are:
|
|
187
|
+
*
|
|
188
|
+
* - `@ecoma-io/archkeep/nx` — the full package specifier from npm
|
|
189
|
+
* - `archkeep/nx` — the bare shorthand (accepted for backward compatibility)
|
|
190
|
+
* - `./packages/archkeep/nx.mjs`, `../../node_modules/@ecoma-io/archkeep/nx`,
|
|
191
|
+
* or other relative/aliased spellings of the same package
|
|
192
|
+
*
|
|
193
|
+
* A third-party plugin whose specifier happens to end with `/archkeep/nx`
|
|
194
|
+
* (e.g., `vendor-archkeep/nx`, `@other-scope/archkeep/nx`) is NOT claimed:
|
|
195
|
+
* such a package has its own options schema and this plugin must neither
|
|
196
|
+
* validate it nor read it. The suffix-only match that existed before would
|
|
197
|
+
* claim those options, validate them against this plugin's schema, and throw
|
|
198
|
+
* on any unknown key — a false violation on a stranger's configuration.
|
|
191
199
|
*
|
|
192
200
|
* The bare package specifier (`@ecoma-io/archkeep`, with no `/nx`) does NOT
|
|
193
201
|
* match: that resolves to the engine entry, which exports neither `name` nor
|
|
@@ -202,7 +210,22 @@ function namesThisPlugin(entry) {
|
|
|
202
210
|
typeof entry === "string" ? entry : /** @type {{ plugin?: unknown }} */ (entry)?.plugin;
|
|
203
211
|
if (typeof specifier !== "string") return false;
|
|
204
212
|
const withoutExt = specifier.replace(/\.mjs$/u, "");
|
|
205
|
-
|
|
213
|
+
|
|
214
|
+
// Exact match against this package's own specifier
|
|
215
|
+
if (withoutExt === "@ecoma-io/archkeep/nx" || withoutExt === "archkeep/nx") {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Match relative/aliased spellings of the same package (e.g.,
|
|
220
|
+
// `./packages/archkeep/nx.mjs`, `../../node_modules/@ecoma-io/archkeep/nx`).
|
|
221
|
+
// These paths start with `./` or `../` and end with `/archkeep/nx`,
|
|
222
|
+
// which distinguishes them from third-party packages like `vendor-archkeep/nx`.
|
|
223
|
+
const isRelativePath = withoutExt.startsWith("./") || withoutExt.startsWith("../");
|
|
224
|
+
if (isRelativePath && withoutExt.endsWith("/archkeep/nx")) {
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return false;
|
|
206
229
|
}
|
|
207
230
|
|
|
208
231
|
/**
|