@effected/workspaces 0.17.2 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/WorkspaceDiscovery.js +66 -3
- package/WorkspaceSnapshots.js +5 -4
- package/WorkspaceStateSnapshot.js +157 -5
- package/Workspaces.js +80 -2
- package/index.d.ts +290 -4
- package/package.json +5 -5
package/WorkspaceDiscovery.js
CHANGED
|
@@ -199,8 +199,17 @@ var WorkspaceDiscovery = class WorkspaceDiscovery extends Context.Service()("@ef
|
|
|
199
199
|
cause
|
|
200
200
|
})));
|
|
201
201
|
});
|
|
202
|
-
|
|
203
|
-
|
|
202
|
+
/**
|
|
203
|
+
* Discovery for ONE already-resolved root — the whole read, with the root
|
|
204
|
+
* as a parameter rather than a closed-over constant.
|
|
205
|
+
*
|
|
206
|
+
* Everything derived here (patterns, member manifests, names, versions) is
|
|
207
|
+
* read beneath `root`, which is what makes the per-call-root methods
|
|
208
|
+
* honest: re-rooting an already-discovered package list onto a different
|
|
209
|
+
* directory rewrites paths but keeps the ORIGINAL root's manifests, so a
|
|
210
|
+
* branch that adds or removes a package stays invisible. This re-reads.
|
|
211
|
+
*/
|
|
212
|
+
const discoverAt = (root) => Effect.gen(function* () {
|
|
204
213
|
const patterns = yield* readPatterns(root).pipe(Effect.mapError((failure) => new WorkspaceDiscoveryError({
|
|
205
214
|
root,
|
|
206
215
|
path: failure.path,
|
|
@@ -233,8 +242,43 @@ var WorkspaceDiscovery = class WorkspaceDiscovery extends Context.Service()("@ef
|
|
|
233
242
|
packages
|
|
234
243
|
};
|
|
235
244
|
}).pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path));
|
|
245
|
+
const discover = Effect.gen(function* () {
|
|
246
|
+
const root = yield* Effect.suspend(() => roots.find(options?.cwd ?? process.cwd()));
|
|
247
|
+
return yield* discoverAt(root);
|
|
248
|
+
});
|
|
236
249
|
const [resolveOnce, invalidate] = yield* Effect.cachedInvalidateWithTTL(discover, Duration.infinity);
|
|
237
250
|
const memo = Effect.onExit(resolveOnce, (exit) => Exit.isSuccess(exit) ? Effect.void : invalidate);
|
|
251
|
+
/**
|
|
252
|
+
* Per-resolved-root memos for the `…In` methods, each success-only on the
|
|
253
|
+
* same discipline as the layer-bound one above.
|
|
254
|
+
*
|
|
255
|
+
* Deliberately SEPARATE from that memo rather than replacing it: the
|
|
256
|
+
* layer-bound path resolves its root once, at first use, and folding it
|
|
257
|
+
* into this map would make every call re-run the root ascent. A caller
|
|
258
|
+
* that asks for the layer's own root through `listPackagesIn` pays one
|
|
259
|
+
* extra discovery — the honest price for not changing what the existing
|
|
260
|
+
* methods do.
|
|
261
|
+
*
|
|
262
|
+
* The map grows one entry per distinct root, which is the point for a
|
|
263
|
+
* long-lived host serving many worktrees, and is why `refresh()` clears
|
|
264
|
+
* it wholesale rather than invalidating one cell.
|
|
265
|
+
*/
|
|
266
|
+
const rootMemos = /* @__PURE__ */ new Map();
|
|
267
|
+
/** The memoized discovery for the workspace root containing `directory`. */
|
|
268
|
+
const memoIn = Effect.fn("WorkspaceDiscovery.memoIn")(function* (directory) {
|
|
269
|
+
const root = yield* roots.find(directory);
|
|
270
|
+
const existing = rootMemos.get(root);
|
|
271
|
+
if (existing !== void 0) return yield* existing.memo;
|
|
272
|
+
const [resolveOnce, invalidateOne] = yield* Effect.cachedInvalidateWithTTL(discoverAt(root), Duration.infinity);
|
|
273
|
+
const built = Effect.onExit(resolveOnce, (exit) => Exit.isSuccess(exit) ? Effect.void : invalidateOne);
|
|
274
|
+
const raced = rootMemos.get(root);
|
|
275
|
+
if (raced !== void 0) return yield* raced.memo;
|
|
276
|
+
rootMemos.set(root, {
|
|
277
|
+
memo: built,
|
|
278
|
+
invalidate: invalidateOne
|
|
279
|
+
});
|
|
280
|
+
return yield* built;
|
|
281
|
+
});
|
|
238
282
|
const packages = memo.pipe(Effect.map((state) => state.packages));
|
|
239
283
|
/**
|
|
240
284
|
* The longest-prefix index, built once per package list.
|
|
@@ -292,6 +336,12 @@ var WorkspaceDiscovery = class WorkspaceDiscovery extends Context.Service()("@ef
|
|
|
292
336
|
const all = yield* packages;
|
|
293
337
|
return ownerOf(filePath, owners(all));
|
|
294
338
|
}),
|
|
339
|
+
infoIn: Effect.fn("WorkspaceDiscovery.infoIn")(function* (directory) {
|
|
340
|
+
return (yield* memoIn(directory)).info;
|
|
341
|
+
}),
|
|
342
|
+
listPackagesIn: Effect.fn("WorkspaceDiscovery.listPackagesIn")(function* (directory) {
|
|
343
|
+
return (yield* memoIn(directory)).packages;
|
|
344
|
+
}),
|
|
295
345
|
resolveFiles: Effect.fn("WorkspaceDiscovery.resolveFiles")(function* (filePaths) {
|
|
296
346
|
const all = yield* packages;
|
|
297
347
|
const index = owners(all);
|
|
@@ -302,7 +352,17 @@ var WorkspaceDiscovery = class WorkspaceDiscovery extends Context.Service()("@ef
|
|
|
302
352
|
}
|
|
303
353
|
return [...seen.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
304
354
|
}),
|
|
305
|
-
|
|
355
|
+
refreshIn: Effect.fn("WorkspaceDiscovery.refreshIn")(function* (directory) {
|
|
356
|
+
const root = yield* roots.find(directory);
|
|
357
|
+
const cell = rootMemos.get(root);
|
|
358
|
+
if (cell === void 0) return;
|
|
359
|
+
yield* cell.invalidate;
|
|
360
|
+
rootMemos.delete(root);
|
|
361
|
+
}),
|
|
362
|
+
refresh: () => Effect.flatMap(Effect.forEach([...rootMemos.values()], (cell) => cell.invalidate, { discard: true }), () => {
|
|
363
|
+
rootMemos.clear();
|
|
364
|
+
return invalidate;
|
|
365
|
+
})
|
|
306
366
|
};
|
|
307
367
|
});
|
|
308
368
|
/**
|
|
@@ -396,7 +456,10 @@ var WorkspaceDiscovery = class WorkspaceDiscovery extends Context.Service()("@ef
|
|
|
396
456
|
}
|
|
397
457
|
return [...seen.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
398
458
|
}),
|
|
459
|
+
infoIn: () => Effect.die(/* @__PURE__ */ new Error("WorkspaceDiscovery.makeTest: infoIn() was called but not stubbed — a double cannot know what another root's workspace looks like; pass an `infoIn` override.")),
|
|
460
|
+
listPackagesIn: () => Effect.die(/* @__PURE__ */ new Error("WorkspaceDiscovery.makeTest: listPackagesIn() was called but not stubbed — deriving it from `listPackages` would model every root as identical, which is the bug this method exists to prevent; pass a `listPackagesIn` override.")),
|
|
399
461
|
refresh: () => Effect.void,
|
|
462
|
+
refreshIn: () => Effect.void,
|
|
400
463
|
...overrides
|
|
401
464
|
};
|
|
402
465
|
};
|
package/WorkspaceSnapshots.js
CHANGED
|
@@ -104,6 +104,7 @@ var WorkspaceSnapshots = class WorkspaceSnapshots extends Context.Service()("@ef
|
|
|
104
104
|
const roots = yield* WorkspaceRoot;
|
|
105
105
|
const discovery = yield* WorkspaceDiscovery;
|
|
106
106
|
const catalogsService = yield* WorkspaceCatalogs;
|
|
107
|
+
const seeded = (snapshot) => options?.seedCatalogs === void 0 ? snapshot : snapshot.withSeededCatalogs(options.seedCatalogs);
|
|
107
108
|
/**
|
|
108
109
|
* What a manager's lockfile records at the ref: its catalog set and its
|
|
109
110
|
* importer versions, from ONE parse. Empty on both counts when the
|
|
@@ -166,11 +167,11 @@ var WorkspaceSnapshots = class WorkspaceSnapshots extends Context.Service()("@ef
|
|
|
166
167
|
const packages = [];
|
|
167
168
|
if (Option.isSome(rootPackage)) packages.push(rootPackage.value);
|
|
168
169
|
for (const member of members) if (Option.isSome(member)) packages.push(member.value);
|
|
169
|
-
return WorkspaceStateSnapshot.make({
|
|
170
|
+
return seeded(WorkspaceStateSnapshot.make({
|
|
170
171
|
packages,
|
|
171
172
|
catalogs,
|
|
172
173
|
importerVersions: recorded.importerVersions
|
|
173
|
-
});
|
|
174
|
+
}));
|
|
174
175
|
});
|
|
175
176
|
const atCaches = /* @__PURE__ */ new Map();
|
|
176
177
|
return {
|
|
@@ -203,11 +204,11 @@ var WorkspaceSnapshots = class WorkspaceSnapshots extends Context.Service()("@ef
|
|
|
203
204
|
peerDependencies: pkg.peerDependencies,
|
|
204
205
|
optionalDependencies: pkg.optionalDependencies
|
|
205
206
|
}));
|
|
206
|
-
return WorkspaceStateSnapshot.make({
|
|
207
|
+
return seeded(WorkspaceStateSnapshot.make({
|
|
207
208
|
packages: snapshotPackages,
|
|
208
209
|
catalogs,
|
|
209
210
|
importerVersions
|
|
210
|
-
});
|
|
211
|
+
}));
|
|
211
212
|
})
|
|
212
213
|
};
|
|
213
214
|
});
|
|
@@ -83,7 +83,7 @@ var PackageStateSnapshot = class extends Schema.Class("PackageStateSnapshot")({
|
|
|
83
83
|
*
|
|
84
84
|
* @public
|
|
85
85
|
*/
|
|
86
|
-
var WorkspaceStateSnapshot = class extends Schema.Class("WorkspaceStateSnapshot")({
|
|
86
|
+
var WorkspaceStateSnapshot = class WorkspaceStateSnapshot extends Schema.Class("WorkspaceStateSnapshot")({
|
|
87
87
|
/** Every workspace package captured at this moment. */
|
|
88
88
|
packages: Schema.Array(PackageStateSnapshot),
|
|
89
89
|
/** The catalog set assembled at this moment. */
|
|
@@ -99,7 +99,30 @@ var WorkspaceStateSnapshot = class extends Schema.Class("WorkspaceStateSnapshot"
|
|
|
99
99
|
* the behavior those older values were captured under. Only pnpm records
|
|
100
100
|
* importer versions; bun and npm yield an empty index.
|
|
101
101
|
*/
|
|
102
|
-
importerVersions: Schema.optionalKey(Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.String)))
|
|
102
|
+
importerVersions: Schema.optionalKey(Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.String))),
|
|
103
|
+
/**
|
|
104
|
+
* Catalogs supplied from OUTSIDE this moment, consulted only when
|
|
105
|
+
* `catalogs` cannot answer.
|
|
106
|
+
*
|
|
107
|
+
* @remarks
|
|
108
|
+
* **This is deliberately not merged into `catalogs`.** That field means "the
|
|
109
|
+
* catalog set assembled at this moment" and a snapshot is a serializable
|
|
110
|
+
* value someone stores and diffs; blending an external set into it would
|
|
111
|
+
* quietly make the field mean something else, and nothing downstream could
|
|
112
|
+
* tell the two apart afterwards. Kept separate, the ref's own declaration
|
|
113
|
+
* always wins and both halves stay readable.
|
|
114
|
+
*
|
|
115
|
+
* The motivating case is a catalog injected by a config-dependency
|
|
116
|
+
* `pnpmfile` hook. It is recorded in no committed catalog source, so
|
|
117
|
+
* `WorkspaceSnapshots.at(ref)` — which never replays hooks, by design — cannot
|
|
118
|
+
* see it, and a `catalog:` specifier against it resolves to nothing on BOTH
|
|
119
|
+
* sides of a diff. Seeding the live hook-injected set, or the other side's
|
|
120
|
+
* set, restores a declared RANGE without executing any historical code.
|
|
121
|
+
*
|
|
122
|
+
* Defaults to absent, which makes the seed inert — exactly the behavior of
|
|
123
|
+
* every snapshot captured before this field existed.
|
|
124
|
+
*/
|
|
125
|
+
seededCatalogs: Schema.optionalKey(CatalogSet)
|
|
103
126
|
}) {
|
|
104
127
|
#versionIndex;
|
|
105
128
|
#packageIndex;
|
|
@@ -134,7 +157,16 @@ var WorkspaceStateSnapshot = class extends Schema.Class("WorkspaceStateSnapshot"
|
|
|
134
157
|
* unparseable string — is `Option.none()`, because there is no indirection to
|
|
135
158
|
* resolve. Total.
|
|
136
159
|
*
|
|
137
|
-
* A `catalog:` specifier
|
|
160
|
+
* A `catalog:` specifier resolves in three steps, and the order is the
|
|
161
|
+
* contract: this moment's own `catalogs` first,
|
|
162
|
+
* then `seededCatalogs` if one was supplied,
|
|
163
|
+
* then the `importerVersions` fallback below. The first two answer with a
|
|
164
|
+
* declared RANGE and the third with a concrete version, so a seeded snapshot
|
|
165
|
+
* reports a range change where an unseeded one could only report a version —
|
|
166
|
+
* which is the difference between a diff row and no row when both refs
|
|
167
|
+
* recorded the same installed version.
|
|
168
|
+
*
|
|
169
|
+
* A `catalog:` specifier neither catalog set can resolve falls back to this
|
|
138
170
|
* snapshot's `importerVersions` — but only to a version
|
|
139
171
|
* **every** importer recording that dependency agrees on. A catalog injected
|
|
140
172
|
* by a config-dependency pnpmfile hook appears in no committed catalog source,
|
|
@@ -187,7 +219,7 @@ var WorkspaceStateSnapshot = class extends Schema.Class("WorkspaceStateSnapshot"
|
|
|
187
219
|
const classified = exit.value;
|
|
188
220
|
switch (classified._tag) {
|
|
189
221
|
case "catalog": {
|
|
190
|
-
const fromCatalogs = this
|
|
222
|
+
const fromCatalogs = this.#catalogRange(dependency, classified.name);
|
|
191
223
|
return Option.isSome(fromCatalogs) ? fromCatalogs : onUnresolvedCatalog();
|
|
192
224
|
}
|
|
193
225
|
case "workspace": return Option.fromUndefinedOr(this.#versions().get(dependency));
|
|
@@ -195,6 +227,126 @@ var WorkspaceStateSnapshot = class extends Schema.Class("WorkspaceStateSnapshot"
|
|
|
195
227
|
}
|
|
196
228
|
}
|
|
197
229
|
/**
|
|
230
|
+
* The catalog half of resolution, in precedence order: this moment's own
|
|
231
|
+
* catalogs first, the external seed second.
|
|
232
|
+
*
|
|
233
|
+
* @remarks
|
|
234
|
+
* The ordering is the whole contract. What the ref itself declared can never
|
|
235
|
+
* be overridden by something handed in from outside, so a seed can only ever
|
|
236
|
+
* ADD an answer where there was none — which is why seeding is safe to do
|
|
237
|
+
* unconditionally and why an over-broad seed cannot corrupt a diff.
|
|
238
|
+
*/
|
|
239
|
+
#catalogRange(dependency, catalog) {
|
|
240
|
+
const own = this.catalogs.rangeOf(dependency, catalog);
|
|
241
|
+
if (Option.isSome(own)) return own;
|
|
242
|
+
return this.seededCatalogs === void 0 ? Option.none() : this.seededCatalogs.rangeOf(dependency, catalog);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* This snapshot with `seed` as its `seededCatalogs`
|
|
246
|
+
* — catalogs consulted only where this moment's own catalogs cannot answer.
|
|
247
|
+
*
|
|
248
|
+
* @remarks
|
|
249
|
+
* Returns a NEW snapshot; the receiver is untouched, and the seed REPLACES
|
|
250
|
+
* any seed already present rather than merging with it (a snapshot is a
|
|
251
|
+
* value, and an accumulating seed would make precedence depend on call
|
|
252
|
+
* order). {@link WorkspaceStateSnapshot.crossSeed} is the deliberate
|
|
253
|
+
* exception: it composes the two seeds explicitly, precisely because a bare
|
|
254
|
+
* replace would discard a layer-level seed. `catalogs`, `packages` and `importerVersions` are carried through
|
|
255
|
+
* unchanged, so what the ref declared is still exactly what it declared.
|
|
256
|
+
*
|
|
257
|
+
* The two seeds worth reaching for: the LIVE hook-injected catalog set (from
|
|
258
|
+
* a `WorkspaceCatalogs` built by one of the config-dependency layers), or the
|
|
259
|
+
* other side of a two-ref diff — see
|
|
260
|
+
* {@link WorkspaceStateSnapshot.crossSeed}.
|
|
261
|
+
*
|
|
262
|
+
* @param seed - The catalogs to consult as a fallback.
|
|
263
|
+
*
|
|
264
|
+
* @example
|
|
265
|
+
* ```ts
|
|
266
|
+
* import { WorkspaceCatalogs, WorkspaceSnapshots } from "@effected/workspaces";
|
|
267
|
+
* import { Effect } from "effect";
|
|
268
|
+
*
|
|
269
|
+
* const program = Effect.gen(function* () {
|
|
270
|
+
* const snapshots = yield* WorkspaceSnapshots;
|
|
271
|
+
* const catalogs = yield* WorkspaceCatalogs;
|
|
272
|
+
* // The live set includes hook-injected catalogs under a config-dependency
|
|
273
|
+
* // layer; the ref's own set never can.
|
|
274
|
+
* const live = yield* catalogs.set();
|
|
275
|
+
* const before = (yield* snapshots.at("origin/main")).withSeededCatalogs(live);
|
|
276
|
+
* return before.resolve("effect", "catalog:");
|
|
277
|
+
* });
|
|
278
|
+
* ```
|
|
279
|
+
*/
|
|
280
|
+
withSeededCatalogs(seed) {
|
|
281
|
+
return WorkspaceStateSnapshot.make({
|
|
282
|
+
packages: this.packages,
|
|
283
|
+
catalogs: this.catalogs,
|
|
284
|
+
...this.importerVersions === void 0 ? {} : { importerVersions: this.importerVersions },
|
|
285
|
+
seededCatalogs: seed
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Both sides of a diff, each seeded with the other's catalogs.
|
|
290
|
+
*
|
|
291
|
+
* @remarks
|
|
292
|
+
* The two-ref symmetry the hook-catalog gap actually needs. A catalog
|
|
293
|
+
* injected by a config-dependency hook is declared in no committed source,
|
|
294
|
+
* so neither ref's snapshot can see it and a `catalog:` specifier against it
|
|
295
|
+
* resolves to nothing on both sides — a real version movement then produces
|
|
296
|
+
* no row. Cross-seeding restores a declared RANGE on whichever side is
|
|
297
|
+
* missing it, at strictly lower precedence than that side's own catalogs, so
|
|
298
|
+
* a genuine change between the refs still reads as a change.
|
|
299
|
+
*
|
|
300
|
+
* **The limitation is inherent and is not a defect to work around.** A range
|
|
301
|
+
* change made purely by bumping the config dependency BETWEEN the two refs is
|
|
302
|
+
* suppressed: neither committed source declares the catalog, so each side
|
|
303
|
+
* falls back to the other's value and the two agree by construction. Seeding
|
|
304
|
+
* the live hook-injected set via
|
|
305
|
+
* {@link WorkspaceStateSnapshot.withSeededCatalogs} has the same blind spot
|
|
306
|
+
* against history — recovering it would mean replaying each ref's pinned
|
|
307
|
+
* config-dependency code, which `at(ref)` will not do. If that case must be
|
|
308
|
+
* detected, diff `configDependencies` in `pnpm-workspace.yaml` directly; it
|
|
309
|
+
* is the only committed evidence that the injection changed.
|
|
310
|
+
*
|
|
311
|
+
* **The seeding relationship is symmetric; the RETURN ORDER is not.** Each
|
|
312
|
+
* snapshot is seeded with the other's catalogs, so neither argument is
|
|
313
|
+
* privileged and swapping them produces the same two values — but they come
|
|
314
|
+
* back mirroring the order they went in, so destructure in the order you
|
|
315
|
+
* passed. The `before`/`after` names describe the intended calling
|
|
316
|
+
* convention for a two-ref diff, not a constraint on what may be passed.
|
|
317
|
+
*
|
|
318
|
+
* **A seed already present on either snapshot is preserved**, beneath the
|
|
319
|
+
* other side's catalogs — which matters because
|
|
320
|
+
* `WorkspaceSnapshotsOptions.seedCatalogs` puts one there on every snapshot
|
|
321
|
+
* the service returns. Composing the two surfaces is therefore safe: the
|
|
322
|
+
* layer-level seed keeps answering what neither ref declared, while the
|
|
323
|
+
* other ref's committed declaration wins where it has one.
|
|
324
|
+
*
|
|
325
|
+
* @param before - One snapshot, conventionally the earlier one.
|
|
326
|
+
* @param after - The other snapshot, conventionally the later one.
|
|
327
|
+
* @returns Both snapshots in the order given, each carrying the other's
|
|
328
|
+
* catalogs as its seed.
|
|
329
|
+
*
|
|
330
|
+
* @example
|
|
331
|
+
* ```ts
|
|
332
|
+
* import { WorkspaceSnapshots, WorkspaceStateSnapshot } from "@effected/workspaces";
|
|
333
|
+
* import { Effect } from "effect";
|
|
334
|
+
*
|
|
335
|
+
* const program = Effect.gen(function* () {
|
|
336
|
+
* const snapshots = yield* WorkspaceSnapshots;
|
|
337
|
+
* const [before, after] = WorkspaceStateSnapshot.crossSeed(
|
|
338
|
+
* yield* snapshots.at("origin/main"),
|
|
339
|
+
* yield* snapshots.worktree(),
|
|
340
|
+
* );
|
|
341
|
+
* return { before: before.resolve("effect", "catalog:"), after: after.resolve("effect", "catalog:") };
|
|
342
|
+
* });
|
|
343
|
+
* ```
|
|
344
|
+
*/
|
|
345
|
+
static crossSeed(before, after) {
|
|
346
|
+
const seedFor = (self, other) => self.seededCatalogs === void 0 ? other.catalogs : CatalogSet.merge(self.seededCatalogs, other.catalogs);
|
|
347
|
+
return [before.withSeededCatalogs(seedFor(before, after)), after.withSeededCatalogs(seedFor(after, before))];
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
198
350
|
* A `CatalogResolver` layer implementing `@effected/npm`'s contract against
|
|
199
351
|
* THIS snapshot's catalog set — so code written to the contract resolves
|
|
200
352
|
* `catalog:` specifiers as of this ref. Built once per instance and cached, so
|
|
@@ -207,7 +359,7 @@ var WorkspaceStateSnapshot = class extends Schema.Class("WorkspaceStateSnapshot"
|
|
|
207
359
|
* `rangeOf` never fails.
|
|
208
360
|
*/
|
|
209
361
|
get catalogResolver() {
|
|
210
|
-
if (this.#catalogResolver === void 0) this.#catalogResolver = Layer.succeed(CatalogResolver, { rangeOf: (packageName, catalog) => Effect.succeed(this
|
|
362
|
+
if (this.#catalogResolver === void 0) this.#catalogResolver = Layer.succeed(CatalogResolver, { rangeOf: (packageName, catalog) => Effect.succeed(this.#catalogRange(packageName, catalog)) });
|
|
211
363
|
return this.#catalogResolver;
|
|
212
364
|
}
|
|
213
365
|
/**
|
package/Workspaces.js
CHANGED
|
@@ -19,14 +19,30 @@ const compose = (options, catalogsFactory) => {
|
|
|
19
19
|
return Layer.mergeAll(roots, detector, discovery, lockfiles, catalogs);
|
|
20
20
|
};
|
|
21
21
|
const layer = (options) => compose(options, WorkspaceCatalogs.layer);
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
/**
|
|
23
|
+
* The git half, over an ALREADY-BUILT core composite.
|
|
24
|
+
*
|
|
25
|
+
* @remarks
|
|
26
|
+
* Taking the core as a parameter rather than building it is what lets the
|
|
27
|
+
* config-dependency composites exist at all: the git services are identical
|
|
28
|
+
* across all three, and only the catalogs layer underneath differs. Copying
|
|
29
|
+
* this graph per variant is exactly what a consumer had to do downstream
|
|
30
|
+
* before `layerWithGitAndConfigDependencies` existed.
|
|
31
|
+
*
|
|
32
|
+
* `core` is threaded in as a value, so the caller's single `layer(options)`
|
|
33
|
+
* reference is shared by `ChangeDetector`, `WorkspaceSnapshots` and the merge —
|
|
34
|
+
* building it twice here would defeat layer memoization inside one composite.
|
|
35
|
+
*/
|
|
36
|
+
const withGit = (core, options) => {
|
|
24
37
|
const git = Git.layer;
|
|
25
38
|
return Layer.mergeAll(core, git, ChangeDetector.layer.pipe(Layer.provide(git), Layer.provide(core)), WorkspaceSnapshots.layer(options).pipe(Layer.provide(git), Layer.provide(core)));
|
|
26
39
|
};
|
|
40
|
+
const layerWithGit = (options) => withGit(layer(options), options);
|
|
27
41
|
const resolvers = Layer.mergeAll(WorkspaceCatalogs.catalogResolver, WorkspaceDiscovery.workspaceResolver);
|
|
28
42
|
const layerWithConfigDependencies = (options) => compose(options, WorkspaceCatalogs.layerWithConfigDependencies);
|
|
29
43
|
const layerWithConfigDependenciesSubprocess = (options) => compose(options, WorkspaceCatalogs.layerWithConfigDependenciesSubprocess);
|
|
44
|
+
const layerWithGitAndConfigDependencies = (options) => withGit(layerWithConfigDependencies(options), options);
|
|
45
|
+
const layerWithGitAndConfigDependenciesSubprocess = (options) => withGit(layerWithConfigDependenciesSubprocess(options), options);
|
|
30
46
|
const resolverLayer = (options) => resolvers.pipe(Layer.provide(layerWithConfigDependencies(options)));
|
|
31
47
|
const resolveManifest = Effect.fn("Workspaces.resolveManifest")(function* (manifest, options) {
|
|
32
48
|
return yield* manifest.resolve().pipe(Effect.provide(resolverLayer(options)));
|
|
@@ -148,6 +164,68 @@ var Workspaces = class {
|
|
|
148
164
|
*/
|
|
149
165
|
static layerWithGit = layerWithGit;
|
|
150
166
|
/**
|
|
167
|
+
* {@link Workspaces.layerWithGit} over
|
|
168
|
+
* {@link Workspaces.layerWithConfigDependencies} — snapshots, change
|
|
169
|
+
* detection and git, with catalog assembly that **replays config dependency
|
|
170
|
+
* `pnpmfile.cjs` hooks**.
|
|
171
|
+
*
|
|
172
|
+
* @remarks
|
|
173
|
+
* The composite that did not exist. `layerWithGit` hard-wires the no-op
|
|
174
|
+
* catalogs layer, so a consumer wanting snapshots + git + hook replay had to
|
|
175
|
+
* rebuild the entire service graph by hand — which is exactly what
|
|
176
|
+
* `@savvy-web/silk-effects` did, and the copy this static deletes.
|
|
177
|
+
*
|
|
178
|
+
* Requirements are unchanged from {@link Workspaces.layerWithGit}: a
|
|
179
|
+
* filesystem, a path service, and core's `ChildProcessSpawner` (behind
|
|
180
|
+
* `Git`). The in-process replay adds no requirement of its own — it is a
|
|
181
|
+
* dynamic `import()`, not a subprocess. **If the consumer is bundled, reach
|
|
182
|
+
* for {@link Workspaces.layerWithGitAndConfigDependenciesSubprocess}
|
|
183
|
+
* instead**; the computed import does not survive a bundler, and the
|
|
184
|
+
* `ChildProcessSpawner` the subprocess variant needs is already required
|
|
185
|
+
* here for git, so on this composite the subprocess form costs nothing extra.
|
|
186
|
+
*
|
|
187
|
+
* Opt in deliberately: this executes config-dependency code in process, and
|
|
188
|
+
* {@link Workspaces.layerWithGit} never does.
|
|
189
|
+
*
|
|
190
|
+
* **Bind the result to a `const`.** A parameterized factory mints a fresh
|
|
191
|
+
* reference per call and layers memoize by reference.
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```ts
|
|
195
|
+
* import { Workspaces } from "@effected/workspaces";
|
|
196
|
+
*
|
|
197
|
+
* const KitLayer = Workspaces.layerWithGitAndConfigDependencies();
|
|
198
|
+
* ```
|
|
199
|
+
*/
|
|
200
|
+
static layerWithGitAndConfigDependencies = layerWithGitAndConfigDependencies;
|
|
201
|
+
/**
|
|
202
|
+
* {@link Workspaces.layerWithGitAndConfigDependencies}, with the hook replay
|
|
203
|
+
* in a `node` **child process** rather than in process.
|
|
204
|
+
*
|
|
205
|
+
* @remarks
|
|
206
|
+
* Same typed semantics; the difference is mechanism, and it decides whether
|
|
207
|
+
* catalog assembly works at all in a **bundled** consumer — see
|
|
208
|
+
* {@link Workspaces.layerWithConfigDependenciesSubprocess} for why the
|
|
209
|
+
* in-process computed `import()` dies under a bundler.
|
|
210
|
+
*
|
|
211
|
+
* **This composite's requirement set is identical to the in-process one.**
|
|
212
|
+
* The subprocess replay needs core's `ChildProcessSpawner`, which this
|
|
213
|
+
* composite already requires for `Git` — so unlike the git-free pair, there
|
|
214
|
+
* is no R-widening to weigh here and a bundled consumer pays nothing to be
|
|
215
|
+
* correct. Prefer this variant whenever the program ships as a bundle.
|
|
216
|
+
*
|
|
217
|
+
* **Bind the result to a `const`.**
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* ```ts
|
|
221
|
+
* import { Workspaces } from "@effected/workspaces";
|
|
222
|
+
*
|
|
223
|
+
* // A bundled CLI or MCP server: the computed import never enters the graph.
|
|
224
|
+
* const KitLayer = Workspaces.layerWithGitAndConfigDependenciesSubprocess();
|
|
225
|
+
* ```
|
|
226
|
+
*/
|
|
227
|
+
static layerWithGitAndConfigDependenciesSubprocess = layerWithGitAndConfigDependenciesSubprocess;
|
|
228
|
+
/**
|
|
151
229
|
* This package's implementation of `@effected/commands`' `LocalExec`
|
|
152
230
|
* contract: how to run a project-local binary here.
|
|
153
231
|
*
|
package/index.d.ts
CHANGED
|
@@ -537,8 +537,65 @@ interface WorkspaceDiscoveryShape {
|
|
|
537
537
|
readonly resolveFile: (filePath: string) => Effect.Effect<Option.Option<WorkspacePackage>, WorkspaceDiscoveryFailure>;
|
|
538
538
|
/** The distinct packages owning any of `filePaths`. */
|
|
539
539
|
readonly resolveFiles: (filePaths: ReadonlyArray<string>) => Effect.Effect<ReadonlyArray<WorkspacePackage>, WorkspaceDiscoveryFailure>;
|
|
540
|
-
/**
|
|
540
|
+
/**
|
|
541
|
+
* Facts about the workspace containing `directory`, discovered against THAT
|
|
542
|
+
* root rather than the layer-bound one.
|
|
543
|
+
*/
|
|
544
|
+
readonly infoIn: (directory: string) => Effect.Effect<WorkspaceInfo, WorkspaceDiscoveryFailure>;
|
|
545
|
+
/**
|
|
546
|
+
* Every package of the workspace containing `directory`, discovered against
|
|
547
|
+
* THAT root rather than the layer-bound one.
|
|
548
|
+
*
|
|
549
|
+
* @remarks
|
|
550
|
+
* For a **long-lived host serving many roots** — an MCP server or a language
|
|
551
|
+
* server that resolves one workspace at startup and then answers calls
|
|
552
|
+
* scoped to a git worktree, a nested repository, or another project
|
|
553
|
+
* entirely. The layer-bound {@link WorkspaceDiscoveryShape.listPackages}
|
|
554
|
+
* answers about the root discovered from `options.cwd`, which such a host
|
|
555
|
+
* has no way to vary per call without building a fresh layer.
|
|
556
|
+
*
|
|
557
|
+
* **This re-reads; it does not re-root.** The tempting cheap fix — take the
|
|
558
|
+
* layer's package list and rewrite each `path` onto the caller's directory —
|
|
559
|
+
* produces correct-looking paths over the ORIGINAL root's manifests, so a
|
|
560
|
+
* worktree whose branch adds, removes or renames a package reports the other
|
|
561
|
+
* branch's membership with no error. Patterns, member manifests, names and
|
|
562
|
+
* versions all come from beneath `directory`'s own root here.
|
|
563
|
+
*
|
|
564
|
+
* `directory` may be the workspace root or anything inside it: the root is
|
|
565
|
+
* resolved by the same upward walk the layer-bound path uses, and results are
|
|
566
|
+
* memoized per RESOLVED root, so many directories in one workspace share one
|
|
567
|
+
* discovery. The memo holds one entry per distinct root for the layer's
|
|
568
|
+
* lifetime; {@link WorkspaceDiscoveryShape.refresh} drops all of them.
|
|
569
|
+
*
|
|
570
|
+
* @param directory - Absolute path to the workspace root, or to any
|
|
571
|
+
* directory inside it.
|
|
572
|
+
*/
|
|
573
|
+
readonly listPackagesIn: (directory: string) => Effect.Effect<ReadonlyArray<WorkspacePackage>, WorkspaceDiscoveryFailure>;
|
|
574
|
+
/**
|
|
575
|
+
* Drop every memoized discovery — the layer-bound one and each per-root memo
|
|
576
|
+
* — so the next call re-reads the filesystem.
|
|
577
|
+
*/
|
|
541
578
|
readonly refresh: () => Effect.Effect<void>;
|
|
579
|
+
/**
|
|
580
|
+
* Drop only the memo for the workspace containing `directory`, leaving the
|
|
581
|
+
* layer-bound memo and every other root's untouched.
|
|
582
|
+
*
|
|
583
|
+
* @remarks
|
|
584
|
+
* The precise counterpart to {@link WorkspaceDiscoveryShape.refresh} for a
|
|
585
|
+
* host serving several roots: refreshing one worktree because it changed
|
|
586
|
+
* should not discard sibling worktrees that did not, which is all `refresh`
|
|
587
|
+
* can do.
|
|
588
|
+
*
|
|
589
|
+
* **Fails typed on a directory in no workspace**, exactly as
|
|
590
|
+
* {@link WorkspaceDiscoveryShape.listPackagesIn} does for the same input —
|
|
591
|
+
* the three per-root methods answer a bad path the same way, and a caller
|
|
592
|
+
* that would rather treat it as a no-op writes `Effect.ignore`. Refreshing a
|
|
593
|
+
* root that HAS no memo is an ordinary no-op and not an error.
|
|
594
|
+
*
|
|
595
|
+
* @param directory - Absolute path to the workspace root, or to any
|
|
596
|
+
* directory inside it.
|
|
597
|
+
*/
|
|
598
|
+
readonly refreshIn: (directory: string) => Effect.Effect<void, WorkspaceRootNotFoundError>;
|
|
542
599
|
}
|
|
543
600
|
declare const WorkspaceDiscovery_base: Context.ServiceClass<WorkspaceDiscovery, "@effected/workspaces/WorkspaceDiscovery", WorkspaceDiscoveryShape>;
|
|
544
601
|
/**
|
|
@@ -2687,6 +2744,29 @@ declare const WorkspaceStateSnapshot_base: Schema.Class<WorkspaceStateSnapshot,
|
|
|
2687
2744
|
* importer versions; bun and npm yield an empty index.
|
|
2688
2745
|
*/
|
|
2689
2746
|
readonly importerVersions: Schema.optionalKey<Schema.$Record<Schema.String, Schema.$Record<Schema.String, Schema.String>>>;
|
|
2747
|
+
/**
|
|
2748
|
+
* Catalogs supplied from OUTSIDE this moment, consulted only when
|
|
2749
|
+
* `catalogs` cannot answer.
|
|
2750
|
+
*
|
|
2751
|
+
* @remarks
|
|
2752
|
+
* **This is deliberately not merged into `catalogs`.** That field means "the
|
|
2753
|
+
* catalog set assembled at this moment" and a snapshot is a serializable
|
|
2754
|
+
* value someone stores and diffs; blending an external set into it would
|
|
2755
|
+
* quietly make the field mean something else, and nothing downstream could
|
|
2756
|
+
* tell the two apart afterwards. Kept separate, the ref's own declaration
|
|
2757
|
+
* always wins and both halves stay readable.
|
|
2758
|
+
*
|
|
2759
|
+
* The motivating case is a catalog injected by a config-dependency
|
|
2760
|
+
* `pnpmfile` hook. It is recorded in no committed catalog source, so
|
|
2761
|
+
* `WorkspaceSnapshots.at(ref)` — which never replays hooks, by design — cannot
|
|
2762
|
+
* see it, and a `catalog:` specifier against it resolves to nothing on BOTH
|
|
2763
|
+
* sides of a diff. Seeding the live hook-injected set, or the other side's
|
|
2764
|
+
* set, restores a declared RANGE without executing any historical code.
|
|
2765
|
+
*
|
|
2766
|
+
* Defaults to absent, which makes the seed inert — exactly the behavior of
|
|
2767
|
+
* every snapshot captured before this field existed.
|
|
2768
|
+
*/
|
|
2769
|
+
readonly seededCatalogs: Schema.optionalKey<typeof CatalogSet>;
|
|
2690
2770
|
}>, {}>;
|
|
2691
2771
|
/**
|
|
2692
2772
|
* The state of a whole workspace at one moment — its packages and its assembled
|
|
@@ -2736,7 +2816,16 @@ declare class WorkspaceStateSnapshot extends WorkspaceStateSnapshot_base {
|
|
|
2736
2816
|
* unparseable string — is `Option.none()`, because there is no indirection to
|
|
2737
2817
|
* resolve. Total.
|
|
2738
2818
|
*
|
|
2739
|
-
* A `catalog:` specifier
|
|
2819
|
+
* A `catalog:` specifier resolves in three steps, and the order is the
|
|
2820
|
+
* contract: this moment's own `catalogs` first,
|
|
2821
|
+
* then `seededCatalogs` if one was supplied,
|
|
2822
|
+
* then the `importerVersions` fallback below. The first two answer with a
|
|
2823
|
+
* declared RANGE and the third with a concrete version, so a seeded snapshot
|
|
2824
|
+
* reports a range change where an unseeded one could only report a version —
|
|
2825
|
+
* which is the difference between a diff row and no row when both refs
|
|
2826
|
+
* recorded the same installed version.
|
|
2827
|
+
*
|
|
2828
|
+
* A `catalog:` specifier neither catalog set can resolve falls back to this
|
|
2740
2829
|
* snapshot's `importerVersions` — but only to a version
|
|
2741
2830
|
* **every** importer recording that dependency agrees on. A catalog injected
|
|
2742
2831
|
* by a config-dependency pnpmfile hook appears in no committed catalog source,
|
|
@@ -2773,6 +2862,101 @@ declare class WorkspaceStateSnapshot extends WorkspaceStateSnapshot_base {
|
|
|
2773
2862
|
* @param specifier - The raw specifier string.
|
|
2774
2863
|
*/
|
|
2775
2864
|
resolveIn(importerPath: string, dependency: string, specifier: string): Option.Option<string>;
|
|
2865
|
+
/**
|
|
2866
|
+
* This snapshot with `seed` as its `seededCatalogs`
|
|
2867
|
+
* — catalogs consulted only where this moment's own catalogs cannot answer.
|
|
2868
|
+
*
|
|
2869
|
+
* @remarks
|
|
2870
|
+
* Returns a NEW snapshot; the receiver is untouched, and the seed REPLACES
|
|
2871
|
+
* any seed already present rather than merging with it (a snapshot is a
|
|
2872
|
+
* value, and an accumulating seed would make precedence depend on call
|
|
2873
|
+
* order). {@link WorkspaceStateSnapshot.crossSeed} is the deliberate
|
|
2874
|
+
* exception: it composes the two seeds explicitly, precisely because a bare
|
|
2875
|
+
* replace would discard a layer-level seed. `catalogs`, `packages` and `importerVersions` are carried through
|
|
2876
|
+
* unchanged, so what the ref declared is still exactly what it declared.
|
|
2877
|
+
*
|
|
2878
|
+
* The two seeds worth reaching for: the LIVE hook-injected catalog set (from
|
|
2879
|
+
* a `WorkspaceCatalogs` built by one of the config-dependency layers), or the
|
|
2880
|
+
* other side of a two-ref diff — see
|
|
2881
|
+
* {@link WorkspaceStateSnapshot.crossSeed}.
|
|
2882
|
+
*
|
|
2883
|
+
* @param seed - The catalogs to consult as a fallback.
|
|
2884
|
+
*
|
|
2885
|
+
* @example
|
|
2886
|
+
* ```ts
|
|
2887
|
+
* import { WorkspaceCatalogs, WorkspaceSnapshots } from "@effected/workspaces";
|
|
2888
|
+
* import { Effect } from "effect";
|
|
2889
|
+
*
|
|
2890
|
+
* const program = Effect.gen(function* () {
|
|
2891
|
+
* const snapshots = yield* WorkspaceSnapshots;
|
|
2892
|
+
* const catalogs = yield* WorkspaceCatalogs;
|
|
2893
|
+
* // The live set includes hook-injected catalogs under a config-dependency
|
|
2894
|
+
* // layer; the ref's own set never can.
|
|
2895
|
+
* const live = yield* catalogs.set();
|
|
2896
|
+
* const before = (yield* snapshots.at("origin/main")).withSeededCatalogs(live);
|
|
2897
|
+
* return before.resolve("effect", "catalog:");
|
|
2898
|
+
* });
|
|
2899
|
+
* ```
|
|
2900
|
+
*/
|
|
2901
|
+
withSeededCatalogs(seed: CatalogSet): WorkspaceStateSnapshot;
|
|
2902
|
+
/**
|
|
2903
|
+
* Both sides of a diff, each seeded with the other's catalogs.
|
|
2904
|
+
*
|
|
2905
|
+
* @remarks
|
|
2906
|
+
* The two-ref symmetry the hook-catalog gap actually needs. A catalog
|
|
2907
|
+
* injected by a config-dependency hook is declared in no committed source,
|
|
2908
|
+
* so neither ref's snapshot can see it and a `catalog:` specifier against it
|
|
2909
|
+
* resolves to nothing on both sides — a real version movement then produces
|
|
2910
|
+
* no row. Cross-seeding restores a declared RANGE on whichever side is
|
|
2911
|
+
* missing it, at strictly lower precedence than that side's own catalogs, so
|
|
2912
|
+
* a genuine change between the refs still reads as a change.
|
|
2913
|
+
*
|
|
2914
|
+
* **The limitation is inherent and is not a defect to work around.** A range
|
|
2915
|
+
* change made purely by bumping the config dependency BETWEEN the two refs is
|
|
2916
|
+
* suppressed: neither committed source declares the catalog, so each side
|
|
2917
|
+
* falls back to the other's value and the two agree by construction. Seeding
|
|
2918
|
+
* the live hook-injected set via
|
|
2919
|
+
* {@link WorkspaceStateSnapshot.withSeededCatalogs} has the same blind spot
|
|
2920
|
+
* against history — recovering it would mean replaying each ref's pinned
|
|
2921
|
+
* config-dependency code, which `at(ref)` will not do. If that case must be
|
|
2922
|
+
* detected, diff `configDependencies` in `pnpm-workspace.yaml` directly; it
|
|
2923
|
+
* is the only committed evidence that the injection changed.
|
|
2924
|
+
*
|
|
2925
|
+
* **The seeding relationship is symmetric; the RETURN ORDER is not.** Each
|
|
2926
|
+
* snapshot is seeded with the other's catalogs, so neither argument is
|
|
2927
|
+
* privileged and swapping them produces the same two values — but they come
|
|
2928
|
+
* back mirroring the order they went in, so destructure in the order you
|
|
2929
|
+
* passed. The `before`/`after` names describe the intended calling
|
|
2930
|
+
* convention for a two-ref diff, not a constraint on what may be passed.
|
|
2931
|
+
*
|
|
2932
|
+
* **A seed already present on either snapshot is preserved**, beneath the
|
|
2933
|
+
* other side's catalogs — which matters because
|
|
2934
|
+
* `WorkspaceSnapshotsOptions.seedCatalogs` puts one there on every snapshot
|
|
2935
|
+
* the service returns. Composing the two surfaces is therefore safe: the
|
|
2936
|
+
* layer-level seed keeps answering what neither ref declared, while the
|
|
2937
|
+
* other ref's committed declaration wins where it has one.
|
|
2938
|
+
*
|
|
2939
|
+
* @param before - One snapshot, conventionally the earlier one.
|
|
2940
|
+
* @param after - The other snapshot, conventionally the later one.
|
|
2941
|
+
* @returns Both snapshots in the order given, each carrying the other's
|
|
2942
|
+
* catalogs as its seed.
|
|
2943
|
+
*
|
|
2944
|
+
* @example
|
|
2945
|
+
* ```ts
|
|
2946
|
+
* import { WorkspaceSnapshots, WorkspaceStateSnapshot } from "@effected/workspaces";
|
|
2947
|
+
* import { Effect } from "effect";
|
|
2948
|
+
*
|
|
2949
|
+
* const program = Effect.gen(function* () {
|
|
2950
|
+
* const snapshots = yield* WorkspaceSnapshots;
|
|
2951
|
+
* const [before, after] = WorkspaceStateSnapshot.crossSeed(
|
|
2952
|
+
* yield* snapshots.at("origin/main"),
|
|
2953
|
+
* yield* snapshots.worktree(),
|
|
2954
|
+
* );
|
|
2955
|
+
* return { before: before.resolve("effect", "catalog:"), after: after.resolve("effect", "catalog:") };
|
|
2956
|
+
* });
|
|
2957
|
+
* ```
|
|
2958
|
+
*/
|
|
2959
|
+
static crossSeed(before: WorkspaceStateSnapshot, after: WorkspaceStateSnapshot): readonly [WorkspaceStateSnapshot, WorkspaceStateSnapshot];
|
|
2776
2960
|
/**
|
|
2777
2961
|
* A `CatalogResolver` layer implementing `@effected/npm`'s contract against
|
|
2778
2962
|
* THIS snapshot's catalog set — so code written to the contract resolves
|
|
@@ -2838,6 +3022,32 @@ interface WorkspaceSnapshotsOptions {
|
|
|
2838
3022
|
* first call is honoured.
|
|
2839
3023
|
*/
|
|
2840
3024
|
readonly cwd?: string;
|
|
3025
|
+
/**
|
|
3026
|
+
* Catalogs every snapshot this service produces carries as its
|
|
3027
|
+
* `seededCatalogs` — consulted only where the snapshot's own catalogs cannot
|
|
3028
|
+
* answer.
|
|
3029
|
+
*
|
|
3030
|
+
* @remarks
|
|
3031
|
+
* The layer-level spelling of `WorkspaceStateSnapshot.withSeededCatalogs`,
|
|
3032
|
+
* for the common case where the seed is the same for every read: a consumer
|
|
3033
|
+
* diffing many refs against one live workspace seeds once here instead of
|
|
3034
|
+
* remembering to call `withSeededCatalogs` at each site — and a forgotten
|
|
3035
|
+
* call is a silently missing diff row, not a type error.
|
|
3036
|
+
*
|
|
3037
|
+
* **This executes nothing.** The caller supplies the set; `at(ref)` still
|
|
3038
|
+
* never replays config-dependency hooks and still never fetches. The
|
|
3039
|
+
* deliberate at/worktree asymmetry is unchanged — this only lets a consumer
|
|
3040
|
+
* who has already paid for the live set share it with the ref side.
|
|
3041
|
+
*
|
|
3042
|
+
* Applied to `worktree()` too, for symmetry. There it is usually inert: a
|
|
3043
|
+
* live set assembled under a config-dependency layer already contains the
|
|
3044
|
+
* hook-injected catalogs at full precedence, so the seed answers nothing the
|
|
3045
|
+
* snapshot could not answer itself.
|
|
3046
|
+
*
|
|
3047
|
+
* @defaultValue absent — no seed, and resolution behaves exactly as before
|
|
3048
|
+
* this option existed.
|
|
3049
|
+
*/
|
|
3050
|
+
readonly seedCatalogs?: CatalogSet;
|
|
2841
3051
|
}
|
|
2842
3052
|
/**
|
|
2843
3053
|
* The {@link WorkspaceSnapshots} service shape.
|
|
@@ -2969,6 +3179,20 @@ interface WorkspacesOptions {
|
|
|
2969
3179
|
/** Descent cap for segment-crossing `packages:` patterns. Defaults to 32. */
|
|
2970
3180
|
readonly maxDepth?: number;
|
|
2971
3181
|
}
|
|
3182
|
+
/**
|
|
3183
|
+
* Options for the composites that additionally provide {@link WorkspaceSnapshots}
|
|
3184
|
+
* — {@link Workspaces.layerWithGit} and the two config-dependency variants.
|
|
3185
|
+
*
|
|
3186
|
+
* @remarks
|
|
3187
|
+
* {@link WorkspacesOptions} plus whatever {@link WorkspaceSnapshotsOptions}
|
|
3188
|
+
* adds, so `seedCatalogs` reaches `at(ref)` through the composite instead of
|
|
3189
|
+
* forcing a consumer to hand-compose the graph just to pass one option. The
|
|
3190
|
+
* two interfaces agree on `cwd`, which stays one explicit concern applied
|
|
3191
|
+
* uniformly.
|
|
3192
|
+
*
|
|
3193
|
+
* @public
|
|
3194
|
+
*/
|
|
3195
|
+
interface WorkspacesGitOptions extends WorkspacesOptions, WorkspaceSnapshotsOptions {}
|
|
2972
3196
|
/**
|
|
2973
3197
|
* Every service the git-free composite layer provides.
|
|
2974
3198
|
*
|
|
@@ -3068,7 +3292,69 @@ declare class Workspaces {
|
|
|
3068
3292
|
* provides `Git.layerTest({ … })` — git's own shipped double, whose
|
|
3069
3293
|
* unstubbed members die named — and needs no repository on disk.
|
|
3070
3294
|
*/
|
|
3071
|
-
static readonly layerWithGit: (options?:
|
|
3295
|
+
static readonly layerWithGit: (options?: WorkspacesGitOptions) => Layer.Layer<WorkspacesServices | ChangeDetector | WorkspaceSnapshots | Git, never, FileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner>;
|
|
3296
|
+
/**
|
|
3297
|
+
* {@link Workspaces.layerWithGit} over
|
|
3298
|
+
* {@link Workspaces.layerWithConfigDependencies} — snapshots, change
|
|
3299
|
+
* detection and git, with catalog assembly that **replays config dependency
|
|
3300
|
+
* `pnpmfile.cjs` hooks**.
|
|
3301
|
+
*
|
|
3302
|
+
* @remarks
|
|
3303
|
+
* The composite that did not exist. `layerWithGit` hard-wires the no-op
|
|
3304
|
+
* catalogs layer, so a consumer wanting snapshots + git + hook replay had to
|
|
3305
|
+
* rebuild the entire service graph by hand — which is exactly what
|
|
3306
|
+
* `@savvy-web/silk-effects` did, and the copy this static deletes.
|
|
3307
|
+
*
|
|
3308
|
+
* Requirements are unchanged from {@link Workspaces.layerWithGit}: a
|
|
3309
|
+
* filesystem, a path service, and core's `ChildProcessSpawner` (behind
|
|
3310
|
+
* `Git`). The in-process replay adds no requirement of its own — it is a
|
|
3311
|
+
* dynamic `import()`, not a subprocess. **If the consumer is bundled, reach
|
|
3312
|
+
* for {@link Workspaces.layerWithGitAndConfigDependenciesSubprocess}
|
|
3313
|
+
* instead**; the computed import does not survive a bundler, and the
|
|
3314
|
+
* `ChildProcessSpawner` the subprocess variant needs is already required
|
|
3315
|
+
* here for git, so on this composite the subprocess form costs nothing extra.
|
|
3316
|
+
*
|
|
3317
|
+
* Opt in deliberately: this executes config-dependency code in process, and
|
|
3318
|
+
* {@link Workspaces.layerWithGit} never does.
|
|
3319
|
+
*
|
|
3320
|
+
* **Bind the result to a `const`.** A parameterized factory mints a fresh
|
|
3321
|
+
* reference per call and layers memoize by reference.
|
|
3322
|
+
*
|
|
3323
|
+
* @example
|
|
3324
|
+
* ```ts
|
|
3325
|
+
* import { Workspaces } from "@effected/workspaces";
|
|
3326
|
+
*
|
|
3327
|
+
* const KitLayer = Workspaces.layerWithGitAndConfigDependencies();
|
|
3328
|
+
* ```
|
|
3329
|
+
*/
|
|
3330
|
+
static readonly layerWithGitAndConfigDependencies: (options?: WorkspacesGitOptions) => Layer.Layer<WorkspacesServices | ChangeDetector | WorkspaceSnapshots | Git, never, FileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner>;
|
|
3331
|
+
/**
|
|
3332
|
+
* {@link Workspaces.layerWithGitAndConfigDependencies}, with the hook replay
|
|
3333
|
+
* in a `node` **child process** rather than in process.
|
|
3334
|
+
*
|
|
3335
|
+
* @remarks
|
|
3336
|
+
* Same typed semantics; the difference is mechanism, and it decides whether
|
|
3337
|
+
* catalog assembly works at all in a **bundled** consumer — see
|
|
3338
|
+
* {@link Workspaces.layerWithConfigDependenciesSubprocess} for why the
|
|
3339
|
+
* in-process computed `import()` dies under a bundler.
|
|
3340
|
+
*
|
|
3341
|
+
* **This composite's requirement set is identical to the in-process one.**
|
|
3342
|
+
* The subprocess replay needs core's `ChildProcessSpawner`, which this
|
|
3343
|
+
* composite already requires for `Git` — so unlike the git-free pair, there
|
|
3344
|
+
* is no R-widening to weigh here and a bundled consumer pays nothing to be
|
|
3345
|
+
* correct. Prefer this variant whenever the program ships as a bundle.
|
|
3346
|
+
*
|
|
3347
|
+
* **Bind the result to a `const`.**
|
|
3348
|
+
*
|
|
3349
|
+
* @example
|
|
3350
|
+
* ```ts
|
|
3351
|
+
* import { Workspaces } from "@effected/workspaces";
|
|
3352
|
+
*
|
|
3353
|
+
* // A bundled CLI or MCP server: the computed import never enters the graph.
|
|
3354
|
+
* const KitLayer = Workspaces.layerWithGitAndConfigDependenciesSubprocess();
|
|
3355
|
+
* ```
|
|
3356
|
+
*/
|
|
3357
|
+
static readonly layerWithGitAndConfigDependenciesSubprocess: (options?: WorkspacesGitOptions) => Layer.Layer<WorkspacesServices | ChangeDetector | WorkspaceSnapshots | Git, never, FileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner>;
|
|
3072
3358
|
/**
|
|
3073
3359
|
* This package's implementation of `@effected/commands`' `LocalExec`
|
|
3074
3360
|
* contract: how to run a project-local binary here.
|
|
@@ -3454,5 +3740,5 @@ interface GetWorkspacePackagesSyncOptions extends WorkspacesSyncOptions {
|
|
|
3454
3740
|
*/
|
|
3455
3741
|
declare const getWorkspacePackagesSync: (root: string, options: GetWorkspacePackagesSyncOptions) => ReadonlyArray<WorkspacePackage>;
|
|
3456
3742
|
//#endregion
|
|
3457
|
-
export { type CatalogAssemblyFailure, CatalogSet, ChangeDetectionError, type ChangeDetectionFailure, ChangeDetectionOptions, ChangeDetector, type ChangeDetectorShape, type ClassifyOptions, ConfigDependencyHooks, type ConfigDependencyHooksShape, CyclicDependencyError, type DependencyDiff, DependencyGraph, DetectedPackageManager, type FindWorkspaceRootOptions, type GetWorkspacePackagesSyncOptions, type HookInjection, type ImporterVersions, LockfileReadError, type LockfileReadFailure, LockfileReader, type LockfileReaderOptions, type LockfileReaderShape, NoPeerDependencyRules, PackageManagerDetectionError, type PackageManagerDetectionFailure, PackageManagerDetector, type PackageManagerDetectorShape, PackageManagerEvidence, PackageManagerName, PackageNotFoundError, type PackageRelease, PackageStateSnapshot, PeerCheck, type PeerCheckOptions, type PeerDependencyRules, PeerParent, PublishConfig, PublishTarget, PublishabilityDetector, type PublishabilityDetectorShape, ReleaseTag, type SyncDirectoryEntry, type SyncFileSystem, type SyncPath, type TagClassification, type TagFormatOptions, TagStyle, TrackingTag, type TrackingTagOptions, UnsatisfiedPeer, type UnverifiedReason, type VersioningDetectOptions, VersioningStrategy, VersioningStrategyType, 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, classifyTag, findWorkspaceRootSync, getWorkspacePackagesSync };
|
|
3743
|
+
export { type CatalogAssemblyFailure, CatalogSet, ChangeDetectionError, type ChangeDetectionFailure, ChangeDetectionOptions, ChangeDetector, type ChangeDetectorShape, type ClassifyOptions, ConfigDependencyHooks, type ConfigDependencyHooksShape, CyclicDependencyError, type DependencyDiff, DependencyGraph, DetectedPackageManager, type FindWorkspaceRootOptions, type GetWorkspacePackagesSyncOptions, type HookInjection, type ImporterVersions, LockfileReadError, type LockfileReadFailure, LockfileReader, type LockfileReaderOptions, type LockfileReaderShape, NoPeerDependencyRules, PackageManagerDetectionError, type PackageManagerDetectionFailure, PackageManagerDetector, type PackageManagerDetectorShape, PackageManagerEvidence, PackageManagerName, PackageNotFoundError, type PackageRelease, PackageStateSnapshot, PeerCheck, type PeerCheckOptions, type PeerDependencyRules, PeerParent, PublishConfig, PublishTarget, PublishabilityDetector, type PublishabilityDetectorShape, ReleaseTag, type SyncDirectoryEntry, type SyncFileSystem, type SyncPath, type TagClassification, type TagFormatOptions, TagStyle, TrackingTag, type TrackingTagOptions, UnsatisfiedPeer, type UnverifiedReason, type VersioningDetectOptions, VersioningStrategy, VersioningStrategyType, 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 WorkspacesGitOptions, type WorkspacesOptions, type WorkspacesServices, type WorkspacesSyncOptions, classifyTag, findWorkspaceRootSync, getWorkspacePackagesSync };
|
|
3458
3744
|
//# sourceMappingURL=index.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/workspaces",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Monorepo workspace tooling as Effect services — root discovery, package enumeration, the dependency graph, package-manager detection, pnpm catalog resolution, lockfile IO and git-based change detection.",
|
|
6
6
|
"keywords": [
|
|
@@ -47,11 +47,11 @@
|
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@effected/commands": "^0.5.0",
|
|
50
|
-
"@effected/git": "^0.
|
|
50
|
+
"@effected/git": "^0.10.0",
|
|
51
51
|
"@effected/glob": "^0.4.0",
|
|
52
|
-
"@effected/lockfiles": "^0.
|
|
53
|
-
"@effected/npm": "^0.
|
|
54
|
-
"@effected/package-json": "^0.
|
|
52
|
+
"@effected/lockfiles": "^0.7.0",
|
|
53
|
+
"@effected/npm": "^0.12.0",
|
|
54
|
+
"@effected/package-json": "^0.11.0",
|
|
55
55
|
"@effected/semver": "^0.5.0",
|
|
56
56
|
"@effected/walker": "^0.5.0",
|
|
57
57
|
"@effected/yaml": "^0.11.0",
|