@effected/workspaces 0.8.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.
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