@effected/workspaces 0.21.1 → 0.22.1

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.
@@ -0,0 +1,293 @@
1
+ import { indexInstances, rootInstances } from "./internal/roots.js";
2
+ import { Schema } from "effect";
3
+
4
+ //#region src/DuplicateCheck.ts
5
+ /**
6
+ * Who pulls one resolved instance into the graph.
7
+ *
8
+ * @remarks
9
+ * A tagged union a renderer narrows with `switch (dependent._tag)`:
10
+ *
11
+ * - `"importer"` — a workspace importer takes the instance directly; `path`
12
+ * is the importer path (`"."` for the root).
13
+ * - `"package"` — a resolved package's own dependency edge points at the
14
+ * instance; `name` and `version` are that package's.
15
+ *
16
+ * An importer's dependencies are attributed to the importer by path even when
17
+ * the lockfile records a workspace row for it — that row IS the importer, and
18
+ * reporting it as a `"package"` would name a directory with a placeholder
19
+ * version rather than the project that took the dependency.
20
+ *
21
+ * @public
22
+ */
23
+ const Dependent = Schema.Union([Schema.TaggedStruct("importer", { path: Schema.NonEmptyString }), Schema.TaggedStruct("package", {
24
+ name: Schema.NonEmptyString,
25
+ version: Schema.String
26
+ })]);
27
+ /**
28
+ * One resolved instance of a duplicated version, with everything that pulls it.
29
+ *
30
+ * @remarks
31
+ * `dependents` is deduplicated and in lockfile order: importers first, in the
32
+ * order the lockfile lists them, then packages in the order the lockfile lists
33
+ * them. Two instances of one `name@version` that differ only by peer suffix
34
+ * each get their own row, so the dependents of each variant stay attributed to
35
+ * the variant they actually took.
36
+ *
37
+ * @public
38
+ */
39
+ var DuplicateInstance = class extends Schema.Class("DuplicateInstance")({
40
+ /** The opaque instance id, as `@effected/lockfiles` records it. */
41
+ instanceId: Schema.NonEmptyString,
42
+ /** Every importer and package that pulls THIS copy. */
43
+ dependents: Schema.Array(Dependent)
44
+ }) {};
45
+ /**
46
+ * One version a duplicated package resolved at.
47
+ *
48
+ * @public
49
+ */
50
+ var DuplicatedVersion = class extends Schema.Class("DuplicatedVersion")({
51
+ /** The resolved version. */
52
+ version: Schema.String,
53
+ /** Every instance at this version — more than one when peer suffixes differ. */
54
+ instances: Schema.Array(DuplicateInstance)
55
+ }) {};
56
+ /**
57
+ * One package name reached at two or more distinct versions.
58
+ *
59
+ * @remarks
60
+ * `versions` always has at least two entries: a name resolved at one version
61
+ * is never a duplicate, however many peer-suffix instances that version has.
62
+ * Two instances of the same version load identical code and cannot produce the
63
+ * type-identity skew this check exists to catch, so counting them would report
64
+ * a problem nobody has.
65
+ *
66
+ * @public
67
+ */
68
+ var DuplicatedPackage = class extends Schema.Class("DuplicatedPackage")({
69
+ /** The package name. */
70
+ name: Schema.NonEmptyString,
71
+ /** Every version it resolved at, in lockfile order. Never fewer than two. */
72
+ versions: Schema.Array(DuplicatedVersion)
73
+ }) {};
74
+ /**
75
+ * The result of checking a lockfile for packages resolved at more than one
76
+ * version.
77
+ *
78
+ * @remarks
79
+ * A report rather than a bare array, for the reason `PeerCheck` is one: an
80
+ * empty array is indistinguishable from "this importer could not be looked
81
+ * at". `unresolvedImporters` makes that difference legible.
82
+ *
83
+ * Pure and total — construct it with {@link DuplicateCheck.run}, which never
84
+ * fails.
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * import { DuplicateCheck } from "@effected/workspaces";
89
+ * import { Lockfile } from "@effected/lockfiles";
90
+ * import { Effect } from "effect";
91
+ *
92
+ * const program = Effect.gen(function* () {
93
+ * const lockfile = yield* Lockfile.parse(text, { format: "pnpm" });
94
+ * const report = DuplicateCheck.run(lockfile, { names: DuplicateCheck.kit });
95
+ * for (const pkg of report.duplicates) {
96
+ * for (const version of pkg.versions) {
97
+ * for (const instance of version.instances) {
98
+ * console.log(pkg.name, version.version, instance.dependents);
99
+ * }
100
+ * }
101
+ * }
102
+ * return report.isClean;
103
+ * });
104
+ * ```
105
+ *
106
+ * @public
107
+ */
108
+ var DuplicateCheck = class DuplicateCheck extends Schema.Class("DuplicateCheck")({
109
+ /**
110
+ * Every reported name resolved at two or more distinct versions, in
111
+ * lockfile order.
112
+ */
113
+ duplicates: Schema.Array(DuplicatedPackage),
114
+ /**
115
+ * Importers whose dependencies could not be resolved to instances, so
116
+ * nothing reachable only through them was counted.
117
+ *
118
+ * @remarks
119
+ * In practice this is the **root importer under npm and bun**: neither
120
+ * records a resolved version per importer dependency, and neither emits a
121
+ * package row for the root, so there is nothing to join on. pnpm records a
122
+ * version per importer dependency and is unaffected. The same limitation
123
+ * `PeerCheck` reports, measured by the same code.
124
+ *
125
+ * Reported rather than silently skipped: a gate that sees no duplicates is
126
+ * entitled to know whether that means "clean" or "not looked at".
127
+ */
128
+ unresolvedImporters: Schema.Array(Schema.String)
129
+ }) {
130
+ /**
131
+ * Whether no reported name is duplicated.
132
+ *
133
+ * @remarks
134
+ * Answers for the names asked about, not for the whole graph: with a
135
+ * `names` filter, a duplicated name the filter rejects does not make the
136
+ * report unclean. It says nothing about `unresolvedImporters` — a gate
137
+ * wanting a proven-clean answer checks both.
138
+ */
139
+ get isClean() {
140
+ return this.duplicates.length === 0;
141
+ }
142
+ /**
143
+ * The names every consumer of this kit asks about: `effect` itself and every
144
+ * `@effected/*` package.
145
+ *
146
+ * @remarks
147
+ * Exactly those — not `@effect/*`, and not names merely starting with
148
+ * `effect`. A duplicated kit package presents as a `Layer` requirement that
149
+ * looks provided yet cannot be satisfied, at the entry point, naming neither
150
+ * the package nor the skew (issue #298); this predicate is the check that
151
+ * names both.
152
+ *
153
+ * @param name - a package name
154
+ * @returns whether the name belongs to the kit
155
+ */
156
+ static kit(name) {
157
+ return name === "effect" || name.startsWith("@effected/");
158
+ }
159
+ /**
160
+ * Compute which packages a parsed lockfile resolves at more than one
161
+ * version, and who pulls each copy.
162
+ *
163
+ * @remarks
164
+ * Pure, total and format-free: no IO, no error channel, and no knowledge of
165
+ * which package manager wrote the file.
166
+ *
167
+ * The walk starts at each importer's resolved dependencies and follows
168
+ * `resolved` edges, so only instances some importer actually reaches are
169
+ * counted — a stale row the lockfile still carries but nothing depends on is
170
+ * not a duplicate. A name is a duplicate when the walk reaches it at **two
171
+ * or more distinct versions**; peer-suffix instances of one version are
172
+ * listed under that version but never make a name a duplicate on their own.
173
+ *
174
+ * Each instance's `dependents` are every importer whose roots include it and
175
+ * every reached package whose `resolved` map points at it, deduplicated, in
176
+ * lockfile order. `options.names` narrows what is reported, never what is
177
+ * walked, so a filtered-out package still appears as a dependent where it
178
+ * pulls a reported copy.
179
+ *
180
+ * The one limit it does not paper over is the npm and bun root importer
181
+ * (see `unresolvedImporters`).
182
+ *
183
+ * @param lockfile - a lockfile parsed by `@effected/lockfiles`
184
+ * @param options - see {@link DuplicateCheckOptions}
185
+ * @returns the report; never fails
186
+ */
187
+ static run(lockfile, options) {
188
+ const names = options?.names ?? (() => true);
189
+ const index = indexInstances(lockfile);
190
+ const { byId } = index;
191
+ const importerRoots = /* @__PURE__ */ new Map();
192
+ const unresolved = [];
193
+ const queue = [];
194
+ for (const importer of lockfile.importers) {
195
+ const roots = rootInstances(lockfile, importer.path, index);
196
+ if (roots === void 0) {
197
+ unresolved.push(importer.path);
198
+ continue;
199
+ }
200
+ if (roots._tag === "own") {
201
+ queue.push(roots.instance);
202
+ continue;
203
+ }
204
+ for (const instance of roots.instances) {
205
+ const paths = importerRoots.get(instance.instanceId) ?? [];
206
+ paths.push(importer.path);
207
+ importerRoots.set(instance.instanceId, paths);
208
+ queue.push(instance);
209
+ }
210
+ }
211
+ const reached = /* @__PURE__ */ new Set();
212
+ while (queue.length > 0) {
213
+ const current = queue.shift();
214
+ if (current === void 0) break;
215
+ if (reached.has(current.instanceId)) continue;
216
+ reached.add(current.instanceId);
217
+ for (const targetId of Object.values(current.resolved)) {
218
+ const next = byId.get(targetId);
219
+ if (next !== void 0 && !reached.has(targetId)) queue.push(next);
220
+ }
221
+ }
222
+ const dependents = /* @__PURE__ */ new Map();
223
+ const seen = /* @__PURE__ */ new Set();
224
+ const record = (targetId, dependent) => {
225
+ const key = `${targetId}${renderKey(dependent)}`;
226
+ if (seen.has(key)) return;
227
+ seen.add(key);
228
+ const list = dependents.get(targetId) ?? [];
229
+ list.push(dependent);
230
+ dependents.set(targetId, list);
231
+ };
232
+ for (const importer of lockfile.importers) for (const [instanceId, paths] of importerRoots) if (paths.includes(importer.path)) record(instanceId, {
233
+ _tag: "importer",
234
+ path: importer.path
235
+ });
236
+ for (const pkg of lockfile.packages) {
237
+ if (!reached.has(pkg.instanceId)) continue;
238
+ const from = dependentOf(pkg);
239
+ for (const targetId of Object.values(pkg.resolved)) if (byId.has(targetId)) record(targetId, from);
240
+ }
241
+ const byName = /* @__PURE__ */ new Map();
242
+ for (const pkg of lockfile.packages) {
243
+ if (!reached.has(pkg.instanceId)) continue;
244
+ const versions = byName.get(pkg.name) ?? /* @__PURE__ */ new Map();
245
+ const instances = versions.get(pkg.version) ?? [];
246
+ instances.push(pkg);
247
+ versions.set(pkg.version, instances);
248
+ byName.set(pkg.name, versions);
249
+ }
250
+ const duplicates = [];
251
+ for (const [name, versions] of byName) {
252
+ if (versions.size < 2) continue;
253
+ if (!names(name)) continue;
254
+ duplicates.push(DuplicatedPackage.make({
255
+ name,
256
+ versions: [...versions].map(([version, instances]) => DuplicatedVersion.make({
257
+ version,
258
+ instances: instances.map((instance) => DuplicateInstance.make({
259
+ instanceId: instance.instanceId,
260
+ dependents: dependents.get(instance.instanceId) ?? []
261
+ }))
262
+ }))
263
+ }));
264
+ }
265
+ return DuplicateCheck.make({
266
+ duplicates,
267
+ unresolvedImporters: unresolved
268
+ });
269
+ }
270
+ };
271
+ /**
272
+ * The dependent an edge leaving `from` is attributed to.
273
+ *
274
+ * A workspace row standing for an importer is that importer: its edges are the
275
+ * project's own dependencies, so they are attributed by path. A workspace row
276
+ * the lockfile records without a path (npm can) falls back to the package
277
+ * form, which is at least truthful about the name.
278
+ *
279
+ * @internal
280
+ */
281
+ const dependentOf = (from) => from.isWorkspace && from.relativePath !== void 0 ? {
282
+ _tag: "importer",
283
+ path: from.relativePath
284
+ } : {
285
+ _tag: "package",
286
+ name: from.name,
287
+ version: from.version
288
+ };
289
+ /** @internal */
290
+ const renderKey = (dependent) => dependent._tag === "importer" ? `importer${dependent.path}` : `package${dependent.name}${dependent.version}`;
291
+
292
+ //#endregion
293
+ export { Dependent, DuplicateCheck, DuplicateInstance, DuplicatedPackage, DuplicatedVersion };
package/PeerCheck.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { indexInstances, rootInstances } from "./internal/roots.js";
1
2
  import { Result, Schema } from "effect";
2
3
  import { Range, SemVer } from "@effected/semver";
3
4
 
@@ -196,19 +197,18 @@ var PeerCheck = class PeerCheck extends Schema.Class("PeerCheck")({
196
197
  unresolvedImporters: [],
197
198
  unverified
198
199
  });
199
- const byId = new Map(lockfile.packages.map((pkg) => [pkg.instanceId, pkg]));
200
- const workspaceByPath = /* @__PURE__ */ new Map();
201
- for (const pkg of lockfile.packages) if (pkg.isWorkspace && pkg.relativePath !== void 0) workspaceByPath.set(pkg.relativePath, pkg);
200
+ const index = indexInstances(lockfile);
201
+ const { byId } = index;
202
202
  const rows = [];
203
203
  const unresolved = [];
204
204
  const seen = /* @__PURE__ */ new Set();
205
205
  for (const importer of lockfile.importers) {
206
- const roots = rootInstances(lockfile, importer.path, workspaceByPath, byId);
206
+ const roots = rootInstances(lockfile, importer.path, index);
207
207
  if (roots === void 0) {
208
208
  unresolved.push(importer.path);
209
209
  continue;
210
210
  }
211
- collect(importer.path, roots, byId, rows, seen, allowed);
211
+ collect(importer.path, walksFrom(roots), byId, rows, seen, allowed);
212
212
  }
213
213
  if (lockfile.packages.some((pkg) => pkg.unresolvedEdges.length > 0)) unverified.push("unresolvedEdge");
214
214
  return PeerCheck.make({
@@ -220,40 +220,24 @@ var PeerCheck = class PeerCheck extends Schema.Class("PeerCheck")({
220
220
  }
221
221
  };
222
222
  /**
223
- * The instances an importer's dependencies resolved to, plus the importer's own
224
- * workspace row when the lockfile records one.
223
+ * An importer's roots as walk nodes with their `parents` chain started.
225
224
  *
226
- * Returns `undefined` when the importer cannot be resolved at all, which the
227
- * caller reports rather than treating as "no problems here".
225
+ * The importer's own row is the walk's first node with an empty chain — its
226
+ * peers are the importer's own. A dependency instance joined from the importer
227
+ * entry starts the chain with itself, as `pnpm peers check` reports it.
228
228
  *
229
229
  * @internal
230
230
  */
231
- const rootInstances = (lockfile, importerPath, workspaceByPath, byId) => {
232
- const own = workspaceByPath.get(importerPath);
233
- if (own !== void 0) return [{
234
- instance: own,
235
- path: []
236
- }];
237
- const importer = lockfile.importer(importerPath);
238
- if (importer._tag === "None") return void 0;
239
- const walks = [];
240
- let resolvable = false;
241
- for (const dep of importer.value.dependencies) {
242
- if (dep.version === void 0) continue;
243
- const composed = byId.get(`${dep.name}@${dep.version}${dep.peerSuffix ?? ""}`);
244
- if (composed === void 0) continue;
245
- resolvable = true;
246
- walks.push({
247
- instance: composed,
248
- path: [PeerParent.make({
249
- name: composed.name,
250
- version: composed.version
251
- })]
252
- });
253
- }
254
- if (!resolvable && importer.value.dependencies.length > 0) return void 0;
255
- return walks;
256
- };
231
+ const walksFrom = (roots) => roots._tag === "own" ? [{
232
+ instance: roots.instance,
233
+ path: []
234
+ }] : roots.instances.map((instance) => ({
235
+ instance,
236
+ path: [PeerParent.make({
237
+ name: instance.name,
238
+ version: instance.version
239
+ })]
240
+ }));
257
241
  /**
258
242
  * Walk the resolution graph from an importer's roots, emitting a row for every
259
243
  * unsatisfied peer declared anywhere along the way.
package/index.d.ts CHANGED
@@ -1211,6 +1211,225 @@ export declare class DependencyGraph extends DependencyGraph_base {
1211
1211
  toMermaid(): string;
1212
1212
  }
1213
1213
  //#endregion
1214
+ //#region src/DuplicateCheck.d.ts
1215
+ /**
1216
+ * Options for {@link DuplicateCheck.run}.
1217
+ *
1218
+ * @public
1219
+ */
1220
+ interface DuplicateCheckOptions {
1221
+ /**
1222
+ * Which package names to REPORT on. Defaults to every name.
1223
+ *
1224
+ * @remarks
1225
+ * The predicate narrows the report, never the walk: a package it rejects is
1226
+ * still followed and still appears as a dependent wherever it pulls a copy
1227
+ * of a package it accepts. Pruning the walk instead would drop exactly the
1228
+ * culprit a consumer needs named — the untouched third package holding the
1229
+ * stale copy. {@link DuplicateCheck.kit} is the predicate every consumer of
1230
+ * this kit wants.
1231
+ */
1232
+ readonly names?: (name: string) => boolean;
1233
+ }
1234
+ /**
1235
+ * Who pulls one resolved instance into the graph.
1236
+ *
1237
+ * @remarks
1238
+ * A tagged union a renderer narrows with `switch (dependent._tag)`:
1239
+ *
1240
+ * - `"importer"` — a workspace importer takes the instance directly; `path`
1241
+ * is the importer path (`"."` for the root).
1242
+ * - `"package"` — a resolved package's own dependency edge points at the
1243
+ * instance; `name` and `version` are that package's.
1244
+ *
1245
+ * An importer's dependencies are attributed to the importer by path even when
1246
+ * the lockfile records a workspace row for it — that row IS the importer, and
1247
+ * reporting it as a `"package"` would name a directory with a placeholder
1248
+ * version rather than the project that took the dependency.
1249
+ *
1250
+ * @public
1251
+ */
1252
+ export declare const Dependent: Schema.Union<readonly [Schema.TaggedStruct<"importer", {
1253
+ readonly path: Schema.NonEmptyString;
1254
+ }>, Schema.TaggedStruct<"package", {
1255
+ readonly name: Schema.NonEmptyString;
1256
+ readonly version: Schema.String;
1257
+ }>]>;
1258
+ /**
1259
+ * The decoded form of {@link (Dependent:variable)}: who pulls one resolved
1260
+ * instance, as a tagged union a renderer narrows with `switch (dependent._tag)`.
1261
+ *
1262
+ * @public
1263
+ */
1264
+ export type Dependent = typeof Dependent.Type;
1265
+ declare const DuplicateInstance_base: Schema.Class<DuplicateInstance, Schema.Struct<{
1266
+ /** The opaque instance id, as `@effected/lockfiles` records it. */
1267
+ readonly instanceId: Schema.NonEmptyString;
1268
+ /** Every importer and package that pulls THIS copy. */
1269
+ readonly dependents: Schema.$Array<Schema.Union<readonly [Schema.TaggedStruct<"importer", {
1270
+ readonly path: Schema.NonEmptyString;
1271
+ }>, Schema.TaggedStruct<"package", {
1272
+ readonly name: Schema.NonEmptyString;
1273
+ readonly version: Schema.String;
1274
+ }>]>>;
1275
+ }>, {}>;
1276
+ /**
1277
+ * One resolved instance of a duplicated version, with everything that pulls it.
1278
+ *
1279
+ * @remarks
1280
+ * `dependents` is deduplicated and in lockfile order: importers first, in the
1281
+ * order the lockfile lists them, then packages in the order the lockfile lists
1282
+ * them. Two instances of one `name@version` that differ only by peer suffix
1283
+ * each get their own row, so the dependents of each variant stay attributed to
1284
+ * the variant they actually took.
1285
+ *
1286
+ * @public
1287
+ */
1288
+ export declare class DuplicateInstance extends DuplicateInstance_base {}
1289
+ declare const DuplicatedVersion_base: Schema.Class<DuplicatedVersion, Schema.Struct<{
1290
+ /** The resolved version. */
1291
+ readonly version: Schema.String;
1292
+ /** Every instance at this version — more than one when peer suffixes differ. */
1293
+ readonly instances: Schema.$Array<typeof DuplicateInstance>;
1294
+ }>, {}>;
1295
+ /**
1296
+ * One version a duplicated package resolved at.
1297
+ *
1298
+ * @public
1299
+ */
1300
+ export declare class DuplicatedVersion extends DuplicatedVersion_base {}
1301
+ declare const DuplicatedPackage_base: Schema.Class<DuplicatedPackage, Schema.Struct<{
1302
+ /** The package name. */
1303
+ readonly name: Schema.NonEmptyString;
1304
+ /** Every version it resolved at, in lockfile order. Never fewer than two. */
1305
+ readonly versions: Schema.$Array<typeof DuplicatedVersion>;
1306
+ }>, {}>;
1307
+ /**
1308
+ * One package name reached at two or more distinct versions.
1309
+ *
1310
+ * @remarks
1311
+ * `versions` always has at least two entries: a name resolved at one version
1312
+ * is never a duplicate, however many peer-suffix instances that version has.
1313
+ * Two instances of the same version load identical code and cannot produce the
1314
+ * type-identity skew this check exists to catch, so counting them would report
1315
+ * a problem nobody has.
1316
+ *
1317
+ * @public
1318
+ */
1319
+ export declare class DuplicatedPackage extends DuplicatedPackage_base {}
1320
+ declare const DuplicateCheck_base: Schema.Class<DuplicateCheck, Schema.Struct<{
1321
+ /**
1322
+ * Every reported name resolved at two or more distinct versions, in
1323
+ * lockfile order.
1324
+ */
1325
+ readonly duplicates: Schema.$Array<typeof DuplicatedPackage>;
1326
+ /**
1327
+ * Importers whose dependencies could not be resolved to instances, so
1328
+ * nothing reachable only through them was counted.
1329
+ *
1330
+ * @remarks
1331
+ * In practice this is the **root importer under npm and bun**: neither
1332
+ * records a resolved version per importer dependency, and neither emits a
1333
+ * package row for the root, so there is nothing to join on. pnpm records a
1334
+ * version per importer dependency and is unaffected. The same limitation
1335
+ * `PeerCheck` reports, measured by the same code.
1336
+ *
1337
+ * Reported rather than silently skipped: a gate that sees no duplicates is
1338
+ * entitled to know whether that means "clean" or "not looked at".
1339
+ */
1340
+ readonly unresolvedImporters: Schema.$Array<Schema.String>;
1341
+ }>, {}>;
1342
+ /**
1343
+ * The result of checking a lockfile for packages resolved at more than one
1344
+ * version.
1345
+ *
1346
+ * @remarks
1347
+ * A report rather than a bare array, for the reason `PeerCheck` is one: an
1348
+ * empty array is indistinguishable from "this importer could not be looked
1349
+ * at". `unresolvedImporters` makes that difference legible.
1350
+ *
1351
+ * Pure and total — construct it with {@link DuplicateCheck.run}, which never
1352
+ * fails.
1353
+ *
1354
+ * @example
1355
+ * ```ts
1356
+ * import { DuplicateCheck } from "@effected/workspaces";
1357
+ * import { Lockfile } from "@effected/lockfiles";
1358
+ * import { Effect } from "effect";
1359
+ *
1360
+ * const program = Effect.gen(function* () {
1361
+ * const lockfile = yield* Lockfile.parse(text, { format: "pnpm" });
1362
+ * const report = DuplicateCheck.run(lockfile, { names: DuplicateCheck.kit });
1363
+ * for (const pkg of report.duplicates) {
1364
+ * for (const version of pkg.versions) {
1365
+ * for (const instance of version.instances) {
1366
+ * console.log(pkg.name, version.version, instance.dependents);
1367
+ * }
1368
+ * }
1369
+ * }
1370
+ * return report.isClean;
1371
+ * });
1372
+ * ```
1373
+ *
1374
+ * @public
1375
+ */
1376
+ export declare class DuplicateCheck extends DuplicateCheck_base {
1377
+ /**
1378
+ * Whether no reported name is duplicated.
1379
+ *
1380
+ * @remarks
1381
+ * Answers for the names asked about, not for the whole graph: with a
1382
+ * `names` filter, a duplicated name the filter rejects does not make the
1383
+ * report unclean. It says nothing about `unresolvedImporters` — a gate
1384
+ * wanting a proven-clean answer checks both.
1385
+ */
1386
+ get isClean(): boolean;
1387
+ /**
1388
+ * The names every consumer of this kit asks about: `effect` itself and every
1389
+ * `@effected/*` package.
1390
+ *
1391
+ * @remarks
1392
+ * Exactly those — not `@effect/*`, and not names merely starting with
1393
+ * `effect`. A duplicated kit package presents as a `Layer` requirement that
1394
+ * looks provided yet cannot be satisfied, at the entry point, naming neither
1395
+ * the package nor the skew (issue #298); this predicate is the check that
1396
+ * names both.
1397
+ *
1398
+ * @param name - a package name
1399
+ * @returns whether the name belongs to the kit
1400
+ */
1401
+ static kit(name: string): boolean;
1402
+ /**
1403
+ * Compute which packages a parsed lockfile resolves at more than one
1404
+ * version, and who pulls each copy.
1405
+ *
1406
+ * @remarks
1407
+ * Pure, total and format-free: no IO, no error channel, and no knowledge of
1408
+ * which package manager wrote the file.
1409
+ *
1410
+ * The walk starts at each importer's resolved dependencies and follows
1411
+ * `resolved` edges, so only instances some importer actually reaches are
1412
+ * counted — a stale row the lockfile still carries but nothing depends on is
1413
+ * not a duplicate. A name is a duplicate when the walk reaches it at **two
1414
+ * or more distinct versions**; peer-suffix instances of one version are
1415
+ * listed under that version but never make a name a duplicate on their own.
1416
+ *
1417
+ * Each instance's `dependents` are every importer whose roots include it and
1418
+ * every reached package whose `resolved` map points at it, deduplicated, in
1419
+ * lockfile order. `options.names` narrows what is reported, never what is
1420
+ * walked, so a filtered-out package still appears as a dependent where it
1421
+ * pulls a reported copy.
1422
+ *
1423
+ * The one limit it does not paper over is the npm and bun root importer
1424
+ * (see `unresolvedImporters`).
1425
+ *
1426
+ * @param lockfile - a lockfile parsed by `@effected/lockfiles`
1427
+ * @param options - see {@link DuplicateCheckOptions}
1428
+ * @returns the report; never fails
1429
+ */
1430
+ static run(lockfile: Lockfile, options?: DuplicateCheckOptions): DuplicateCheck;
1431
+ }
1432
+ //#endregion
1214
1433
  //#region src/PackageManagerName.d.ts
1215
1434
  /**
1216
1435
  * The four package managers this package understands.
@@ -3873,5 +4092,5 @@ interface GetWorkspacePackagesSyncOptions extends WorkspacesSyncOptions {
3873
4092
  */
3874
4093
  export declare const getWorkspacePackagesSync: (root: string, options: GetWorkspacePackagesSyncOptions) => ReadonlyArray<WorkspacePackage>;
3875
4094
  //#endregion
3876
- export type { CatalogAssemblyFailure, ChangeDetectionFailure, ChangeDetectorShape, ClassifyOptions, ConfigDependencyHooksShape, DependencyDiff, FindWorkspaceRootOptions, GetWorkspacePackagesSyncOptions, HookInjection, ImporterVersions, LockfileReadFailure, LockfileReaderOptions, LockfileReaderShape, PackageManagerDetectionFailure, PackageManagerDetectorShape, PackageRelease, PeerCheckOptions, PeerDependencyRules, PublishabilityDetectorShape, SyncDirectoryEntry, SyncFileSystem, SyncPath, TagClassification, TagFormatOptions, TrackingTagOptions, UnverifiedReason, VersioningDetectOptions, WorkspaceCatalogsOptions, WorkspaceCatalogsShape, WorkspaceDiscoveryFailure, WorkspaceDiscoveryOptions, WorkspaceDiscoveryShape, WorkspaceDiscoverySkip, WorkspaceDiscoverySkipKind, WorkspaceLookupFailure, WorkspaceRootShape, WorkspaceSnapshotAtFailure, WorkspaceSnapshotWorktreeFailure, WorkspaceSnapshotsOptions, WorkspaceSnapshotsShape, WorkspacesGitOptions, WorkspacesOptions, WorkspacesServices, WorkspacesSyncOptions };
4095
+ export type { CatalogAssemblyFailure, ChangeDetectionFailure, ChangeDetectorShape, ClassifyOptions, ConfigDependencyHooksShape, DependencyDiff, DuplicateCheckOptions, FindWorkspaceRootOptions, GetWorkspacePackagesSyncOptions, HookInjection, ImporterVersions, LockfileReadFailure, LockfileReaderOptions, LockfileReaderShape, PackageManagerDetectionFailure, PackageManagerDetectorShape, PackageRelease, PeerCheckOptions, PeerDependencyRules, PublishabilityDetectorShape, SyncDirectoryEntry, SyncFileSystem, SyncPath, TagClassification, TagFormatOptions, TrackingTagOptions, UnverifiedReason, VersioningDetectOptions, WorkspaceCatalogsOptions, WorkspaceCatalogsShape, WorkspaceDiscoveryFailure, WorkspaceDiscoveryOptions, WorkspaceDiscoveryShape, WorkspaceDiscoverySkip, WorkspaceDiscoverySkipKind, WorkspaceLookupFailure, WorkspaceRootShape, WorkspaceSnapshotAtFailure, WorkspaceSnapshotWorktreeFailure, WorkspaceSnapshotsOptions, WorkspaceSnapshotsShape, WorkspacesGitOptions, WorkspacesOptions, WorkspacesServices, WorkspacesSyncOptions };
3877
4096
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -4,6 +4,7 @@ import { PackageNotFoundError, WorkspaceDiscovery, WorkspaceDiscoveryError, Work
4
4
  import { CyclicDependencyError, DependencyGraph } from "./DependencyGraph.js";
5
5
  import { ChangeDetectionError, ChangeDetectionOptions, ChangeDetector } from "./ChangeDetector.js";
6
6
  import { ConfigDependencyHooks, NoPeerDependencyRules } from "./ConfigDependencyHooks.js";
7
+ import { Dependent, DuplicateCheck, DuplicateInstance, DuplicatedPackage, DuplicatedVersion } from "./DuplicateCheck.js";
7
8
  import { DetectedPackageManager, PackageManagerDetectionError, PackageManagerDetector, PackageManagerEvidence, PackageManagerName } from "./PackageManagerName.js";
8
9
  import { LockfileReadError, LockfileReader } from "./LockfileReader.js";
9
10
  import { PeerCheck, PeerParent, UnsatisfiedPeer } from "./PeerCheck.js";
@@ -16,4 +17,4 @@ import { WorkspaceSnapshots } from "./WorkspaceSnapshots.js";
16
17
  import { Workspaces } from "./Workspaces.js";
17
18
  import { findWorkspaceRootSync, getWorkspacePackagesSync } from "./WorkspacesSync.js";
18
19
 
19
- export { CatalogSet, ChangeDetectionError, ChangeDetectionOptions, ChangeDetector, ConfigDependencyHooks, CyclicDependencyError, DependencyGraph, DetectedPackageManager, LockfileReadError, LockfileReader, NoPeerDependencyRules, PackageManagerDetectionError, PackageManagerDetector, PackageManagerEvidence, PackageManagerName, PackageNotFoundError, PackageStateSnapshot, PeerCheck, PeerParent, PublishConfig, PublishTarget, PublishabilityDetector, ReleaseTag, TagStyle, TrackingTag, UnsatisfiedPeer, VersioningStrategy, VersioningStrategyType, WORKSPACE_MARKERS, WorkspaceCatalogs, WorkspaceDiscovery, WorkspaceDiscoveryError, WorkspaceInfo, WorkspaceManifestError, WorkspacePackage, WorkspacePatternError, WorkspaceRoot, WorkspaceRootNotFoundError, WorkspaceSnapshots, WorkspaceStateSnapshot, Workspaces, classifyTag, findWorkspaceRootSync, getWorkspacePackagesSync };
20
+ export { CatalogSet, ChangeDetectionError, ChangeDetectionOptions, ChangeDetector, ConfigDependencyHooks, CyclicDependencyError, DependencyGraph, Dependent, DetectedPackageManager, DuplicateCheck, DuplicateInstance, DuplicatedPackage, DuplicatedVersion, LockfileReadError, LockfileReader, NoPeerDependencyRules, PackageManagerDetectionError, PackageManagerDetector, PackageManagerEvidence, PackageManagerName, PackageNotFoundError, PackageStateSnapshot, PeerCheck, PeerParent, PublishConfig, PublishTarget, PublishabilityDetector, ReleaseTag, TagStyle, TrackingTag, UnsatisfiedPeer, VersioningStrategy, VersioningStrategyType, WORKSPACE_MARKERS, WorkspaceCatalogs, WorkspaceDiscovery, WorkspaceDiscoveryError, WorkspaceInfo, WorkspaceManifestError, WorkspacePackage, WorkspacePatternError, WorkspaceRoot, WorkspaceRootNotFoundError, WorkspaceSnapshots, WorkspaceStateSnapshot, Workspaces, classifyTag, findWorkspaceRootSync, getWorkspacePackagesSync };
@@ -0,0 +1,43 @@
1
+ //#region src/internal/roots.ts
2
+ const indexInstances = (lockfile) => {
3
+ const byId = new Map(lockfile.packages.map((pkg) => [pkg.instanceId, pkg]));
4
+ const workspaceByPath = /* @__PURE__ */ new Map();
5
+ for (const pkg of lockfile.packages) if (pkg.isWorkspace && pkg.relativePath !== void 0) workspaceByPath.set(pkg.relativePath, pkg);
6
+ return {
7
+ byId,
8
+ workspaceByPath
9
+ };
10
+ };
11
+ /**
12
+ * The instances an importer's dependencies resolved to, or the importer's own
13
+ * workspace row when the lockfile records one.
14
+ *
15
+ * Returns `undefined` when the importer cannot be resolved at all, which every
16
+ * caller reports rather than treating as "no problems here".
17
+ */
18
+ const rootInstances = (lockfile, importerPath, index) => {
19
+ const own = index.workspaceByPath.get(importerPath);
20
+ if (own !== void 0) return {
21
+ _tag: "own",
22
+ instance: own
23
+ };
24
+ const importer = lockfile.importer(importerPath);
25
+ if (importer._tag === "None") return void 0;
26
+ const instances = [];
27
+ let resolvable = false;
28
+ for (const dep of importer.value.dependencies) {
29
+ if (dep.version === void 0) continue;
30
+ const composed = index.byId.get(`${dep.name}@${dep.version}${dep.peerSuffix ?? ""}`);
31
+ if (composed === void 0) continue;
32
+ resolvable = true;
33
+ instances.push(composed);
34
+ }
35
+ if (!resolvable && importer.value.dependencies.length > 0) return void 0;
36
+ return {
37
+ _tag: "dependencies",
38
+ instances
39
+ };
40
+ };
41
+
42
+ //#endregion
43
+ export { indexInstances, rootInstances };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/workspaces",
3
- "version": "0.21.1",
3
+ "version": "0.22.1",
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": [
@@ -46,15 +46,16 @@
46
46
  "./package.json": "./package.json"
47
47
  },
48
48
  "dependencies": {
49
- "@effected/commands": "^0.7.0",
50
- "@effected/git": "^0.15.1",
51
- "@effected/glob": "^0.6.0",
52
- "@effected/lockfiles": "^0.9.0",
53
- "@effected/npm": "^0.14.0",
54
- "@effected/package-json": "^0.15.0",
55
- "@effected/semver": "^0.7.0",
56
- "@effected/walker": "^0.9.0",
57
- "@effected/yaml": "^0.15.1",
49
+ "@effected/commands": "^0.7.2",
50
+ "@effected/git": "^0.15.2",
51
+ "@effected/glob": "^0.6.1",
52
+ "@effected/jsonc": "^0.11.1",
53
+ "@effected/lockfiles": "^0.9.1",
54
+ "@effected/npm": "^0.14.1",
55
+ "@effected/package-json": "^0.15.1",
56
+ "@effected/semver": "^0.7.1",
57
+ "@effected/walker": "^0.9.1",
58
+ "@effected/yaml": "^0.15.2",
58
59
  "@pnpm/catalogs.config": "^1100.0.5",
59
60
  "@pnpm/catalogs.protocol-parser": "^1100.0.0",
60
61
  "@pnpm/catalogs.resolver": "^1100.0.0",