@maxgfr/codeindex 2.7.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 +115 -0
- package/docs/MIGRATION.md +100 -0
- package/package.json +89 -0
- package/scripts/cli.mjs +10 -0
- package/scripts/engine.d.mts +618 -0
- package/scripts/engine.mjs +10655 -0
- package/scripts/grammars/bash.wasm +0 -0
- package/scripts/grammars/c.wasm +0 -0
- package/scripts/grammars/c_sharp.wasm +0 -0
- package/scripts/grammars/cpp.wasm +0 -0
- package/scripts/grammars/go.wasm +0 -0
- package/scripts/grammars/java.wasm +0 -0
- package/scripts/grammars/javascript.wasm +0 -0
- package/scripts/grammars/lua.wasm +0 -0
- package/scripts/grammars/php.wasm +0 -0
- package/scripts/grammars/python.wasm +0 -0
- package/scripts/grammars/ruby.wasm +0 -0
- package/scripts/grammars/rust.wasm +0 -0
- package/scripts/grammars/scala.wasm +0 -0
- package/scripts/grammars/tsx.wasm +0 -0
- package/scripts/grammars/typescript.wasm +0 -0
- package/scripts/grammars/web-tree-sitter.wasm +0 -0
- package/src/ast/extract.ts +713 -0
- package/src/ast/loader.ts +104 -0
- package/src/bm25.ts +156 -0
- package/src/callers.ts +0 -0
- package/src/calls.ts +131 -0
- package/src/categorize.ts +80 -0
- package/src/centrality.ts +148 -0
- package/src/classify.ts +53 -0
- package/src/cli-entry.ts +16 -0
- package/src/community.ts +345 -0
- package/src/complexity.ts +69 -0
- package/src/coupling.ts +0 -0
- package/src/deadcode.ts +46 -0
- package/src/edit.ts +93 -0
- package/src/engine-cli.ts +278 -0
- package/src/engine.ts +148 -0
- package/src/extract/code.ts +376 -0
- package/src/extract/markdown.ts +135 -0
- package/src/git.ts +205 -0
- package/src/glob.ts +56 -0
- package/src/graph.ts +0 -0
- package/src/grep.ts +115 -0
- package/src/hash.ts +13 -0
- package/src/ignore.ts +134 -0
- package/src/lang/c.ts +26 -0
- package/src/lang/common.ts +68 -0
- package/src/lang/csharp.ts +22 -0
- package/src/lang/elixir.ts +18 -0
- package/src/lang/go.ts +22 -0
- package/src/lang/java.ts +19 -0
- package/src/lang/js-ts.ts +112 -0
- package/src/lang/kotlin.ts +20 -0
- package/src/lang/lua.ts +18 -0
- package/src/lang/php.ts +24 -0
- package/src/lang/python.ts +22 -0
- package/src/lang/registry.ts +54 -0
- package/src/lang/ruby.ts +17 -0
- package/src/lang/rust.ts +22 -0
- package/src/lang/scala.ts +19 -0
- package/src/lang/shell.ts +17 -0
- package/src/lang/swift.ts +23 -0
- package/src/mcp.ts +512 -0
- package/src/memory.ts +75 -0
- package/src/modules.ts +128 -0
- package/src/pipeline.ts +59 -0
- package/src/query.ts +118 -0
- package/src/render/graph-json.ts +16 -0
- package/src/render/symbols-json.ts +79 -0
- package/src/repomap.ts +52 -0
- package/src/resolve.ts +950 -0
- package/src/rules.ts +249 -0
- package/src/scan.ts +172 -0
- package/src/sort.ts +12 -0
- package/src/surprise.ts +58 -0
- package/src/tests-map.ts +105 -0
- package/src/types.ts +201 -0
- package/src/util.ts +169 -0
- package/src/viz.ts +44 -0
- package/src/walk.ts +216 -0
- package/src/workspaces.ts +748 -0
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
// Multi-ecosystem workspace/monorepo detection (merged from ultradoc's manifest
|
|
2
|
+
// probing and reconstruct's superset): npm/yarn workspaces, pnpm, lerna, nx,
|
|
3
|
+
// cargo workspaces, go.work, maven modules, uv workspaces (pyproject), Composer
|
|
4
|
+
// path repositories, and Gradle settings includes. Returns the package list
|
|
5
|
+
// with a workspace-level dependency graph (name edges + path edges), one cycle
|
|
6
|
+
// when present, a topological order, malformed-manifest warnings, and a
|
|
7
|
+
// longest-prefix packageOf() matcher. Deterministic: packages sorted by dir,
|
|
8
|
+
// edges and warnings sorted, no wall-clock.
|
|
9
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { readText } from "./walk.js";
|
|
12
|
+
import { byStr } from "./sort.js";
|
|
13
|
+
import { escapeRegExp } from "./util.js";
|
|
14
|
+
|
|
15
|
+
export type WorkspaceKind =
|
|
16
|
+
| "npm"
|
|
17
|
+
| "pnpm"
|
|
18
|
+
| "lerna"
|
|
19
|
+
| "nx"
|
|
20
|
+
| "cargo"
|
|
21
|
+
| "go"
|
|
22
|
+
| "maven"
|
|
23
|
+
| "uv"
|
|
24
|
+
| "composer"
|
|
25
|
+
| "gradle";
|
|
26
|
+
|
|
27
|
+
export interface WorkspacePackage {
|
|
28
|
+
name: string;
|
|
29
|
+
dir: string; // repo-relative posix path
|
|
30
|
+
kind: WorkspaceKind;
|
|
31
|
+
manifest: string; // repo-relative manifest path that named this package
|
|
32
|
+
// Free-text description when the naming manifest carries one (package.json,
|
|
33
|
+
// composer.json, pyproject [project]/[tool.poetry], Cargo [package]).
|
|
34
|
+
description?: string;
|
|
35
|
+
dependsOn?: string[]; // sibling package names, sorted
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface WorkspaceInfo {
|
|
39
|
+
packages: WorkspacePackage[];
|
|
40
|
+
cycle?: string[]; // one dependency cycle (first found, deterministic order)
|
|
41
|
+
topoOrder: string[]; // dependency-first package names (cycles appended last)
|
|
42
|
+
// Malformed manifests met during detection (e.g. an unparseable
|
|
43
|
+
// package.json). The member dir is still registered — named by its full dir
|
|
44
|
+
// path — and the reason lands here instead of being silently dropped.
|
|
45
|
+
// Deduplicated and sorted for determinism.
|
|
46
|
+
warnings: string[];
|
|
47
|
+
packageOf(rel: string): WorkspacePackage | undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const WS_SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", "target", "coverage"]);
|
|
51
|
+
const MAX_RECURSE_DEPTH = 4;
|
|
52
|
+
|
|
53
|
+
function readJson(path: string, label?: string, warnings?: string[]): Record<string, unknown> | undefined {
|
|
54
|
+
const raw = readText(path);
|
|
55
|
+
if (!raw) return undefined;
|
|
56
|
+
try {
|
|
57
|
+
const parsed: unknown = JSON.parse(raw);
|
|
58
|
+
if (parsed && typeof parsed === "object") return parsed as Record<string, unknown>;
|
|
59
|
+
if (label && warnings) warnings.push(`malformed ${label}: not a JSON object`);
|
|
60
|
+
return undefined;
|
|
61
|
+
} catch (e) {
|
|
62
|
+
if (label && warnings) {
|
|
63
|
+
const reason = String(e instanceof Error ? e.message : e).split("\n")[0];
|
|
64
|
+
warnings.push(`malformed ${label}: ${reason}`);
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function tomlSectionBody(toml: string, section: string): string | null {
|
|
71
|
+
const re = new RegExp(`^\\[${escapeRegExp(section)}\\]\\s*$([\\s\\S]*?)(?=^\\[|$(?![\\s\\S]))`, "m");
|
|
72
|
+
const m = toml.match(re);
|
|
73
|
+
return m ? m[1]! : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function tomlStringArray(body: string, key: string): string[] {
|
|
77
|
+
const m = body.match(new RegExp(`${escapeRegExp(key)}\\s*=\\s*\\[([^\\]]*)\\]`));
|
|
78
|
+
if (!m) return [];
|
|
79
|
+
return m[1]!
|
|
80
|
+
.split(/\r?\n/)
|
|
81
|
+
.map((line) => line.replace(/#.*$/, ""))
|
|
82
|
+
.join("\n")
|
|
83
|
+
.split(",")
|
|
84
|
+
.map((s) => s.trim().replace(/^["']|["']$/g, ""))
|
|
85
|
+
.filter(Boolean);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function tomlString(body: string | null, key: string): string | undefined {
|
|
89
|
+
return body?.match(new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*["']([^"']+)["']`, "m"))?.[1];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Glob for workspace patterns/negations (npm semantics: `*` one level, `**`
|
|
93
|
+
// crossing, match a prefix so `packages/*` covers the package dir itself).
|
|
94
|
+
function wsGlobToRegExp(pat: string): RegExp {
|
|
95
|
+
let re = "";
|
|
96
|
+
for (let i = 0; i < pat.length; i++) {
|
|
97
|
+
const c = pat[i]!;
|
|
98
|
+
if (c === "*") {
|
|
99
|
+
if (pat[i + 1] === "*") {
|
|
100
|
+
re += ".*";
|
|
101
|
+
i++;
|
|
102
|
+
if (pat[i + 1] === "/") i++;
|
|
103
|
+
} else {
|
|
104
|
+
re += "[^/]*";
|
|
105
|
+
}
|
|
106
|
+
} else if ("\\^$.|?+()[]{}".includes(c)) {
|
|
107
|
+
re += "\\" + c;
|
|
108
|
+
} else {
|
|
109
|
+
re += c;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return new RegExp(`^${re}($|/)`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// --- per-manifest package probes ------------------------------------------
|
|
116
|
+
// Each probe identifies `dir` through one manifest kind; packageAt() runs them
|
|
117
|
+
// in a kind-aware order. When a manifest exists but names nothing (or cannot
|
|
118
|
+
// be parsed), the FULL dir path is the name — basenames collide across trees
|
|
119
|
+
// (`packages/a/utils` vs `packages/b/utils`).
|
|
120
|
+
|
|
121
|
+
function probeNodePkg(root: string, dir: string, kind: WorkspaceKind, warnings: string[]): WorkspacePackage | undefined {
|
|
122
|
+
const path = join(root, dir, "package.json");
|
|
123
|
+
if (!existsSync(path)) return undefined;
|
|
124
|
+
const manifest = `${dir}/package.json`;
|
|
125
|
+
const pkg = readJson(path, manifest, warnings);
|
|
126
|
+
const out: WorkspacePackage = {
|
|
127
|
+
name: typeof pkg?.name === "string" && pkg.name ? pkg.name : dir,
|
|
128
|
+
dir,
|
|
129
|
+
kind,
|
|
130
|
+
manifest,
|
|
131
|
+
};
|
|
132
|
+
if (typeof pkg?.description === "string" && pkg.description) out.description = pkg.description;
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function probeCargo(root: string, dir: string): WorkspacePackage | undefined {
|
|
137
|
+
const path = join(root, dir, "Cargo.toml");
|
|
138
|
+
if (!existsSync(path)) return undefined;
|
|
139
|
+
const body = tomlSectionBody(readText(path), "package");
|
|
140
|
+
const out: WorkspacePackage = {
|
|
141
|
+
name: tomlString(body, "name") ?? dir,
|
|
142
|
+
dir,
|
|
143
|
+
kind: "cargo",
|
|
144
|
+
manifest: `${dir}/Cargo.toml`,
|
|
145
|
+
};
|
|
146
|
+
const description = tomlString(body, "description");
|
|
147
|
+
if (description) out.description = description;
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function probeGoMod(root: string, dir: string): WorkspacePackage | undefined {
|
|
152
|
+
const path = join(root, dir, "go.mod");
|
|
153
|
+
if (!existsSync(path)) return undefined;
|
|
154
|
+
const name = readText(path).match(/^module\s+(\S+)/m)?.[1] ?? dir;
|
|
155
|
+
return { name, dir, kind: "go", manifest: `${dir}/go.mod` };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function probeMaven(root: string, dir: string): WorkspacePackage | undefined {
|
|
159
|
+
const path = join(root, dir, "pom.xml");
|
|
160
|
+
if (!existsSync(path)) return undefined;
|
|
161
|
+
return { name: ownArtifactId(readText(path)) ?? dir, dir, kind: "maven", manifest: `${dir}/pom.xml` };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function probePyproject(root: string, dir: string): WorkspacePackage | undefined {
|
|
165
|
+
const path = join(root, dir, "pyproject.toml");
|
|
166
|
+
if (!existsSync(path)) return undefined;
|
|
167
|
+
const toml = readText(path);
|
|
168
|
+
const project = tomlSectionBody(toml, "project");
|
|
169
|
+
const poetry = tomlSectionBody(toml, "tool.poetry");
|
|
170
|
+
const out: WorkspacePackage = {
|
|
171
|
+
name: tomlString(project, "name") ?? tomlString(poetry, "name") ?? dir,
|
|
172
|
+
dir,
|
|
173
|
+
kind: "uv",
|
|
174
|
+
manifest: `${dir}/pyproject.toml`,
|
|
175
|
+
};
|
|
176
|
+
const description = tomlString(project, "description") ?? tomlString(poetry, "description");
|
|
177
|
+
if (description) out.description = description;
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function probeComposer(root: string, dir: string, warnings: string[]): WorkspacePackage | undefined {
|
|
182
|
+
const path = join(root, dir, "composer.json");
|
|
183
|
+
if (!existsSync(path)) return undefined;
|
|
184
|
+
const manifest = `${dir}/composer.json`;
|
|
185
|
+
const pkg = readJson(path, manifest, warnings);
|
|
186
|
+
const out: WorkspacePackage = {
|
|
187
|
+
name: typeof pkg?.name === "string" && pkg.name ? pkg.name : dir,
|
|
188
|
+
dir,
|
|
189
|
+
kind: "composer",
|
|
190
|
+
manifest,
|
|
191
|
+
};
|
|
192
|
+
if (typeof pkg?.description === "string" && pkg.description) out.description = pkg.description;
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Nx projects may carry NO package.json — `project.json` alone names them.
|
|
197
|
+
function probeNxProject(root: string, dir: string, warnings: string[]): WorkspacePackage | undefined {
|
|
198
|
+
const path = join(root, dir, "project.json");
|
|
199
|
+
if (!existsSync(path)) return undefined;
|
|
200
|
+
const manifest = `${dir}/project.json`;
|
|
201
|
+
const proj = readJson(path, manifest, warnings);
|
|
202
|
+
return {
|
|
203
|
+
name: typeof proj?.name === "string" && proj.name ? proj.name : dir,
|
|
204
|
+
dir,
|
|
205
|
+
kind: "nx",
|
|
206
|
+
manifest,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function probeGradle(root: string, dir: string): WorkspacePackage | undefined {
|
|
211
|
+
for (const f of ["build.gradle", "build.gradle.kts"]) {
|
|
212
|
+
if (existsSync(join(root, dir, f))) {
|
|
213
|
+
// Gradle build files carry no project name — settings.gradle assigns
|
|
214
|
+
// paths, so the full dir path IS the identity.
|
|
215
|
+
return { name: dir, dir, kind: "gradle", manifest: `${dir}/${f}` };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Identify a directory as a package: probe manifests in a kind-aware order.
|
|
222
|
+
// The discovering ecosystem's manifest wins the name — a go.work member with a
|
|
223
|
+
// coexisting package.json is a Go module and takes its name from go.mod; a uv
|
|
224
|
+
// member is a Python package first; Composer path repos read composer.json
|
|
225
|
+
// first. The generic tail still identifies packages of other ecosystems, and
|
|
226
|
+
// project.json (nx) is probed last everywhere so project.json-only members
|
|
227
|
+
// are never invisible.
|
|
228
|
+
function packageAt(root: string, dir: string, kind: WorkspaceKind, warnings: string[]): WorkspacePackage | undefined {
|
|
229
|
+
const node = () => probeNodePkg(root, dir, kind, warnings);
|
|
230
|
+
const cargo = () => probeCargo(root, dir);
|
|
231
|
+
const gomod = () => probeGoMod(root, dir);
|
|
232
|
+
const maven = () => probeMaven(root, dir);
|
|
233
|
+
const py = () => probePyproject(root, dir);
|
|
234
|
+
const composer = () => probeComposer(root, dir, warnings);
|
|
235
|
+
const nx = () => probeNxProject(root, dir, warnings);
|
|
236
|
+
const gradle = () => probeGradle(root, dir);
|
|
237
|
+
const probes =
|
|
238
|
+
kind === "go"
|
|
239
|
+
? [gomod, node, cargo, maven, py, composer, nx]
|
|
240
|
+
: kind === "uv"
|
|
241
|
+
? [py, node, cargo, gomod, maven, composer, nx]
|
|
242
|
+
: kind === "composer"
|
|
243
|
+
? [composer, node, py, cargo, gomod, maven, nx]
|
|
244
|
+
: kind === "gradle"
|
|
245
|
+
? [node, maven, cargo, gomod, py, composer, nx, gradle]
|
|
246
|
+
: [node, cargo, gomod, maven, py, composer, nx];
|
|
247
|
+
for (const probe of probes) {
|
|
248
|
+
const pkg = probe();
|
|
249
|
+
if (pkg) return pkg;
|
|
250
|
+
}
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// A child pom's FIRST <artifactId> usually belongs to its <parent> block —
|
|
255
|
+
// strip that block (and <dependencies>, whose entries also carry artifactIds)
|
|
256
|
+
// before reading the module's own coordinate.
|
|
257
|
+
function ownArtifactId(pom: string): string | undefined {
|
|
258
|
+
const stripped = pom.replace(/<parent>[\s\S]*?<\/parent>/g, "").replace(/<dependencies>[\s\S]*?<\/dependencies>/g, "");
|
|
259
|
+
return stripped.match(/<artifactId>\s*([^<]+?)\s*<\/artifactId>/)?.[1];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function addPackage(
|
|
263
|
+
root: string,
|
|
264
|
+
dir: string,
|
|
265
|
+
found: Map<string, WorkspacePackage>,
|
|
266
|
+
kind: WorkspaceKind,
|
|
267
|
+
warnings: string[],
|
|
268
|
+
): void {
|
|
269
|
+
const clean = dir.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
270
|
+
if (!clean || clean === "." || found.has(clean)) return;
|
|
271
|
+
if (clean.split("/").includes("..")) return; // never leave the repo root
|
|
272
|
+
const pkg = packageAt(root, clean, kind, warnings);
|
|
273
|
+
if (pkg) found.set(clean, pkg);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// --- glob expansion --------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
function isDirAt(root: string, rel: string): boolean {
|
|
279
|
+
try {
|
|
280
|
+
return statSync(join(root, rel)).isDirectory();
|
|
281
|
+
} catch {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function subdirsOf(root: string, base: string): string[] {
|
|
287
|
+
let entries;
|
|
288
|
+
try {
|
|
289
|
+
entries = readdirSync(base ? join(root, base) : root, { withFileTypes: true });
|
|
290
|
+
} catch {
|
|
291
|
+
return [];
|
|
292
|
+
}
|
|
293
|
+
return entries
|
|
294
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith(".") && !WS_SKIP_DIRS.has(e.name))
|
|
295
|
+
.map((e) => (base ? `${base}/${e.name}` : e.name))
|
|
296
|
+
.sort(byStr);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function descendantsOf(root: string, base: string, depth: number, out: string[]): void {
|
|
300
|
+
if (depth > MAX_RECURSE_DEPTH) return;
|
|
301
|
+
for (const sub of subdirsOf(root, base)) {
|
|
302
|
+
out.push(sub);
|
|
303
|
+
descendantsOf(root, sub, depth + 1, out);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Segment-based expansion of one workspace glob into existing directories:
|
|
308
|
+
// `*` spans one level, a partial segment (`libs-*`) filters that level, `**`
|
|
309
|
+
// matches this level plus every descendant (bounded) — so nested patterns
|
|
310
|
+
// like `packages/*/plugins/*` expand at arbitrary depth. Wildcards never
|
|
311
|
+
// enter dot-dirs or WS_SKIP_DIRS (npm's own expansion skips node_modules
|
|
312
|
+
// likewise); literal segments always resolve.
|
|
313
|
+
function expandGlobDirs(root: string, pat: string): string[] {
|
|
314
|
+
const segs = pat.split("/").filter((s) => s && s !== ".");
|
|
315
|
+
if (segs.includes("..")) return [];
|
|
316
|
+
let dirs: string[] = [""];
|
|
317
|
+
for (const seg of segs) {
|
|
318
|
+
const next = new Set<string>();
|
|
319
|
+
if (seg === "**") {
|
|
320
|
+
for (const d of dirs) {
|
|
321
|
+
if (d) next.add(d);
|
|
322
|
+
const desc: string[] = [];
|
|
323
|
+
descendantsOf(root, d, 0, desc);
|
|
324
|
+
for (const s of desc) next.add(s);
|
|
325
|
+
}
|
|
326
|
+
} else if (seg.includes("*")) {
|
|
327
|
+
const re = new RegExp(`^${seg.split("*").map(escapeRegExp).join("[^/]*")}$`);
|
|
328
|
+
for (const d of dirs) {
|
|
329
|
+
for (const sub of subdirsOf(root, d)) {
|
|
330
|
+
if (re.test(sub.split("/").pop()!)) next.add(sub);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
} else {
|
|
334
|
+
for (const d of dirs) {
|
|
335
|
+
const cand = d ? `${d}/${seg}` : seg;
|
|
336
|
+
if (isDirAt(root, cand)) next.add(cand);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
dirs = [...next];
|
|
340
|
+
if (!dirs.length) return [];
|
|
341
|
+
}
|
|
342
|
+
return dirs.filter(Boolean);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function expandPattern(
|
|
346
|
+
root: string,
|
|
347
|
+
raw: string,
|
|
348
|
+
found: Map<string, WorkspacePackage>,
|
|
349
|
+
kind: WorkspaceKind,
|
|
350
|
+
warnings: string[],
|
|
351
|
+
): void {
|
|
352
|
+
const pat = raw.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
353
|
+
if (!pat) return;
|
|
354
|
+
if (!pat.includes("*")) {
|
|
355
|
+
addPackage(root, pat, found, kind, warnings);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
for (const dir of expandGlobDirs(root, pat)) addPackage(root, dir, found, kind, warnings);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
interface WsPattern {
|
|
362
|
+
pattern: string;
|
|
363
|
+
kind: WorkspaceKind;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function npmFamilyPatterns(root: string, warnings: string[]): { positives: WsPattern[]; negations: string[] } {
|
|
367
|
+
const positives: WsPattern[] = [];
|
|
368
|
+
const negations: string[] = [];
|
|
369
|
+
const push = (raw: string, kind: WorkspaceKind): void => {
|
|
370
|
+
const t = raw.trim();
|
|
371
|
+
if (!t) return;
|
|
372
|
+
if (t.startsWith("!")) negations.push(t.slice(1));
|
|
373
|
+
else positives.push({ pattern: t, kind });
|
|
374
|
+
};
|
|
375
|
+
const pkg = readJson(join(root, "package.json"), "package.json", warnings);
|
|
376
|
+
const ws = pkg?.workspaces;
|
|
377
|
+
if (Array.isArray(ws)) {
|
|
378
|
+
for (const x of ws) if (typeof x === "string") push(x, "npm");
|
|
379
|
+
} else if (ws && typeof ws === "object" && Array.isArray((ws as { packages?: unknown }).packages)) {
|
|
380
|
+
for (const x of (ws as { packages: unknown[] }).packages) if (typeof x === "string") push(x, "npm");
|
|
381
|
+
}
|
|
382
|
+
const pnpm = readText(join(root, "pnpm-workspace.yaml"));
|
|
383
|
+
let inPackages = false;
|
|
384
|
+
for (const line of pnpm.split(/\r?\n/)) {
|
|
385
|
+
if (/^\S/.test(line)) {
|
|
386
|
+
inPackages = /^packages\s*:/.test(line);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (!inPackages) continue;
|
|
390
|
+
const m = line.match(/^\s*-\s*['"]?([^'"#]+?)['"]?\s*(?:#.*)?$/);
|
|
391
|
+
if (m) push(m[1]!.trim(), "pnpm");
|
|
392
|
+
}
|
|
393
|
+
return { positives, negations };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function fallbackNpmPatterns(root: string, warnings: string[]): WsPattern[] {
|
|
397
|
+
const lerna = readJson(join(root, "lerna.json"), "lerna.json", warnings);
|
|
398
|
+
if (lerna && Array.isArray(lerna.packages)) {
|
|
399
|
+
return (lerna.packages as unknown[])
|
|
400
|
+
.filter((x): x is string => typeof x === "string")
|
|
401
|
+
.map((pattern) => ({ pattern, kind: "lerna" as const }));
|
|
402
|
+
}
|
|
403
|
+
const nx = readJson(join(root, "nx.json"), "nx.json", warnings);
|
|
404
|
+
if (nx) {
|
|
405
|
+
const layout = (nx.workspaceLayout ?? {}) as { appsDir?: unknown; libsDir?: unknown };
|
|
406
|
+
const appsDir = typeof layout.appsDir === "string" ? layout.appsDir : "apps";
|
|
407
|
+
const libsDir = typeof layout.libsDir === "string" ? layout.libsDir : "libs";
|
|
408
|
+
return [...new Set([appsDir, libsDir])].map((dir) => ({ pattern: `${dir}/*`, kind: "nx" as const }));
|
|
409
|
+
}
|
|
410
|
+
return [];
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function detectCargoMembers(root: string, found: Map<string, WorkspacePackage>, warnings: string[]): void {
|
|
414
|
+
const toml = readText(join(root, "Cargo.toml"));
|
|
415
|
+
if (!toml) return;
|
|
416
|
+
const body = tomlSectionBody(toml, "workspace");
|
|
417
|
+
if (!body) return;
|
|
418
|
+
const members = tomlStringArray(body, "members");
|
|
419
|
+
if (!members.length) return;
|
|
420
|
+
const excludes = tomlStringArray(body, "exclude").map(wsGlobToRegExp);
|
|
421
|
+
const candidates = new Map<string, WorkspacePackage>();
|
|
422
|
+
for (const pat of members) expandPattern(root, pat, candidates, "cargo", warnings);
|
|
423
|
+
for (const [dir, pkg] of candidates) {
|
|
424
|
+
if (excludes.some((re) => re.test(dir))) continue;
|
|
425
|
+
if (!found.has(dir)) found.set(dir, pkg);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function detectGoWork(root: string, found: Map<string, WorkspacePackage>, warnings: string[]): void {
|
|
430
|
+
const gowork = readText(join(root, "go.work"));
|
|
431
|
+
if (!gowork) return;
|
|
432
|
+
const dirs: string[] = [];
|
|
433
|
+
for (const block of gowork.matchAll(/^use\s*\(([\s\S]*?)\)/gm)) {
|
|
434
|
+
for (const line of block[1]!.split(/\r?\n/)) {
|
|
435
|
+
const t = line.replace(/\/\/.*$/, "").trim();
|
|
436
|
+
if (t) dirs.push(t);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
for (const m of gowork.matchAll(/^use\s+([^\s(]+)/gm)) dirs.push(m[1]!);
|
|
440
|
+
for (const dir of dirs) {
|
|
441
|
+
if (dir === "." || dir === "./") continue;
|
|
442
|
+
addPackage(root, dir, found, "go", warnings);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function detectMavenModules(root: string, found: Map<string, WorkspacePackage>, warnings: string[]): void {
|
|
447
|
+
const pom = readText(join(root, "pom.xml"));
|
|
448
|
+
if (!pom) return;
|
|
449
|
+
const modules = pom.match(/<modules>([\s\S]*?)<\/modules>/)?.[1];
|
|
450
|
+
if (!modules) return;
|
|
451
|
+
for (const m of modules.matchAll(/<module>\s*([^<]+?)\s*<\/module>/g)) {
|
|
452
|
+
addPackage(root, m[1]!, found, "maven", warnings);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// uv workspaces: [tool.uv.workspace] members/exclude in the root pyproject.
|
|
457
|
+
function detectUvMembers(root: string, found: Map<string, WorkspacePackage>, warnings: string[]): void {
|
|
458
|
+
const toml = readText(join(root, "pyproject.toml"));
|
|
459
|
+
if (!toml) return;
|
|
460
|
+
const body = tomlSectionBody(toml, "tool.uv.workspace");
|
|
461
|
+
if (!body) return;
|
|
462
|
+
const members = tomlStringArray(body, "members");
|
|
463
|
+
if (!members.length) return;
|
|
464
|
+
const excludes = tomlStringArray(body, "exclude").map(wsGlobToRegExp);
|
|
465
|
+
const candidates = new Map<string, WorkspacePackage>();
|
|
466
|
+
for (const pat of members) expandPattern(root, pat, candidates, "uv", warnings);
|
|
467
|
+
for (const [dir, pkg] of candidates) {
|
|
468
|
+
if (excludes.some((re) => re.test(dir))) continue;
|
|
469
|
+
if (!found.has(dir)) found.set(dir, pkg);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Composer path repositories: { "repositories": [{ "type": "path", "url": … }] }
|
|
474
|
+
// — the url may be a glob (packages/*), same expansion as npm patterns.
|
|
475
|
+
function detectComposerPathRepos(root: string, found: Map<string, WorkspacePackage>, warnings: string[]): void {
|
|
476
|
+
const composer = readJson(join(root, "composer.json"), "composer.json", warnings);
|
|
477
|
+
const repos = composer?.repositories;
|
|
478
|
+
if (!Array.isArray(repos)) return;
|
|
479
|
+
for (const r of repos) {
|
|
480
|
+
if (!r || typeof r !== "object") continue;
|
|
481
|
+
const { type, url } = r as { type?: unknown; url?: unknown };
|
|
482
|
+
if (type === "path" && typeof url === "string" && url) expandPattern(root, url, found, "composer", warnings);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Gradle multi-project builds: settings.gradle(.kts) `include ':a', ':b:c'` or
|
|
487
|
+
// `include("x")` — a `:`-separated project path maps to a directory path.
|
|
488
|
+
function detectGradleIncludes(root: string, found: Map<string, WorkspacePackage>, warnings: string[]): void {
|
|
489
|
+
for (const f of ["settings.gradle", "settings.gradle.kts"]) {
|
|
490
|
+
const text = readText(join(root, f));
|
|
491
|
+
if (!text) continue;
|
|
492
|
+
for (const line of text.split(/\r?\n/)) {
|
|
493
|
+
if (!/^\s*include[\s(]/.test(line)) continue;
|
|
494
|
+
for (const m of line.matchAll(/["']([^"']+)["']/g)) {
|
|
495
|
+
const dir = m[1]!.replace(/^:/, "").replace(/:/g, "/");
|
|
496
|
+
if (dir) addPackage(root, dir, found, "gradle", warnings);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// --- workspace dependency edges -------------------------------------------
|
|
503
|
+
|
|
504
|
+
function npmEdges(root: string, pkg: WorkspacePackage, byName: Set<string>, warnings: string[]): string[] {
|
|
505
|
+
const manifest = readJson(join(root, pkg.dir, "package.json"), `${pkg.dir}/package.json`, warnings);
|
|
506
|
+
if (!manifest) return [];
|
|
507
|
+
const edges = new Set<string>();
|
|
508
|
+
for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
509
|
+
const deps = manifest[field];
|
|
510
|
+
if (!deps || typeof deps !== "object") continue;
|
|
511
|
+
for (const dep of Object.keys(deps)) {
|
|
512
|
+
if (dep !== pkg.name && byName.has(dep)) edges.add(dep);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return [...edges];
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function normalizeDepPath(fromDir: string, rel: string): string {
|
|
519
|
+
const parts = `${fromDir}/${rel}`.split("/");
|
|
520
|
+
const out: string[] = [];
|
|
521
|
+
for (const p of parts) {
|
|
522
|
+
if (!p || p === ".") continue;
|
|
523
|
+
if (p === "..") out.pop();
|
|
524
|
+
else out.push(p);
|
|
525
|
+
}
|
|
526
|
+
return out.join("/");
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function cargoEdges(root: string, pkg: WorkspacePackage, byName: Set<string>, byDir: Map<string, string>): string[] {
|
|
530
|
+
const toml = readText(join(root, pkg.dir, "Cargo.toml"));
|
|
531
|
+
if (!toml) return [];
|
|
532
|
+
const edges = new Set<string>();
|
|
533
|
+
for (const section of ["dependencies", "dev-dependencies", "build-dependencies"]) {
|
|
534
|
+
const body = tomlSectionBody(toml, section);
|
|
535
|
+
if (!body) continue;
|
|
536
|
+
for (const line of body.split(/\r?\n/)) {
|
|
537
|
+
const kv = line.match(/^\s*([A-Za-z0-9_-]+)\s*=\s*(.+)$/);
|
|
538
|
+
if (!kv) continue;
|
|
539
|
+
const dep = kv[1]!;
|
|
540
|
+
if (dep !== pkg.name && byName.has(dep)) {
|
|
541
|
+
edges.add(dep);
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
const pathDep = kv[2]!.match(/path\s*=\s*["']([^"']+)["']/);
|
|
545
|
+
if (pathDep) {
|
|
546
|
+
const target = byDir.get(normalizeDepPath(pkg.dir, pathDep[1]!));
|
|
547
|
+
if (target && target !== pkg.name) edges.add(target);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return [...edges];
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function goPkgEdges(root: string, pkg: WorkspacePackage, byName: Set<string>, byDir: Map<string, string>): string[] {
|
|
555
|
+
const gomod = readText(join(root, pkg.dir, "go.mod"));
|
|
556
|
+
if (!gomod) return [];
|
|
557
|
+
const edges = new Set<string>();
|
|
558
|
+
for (const m of gomod.matchAll(/^\s*(?:require\s+)?([^\s/(][^\s]*)\s+v[^\s]+/gm)) {
|
|
559
|
+
const dep = m[1]!;
|
|
560
|
+
if (dep !== pkg.name && byName.has(dep)) edges.add(dep);
|
|
561
|
+
}
|
|
562
|
+
for (const m of gomod.matchAll(/^\s*(?:replace\s+)?(\S+)(?:\s+\S+)?\s*=>\s*(\.\.?\/\S+)/gm)) {
|
|
563
|
+
const target = byDir.get(normalizeDepPath(pkg.dir, m[2]!));
|
|
564
|
+
if (target && target !== pkg.name) edges.add(target);
|
|
565
|
+
}
|
|
566
|
+
return [...edges];
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function mavenEdges(root: string, pkg: WorkspacePackage, byName: Set<string>): string[] {
|
|
570
|
+
const pom = readText(join(root, pkg.dir, "pom.xml"));
|
|
571
|
+
if (!pom) return [];
|
|
572
|
+
const edges = new Set<string>();
|
|
573
|
+
// Only <dependency> entries count as edges; the <parent> coordinate is a
|
|
574
|
+
// build-inheritance link, not a workspace dependency.
|
|
575
|
+
for (const m of pom.replace(/<parent>[\s\S]*?<\/parent>/g, "").matchAll(/<dependency>([\s\S]*?)<\/dependency>/g)) {
|
|
576
|
+
const aid = m[1]!.match(/<artifactId>\s*([^<]+?)\s*<\/artifactId>/)?.[1];
|
|
577
|
+
if (aid && aid !== pkg.name && byName.has(aid)) edges.add(aid);
|
|
578
|
+
}
|
|
579
|
+
return [...edges];
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function uvEdges(root: string, pkg: WorkspacePackage, byName: Set<string>): string[] {
|
|
583
|
+
const toml = readText(join(root, pkg.dir, "pyproject.toml"));
|
|
584
|
+
if (!toml) return [];
|
|
585
|
+
const edges = new Set<string>();
|
|
586
|
+
// [project] dependencies = ["sibling", "requests>=2"] — bare name prefix.
|
|
587
|
+
const project = tomlSectionBody(toml, "project");
|
|
588
|
+
if (project) {
|
|
589
|
+
for (const dep of tomlStringArray(project, "dependencies")) {
|
|
590
|
+
const name = dep.match(/^[A-Za-z0-9_.-]+/)?.[0];
|
|
591
|
+
if (name && name !== pkg.name && byName.has(name)) edges.add(name);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
// [tool.uv.sources] sibling = { workspace = true }
|
|
595
|
+
const sources = tomlSectionBody(toml, "tool.uv.sources");
|
|
596
|
+
if (sources) {
|
|
597
|
+
for (const line of sources.split(/\r?\n/)) {
|
|
598
|
+
const m = line.match(/^\s*([A-Za-z0-9_.-]+)\s*=\s*\{[^}]*workspace\s*=\s*true/);
|
|
599
|
+
if (m && m[1] !== pkg.name && byName.has(m[1]!)) edges.add(m[1]!);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return [...edges];
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function composerEdges(root: string, pkg: WorkspacePackage, byName: Set<string>, warnings: string[]): string[] {
|
|
606
|
+
const manifest = readJson(join(root, pkg.dir, "composer.json"), `${pkg.dir}/composer.json`, warnings);
|
|
607
|
+
if (!manifest) return [];
|
|
608
|
+
const edges = new Set<string>();
|
|
609
|
+
for (const field of ["require", "require-dev"]) {
|
|
610
|
+
const deps = manifest[field];
|
|
611
|
+
if (!deps || typeof deps !== "object") continue;
|
|
612
|
+
for (const dep of Object.keys(deps)) {
|
|
613
|
+
if (dep !== pkg.name && byName.has(dep)) edges.add(dep);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return [...edges];
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function gradleEdges(root: string, pkg: WorkspacePackage, byName: Set<string>, byDir: Map<string, string>): string[] {
|
|
620
|
+
for (const f of ["build.gradle", "build.gradle.kts"]) {
|
|
621
|
+
const text = readText(join(root, pkg.dir, f));
|
|
622
|
+
if (!text) continue;
|
|
623
|
+
const edges = new Set<string>();
|
|
624
|
+
// implementation project(':libs:core') — a project path is a dir path.
|
|
625
|
+
for (const m of text.matchAll(/project\s*\(\s*["']:?([^"']+)["']\s*\)/g)) {
|
|
626
|
+
const path = m[1]!.replace(/:/g, "/");
|
|
627
|
+
const target = byDir.get(path) ?? (byName.has(path) ? path : undefined);
|
|
628
|
+
if (target && target !== pkg.name) edges.add(target);
|
|
629
|
+
}
|
|
630
|
+
return [...edges];
|
|
631
|
+
}
|
|
632
|
+
return [];
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function edgesFor(
|
|
636
|
+
root: string,
|
|
637
|
+
pkg: WorkspacePackage,
|
|
638
|
+
byName: Set<string>,
|
|
639
|
+
byDir: Map<string, string>,
|
|
640
|
+
warnings: string[],
|
|
641
|
+
): string[] {
|
|
642
|
+
switch (pkg.kind) {
|
|
643
|
+
case "cargo":
|
|
644
|
+
return cargoEdges(root, pkg, byName, byDir);
|
|
645
|
+
case "go":
|
|
646
|
+
return goPkgEdges(root, pkg, byName, byDir);
|
|
647
|
+
case "maven":
|
|
648
|
+
return mavenEdges(root, pkg, byName);
|
|
649
|
+
case "uv":
|
|
650
|
+
return uvEdges(root, pkg, byName);
|
|
651
|
+
case "composer":
|
|
652
|
+
return composerEdges(root, pkg, byName, warnings);
|
|
653
|
+
case "gradle":
|
|
654
|
+
return gradleEdges(root, pkg, byName, byDir);
|
|
655
|
+
default:
|
|
656
|
+
return npmEdges(root, pkg, byName, warnings);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function findCycle(packages: WorkspacePackage[]): string[] | undefined {
|
|
661
|
+
const deps = new Map(packages.map((p) => [p.name, [...(p.dependsOn ?? [])].sort(byStr)]));
|
|
662
|
+
const state = new Map<string, "visiting" | "done">();
|
|
663
|
+
const stack: string[] = [];
|
|
664
|
+
const visit = (name: string): string[] | null => {
|
|
665
|
+
state.set(name, "visiting");
|
|
666
|
+
stack.push(name);
|
|
667
|
+
for (const dep of deps.get(name) ?? []) {
|
|
668
|
+
if (!deps.has(dep)) continue;
|
|
669
|
+
if (state.get(dep) === "visiting") return [...stack.slice(stack.indexOf(dep)), dep];
|
|
670
|
+
if (!state.has(dep)) {
|
|
671
|
+
const found = visit(dep);
|
|
672
|
+
if (found) return found;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
stack.pop();
|
|
676
|
+
state.set(name, "done");
|
|
677
|
+
return null;
|
|
678
|
+
};
|
|
679
|
+
for (const name of [...deps.keys()].sort(byStr)) {
|
|
680
|
+
if (!state.has(name)) {
|
|
681
|
+
const found = visit(name);
|
|
682
|
+
if (found) return found;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return undefined;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function topoOrder(packages: WorkspacePackage[]): string[] {
|
|
689
|
+
const remaining = new Map(packages.map((p) => [p.name, new Set(p.dependsOn ?? [])]));
|
|
690
|
+
const order: string[] = [];
|
|
691
|
+
while (remaining.size > 0) {
|
|
692
|
+
const ready = [...remaining.entries()]
|
|
693
|
+
.filter(([, deps]) => [...deps].every((d) => !remaining.has(d)))
|
|
694
|
+
.map(([name]) => name)
|
|
695
|
+
.sort(byStr);
|
|
696
|
+
if (!ready.length) {
|
|
697
|
+
// A cycle — append what's left in stable order rather than looping.
|
|
698
|
+
order.push(...[...remaining.keys()].sort(byStr));
|
|
699
|
+
break;
|
|
700
|
+
}
|
|
701
|
+
for (const name of ready) {
|
|
702
|
+
order.push(name);
|
|
703
|
+
remaining.delete(name);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return order;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
export function detectWorkspaces(root: string): WorkspaceInfo {
|
|
710
|
+
const warnings: string[] = [];
|
|
711
|
+
const found = new Map<string, WorkspacePackage>();
|
|
712
|
+
|
|
713
|
+
const { positives, negations } = npmFamilyPatterns(root, warnings);
|
|
714
|
+
const npmPatterns = positives.length ? positives : fallbackNpmPatterns(root, warnings);
|
|
715
|
+
if (npmPatterns.length) {
|
|
716
|
+
const candidates = new Map<string, WorkspacePackage>();
|
|
717
|
+
for (const { pattern, kind } of npmPatterns) expandPattern(root, pattern, candidates, kind, warnings);
|
|
718
|
+
const negRes = negations.map(wsGlobToRegExp);
|
|
719
|
+
for (const [dir, pkg] of candidates) {
|
|
720
|
+
if (negRes.some((re) => re.test(dir))) continue;
|
|
721
|
+
found.set(dir, pkg);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
detectCargoMembers(root, found, warnings);
|
|
725
|
+
detectGoWork(root, found, warnings);
|
|
726
|
+
detectMavenModules(root, found, warnings);
|
|
727
|
+
detectUvMembers(root, found, warnings);
|
|
728
|
+
detectComposerPathRepos(root, found, warnings);
|
|
729
|
+
detectGradleIncludes(root, found, warnings);
|
|
730
|
+
|
|
731
|
+
const packages = [...found.values()].sort((a, b) => byStr(a.dir, b.dir));
|
|
732
|
+
|
|
733
|
+
const byName = new Set(packages.map((p) => p.name));
|
|
734
|
+
const byDir = new Map(packages.map((p) => [p.dir, p.name]));
|
|
735
|
+
for (const pkg of packages) {
|
|
736
|
+
const edges = edgesFor(root, pkg, byName, byDir, warnings);
|
|
737
|
+
if (edges.length) pkg.dependsOn = edges.sort(byStr);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const byDepth = [...packages].sort((a, b) => b.dir.length - a.dir.length);
|
|
741
|
+
return {
|
|
742
|
+
packages,
|
|
743
|
+
cycle: findCycle(packages),
|
|
744
|
+
topoOrder: topoOrder(packages),
|
|
745
|
+
warnings: [...new Set(warnings)].sort(byStr),
|
|
746
|
+
packageOf: (rel: string) => byDepth.find((p) => rel === p.dir || rel.startsWith(p.dir + "/")),
|
|
747
|
+
};
|
|
748
|
+
}
|