@ecoma-io/archkeep 0.13.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/LICENSE +202 -0
- package/README.md +262 -0
- package/cli.mjs +2792 -0
- package/index.mjs +85 -0
- package/lsp.mjs +81 -0
- package/nx.mjs +24 -0
- package/package.json +81 -0
- package/presets/clean-architecture.json +78 -0
- package/presets/ddd-bounded-contexts.json +88 -0
- package/presets/hexagonal.json +68 -0
- package/presets/layered.json +92 -0
- package/presets/modular-monolith.json +85 -0
- package/presets/vertical-slice.json +68 -0
- package/src/analysis/analyze.mjs +218 -0
- package/src/analysis/contract.md +259 -0
- package/src/analysis/go.mjs +414 -0
- package/src/analysis/manifest-util.mjs +68 -0
- package/src/analysis/python.mjs +1266 -0
- package/src/analysis/registry.mjs +74 -0
- package/src/analysis/rust.mjs +674 -0
- package/src/analysis/source-util.mjs +230 -0
- package/src/analysis/typescript.mjs +1034 -0
- package/src/analysis/vue.mjs +156 -0
- package/src/architecture-intent/intent-fingerprint.mjs +29 -0
- package/src/architecture-intent/judge.mjs +539 -0
- package/src/architecture-intent/model.mjs +703 -0
- package/src/architecture-intent/selectors.mjs +170 -0
- package/src/canonical.mjs +48 -0
- package/src/commands/README.md +266 -0
- package/src/commands/adr.mjs +248 -0
- package/src/commands/check.mjs +989 -0
- package/src/commands/context-command.mjs +212 -0
- package/src/commands/context.mjs +790 -0
- package/src/commands/custom-rules.mjs +428 -0
- package/src/commands/debt.mjs +218 -0
- package/src/commands/diff.mjs +523 -0
- package/src/commands/discover.mjs +159 -0
- package/src/commands/drift.mjs +473 -0
- package/src/commands/edge-constraints.mjs +355 -0
- package/src/commands/explain.mjs +359 -0
- package/src/commands/fitness.mjs +226 -0
- package/src/commands/graph.mjs +297 -0
- package/src/commands/health.mjs +213 -0
- package/src/commands/history.mjs +614 -0
- package/src/commands/impact.mjs +226 -0
- package/src/commands/plan-context-command.mjs +496 -0
- package/src/commands/policy.mjs +138 -0
- package/src/commands/provenance-command.mjs +352 -0
- package/src/commands/provenance.mjs +159 -0
- package/src/commands/reconcile.mjs +219 -0
- package/src/commands/report.mjs +553 -0
- package/src/commands/snapshot-meta.mjs +107 -0
- package/src/commands/waivers.mjs +240 -0
- package/src/config.mjs +1308 -0
- package/src/containment.mjs +234 -0
- package/src/custom-rules/evidence.mjs +340 -0
- package/src/custom-rules/host.mjs +1023 -0
- package/src/custom-rules/values.mjs +43 -0
- package/src/entry-point.mjs +55 -0
- package/src/errors.mjs +36 -0
- package/src/eslint-config.mjs +542 -0
- package/src/go-work.mjs +394 -0
- package/src/governance/adr-registry.mjs +539 -0
- package/src/governance/clock.mjs +69 -0
- package/src/governance/debt-ledger.mjs +274 -0
- package/src/governance/discovery-proposal.mjs +423 -0
- package/src/governance/fitness-registry.mjs +504 -0
- package/src/governance/fitness-rules.mjs +668 -0
- package/src/governance/metrics.mjs +392 -0
- package/src/governance/preset-fingerprints.json +16 -0
- package/src/governance/profile-registry.mjs +366 -0
- package/src/governance/provenance-record.mjs +177 -0
- package/src/governance/reconcile-candidates.mjs +301 -0
- package/src/governance/reconcile-score.mjs +503 -0
- package/src/governance/row-schema.mjs +208 -0
- package/src/governance/verdict.mjs +127 -0
- package/src/governance/waiver.mjs +105 -0
- package/src/graph/create-dependencies.mjs +96 -0
- package/src/intent/intent-manifest.json +347 -0
- package/src/intent/mask-non-code.mjs +640 -0
- package/src/lsp/boundary-config.mjs +225 -0
- package/src/lsp/diagnose.mjs +202 -0
- package/src/lsp/diagnostics.mjs +241 -0
- package/src/lsp/protocol.mjs +215 -0
- package/src/lsp/server.mjs +922 -0
- package/src/lsp/workspace-index.mjs +891 -0
- package/src/nx-json.mjs +95 -0
- package/src/options.mjs +611 -0
- package/src/process.mjs +91 -0
- package/src/providers/moon.mjs +733 -0
- package/src/providers/native/README.md +204 -0
- package/src/providers/native/coverage.mjs +74 -0
- package/src/providers/native/differential.fixtures.mjs +1277 -0
- package/src/providers/native/discover.mjs +431 -0
- package/src/providers/native/graph.mjs +234 -0
- package/src/providers/native/index.mjs +152 -0
- package/src/providers/native/model.mjs +755 -0
- package/src/providers/nx.mjs +178 -0
- package/src/report/README.md +89 -0
- package/src/report/adr-text.mjs +129 -0
- package/src/report/context-text.mjs +109 -0
- package/src/report/debt-text.mjs +105 -0
- package/src/report/diff-text.mjs +219 -0
- package/src/report/discover-text.mjs +186 -0
- package/src/report/drift-text.mjs +194 -0
- package/src/report/envelope-shape.mjs +161 -0
- package/src/report/evidence.mjs +157 -0
- package/src/report/explain-text.mjs +159 -0
- package/src/report/graph-text.mjs +116 -0
- package/src/report/health-text.mjs +123 -0
- package/src/report/history-text.mjs +204 -0
- package/src/report/impact-text.mjs +128 -0
- package/src/report/json.mjs +173 -0
- package/src/report/plan-context-text.mjs +159 -0
- package/src/report/provenance-text.mjs +78 -0
- package/src/report/reconcile-text.mjs +159 -0
- package/src/report/report-text.mjs +264 -0
- package/src/report/sarif.mjs +953 -0
- package/src/report/text.mjs +823 -0
- package/src/report/waivers-text.mjs +100 -0
- package/src/rules/README.md +123 -0
- package/src/rules/index.mjs +962 -0
- package/src/rules/match.mjs +1708 -0
- package/src/rules/messages.mjs +73 -0
- package/src/rules/reachability.mjs +224 -0
- package/src/rules/specifiers.mjs +300 -0
- package/src/rules/tags.mjs +238 -0
- package/src/rules/topology.mjs +333 -0
- package/src/tsconfig-paths.mjs +237 -0
- package/src/verdict.mjs +145 -0
- package/src/workspace.mjs +580 -0
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Moon project-model provider: `.moon/` or `.config/moon/` directory, no Nx installed.
|
|
3
|
+
*
|
|
4
|
+
* A Moonrepo workspace is identified by a `.moon/` directory at its root.
|
|
5
|
+
* Moonrepo v2.0+ also supports `.config/moon/` as an alternative config
|
|
6
|
+
* directory; which one a given root carries is answered by `moonMarkerAt`
|
|
7
|
+
* below — the one dispatcher every consumer shares, and the place a root
|
|
8
|
+
* carrying both is refused. The winner is recorded in the `CommandContext`'s
|
|
9
|
+
* `marker` field (`../commands/context.mjs`).
|
|
10
|
+
*
|
|
11
|
+
* This provider reads the project graph via `moon project-graph --json`,
|
|
12
|
+
* the same way `./nx.mjs` reads the Nx graph via `nx graph --file=`.
|
|
13
|
+
*
|
|
14
|
+
* This provider implements the one-call contract `./nx.mjs` already follows
|
|
15
|
+
* (`readProjectGraph`), not the two-call `./native/` contract
|
|
16
|
+
* (`discover`/`buildGraph`): Moon already resolves projects, tags and edges
|
|
17
|
+
* before this package ever asks, so a single call is enough — the same
|
|
18
|
+
* reason the Nx provider has no `discover` step.
|
|
19
|
+
*
|
|
20
|
+
* @typedef {object} ProjectModelProvider
|
|
21
|
+
* @property {string} name Short, stable identifier — `"moon"` here — for a
|
|
22
|
+
* diagnostic that needs to say which provider answered (or failed to).
|
|
23
|
+
* @property {(workspaceRoot: string, io?: object) => object} readProjectGraph
|
|
24
|
+
* The workspace root, plus whatever spawns/reads this provider needs
|
|
25
|
+
* injected for a test — and back comes the graph half of the shape
|
|
26
|
+
* `evaluate()` consumes: `{nodes, dependencies}`, plus `workspaceLayout`
|
|
27
|
+
* when the provider infers one.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { existsSync } from "node:fs";
|
|
31
|
+
import { delimiter, join } from "node:path";
|
|
32
|
+
|
|
33
|
+
import { environmentForTree, runProcess } from "../process.mjs";
|
|
34
|
+
import { buildDependencies } from "./native/graph.mjs";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The directory that marks a Moonrepo workspace root.
|
|
38
|
+
*
|
|
39
|
+
* Moonrepo v1 uses `.moon/`; Moonrepo v2.0+ also supports `.config/moon/`
|
|
40
|
+
* as an alternative config directory. Both are valid markers; which one a
|
|
41
|
+
* workspace uses is Moon's own convention, not this tool's to decide.
|
|
42
|
+
*/
|
|
43
|
+
export const MOON_DIR = ".moon";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The alternative config directory Moonrepo v2.0+ supports.
|
|
47
|
+
*
|
|
48
|
+
* A workspace may carry either `.moon/` or `.config/moon/`. Moon treats them
|
|
49
|
+
* as mutually exclusive roots, and so does this tool: a root carrying BOTH is
|
|
50
|
+
* refused outright by `moonMarkerAt` below, naming both directories, rather
|
|
51
|
+
* than silently judged against one of them. This constant lets that check —
|
|
52
|
+
* and the marker walk (`../commands/context.mjs`'s `WORKSPACE_MARKERS`) —
|
|
53
|
+
* recognize either spelling, the same way they already check for `nx.json`
|
|
54
|
+
* and `archkeep.json`.
|
|
55
|
+
*/
|
|
56
|
+
export const MOON_ALT_DIR = ".config/moon";
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Which Moon directory marks `root` as a Moonrepo workspace — `.moon/`,
|
|
60
|
+
* `.config/moon/`, or neither.
|
|
61
|
+
*
|
|
62
|
+
* The one answer to that question, shared by every entry point that must
|
|
63
|
+
* answer it identically: `../commands/context.mjs`'s provider choice reads it
|
|
64
|
+
* for the CLI, and `../lsp/workspace-index.mjs`'s dispatch reads it for the
|
|
65
|
+
* language server — so the editor and `check` cannot disagree about which
|
|
66
|
+
* marker resolves a Moon tree (#223's two-faces defect).
|
|
67
|
+
*
|
|
68
|
+
* A root carrying both directories is REFUSED rather than silently resolved:
|
|
69
|
+
* Moon treats them as mutually exclusive roots, and picking one for the
|
|
70
|
+
* consumer would judge their workspace against a config nobody chose. It is
|
|
71
|
+
* the same posture the `.moon`+`nx.json`, `.moon`+`archkeep.json` and
|
|
72
|
+
* `nx.json`+`archkeep.json` refusals take one level out
|
|
73
|
+
* (`../commands/context.mjs`), applied inside the pair itself.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} root Absolute workspace root.
|
|
76
|
+
* @param {{exists?: (path: string) => boolean}} [io] Injectable existence test
|
|
77
|
+
* (absolute paths), so a test drives this without a filesystem. The default
|
|
78
|
+
* is plain filesystem existence, matching how every other marker is read.
|
|
79
|
+
* @returns {string} `MOON_DIR` or `MOON_ALT_DIR` — whichever the root carries;
|
|
80
|
+
* `.config/moon/` alone wins nothing over `.moon/` alone, each simply names
|
|
81
|
+
* itself when it is the only one present.
|
|
82
|
+
* @throws {Error} when both directories are present.
|
|
83
|
+
*/
|
|
84
|
+
export function moonMarkerAt(root, { exists = existsSync } = {}) {
|
|
85
|
+
const hasPrimary = exists(join(root, MOON_DIR));
|
|
86
|
+
const hasAlt = exists(join(root, MOON_ALT_DIR));
|
|
87
|
+
if (hasPrimary && hasAlt) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`archkeep: ${root} declares both ${MOON_DIR} and ${MOON_ALT_DIR} — Moonrepo treats them as ` +
|
|
90
|
+
`mutually exclusive config roots, and this tool refuses to pick between them rather than ` +
|
|
91
|
+
`judge the workspace against a config nobody chose. Remove whichever one is not the ` +
|
|
92
|
+
`workspace's real source of truth.`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return hasAlt ? MOON_ALT_DIR : hasPrimary ? MOON_DIR : null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The key `env` spells PATH under, decided by the PLATFORM rather than by
|
|
100
|
+
* which spellings the object happens to carry.
|
|
101
|
+
*
|
|
102
|
+
* The two platforms disagree about what `Path` and `PATH` even ARE, and
|
|
103
|
+
* nothing about the keys present can settle it:
|
|
104
|
+
*
|
|
105
|
+
* - **Windows** — variable names are case-INSENSITIVE. `Path`, `PATH` and
|
|
106
|
+
* `path` are three spellings of ONE variable, and `Path` is the spelling
|
|
107
|
+
* the system itself writes. `process.env` mirrors that case-insensitivity,
|
|
108
|
+
* but the copy `../process.mjs`'s `environmentForTree` hands back is a plain
|
|
109
|
+
* spread, and a plain object is not: reading `clean.PATH` there answers
|
|
110
|
+
* `undefined` on a machine whose PATH is perfectly present, and writing
|
|
111
|
+
* `PATH` back onto it would leave the child holding two spellings of one
|
|
112
|
+
* variable with only the synthesized one populated — the system PATH
|
|
113
|
+
* dropped, silently. So on Windows the key is found case-insensitively and
|
|
114
|
+
* written back under the spelling it was read from, and `resolveMoonEnv`
|
|
115
|
+
* collapses the remaining case-variants.
|
|
116
|
+
* - **POSIX** — variable names are case-SENSITIVE. `Path` and `PATH` are two
|
|
117
|
+
* ordinary, unrelated variables; both may exist, and the loader resolves the
|
|
118
|
+
* child's binary from `PATH` alone. So `PATH` is the only key this function
|
|
119
|
+
* may answer with there, and the only one `resolveMoonEnv` may write or
|
|
120
|
+
* remove.
|
|
121
|
+
*
|
|
122
|
+
* Guessing the key from `Object.keys` order instead is what made this
|
|
123
|
+
* function delete a real POSIX PATH: measured, env `{Path: "/opt/foo", PATH:
|
|
124
|
+
* "/usr/bin:/bin"}` reached the child as `{Path:
|
|
125
|
+
* "<root>/node_modules/.bin:/opt/foo"}` and nothing else — `PATH` gone, `moon`
|
|
126
|
+
* therefore ENOENT, and `isMoonBinaryMissing` below telling a consumer with
|
|
127
|
+
* Moon installed to install Moon. Loudly, confidently wrong.
|
|
128
|
+
*
|
|
129
|
+
* On Windows a populated spelling wins over an empty one; with no spelling
|
|
130
|
+
* present at all the answer is `"PATH"` on both platforms, the name the
|
|
131
|
+
* variable is then created under.
|
|
132
|
+
*
|
|
133
|
+
* @param {Record<string, string|undefined>} env
|
|
134
|
+
* @param {boolean} windows Whether the platform this env is built for treats
|
|
135
|
+
* variable names case-insensitively.
|
|
136
|
+
* @returns {string}
|
|
137
|
+
*/
|
|
138
|
+
function pathVariableKey(env, windows) {
|
|
139
|
+
if (!windows) return "PATH";
|
|
140
|
+
const spellings = Object.keys(env).filter((key) => key.toUpperCase() === "PATH");
|
|
141
|
+
return spellings.find((key) => env[key]) ?? spellings[0] ?? "PATH";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Resolves the Moon CLI binary by adding the workspace root's
|
|
146
|
+
* `node_modules/.bin` to PATH, the same directory pnpm installs platform
|
|
147
|
+
* shims into. This is the convention `npx` uses and the one
|
|
148
|
+
* `scripts/check-packages.mjs` follows for `moon projects --json`.
|
|
149
|
+
*
|
|
150
|
+
* Moon is not a peer dependency of this package — it is a dev dependency of
|
|
151
|
+
* the consumer workspace — so `require.resolve` cannot find it the way
|
|
152
|
+
* `./nx.mjs` resolves `nx`. Instead, the binary is found on PATH after the
|
|
153
|
+
* workspace's `node_modules/.bin` is prepended.
|
|
154
|
+
*
|
|
155
|
+
* **No entry of the PATH this builds is ever empty.** POSIX resolves an empty
|
|
156
|
+
* entry — a leading, trailing, or doubled delimiter — as the CURRENT
|
|
157
|
+
* DIRECTORY, and the current directory of the spawn this env is built for is
|
|
158
|
+
* the untrusted workspace being judged: a `moon` file committed into that tree
|
|
159
|
+
* would be executed in place of the real CLI. Joining the bin dir to an absent
|
|
160
|
+
* PATH produced exactly that trailing empty entry, and the Windows spelling
|
|
161
|
+
* `pathVariableKey` exists for is what made "absent" reachable on an ordinary
|
|
162
|
+
* machine. Empty entries inherited from the caller are dropped for the same
|
|
163
|
+
* reason: this env is built for one spawn into one untrusted tree, and no
|
|
164
|
+
* entry of it may mean "here".
|
|
165
|
+
*
|
|
166
|
+
* **Exactly one variable is touched, and which one is the platform's answer.**
|
|
167
|
+
* `pathVariableKey` above owns that decision; what follows from it here is the
|
|
168
|
+
* removal, which happens on Windows ONLY. There, the leftover case-variants
|
|
169
|
+
* are the same variable under other spellings and handing the child two views
|
|
170
|
+
* of it — one populated, one not — is ambiguous. On POSIX they are unrelated
|
|
171
|
+
* variables that merely look alike, so nothing is removed: deleting a POSIX
|
|
172
|
+
* `Path` because `PATH` was written is destroying a variable this tool was
|
|
173
|
+
* never asked about, and deleting the `PATH` a child is resolved through is
|
|
174
|
+
* how `moon` became ENOENT on a machine that had it.
|
|
175
|
+
*
|
|
176
|
+
* This function cannot fail. It reports where Moon will be looked for, not
|
|
177
|
+
* whether it is there — that is answered by the spawn itself, in
|
|
178
|
+
* `readProjectGraph`, which is the only place that can tell.
|
|
179
|
+
*
|
|
180
|
+
* @param {string} workspaceRoot
|
|
181
|
+
* @param {{ env?: Record<string, string|undefined>, platform?: string }} [io]
|
|
182
|
+
* The environment to build from, and the platform whose variable-name case
|
|
183
|
+
* rules apply. `platform` is injectable for one reason: the two branches
|
|
184
|
+
* below are opposite behaviours and a machine can only run one of them, so a
|
|
185
|
+
* Windows-only rule tested nowhere is a rule that ships unproven. It governs
|
|
186
|
+
* the case rule alone — the separator stays `node:path`'s `delimiter`,
|
|
187
|
+
* because the child really does run on this host.
|
|
188
|
+
* @returns {{ moon: string, env: Record<string, string|undefined> }}
|
|
189
|
+
* The Moon binary name and an env with the adjusted PATH.
|
|
190
|
+
*/
|
|
191
|
+
function resolveMoonEnv(workspaceRoot, { env = process.env, platform = process.platform } = {}) {
|
|
192
|
+
const windows = platform === "win32";
|
|
193
|
+
// Strip ambient git redirects first — the same protection every other
|
|
194
|
+
// provider gets — then add the workspace's node_modules/.bin to PATH.
|
|
195
|
+
const clean = environmentForTree(env);
|
|
196
|
+
const binDir = join(workspaceRoot, "node_modules", ".bin");
|
|
197
|
+
const key = pathVariableKey(clean, windows);
|
|
198
|
+
const entries = String(clean[key] ?? "")
|
|
199
|
+
.split(delimiter)
|
|
200
|
+
.filter((entry) => entry !== "");
|
|
201
|
+
// Segment equality, never substring containment: a directory named
|
|
202
|
+
// `<root>/node_modules/.bin-old` CONTAINS the bin dir as a substring, and a
|
|
203
|
+
// containment test reads that as "already on PATH" and never prepends the
|
|
204
|
+
// real one — leaving `moon` to resolve to whatever else on the machine
|
|
205
|
+
// answers to the name.
|
|
206
|
+
if (!entries.includes(binDir)) entries.unshift(binDir);
|
|
207
|
+
/** @type {Record<string, string|undefined>} */
|
|
208
|
+
const next = { ...clean, [key]: entries.join(delimiter) };
|
|
209
|
+
if (windows) {
|
|
210
|
+
// One variable, one spelling, on the platform where they are one variable.
|
|
211
|
+
for (const spelling of Object.keys(next)) {
|
|
212
|
+
if (spelling !== key && spelling.toUpperCase() === "PATH") delete next[spelling];
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return { moon: "moon", env: next };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Moon dependency scope (plus, where known, its `source`) → Archkeep edge type.
|
|
220
|
+
*
|
|
221
|
+
* **Moon's `"implicit"` is the INVERSE of Archkeep's, and this is the one place
|
|
222
|
+
* that has to know it.** The two vocabularies use the same word for opposite
|
|
223
|
+
* facts:
|
|
224
|
+
*
|
|
225
|
+
* - **Archkeep** (from Nx's `implicitDependencies`, and `archkeep.json`'s own
|
|
226
|
+
* row of that name): `type: "implicit"` means *a human declared this edge
|
|
227
|
+
* and there is no import behind it*. That is precisely why
|
|
228
|
+
* `../commands/edge-constraints.mjs`'s `declaredEdgeViolationsForCheck`
|
|
229
|
+
* exists — such an edge never becomes an `importSites` record, so
|
|
230
|
+
* `evaluate()` structurally cannot reach it and `check` judges it as an edge
|
|
231
|
+
* instead. `../commands/drift.mjs` and `../commands/discover.mjs` exclude
|
|
232
|
+
* the same set for the mirror reason: a declaration is not evidence of code.
|
|
233
|
+
* - **Moon**: `source` is `"explicit"` when a human wrote the dependency in
|
|
234
|
+
* `moon.yml`'s `dependsOn`, and `"implicit"` when **Moon derived it from
|
|
235
|
+
* source files** — a `package.json` entry under
|
|
236
|
+
* `javascript.syncProjectWorkspaceDependencies`, say. Moon's own shipped
|
|
237
|
+
* schema says so in as many words (`.moon/cache/schemas/project.json`,
|
|
238
|
+
* `DependencySource`, moon 2.4.6): *"The source where the dependency comes
|
|
239
|
+
* from. Either explicitly defined in configuration, or implicitly derived
|
|
240
|
+
* from source files."*
|
|
241
|
+
*
|
|
242
|
+
* So Moon's `"explicit"` carries Archkeep's `"implicit"` fact, and Moon's
|
|
243
|
+
* `"implicit"` is a code-backed edge that gets a `scope`-derived type. Mapping
|
|
244
|
+
* the two words onto each other by their spelling — which is what this
|
|
245
|
+
* function did — made `declaredEdgeViolationsForCheck` judge exactly the set it
|
|
246
|
+
* was written NOT to judge: every manifest-derived edge judged as though it had
|
|
247
|
+
* no import site, and every hand-declared edge, the only kind that really has
|
|
248
|
+
* none, skipped. A workspace with a forbidden dependency written into a
|
|
249
|
+
* `moon.yml` and no import to hide behind reported `no declared-edge
|
|
250
|
+
* violations` and exited 0 (#262).
|
|
251
|
+
*
|
|
252
|
+
* `source` is only available on a project node's own `dependencies[]` array
|
|
253
|
+
* (`transformMoonGraph`'s second edge-building loop) — `raw.graph.edges`'
|
|
254
|
+
* `[source, target, scope]` tuples carry no such field, so that loop's call
|
|
255
|
+
* always passes `source: undefined`, which cannot equal `"explicit"` and so
|
|
256
|
+
* always falls through to `scope`. The node loop is the only place a
|
|
257
|
+
* dependency can be typed `"implicit"` at all, which is what makes the
|
|
258
|
+
* "implicit wins per pair" rule in `transformMoonGraph`'s `add` correct: the
|
|
259
|
+
* loop that can see `source` overrules the loop that cannot.
|
|
260
|
+
*
|
|
261
|
+
* Once `source` is not `"explicit"` (or is unknown), Moon's `scope` decides —
|
|
262
|
+
* and every scope it can decide maps to `"static"`, never to `"dynamic"`
|
|
263
|
+
* (#280). A scope is a fact about a MANIFEST ROW — when the dependency is
|
|
264
|
+
* needed, runtime or build time — while `"dynamic"` upstream is a fact about
|
|
265
|
+
* SOURCE TEXT (`import()` written at an import site,
|
|
266
|
+
* `../../rules/topology.mjs`'s `noImportsOfLazyLoadedLibraries`). Feeding the
|
|
267
|
+
* first to rules that read the second manufactured lazy-loading nobody wrote:
|
|
268
|
+
* every dev-only dependency looked lazy-loaded, and that rule fired at the
|
|
269
|
+
* declaring project's own test file. The previous `"dynamic"` mapping existed
|
|
270
|
+
* only to serve that rule, which is exactly why a scope cannot feed it.
|
|
271
|
+
* Nothing is lost by refusing: genuine lazy loading still arrives through
|
|
272
|
+
* ANALYSIS — `mergeImportEdges` below folds real `import()` sites into this
|
|
273
|
+
* graph keyed `[source, target, type]`, so a real dynamic import adds its own
|
|
274
|
+
* `dynamic` edge regardless of the declared scope:
|
|
275
|
+
* - `"production"` — a runtime dependency. Maps to `"static"`.
|
|
276
|
+
* - `"development"` — a build-time-only dependency. Maps to `"static"`
|
|
277
|
+
* (#280).
|
|
278
|
+
* - `"build"` — a build-system dependency (not a source-level import). Maps
|
|
279
|
+
* to `"static"` as a conservative default; Archkeep judges source imports,
|
|
280
|
+
* not build graphs.
|
|
281
|
+
* - `"peer"` — a peer dependency. Maps to `"static"`.
|
|
282
|
+
* - `"root"` — the root workspace depends on a project. These are not
|
|
283
|
+
* project-to-project edges Archkeep judges, so they are omitted — checked
|
|
284
|
+
* before `source`, because a root-to-project edge is not a boundary either
|
|
285
|
+
* way.
|
|
286
|
+
*
|
|
287
|
+
* @param {string} scope
|
|
288
|
+
* @param {string} [source] Moon's own `"explicit"`/`"implicit"` marker for
|
|
289
|
+
* this specific dependency, when the caller has one.
|
|
290
|
+
* @returns {string|undefined} Archkeep edge type, or `undefined` to skip.
|
|
291
|
+
*/
|
|
292
|
+
function edgeTypeFromScope(scope, source) {
|
|
293
|
+
if (scope === "root") {
|
|
294
|
+
// Root-to-project edges are not project-to-project boundaries, declared
|
|
295
|
+
// or not.
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
298
|
+
// Moon "explicit" — written by hand in `moon.yml` — IS Archkeep "implicit".
|
|
299
|
+
if (source === "explicit") return "implicit";
|
|
300
|
+
switch (scope) {
|
|
301
|
+
// A manifest scope says WHEN a dependency is needed, never HOW its imports
|
|
302
|
+
// are written — "dynamic" is a source-text fact only analysis can attest,
|
|
303
|
+
// which `mergeImportEdges` below keeps flowing — so every scope lands on
|
|
304
|
+
// "static" (#280).
|
|
305
|
+
case "production":
|
|
306
|
+
case "development":
|
|
307
|
+
case "build":
|
|
308
|
+
case "peer":
|
|
309
|
+
return "static";
|
|
310
|
+
default:
|
|
311
|
+
// Unknown scopes become "static" — conservative, and never silent.
|
|
312
|
+
return "static";
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Maps a Moon `layer` value to a Archkeep `node.type`.
|
|
318
|
+
*
|
|
319
|
+
* Moon's `layer` is one of: `automation`, `application`, `tool`, `library`,
|
|
320
|
+
* `scaffolding`, `configuration`, `unknown` (or null). Archkeep's `type` is
|
|
321
|
+
* `"app"`, `"e2e"`, or `"lib"` — a coarser taxonomy that the rule engine
|
|
322
|
+
* uses for the `noImportsOfApps`/`enforceBuildableLibDependency` checks.
|
|
323
|
+
*
|
|
324
|
+
* @param {string|null} layer
|
|
325
|
+
* @returns {string} One of `"app"`, `"e2e"`, `"lib"`.
|
|
326
|
+
*/
|
|
327
|
+
function nodeTypeFromLayer(layer) {
|
|
328
|
+
switch (layer) {
|
|
329
|
+
case "application":
|
|
330
|
+
return "app";
|
|
331
|
+
case "automation":
|
|
332
|
+
return "e2e";
|
|
333
|
+
default:
|
|
334
|
+
return "lib";
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Derives the set of Archkeep tags from a Moon project node's metadata.
|
|
340
|
+
*
|
|
341
|
+
* Moon provides three sources of tag-like information:
|
|
342
|
+
* - `config.tags[]` — explicit tags declared in `moon.yml`. These become
|
|
343
|
+
* Archkeep tags verbatim.
|
|
344
|
+
* - `layer` — the project's layer classification. Becomes a derived tag
|
|
345
|
+
* `layer:<value>` (e.g. `layer:application`), matching Archkeep's tag
|
|
346
|
+
* convention for constraint tables. Moon tags use dashes (`type-lib`);
|
|
347
|
+
* the derived `layer:` and `stack:` tags use colons to match the
|
|
348
|
+
* convention the constraint table will use, just as Nx tags do.
|
|
349
|
+
* - `stack` — the project's stack. Becomes `stack:<value>`.
|
|
350
|
+
*
|
|
351
|
+
* All three are merged, deduplicated, and sorted.
|
|
352
|
+
*
|
|
353
|
+
* @param {object} projectNode A project node from `moon project-graph --json`.
|
|
354
|
+
* @returns {string[]}
|
|
355
|
+
*/
|
|
356
|
+
function deriveTags(projectNode) {
|
|
357
|
+
const tags = new Set();
|
|
358
|
+
// Explicit tags from moon.yml — carried verbatim.
|
|
359
|
+
const declared = projectNode.config?.tags;
|
|
360
|
+
if (Array.isArray(declared)) {
|
|
361
|
+
for (const tag of declared) tags.add(tag);
|
|
362
|
+
}
|
|
363
|
+
// Derived tags from layer/stack — colon-prefixed to match Archkeep
|
|
364
|
+
// convention for constraint tables.
|
|
365
|
+
if (projectNode.layer) tags.add(`layer:${projectNode.layer}`);
|
|
366
|
+
if (projectNode.stack) tags.add(`stack:${projectNode.stack}`);
|
|
367
|
+
return [...tags].sort();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Infers `workspaceLayout` from the source paths of Moon's projects.
|
|
372
|
+
*
|
|
373
|
+
* Moon does not declare `appsDir`/`libsDir` — there is no `nx.json`-style
|
|
374
|
+
* `workspaceLayout` key. This function examines each project's `source`
|
|
375
|
+
* (workspace-relative root) and checks for a common directory prefix shared
|
|
376
|
+
* by all projects of the same `layer`:
|
|
377
|
+
* - `application`-layer projects whose sources all share a prefix → `appsDir`
|
|
378
|
+
* - `library`-layer projects whose sources all share a prefix → `libsDir`
|
|
379
|
+
*
|
|
380
|
+
* A project at the workspace ROOT contributes to neither. Its `source` is
|
|
381
|
+
* `"."` (or, on some Moon versions, `""`), whose top segment names no
|
|
382
|
+
* directory the workspace keeps apps or libs in — see the guard in the loop
|
|
383
|
+
* below for what inferring one from it would do.
|
|
384
|
+
*
|
|
385
|
+
* A prefix is "shared" when every project of that layer starts with the same
|
|
386
|
+
* top-level directory. If no consistent prefix exists, that key is omitted
|
|
387
|
+
* from the result. If either prefix is found but the other is not, `null` is
|
|
388
|
+
* returned — a partial layout like `{appsDir: "apps"}` would pass through
|
|
389
|
+
* `graph.workspaceLayout ?? DEFAULT_WORKSPACE_LAYOUT` unchanged (the object
|
|
390
|
+
* is truthy, so `??` does not fire) and `isAbsoluteImportIntoAnotherProject`
|
|
391
|
+
* would evaluate `${workspaceLayout.libsDir}/` as `"undefined/"`, silently
|
|
392
|
+
* disabling the absolute-import rule for the missing axis. Returning `null`
|
|
393
|
+
* forces the `??` fallback, which applies the complete default
|
|
394
|
+
* `{libsDir: "libs", appsDir: "apps"}`. If neither prefix is found, `null`
|
|
395
|
+
* is returned for the same reason.
|
|
396
|
+
*
|
|
397
|
+
* @param {object[]} projectNodes Project nodes from Moon's `data` map values.
|
|
398
|
+
* @returns {{appsDir: string, libsDir: string}|null}
|
|
399
|
+
*/
|
|
400
|
+
function inferWorkspaceLayout(projectNodes) {
|
|
401
|
+
const appDirs = new Set();
|
|
402
|
+
const libDirs = new Set();
|
|
403
|
+
for (const node of projectNodes) {
|
|
404
|
+
if (!node.source) continue;
|
|
405
|
+
const topDir = node.source.split("/")[0];
|
|
406
|
+
// A source that names no directory BELOW the workspace root contributes no
|
|
407
|
+
// prefix. Moon spells the root project's source `"."`, and
|
|
408
|
+
// `".".split("/")[0]` is `"."` — which infers `appsDir: "."` and makes
|
|
409
|
+
// `../rules/specifiers.mjs`'s `isAbsoluteImportIntoAnotherProject` test
|
|
410
|
+
// `imp.startsWith("./")`: EVERY ordinary relative import in the workspace
|
|
411
|
+
// reported as an absolute import into another project, from one
|
|
412
|
+
// root-level `moon.yml`. The guard is on the resolved top SEGMENT rather
|
|
413
|
+
// than on `source` itself, so every spelling that lands there — `"."`,
|
|
414
|
+
// `"./"`, `"./apps/web"`, a leading `/` — is covered by one test instead
|
|
415
|
+
// of a list the next spelling escapes. Excluding a root project can only
|
|
416
|
+
// leave a prefix set empty, which returns the incomplete layout `null`
|
|
417
|
+
// above and falls back to the complete default: the loud direction.
|
|
418
|
+
if (topDir === "" || topDir === "." || topDir === "..") continue;
|
|
419
|
+
if (node.layer === "application") appDirs.add(topDir);
|
|
420
|
+
else if (node.layer === "library") libDirs.add(topDir);
|
|
421
|
+
}
|
|
422
|
+
const appsDir = appDirs.size === 1 ? [...appDirs][0] : undefined;
|
|
423
|
+
const libsDir = libDirs.size === 1 ? [...libDirs][0] : undefined;
|
|
424
|
+
// Both keys must be present, or neither. A partial layout would silently
|
|
425
|
+
// disable the absolute-import rule for the missing axis — see the header
|
|
426
|
+
// comment for the mechanism.
|
|
427
|
+
if (appsDir === undefined || libsDir === undefined) return null;
|
|
428
|
+
return { appsDir, libsDir };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Transforms Moon's `project-graph --json` output into the shape
|
|
433
|
+
* `evaluate()` consumes: `{nodes, dependencies}`.
|
|
434
|
+
*
|
|
435
|
+
* Moon's graph uses integer-indexed nodes and edges:
|
|
436
|
+
* ```jsonc
|
|
437
|
+
* {
|
|
438
|
+
* "graph": { "nodes": [0, 1], "edges": [[0, 1, "production"]] },
|
|
439
|
+
* "data": { "0": { "id": "web", "source": "apps/web", ... }, ... }
|
|
440
|
+
* }
|
|
441
|
+
* ```
|
|
442
|
+
*
|
|
443
|
+
* Archkeep's graph uses name-keyed records:
|
|
444
|
+
* ```js
|
|
445
|
+
* {
|
|
446
|
+
* nodes: { "web": { name: "web", type: "app", data: { root: "apps/web", tags: [...] } } },
|
|
447
|
+
* dependencies: { "web": [{ source: "web", target: "api", type: "static" }] }
|
|
448
|
+
* }
|
|
449
|
+
* ```
|
|
450
|
+
*
|
|
451
|
+
* Null-prototype objects for `nodes` and `dependencies`, for the same reason
|
|
452
|
+
* `./native/graph.mjs` uses them: a project literally named `__proto__` is a
|
|
453
|
+
* name this provider does not control (it comes from `moon.yml`'s `id`), and a
|
|
454
|
+
* plain `{}` answers `nodes["__proto__"] = …` by repointing the object's own
|
|
455
|
+
* prototype rather than adding an entry — silent, and exactly the shape
|
|
456
|
+
* `../../../AGENTS.md`'s invariant refuses.
|
|
457
|
+
*
|
|
458
|
+
* @param {object} raw The parsed JSON from `moon project-graph --json`.
|
|
459
|
+
* @returns {{nodes: Record<string, object>, dependencies: Record<string, object[]>, workspaceLayout?: object}}
|
|
460
|
+
*/
|
|
461
|
+
export function transformMoonGraph(raw) {
|
|
462
|
+
if (!raw?.data) {
|
|
463
|
+
throw new Error(
|
|
464
|
+
"archkeep: `moon project-graph` produced no `data` map — " +
|
|
465
|
+
"nothing can be judged against a graph with no projects in it",
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** @type {Record<string, object>} */
|
|
470
|
+
const nodes = Object.create(null);
|
|
471
|
+
/** @type {Record<string, {source: string, target: string, type: string}[]>} */
|
|
472
|
+
const dependencies = Object.create(null);
|
|
473
|
+
// Keyed on the (source, target) PAIR alone, never on `type`: the same
|
|
474
|
+
// dependency is represented twice in Moon's own output — once in
|
|
475
|
+
// `raw.graph.edges` (typed only from `scope`, since that tuple carries no
|
|
476
|
+
// `source` field) and once in the owning node's own `dependencies[]`
|
|
477
|
+
// (which does carry `source`, and so is the only place a dependency can be
|
|
478
|
+
// typed `"implicit"` at all). Keying on `[source, target, type]` — the
|
|
479
|
+
// previous shape — let both survive as two distinct edges for one real
|
|
480
|
+
// dependency: an `implicit` edge from the second loop, plus a phantom
|
|
481
|
+
// `static`/`dynamic` duplicate from the first, and only the second loop's
|
|
482
|
+
// exclusion callers (`../commands/drift.mjs`, `../commands/discover.mjs`)
|
|
483
|
+
// ever look at `type`, so the phantom slipped past every `edge.type ===
|
|
484
|
+
// "implicit"` check as a fake code-derived edge. One pair now always
|
|
485
|
+
// resolves to exactly one entry, and `"implicit"` — the hand-declared fact,
|
|
486
|
+
// which only the node loop can see (`edgeTypeFromScope`'s header) — always
|
|
487
|
+
// wins over a scope-derived type for that same pair, in whichever order the
|
|
488
|
+
// two loops below discover it.
|
|
489
|
+
/** @type {Map<string, {source: string, target: string, type: string}>} */
|
|
490
|
+
const edgesByPair = new Map();
|
|
491
|
+
const add = (source, target, type) => {
|
|
492
|
+
if (!source || !target || source === target) return;
|
|
493
|
+
if (!nodes[target]) return;
|
|
494
|
+
const key = JSON.stringify([source, target]);
|
|
495
|
+
const existing = edgesByPair.get(key);
|
|
496
|
+
if (existing) {
|
|
497
|
+
if (type === "implicit" && existing.type !== "implicit") existing.type = "implicit";
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
const entry = { source, target, type };
|
|
501
|
+
edgesByPair.set(key, entry);
|
|
502
|
+
(dependencies[source] ??= []).push(entry);
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
// Build nodes from Moon's data map. Each entry is indexed by integer key
|
|
506
|
+
// but identified by its `id` string — which becomes the Archkeep node key.
|
|
507
|
+
const projectNodes = Object.values(raw.data);
|
|
508
|
+
for (const node of projectNodes) {
|
|
509
|
+
if (!node.id || !node.source) continue;
|
|
510
|
+
const tags = deriveTags(node);
|
|
511
|
+
// Nx's `implicitDependencies` is the hand-declared list — so it is Moon's
|
|
512
|
+
// `"explicit"` dependencies that belong here, not its `"implicit"` ones.
|
|
513
|
+
// Same inversion as `edgeTypeFromScope` above, same reason.
|
|
514
|
+
const implicitDeps = Array.isArray(node.dependencies)
|
|
515
|
+
? node.dependencies.filter((d) => d.source === "explicit").map((d) => d.id)
|
|
516
|
+
: [];
|
|
517
|
+
const taskTargets = Array.isArray(node.taskTargets) ? node.taskTargets : [];
|
|
518
|
+
nodes[node.id] = {
|
|
519
|
+
name: node.id,
|
|
520
|
+
type: nodeTypeFromLayer(node.layer),
|
|
521
|
+
data: {
|
|
522
|
+
root: node.source,
|
|
523
|
+
tags,
|
|
524
|
+
implicitDependencies: implicitDeps,
|
|
525
|
+
...(taskTargets.length > 0
|
|
526
|
+
? {
|
|
527
|
+
targets: Object.fromEntries(
|
|
528
|
+
taskTargets.map((target) => {
|
|
529
|
+
// Task targets are "projectId:taskId" — extract the task name.
|
|
530
|
+
const taskId = target.includes(":")
|
|
531
|
+
? target.split(":").slice(1).join(":")
|
|
532
|
+
: target;
|
|
533
|
+
return [taskId, { executor: "moon:declared" }];
|
|
534
|
+
}),
|
|
535
|
+
),
|
|
536
|
+
}
|
|
537
|
+
: {}),
|
|
538
|
+
},
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Build dependencies from Moon's graph edges.
|
|
543
|
+
if (Array.isArray(raw.graph?.edges)) {
|
|
544
|
+
for (const [srcIdx, tgtIdx, scope] of raw.graph.edges) {
|
|
545
|
+
const srcNode = raw.data[String(srcIdx)];
|
|
546
|
+
const tgtNode = raw.data[String(tgtIdx)];
|
|
547
|
+
if (!srcNode?.id || !tgtNode?.id) continue;
|
|
548
|
+
const type = edgeTypeFromScope(scope);
|
|
549
|
+
if (type === undefined) continue; // e.g. "root" scope
|
|
550
|
+
add(srcNode.id, tgtNode.id, type);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Also add dependencies from each project node's own `dependencies` array,
|
|
555
|
+
// which carries `scope` and `source` metadata. This covers implicit
|
|
556
|
+
// dependencies that the graph edges may not explicitly represent as edges.
|
|
557
|
+
// `dep.source` is this loop's own reason to exist over the one above: it is
|
|
558
|
+
// the only place this function's `source` argument is ever real.
|
|
559
|
+
for (const node of projectNodes) {
|
|
560
|
+
if (!node.id || !Array.isArray(node.dependencies)) continue;
|
|
561
|
+
for (const dep of node.dependencies) {
|
|
562
|
+
if (!dep.id) continue;
|
|
563
|
+
const type = edgeTypeFromScope(dep.scope, dep.source);
|
|
564
|
+
if (type === undefined) continue;
|
|
565
|
+
add(node.id, dep.id, type);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const workspaceLayout = inferWorkspaceLayout(projectNodes);
|
|
570
|
+
return workspaceLayout === null
|
|
571
|
+
? { nodes, dependencies }
|
|
572
|
+
: { nodes, dependencies, workspaceLayout };
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Folds the package's own import analysis into a Moon graph, in place.
|
|
577
|
+
*
|
|
578
|
+
* `moon project-graph --json` knows a project depends on another only when
|
|
579
|
+
* `moon.yml` says `dependsOn` — Moon has no hook this package can register
|
|
580
|
+
* the way `./nx.mjs`'s `createDependencies` registers into Nx's own graph
|
|
581
|
+
* computation. A Go, Rust or Python import crossing a project boundary with
|
|
582
|
+
* no such entry is therefore invisible to the graph alone, while the
|
|
583
|
+
* reachability rules read exactly these lists (`noCircularDependencies`, the
|
|
584
|
+
* upstream half of `notDependOnLibsWithTags`) — an edge missing here hides a
|
|
585
|
+
* cycle or a transitive finding from every verdict built on the graph, which
|
|
586
|
+
* is the silent direction. The dedupe key is `[source, target, type]`: an
|
|
587
|
+
* import that agrees with a declared edge adds nothing; one that disagrees in
|
|
588
|
+
* kind (a dynamic import of a statically declared dependency) survives as its
|
|
589
|
+
* own record, because `noImportsOfLazyLoadedLibraries` turns on that kind.
|
|
590
|
+
*
|
|
591
|
+
* ONE implementation serves both faces that compose a Moon graph — the CLI's
|
|
592
|
+
* preamble (`../commands/context.mjs`) and the language server's index
|
|
593
|
+
* (`../lsp/workspace-index.mjs`) — so the editor and `check` cannot disagree
|
|
594
|
+
* about which edges exist (#223).
|
|
595
|
+
*
|
|
596
|
+
* @param {{nodes: object, dependencies: Record<string, object[]>}} graph The
|
|
597
|
+
* graph `transformMoonGraph` (or an equivalent) returned; mutated in place.
|
|
598
|
+
* @param {{importSites: object[], projectOf: (file: string) => string|undefined}} analysis
|
|
599
|
+
* Import records in the analysis contract's shape, and the file→project map
|
|
600
|
+
* to attribute them with.
|
|
601
|
+
* @returns {{nodes: object, dependencies: Record<string, object[]>}} The same graph.
|
|
602
|
+
*/
|
|
603
|
+
export function mergeImportEdges(graph, { importSites, projectOf }) {
|
|
604
|
+
const importEdges = buildDependencies({ importSites, nodes: graph.nodes, projectOf });
|
|
605
|
+
const seenEdges = new Set(
|
|
606
|
+
Object.values(graph.dependencies)
|
|
607
|
+
.flat()
|
|
608
|
+
.map((edge) => JSON.stringify([edge.source, edge.target, edge.type])),
|
|
609
|
+
);
|
|
610
|
+
for (const [source, edges] of Object.entries(importEdges)) {
|
|
611
|
+
for (const edge of edges) {
|
|
612
|
+
const key = JSON.stringify([edge.source, edge.target, edge.type]);
|
|
613
|
+
if (seenEdges.has(key)) continue;
|
|
614
|
+
seenEdges.add(key);
|
|
615
|
+
(graph.dependencies[source] ??= []).push(edge);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return graph;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* The name or path this provider will spawn as the Moon CLI.
|
|
623
|
+
*
|
|
624
|
+
* Unlike `./nx.mjs`'s `nxCli()`, this resolves nothing and verifies nothing.
|
|
625
|
+
* `moon` is not a peer dependency of this package, so there is no
|
|
626
|
+
* `node_modules/moon` to `require.resolve`, and the binary may legitimately
|
|
627
|
+
* sit either in the workspace's own `node_modules/.bin` (the usual case, which
|
|
628
|
+
* `resolveMoonEnv` puts on PATH) or anywhere else on the consumer's PATH. The
|
|
629
|
+
* default therefore returns the bare name `"moon"` and lets the spawn be the
|
|
630
|
+
* existence check — the only test that covers both places without this file
|
|
631
|
+
* reimplementing `which`, and one that cannot refuse a globally installed Moon
|
|
632
|
+
* the way a `node_modules/.bin` stat would.
|
|
633
|
+
*
|
|
634
|
+
* It used to document a "Moon cannot be found" error it had no way to throw:
|
|
635
|
+
* the default resolver is a constant. The error a consumer actually meets is
|
|
636
|
+
* built in `readProjectGraph` below, from the spawn's own `ENOENT` — the only
|
|
637
|
+
* place the fact is available to name.
|
|
638
|
+
*
|
|
639
|
+
* `resolveMoon` stays injectable so a test can point the spawn at a known
|
|
640
|
+
* binary path without a system-wide Moon installation.
|
|
641
|
+
*
|
|
642
|
+
* @param {string} workspaceRoot
|
|
643
|
+
* @param {{ resolveMoon?: (workspaceRoot: string) => string }} [io]
|
|
644
|
+
* @returns {string} Path or name of the Moon CLI binary.
|
|
645
|
+
*/
|
|
646
|
+
function resolveMoonCli(workspaceRoot, { resolveMoon = () => "moon" } = {}) {
|
|
647
|
+
return resolveMoon(workspaceRoot);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Is this spawn failure "the binary is not there", as opposed to "it ran and
|
|
652
|
+
* failed"?
|
|
653
|
+
*
|
|
654
|
+
* Those are different facts and only the first has an install action behind
|
|
655
|
+
* it. Telling a consumer whose Moon ran and exited 2 to install Moon sends
|
|
656
|
+
* them to fix something that is not broken — the same mistake `./nx.mjs`'s
|
|
657
|
+
* `nxCli` guards against from the other direction, where only a
|
|
658
|
+
* `MODULE_NOT_FOUND` earns the "not installed" story.
|
|
659
|
+
*
|
|
660
|
+
* `../process.mjs`'s `runProcess` wraps the child's failure and carries the
|
|
661
|
+
* original on `cause`, so the code is read from there; the direct `code` is
|
|
662
|
+
* read too, for a `run` seam that surfaces a spawn error unwrapped.
|
|
663
|
+
*
|
|
664
|
+
* @param {unknown} error
|
|
665
|
+
* @returns {boolean}
|
|
666
|
+
*/
|
|
667
|
+
function isMoonBinaryMissing(error) {
|
|
668
|
+
const thrown = /** @type {{code?: unknown, cause?: {code?: unknown}}|null|undefined} */ (error);
|
|
669
|
+
return thrown?.code === "ENOENT" || thrown?.cause?.code === "ENOENT";
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* The Moon project graph for `workspaceRoot`, in the shape `evaluate()` consumes.
|
|
674
|
+
*
|
|
675
|
+
* `moon project-graph --json` emits a project graph that includes the nodes
|
|
676
|
+
* and dependencies Moonrepo computed. This function reads that output,
|
|
677
|
+
* transforms it into the `{nodes, dependencies}` shape the rule engine
|
|
678
|
+
* expects, and returns it.
|
|
679
|
+
*
|
|
680
|
+
* @param {string} workspaceRoot
|
|
681
|
+
* @param {{ run?: typeof runProcess, resolveMoon?: (workspaceRoot: string) => string, env?: Record<string, string|undefined>, platform?: string }} [io]
|
|
682
|
+
* Injectable spawn, injectable Moon resolution, injectable env, and the
|
|
683
|
+
* platform whose environment-variable case rules `resolveMoonEnv` applies —
|
|
684
|
+
* see its own header for why that one is a seam.
|
|
685
|
+
* @returns {object} `{ nodes, dependencies }`, plus `workspaceLayout` when
|
|
686
|
+
* the project paths imply one.
|
|
687
|
+
* @throws {Error} naming the install action when the Moon binary is in neither
|
|
688
|
+
* place it is looked for. Every other spawn failure, and a JSON parse
|
|
689
|
+
* failure, propagates untouched — already named by `../process.mjs`'s
|
|
690
|
+
* `runProcess`. No path here answers with an empty graph.
|
|
691
|
+
*/
|
|
692
|
+
export function readProjectGraph(
|
|
693
|
+
workspaceRoot,
|
|
694
|
+
{ run = runProcess, resolveMoon, env, platform } = {},
|
|
695
|
+
) {
|
|
696
|
+
const moonEnv = resolveMoonEnv(workspaceRoot, {
|
|
697
|
+
env: env ?? process.env,
|
|
698
|
+
platform: platform ?? process.platform,
|
|
699
|
+
});
|
|
700
|
+
const moon = resolveMoonCli(workspaceRoot, { resolveMoon });
|
|
701
|
+
let output;
|
|
702
|
+
try {
|
|
703
|
+
output = run(moon, ["project-graph", "--json"], workspaceRoot, moonEnv.env);
|
|
704
|
+
} catch (cause) {
|
|
705
|
+
// Everything except an absent binary propagates untouched: `runProcess`
|
|
706
|
+
// already names the command, the working directory and the child's own
|
|
707
|
+
// stderr, and rewriting a real Moon error here would bury it. The absent
|
|
708
|
+
// binary is re-thrown because it is the failure a consumer meets FIRST and
|
|
709
|
+
// its raw text — `spawnSync moon ENOENT` — names no action they can take.
|
|
710
|
+
if (!isMoonBinaryMissing(cause)) throw cause;
|
|
711
|
+
throw new Error(
|
|
712
|
+
`archkeep: the Moon CLI (\`${moon}\`) could not be run in ${workspaceRoot} — the Moon ` +
|
|
713
|
+
"provider reads the project graph with `moon project-graph --json`, looking for the " +
|
|
714
|
+
`binary in ${join(workspaceRoot, "node_modules", ".bin")} and then on PATH, and neither ` +
|
|
715
|
+
"answered. Install Moon in this workspace (`pnpm add -D @moonrepo/cli`), or put `moon` " +
|
|
716
|
+
"on PATH.",
|
|
717
|
+
{ cause },
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
const raw = JSON.parse(output);
|
|
721
|
+
return transformMoonGraph(raw);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* The `ProjectModelProvider`-family object `../../cli.mjs` selects when a
|
|
726
|
+
* workspace root carries a `.moon/` or `.config/moon/` directory rather than
|
|
727
|
+
* `nx.json` or `archkeep.json`.
|
|
728
|
+
*/
|
|
729
|
+
export const moonProvider = {
|
|
730
|
+
name: "moon",
|
|
731
|
+
marker: MOON_DIR,
|
|
732
|
+
readProjectGraph,
|
|
733
|
+
};
|