@effected/workspaces 0.23.0 → 0.24.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/ConfigDependencyHooks.js +208 -170
- package/PeerCheck.js +27 -20
- package/WorkspaceCatalogs.js +62 -57
- package/WorkspaceSnapshots.js +37 -7
- package/WorkspaceStateSnapshot.js +41 -17
- package/Workspaces.js +55 -22
- package/index.d.ts +301 -81
- package/internal/configDependencyResolution.js +307 -0
- package/internal/patterns.js +2 -1
- package/internal/peerPatterns.js +35 -0
- package/internal/workspaceYaml.js +44 -0
- package/package.json +2 -2
package/WorkspaceCatalogs.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { WorkspaceRoot } from "./WorkspaceRoot.js";
|
|
2
2
|
import { inlineCatalogs, merge, normalize, rangeOf } from "./internal/catalogs.js";
|
|
3
|
-
import { ConfigDependencyHooks } from "./ConfigDependencyHooks.js";
|
|
3
|
+
import { ConfigDependencyHooks, NoPeerDependencyRules } from "./ConfigDependencyHooks.js";
|
|
4
4
|
import { LockfileReader } from "./LockfileReader.js";
|
|
5
5
|
import { importerVersionsOf } from "./internal/importerVersions.js";
|
|
6
|
+
import { configDependenciesOf, inlinePeerDependencyRules } from "./internal/workspaceYaml.js";
|
|
6
7
|
import { Context, Duration, Effect, Exit, FileSystem, Layer, Option, Path, PlatformError, Schema } from "effect";
|
|
7
8
|
import { CatalogAssemblyError, CatalogResolver, DependencyResolutionError, PartialReleaseAgeGate, ReleaseAgeGate } from "@effected/npm";
|
|
8
9
|
import { Yaml } from "@effected/yaml";
|
|
@@ -173,13 +174,6 @@ const catalogBlocksOf = (document) => {
|
|
|
173
174
|
catalogs: raw.catalogs
|
|
174
175
|
};
|
|
175
176
|
};
|
|
176
|
-
/** The `configDependencies` map (name → version+integrity) of a parsed pnpm-workspace document. */
|
|
177
|
-
const configDependenciesOf = (document) => {
|
|
178
|
-
if (!isObject(document) || !isObject(document.configDependencies)) return {};
|
|
179
|
-
const out = {};
|
|
180
|
-
for (const [name, spec] of Object.entries(document.configDependencies)) if (typeof spec === "string") out[name] = spec;
|
|
181
|
-
return out;
|
|
182
|
-
};
|
|
183
177
|
/**
|
|
184
178
|
* The inline release-age gate contribution of a parsed `pnpm-workspace.yaml`
|
|
185
179
|
* document — pnpm's top-level `minimumReleaseAge` (minutes) and
|
|
@@ -211,41 +205,6 @@ const inlineReleaseAge = (document) => {
|
|
|
211
205
|
cause
|
|
212
206
|
})));
|
|
213
207
|
};
|
|
214
|
-
/**
|
|
215
|
-
* The `peerDependencyRules` a `pnpm-workspace.yaml` declares inline — the half
|
|
216
|
-
* `pnpm:export` materializes into the file, as opposed to the half a config
|
|
217
|
-
* dependency injects at replay time.
|
|
218
|
-
*
|
|
219
|
-
* @remarks
|
|
220
|
-
* **Tolerant, unlike the catalog blocks**, and the asymmetry is deliberate: a
|
|
221
|
-
* malformed catalog block must hard-fail because a silently-empty catalog makes
|
|
222
|
-
* every dependency look newly added, whereas a malformed rules block costs only
|
|
223
|
-
* suppression — the failure mode is reporting a peer pnpm would have hidden,
|
|
224
|
-
* which is visible and safe. Failing the whole assembly over it would take the
|
|
225
|
-
* catalogs down with it.
|
|
226
|
-
*
|
|
227
|
-
* Every axis is read independently, so a malformed `ignoreMissing` does not
|
|
228
|
-
* discard a well-formed `allowedVersions`.
|
|
229
|
-
*/
|
|
230
|
-
const EMPTY_PEER_RULES = {
|
|
231
|
-
allowedVersions: {},
|
|
232
|
-
ignoreMissing: [],
|
|
233
|
-
allowAny: []
|
|
234
|
-
};
|
|
235
|
-
const inlinePeerDependencyRules = (document) => {
|
|
236
|
-
if (!isObject(document) || !isObject(document.peerDependencyRules)) return EMPTY_PEER_RULES;
|
|
237
|
-
const block = document.peerDependencyRules;
|
|
238
|
-
const allowedVersions = {};
|
|
239
|
-
if (isObject(block.allowedVersions)) {
|
|
240
|
-
for (const [key, value] of Object.entries(block.allowedVersions)) if (typeof value === "string") allowedVersions[key] = value;
|
|
241
|
-
}
|
|
242
|
-
const strings = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
243
|
-
return {
|
|
244
|
-
allowedVersions,
|
|
245
|
-
ignoreMissing: strings(block.ignoreMissing),
|
|
246
|
-
allowAny: strings(block.allowAny)
|
|
247
|
-
};
|
|
248
|
-
};
|
|
249
208
|
/** A hard-fail catalog-assembly failure naming the malformed part of a `workspaces` field. */
|
|
250
209
|
const malformed = (source, path, detail) => new CatalogAssemblyError({
|
|
251
210
|
source,
|
|
@@ -321,6 +280,23 @@ const validatePnpmWorkspaceCatalogs = (document) => {
|
|
|
321
280
|
/** A defect naming the unstubbed test-double method — a test-wiring mistake, not a typed failure. */
|
|
322
281
|
const unstubbed = (method) => Effect.die(/* @__PURE__ */ new Error(`WorkspaceCatalogs.makeTest: ${method}() was called but not stubbed — pass a \`${method}\` override.`));
|
|
323
282
|
/**
|
|
283
|
+
* Replay a parsed `pnpm-workspace.yaml` document's `configDependencies`
|
|
284
|
+
* hooks over its inline catalogs, seeded by its inline `peerDependencyRules`
|
|
285
|
+
* — the ONE call into `hooks.inject` that both the live assembler and the
|
|
286
|
+
* at-ref reader (`WorkspaceSnapshots`) make, so the two sides of a diff
|
|
287
|
+
* cannot disagree about what a document declares or how it is seeded.
|
|
288
|
+
*
|
|
289
|
+
* @remarks
|
|
290
|
+
* Rules are SEEDED, not merged afterwards: the workspace file's block goes
|
|
291
|
+
* into the threaded config so the hooks merge onto it, exactly as pnpm seeds
|
|
292
|
+
* its own config and takes back what the hooks return. Not part of the public
|
|
293
|
+
* surface; `WorkspaceSnapshots` imports it directly.
|
|
294
|
+
*/
|
|
295
|
+
const injectFromDocument = (hooks, root, document, inline) => Effect.map(hooks.inject(root, configDependenciesOf(document), inline.entries, inlinePeerDependencyRules(document)), (injection) => ({
|
|
296
|
+
injected: CatalogSet.fromCatalogs(injection.catalogs),
|
|
297
|
+
injection
|
|
298
|
+
}));
|
|
299
|
+
/**
|
|
324
300
|
* Assembles a workspace's catalogs, package-manager-aware.
|
|
325
301
|
*
|
|
326
302
|
* @remarks
|
|
@@ -378,7 +354,8 @@ var WorkspaceCatalogs = class WorkspaceCatalogs extends Context.Service()("@effe
|
|
|
378
354
|
let injected;
|
|
379
355
|
let inlineGate = {};
|
|
380
356
|
let hookGate = {};
|
|
381
|
-
let peerDependencyRules =
|
|
357
|
+
let peerDependencyRules = NoPeerDependencyRules;
|
|
358
|
+
let hookReplays = {};
|
|
382
359
|
if (hasPnpmWorkspace) {
|
|
383
360
|
const text = yield* fs.readFileString(workspaceYaml).pipe(Effect.mapError((cause) => new CatalogAssemblyError({
|
|
384
361
|
source: "manifest",
|
|
@@ -393,17 +370,19 @@ var WorkspaceCatalogs = class WorkspaceCatalogs extends Context.Service()("@effe
|
|
|
393
370
|
yield* validatePnpmWorkspaceCatalogs(document);
|
|
394
371
|
inline = CatalogSet.fromCatalogs(inlineCatalogs(catalogBlocksOf(document)));
|
|
395
372
|
inlineGate = yield* inlineReleaseAge(document);
|
|
396
|
-
const
|
|
397
|
-
injected =
|
|
398
|
-
hookGate = injection.releaseAge;
|
|
399
|
-
peerDependencyRules = injection.peerDependencyRules;
|
|
373
|
+
const replayed = yield* injectFromDocument(hooks, root, document, inline);
|
|
374
|
+
injected = replayed.injected;
|
|
375
|
+
hookGate = replayed.injection.releaseAge;
|
|
376
|
+
peerDependencyRules = replayed.injection.peerDependencyRules;
|
|
377
|
+
hookReplays = replayed.injection.replays;
|
|
400
378
|
} else {
|
|
401
379
|
const manifestPath = path.join(root, "package.json");
|
|
402
380
|
if (!(yield* probeExists(manifestPath))) return {
|
|
403
381
|
catalogs: fromLockfile,
|
|
404
382
|
releaseAgeGate: ReleaseAgeGate.combine(),
|
|
405
383
|
importerVersions,
|
|
406
|
-
peerDependencyRules:
|
|
384
|
+
peerDependencyRules: NoPeerDependencyRules,
|
|
385
|
+
hookReplays: {}
|
|
407
386
|
};
|
|
408
387
|
const text = yield* fs.readFileString(manifestPath).pipe(Effect.mapError((cause) => new CatalogAssemblyError({
|
|
409
388
|
source: "manifest",
|
|
@@ -424,7 +403,8 @@ var WorkspaceCatalogs = class WorkspaceCatalogs extends Context.Service()("@effe
|
|
|
424
403
|
catalogs: assembled,
|
|
425
404
|
releaseAgeGate,
|
|
426
405
|
importerVersions,
|
|
427
|
-
peerDependencyRules
|
|
406
|
+
peerDependencyRules,
|
|
407
|
+
hookReplays
|
|
428
408
|
};
|
|
429
409
|
});
|
|
430
410
|
const [resolveOnce, invalidate] = yield* Effect.cachedInvalidateWithTTL(assemble, Duration.infinity);
|
|
@@ -445,10 +425,33 @@ var WorkspaceCatalogs = class WorkspaceCatalogs extends Context.Service()("@effe
|
|
|
445
425
|
importerVersions: Effect.fn("WorkspaceCatalogs.importerVersions")(function* () {
|
|
446
426
|
return (yield* memo).importerVersions;
|
|
447
427
|
}),
|
|
428
|
+
hookReplays: Effect.fn("WorkspaceCatalogs.hookReplays")(function* () {
|
|
429
|
+
return (yield* memo).hookReplays;
|
|
430
|
+
}),
|
|
448
431
|
refresh: () => invalidate
|
|
449
432
|
};
|
|
450
433
|
});
|
|
451
434
|
/**
|
|
435
|
+
* The service over an explicit {@link ConfigDependencyHooks} layer — the
|
|
436
|
+
* one builder every `layer*` static and the `Workspaces` composites
|
|
437
|
+
* delegate to, so the hooks layer is provided to {@link WorkspaceCatalogs.make}
|
|
438
|
+
* in exactly one place.
|
|
439
|
+
*
|
|
440
|
+
* @remarks
|
|
441
|
+
* Reach for it when the hooks layer is chosen elsewhere — a composite that
|
|
442
|
+
* must hand ONE hooks reference to both this service and
|
|
443
|
+
* `WorkspaceSnapshots` (layers memoize by reference, so two references
|
|
444
|
+
* would replay twice and could run two policies), or a test wiring
|
|
445
|
+
* {@link ConfigDependencyHooks.layerFrom}. The three `layer*` statics are
|
|
446
|
+
* this with the hooks layer fixed. `R` is the hooks layer's own
|
|
447
|
+
* requirement (the subprocess variant's `ChildProcessSpawner`), threaded
|
|
448
|
+
* through. Parameterized, so bind it to a `const` and reuse it.
|
|
449
|
+
*
|
|
450
|
+
* @param hooks - The {@link ConfigDependencyHooks} layer to replay through.
|
|
451
|
+
* @param options - The same options the other layer statics take.
|
|
452
|
+
*/
|
|
453
|
+
static layerWithHooks = (hooks, options) => Layer.effect(WorkspaceCatalogs, WorkspaceCatalogs.make(options)).pipe(Layer.provide(hooks));
|
|
454
|
+
/**
|
|
452
455
|
* The live layer — the default, which **never executes config-dependency
|
|
453
456
|
* code**: it wires {@link ConfigDependencyHooks.layerNoop}, whose hooks return
|
|
454
457
|
* the inline-catalog seed untouched.
|
|
@@ -457,7 +460,7 @@ var WorkspaceCatalogs = class WorkspaceCatalogs extends Context.Service()("@effe
|
|
|
457
460
|
* Parameterized, so it mints a fresh reference per call — bind it to a
|
|
458
461
|
* `const` and reuse it, or layer memoization does not apply.
|
|
459
462
|
*/
|
|
460
|
-
static layer = (options) =>
|
|
463
|
+
static layer = (options) => WorkspaceCatalogs.layerWithHooks(ConfigDependencyHooks.layerNoop, options);
|
|
461
464
|
/**
|
|
462
465
|
* The opt-in live layer that **does** replay config-dependency `pnpmfile.cjs`
|
|
463
466
|
* hooks: it wires {@link ConfigDependencyHooks.layerLive}, which dynamically
|
|
@@ -469,7 +472,7 @@ var WorkspaceCatalogs = class WorkspaceCatalogs extends Context.Service()("@effe
|
|
|
469
472
|
* the default catalog path stays free of any config-dependency execution.
|
|
470
473
|
* Parameterized, so bind it to a `const` and reuse it.
|
|
471
474
|
*/
|
|
472
|
-
static layerWithConfigDependencies = (options) =>
|
|
475
|
+
static layerWithConfigDependencies = (options) => WorkspaceCatalogs.layerWithHooks(ConfigDependencyHooks.layerLive, options);
|
|
473
476
|
/**
|
|
474
477
|
* The opt-in layer that replays config-dependency `pnpmfile` hooks in a
|
|
475
478
|
* `node` **child process**: it wires
|
|
@@ -487,7 +490,7 @@ var WorkspaceCatalogs = class WorkspaceCatalogs extends Context.Service()("@effe
|
|
|
487
490
|
* same sanctioned R-widening as `Workspaces.layerWithGit`. Parameterized, so
|
|
488
491
|
* bind it to a `const` and reuse it.
|
|
489
492
|
*/
|
|
490
|
-
static layerWithConfigDependenciesSubprocess = (options) =>
|
|
493
|
+
static layerWithConfigDependenciesSubprocess = (options) => WorkspaceCatalogs.layerWithHooks(ConfigDependencyHooks.layerSubprocess, options);
|
|
491
494
|
/**
|
|
492
495
|
* A test double satisfying the full {@link WorkspaceCatalogsShape} with no
|
|
493
496
|
* filesystem, lockfile read, or hook replay.
|
|
@@ -505,9 +508,10 @@ var WorkspaceCatalogs = class WorkspaceCatalogs extends Context.Service()("@effe
|
|
|
505
508
|
* `resolveSpecifier` answers from that `CatalogSet`'s own
|
|
506
509
|
* {@link CatalogSet.resolveSpecifier} — exactly what the live service runs
|
|
507
510
|
* over its assembled set — so the two stay consistent by construction.
|
|
508
|
-
* `releaseAgeGate` and `
|
|
509
|
-
* catalog set (the gate comes from release-age keys and
|
|
510
|
-
* the importer index from the lockfile's importer
|
|
511
|
+
* `releaseAgeGate`, `importerVersions` and `hookReplays` are **not**
|
|
512
|
+
* derivable from a catalog set (the gate comes from release-age keys and
|
|
513
|
+
* hook contributions, the importer index from the lockfile's importer
|
|
514
|
+
* blocks, the replay record from the hooks layer — none is in a
|
|
511
515
|
* `CatalogSet`) and always die unless stubbed.
|
|
512
516
|
*
|
|
513
517
|
* `refresh` defaults to `Effect.void` honestly: the double holds no memo,
|
|
@@ -534,6 +538,7 @@ var WorkspaceCatalogs = class WorkspaceCatalogs extends Context.Service()("@effe
|
|
|
534
538
|
peerDependencyRules: () => unstubbed("peerDependencyRules"),
|
|
535
539
|
releaseAgeGate: () => unstubbed("releaseAgeGate"),
|
|
536
540
|
importerVersions: () => unstubbed("importerVersions"),
|
|
541
|
+
hookReplays: () => unstubbed("hookReplays"),
|
|
537
542
|
refresh: () => Effect.void,
|
|
538
543
|
...overrides
|
|
539
544
|
};
|
|
@@ -585,4 +590,4 @@ var WorkspaceCatalogs = class WorkspaceCatalogs extends Context.Service()("@effe
|
|
|
585
590
|
};
|
|
586
591
|
|
|
587
592
|
//#endregion
|
|
588
|
-
export { CatalogSet, WorkspaceCatalogs };
|
|
593
|
+
export { CatalogSet, WorkspaceCatalogs, injectFromDocument };
|
package/WorkspaceSnapshots.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { manifestPatternsOf, pnpmPatternsOf } from "./internal/patterns.js";
|
|
2
2
|
import { WorkspaceRoot } from "./WorkspaceRoot.js";
|
|
3
3
|
import { WorkspaceDiscovery } from "./WorkspaceDiscovery.js";
|
|
4
|
+
import { ConfigDependencyHooks } from "./ConfigDependencyHooks.js";
|
|
4
5
|
import { importerVersionsOf } from "./internal/importerVersions.js";
|
|
5
|
-
import { CatalogSet, WorkspaceCatalogs } from "./WorkspaceCatalogs.js";
|
|
6
|
+
import { CatalogSet, WorkspaceCatalogs, injectFromDocument } from "./WorkspaceCatalogs.js";
|
|
6
7
|
import { PackageStateSnapshot, WorkspaceStateSnapshot } from "./WorkspaceStateSnapshot.js";
|
|
7
8
|
import { Git } from "@effected/git";
|
|
8
9
|
import { Context, Duration, Effect, Exit, Layer, Option } from "effect";
|
|
@@ -12,6 +13,17 @@ import { Yaml } from "@effected/yaml";
|
|
|
12
13
|
import { Lockfile, filenameFor } from "@effected/lockfiles";
|
|
13
14
|
|
|
14
15
|
//#region src/WorkspaceSnapshots.ts
|
|
16
|
+
/**
|
|
17
|
+
* Project the live replay record onto the snapshot's `name → version` field:
|
|
18
|
+
* a snapshot is "what the workspace looked like then", and WHERE this machine
|
|
19
|
+
* found each pnpmfile is machine-local provenance that does not belong on a
|
|
20
|
+
* serializable value.
|
|
21
|
+
*/
|
|
22
|
+
const hookVersionsOf = (replays) => {
|
|
23
|
+
const versions = {};
|
|
24
|
+
for (const [name, replay] of Object.entries(replays)) versions[name] = replay.version;
|
|
25
|
+
return versions;
|
|
26
|
+
};
|
|
15
27
|
/** Whether `value` is a non-null, non-array object. */
|
|
16
28
|
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17
29
|
/** Whether every value in a record is a string — a usable dependency map. */
|
|
@@ -98,12 +110,22 @@ const unstubbed = (method) => Effect.die(/* @__PURE__ */ new Error(`WorkspaceSna
|
|
|
98
110
|
* @public
|
|
99
111
|
*/
|
|
100
112
|
var WorkspaceSnapshots = class WorkspaceSnapshots extends Context.Service()("@effected/workspaces/WorkspaceSnapshots") {
|
|
101
|
-
/**
|
|
113
|
+
/**
|
|
114
|
+
* Builds the service over `Git`, {@link WorkspaceRoot}, {@link WorkspaceDiscovery},
|
|
115
|
+
* {@link WorkspaceCatalogs} and {@link ConfigDependencyHooks}.
|
|
116
|
+
*
|
|
117
|
+
* @remarks
|
|
118
|
+
* `ConfigDependencyHooks` is what `at(ref)` replays a ref's
|
|
119
|
+
* `configDependencies` through — at the versions THAT ref declares, which is
|
|
120
|
+
* why the composites hand the same hooks layer to this service and to
|
|
121
|
+
* `WorkspaceCatalogs`. Under `layerNoop` the ref read executes nothing.
|
|
122
|
+
*/
|
|
102
123
|
static make = (options) => Effect.gen(function* () {
|
|
103
124
|
const git = yield* Git;
|
|
104
125
|
const roots = yield* WorkspaceRoot;
|
|
105
126
|
const discovery = yield* WorkspaceDiscovery;
|
|
106
127
|
const catalogsService = yield* WorkspaceCatalogs;
|
|
128
|
+
const hooks = yield* ConfigDependencyHooks;
|
|
107
129
|
const seeded = (snapshot) => options?.seedCatalogs === void 0 ? snapshot : snapshot.withSeededCatalogs(options.seedCatalogs);
|
|
108
130
|
/**
|
|
109
131
|
* What a manager's lockfile records at the ref: its catalog set and its
|
|
@@ -118,15 +140,16 @@ var WorkspaceSnapshots = class WorkspaceSnapshots extends Context.Service()("@ef
|
|
|
118
140
|
})), Effect.catch(() => Effect.succeed(EMPTY_LOCKFILE_RECORD)))
|
|
119
141
|
})));
|
|
120
142
|
const computeAt = (root, ref) => Effect.gen(function* () {
|
|
121
|
-
const pnpmWorkspaceText = yield* git.show(root, ref, "./pnpm-workspace.yaml");
|
|
122
|
-
const rootManifestText = yield* git.show(root, ref, "./package.json");
|
|
143
|
+
const [pnpmWorkspaceText, rootManifestText] = yield* Effect.all([git.show(root, ref, "./pnpm-workspace.yaml"), git.show(root, ref, "./package.json")], { concurrency: 2 });
|
|
123
144
|
const rootManifest = Option.match(rootManifestText, {
|
|
124
145
|
onNone: () => ({}),
|
|
125
146
|
onSome: parseJsonObject
|
|
126
147
|
});
|
|
127
148
|
let patterns;
|
|
128
149
|
let inline;
|
|
150
|
+
let injected;
|
|
129
151
|
let recorded;
|
|
152
|
+
let hookReplays = {};
|
|
130
153
|
if (Option.isSome(pnpmWorkspaceText)) {
|
|
131
154
|
const document = yield* Yaml.parse(pnpmWorkspaceText.value).pipe(Effect.mapError((cause) => new CatalogAssemblyError({
|
|
132
155
|
source: "manifest",
|
|
@@ -136,13 +159,17 @@ var WorkspaceSnapshots = class WorkspaceSnapshots extends Context.Service()("@ef
|
|
|
136
159
|
const pnpmPatterns = pnpmPatternsOf(document);
|
|
137
160
|
patterns = pnpmPatterns.length > 0 ? pnpmPatterns : manifestPatternsOf(rootManifest);
|
|
138
161
|
inline = yield* CatalogSet.fromWorkspaceYaml(pnpmWorkspaceText.value);
|
|
162
|
+
const replayed = yield* injectFromDocument(hooks, root, document, inline);
|
|
163
|
+
injected = replayed.injected;
|
|
164
|
+
hookReplays = replayed.injection.replays;
|
|
139
165
|
recorded = yield* lockfileRecord(root, ref, "pnpm");
|
|
140
166
|
} else {
|
|
141
167
|
patterns = manifestPatternsOf(rootManifest);
|
|
142
168
|
inline = bunInlineCatalogs(rootManifest);
|
|
169
|
+
injected = CatalogSet.empty();
|
|
143
170
|
recorded = yield* lockfileRecord(root, ref, "bun");
|
|
144
171
|
}
|
|
145
|
-
const catalogs = CatalogSet.merge(recorded.catalogs, inline);
|
|
172
|
+
const catalogs = CatalogSet.merge(recorded.catalogs, inline, injected);
|
|
146
173
|
const globs = yield* GlobSet.compile(patterns).pipe(Effect.mapError((error) => new CatalogAssemblyError({
|
|
147
174
|
source: "manifest",
|
|
148
175
|
path: error.pattern,
|
|
@@ -170,7 +197,8 @@ var WorkspaceSnapshots = class WorkspaceSnapshots extends Context.Service()("@ef
|
|
|
170
197
|
return seeded(WorkspaceStateSnapshot.make({
|
|
171
198
|
packages,
|
|
172
199
|
catalogs,
|
|
173
|
-
importerVersions: recorded.importerVersions
|
|
200
|
+
importerVersions: recorded.importerVersions,
|
|
201
|
+
hookReplays: hookVersionsOf(hookReplays)
|
|
174
202
|
}));
|
|
175
203
|
});
|
|
176
204
|
const atCaches = /* @__PURE__ */ new Map();
|
|
@@ -195,6 +223,7 @@ var WorkspaceSnapshots = class WorkspaceSnapshots extends Context.Service()("@ef
|
|
|
195
223
|
const packages = yield* discovery.listPackages();
|
|
196
224
|
const catalogs = yield* catalogsService.set();
|
|
197
225
|
const importerVersions = yield* catalogsService.importerVersions();
|
|
226
|
+
const hookReplays = yield* catalogsService.hookReplays();
|
|
198
227
|
const snapshotPackages = packages.map((pkg) => PackageStateSnapshot.make({
|
|
199
228
|
name: pkg.name,
|
|
200
229
|
version: pkg.version ?? "",
|
|
@@ -207,7 +236,8 @@ var WorkspaceSnapshots = class WorkspaceSnapshots extends Context.Service()("@ef
|
|
|
207
236
|
return seeded(WorkspaceStateSnapshot.make({
|
|
208
237
|
packages: snapshotPackages,
|
|
209
238
|
catalogs,
|
|
210
|
-
importerVersions
|
|
239
|
+
importerVersions,
|
|
240
|
+
hookReplays: hookVersionsOf(hookReplays)
|
|
211
241
|
}));
|
|
212
242
|
})
|
|
213
243
|
};
|
|
@@ -117,6 +117,23 @@ var WorkspaceStateSnapshot = class WorkspaceStateSnapshot extends Schema.Class("
|
|
|
117
117
|
*/
|
|
118
118
|
importerVersions: Schema.optionalKey(Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.String))),
|
|
119
119
|
/**
|
|
120
|
+
* Which version each declared config dependency's hook was replayed from at
|
|
121
|
+
* this moment, keyed by name.
|
|
122
|
+
*
|
|
123
|
+
* @remarks
|
|
124
|
+
* Versions only — WHERE this machine found each pnpmfile (the `source` of a
|
|
125
|
+
* live `HookReplay`) is machine-local provenance, not part of "what the
|
|
126
|
+
* workspace looked like then", so it stays on the live `HookInjection`
|
|
127
|
+
* diagnostic and off this serializable value. Every fresh read sets the
|
|
128
|
+
* field: `{}` under the no-op layer, which resolves nothing, when the file
|
|
129
|
+
* declares no config dependencies, or on the bun / `package.json` path,
|
|
130
|
+
* where config dependencies do not exist. Absent only when decoding a
|
|
131
|
+
* snapshot serialized before the field existed, so stored values stay
|
|
132
|
+
* readable. Carried through `withSeededCatalogs` and `crossSeed`
|
|
133
|
+
* unchanged, like `importerVersions`.
|
|
134
|
+
*/
|
|
135
|
+
hookReplays: Schema.optionalKey(Schema.Record(Schema.String, Schema.String)),
|
|
136
|
+
/**
|
|
120
137
|
* Catalogs supplied from OUTSIDE this moment, consulted only when
|
|
121
138
|
* `catalogs` cannot answer.
|
|
122
139
|
*
|
|
@@ -129,11 +146,14 @@ var WorkspaceStateSnapshot = class WorkspaceStateSnapshot extends Schema.Class("
|
|
|
129
146
|
* always wins and both halves stay readable.
|
|
130
147
|
*
|
|
131
148
|
* The motivating case is a catalog injected by a config-dependency
|
|
132
|
-
* `pnpmfile` hook. It is recorded in no committed catalog source, so
|
|
133
|
-
* `
|
|
134
|
-
* see it, and a `catalog:` specifier
|
|
135
|
-
* sides of a diff. Seeding the live
|
|
136
|
-
* set,
|
|
149
|
+
* `pnpmfile` hook. It is recorded in no committed catalog source, so under
|
|
150
|
+
* a non-replaying `ConfigDependencyHooks` layer (`layerNoop`, the default)
|
|
151
|
+
* `WorkspaceSnapshots.at(ref)` cannot see it, and a `catalog:` specifier
|
|
152
|
+
* against it resolves to nothing on BOTH sides of a diff. Seeding the live
|
|
153
|
+
* hook-injected set, or the other side's set, restores a declared RANGE
|
|
154
|
+
* without executing any historical code. Under a replaying layer `at(ref)`
|
|
155
|
+
* replays the ref's own pinned hook and the seed answers only what neither
|
|
156
|
+
* source declared.
|
|
137
157
|
*
|
|
138
158
|
* Defaults to absent, which makes the seed inert — exactly the behavior of
|
|
139
159
|
* every snapshot captured before this field existed.
|
|
@@ -280,8 +300,8 @@ var WorkspaceStateSnapshot = class WorkspaceStateSnapshot extends Schema.Class("
|
|
|
280
300
|
* value, and an accumulating seed would make precedence depend on call
|
|
281
301
|
* order). {@link WorkspaceStateSnapshot.crossSeed} is the deliberate
|
|
282
302
|
* exception: it composes the two seeds explicitly, precisely because a bare
|
|
283
|
-
* replace would discard a layer-level seed. `catalogs`, `packages` and `
|
|
284
|
-
* unchanged, so what the ref declared is still exactly what it declared.
|
|
303
|
+
* replace would discard a layer-level seed. `catalogs`, `packages`, `importerVersions` and `hookReplays` are
|
|
304
|
+
* carried through unchanged, so what the ref declared is still exactly what it declared.
|
|
285
305
|
*
|
|
286
306
|
* The two seeds worth reaching for: the LIVE hook-injected catalog set (from
|
|
287
307
|
* a `WorkspaceCatalogs` built by one of the config-dependency layers), or the
|
|
@@ -311,6 +331,7 @@ var WorkspaceStateSnapshot = class WorkspaceStateSnapshot extends Schema.Class("
|
|
|
311
331
|
packages: this.packages,
|
|
312
332
|
catalogs: this.catalogs,
|
|
313
333
|
...this.importerVersions === void 0 ? {} : { importerVersions: this.importerVersions },
|
|
334
|
+
...this.hookReplays === void 0 ? {} : { hookReplays: this.hookReplays },
|
|
314
335
|
seededCatalogs: seed
|
|
315
336
|
});
|
|
316
337
|
}
|
|
@@ -326,16 +347,19 @@ var WorkspaceStateSnapshot = class WorkspaceStateSnapshot extends Schema.Class("
|
|
|
326
347
|
* missing it, at strictly lower precedence than that side's own catalogs, so
|
|
327
348
|
* a genuine change between the refs still reads as a change.
|
|
328
349
|
*
|
|
329
|
-
* **
|
|
330
|
-
* change made purely by bumping the config dependency BETWEEN the two
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
* the
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
350
|
+
* **What cross-seeding can and cannot see depends on the hooks layer.** A
|
|
351
|
+
* range change made purely by bumping the config dependency BETWEEN the two
|
|
352
|
+
* refs is detected when the snapshots were read under a replaying
|
|
353
|
+
* `ConfigDependencyHooks` layer: `WorkspaceSnapshots.at(ref)` replays each
|
|
354
|
+
* ref's hook at the version that ref's `configDependencies` declares, so
|
|
355
|
+
* each side's OWN catalogs carry that side's injected range and the seed
|
|
356
|
+
* never gets a say. Under `layerNoop` the case stays suppressed — neither
|
|
357
|
+
* committed source declares the catalog, so each side falls back to the
|
|
358
|
+
* other's value and the two agree by construction; there, diff
|
|
359
|
+
* `configDependencies` in `pnpm-workspace.yaml` directly, the only committed
|
|
360
|
+
* evidence that the injection changed. Cross-seeding is harmless under a
|
|
361
|
+
* replaying layer (own catalogs take precedence) and still worthwhile under
|
|
362
|
+
* the no-op one, which is why it stays.
|
|
339
363
|
*
|
|
340
364
|
* **The seeding relationship is symmetric; the RETURN ORDER is not.** Each
|
|
341
365
|
* snapshot is seeded with the other's catalogs, so neither argument is
|
package/Workspaces.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { WorkspaceRoot } from "./WorkspaceRoot.js";
|
|
2
2
|
import { WorkspaceDiscovery } from "./WorkspaceDiscovery.js";
|
|
3
3
|
import { ChangeDetector } from "./ChangeDetector.js";
|
|
4
|
+
import { ConfigDependencyHooks } from "./ConfigDependencyHooks.js";
|
|
4
5
|
import { PackageManagerDetector } from "./PackageManagerName.js";
|
|
5
6
|
import { LockfileReader } from "./LockfileReader.js";
|
|
6
7
|
import { WorkspaceCatalogs } from "./WorkspaceCatalogs.js";
|
|
@@ -10,39 +11,34 @@ import { Effect, Layer, Option } from "effect";
|
|
|
10
11
|
import { ExecContext, LocalExec, LocalExecError } from "@effected/commands";
|
|
11
12
|
|
|
12
13
|
//#region src/Workspaces.ts
|
|
13
|
-
const compose = (options,
|
|
14
|
+
const compose = (options, hooks) => {
|
|
14
15
|
const roots = WorkspaceRoot.layer;
|
|
15
16
|
const detector = PackageManagerDetector.layer;
|
|
16
17
|
const discovery = WorkspaceDiscovery.layer(options).pipe(Layer.provide(roots));
|
|
17
18
|
const lockfiles = LockfileReader.layer(options).pipe(Layer.provide(roots), Layer.provide(detector), Layer.provide(discovery));
|
|
18
|
-
const catalogs =
|
|
19
|
+
const catalogs = WorkspaceCatalogs.layerWithHooks(hooks, options).pipe(Layer.provide(roots), Layer.provide(lockfiles));
|
|
19
20
|
return Layer.mergeAll(roots, detector, discovery, lockfiles, catalogs);
|
|
20
21
|
};
|
|
21
|
-
const layer = (options) => compose(options,
|
|
22
|
+
const layer = (options) => compose(options, ConfigDependencyHooks.layerNoop);
|
|
22
23
|
/**
|
|
23
|
-
* The
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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.
|
|
24
|
+
* The core composite widened with the git tier: `Git`, `ChangeDetector` and
|
|
25
|
+
* `WorkspaceSnapshots`. It builds the core over `hooks` ITSELF, so the SAME
|
|
26
|
+
* hooks layer reaches `WorkspaceCatalogs` (through `compose`) and
|
|
27
|
+
* `WorkspaceSnapshots` by construction — `at(ref)` replays a ref's config
|
|
28
|
+
* dependencies exactly as the live assembler replays the worktree's: one
|
|
29
|
+
* reference, one memo, one policy for both sides of a diff.
|
|
35
30
|
*/
|
|
36
|
-
const withGit = (
|
|
31
|
+
const withGit = (hooks, options) => {
|
|
32
|
+
const core = compose(options, hooks);
|
|
37
33
|
const git = Git.layer;
|
|
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)));
|
|
34
|
+
return Layer.mergeAll(core, git, ChangeDetector.layer.pipe(Layer.provide(git), Layer.provide(core)), WorkspaceSnapshots.layer(options).pipe(Layer.provide(git), Layer.provide(hooks), Layer.provide(core)));
|
|
39
35
|
};
|
|
40
|
-
const layerWithGit = (options) => withGit(
|
|
36
|
+
const layerWithGit = (options) => withGit(ConfigDependencyHooks.layerNoop, options);
|
|
41
37
|
const resolvers = Layer.mergeAll(WorkspaceCatalogs.catalogResolver, WorkspaceDiscovery.workspaceResolver);
|
|
42
|
-
const layerWithConfigDependencies = (options) => compose(options,
|
|
43
|
-
const layerWithConfigDependenciesSubprocess = (options) => compose(options,
|
|
44
|
-
const layerWithGitAndConfigDependencies = (options) => withGit(
|
|
45
|
-
const layerWithGitAndConfigDependenciesSubprocess = (options) => withGit(
|
|
38
|
+
const layerWithConfigDependencies = (options) => compose(options, ConfigDependencyHooks.layerLive);
|
|
39
|
+
const layerWithConfigDependenciesSubprocess = (options) => compose(options, ConfigDependencyHooks.layerSubprocess);
|
|
40
|
+
const layerWithGitAndConfigDependencies = (options) => withGit(ConfigDependencyHooks.layerLive, options);
|
|
41
|
+
const layerWithGitAndConfigDependenciesSubprocess = (options) => withGit(ConfigDependencyHooks.layerSubprocess, options);
|
|
46
42
|
const resolverLayer = (options) => resolvers.pipe(Layer.provide(layerWithConfigDependencies(options)));
|
|
47
43
|
const resolveManifest = Effect.fn("Workspaces.resolveManifest")(function* (manifest, options) {
|
|
48
44
|
return yield* manifest.resolve().pipe(Effect.provide(resolverLayer(options)));
|
|
@@ -226,6 +222,43 @@ var Workspaces = class {
|
|
|
226
222
|
*/
|
|
227
223
|
static layerWithGitAndConfigDependenciesSubprocess = layerWithGitAndConfigDependenciesSubprocess;
|
|
228
224
|
/**
|
|
225
|
+
* The git composite over a **caller-supplied** hooks layer — the same graph
|
|
226
|
+
* as {@link Workspaces.layerWithGit} and its two config-dependency
|
|
227
|
+
* variants, with the `ConfigDependencyHooks` policy chosen by the caller.
|
|
228
|
+
*
|
|
229
|
+
* @remarks
|
|
230
|
+
* The three fixed git composites are this function applied to
|
|
231
|
+
* {@link ConfigDependencyHooks.layerNoop}, {@link ConfigDependencyHooks.layerLive}
|
|
232
|
+
* and {@link ConfigDependencyHooks.layerSubprocess}. It exists for the fourth
|
|
233
|
+
* policy: {@link ConfigDependencyHooks.layerFrom}, the hermetic seam, which a
|
|
234
|
+
* snapshot-backed test otherwise cannot reach without rebuilding the whole
|
|
235
|
+
* git graph by hand — the copy `layerWithGitAndConfigDependencies` was added
|
|
236
|
+
* to delete. The ONE hooks reference is handed to both `WorkspaceCatalogs`
|
|
237
|
+
* and `WorkspaceSnapshots`, so `at(ref)` and `worktree()` cannot drift on
|
|
238
|
+
* which policy they replay. A hooks layer carrying its own requirement (the
|
|
239
|
+
* subprocess variant's `ChildProcessSpawner`) threads it through to the
|
|
240
|
+
* composite's `R` unchanged.
|
|
241
|
+
*
|
|
242
|
+
* **Bind the result to a `const`.**
|
|
243
|
+
*
|
|
244
|
+
* @param hooks - The `ConfigDependencyHooks` layer to wire on both sides.
|
|
245
|
+
* @param options - The composite options, as for {@link Workspaces.layerWithGit}.
|
|
246
|
+
*
|
|
247
|
+
* @example
|
|
248
|
+
* ```ts
|
|
249
|
+
* import { ConfigDependencyHooks, Workspaces } from "@effected/workspaces";
|
|
250
|
+
*
|
|
251
|
+
* // A test pinning what at(ref) replays for two declared plugin versions.
|
|
252
|
+
* const KitLayer = Workspaces.layerWithGitAndHooks(
|
|
253
|
+
* ConfigDependencyHooks.layerFrom({
|
|
254
|
+
* "@scope/plugin@1.0.0": "/fixtures/plugin-1/pnpmfile.mjs",
|
|
255
|
+
* "@scope/plugin@2.0.0": "/fixtures/plugin-2/pnpmfile.mjs",
|
|
256
|
+
* }),
|
|
257
|
+
* );
|
|
258
|
+
* ```
|
|
259
|
+
*/
|
|
260
|
+
static layerWithGitAndHooks = withGit;
|
|
261
|
+
/**
|
|
229
262
|
* This package's implementation of `@effected/commands`' `LocalExec`
|
|
230
263
|
* contract: how to run a project-local binary here.
|
|
231
264
|
*
|