@effected/workspaces 0.6.1 → 0.7.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/WorkspaceCatalogs.js +17 -3
- package/WorkspaceSnapshots.js +0 -0
- package/WorkspaceStateSnapshot.js +61 -2
- package/index.d.ts +78 -1
- package/internal/importerVersions.js +88 -0
- package/package.json +10 -10
package/WorkspaceCatalogs.js
CHANGED
|
@@ -2,6 +2,7 @@ import { WorkspaceRoot } from "./WorkspaceRoot.js";
|
|
|
2
2
|
import { inlineCatalogs, merge, normalize, rangeOf } from "./internal/catalogs.js";
|
|
3
3
|
import { ConfigDependencyHooks } from "./ConfigDependencyHooks.js";
|
|
4
4
|
import { LockfileReader } from "./LockfileReader.js";
|
|
5
|
+
import { importerVersionsOf } from "./internal/importerVersions.js";
|
|
5
6
|
import { Context, Duration, Effect, Exit, FileSystem, Layer, Option, Path, PlatformError, Schema } from "effect";
|
|
6
7
|
import { CatalogAssemblyError, CatalogResolver, DependencyResolutionError, PartialReleaseAgeGate, ReleaseAgeGate } from "@effected/npm";
|
|
7
8
|
import { Yaml } from "@effected/yaml";
|
|
@@ -325,7 +326,15 @@ var WorkspaceCatalogs = class WorkspaceCatalogs extends Context.Service()("@effe
|
|
|
325
326
|
}));
|
|
326
327
|
const assemble = Effect.gen(function* () {
|
|
327
328
|
const root = yield* Effect.suspend(() => roots.find(options?.cwd ?? process.cwd()));
|
|
328
|
-
const
|
|
329
|
+
const lockfileOutputs = yield* lockfiles.read().pipe(Effect.map((lockfile) => ({
|
|
330
|
+
catalogs: CatalogSet.fromLockfile(lockfile),
|
|
331
|
+
importerVersions: importerVersionsOf(lockfile)
|
|
332
|
+
})), Effect.catch((_failure) => Effect.succeed({
|
|
333
|
+
catalogs: CatalogSet.empty(),
|
|
334
|
+
importerVersions: {}
|
|
335
|
+
})));
|
|
336
|
+
const fromLockfile = lockfileOutputs.catalogs;
|
|
337
|
+
const importerVersions = lockfileOutputs.importerVersions;
|
|
329
338
|
const workspaceYaml = path.join(root, "pnpm-workspace.yaml");
|
|
330
339
|
const hasPnpmWorkspace = yield* probeExists(workspaceYaml);
|
|
331
340
|
let inline;
|
|
@@ -353,7 +362,8 @@ var WorkspaceCatalogs = class WorkspaceCatalogs extends Context.Service()("@effe
|
|
|
353
362
|
const manifestPath = path.join(root, "package.json");
|
|
354
363
|
if (!(yield* probeExists(manifestPath))) return {
|
|
355
364
|
catalogs: fromLockfile,
|
|
356
|
-
releaseAgeGate: ReleaseAgeGate.combine()
|
|
365
|
+
releaseAgeGate: ReleaseAgeGate.combine(),
|
|
366
|
+
importerVersions
|
|
357
367
|
};
|
|
358
368
|
const text = yield* fs.readFileString(manifestPath).pipe(Effect.mapError((cause) => new CatalogAssemblyError({
|
|
359
369
|
source: "manifest",
|
|
@@ -372,7 +382,8 @@ var WorkspaceCatalogs = class WorkspaceCatalogs extends Context.Service()("@effe
|
|
|
372
382
|
}));
|
|
373
383
|
return {
|
|
374
384
|
catalogs: assembled,
|
|
375
|
-
releaseAgeGate
|
|
385
|
+
releaseAgeGate,
|
|
386
|
+
importerVersions
|
|
376
387
|
};
|
|
377
388
|
});
|
|
378
389
|
const [resolveOnce, invalidate] = yield* Effect.cachedInvalidateWithTTL(assemble, Duration.infinity);
|
|
@@ -386,6 +397,9 @@ var WorkspaceCatalogs = class WorkspaceCatalogs extends Context.Service()("@effe
|
|
|
386
397
|
}),
|
|
387
398
|
releaseAgeGate: Effect.fn("WorkspaceCatalogs.releaseAgeGate")(function* () {
|
|
388
399
|
return (yield* memo).releaseAgeGate;
|
|
400
|
+
}),
|
|
401
|
+
importerVersions: Effect.fn("WorkspaceCatalogs.importerVersions")(function* () {
|
|
402
|
+
return (yield* memo).importerVersions;
|
|
389
403
|
})
|
|
390
404
|
};
|
|
391
405
|
});
|
package/WorkspaceSnapshots.js
CHANGED
|
Binary file
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { unanimousVersionOf } from "./internal/importerVersions.js";
|
|
1
2
|
import { CatalogSet } from "./WorkspaceCatalogs.js";
|
|
2
3
|
import { Effect, Exit, Layer, Option, Schema } from "effect";
|
|
3
4
|
import { CatalogResolver, DependencySpecifier, WorkspaceResolver } from "@effected/npm";
|
|
@@ -86,7 +87,19 @@ var WorkspaceStateSnapshot = class extends Schema.Class("WorkspaceStateSnapshot"
|
|
|
86
87
|
/** Every workspace package captured at this moment. */
|
|
87
88
|
packages: Schema.Array(PackageStateSnapshot),
|
|
88
89
|
/** The catalog set assembled at this moment. */
|
|
89
|
-
catalogs: CatalogSet
|
|
90
|
+
catalogs: CatalogSet,
|
|
91
|
+
/**
|
|
92
|
+
* Each importer's dependency-name → resolved-version map, as the manager's
|
|
93
|
+
* lockfile recorded it at this moment.
|
|
94
|
+
*
|
|
95
|
+
* @remarks
|
|
96
|
+
* Defaults to `{}`, so a `WorkspaceStateSnapshot` serialized before this field
|
|
97
|
+
* existed still decodes — and an empty index simply makes the `catalog:`
|
|
98
|
+
* fallback in {@link WorkspaceStateSnapshot.resolve} inert, which is exactly
|
|
99
|
+
* the behavior those older values were captured under. Only pnpm records
|
|
100
|
+
* importer versions; bun and npm yield an empty index.
|
|
101
|
+
*/
|
|
102
|
+
importerVersions: Schema.optionalKey(Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.String)))
|
|
90
103
|
}) {
|
|
91
104
|
#versionIndex;
|
|
92
105
|
#packageIndex;
|
|
@@ -121,16 +134,62 @@ var WorkspaceStateSnapshot = class extends Schema.Class("WorkspaceStateSnapshot"
|
|
|
121
134
|
* unparseable string — is `Option.none()`, because there is no indirection to
|
|
122
135
|
* resolve. Total.
|
|
123
136
|
*
|
|
137
|
+
* A `catalog:` specifier the catalog set cannot resolve falls back to this
|
|
138
|
+
* snapshot's `importerVersions` — but only to a version
|
|
139
|
+
* **every** importer recording that dependency agrees on. A catalog injected
|
|
140
|
+
* by a config-dependency pnpmfile hook appears in no committed catalog source,
|
|
141
|
+
* so without this fallback both sides of a before/after diff resolve it to the
|
|
142
|
+
* same raw string and a real version movement produces no row. When importers
|
|
143
|
+
* disagree there is no single correct answer, so this stays `Option.none()`
|
|
144
|
+
* rather than inventing one; {@link WorkspaceStateSnapshot.resolveIn} answers
|
|
145
|
+
* precisely for callers that know which importer is asking.
|
|
146
|
+
*
|
|
124
147
|
* @param dependency - The dependency's package name (what `workspace:` /
|
|
125
148
|
* `catalog:` resolve for).
|
|
126
149
|
* @param specifier - The raw specifier string.
|
|
127
150
|
*/
|
|
128
151
|
resolve(dependency, specifier) {
|
|
152
|
+
return this.#resolveWith(dependency, specifier, () => Option.fromUndefinedOr(unanimousVersionOf(this.importerVersions ?? {}, dependency)));
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The concrete range or version a specifier resolved to AS OF this snapshot,
|
|
156
|
+
* scoped to the importer that declared it.
|
|
157
|
+
*
|
|
158
|
+
* @remarks
|
|
159
|
+
* Identical to {@link WorkspaceStateSnapshot.resolve} except in how an
|
|
160
|
+
* unresolvable `catalog:` specifier falls back: this consults **only**
|
|
161
|
+
* `importerPath`'s own recorded versions, so a monorepo whose packages hold
|
|
162
|
+
* different versions of one dependency still gets an exact answer where
|
|
163
|
+
* `resolve` must abstain. Prefer this whenever the caller knows the importer —
|
|
164
|
+
* a consumer iterating `packages` has `relativePath` in hand, which is the
|
|
165
|
+
* importer key (`"."` for the root package).
|
|
166
|
+
*
|
|
167
|
+
* An unknown `importerPath`, or one recording nothing for `dependency`, is
|
|
168
|
+
* `Option.none()`. Total.
|
|
169
|
+
*
|
|
170
|
+
* @param importerPath - The importer's path relative to the workspace root,
|
|
171
|
+
* `"."` for the root package — `PackageStateSnapshot.relativePath`.
|
|
172
|
+
* @param dependency - The dependency's package name.
|
|
173
|
+
* @param specifier - The raw specifier string.
|
|
174
|
+
*/
|
|
175
|
+
resolveIn(importerPath, dependency, specifier) {
|
|
176
|
+
return this.#resolveWith(dependency, specifier, () => Option.fromUndefinedOr(this.importerVersions?.[importerPath]?.[dependency]));
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* The shared resolution path: classify, answer from the captured state, and
|
|
180
|
+
* consult `onUnresolvedCatalog` only for a `catalog:` specifier the catalog set
|
|
181
|
+
* could not answer. A plain range is already its own answer and must keep
|
|
182
|
+
* resolving to `Option.none()` so the caller falls back to the raw string.
|
|
183
|
+
*/
|
|
184
|
+
#resolveWith(dependency, specifier, onUnresolvedCatalog) {
|
|
129
185
|
const exit = Schema.decodeUnknownExit(DependencySpecifier.FromString)(specifier);
|
|
130
186
|
if (!Exit.isSuccess(exit)) return Option.none();
|
|
131
187
|
const classified = exit.value;
|
|
132
188
|
switch (classified._tag) {
|
|
133
|
-
case "catalog":
|
|
189
|
+
case "catalog": {
|
|
190
|
+
const fromCatalogs = this.catalogs.rangeOf(dependency, classified.name);
|
|
191
|
+
return Option.isSome(fromCatalogs) ? fromCatalogs : onUnresolvedCatalog();
|
|
192
|
+
}
|
|
134
193
|
case "workspace": return Option.fromUndefinedOr(this.#versions().get(dependency));
|
|
135
194
|
default: return Option.none();
|
|
136
195
|
}
|
package/index.d.ts
CHANGED
|
@@ -1323,6 +1323,26 @@ declare class PublishabilityDetector extends PublishabilityDetector_base {
|
|
|
1323
1323
|
}
|
|
1324
1324
|
//#endregion
|
|
1325
1325
|
//#region src/WorkspaceCatalogs.d.ts
|
|
1326
|
+
/**
|
|
1327
|
+
* Each importer's dependency-name → resolved-version map, keyed by importer path
|
|
1328
|
+
* (`"."` for the root package — the same keys `WorkspaceDiscovery.importerMap()`
|
|
1329
|
+
* uses, and the same value `PackageStateSnapshot.relativePath` carries).
|
|
1330
|
+
*
|
|
1331
|
+
* @remarks
|
|
1332
|
+
* What a manager's lockfile records as actually installed, as opposed to what a
|
|
1333
|
+
* manifest declares. It answers a `catalog:` specifier no committed catalog
|
|
1334
|
+
* source declares — the shape a pnpm config-dependency pnpmfile hook produces —
|
|
1335
|
+
* which would otherwise resolve to nothing on both sides of a diff and hide a
|
|
1336
|
+
* real version movement.
|
|
1337
|
+
*
|
|
1338
|
+
* Versions are normalized: pnpm's peer-disambiguation suffix
|
|
1339
|
+
* (`1.2.3(effect@4.0.0)`) is stripped, and `link:` / `file:` entries are omitted
|
|
1340
|
+
* because a filesystem edge is not a version. Only pnpm records importer
|
|
1341
|
+
* versions; bun and npm yield an empty index.
|
|
1342
|
+
*
|
|
1343
|
+
* @public
|
|
1344
|
+
*/
|
|
1345
|
+
type ImporterVersions = Readonly<Record<string, Readonly<Record<string, string>>>>;
|
|
1326
1346
|
declare const CatalogSet_base: Schema.Class<CatalogSet, Schema.Struct<{
|
|
1327
1347
|
/** Catalog name → dependency name → version range. */
|
|
1328
1348
|
readonly entries: Schema.$Record<Schema.String, Schema.$Record<Schema.String, Schema.String>>;
|
|
@@ -1455,6 +1475,19 @@ interface WorkspaceCatalogsShape {
|
|
|
1455
1475
|
* workspace) has no release-age keys, so the gate is the inert zero gate.
|
|
1456
1476
|
*/
|
|
1457
1477
|
readonly releaseAgeGate: () => Effect.Effect<ReleaseAgeGate, CatalogAssemblyFailure>;
|
|
1478
|
+
/**
|
|
1479
|
+
* Each importer's dependency-name → resolved-version map, as the manager's
|
|
1480
|
+
* lockfile records it. Read from the same single lockfile read as `set`, and
|
|
1481
|
+
* memoized with it.
|
|
1482
|
+
*
|
|
1483
|
+
* @remarks
|
|
1484
|
+
* Feeds `WorkspaceStateSnapshot`'s fallback for a `catalog:` specifier no
|
|
1485
|
+
* committed catalog source declares — the shape a config-dependency pnpmfile
|
|
1486
|
+
* hook produces. Only pnpm records importer versions; a bun or npm workspace
|
|
1487
|
+
* yields an empty index, and an absent or unreadable lockfile contributes
|
|
1488
|
+
* nothing rather than failing.
|
|
1489
|
+
*/
|
|
1490
|
+
readonly importerVersions: () => Effect.Effect<ImporterVersions, CatalogAssemblyFailure>;
|
|
1458
1491
|
}
|
|
1459
1492
|
/**
|
|
1460
1493
|
* Options for the {@link WorkspaceCatalogs} layer.
|
|
@@ -1583,6 +1616,18 @@ declare const WorkspaceStateSnapshot_base: Schema.Class<WorkspaceStateSnapshot,
|
|
|
1583
1616
|
readonly packages: Schema.$Array<typeof PackageStateSnapshot>;
|
|
1584
1617
|
/** The catalog set assembled at this moment. */
|
|
1585
1618
|
readonly catalogs: typeof CatalogSet;
|
|
1619
|
+
/**
|
|
1620
|
+
* Each importer's dependency-name → resolved-version map, as the manager's
|
|
1621
|
+
* lockfile recorded it at this moment.
|
|
1622
|
+
*
|
|
1623
|
+
* @remarks
|
|
1624
|
+
* Defaults to `{}`, so a `WorkspaceStateSnapshot` serialized before this field
|
|
1625
|
+
* existed still decodes — and an empty index simply makes the `catalog:`
|
|
1626
|
+
* fallback in {@link WorkspaceStateSnapshot.resolve} inert, which is exactly
|
|
1627
|
+
* the behavior those older values were captured under. Only pnpm records
|
|
1628
|
+
* importer versions; bun and npm yield an empty index.
|
|
1629
|
+
*/
|
|
1630
|
+
readonly importerVersions: Schema.optionalKey<Schema.$Record<Schema.String, Schema.$Record<Schema.String, Schema.String>>>;
|
|
1586
1631
|
}>, {}>;
|
|
1587
1632
|
/**
|
|
1588
1633
|
* The state of a whole workspace at one moment — its packages and its assembled
|
|
@@ -1632,11 +1677,43 @@ declare class WorkspaceStateSnapshot extends WorkspaceStateSnapshot_base {
|
|
|
1632
1677
|
* unparseable string — is `Option.none()`, because there is no indirection to
|
|
1633
1678
|
* resolve. Total.
|
|
1634
1679
|
*
|
|
1680
|
+
* A `catalog:` specifier the catalog set cannot resolve falls back to this
|
|
1681
|
+
* snapshot's `importerVersions` — but only to a version
|
|
1682
|
+
* **every** importer recording that dependency agrees on. A catalog injected
|
|
1683
|
+
* by a config-dependency pnpmfile hook appears in no committed catalog source,
|
|
1684
|
+
* so without this fallback both sides of a before/after diff resolve it to the
|
|
1685
|
+
* same raw string and a real version movement produces no row. When importers
|
|
1686
|
+
* disagree there is no single correct answer, so this stays `Option.none()`
|
|
1687
|
+
* rather than inventing one; {@link WorkspaceStateSnapshot.resolveIn} answers
|
|
1688
|
+
* precisely for callers that know which importer is asking.
|
|
1689
|
+
*
|
|
1635
1690
|
* @param dependency - The dependency's package name (what `workspace:` /
|
|
1636
1691
|
* `catalog:` resolve for).
|
|
1637
1692
|
* @param specifier - The raw specifier string.
|
|
1638
1693
|
*/
|
|
1639
1694
|
resolve(dependency: string, specifier: string): Option.Option<string>;
|
|
1695
|
+
/**
|
|
1696
|
+
* The concrete range or version a specifier resolved to AS OF this snapshot,
|
|
1697
|
+
* scoped to the importer that declared it.
|
|
1698
|
+
*
|
|
1699
|
+
* @remarks
|
|
1700
|
+
* Identical to {@link WorkspaceStateSnapshot.resolve} except in how an
|
|
1701
|
+
* unresolvable `catalog:` specifier falls back: this consults **only**
|
|
1702
|
+
* `importerPath`'s own recorded versions, so a monorepo whose packages hold
|
|
1703
|
+
* different versions of one dependency still gets an exact answer where
|
|
1704
|
+
* `resolve` must abstain. Prefer this whenever the caller knows the importer —
|
|
1705
|
+
* a consumer iterating `packages` has `relativePath` in hand, which is the
|
|
1706
|
+
* importer key (`"."` for the root package).
|
|
1707
|
+
*
|
|
1708
|
+
* An unknown `importerPath`, or one recording nothing for `dependency`, is
|
|
1709
|
+
* `Option.none()`. Total.
|
|
1710
|
+
*
|
|
1711
|
+
* @param importerPath - The importer's path relative to the workspace root,
|
|
1712
|
+
* `"."` for the root package — `PackageStateSnapshot.relativePath`.
|
|
1713
|
+
* @param dependency - The dependency's package name.
|
|
1714
|
+
* @param specifier - The raw specifier string.
|
|
1715
|
+
*/
|
|
1716
|
+
resolveIn(importerPath: string, dependency: string, specifier: string): Option.Option<string>;
|
|
1640
1717
|
/**
|
|
1641
1718
|
* A `CatalogResolver` layer implementing `@effected/npm`'s contract against
|
|
1642
1719
|
* THIS snapshot's catalog set — so code written to the contract resolves
|
|
@@ -1979,5 +2056,5 @@ interface GetWorkspacePackagesSyncOptions extends WorkspacesSyncOptions {
|
|
|
1979
2056
|
*/
|
|
1980
2057
|
declare const getWorkspacePackagesSync: (root: string, options: GetWorkspacePackagesSyncOptions) => ReadonlyArray<WorkspacePackage>;
|
|
1981
2058
|
//#endregion
|
|
1982
|
-
export { type CatalogAssemblyFailure, CatalogSet, ChangeDetectionError, type ChangeDetectionFailure, ChangeDetectionOptions, ChangeDetector, type ChangeDetectorShape, ConfigDependencyHooks, type ConfigDependencyHooksShape, CyclicDependencyError, type DependencyDiff, DependencyGraph, DetectedPackageManager, type FindWorkspaceRootOptions, type GetWorkspacePackagesSyncOptions, type HookInjection, LockfileReadError, type LockfileReadFailure, LockfileReader, type LockfileReaderOptions, type LockfileReaderShape, PackageManagerDetectionError, type PackageManagerDetectionFailure, PackageManagerDetector, PackageManagerName, PackageNotFoundError, PackageStateSnapshot, PublishConfig, PublishTarget, PublishabilityDetector, type PublishabilityDetectorShape, type SyncFileSystem, type SyncPath, WORKSPACE_MARKERS, WorkspaceCatalogs, type WorkspaceCatalogsOptions, type WorkspaceCatalogsShape, WorkspaceDiscovery, WorkspaceDiscoveryError, type WorkspaceDiscoveryFailure, type WorkspaceDiscoveryOptions, type WorkspaceDiscoveryShape, WorkspaceInfo, type WorkspaceLookupFailure, WorkspaceManifestError, WorkspacePackage, WorkspacePatternError, WorkspaceRoot, WorkspaceRootNotFoundError, type WorkspaceRootShape, type WorkspaceSnapshotAtFailure, type WorkspaceSnapshotWorktreeFailure, WorkspaceSnapshots, type WorkspaceSnapshotsOptions, type WorkspaceSnapshotsShape, WorkspaceStateSnapshot, Workspaces, type WorkspacesOptions, type WorkspacesServices, type WorkspacesSyncOptions, findWorkspaceRootSync, getWorkspacePackagesSync };
|
|
2059
|
+
export { type CatalogAssemblyFailure, CatalogSet, ChangeDetectionError, type ChangeDetectionFailure, ChangeDetectionOptions, ChangeDetector, type ChangeDetectorShape, ConfigDependencyHooks, type ConfigDependencyHooksShape, CyclicDependencyError, type DependencyDiff, DependencyGraph, DetectedPackageManager, type FindWorkspaceRootOptions, type GetWorkspacePackagesSyncOptions, type HookInjection, type ImporterVersions, LockfileReadError, type LockfileReadFailure, LockfileReader, type LockfileReaderOptions, type LockfileReaderShape, PackageManagerDetectionError, type PackageManagerDetectionFailure, PackageManagerDetector, PackageManagerName, PackageNotFoundError, PackageStateSnapshot, PublishConfig, PublishTarget, PublishabilityDetector, type PublishabilityDetectorShape, type SyncFileSystem, type SyncPath, WORKSPACE_MARKERS, WorkspaceCatalogs, type WorkspaceCatalogsOptions, type WorkspaceCatalogsShape, WorkspaceDiscovery, WorkspaceDiscoveryError, type WorkspaceDiscoveryFailure, type WorkspaceDiscoveryOptions, type WorkspaceDiscoveryShape, WorkspaceInfo, type WorkspaceLookupFailure, WorkspaceManifestError, WorkspacePackage, WorkspacePatternError, WorkspaceRoot, WorkspaceRootNotFoundError, type WorkspaceRootShape, type WorkspaceSnapshotAtFailure, type WorkspaceSnapshotWorktreeFailure, WorkspaceSnapshots, type WorkspaceSnapshotsOptions, type WorkspaceSnapshotsShape, WorkspaceStateSnapshot, Workspaces, type WorkspacesOptions, type WorkspacesServices, type WorkspacesSyncOptions, findWorkspaceRootSync, getWorkspacePackagesSync };
|
|
1983
2060
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
//#region src/internal/importerVersions.ts
|
|
2
|
+
/**
|
|
3
|
+
* Strip pnpm's peer-disambiguation suffix from a recorded importer version.
|
|
4
|
+
*
|
|
5
|
+
* pnpm records an importer version as the resolved version followed by the peer
|
|
6
|
+
* context it was resolved under — `4.0.0-beta.101(effect@4.0.0-beta.101)(ioredis@5.11.1(supports-color@8.1.1))`.
|
|
7
|
+
* `@effected/lockfiles` stores that string verbatim (its `packages:` key parser
|
|
8
|
+
* strips the suffix; the importer entries deliberately keep it), so the raw
|
|
9
|
+
* value must never reach a consumer's dependency table — it would render the
|
|
10
|
+
* whole parenthesized chain as the "version" a dependency moved to.
|
|
11
|
+
*
|
|
12
|
+
* Everything from the first `(` is dropped; a version with no suffix passes
|
|
13
|
+
* through untouched.
|
|
14
|
+
*/
|
|
15
|
+
const stripPeerSuffix = (version) => {
|
|
16
|
+
const paren = version.indexOf("(");
|
|
17
|
+
return paren === -1 ? version : version.slice(0, paren);
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Whether a recorded importer version is a concrete version usable as a
|
|
21
|
+
* resolution answer.
|
|
22
|
+
*
|
|
23
|
+
* A `link:` / `file:` entry records a filesystem edge rather than a version, and
|
|
24
|
+
* an empty string is pnpm's "specifier only, nothing installed" marker. Both
|
|
25
|
+
* would be worse than no answer at all: the fallback exists to report a concrete
|
|
26
|
+
* version movement, and reporting `link:../foo` as a version is noise a
|
|
27
|
+
* changeset would carry into release notes.
|
|
28
|
+
*/
|
|
29
|
+
const isConcreteVersion = (version) => version.length > 0 && !version.startsWith("link:") && !version.startsWith("file:");
|
|
30
|
+
/**
|
|
31
|
+
* Build the importer-path → dependency-name → version index a snapshot uses to
|
|
32
|
+
* answer `catalog:` specifiers its catalog set cannot resolve.
|
|
33
|
+
*
|
|
34
|
+
* @remarks
|
|
35
|
+
* Keyed by **name only** within each importer, deliberately across every
|
|
36
|
+
* dependency field. The bug this fixes is a peer declared `catalog:effect:peers`
|
|
37
|
+
* whose importer records no `peerDependencies` entry at all — pnpm writes peer
|
|
38
|
+
* declarations into the importer block only when they are also installed, so the
|
|
39
|
+
* concrete version lives on the package's `devDependencies` row for the same
|
|
40
|
+
* name. Joining by name across fields is what finds it; joining by field, or by
|
|
41
|
+
* matching the recorded specifier, would not.
|
|
42
|
+
*
|
|
43
|
+
* Only pnpm populates importer versions; bun and npm record resolved versions on
|
|
44
|
+
* their package entries instead, so those formats yield an empty index and the
|
|
45
|
+
* fallback is inert for them.
|
|
46
|
+
*/
|
|
47
|
+
const importerVersionsOf = (lockfile) => {
|
|
48
|
+
const index = Object.create(null);
|
|
49
|
+
for (const importer of lockfile.importers) {
|
|
50
|
+
const versions = Object.create(null);
|
|
51
|
+
for (const dependency of importer.dependencies) {
|
|
52
|
+
const recorded = dependency.version;
|
|
53
|
+
if (recorded === void 0) continue;
|
|
54
|
+
const version = stripPeerSuffix(recorded);
|
|
55
|
+
if (!isConcreteVersion(version)) continue;
|
|
56
|
+
versions[dependency.name] ??= version;
|
|
57
|
+
}
|
|
58
|
+
index[importer.path] = versions;
|
|
59
|
+
}
|
|
60
|
+
return index;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* The version every importer agrees `dependency` resolved to, or `undefined`
|
|
64
|
+
* when they disagree or none record it.
|
|
65
|
+
*
|
|
66
|
+
* @remarks
|
|
67
|
+
* The unambiguity rule is what makes a workspace-wide answer safe from a
|
|
68
|
+
* snapshot method that receives no importer context. Two packages in one
|
|
69
|
+
* monorepo may legitimately hold different versions of the same dependency; in
|
|
70
|
+
* that case there is no single correct answer, so this yields nothing and the
|
|
71
|
+
* caller keeps today's behavior (no row) rather than inventing a wrong one.
|
|
72
|
+
*/
|
|
73
|
+
const unanimousVersionOf = (index, dependency) => {
|
|
74
|
+
let agreed;
|
|
75
|
+
for (const versions of Object.values(index)) {
|
|
76
|
+
const version = versions[dependency];
|
|
77
|
+
if (version === void 0) continue;
|
|
78
|
+
if (agreed === void 0) {
|
|
79
|
+
agreed = version;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (agreed !== version) return void 0;
|
|
83
|
+
}
|
|
84
|
+
return agreed;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
//#endregion
|
|
88
|
+
export { importerVersionsOf, unanimousVersionOf };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/workspaces",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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": [
|
|
@@ -46,21 +46,21 @@
|
|
|
46
46
|
"./package.json": "./package.json"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@effected/git": "~0.4.
|
|
50
|
-
"@effected/glob": "~0.2.
|
|
51
|
-
"@effected/lockfiles": "~0.1.
|
|
52
|
-
"@effected/npm": "~0.3.
|
|
53
|
-
"@effected/package-json": "~0.5.
|
|
54
|
-
"@effected/semver": "~0.2.
|
|
55
|
-
"@effected/walker": "~0.3.
|
|
56
|
-
"@effected/yaml": "~0.5.
|
|
49
|
+
"@effected/git": "~0.4.2",
|
|
50
|
+
"@effected/glob": "~0.2.1",
|
|
51
|
+
"@effected/lockfiles": "~0.1.10",
|
|
52
|
+
"@effected/npm": "~0.3.1",
|
|
53
|
+
"@effected/package-json": "~0.5.1",
|
|
54
|
+
"@effected/semver": "~0.2.1",
|
|
55
|
+
"@effected/walker": "~0.3.2",
|
|
56
|
+
"@effected/yaml": "~0.5.1",
|
|
57
57
|
"@pnpm/catalogs.config": "^1100.0.0",
|
|
58
58
|
"@pnpm/catalogs.protocol-parser": "^1100.0.0",
|
|
59
59
|
"@pnpm/catalogs.resolver": "^1100.0.0",
|
|
60
60
|
"@pnpm/catalogs.types": "^1100.0.0"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
|
-
"effect": "4.0.0-beta.
|
|
63
|
+
"effect": "4.0.0-beta.101"
|
|
64
64
|
},
|
|
65
65
|
"engines": {
|
|
66
66
|
"node": ">=24.11.0"
|