@effected/workspaces 0.21.0 → 0.22.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/DuplicateCheck.js +293 -0
- package/PeerCheck.js +19 -35
- package/index.d.ts +220 -1
- package/index.js +2 -1
- package/internal/roots.js +43 -0
- package/package.json +4 -4
|
@@ -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
|
|
200
|
-
const
|
|
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,
|
|
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
|
-
*
|
|
224
|
-
* workspace row when the lockfile records one.
|
|
223
|
+
* An importer's roots as walk nodes with their `parents` chain started.
|
|
225
224
|
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
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
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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.
|
|
3
|
+
"version": "0.22.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": [
|
|
@@ -47,14 +47,14 @@
|
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@effected/commands": "^0.7.0",
|
|
50
|
-
"@effected/git": "^0.15.
|
|
50
|
+
"@effected/git": "^0.15.1",
|
|
51
51
|
"@effected/glob": "^0.6.0",
|
|
52
52
|
"@effected/lockfiles": "^0.9.0",
|
|
53
53
|
"@effected/npm": "^0.14.0",
|
|
54
54
|
"@effected/package-json": "^0.15.0",
|
|
55
55
|
"@effected/semver": "^0.7.0",
|
|
56
|
-
"@effected/walker": "^0.
|
|
57
|
-
"@effected/yaml": "^0.15.
|
|
56
|
+
"@effected/walker": "^0.9.0",
|
|
57
|
+
"@effected/yaml": "^0.15.1",
|
|
58
58
|
"@pnpm/catalogs.config": "^1100.0.5",
|
|
59
59
|
"@pnpm/catalogs.protocol-parser": "^1100.0.0",
|
|
60
60
|
"@pnpm/catalogs.resolver": "^1100.0.0",
|