@effected/workspaces 0.11.2 → 0.12.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/DependencyGraph.js +74 -8
- package/README.md +12 -2
- package/index.d.ts +16 -3
- package/package.json +1 -1
package/DependencyGraph.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { WorkspacePackage } from "./WorkspacePackage.js";
|
|
2
2
|
import { PackageNotFoundError } from "./WorkspaceDiscovery.js";
|
|
3
|
-
import { Effect, Schema } from "effect";
|
|
3
|
+
import { Effect, Graph, Schema } from "effect";
|
|
4
4
|
|
|
5
5
|
//#region src/DependencyGraph.ts
|
|
6
6
|
/**
|
|
@@ -8,9 +8,10 @@ import { Effect, Schema } from "effect";
|
|
|
8
8
|
* because it contains a cycle.
|
|
9
9
|
*
|
|
10
10
|
* @remarks
|
|
11
|
-
* `cycle`
|
|
12
|
-
*
|
|
13
|
-
* set to break, not necessarily
|
|
11
|
+
* `cycle` names the actual cycle members — the sorted union of every strongly
|
|
12
|
+
* connected component with more than one package. Packages merely downstream
|
|
13
|
+
* of a cycle are excluded, so it is exactly the set to break, not necessarily
|
|
14
|
+
* a single ordered loop.
|
|
14
15
|
*
|
|
15
16
|
* @public
|
|
16
17
|
*/
|
|
@@ -165,7 +166,11 @@ packages: Schema.Array(WorkspacePackage) }) {
|
|
|
165
166
|
* the whole adjacency map per processed node. Each level is sorted
|
|
166
167
|
* lexicographically, so the output is deterministic.
|
|
167
168
|
*/
|
|
168
|
-
levels = Effect.fn("DependencyGraph.levels")(() => Effect.suspend(() =>
|
|
169
|
+
levels = Effect.fn("DependencyGraph.levels")(() => Effect.suspend(() => {
|
|
170
|
+
const edges = this.#index();
|
|
171
|
+
const result = kahn(edges);
|
|
172
|
+
return result.stalled.length > 0 ? Effect.fail(new CyclicDependencyError({ cycle: cycleMembers(edges) })) : Effect.succeed(result.levels);
|
|
173
|
+
}));
|
|
169
174
|
/** The flattened topological order — `levels()` concatenated. */
|
|
170
175
|
sort = Effect.fn("DependencyGraph.sort")(() => this.levels().pipe(Effect.map((levels) => levels.flat())));
|
|
171
176
|
/**
|
|
@@ -195,17 +200,78 @@ packages: Schema.Array(WorkspacePackage) }) {
|
|
|
195
200
|
subForward.set(node, deps);
|
|
196
201
|
for (const dep of deps) subReverse.get(dep)?.add(node);
|
|
197
202
|
}
|
|
198
|
-
const
|
|
203
|
+
const subEdges = {
|
|
199
204
|
forward: subForward,
|
|
200
205
|
reverse: subReverse
|
|
201
|
-
}
|
|
202
|
-
|
|
206
|
+
};
|
|
207
|
+
const result = kahn(subEdges);
|
|
208
|
+
return result.stalled.length > 0 ? Effect.fail(new CyclicDependencyError({ cycle: cycleMembers(subEdges) })) : Effect.succeed(result.levels.flat());
|
|
203
209
|
}));
|
|
210
|
+
/**
|
|
211
|
+
* The graph rendered as a Mermaid `flowchart TD`. Total.
|
|
212
|
+
*
|
|
213
|
+
* @remarks
|
|
214
|
+
* Renders through core's `Graph.toMermaid` over a transient graph built from
|
|
215
|
+
* the edge index. Node IDs are numeric indexes assigned in sorted-name order
|
|
216
|
+
* and package names appear only inside quoted labels, so scoped names
|
|
217
|
+
* (`@scope/a`) never break Mermaid syntax. Nodes and each node's edges are
|
|
218
|
+
* emitted in sorted order — the output is deterministic regardless of
|
|
219
|
+
* manifest key order.
|
|
220
|
+
*/
|
|
221
|
+
toMermaid() {
|
|
222
|
+
return Graph.toMermaid(materialize(this.#index()).graph, { edgeLabel: () => "" });
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
/**
|
|
226
|
+
* Materializes the forward map into a transient core `Graph`: nodes in
|
|
227
|
+
* sorted-name order (so `NodeIndex` *i* is `names[i]`) and each node's edges
|
|
228
|
+
* in sorted-target order, making the graph — and everything derived from it —
|
|
229
|
+
* deterministic for a given edge index.
|
|
230
|
+
*/
|
|
231
|
+
const materialize = (edges) => {
|
|
232
|
+
const names = [...edges.forward.keys()].sort();
|
|
233
|
+
return {
|
|
234
|
+
graph: Graph.directed((mutable) => {
|
|
235
|
+
const indexOf = /* @__PURE__ */ new Map();
|
|
236
|
+
for (const name of names) indexOf.set(name, Graph.addNode(mutable, name));
|
|
237
|
+
for (const name of names) {
|
|
238
|
+
const source = indexOf.get(name);
|
|
239
|
+
if (source === void 0) continue;
|
|
240
|
+
for (const dependency of [...edges.forward.get(name) ?? []].sort()) {
|
|
241
|
+
const target = indexOf.get(dependency);
|
|
242
|
+
if (target !== void 0) Graph.addEdge(mutable, source, target, "");
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}),
|
|
246
|
+
names
|
|
247
|
+
};
|
|
248
|
+
};
|
|
249
|
+
/**
|
|
250
|
+
* The packages participating in a dependency cycle — the sorted union of every
|
|
251
|
+
* strongly connected component with more than one member, via core's
|
|
252
|
+
* `Graph.stronglyConnectedComponents`. Self-edges are dropped at index time,
|
|
253
|
+
* so a single-member component is never cyclic here.
|
|
254
|
+
*/
|
|
255
|
+
const cycleMembers = (edges) => {
|
|
256
|
+
const { graph, names } = materialize(edges);
|
|
257
|
+
const members = /* @__PURE__ */ new Set();
|
|
258
|
+
for (const component of Graph.stronglyConnectedComponents(graph)) {
|
|
259
|
+
if (component.length < 2) continue;
|
|
260
|
+
for (const index of component) {
|
|
261
|
+
const name = names[index];
|
|
262
|
+
if (name !== void 0) members.add(name);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return [...members].sort();
|
|
204
266
|
};
|
|
205
267
|
/**
|
|
206
268
|
* Kahn's algorithm. `forward[A] = {B}` reads "A depends on B", so level 0 is
|
|
207
269
|
* the set with an out-degree of zero and each completed level decrements its
|
|
208
270
|
* dependents through the reverse index.
|
|
271
|
+
*
|
|
272
|
+
* A non-empty `stalled` only signals *that* a cycle exists — it holds every
|
|
273
|
+
* unprocessed node, including ones merely downstream of a cycle. The error
|
|
274
|
+
* payload names the actual members via `cycleMembers`.
|
|
209
275
|
*/
|
|
210
276
|
const kahn = (edges) => {
|
|
211
277
|
const remaining = /* @__PURE__ */ new Map();
|
package/README.md
CHANGED
|
@@ -70,7 +70,17 @@ Effect.runPromise(program.pipe(Effect.provide(WorkspacesLayer))).then(console.lo
|
|
|
70
70
|
// [ [ ...names with no workspace dependencies ], [ ...names that depend only on level 0 ], ... ]
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
`DependencyGraph` is a value class, not a service: build it from packages you already have. A cycle fails with `CyclicDependencyError`
|
|
73
|
+
`DependencyGraph` is a value class, not a service: build it from packages you already have. A cycle fails with `CyclicDependencyError`, whose `cycle` field names the packages actually in the cycle — the members of the strongly-connected components — and not the ones merely stalled behind it, which is the difference between a fix list and a suspect list.
|
|
74
|
+
|
|
75
|
+
`toMermaid()` renders the same graph for a job summary, an issue or a design doc. It is total, deterministic (nodes and edges both in sorted order) and safe for scoped names, which appear only inside quoted labels:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
console.log(graph.toMermaid());
|
|
79
|
+
// flowchart TD
|
|
80
|
+
// 0["@acme/app"]
|
|
81
|
+
// 1["@acme/utils"]
|
|
82
|
+
// 0 --> 1
|
|
83
|
+
```
|
|
74
84
|
|
|
75
85
|
## Change detection
|
|
76
86
|
|
|
@@ -228,7 +238,7 @@ A name miss in the derived `getPackage` fails with the service's own typed `Pack
|
|
|
228
238
|
- `WorkspaceRoot` — root discovery from a `cwd`, over `WORKSPACE_MARKERS`.
|
|
229
239
|
- `WorkspaceDiscovery` — package enumeration with a bounded descent for segment-crossing `packages/**` patterns, per-package lookup and the `makeTest` / `layerTest` in-memory test doubles.
|
|
230
240
|
- `WorkspacePackage` — a deliberately tolerant manifest model, so one member with an odd version cannot fail discovery for the whole repo. `manifestRecord` keeps the as-read `package.json` for tolerant access to fields outside the typed slice without a second read; `WorkspacePackage.manifest(pkg)` re-reads and is the opt-in bridge to `@effected/package-json`'s strict `Package`.
|
|
231
|
-
- `DependencyGraph` — a value class over discovered packages: `levels()` for parallel build tiers, the flattened topological order, and `CyclicDependencyError` when there
|
|
241
|
+
- `DependencyGraph` — a value class over discovered packages: `levels()` for parallel build tiers, the flattened topological order, `toMermaid()` for a deterministic Mermaid `flowchart TD` of the whole graph, and `CyclicDependencyError` — naming the cycle's actual members — when there is no order.
|
|
232
242
|
- `PackageManagerDetector` — npm, pnpm, yarn or bun from lockfiles and the `packageManager` field.
|
|
233
243
|
- `WorkspaceCatalogs` — pnpm catalog assembly and `catalog:` resolution, on pnpm's own catalog packages; `releaseAgeGate()` assembles the effective `@effected/npm` `ReleaseAgeGate` from inline `pnpm-workspace.yaml` release-age keys and replayed hook contributions, strictest-wins, in the same pass as the catalogs.
|
|
234
244
|
- `LockfileReader` — locate and parse the workspace's lockfile through `@effected/lockfiles`.
|
package/index.d.ts
CHANGED
|
@@ -960,9 +960,10 @@ declare const CyclicDependencyError_base: Schema.Class<CyclicDependencyError, Sc
|
|
|
960
960
|
* because it contains a cycle.
|
|
961
961
|
*
|
|
962
962
|
* @remarks
|
|
963
|
-
* `cycle`
|
|
964
|
-
*
|
|
965
|
-
* set to break, not necessarily
|
|
963
|
+
* `cycle` names the actual cycle members — the sorted union of every strongly
|
|
964
|
+
* connected component with more than one package. Packages merely downstream
|
|
965
|
+
* of a cycle are excluded, so it is exactly the set to break, not necessarily
|
|
966
|
+
* a single ordered loop.
|
|
966
967
|
*
|
|
967
968
|
* @public
|
|
968
969
|
*/
|
|
@@ -1041,6 +1042,18 @@ declare class DependencyGraph extends DependencyGraph_base {
|
|
|
1041
1042
|
* dependencies — the build order for a subset.
|
|
1042
1043
|
*/
|
|
1043
1044
|
readonly sortSubset: (names: readonly string[]) => Effect.Effect<readonly string[], CyclicDependencyError | PackageNotFoundError, never>;
|
|
1045
|
+
/**
|
|
1046
|
+
* The graph rendered as a Mermaid `flowchart TD`. Total.
|
|
1047
|
+
*
|
|
1048
|
+
* @remarks
|
|
1049
|
+
* Renders through core's `Graph.toMermaid` over a transient graph built from
|
|
1050
|
+
* the edge index. Node IDs are numeric indexes assigned in sorted-name order
|
|
1051
|
+
* and package names appear only inside quoted labels, so scoped names
|
|
1052
|
+
* (`@scope/a`) never break Mermaid syntax. Nodes and each node's edges are
|
|
1053
|
+
* emitted in sorted order — the output is deterministic regardless of
|
|
1054
|
+
* manifest key order.
|
|
1055
|
+
*/
|
|
1056
|
+
toMermaid(): string;
|
|
1044
1057
|
}
|
|
1045
1058
|
//#endregion
|
|
1046
1059
|
//#region src/PackageManagerName.d.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/workspaces",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Monorepo workspace tooling as Effect services — root discovery, package enumeration, the dependency graph, package-manager detection, pnpm catalog resolution, lockfile IO and git-based change detection.",
|
|
6
6
|
"keywords": [
|