@procedures/lib 0.2.3 → 0.3.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/CHANGELOG.md +52 -0
- package/README.md +15 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +18 -1
- package/dist/module.js.map +1 -1
- package/dist/procedures/lib/graph.d.ts +21 -0
- package/dist/procedures/lib/graph.d.ts.map +1 -0
- package/dist/procedures/lib/graph.js +118 -0
- package/dist/procedures/lib/graph.js.map +1 -0
- package/dist/procedures/lib/index.d.ts +1 -0
- package/dist/procedures/lib/index.d.ts.map +1 -1
- package/dist/procedures/lib/index.js +1 -0
- package/dist/procedures/lib/index.js.map +1 -1
- package/dist/types.d.ts +42 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +29 -0
- package/dist/types.js.map +1 -1
- package/package.json +9 -9
- package/src/index.ts +2 -0
- package/src/module.ts +19 -0
- package/src/procedures/lib/graph.ts +134 -0
- package/src/procedures/lib/index.ts +1 -0
- package/src/types.ts +82 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib.graph procedure (roadmap R44)
|
|
3
|
+
*
|
|
4
|
+
* The dependency graph of the workspace packages (`<root>/packages/*`): one node for each
|
|
5
|
+
* package, and one edge for each dependency with a `workspace:` specifier. The procedure finds
|
|
6
|
+
* the cycles (the strongly connected components, and a package that depends on itself). Without
|
|
7
|
+
* a cycle, it also gives the levels of a build order. It only reads, through `fs.readdir` and
|
|
8
|
+
* `fs.read.json`, so a root of the host confines it as it confines `lib.scan`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { buildLeveledDAG, type DAGNode } from "@procedures/dag";
|
|
13
|
+
import type { ProcedureContext } from "@procedures/client";
|
|
14
|
+
import type { DependencyKind, LibGraphEdge, LibGraphInput, LibGraphNode, LibGraphOutput } from "../../types.js";
|
|
15
|
+
import { packagesDir, resolveWorkspaceRoot } from "../../workspace.js";
|
|
16
|
+
|
|
17
|
+
const KINDS: readonly DependencyKind[] = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
|
|
18
|
+
|
|
19
|
+
interface PackageJson {
|
|
20
|
+
name?: unknown;
|
|
21
|
+
version?: unknown;
|
|
22
|
+
private?: unknown;
|
|
23
|
+
dependencies?: Record<string, string>;
|
|
24
|
+
devDependencies?: Record<string, string>;
|
|
25
|
+
peerDependencies?: Record<string, string>;
|
|
26
|
+
optionalDependencies?: Record<string, string>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface FsReaddirOutput {
|
|
30
|
+
entries: Array<{ name: string; type: string }>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The cycles of a graph: each strongly connected component with more than one node, and each
|
|
35
|
+
* node with an edge to itself (Tarjan's algorithm). Each cycle and the list are in name order.
|
|
36
|
+
*/
|
|
37
|
+
export function findCycles(names: readonly string[], edges: ReadonlyMap<string, readonly string[]>): string[][] {
|
|
38
|
+
let counter = 0;
|
|
39
|
+
const index = new Map<string, number>();
|
|
40
|
+
const low = new Map<string, number>();
|
|
41
|
+
const stack: string[] = [];
|
|
42
|
+
const onStack = new Set<string>();
|
|
43
|
+
const cycles: string[][] = [];
|
|
44
|
+
|
|
45
|
+
const visit = (name: string): void => {
|
|
46
|
+
index.set(name, counter);
|
|
47
|
+
low.set(name, counter);
|
|
48
|
+
counter++;
|
|
49
|
+
stack.push(name);
|
|
50
|
+
onStack.add(name);
|
|
51
|
+
for (const next of edges.get(name) ?? []) {
|
|
52
|
+
if (!index.has(next)) {
|
|
53
|
+
visit(next);
|
|
54
|
+
low.set(name, Math.min(low.get(name)!, low.get(next)!));
|
|
55
|
+
} else if (onStack.has(next)) {
|
|
56
|
+
low.set(name, Math.min(low.get(name)!, index.get(next)!));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (low.get(name) === index.get(name)) {
|
|
60
|
+
const component: string[] = [];
|
|
61
|
+
let member: string;
|
|
62
|
+
do {
|
|
63
|
+
member = stack.pop()!;
|
|
64
|
+
onStack.delete(member);
|
|
65
|
+
component.push(member);
|
|
66
|
+
} while (member !== name);
|
|
67
|
+
const selfEdge = component.length === 1 && (edges.get(name) ?? []).includes(name);
|
|
68
|
+
if (component.length > 1 || selfEdge) cycles.push(component.sort());
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
for (const name of names) if (!index.has(name)) visit(name);
|
|
73
|
+
return cycles.sort((a, b) => a[0]!.localeCompare(b[0]!));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The dependency graph of the workspace packages
|
|
78
|
+
*/
|
|
79
|
+
export async function libGraph(input: LibGraphInput, ctx: ProcedureContext): Promise<LibGraphOutput> {
|
|
80
|
+
const root = resolveWorkspaceRoot(input.rootPath);
|
|
81
|
+
const dir = packagesDir(root);
|
|
82
|
+
const warnings: LibGraphOutput["warnings"] = [];
|
|
83
|
+
const listing = await ctx.client.call<{ path: string }, FsReaddirOutput>(["fs", "readdir"], { path: dir });
|
|
84
|
+
|
|
85
|
+
const packages: Array<{ folder: string; manifest: PackageJson }> = [];
|
|
86
|
+
for (const entry of listing.entries.filter((item) => item.type === "directory").sort((a, b) => a.name.localeCompare(b.name))) {
|
|
87
|
+
const path = join(dir, entry.name, "package.json");
|
|
88
|
+
try {
|
|
89
|
+
const { data } = await ctx.client.call<{ path: string }, { data: unknown }>(["fs", "read.json"], { path });
|
|
90
|
+
const manifest = data as PackageJson;
|
|
91
|
+
if (typeof manifest.name !== "string" || manifest.name.length === 0) {
|
|
92
|
+
warnings.push({ path, issue: "package.json has no name" });
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
packages.push({ folder: entry.name, manifest });
|
|
96
|
+
} catch (error) {
|
|
97
|
+
warnings.push({ path, issue: `Failed to read package.json: ${error instanceof Error ? error.message : String(error)}` });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const names = new Set(packages.map((pkg) => pkg.manifest.name as string));
|
|
102
|
+
const nodes: LibGraphNode[] = [];
|
|
103
|
+
const edges: LibGraphEdge[] = [];
|
|
104
|
+
const adjacency = new Map<string, string[]>();
|
|
105
|
+
for (const { folder, manifest } of packages) {
|
|
106
|
+
const name = manifest.name as string;
|
|
107
|
+
const node: LibGraphNode = { name, folder };
|
|
108
|
+
if (typeof manifest.version === "string") node.version = manifest.version;
|
|
109
|
+
if (typeof manifest.private === "boolean") node.private = manifest.private;
|
|
110
|
+
nodes.push(node);
|
|
111
|
+
const targets = new Set<string>();
|
|
112
|
+
for (const kind of KINDS) {
|
|
113
|
+
for (const [dependency, spec] of Object.entries(manifest[kind] ?? {})) {
|
|
114
|
+
if (typeof spec !== "string" || !spec.startsWith("workspace:")) continue;
|
|
115
|
+
if (!names.has(dependency)) {
|
|
116
|
+
warnings.push({ path: join(dir, folder, "package.json"), issue: `${dependency} (${spec}) is not a package of the workspace` });
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
edges.push({ from: name, to: dependency, kind, spec });
|
|
120
|
+
targets.add(dependency);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
adjacency.set(name, [...targets].sort());
|
|
124
|
+
}
|
|
125
|
+
nodes.sort((a, b) => a.name.localeCompare(b.name));
|
|
126
|
+
|
|
127
|
+
const cycles = findCycles(nodes.map((node) => node.name), adjacency);
|
|
128
|
+
const output: LibGraphOutput = { root, nodes, edges, acyclic: cycles.length === 0, cycles, warnings };
|
|
129
|
+
if (cycles.length === 0) {
|
|
130
|
+
const dagNodes = new Map<string, DAGNode>(nodes.map((node) => [node.name, { id: node.name, dependencies: adjacency.get(node.name) ?? [] }]));
|
|
131
|
+
output.levels = buildLeveledDAG(dagNodes).levels.map((level) => level.map((node) => node.id).sort());
|
|
132
|
+
}
|
|
133
|
+
return output;
|
|
134
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -524,3 +524,85 @@ export const CoreCatchOutputSchema: z.ZodType<CoreCatchOutput> = z.looseObject({
|
|
|
524
524
|
error: z.string().optional().describe("The error of try, when it failed"),
|
|
525
525
|
continue: z.boolean().describe("True when the caller can continue"),
|
|
526
526
|
});
|
|
527
|
+
|
|
528
|
+
// =============================================================================
|
|
529
|
+
// lib.graph Types (roadmap R44)
|
|
530
|
+
// =============================================================================
|
|
531
|
+
|
|
532
|
+
/** The fields of package.json that give the edges of the graph. */
|
|
533
|
+
export type DependencyKind = "dependencies" | "devDependencies" | "peerDependencies" | "optionalDependencies";
|
|
534
|
+
|
|
535
|
+
export interface LibGraphInput {
|
|
536
|
+
/** The workspace root (default: the pnpm workspace that contains client-lib) */
|
|
537
|
+
rootPath?: string | undefined;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export interface LibGraphNode {
|
|
541
|
+
/** The name in package.json */
|
|
542
|
+
name: string;
|
|
543
|
+
/** The folder in packages/ */
|
|
544
|
+
folder: string;
|
|
545
|
+
version?: string | undefined;
|
|
546
|
+
private?: boolean | undefined;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export interface LibGraphEdge {
|
|
550
|
+
/** The package that depends */
|
|
551
|
+
from: string;
|
|
552
|
+
/** The package that it depends on */
|
|
553
|
+
to: string;
|
|
554
|
+
kind: DependencyKind;
|
|
555
|
+
/** The specifier, for example workspace:* */
|
|
556
|
+
spec: string;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
export interface LibGraphOutput {
|
|
560
|
+
/** The workspace root */
|
|
561
|
+
root: string;
|
|
562
|
+
nodes: LibGraphNode[];
|
|
563
|
+
edges: LibGraphEdge[];
|
|
564
|
+
/** True when the graph has no cycle */
|
|
565
|
+
acyclic: boolean;
|
|
566
|
+
/** Each cycle: the packages of a strongly connected component, in name order */
|
|
567
|
+
cycles: string[][];
|
|
568
|
+
/** Without a cycle: the packages in levels. Level 0 depends on no workspace package. */
|
|
569
|
+
levels?: string[][] | undefined;
|
|
570
|
+
/** The folders that the procedure could not read */
|
|
571
|
+
warnings: Array<{ path: string; issue: string }>;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
export const LibGraphInputSchema: z.ZodType<LibGraphInput> = z.object({
|
|
575
|
+
rootPath: z.string().optional().describe("The workspace root (default: the pnpm workspace that contains client-lib)"),
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
const DEPENDENCY_KINDS = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"] as const;
|
|
579
|
+
|
|
580
|
+
export const LibGraphOutputSchema: z.ZodType<LibGraphOutput> = z.looseObject({
|
|
581
|
+
root: z.string().describe("The workspace root"),
|
|
582
|
+
nodes: z
|
|
583
|
+
.array(
|
|
584
|
+
z.looseObject({
|
|
585
|
+
name: z.string().describe("The name in package.json"),
|
|
586
|
+
folder: z.string().describe("The folder in packages/"),
|
|
587
|
+
version: z.string().optional().describe("The version in package.json"),
|
|
588
|
+
private: z.boolean().optional().describe("True for a private package"),
|
|
589
|
+
})
|
|
590
|
+
)
|
|
591
|
+
.describe("The workspace packages, in name order"),
|
|
592
|
+
edges: z
|
|
593
|
+
.array(
|
|
594
|
+
z.looseObject({
|
|
595
|
+
from: z.string().describe("The package that depends"),
|
|
596
|
+
to: z.string().describe("The package that it depends on"),
|
|
597
|
+
kind: z.enum(DEPENDENCY_KINDS).describe("The field of package.json"),
|
|
598
|
+
spec: z.string().describe("The specifier, for example workspace:*"),
|
|
599
|
+
})
|
|
600
|
+
)
|
|
601
|
+
.describe("The workspace: dependencies between the packages"),
|
|
602
|
+
acyclic: z.boolean().describe("True when the graph has no cycle"),
|
|
603
|
+
cycles: z.array(z.array(z.string())).describe("Each cycle: the packages of a strongly connected component, in name order"),
|
|
604
|
+
levels: z.array(z.array(z.string())).optional().describe("Without a cycle: the packages in levels. Level 0 depends on no workspace package."),
|
|
605
|
+
warnings: z
|
|
606
|
+
.array(z.looseObject({ path: z.string().describe("The folder"), issue: z.string().describe("The problem") }))
|
|
607
|
+
.describe("The folders that the procedure could not read"),
|
|
608
|
+
});
|