@effected/workspaces 0.28.0-next-20260927071821 → 0.29.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/ConfigDependencyHooks.js +13 -1
- package/LockfileReader.js +7 -2
- package/PeerCheck.js +449 -36
- package/README.md +20 -5
- package/WorkspaceCatalogs.js +2 -1
- package/WorkspaceDiscovery.js +4 -3
- package/WorkspaceSnapshots.js +5 -4
- package/WorkspaceStateSnapshot.js +47 -30
- package/Workspaces.js +6 -2
- package/WorkspacesSync.js +11 -1
- package/index.d.ts +972 -721
- package/internal/configDependencyFetch.js +39 -6
- package/internal/configDependencyResolution.js +1 -0
- package/internal/layerRoot.js +14 -0
- package/package.json +4 -4
- package/testing.d.ts +5 -0
package/ConfigDependencyHooks.js
CHANGED
|
@@ -482,7 +482,19 @@ var ConfigDependencyHooks = class ConfigDependencyHooks extends Context.Service(
|
|
|
482
482
|
* with `reason: "integrityUnavailable"` — nothing is fetched in either case
|
|
483
483
|
* — and a fetch that fails with `reason: "fetchFailed"`, keeping the
|
|
484
484
|
* not-installed remediation. The fetch needs `pnpm` 11 or 12 on `PATH` and
|
|
485
|
-
* registry access, and is bounded at two minutes.
|
|
485
|
+
* registry access, and is bounded at two minutes.
|
|
486
|
+
*
|
|
487
|
+
* The scratch workspace fetches through the same registries a
|
|
488
|
+
* `pnpm install` in the workspace would: the workspace root's `.npmrc` (scoped
|
|
489
|
+
* registries, mirrors, auth) is copied into it as-is, with `${VAR}`
|
|
490
|
+
* references left for pnpm to expand, never read or logged, and removed
|
|
491
|
+
* with the scratch; and the root `pnpm-workspace.yaml`'s `registry` and
|
|
492
|
+
* `registries` keys are carried into the scratch's. That is the CURRENT
|
|
493
|
+
* checkout's registry config for both sides of a diff — a base ref's
|
|
494
|
+
* `.npmrc` is not read through git — and relative paths inside the `.npmrc`
|
|
495
|
+
* (a `cafile=./ca.pem`) resolve against the scratch, not the root.
|
|
496
|
+
*
|
|
497
|
+
* An empty `configDependencies`, or one whose dependencies all
|
|
486
498
|
* ship no pnpmfile, returns the seed without spawning anything; a `..` path
|
|
487
499
|
* segment in a dependency name fails typed **before** any spawn; a
|
|
488
500
|
* load failure in the child — a syntax error, a throwing top level, an
|
package/LockfileReader.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { findLayerRoot } from "./internal/layerRoot.js";
|
|
1
2
|
import { WorkspaceRoot } from "./WorkspaceRoot.js";
|
|
2
3
|
import { WorkspaceDiscovery } from "./WorkspaceDiscovery.js";
|
|
3
4
|
import { PackageManagerDetector } from "./PackageManagerName.js";
|
|
@@ -60,7 +61,7 @@ var LockfileReader = class LockfileReader extends Context.Service()("@effected/w
|
|
|
60
61
|
const fs = yield* FileSystem.FileSystem;
|
|
61
62
|
const path = yield* Path.Path;
|
|
62
63
|
const init = Effect.gen(function* () {
|
|
63
|
-
const root = yield*
|
|
64
|
+
const root = yield* findLayerRoot(roots, options);
|
|
64
65
|
const format = (yield* detector.detect(root)).name;
|
|
65
66
|
const lockfilePath = path.join(root, filenameFor(format));
|
|
66
67
|
const content = yield* fs.readFileString(lockfilePath).pipe(Effect.mapError((cause) => new LockfileReadError({
|
|
@@ -68,7 +69,11 @@ var LockfileReader = class LockfileReader extends Context.Service()("@effected/w
|
|
|
68
69
|
format,
|
|
69
70
|
cause
|
|
70
71
|
})));
|
|
71
|
-
const
|
|
72
|
+
const configOnly = format === "pnpm" && !(yield* fs.exists(path.join(root, "package.json")).pipe(Effect.orElseSucceed(() => true)));
|
|
73
|
+
const lockfile = yield* Lockfile.parse(content, {
|
|
74
|
+
format,
|
|
75
|
+
configOnly
|
|
76
|
+
});
|
|
72
77
|
if (format !== "pnpm") return lockfile;
|
|
73
78
|
const importers = lockfile.packages.filter((pkg) => pkg.isWorkspace && pkg.relativePath !== void 0);
|
|
74
79
|
const resolved = yield* Effect.forEach(importers, (pkg) => readName(path.join(root, pkg.relativePath, "package.json")).pipe(Effect.map((name) => [pkg.relativePath, name])), { concurrency: 10 });
|
package/PeerCheck.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { indexInstances, rootInstances } from "./internal/roots.js";
|
|
2
2
|
import { peerNameMatcher } from "./internal/peerPatterns.js";
|
|
3
|
-
import { Result, Schema } from "effect";
|
|
3
|
+
import { Option, Result, Schema } from "effect";
|
|
4
4
|
import { Range, SemVer } from "@effected/semver";
|
|
5
5
|
|
|
6
6
|
//#region src/PeerCheck.ts
|
|
@@ -79,6 +79,316 @@ const PEER_RESOLVING_FORMATS = /* @__PURE__ */ new Set([
|
|
|
79
79
|
/** @internal */
|
|
80
80
|
const supportsPeerResolution = (format) => PEER_RESOLVING_FORMATS.has(format);
|
|
81
81
|
/**
|
|
82
|
+
* The protocol pnpm records a workspace-directory resolution under, passed
|
|
83
|
+
* through verbatim on `ImporterDependency.version` by `@effected/lockfiles`.
|
|
84
|
+
*
|
|
85
|
+
* @internal
|
|
86
|
+
*/
|
|
87
|
+
const LINK_PREFIX = "link:";
|
|
88
|
+
/**
|
|
89
|
+
* The workspace-relative directory a `link:` version names, resolved against
|
|
90
|
+
* the importer that recorded it.
|
|
91
|
+
*
|
|
92
|
+
* @remarks
|
|
93
|
+
* `link:../a` in `packages/b` is `packages/a`. Deliberately pure string
|
|
94
|
+
* arithmetic rather than `node:path`: `link:` targets are spelled POSIX in a
|
|
95
|
+
* lockfile, and a Windows host would otherwise translate them to backslashes
|
|
96
|
+
* and stop matching the importer paths the same file uses.
|
|
97
|
+
*
|
|
98
|
+
* @internal
|
|
99
|
+
*/
|
|
100
|
+
const linkTargetPath = (importerPath, target) => {
|
|
101
|
+
const segments = importerPath === "." ? [] : importerPath.split("/");
|
|
102
|
+
const resolved = [];
|
|
103
|
+
for (const segment of [...segments, ...target.split("/")]) {
|
|
104
|
+
if (segment === "" || segment === ".") continue;
|
|
105
|
+
if (segment === "..") {
|
|
106
|
+
resolved.pop();
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
resolved.push(segment);
|
|
110
|
+
}
|
|
111
|
+
return resolved.length === 0 ? "." : resolved.join("/");
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* An importer's walk roots with its `link:` targets seeded in.
|
|
115
|
+
*
|
|
116
|
+
* @remarks
|
|
117
|
+
* An importer with its own workspace row already reaches its link targets
|
|
118
|
+
* through that row's `resolved` edges. One without — the ROOT under pnpm —
|
|
119
|
+
* does not: the shared join composes `name@link:…`, matches no instance, and
|
|
120
|
+
* drops the dependency, so a covered target there would never be walked.
|
|
121
|
+
* Seeding the target rows here, rather than forking the shared join in
|
|
122
|
+
* `internal/roots.ts`, keeps `DuplicateCheck` on the join it was measured
|
|
123
|
+
* against while letting the peer walk judge the root's linked parents against
|
|
124
|
+
* the root's own provider context, as pnpm does (`linkdeep-root/`).
|
|
125
|
+
*
|
|
126
|
+
* An importer the shared join gave up on is rescued only when EVERY versioned
|
|
127
|
+
* dependency it has is a `link:` edge with a row: that failure was then the
|
|
128
|
+
* missing link seed and nothing else. Any other unjoinable dependency keeps the
|
|
129
|
+
* importer unresolved.
|
|
130
|
+
*
|
|
131
|
+
* @internal
|
|
132
|
+
*/
|
|
133
|
+
const withLinkRoots = (roots, links, dependencies) => {
|
|
134
|
+
const rows = links.flatMap((link) => link.row === void 0 ? [] : [link.row]);
|
|
135
|
+
if (roots?._tag === "own" || rows.length === 0) return roots;
|
|
136
|
+
if (roots === void 0) {
|
|
137
|
+
if (dependencies.filter((dep) => dep.version !== void 0).length !== rows.length) return void 0;
|
|
138
|
+
return {
|
|
139
|
+
_tag: "dependencies",
|
|
140
|
+
instances: rows
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
_tag: "dependencies",
|
|
145
|
+
instances: [...roots.instances, ...rows]
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* The workspace-relative directories a `link:` to `pkg` can name.
|
|
150
|
+
*
|
|
151
|
+
* @remarks
|
|
152
|
+
* The package directory always, and — when `publishConfig.directory` is set
|
|
153
|
+
* and `publishConfig.linkDirectory` is not `false` — the publish directory
|
|
154
|
+
* inside it, because that is where pnpm links a workspace dependency then.
|
|
155
|
+
* Measured against pnpm 12.6.0, not assumed: `directory` alone records
|
|
156
|
+
* `link:../a/dist` (`linkdeep-directory/`, byte-identical with and without
|
|
157
|
+
* `linkDirectory: true`), and `linkDirectory: false` records `link:../a`
|
|
158
|
+
* (`linkdeep-directory-false/`). pnpm defaults `linkDirectory` to true.
|
|
159
|
+
*
|
|
160
|
+
* pnpm reads a publish-directory target's peers from the manifest AT that
|
|
161
|
+
* target — the built one — while the join reads the supplied source manifest
|
|
162
|
+
* and resolves `catalog:` ranges through the caller's catalogs. The two agree
|
|
163
|
+
* exactly when the build emits the peer ranges the source's specifiers
|
|
164
|
+
* resolve to, which is the contract a publish-directory build keeps.
|
|
165
|
+
*
|
|
166
|
+
* @internal
|
|
167
|
+
*/
|
|
168
|
+
const linkTargetsOf = (relativePath, pkg) => {
|
|
169
|
+
const directory = pkg.publishConfig?.directory;
|
|
170
|
+
if (directory === void 0 || pkg.publishConfig?.linkDirectory === false) return [relativePath];
|
|
171
|
+
const published = linkTargetPath(relativePath, directory);
|
|
172
|
+
return published === relativePath ? [relativePath] : [relativePath, published];
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* The dependency instances a joined parent's peers resolve against.
|
|
176
|
+
*
|
|
177
|
+
* @remarks
|
|
178
|
+
* **The importer's own dependency set, not the workspace's.** Measured
|
|
179
|
+
* against pnpm 12.5.1 and 12.6.0 on the `linkdeep*` probe workspace, one
|
|
180
|
+
* variable at a time: `react@18.3.1` as `packages/b`'s OWN dependency leaves
|
|
181
|
+
* the linked `probe-a`'s `react: ^18.0.0` peer satisfied (pnpm 12.5.1 reports
|
|
182
|
+
* the importer clean), the same version installed only by a SIBLING importer
|
|
183
|
+
* does not (`packages/b` reports it missing), and `react@17.0.2` of its own
|
|
184
|
+
* is a `bad` row carrying `foundVersion: "17.0.2"`. A workspace-wide
|
|
185
|
+
* name lookup — the obvious shortcut, since the instances are all in one
|
|
186
|
+
* lockfile — would have reported the sibling case clean, which pnpm does not.
|
|
187
|
+
*
|
|
188
|
+
* Both root shapes answer the same question: an importer with its own
|
|
189
|
+
* workspace row resolves peers against that row's recorded edges, and an
|
|
190
|
+
* importer without one (the root under pnpm, npm and bun) against the
|
|
191
|
+
* instances its dependency records joined to.
|
|
192
|
+
*
|
|
193
|
+
* @internal
|
|
194
|
+
*/
|
|
195
|
+
const providerContext = (roots, byId, links) => {
|
|
196
|
+
const context = /* @__PURE__ */ new Map();
|
|
197
|
+
if (roots._tag === "dependencies") {
|
|
198
|
+
for (const instance of roots.instances) context.set(instance.name, instance);
|
|
199
|
+
for (const link of links) if (link.row !== void 0) context.set(link.name, link.row);
|
|
200
|
+
return context;
|
|
201
|
+
}
|
|
202
|
+
for (const [name, instanceId] of Object.entries(roots.instance.resolved)) {
|
|
203
|
+
const provider = byId.get(instanceId);
|
|
204
|
+
if (provider !== void 0) context.set(name, provider);
|
|
205
|
+
}
|
|
206
|
+
return context;
|
|
207
|
+
};
|
|
208
|
+
/**
|
|
209
|
+
* The supplied manifest for a node the walk reached through a covered `link:`
|
|
210
|
+
* edge, or `undefined` for every other node.
|
|
211
|
+
*
|
|
212
|
+
* @remarks
|
|
213
|
+
* The importer's OWN node is deliberately excluded — {@link collect} only
|
|
214
|
+
* consults this for nodes with a non-empty chain. pnpm reports nothing for a
|
|
215
|
+
* linked package's own unsatisfied peers (it reports them through a consumer),
|
|
216
|
+
* so joining them onto the importer's own chain would emit rows the oracle
|
|
217
|
+
* does not have.
|
|
218
|
+
*
|
|
219
|
+
* @internal
|
|
220
|
+
*/
|
|
221
|
+
const joinedManifest = (join, instance) => {
|
|
222
|
+
if (instance.relativePath === void 0 || !join.joinable.has(instance.instanceId)) return void 0;
|
|
223
|
+
return join.manifests.get(instance.relativePath);
|
|
224
|
+
};
|
|
225
|
+
/**
|
|
226
|
+
* The peers a joined manifest marks optional.
|
|
227
|
+
*
|
|
228
|
+
* @remarks
|
|
229
|
+
* Read from the raw manifest record rather than from a typed model, because
|
|
230
|
+
* `WorkspacePackage` carries `peerDependenciesMeta` as it was written, exactly
|
|
231
|
+
* as the lockfile model carries its own. A malformed block yields no optional
|
|
232
|
+
* peers, which is the stricter answer: an unsatisfied peer is reported rather
|
|
233
|
+
* than silently excused.
|
|
234
|
+
*
|
|
235
|
+
* @internal
|
|
236
|
+
*/
|
|
237
|
+
const optionalPeers = (manifest) => {
|
|
238
|
+
const optional = /* @__PURE__ */ new Set();
|
|
239
|
+
const meta = manifest.manifestRecord.peerDependenciesMeta;
|
|
240
|
+
if (typeof meta !== "object" || meta === null) return optional;
|
|
241
|
+
for (const [name, entry] of Object.entries(meta)) if (typeof entry === "object" && entry !== null && entry.optional === true) optional.add(name);
|
|
242
|
+
return optional;
|
|
243
|
+
};
|
|
244
|
+
/**
|
|
245
|
+
* The chain element a node contributes, named from the supplied manifest when
|
|
246
|
+
* the node is a joined link target.
|
|
247
|
+
*
|
|
248
|
+
* @remarks
|
|
249
|
+
* The lockfile names a workspace row by its DIRECTORY (`packages/a`) at the
|
|
250
|
+
* placeholder version `0.0.0`, because pnpm records neither for an importer;
|
|
251
|
+
* `pnpm peers check` reports the real `probe-a@1.0.0` it read from the
|
|
252
|
+
* manifest, and the supplied manifest is the only place those two facts exist.
|
|
253
|
+
*
|
|
254
|
+
* @internal
|
|
255
|
+
*/
|
|
256
|
+
const parentLabel = (join, instance) => {
|
|
257
|
+
const manifest = joinedManifest(join, instance);
|
|
258
|
+
return PeerParent.make({
|
|
259
|
+
name: manifest?.name ?? instance.name,
|
|
260
|
+
version: manifest?.version ?? instance.version
|
|
261
|
+
});
|
|
262
|
+
};
|
|
263
|
+
/**
|
|
264
|
+
* A range or version that is really a protocol specifier (`catalog:`,
|
|
265
|
+
* `workspace:`, `npm:`, `file:` …). Neither a semver range nor a semver
|
|
266
|
+
* version starts with `<scheme>:`.
|
|
267
|
+
*
|
|
268
|
+
* @internal
|
|
269
|
+
*/
|
|
270
|
+
const PROTOCOL_SPECIFIER = /^[A-Za-z][A-Za-z0-9+.-]*:/;
|
|
271
|
+
/**
|
|
272
|
+
* Whether a declared range or a resolved version is a protocol specifier
|
|
273
|
+
* rather than something semver can compare.
|
|
274
|
+
*
|
|
275
|
+
* @remarks
|
|
276
|
+
* The one test both halves of a comparison use: a range that is one yields
|
|
277
|
+
* `"peerRangeUnresolved"`, a provider version that is one (`file:vendor/x`,
|
|
278
|
+
* which `@effected/lockfiles` passes through for a `file:` provider) yields
|
|
279
|
+
* `"peerVersionUnresolved"`.
|
|
280
|
+
*
|
|
281
|
+
* @internal
|
|
282
|
+
*/
|
|
283
|
+
const isProtocolSpecifier = (value) => PROTOCOL_SPECIFIER.test(value);
|
|
284
|
+
/** @internal */
|
|
285
|
+
const CATALOG_PREFIX = "catalog:";
|
|
286
|
+
/**
|
|
287
|
+
* The range a joined manifest's declared peer specifier stands for, or
|
|
288
|
+
* `undefined` when it is a protocol specifier that cannot be resolved.
|
|
289
|
+
*
|
|
290
|
+
* @remarks
|
|
291
|
+
* A `catalog:` specifier resolves through the caller's catalogs; an omitted
|
|
292
|
+
* set resolves nothing, since nothing was looked up. A resolution that is
|
|
293
|
+
* itself a protocol specifier is no more a range than the input was. A plain
|
|
294
|
+
* range passes through untouched, parseable or not — an unparseable
|
|
295
|
+
* non-protocol range is declined as {@link judge} declines one.
|
|
296
|
+
*
|
|
297
|
+
* @internal
|
|
298
|
+
*/
|
|
299
|
+
const resolvePeerRange = (catalogs, peer, wanted) => {
|
|
300
|
+
let range = wanted;
|
|
301
|
+
if (wanted.startsWith(CATALOG_PREFIX)) {
|
|
302
|
+
const resolved = catalogs === void 0 ? Option.none() : catalogs.resolveSpecifier(peer, wanted);
|
|
303
|
+
if (Option.isNone(resolved)) return void 0;
|
|
304
|
+
range = resolved.value;
|
|
305
|
+
}
|
|
306
|
+
return isProtocolSpecifier(range) ? void 0 : range;
|
|
307
|
+
};
|
|
308
|
+
/** @internal */
|
|
309
|
+
const SATISFIED = { _tag: "satisfied" };
|
|
310
|
+
/**
|
|
311
|
+
* A lockfile row's {@link Verdict}, carrying the declared range as `wanted`.
|
|
312
|
+
*
|
|
313
|
+
* @internal
|
|
314
|
+
*/
|
|
315
|
+
const withWanted = (verdict, wanted) => verdict._tag === "unsatisfied" ? {
|
|
316
|
+
...verdict,
|
|
317
|
+
wanted
|
|
318
|
+
} : verdict;
|
|
319
|
+
/**
|
|
320
|
+
* Compare a resolved version against a wanted range, both already known not
|
|
321
|
+
* to be protocol specifiers.
|
|
322
|
+
*
|
|
323
|
+
* @remarks
|
|
324
|
+
* Either side failing to parse is `"satisfied"` in the sense of "no row":
|
|
325
|
+
* asserting "unsatisfied" on a comparison that was never performed would be a
|
|
326
|
+
* wrong answer rather than a gap (see {@link PeerCheck.run}).
|
|
327
|
+
*
|
|
328
|
+
* @internal
|
|
329
|
+
*/
|
|
330
|
+
const compare = (wanted, found) => {
|
|
331
|
+
const range = Range.parseResult(wanted);
|
|
332
|
+
const version = SemVer.parseResult(found);
|
|
333
|
+
if (Result.isFailure(range) || Result.isFailure(version)) return SATISFIED;
|
|
334
|
+
if (Range.satisfies(version.success, range.success)) return SATISFIED;
|
|
335
|
+
return {
|
|
336
|
+
_tag: "unsatisfied",
|
|
337
|
+
found
|
|
338
|
+
};
|
|
339
|
+
};
|
|
340
|
+
/**
|
|
341
|
+
* Decide whether one peer DECLARED BY A JOINED MANIFEST is unsatisfied.
|
|
342
|
+
*
|
|
343
|
+
* @remarks
|
|
344
|
+
* Same three judgements as {@link judge}, against a different provider set:
|
|
345
|
+
* the linked parent's own `resolved` map is empty by construction (pnpm
|
|
346
|
+
* records no peer declarations or resolutions for workspace projects), so the
|
|
347
|
+
* provider is whatever the importer's own dependency set gives that name —
|
|
348
|
+
* see {@link providerContext}.
|
|
349
|
+
*
|
|
350
|
+
* A manifest range may be a protocol specifier rather than a range —
|
|
351
|
+
* `catalog:build:peers` in a repo that sources peers from a catalog,
|
|
352
|
+
* `workspace:*` in one that pins peers to its own version. A `catalog:`
|
|
353
|
+
* specifier is resolved through the supplied catalogs (see
|
|
354
|
+
* {@link resolvePeerRange}) and reported by its resolved range, as pnpm does.
|
|
355
|
+
* One that stays unresolvable with a non-workspace provider present is
|
|
356
|
+
* `"unjudged"`: the comparison was never performed, so the report is marked
|
|
357
|
+
* `"peerRangeUnresolved"` rather than calling the peer satisfied. With NO
|
|
358
|
+
* provider the row needs no range, so it is still reported, echoing whatever
|
|
359
|
+
* range could be named.
|
|
360
|
+
*
|
|
361
|
+
* A non-workspace provider whose version is a protocol specifier (a `file:`
|
|
362
|
+
* dependency) is `"unjudged"` the same way, as `"peerVersionUnresolved"`: see
|
|
363
|
+
* {@link judge}. pnpm reads such a provider's real version off disk for a
|
|
364
|
+
* joined parent (`filedep-joined/`), which this check cannot, so it declines
|
|
365
|
+
* rather than guessing. An unresolvable range and version raise both reasons.
|
|
366
|
+
*
|
|
367
|
+
* @internal
|
|
368
|
+
*/
|
|
369
|
+
const judgeJoined = (context, catalogs, peer, declared, optional) => {
|
|
370
|
+
const resolvedRange = resolvePeerRange(catalogs, peer, declared);
|
|
371
|
+
const wanted = resolvedRange ?? declared;
|
|
372
|
+
const provider = context.get(peer);
|
|
373
|
+
if (provider === void 0) {
|
|
374
|
+
if (optional) return SATISFIED;
|
|
375
|
+
return {
|
|
376
|
+
_tag: "unsatisfied",
|
|
377
|
+
found: null,
|
|
378
|
+
wanted
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
if (provider.isWorkspace) return SATISFIED;
|
|
382
|
+
const reasons = [];
|
|
383
|
+
if (resolvedRange === void 0) reasons.push("peerRangeUnresolved");
|
|
384
|
+
if (isProtocolSpecifier(provider.version)) reasons.push("peerVersionUnresolved");
|
|
385
|
+
if (resolvedRange === void 0 || reasons.length > 0) return {
|
|
386
|
+
_tag: "unjudged",
|
|
387
|
+
reasons
|
|
388
|
+
};
|
|
389
|
+
return withWanted(compare(resolvedRange, provider.version), wanted);
|
|
390
|
+
};
|
|
391
|
+
/**
|
|
82
392
|
* The result of checking a lockfile for unsatisfied peer dependencies.
|
|
83
393
|
*
|
|
84
394
|
* @remarks
|
|
@@ -138,10 +448,15 @@ var PeerCheck = class PeerCheck extends Schema.Class("PeerCheck")({
|
|
|
138
448
|
*
|
|
139
449
|
* @remarks
|
|
140
450
|
* See {@link UnverifiedReason}. A gate must treat a non-empty `unverified`
|
|
141
|
-
* as **not proven clean** rather than as a pass:
|
|
451
|
+
* as **not proven clean** rather than as a pass: every reason means some
|
|
142
452
|
* finding may be missing or spurious, and failing closed is the requirement.
|
|
143
453
|
*/
|
|
144
|
-
unverified: Schema.Array(Schema.Literals([
|
|
454
|
+
unverified: Schema.Array(Schema.Literals([
|
|
455
|
+
"peerRulesNotApplied",
|
|
456
|
+
"unresolvedEdge",
|
|
457
|
+
"peerRangeUnresolved",
|
|
458
|
+
"peerVersionUnresolved"
|
|
459
|
+
]))
|
|
145
460
|
}) {
|
|
146
461
|
/** The unsatisfied peers a gate should act on — the non-optional ones. */
|
|
147
462
|
get required() {
|
|
@@ -159,8 +474,13 @@ var PeerCheck = class PeerCheck extends Schema.Class("PeerCheck")({
|
|
|
159
474
|
* `resolved` edges, so a peer declared by a *transitive* dependency is
|
|
160
475
|
* attributed to the importer that pulls it in, with the chain in
|
|
161
476
|
* `parents` — matching how `pnpm peers check` attributes them.
|
|
477
|
+
* Attribution stops at a workspace package: a linked package's own
|
|
478
|
+
* dependencies — registry or linked — are judged by that package's own
|
|
479
|
+
* importer, and a linked package's manifest peers are judged only for the
|
|
480
|
+
* importer that links it DIRECTLY, against that importer's dependencies.
|
|
481
|
+
* pnpm never surfaces them on a consumer one link further out.
|
|
162
482
|
*
|
|
163
|
-
*
|
|
483
|
+
* Four judgements are deliberate:
|
|
164
484
|
*
|
|
165
485
|
* - **A required peer with nothing resolved is reported** (`found: null`),
|
|
166
486
|
* whether or not the declared range is parseable — a peer with no
|
|
@@ -173,6 +493,9 @@ var PeerCheck = class PeerCheck extends Schema.Class("PeerCheck")({
|
|
|
173
493
|
* - **An unparseable range or version with something resolved is skipped.**
|
|
174
494
|
* The check cannot judge it, and asserting "unsatisfied" on a comparison
|
|
175
495
|
* that was never performed would be a wrong answer rather than a gap.
|
|
496
|
+
* Two protocol-specifier cases are the exception and fail the report
|
|
497
|
+
* closed instead: a joined manifest's range, and a provider whose version
|
|
498
|
+
* is a protocol specifier (both below).
|
|
176
499
|
* - **A workspace-linked provider counts.** Resolution is followed through
|
|
177
500
|
* whatever the edge names, including a workspace row.
|
|
178
501
|
*
|
|
@@ -180,7 +503,44 @@ var PeerCheck = class PeerCheck extends Schema.Class("PeerCheck")({
|
|
|
180
503
|
* bun root importer (see `unresolvedImporters`), and pnpm recording no peer
|
|
181
504
|
* declarations for workspace projects themselves — under pnpm a workspace
|
|
182
505
|
* package's *own* unsatisfied peers are not in the lockfile at all, and
|
|
183
|
-
* `pnpm peers check` does not report them either.
|
|
506
|
+
* `pnpm peers check` does not report them either. A `link:`-resolved
|
|
507
|
+
* importer dependency is the same blind spot from the other side, and there
|
|
508
|
+
* `pnpm peers check` DOES report the linked parent's peers (it reads the
|
|
509
|
+
* manifest on disk): every `link:` edge whose target the caller did not
|
|
510
|
+
* supply therefore fails the report closed rather than letting the
|
|
511
|
+
* invisible parent pass as checked (effected#800).
|
|
512
|
+
*
|
|
513
|
+
* Supplying {@link PeerCheckOptions.workspacePackages} closes that gap for
|
|
514
|
+
* every `link:` target the set covers — the root importer's included,
|
|
515
|
+
* whose linked targets the walk seeds itself because the root has no
|
|
516
|
+
* workspace row to reach them through: the claimed manifest's declared
|
|
517
|
+
* peers are walked, judged against the importer's own dependency set
|
|
518
|
+
* (never a sibling importer's), and reported with the manifest's name and
|
|
519
|
+
* version in `parents` — the two facts a workspace row cannot carry, since
|
|
520
|
+
* the lockfile names an importer by directory at the placeholder version
|
|
521
|
+
* `0.0.0`.
|
|
522
|
+
*
|
|
523
|
+
* A joined manifest's peer range may be a protocol specifier rather than a
|
|
524
|
+
* range. A `catalog:` specifier is resolved through
|
|
525
|
+
* {@link PeerCheckOptions.catalogs} and judged and reported by its
|
|
526
|
+
* resolved range, as `pnpm peers check` reports `wantedRange`. One that
|
|
527
|
+
* stays unresolvable — the catalogs key omitted, a catalog naming nothing
|
|
528
|
+
* for the peer, or another protocol such as `workspace:*` — is not judged
|
|
529
|
+
* against a provider that resolved for it, and the report carries
|
|
530
|
+
* `"peerRangeUnresolved"` rather than passing the peer as satisfied. With
|
|
531
|
+
* nothing resolved for it, the row is still reported, since "no provider"
|
|
532
|
+
* needs no range.
|
|
533
|
+
*
|
|
534
|
+
* The provider side has the same hole: a peer that resolved to a `file:`
|
|
535
|
+
* dependency, directly or through a `file:` override, carries the specifier
|
|
536
|
+
* (`file:vendor/react`) where a version belongs, and for a `file:` directory
|
|
537
|
+
* the lockfile records no version anywhere. `pnpm peers check` reports such a
|
|
538
|
+
* peer as a `bad` row with the specifier as its found version, even when the
|
|
539
|
+
* directory's manifest satisfies the range. Such a peer is neither reported
|
|
540
|
+
* nor passed: no row is fabricated, and the report carries
|
|
541
|
+
* `"peerVersionUnresolved"`, for a lockfile row's peer and a joined
|
|
542
|
+
* manifest's peer alike. A workspace-row provider is still accepted without a
|
|
543
|
+
* version check, and a plain unparseable version is still skipped.
|
|
184
544
|
*
|
|
185
545
|
* @param lockfile - a lockfile parsed by `@effected/lockfiles`
|
|
186
546
|
* @returns the report; never fails
|
|
@@ -202,18 +562,60 @@ var PeerCheck = class PeerCheck extends Schema.Class("PeerCheck")({
|
|
|
202
562
|
});
|
|
203
563
|
const index = indexInstances(lockfile);
|
|
204
564
|
const { byId } = index;
|
|
565
|
+
const manifests = /* @__PURE__ */ new Map();
|
|
566
|
+
const byLinkTarget = /* @__PURE__ */ new Map();
|
|
567
|
+
for (const pkg of options?.workspacePackages ?? []) {
|
|
568
|
+
const relativePath = linkTargetPath(".", pkg.relativePath);
|
|
569
|
+
if (!manifests.has(relativePath)) manifests.set(relativePath, pkg);
|
|
570
|
+
for (const target of linkTargetsOf(relativePath, pkg)) if (!byLinkTarget.has(target)) byLinkTarget.set(target, pkg);
|
|
571
|
+
}
|
|
572
|
+
const joinable = /* @__PURE__ */ new Set();
|
|
573
|
+
const linksByImporter = /* @__PURE__ */ new Map();
|
|
574
|
+
let unjoinedLink = false;
|
|
575
|
+
for (const importer of lockfile.importers) {
|
|
576
|
+
const edges = [];
|
|
577
|
+
for (const dep of importer.dependencies) {
|
|
578
|
+
const version = dep.version;
|
|
579
|
+
if (version?.startsWith(LINK_PREFIX) !== true) continue;
|
|
580
|
+
const target = linkTargetPath(importer.path, version.slice(5));
|
|
581
|
+
const pkg = byLinkTarget.get(target);
|
|
582
|
+
const row = index.workspaceByPath.get(pkg === void 0 ? target : linkTargetPath(".", pkg.relativePath));
|
|
583
|
+
const covered = pkg !== void 0;
|
|
584
|
+
if (!covered) unjoinedLink = true;
|
|
585
|
+
if (covered && row !== void 0) joinable.add(row.instanceId);
|
|
586
|
+
edges.push({
|
|
587
|
+
name: dep.name,
|
|
588
|
+
row,
|
|
589
|
+
covered
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
if (edges.length > 0) linksByImporter.set(importer.path, edges);
|
|
593
|
+
}
|
|
205
594
|
const rows = [];
|
|
206
595
|
const unresolved = [];
|
|
596
|
+
const unjudged = /* @__PURE__ */ new Set();
|
|
207
597
|
const seen = /* @__PURE__ */ new Set();
|
|
208
598
|
for (const importer of lockfile.importers) {
|
|
209
|
-
const
|
|
599
|
+
const links = linksByImporter.get(importer.path) ?? [];
|
|
600
|
+
const roots = withLinkRoots(rootInstances(lockfile, importer.path, index), links, importer.dependencies);
|
|
210
601
|
if (roots === void 0) {
|
|
211
602
|
unresolved.push(importer.path);
|
|
603
|
+
if (links.length > 0) unjoinedLink = true;
|
|
212
604
|
continue;
|
|
213
605
|
}
|
|
214
|
-
|
|
606
|
+
const join = {
|
|
607
|
+
manifests,
|
|
608
|
+
joinable,
|
|
609
|
+
context: providerContext(roots, byId, links),
|
|
610
|
+
catalogs: options?.catalogs
|
|
611
|
+
};
|
|
612
|
+
const judged = /* @__PURE__ */ new Set();
|
|
613
|
+
collect(importer.path, walksFrom(roots, join), byId, rows, seen, policy, join, judged, unjudged);
|
|
614
|
+
for (const edge of links) if (edge.covered && (edge.row === void 0 || !judged.has(edge.row.instanceId))) unjoinedLink = true;
|
|
215
615
|
}
|
|
216
|
-
if (lockfile.packages.some((pkg) => pkg.unresolvedEdges.length > 0)) unverified.push("unresolvedEdge");
|
|
616
|
+
if (unjoinedLink || lockfile.packages.some((pkg) => pkg.unresolvedEdges.length > 0)) unverified.push("unresolvedEdge");
|
|
617
|
+
if (unjudged.has("peerRangeUnresolved")) unverified.push("peerRangeUnresolved");
|
|
618
|
+
if (unjudged.has("peerVersionUnresolved")) unverified.push("peerVersionUnresolved");
|
|
217
619
|
return PeerCheck.make({
|
|
218
620
|
supported: true,
|
|
219
621
|
unsatisfied: rows,
|
|
@@ -231,23 +633,24 @@ var PeerCheck = class PeerCheck extends Schema.Class("PeerCheck")({
|
|
|
231
633
|
*
|
|
232
634
|
* @internal
|
|
233
635
|
*/
|
|
234
|
-
const walksFrom = (roots) => roots._tag === "own" ? [{
|
|
636
|
+
const walksFrom = (roots, join) => roots._tag === "own" ? [{
|
|
235
637
|
instance: roots.instance,
|
|
236
638
|
path: []
|
|
237
639
|
}] : roots.instances.map((instance) => ({
|
|
238
640
|
instance,
|
|
239
|
-
path: [
|
|
240
|
-
name: instance.name,
|
|
241
|
-
version: instance.version
|
|
242
|
-
})]
|
|
641
|
+
path: [parentLabel(join, instance)]
|
|
243
642
|
}));
|
|
244
643
|
/**
|
|
245
644
|
* Walk the resolution graph from an importer's roots, emitting a row for every
|
|
246
645
|
* unsatisfied peer declared anywhere along the way.
|
|
247
646
|
*
|
|
647
|
+
* Adds to `unjudged` the reason each peer it could not judge was never
|
|
648
|
+
* compared (see {@link judge} and {@link judgeJoined}); the caller surfaces
|
|
649
|
+
* them on `unverified`.
|
|
650
|
+
*
|
|
248
651
|
* @internal
|
|
249
652
|
*/
|
|
250
|
-
const collect = (importerPath, roots, byId, rows, seen, policy) => {
|
|
653
|
+
const collect = (importerPath, roots, byId, rows, seen, policy, join, judged, unjudged) => {
|
|
251
654
|
const visited = /* @__PURE__ */ new Set();
|
|
252
655
|
const queue = [...roots];
|
|
253
656
|
while (queue.length > 0) {
|
|
@@ -255,33 +658,37 @@ const collect = (importerPath, roots, byId, rows, seen, policy) => {
|
|
|
255
658
|
if (current === void 0) break;
|
|
256
659
|
if (visited.has(current.instance.instanceId)) continue;
|
|
257
660
|
visited.add(current.instance.instanceId);
|
|
258
|
-
|
|
661
|
+
const linked = current.path.length === 0 ? void 0 : joinedManifest(join, current.instance);
|
|
662
|
+
const declared = linked?.peerDependencies ?? current.instance.peerDependencies;
|
|
663
|
+
const optionalNames = linked === void 0 ? void 0 : optionalPeers(linked);
|
|
664
|
+
const declaring = linked?.name ?? current.instance.name;
|
|
665
|
+
if (linked !== void 0) judged.add(current.instance.instanceId);
|
|
666
|
+
for (const [peer, wanted] of Object.entries(declared)) {
|
|
259
667
|
if (peer === "") continue;
|
|
260
|
-
const optional = current.instance.peerDependenciesMeta[peer]?.optional === true;
|
|
261
|
-
const verdict = judge(current.instance, peer, wanted, optional, byId);
|
|
262
|
-
if (verdict ===
|
|
263
|
-
if (
|
|
668
|
+
const optional = optionalNames === void 0 ? current.instance.peerDependenciesMeta[peer]?.optional === true : optionalNames.has(peer);
|
|
669
|
+
const verdict = linked === void 0 ? withWanted(judge(current.instance, peer, wanted, optional, byId), wanted) : judgeJoined(join.context, join.catalogs, peer, wanted, optional);
|
|
670
|
+
if (verdict._tag === "unjudged") for (const reason of verdict.reasons) unjudged.add(reason);
|
|
671
|
+
if (verdict._tag !== "unsatisfied") continue;
|
|
672
|
+
if (suppressedByRule(policy, declaring, peer, verdict.found)) continue;
|
|
264
673
|
const key = `${importerPath}\u0000${peer}\u0000${current.instance.instanceId}`;
|
|
265
674
|
if (seen.has(key)) continue;
|
|
266
675
|
seen.add(key);
|
|
267
676
|
rows.push(UnsatisfiedPeer.make({
|
|
268
677
|
importer: importerPath,
|
|
269
678
|
dependency: peer,
|
|
270
|
-
wanted,
|
|
679
|
+
wanted: verdict.wanted,
|
|
271
680
|
found: verdict.found,
|
|
272
681
|
optional,
|
|
273
682
|
parents: current.path
|
|
274
683
|
}));
|
|
275
684
|
}
|
|
685
|
+
if (current.path.length > 0 && current.instance.isWorkspace) continue;
|
|
276
686
|
for (const targetId of Object.values(current.instance.resolved)) {
|
|
277
687
|
const next = byId.get(targetId);
|
|
278
688
|
if (next === void 0 || visited.has(targetId)) continue;
|
|
279
689
|
queue.push({
|
|
280
690
|
instance: next,
|
|
281
|
-
path: [...current.path,
|
|
282
|
-
name: next.name,
|
|
283
|
-
version: next.version
|
|
284
|
-
})]
|
|
691
|
+
path: [...current.path, parentLabel(join, next)]
|
|
285
692
|
});
|
|
286
693
|
}
|
|
287
694
|
}
|
|
@@ -360,9 +767,12 @@ const suppressedByRule = (policy, parentName, peer, found) => {
|
|
|
360
767
|
/**
|
|
361
768
|
* Decide whether one declared peer is unsatisfied.
|
|
362
769
|
*
|
|
363
|
-
* Returns `
|
|
364
|
-
* produce no row. They are deliberately merged
|
|
365
|
-
* TSDoc on {@link PeerCheck.run}
|
|
770
|
+
* Returns `"satisfied"` for "satisfied, or not judgeable and skipped" — the
|
|
771
|
+
* two cases that produce no row and no marker. They are deliberately merged
|
|
772
|
+
* here and separated in the TSDoc on {@link PeerCheck.run}. A provider whose
|
|
773
|
+
* version is a protocol specifier is the one unjudgeable case that is NOT
|
|
774
|
+
* skipped: it is `"unjudged"`, and the report carries
|
|
775
|
+
* `"peerVersionUnresolved"`.
|
|
366
776
|
*
|
|
367
777
|
* @internal
|
|
368
778
|
*/
|
|
@@ -370,16 +780,19 @@ const judge = (instance, peer, wanted, optional, byId) => {
|
|
|
370
780
|
const providerId = instance.resolved[peer];
|
|
371
781
|
const provider = providerId === void 0 ? void 0 : byId.get(providerId);
|
|
372
782
|
if (provider === void 0) {
|
|
373
|
-
if (optional) return
|
|
374
|
-
if (instance.unresolvedEdges.includes(peer)) return
|
|
375
|
-
return {
|
|
783
|
+
if (optional) return SATISFIED;
|
|
784
|
+
if (instance.unresolvedEdges.includes(peer)) return SATISFIED;
|
|
785
|
+
return {
|
|
786
|
+
_tag: "unsatisfied",
|
|
787
|
+
found: null
|
|
788
|
+
};
|
|
376
789
|
}
|
|
377
|
-
if (provider.isWorkspace) return
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
return
|
|
790
|
+
if (provider.isWorkspace) return SATISFIED;
|
|
791
|
+
if (isProtocolSpecifier(provider.version)) return {
|
|
792
|
+
_tag: "unjudged",
|
|
793
|
+
reasons: ["peerVersionUnresolved"]
|
|
794
|
+
};
|
|
795
|
+
return compare(wanted, provider.version);
|
|
383
796
|
};
|
|
384
797
|
|
|
385
798
|
//#endregion
|