@effected/workspaces 0.7.0 → 0.9.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.
@@ -0,0 +1,122 @@
1
+ import { WorkspaceDiscovery } from "./WorkspaceDiscovery.js";
2
+ import { PublishabilityDetector } from "./Publishability.js";
3
+ import { ReleaseTag } from "./ReleaseTag.js";
4
+ import { Effect, Schema } from "effect";
5
+
6
+ //#region src/VersioningStrategy.ts
7
+ /**
8
+ * How a workspace assigns versions across its publishable packages.
9
+ *
10
+ * @remarks
11
+ * - `single` — zero or one publishable package, so one tag names the release.
12
+ * - `fixed-group` — every publishable package sits inside one group that
13
+ * versions in lockstep, so one tag still names the release.
14
+ * - `independent` — publishable packages version separately, so a shared tag
15
+ * would be ambiguous and each package needs its own.
16
+ *
17
+ * @public
18
+ */
19
+ const VersioningStrategyType = Schema.Literals([
20
+ "single",
21
+ "fixed-group",
22
+ "independent"
23
+ ]);
24
+ /**
25
+ * How a workspace versions, and the tagging that follows from it.
26
+ *
27
+ * @remarks
28
+ * Built either purely with {@link VersioningStrategy.classify}, or from a live
29
+ * workspace with {@link VersioningStrategy.detect}.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * import { VersioningStrategy } from "@effected/workspaces";
34
+ * import { Effect } from "effect";
35
+ *
36
+ * const program = Effect.gen(function* () {
37
+ * const strategy = yield* VersioningStrategy.detect({ fixedGroups });
38
+ * return strategy.tagsFor(released).map((tag) => tag.value);
39
+ * });
40
+ * ```
41
+ *
42
+ * @public
43
+ */
44
+ var VersioningStrategy = class VersioningStrategy extends Schema.Class("VersioningStrategy")({
45
+ /** The classification. */
46
+ type: VersioningStrategyType,
47
+ /** The groups classification was performed against, as supplied. */
48
+ fixedGroups: Schema.Array(Schema.Array(Schema.String)),
49
+ /** The publishable package names, sorted and de-duplicated. */
50
+ publishablePackages: Schema.Array(Schema.String)
51
+ }) {
52
+ /**
53
+ * Whether a release needs one tag per package rather than one shared tag.
54
+ */
55
+ get perPackageTags() {
56
+ return this.type === "independent";
57
+ }
58
+ /** The tag style this strategy implies. */
59
+ get tagStyle() {
60
+ return this.perPackageTags ? "scoped" : "single";
61
+ }
62
+ /**
63
+ * Classify a workspace from its publishable package names and fixed groups.
64
+ *
65
+ * @remarks
66
+ * Pure and total — no IO, no error channel. `packages` is sorted and
67
+ * de-duplicated first, so a name listed twice cannot inflate a one-package
68
+ * repo into an independent one.
69
+ */
70
+ static classify(options) {
71
+ const fixedGroups = options.fixedGroups ?? [];
72
+ const packages = [...new Set(options.packages)].sort();
73
+ const type = packages.length <= 1 ? "single" : fixedGroups.some((group) => packages.every((name) => group.includes(name))) ? "fixed-group" : "independent";
74
+ return VersioningStrategy.make({
75
+ type,
76
+ fixedGroups,
77
+ publishablePackages: packages
78
+ });
79
+ }
80
+ /**
81
+ * Classify the ambient workspace: enumerate its packages, keep the ones the
82
+ * {@link PublishabilityDetector} says publish somewhere, and classify those.
83
+ *
84
+ * @remarks
85
+ * The publishability question is asked through the service precisely so a
86
+ * consumer with its own rules — honouring a release tool's ignore list, say —
87
+ * swaps the layer instead of filtering afterwards.
88
+ */
89
+ static detect = Effect.fn("VersioningStrategy.detect")(function* (options) {
90
+ const discovery = yield* WorkspaceDiscovery;
91
+ const publishability = yield* PublishabilityDetector;
92
+ const packages = yield* discovery.listPackages();
93
+ const publishable = [];
94
+ for (const candidate of packages) if ((yield* publishability.detect(candidate)).length > 0) publishable.push(candidate.name);
95
+ return VersioningStrategy.classify({
96
+ packages: publishable,
97
+ ...options?.fixedGroups !== void 0 && { fixedGroups: options.fixedGroups }
98
+ });
99
+ });
100
+ /**
101
+ * The tags a release of `releases` produces under this strategy.
102
+ *
103
+ * @remarks
104
+ * Under `independent` this is one {@link ReleaseTag} per release, in the
105
+ * order given. Under `single` and `fixed-group` it is exactly one shared tag
106
+ * carrying the **first** release's version — every release in a lockstep
107
+ * batch shares a version by construction, so the choice is only visible on a
108
+ * batch that should not exist. Whether a batch actually agreed is a property
109
+ * of that batch rather than of the workspace, so it stays the caller's
110
+ * one-line check rather than a field here.
111
+ *
112
+ * An empty batch produces no tags under either style.
113
+ */
114
+ tagsFor(releases, options) {
115
+ if (releases.length === 0) return [];
116
+ if (this.perPackageTags) return releases.map((release) => ReleaseTag.scoped(release.name, release.version, options));
117
+ return [ReleaseTag.single(releases[0].version, options)];
118
+ }
119
+ };
120
+
121
+ //#endregion
122
+ export { VersioningStrategy, VersioningStrategyType };
@@ -283,6 +283,8 @@ const validatePnpmWorkspaceCatalogs = (document) => {
283
283
  catalogs: "catalogs"
284
284
  }));
285
285
  };
286
+ /** A defect naming the unstubbed test-double method — a test-wiring mistake, not a typed failure. */
287
+ const unstubbed = (method) => Effect.die(/* @__PURE__ */ new Error(`WorkspaceCatalogs.makeTest: ${method}() was called but not stubbed — pass a \`${method}\` override.`));
286
288
  /**
287
289
  * Assembles a workspace's catalogs, package-manager-aware.
288
290
  *
@@ -426,6 +428,70 @@ var WorkspaceCatalogs = class WorkspaceCatalogs extends Context.Service()("@effe
426
428
  */
427
429
  static layerWithConfigDependencies = (options) => Layer.effect(WorkspaceCatalogs, WorkspaceCatalogs.make(options)).pipe(Layer.provide(ConfigDependencyHooks.layerLive));
428
430
  /**
431
+ * A test double satisfying the full {@link WorkspaceCatalogsShape} with no
432
+ * filesystem, lockfile read, or hook replay.
433
+ *
434
+ * @remarks
435
+ * There is **no honest default catalog set**: an empty `CatalogSet` that
436
+ * looks like a legitimate answer is the "every dependency looks newly added"
437
+ * failure class the live assembler hard-fails to prevent, so every method
438
+ * **dies** with an instructive defect until stubbed — a test-wiring mistake
439
+ * fails loudly as a defect rather than succeeding with a lie or failing with
440
+ * a dishonest typed error.
441
+ *
442
+ * The one derivation mirrors `WorkspaceDiscovery.makeTest`'s
443
+ * derived-from-the-primary rule: when a `set` override is supplied,
444
+ * `resolveSpecifier` answers from that `CatalogSet`'s own
445
+ * {@link CatalogSet.resolveSpecifier} — exactly what the live service runs
446
+ * over its assembled set — so the two stay consistent by construction.
447
+ * `releaseAgeGate` and `importerVersions` are **not** derivable from a
448
+ * catalog set (the gate comes from release-age keys and hook contributions,
449
+ * the importer index from the lockfile's importer blocks — neither is in a
450
+ * `CatalogSet`) and always die unless stubbed.
451
+ *
452
+ * @example
453
+ * ```ts
454
+ * import { CatalogSet, WorkspaceCatalogs } from "@effected/workspaces";
455
+ * import { Effect } from "effect";
456
+ *
457
+ * const double = WorkspaceCatalogs.makeTest({
458
+ * set: () => Effect.succeed(CatalogSet.fromCatalogs({ default: { effect: "4.0.0" } })),
459
+ * });
460
+ * // `resolveSpecifier` now answers consistently from that set.
461
+ * ```
462
+ */
463
+ static makeTest = (overrides = {}) => {
464
+ const set = overrides.set;
465
+ return {
466
+ set: () => unstubbed("set"),
467
+ resolveSpecifier: set !== void 0 ? (dependency, specifier) => Effect.map(set(), (catalogs) => catalogs.resolveSpecifier(dependency, specifier)) : () => unstubbed("resolveSpecifier"),
468
+ releaseAgeGate: () => unstubbed("releaseAgeGate"),
469
+ importerVersions: () => unstubbed("importerVersions"),
470
+ ...overrides
471
+ };
472
+ };
473
+ /**
474
+ * The test layer: {@link WorkspaceCatalogs.makeTest} behind `Layer.succeed`,
475
+ * so a suite provides only the methods it exercises.
476
+ *
477
+ * @remarks
478
+ * A parameterized layer factory mints a **fresh reference per call**, and
479
+ * layers memoize by reference — bind the result to a `const` and reuse it
480
+ * rather than calling `layerTest(...)` at each composition site.
481
+ *
482
+ * @example
483
+ * ```ts
484
+ * import { CatalogSet, WorkspaceCatalogs } from "@effected/workspaces";
485
+ * import { Effect } from "effect";
486
+ *
487
+ * const TestCatalogs = WorkspaceCatalogs.layerTest({
488
+ * set: () => Effect.succeed(CatalogSet.empty()),
489
+ * });
490
+ * // program.pipe(Effect.provide(TestCatalogs))
491
+ * ```
492
+ */
493
+ static layerTest = (overrides = {}) => Layer.succeed(WorkspaceCatalogs, WorkspaceCatalogs.makeTest(overrides));
494
+ /**
429
495
  * The real implementation of `@effected/npm`'s `CatalogResolver` contract —
430
496
  * the one `@effected/package-json` declares but cannot fill.
431
497
  *
Binary file
package/Workspaces.js CHANGED
@@ -3,183 +3,250 @@ import { WorkspaceDiscovery } from "./WorkspaceDiscovery.js";
3
3
  import { ChangeDetector } from "./ChangeDetector.js";
4
4
  import { PackageManagerDetector } from "./PackageManagerName.js";
5
5
  import { LockfileReader } from "./LockfileReader.js";
6
- import { PublishabilityDetector } from "./Publishability.js";
7
6
  import { WorkspaceCatalogs } from "./WorkspaceCatalogs.js";
8
7
  import { WorkspaceSnapshots } from "./WorkspaceSnapshots.js";
9
8
  import { Git } from "@effected/git";
10
- import { Effect, Layer } from "effect";
9
+ import { Effect, Layer, Option } from "effect";
10
+ import { ExecContext, LocalExec, LocalExecError } from "@effected/commands";
11
11
 
12
12
  //#region src/Workspaces.ts
13
- /**
14
- * Every service that needs only a filesystem: root, package-manager detection,
15
- * discovery, lockfile reading, catalogs and publishability.
16
- *
17
- * @remarks
18
- * Requires core `FileSystem` and `Path`, which the consumer provides at the
19
- * edge (`@effect/platform-node`, `@effect/platform-bun`, or a test's
20
- * `FileSystem.layerNoop`).
21
- *
22
- * **Bind the result to a `const`.** This is a parameterized factory and layers
23
- * memoize by reference, so calling it twice builds everything twice.
24
- *
25
- * @example
26
- * ```ts
27
- * import { Workspaces } from "@effected/workspaces";
28
- * import { Layer } from "effect";
29
- *
30
- * const WorkspacesLayer = Workspaces.layer();
31
- * const AppLayer = Layer.provide(WorkspacesLayer, PlatformLayer);
32
- * ```
33
- *
34
- * @public
35
- */
36
13
  const compose = (options, catalogsFactory) => {
37
14
  const roots = WorkspaceRoot.layer;
38
15
  const detector = PackageManagerDetector.layer;
39
16
  const discovery = WorkspaceDiscovery.layer(options).pipe(Layer.provide(roots));
40
17
  const lockfiles = LockfileReader.layer(options).pipe(Layer.provide(roots), Layer.provide(detector), Layer.provide(discovery));
41
18
  const catalogs = catalogsFactory(options).pipe(Layer.provide(roots), Layer.provide(lockfiles));
42
- return Layer.mergeAll(roots, detector, discovery, lockfiles, catalogs, PublishabilityDetector.layer);
19
+ return Layer.mergeAll(roots, detector, discovery, lockfiles, catalogs);
43
20
  };
44
21
  const layer = (options) => compose(options, WorkspaceCatalogs.layer);
45
- /**
46
- * The git-free composite plus {@link ChangeDetector} and
47
- * {@link WorkspaceSnapshots}, over `@effected/git`'s `Git` service.
48
- *
49
- * @remarks
50
- * The extra requirement is core's `ChildProcessSpawner` (behind `Git`), which
51
- * is why it is a separate layer rather than a flag: a consumer that never
52
- * detects changes or reads at a ref should not have to be able to spawn a
53
- * subprocess. The consumer provides `ChildProcessSpawner` once at the edge
54
- * (`@effect/platform-node`'s `NodeServices.layer`); a test provides
55
- * `Layer.succeed(Git, …)` and needs no repository on disk.
56
- *
57
- * @public
58
- */
59
22
  const layerWithGit = (options) => {
60
23
  const core = layer(options);
61
24
  const git = Git.layer;
62
25
  return Layer.mergeAll(core, git, ChangeDetector.layer.pipe(Layer.provide(git), Layer.provide(core)), WorkspaceSnapshots.layer(options).pipe(Layer.provide(git), Layer.provide(core)));
63
26
  };
64
- /**
65
- * The two `@effected/npm` resolver contracts, implemented for real.
66
- *
67
- * @remarks
68
- * Provide this alongside `@effected/package-json`'s `Package.resolve` and a
69
- * manifest's `catalog:` and `workspace:` specifiers resolve against the actual
70
- * workspace instead of the no-op layers' `Option.none()`.
71
- *
72
- * @example
73
- * ```ts
74
- * import { Package } from "@effected/package-json";
75
- * import { Workspaces } from "@effected/workspaces";
76
- * import { Layer } from "effect";
77
- *
78
- * const WorkspacesLayer = Workspaces.layer();
79
- * const Resolvers = Workspaces.resolvers.pipe(Layer.provide(WorkspacesLayer));
80
- * ```
81
- *
82
- * @public
83
- */
84
27
  const resolvers = Layer.mergeAll(WorkspaceCatalogs.catalogResolver, WorkspaceDiscovery.workspaceResolver);
85
- /**
86
- * The git-free composite, but with catalog assembly that **replays config
87
- * dependency `pnpmfile.cjs` hooks** — {@link WorkspaceCatalogs.layerWithConfigDependencies}
88
- * in place of the default no-op catalogs layer.
89
- *
90
- * @remarks
91
- * Identical requirement set to {@link Workspaces.layer}; the only difference is
92
- * that config-dependency code is executed in process. Opt in deliberately — the
93
- * default {@link Workspaces.layer} never executes config-dependency code.
94
- *
95
- * **Bind the result to a `const`.**
96
- *
97
- * @public
98
- */
99
28
  const layerWithConfigDependencies = (options) => compose(options, WorkspaceCatalogs.layerWithConfigDependencies);
100
- /**
101
- * The one-call resolver factory: {@link Workspaces.resolvers} pre-wired over
102
- * {@link Workspaces.layerWithConfigDependencies}, so the two `@effected/npm`
103
- * contracts (`CatalogResolver`, `WorkspaceResolver`) need only a platform
104
- * (`FileSystem` + `Path`) from the consumer.
105
- *
106
- * @remarks
107
- * This is deliberately a **parameterized layer function, and the fresh layer
108
- * per call is the feature**: layers memoize by reference, so each call mints
109
- * an unmemoized layer whose root discovery re-runs — including a per-call
110
- * `process.cwd()` read when `options.cwd` is omitted. A build tool that
111
- * changes directory between manifests gets a correct re-discovery each time
112
- * precisely because nothing is shared across calls. When you *want* sharing,
113
- * bind one call's result to a `const` and provide that; the memoization rule
114
- * is unchanged, this factory just refuses to hide it.
115
- *
116
- * Catalog assembly replays config-dependency `pnpmfile` hooks (the
117
- * `layerWithConfigDependencies` path) — the semantics a real pnpm install
118
- * has. Compose {@link Workspaces.resolvers} with {@link Workspaces.layer}
119
- * yourself if config-dependency code must not run in process.
120
- *
121
- * @example
122
- * ```ts
123
- * import { Workspaces } from "@effected/workspaces";
124
- * import { Effect } from "effect";
125
- *
126
- * const program = doSomethingWithResolvers.pipe(
127
- * Effect.provide(Workspaces.resolverLayer()),
128
- * );
129
- * ```
130
- *
131
- * @public
132
- */
133
29
  const resolverLayer = (options) => resolvers.pipe(Layer.provide(layerWithConfigDependencies(options)));
134
- /**
135
- * Resolve every `catalog:` and `workspace:` specifier in one `Manifest`
136
- * against the real workspace, in one call — the 90% path. Decode stays at the
137
- * consumer's edge: build the `Manifest` with `Manifest.decode` (from
138
- * `@effected/npm`), hand it here, and get a new `Manifest` back with concrete
139
- * ranges; `toRecord()` returns to the wire shape.
140
- *
141
- * @remarks
142
- * Composes `manifest.resolve()` with a fresh {@link Workspaces.resolverLayer}
143
- * per call, so the workspace root is re-discovered from `options.cwd` (or the
144
- * current `process.cwd()`) on every invocation. Consumers processing many
145
- * manifests should check `manifest.needsResolution` first and skip the call
146
- * entirely when no dependency field carries a `catalog:`/`workspace:`
147
- * specifier — that predicate is pure and avoids catalog assembly altogether.
148
- *
149
- * A specifier the workspace cannot answer fails typed as
150
- * `UnresolvedDependencyError`; assembly and mechanism failures surface as
151
- * `CatalogAssemblyError` / `DependencyResolutionError`.
152
- *
153
- * @example
154
- * ```ts
155
- * import { Manifest } from "@effected/npm";
156
- * import { Workspaces } from "@effected/workspaces";
157
- * import { Effect } from "effect";
158
- *
159
- * const program = Effect.gen(function* () {
160
- * const manifest = yield* Manifest.decode({ dependencies: { effect: "catalog:" } });
161
- * const resolved = manifest.needsResolution ? yield* Workspaces.resolveManifest(manifest) : manifest;
162
- * return resolved.toRecord();
163
- * });
164
- * ```
165
- *
166
- * @public
167
- */
168
30
  const resolveManifest = Effect.fn("Workspaces.resolveManifest")(function* (manifest, options) {
169
31
  return yield* manifest.resolve().pipe(Effect.provide(resolverLayer(options)));
170
32
  });
33
+ const localExecLayer = (options) => Layer.effect(LocalExec, Effect.gen(function* () {
34
+ const roots = yield* WorkspaceRoot;
35
+ const detector = yield* PackageManagerDetector;
36
+ return { context: Effect.gen(function* () {
37
+ const cwd = options?.cwd ?? globalThis.process?.cwd?.() ?? "/";
38
+ const root = yield* roots.find(cwd).pipe(Effect.asSome, Effect.orElseSucceed(Option.none));
39
+ if (Option.isNone(root)) return Option.none();
40
+ const detected = yield* detector.detect(root.value).pipe(Effect.asSome, Effect.catchTag("PackageManagerDetectionError", () => Effect.succeed(Option.none())), Effect.mapError((cause) => new LocalExecError({
41
+ directory: root.value,
42
+ cause
43
+ })));
44
+ if (Option.isNone(detected)) return Option.none();
45
+ const { prefix, dlxPrefix } = LocalExec.prefixes(detected.value.name);
46
+ return Option.some(ExecContext.make({
47
+ label: detected.value.name,
48
+ prefix,
49
+ dlxPrefix,
50
+ directory: root.value
51
+ }));
52
+ }) };
53
+ }));
171
54
  /**
172
55
  * The composite layers.
173
56
  *
174
57
  * @public
175
58
  */
176
- const Workspaces = {
177
- layer,
178
- layerWithConfigDependencies,
179
- layerWithGit,
180
- resolveManifest,
181
- resolverLayer,
182
- resolvers
59
+ var Workspaces = class {
60
+ constructor() {}
61
+ /**
62
+ * Every service that needs only a filesystem: root, package-manager
63
+ * detection, discovery, lockfile reading, catalogs and publishability.
64
+ *
65
+ * @remarks
66
+ * Requires core `FileSystem` and `Path`, which the consumer provides at the
67
+ * edge (`@effect/platform-node`, `@effect/platform-bun`, or a test's
68
+ * `FileSystem.layerNoop`).
69
+ *
70
+ * **Bind the result to a `const`.** This is a parameterized factory and
71
+ * layers memoize by reference, so calling it twice builds everything twice.
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * import { Workspaces } from "@effected/workspaces";
76
+ * import { Layer } from "effect";
77
+ *
78
+ * const WorkspacesLayer = Workspaces.layer();
79
+ * const AppLayer = Layer.provide(WorkspacesLayer, PlatformLayer);
80
+ * ```
81
+ */
82
+ static layer = layer;
83
+ /**
84
+ * The git-free composite, but with catalog assembly that **replays config
85
+ * dependency `pnpmfile.cjs` hooks** —
86
+ * {@link WorkspaceCatalogs.layerWithConfigDependencies} in place of the
87
+ * default no-op catalogs layer.
88
+ *
89
+ * @remarks
90
+ * Identical requirement set to {@link Workspaces.layer}; the only
91
+ * difference is that config-dependency code is executed in process. Opt in
92
+ * deliberately — the default {@link Workspaces.layer} never executes
93
+ * config-dependency code.
94
+ *
95
+ * **Bind the result to a `const`.**
96
+ */
97
+ static layerWithConfigDependencies = layerWithConfigDependencies;
98
+ /**
99
+ * The git-free composite plus {@link ChangeDetector} and
100
+ * {@link WorkspaceSnapshots}, over `@effected/git`'s `Git` service.
101
+ *
102
+ * @remarks
103
+ * The extra requirement is core's `ChildProcessSpawner` (behind `Git`),
104
+ * which is why it is a separate layer rather than a flag: a consumer that
105
+ * never detects changes or reads at a ref should not have to be able to
106
+ * spawn a subprocess. The consumer provides `ChildProcessSpawner` once at
107
+ * the edge (`@effect/platform-node`'s `NodeServices.layer`); a test
108
+ * provides `Layer.succeed(Git, …)` and needs no repository on disk.
109
+ */
110
+ static layerWithGit = layerWithGit;
111
+ /**
112
+ * This package's implementation of `@effected/commands`' `LocalExec`
113
+ * contract: how to run a project-local binary here.
114
+ *
115
+ * @remarks
116
+ * **An inverted contract, the `@effected/npm` `CatalogResolver`
117
+ * precedent.** Tool discovery needs package-manager detection and
118
+ * workspace-root resolution, both of which live here — but a direct edge
119
+ * from `@effected/commands` to this package would make that boundary-tier
120
+ * package integrated, and through the planned `npm` → `commands` edge
121
+ * would drag `npm`, `lockfiles` (pure!) and `package-json` up a tier with
122
+ * it. So `commands` declares the narrow contract and we ship the layer.
123
+ *
124
+ * **The argv knowledge is not duplicated.** `LocalExec.prefixes(name)` is
125
+ * the one home of the four managers' `exec`/`dlx` prefixes; this layer
126
+ * detects *which* manager owns the directory and asks `commands` what that
127
+ * manager's argv looks like. Neither package reimplements the other's
128
+ * half.
129
+ *
130
+ * **`None` is success.** Outside any workspace — and inside one whose
131
+ * manager cannot be identified — the answer is `Option.none()`: "there is
132
+ * no project-local way to run tools here" is an ordinary fact, not an
133
+ * exceptional one, and a consumer running in a bare directory should not
134
+ * have to catch an error to learn it. The contract's typed
135
+ * `LocalExecError` is reserved for **mechanism** failure — a manifest that
136
+ * exists but cannot be read or parsed, which means something is broken
137
+ * rather than absent. That is npm's resolver convention, adopted
138
+ * verbatim.
139
+ *
140
+ * `directory` is the resolved **workspace root**, not the caller's cwd: a
141
+ * project-local launcher has to run where the workspace is.
142
+ *
143
+ * A consumer with no monorepo never needs this layer, and therefore never
144
+ * installs this package — `LocalExec.layerNone` and `LocalExec.layerFor`
145
+ * are one-liners in `@effected/commands`.
146
+ *
147
+ * **Bind the result to a `const`** — a parameterized layer factory mints a
148
+ * fresh reference per call and layers memoize by reference.
149
+ *
150
+ * @example
151
+ * ```ts
152
+ * import { ToolDiscovery } from "@effected/commands";
153
+ * import { Workspaces } from "@effected/workspaces";
154
+ * import { Layer } from "effect";
155
+ *
156
+ * const AppLayer = ToolDiscovery.layer.pipe(
157
+ * Layer.provide(Workspaces.localExecLayer()),
158
+ * Layer.provide(Workspaces.layer()),
159
+ * Layer.provide(NodeServices.layer),
160
+ * );
161
+ * ```
162
+ */
163
+ static localExecLayer = localExecLayer;
164
+ /**
165
+ * Resolve every `catalog:` and `workspace:` specifier in one `Manifest`
166
+ * against the real workspace, in one call — the 90% path. Decode stays at
167
+ * the consumer's edge: build the `Manifest` with `Manifest.decode` (from
168
+ * `@effected/npm`), hand it here, and get a new `Manifest` back with
169
+ * concrete ranges; `toRecord()` returns to the wire shape.
170
+ *
171
+ * @remarks
172
+ * Composes `manifest.resolve()` with a fresh {@link Workspaces.resolverLayer}
173
+ * per call, so the workspace root is re-discovered from `options.cwd` (or
174
+ * the current `process.cwd()`) on every invocation. Consumers processing
175
+ * many manifests should check `manifest.needsResolution` first and skip
176
+ * the call entirely when no dependency field carries a
177
+ * `catalog:`/`workspace:` specifier — that predicate is pure and avoids
178
+ * catalog assembly altogether.
179
+ *
180
+ * A specifier the workspace cannot answer fails typed as
181
+ * `UnresolvedDependencyError`; assembly and mechanism failures surface as
182
+ * `CatalogAssemblyError` / `DependencyResolutionError`.
183
+ *
184
+ * @example
185
+ * ```ts
186
+ * import { Manifest } from "@effected/npm";
187
+ * import { Workspaces } from "@effected/workspaces";
188
+ * import { Effect } from "effect";
189
+ *
190
+ * const program = Effect.gen(function* () {
191
+ * const manifest = yield* Manifest.decode({ dependencies: { effect: "catalog:" } });
192
+ * const resolved = manifest.needsResolution ? yield* Workspaces.resolveManifest(manifest) : manifest;
193
+ * return resolved.toRecord();
194
+ * });
195
+ * ```
196
+ */
197
+ static resolveManifest = resolveManifest;
198
+ /**
199
+ * The one-call resolver factory: {@link Workspaces.resolvers} pre-wired
200
+ * over {@link Workspaces.layerWithConfigDependencies}, so the two
201
+ * `@effected/npm` contracts (`CatalogResolver`, `WorkspaceResolver`) need
202
+ * only a platform (`FileSystem` + `Path`) from the consumer.
203
+ *
204
+ * @remarks
205
+ * This is deliberately a **parameterized layer function, and the fresh
206
+ * layer per call is the feature**: layers memoize by reference, so each
207
+ * call mints an unmemoized layer whose root discovery re-runs — including
208
+ * a per-call `process.cwd()` read when `options.cwd` is omitted. A build
209
+ * tool that changes directory between manifests gets a correct
210
+ * re-discovery each time precisely because nothing is shared across
211
+ * calls. When you *want* sharing, bind one call's result to a `const` and
212
+ * provide that; the memoization rule is unchanged, this factory just
213
+ * refuses to hide it.
214
+ *
215
+ * Catalog assembly replays config-dependency `pnpmfile` hooks (the
216
+ * `layerWithConfigDependencies` path) — the semantics a real pnpm install
217
+ * has. Compose {@link Workspaces.resolvers} with {@link Workspaces.layer}
218
+ * yourself if config-dependency code must not run in process.
219
+ *
220
+ * @example
221
+ * ```ts
222
+ * import { Workspaces } from "@effected/workspaces";
223
+ * import { Effect } from "effect";
224
+ *
225
+ * const program = doSomethingWithResolvers.pipe(
226
+ * Effect.provide(Workspaces.resolverLayer()),
227
+ * );
228
+ * ```
229
+ */
230
+ static resolverLayer = resolverLayer;
231
+ /**
232
+ * The two `@effected/npm` resolver contracts, implemented for real.
233
+ *
234
+ * @remarks
235
+ * Provide this alongside `@effected/package-json`'s `Package.resolve` and
236
+ * a manifest's `catalog:` and `workspace:` specifiers resolve against the
237
+ * actual workspace instead of the no-op layers' `Option.none()`.
238
+ *
239
+ * @example
240
+ * ```ts
241
+ * import { Package } from "@effected/package-json";
242
+ * import { Workspaces } from "@effected/workspaces";
243
+ * import { Layer } from "effect";
244
+ *
245
+ * const WorkspacesLayer = Workspaces.layer();
246
+ * const Resolvers = Workspaces.resolvers.pipe(Layer.provide(WorkspacesLayer));
247
+ * ```
248
+ */
249
+ static resolvers = resolvers;
183
250
  };
184
251
 
185
252
  //#endregion