@effected/workspaces 0.4.1 → 0.5.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.
@@ -98,9 +98,13 @@ var ConfigDependencyHooks = class ConfigDependencyHooks extends Context.Service(
98
98
  * `hooks`-source `CatalogAssemblyError`, never a silent skip.
99
99
  *
100
100
  * @remarks
101
- * Node-coupled by design the `node:fs` / `node:path` / `node:url` imports are
102
- * the sanctioned Node-only overlay, matching the other seams here. Only ever
103
- * wired by `WorkspaceCatalogs.layerWithConfigDependencies`.
101
+ * Runtime-coupled by design, not node-exclusive. The `import()` below loads
102
+ * **and executes** a config dependency's pnpmfile in-process code execution,
103
+ * not IO, so no `FileSystem` / `Path` service abstracts it. The `node:path` and
104
+ * `node:url` imports (`join`, `pathToFileURL`) exist only to build the URL that
105
+ * `import()` consumes; node and bun both implement those builtins and dynamic
106
+ * import, so this layer runs on either runtime. Only ever wired by
107
+ * `WorkspaceCatalogs.layerWithConfigDependencies`.
104
108
  */
105
109
  static layerLive = Layer.succeed(ConfigDependencyHooks, { inject: (root, configDependencies, seed) => Effect.gen(function* () {
106
110
  const names = Object.keys(configDependencies);
@@ -184,6 +184,7 @@ var WorkspaceDiscovery = class WorkspaceDiscovery extends Context.Service()("@ef
184
184
  path: directory,
185
185
  packageJsonPath,
186
186
  relativePath,
187
+ workspaceRoot: root,
187
188
  manifestRecord: raw,
188
189
  ...typeof raw.private === "boolean" ? { private: raw.private } : {},
189
190
  ...isStringRecord(raw.dependencies) ? { dependencies: raw.dependencies } : {},
@@ -341,6 +342,7 @@ var WorkspaceDiscovery = class WorkspaceDiscovery extends Context.Service()("@ef
341
342
  * path: "/repo/packages/utils",
342
343
  * packageJsonPath: "/repo/packages/utils/package.json",
343
344
  * relativePath: "packages/utils",
345
+ * workspaceRoot: "/repo",
344
346
  * }),
345
347
  * ]),
346
348
  * });
@@ -78,10 +78,12 @@ var WorkspaceManifestError = class extends Schema.TaggedErrorClass()("WorkspaceM
78
78
  * path: "/repo/packages/utils",
79
79
  * packageJsonPath: "/repo/packages/utils/package.json",
80
80
  * relativePath: "packages/utils",
81
+ * workspaceRoot: "/repo",
81
82
  * });
82
83
  *
83
84
  * pkg.isRootWorkspace; // false
84
85
  * pkg.unscopedName; // "utils"
86
+ * pkg.workspaceRoot; // "/repo"
85
87
  * ```
86
88
  *
87
89
  * @public
@@ -97,6 +99,21 @@ var WorkspacePackage = class WorkspacePackage extends Schema.Class("WorkspacePac
97
99
  packageJsonPath: Schema.NonEmptyString,
98
100
  /** POSIX path relative to the workspace root; `"."` for the root package. */
99
101
  relativePath: Schema.String,
102
+ /**
103
+ * Absolute path to the workspace root this package was discovered under.
104
+ *
105
+ * @remarks
106
+ * Carried, not derived. Whoever built this value already knew the root —
107
+ * `WorkspaceDiscovery` resolved it before enumerating, and the sync entry
108
+ * point is handed it — so dropping it forced every consumer into per-package
109
+ * root arithmetic (counting `relativePath` segments and re-ascending that
110
+ * many `..`). That reconstruction is exact only while `path` and
111
+ * `relativePath` agree, and it re-derives something the kit never had to
112
+ * lose.
113
+ *
114
+ * For the root package this equals `path`, and `relativePath` is `"."`.
115
+ */
116
+ workspaceRoot: Schema.NonEmptyString,
100
117
  /** Whether the package is marked private. */
101
118
  private: Schema.Boolean.pipe(Schema.withDecodingDefaultKey(Effect.succeed(false)), Schema.withConstructorDefault(Effect.succeed(false))),
102
119
  /** Production dependencies. */
package/WorkspaceRoot.js CHANGED
@@ -21,11 +21,22 @@ var WorkspaceRootNotFoundError = class extends Schema.TaggedErrorClass()("Worksp
21
21
  /** The directory the ascent started from. */
22
22
  searchPath: Schema.String,
23
23
  /** The marker filenames probed at each ancestor. */
24
- markers: Schema.Array(Schema.String)
24
+ markers: Schema.Array(Schema.String),
25
+ /**
26
+ * The resolved ceiling the ascent was bounded by, when one was supplied.
27
+ *
28
+ * @remarks
29
+ * Absent means the ascent ran to the filesystem root. Its presence is what
30
+ * lets a caller tell "there is no workspace root anywhere above me" from
31
+ * "there is none below the ceiling I set" — two failures that otherwise
32
+ * render identically.
33
+ */
34
+ stopAt: Schema.optionalKey(Schema.String)
25
35
  }) {
26
- /** Renders the search path and probed markers into a one-line message. */
36
+ /** Renders the search path, probed markers and any ceiling into a one-line message. */
27
37
  get message() {
28
- return `No workspace root above ${this.searchPath} (looked for ${this.markers.join(", ")})`;
38
+ const bound = this.stopAt === void 0 ? "" : ` up to ${this.stopAt}`;
39
+ return `No workspace root above ${this.searchPath}${bound} (looked for ${this.markers.join(", ")})`;
29
40
  }
30
41
  };
31
42
  /**
@@ -48,6 +59,12 @@ const isWorkspaceRoot = (dir) => Effect.gen(function* () {
48
59
  }).pipe(Effect.orElseSucceed(() => ({})));
49
60
  return parsed.workspaces !== void 0 && parsed.workspaces !== null;
50
61
  });
62
+ /** Whether `descendant` is `ancestor` itself or lies beneath it, on POSIX or win32 separators. */
63
+ const isAtOrBelow = (descendant, ancestor) => {
64
+ if (descendant === ancestor) return true;
65
+ const prefix = ancestor.endsWith("/") || ancestor.endsWith("\\") ? ancestor : `${ancestor}/`;
66
+ return descendant.startsWith(prefix) || descendant.startsWith(prefix.replace(/\/$/, "\\"));
67
+ };
51
68
  /**
52
69
  * Locates the workspace root by ascending from a starting directory.
53
70
  *
@@ -62,6 +79,20 @@ const isWorkspaceRoot = (dir) => Effect.gen(function* () {
62
79
  * });
63
80
  * ```
64
81
  *
82
+ * @example
83
+ * Bounded: an unmarked fixture directory fails typed instead of escaping into
84
+ * the enclosing repository.
85
+ *
86
+ * ```ts
87
+ * import { WorkspaceRoot } from "@effected/workspaces";
88
+ * import { Effect } from "effect";
89
+ *
90
+ * const program = Effect.gen(function* () {
91
+ * const roots = yield* WorkspaceRoot;
92
+ * return yield* roots.find("/tmp/fixture/packages/a", { stopAt: "/tmp/fixture" });
93
+ * });
94
+ * ```
95
+ *
65
96
  * @public
66
97
  */
67
98
  var WorkspaceRoot = class WorkspaceRoot extends Context.Service()("@effected/workspaces/WorkspaceRoot") {
@@ -69,21 +100,92 @@ var WorkspaceRoot = class WorkspaceRoot extends Context.Service()("@effected/wor
69
100
  static make = Effect.gen(function* () {
70
101
  const fs = yield* FileSystem.FileSystem;
71
102
  const path = yield* Path.Path;
72
- const find = Effect.fn("WorkspaceRoot.find")(function* (cwd) {
103
+ const find = Effect.fn("WorkspaceRoot.find")(function* (cwd, options) {
73
104
  const start = path.resolve(cwd);
74
- const chain = yield* Walker.ascend(start);
105
+ const ceiling = options?.stopAt === void 0 ? void 0 : path.resolve(options.stopAt);
106
+ const chain = yield* Walker.ascend(start, {
107
+ ...ceiling === void 0 ? {} : { stopAt: ceiling },
108
+ ...options?.maxDepth === void 0 ? {} : { maxDepth: options.maxDepth }
109
+ });
75
110
  const found = yield* Walker.findRoot(chain, isWorkspaceRoot);
76
111
  if (Option.isNone(found)) return yield* Effect.fail(new WorkspaceRootNotFoundError({
77
112
  searchPath: start,
78
- markers: WORKSPACE_MARKERS
113
+ markers: WORKSPACE_MARKERS,
114
+ ...ceiling === void 0 ? {} : { stopAt: ceiling }
79
115
  }));
80
116
  yield* Effect.logDebug("Workspace root found").pipe(Effect.annotateLogs("workspace.root", found.value));
81
117
  return found.value;
82
118
  });
83
- return { find: (cwd) => find(cwd).pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path)) };
119
+ return { find: (cwd, options) => find(cwd, options).pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path)) };
84
120
  });
85
121
  /** The live layer. */
86
122
  static layer = Layer.effect(WorkspaceRoot, WorkspaceRoot.make);
123
+ /**
124
+ * A test double resolving every `find` to `root`, with no filesystem.
125
+ *
126
+ * @remarks
127
+ * The nine-copies-of-a-four-line-mock case — a `Layer.succeed` over a `find`
128
+ * that ignores its arguments and succeeds with a fixed root is what consumers
129
+ * were writing by hand. The difference is that this double **honours
130
+ * `stopAt`**: a hand-rolled `find` that ignores the ceiling makes a bounded
131
+ * call pass under test and fail against the live service, which is the
132
+ * failure mode the option exists to catch. A `root` above the ceiling fails
133
+ * here exactly as it would live, with the same
134
+ * {@link WorkspaceRootNotFoundError}.
135
+ *
136
+ * The ceiling is `path.resolve`d through the injected `Path` service before
137
+ * the comparison, exactly as the live `make` path does — so a `stopAt`
138
+ * carrying `..` segments bounds the double identically to the live service,
139
+ * not by raw string. This is why `makeTest` yields an `Effect` requiring
140
+ * `Path`: it captures the service once at construction, the same shape as
141
+ * `make`. Consumers reach for {@link WorkspaceRoot.layerTest}, which provides
142
+ * `Path.layer` internally, so the requirement never surfaces at their call
143
+ * site.
144
+ *
145
+ * `maxDepth` is deliberately NOT modelled: the double does not walk, so it has
146
+ * no depth to cap, and pretending otherwise would encode a fiction. A suite
147
+ * exercising the depth guard wants the live service over a fixture tree.
148
+ *
149
+ * @param root - The root every unbounded `find` resolves to.
150
+ */
151
+ static makeTest = (root) => Effect.gen(function* () {
152
+ const path = yield* Path.Path;
153
+ return { find: (cwd, options) => options?.stopAt !== void 0 && !isAtOrBelow(root, path.resolve(options.stopAt)) ? Effect.fail(new WorkspaceRootNotFoundError({
154
+ searchPath: cwd,
155
+ markers: WORKSPACE_MARKERS,
156
+ stopAt: options.stopAt
157
+ })) : Effect.succeed(root) };
158
+ });
159
+ /**
160
+ * The test layer: {@link WorkspaceRoot.makeTest} with `Path.layer` provided.
161
+ *
162
+ * @remarks
163
+ * `makeTest` requires `Path` to normalize the `stopAt` ceiling; this layer
164
+ * supplies core's `Path.layer` internally, so the requirement never reaches a
165
+ * consumer — the published type stays `Layer.Layer<WorkspaceRoot>`.
166
+ *
167
+ * A parameterized layer factory mints a **fresh reference per call**, and
168
+ * layers memoize by reference — bind the result to a `const` and reuse it
169
+ * rather than calling `layerTest(...)` at each composition site.
170
+ *
171
+ * Pair it with `WorkspaceDiscovery.layerTest` to stand up the whole discovery
172
+ * path without a filesystem; between them there is nothing left for a
173
+ * module-level mock of `@effected/workspaces` to do, and a provided layer
174
+ * keeps the service graph — and its typed errors — intact.
175
+ *
176
+ * @example
177
+ * ```ts
178
+ * import { WorkspaceDiscovery, WorkspaceRoot } from "@effected/workspaces";
179
+ * import { Effect } from "effect";
180
+ *
181
+ * const TestRoot = WorkspaceRoot.layerTest("/repo");
182
+ * const TestDiscovery = WorkspaceDiscovery.layerTest({
183
+ * listPackages: () => Effect.succeed([]),
184
+ * });
185
+ * // program.pipe(Effect.provide(TestRoot), Effect.provide(TestDiscovery))
186
+ * ```
187
+ */
188
+ static layerTest = (root) => Layer.effect(WorkspaceRoot, WorkspaceRoot.makeTest(root)).pipe(Layer.provide(Path.layer));
87
189
  };
88
190
 
89
191
  //#endregion
package/WorkspacesSync.js CHANGED
@@ -113,7 +113,7 @@ const readPatternsSync = (options, root) => {
113
113
  return manifestPatternsOf(readJson(fileSystem, path.join(root, "package.json")) ?? {});
114
114
  };
115
115
  /** Build a `WorkspacePackage` from a directory, or `null` if its manifest is unusable. */
116
- const readPackageSync = (options, directory, relativePath) => {
116
+ const readPackageSync = (options, root, directory, relativePath) => {
117
117
  const packageJsonPath = options.path.join(directory, "package.json");
118
118
  const raw = readJson(options.fileSystem, packageJsonPath);
119
119
  if (raw === void 0) return null;
@@ -130,6 +130,7 @@ const readPackageSync = (options, directory, relativePath) => {
130
130
  path: directory,
131
131
  packageJsonPath,
132
132
  relativePath,
133
+ workspaceRoot: root,
133
134
  private: raw.private === true,
134
135
  dependencies: stringRecord(raw.dependencies) ?? {},
135
136
  devDependencies: stringRecord(raw.devDependencies) ?? {},
@@ -233,10 +234,10 @@ const getWorkspacePackagesSync = (root, options) => {
233
234
  for (const [relativePath, absolute] of [...included.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
234
235
  if (relativePath === "." || absolute === root) continue;
235
236
  if (globs.excludes.some((exclude) => exclude.matches(relativePath))) continue;
236
- const pkg = readPackageSync(options, absolute, relativePath);
237
+ const pkg = readPackageSync(options, root, absolute, relativePath);
237
238
  if (pkg !== null) members.push(pkg);
238
239
  }
239
- const rootPackage = readPackageSync(options, root, ".");
240
+ const rootPackage = readPackageSync(options, root, root, ".");
240
241
  return rootPackage === null ? members : [rootPackage, ...members];
241
242
  };
242
243
 
package/index.d.ts CHANGED
@@ -90,6 +90,21 @@ declare const WorkspacePackage_base: Schema.Class<WorkspacePackage, Schema.Struc
90
90
  readonly packageJsonPath: Schema.NonEmptyString;
91
91
  /** POSIX path relative to the workspace root; `"."` for the root package. */
92
92
  readonly relativePath: Schema.String;
93
+ /**
94
+ * Absolute path to the workspace root this package was discovered under.
95
+ *
96
+ * @remarks
97
+ * Carried, not derived. Whoever built this value already knew the root —
98
+ * `WorkspaceDiscovery` resolved it before enumerating, and the sync entry
99
+ * point is handed it — so dropping it forced every consumer into per-package
100
+ * root arithmetic (counting `relativePath` segments and re-ascending that
101
+ * many `..`). That reconstruction is exact only while `path` and
102
+ * `relativePath` agree, and it re-derives something the kit never had to
103
+ * lose.
104
+ *
105
+ * For the root package this equals `path`, and `relativePath` is `"."`.
106
+ */
107
+ readonly workspaceRoot: Schema.NonEmptyString;
93
108
  /** Whether the package is marked private. */
94
109
  readonly private: Schema.withConstructorDefault<Schema.withDecodingDefaultKey<Schema.Boolean, never>>;
95
110
  /** Production dependencies. */
@@ -135,10 +150,12 @@ declare const WorkspacePackage_base: Schema.Class<WorkspacePackage, Schema.Struc
135
150
  * path: "/repo/packages/utils",
136
151
  * packageJsonPath: "/repo/packages/utils/package.json",
137
152
  * relativePath: "packages/utils",
153
+ * workspaceRoot: "/repo",
138
154
  * });
139
155
  *
140
156
  * pkg.isRootWorkspace; // false
141
157
  * pkg.unscopedName; // "utils"
158
+ * pkg.workspaceRoot; // "/repo"
142
159
  * ```
143
160
  *
144
161
  * @public
@@ -212,11 +229,53 @@ declare class WorkspacePackage extends WorkspacePackage_base {
212
229
  * @public
213
230
  */
214
231
  declare const WORKSPACE_MARKERS: ReadonlyArray<string>;
232
+ /**
233
+ * Options for {@link WorkspaceRoot}'s `find`.
234
+ *
235
+ * @remarks
236
+ * Both bounds are passed straight through to `@effected/walker`'s
237
+ * `Walker.ascend`; this package does not re-decide either.
238
+ *
239
+ * @public
240
+ */
241
+ interface FindWorkspaceRootOptions {
242
+ /**
243
+ * A ceiling directory. The ascent stops after probing it, so an unmarked
244
+ * `stopAt` fails typed as {@link WorkspaceRootNotFoundError} rather than
245
+ * silently escaping into an enclosing repository.
246
+ *
247
+ * @remarks
248
+ * Resolved to an absolute path before comparison, exactly as `cwd` is — a
249
+ * relative or non-normalized ceiling that never string-matched an ancestor
250
+ * would reintroduce the unbounded ascent it was passed to prevent.
251
+ */
252
+ readonly stopAt?: string;
253
+ /**
254
+ * Hard cap on the number of directories probed.
255
+ *
256
+ * @remarks
257
+ * A non-integer or non-positive value is a **defect**, not a typed failure —
258
+ * it is developer wiring, and walker's guard raises it.
259
+ *
260
+ * @defaultValue 256
261
+ */
262
+ readonly maxDepth?: number;
263
+ }
215
264
  declare const WorkspaceRootNotFoundError_base: Schema.Class<WorkspaceRootNotFoundError, Schema.TaggedStruct<"WorkspaceRootNotFoundError", {
216
265
  /** The directory the ascent started from. */
217
266
  readonly searchPath: Schema.String;
218
267
  /** The marker filenames probed at each ancestor. */
219
268
  readonly markers: Schema.$Array<Schema.String>;
269
+ /**
270
+ * The resolved ceiling the ascent was bounded by, when one was supplied.
271
+ *
272
+ * @remarks
273
+ * Absent means the ascent ran to the filesystem root. Its presence is what
274
+ * lets a caller tell "there is no workspace root anywhere above me" from
275
+ * "there is none below the ceiling I set" — two failures that otherwise
276
+ * render identically.
277
+ */
278
+ readonly stopAt: Schema.optionalKey<Schema.String>;
220
279
  }>, import("effect/Cause").YieldableError>;
221
280
  /**
222
281
  * Raised when no workspace root can be found by ascending from a directory.
@@ -228,18 +287,32 @@ declare const WorkspaceRootNotFoundError_base: Schema.Class<WorkspaceRootNotFoun
228
287
  * @public
229
288
  */
230
289
  declare class WorkspaceRootNotFoundError extends WorkspaceRootNotFoundError_base {
231
- /** Renders the search path and probed markers into a one-line message. */
290
+ /** Renders the search path, probed markers and any ceiling into a one-line message. */
232
291
  get message(): string;
233
292
  }
234
- declare const WorkspaceRoot_base: Context.ServiceClass<WorkspaceRoot, "@effected/workspaces/WorkspaceRoot", {
293
+ /**
294
+ * The {@link WorkspaceRoot} service contract.
295
+ *
296
+ * @remarks
297
+ * Named so a consumer can type its own double — or a `Layer.succeed` — against
298
+ * the contract rather than re-deriving it, exactly as `WorkspaceDiscoveryShape`
299
+ * does. Prefer {@link WorkspaceRoot.layerTest} to hand-rolling one.
300
+ *
301
+ * @public
302
+ */
303
+ interface WorkspaceRootShape {
235
304
  /**
236
305
  * The nearest workspace root at or above `cwd`.
237
306
  *
238
307
  * @param cwd - The directory to start the ascent from; resolved to an
239
308
  * absolute path first.
309
+ * @param options - Optional ascent bounds. Unbounded by default, which
310
+ * walks to the filesystem root and can therefore resolve to an enclosing
311
+ * repository's root; pass `stopAt` when the caller knows the ceiling.
240
312
  */
241
- readonly find: (cwd: string) => Effect.Effect<string, WorkspaceRootNotFoundError>;
242
- }>;
313
+ readonly find: (cwd: string, options?: FindWorkspaceRootOptions) => Effect.Effect<string, WorkspaceRootNotFoundError>;
314
+ }
315
+ declare const WorkspaceRoot_base: Context.ServiceClass<WorkspaceRoot, "@effected/workspaces/WorkspaceRoot", WorkspaceRootShape>;
243
316
  /**
244
317
  * Locates the workspace root by ascending from a starting directory.
245
318
  *
@@ -254,15 +327,86 @@ declare const WorkspaceRoot_base: Context.ServiceClass<WorkspaceRoot, "@effected
254
327
  * });
255
328
  * ```
256
329
  *
330
+ * @example
331
+ * Bounded: an unmarked fixture directory fails typed instead of escaping into
332
+ * the enclosing repository.
333
+ *
334
+ * ```ts
335
+ * import { WorkspaceRoot } from "@effected/workspaces";
336
+ * import { Effect } from "effect";
337
+ *
338
+ * const program = Effect.gen(function* () {
339
+ * const roots = yield* WorkspaceRoot;
340
+ * return yield* roots.find("/tmp/fixture/packages/a", { stopAt: "/tmp/fixture" });
341
+ * });
342
+ * ```
343
+ *
257
344
  * @public
258
345
  */
259
346
  declare class WorkspaceRoot extends WorkspaceRoot_base {
260
347
  /** Builds the service over core `FileSystem` and `Path`. */
261
- static readonly make: Effect.Effect<{
262
- readonly find: (cwd: string) => Effect.Effect<string, WorkspaceRootNotFoundError>;
263
- }, never, FileSystem.FileSystem | Path.Path>;
348
+ static readonly make: Effect.Effect<WorkspaceRootShape, never, FileSystem.FileSystem | Path.Path>;
264
349
  /** The live layer. */
265
350
  static readonly layer: Layer.Layer<WorkspaceRoot, never, FileSystem.FileSystem | Path.Path>;
351
+ /**
352
+ * A test double resolving every `find` to `root`, with no filesystem.
353
+ *
354
+ * @remarks
355
+ * The nine-copies-of-a-four-line-mock case — a `Layer.succeed` over a `find`
356
+ * that ignores its arguments and succeeds with a fixed root is what consumers
357
+ * were writing by hand. The difference is that this double **honours
358
+ * `stopAt`**: a hand-rolled `find` that ignores the ceiling makes a bounded
359
+ * call pass under test and fail against the live service, which is the
360
+ * failure mode the option exists to catch. A `root` above the ceiling fails
361
+ * here exactly as it would live, with the same
362
+ * {@link WorkspaceRootNotFoundError}.
363
+ *
364
+ * The ceiling is `path.resolve`d through the injected `Path` service before
365
+ * the comparison, exactly as the live `make` path does — so a `stopAt`
366
+ * carrying `..` segments bounds the double identically to the live service,
367
+ * not by raw string. This is why `makeTest` yields an `Effect` requiring
368
+ * `Path`: it captures the service once at construction, the same shape as
369
+ * `make`. Consumers reach for {@link WorkspaceRoot.layerTest}, which provides
370
+ * `Path.layer` internally, so the requirement never surfaces at their call
371
+ * site.
372
+ *
373
+ * `maxDepth` is deliberately NOT modelled: the double does not walk, so it has
374
+ * no depth to cap, and pretending otherwise would encode a fiction. A suite
375
+ * exercising the depth guard wants the live service over a fixture tree.
376
+ *
377
+ * @param root - The root every unbounded `find` resolves to.
378
+ */
379
+ static readonly makeTest: (root: string) => Effect.Effect<WorkspaceRootShape, never, Path.Path>;
380
+ /**
381
+ * The test layer: {@link WorkspaceRoot.makeTest} with `Path.layer` provided.
382
+ *
383
+ * @remarks
384
+ * `makeTest` requires `Path` to normalize the `stopAt` ceiling; this layer
385
+ * supplies core's `Path.layer` internally, so the requirement never reaches a
386
+ * consumer — the published type stays `Layer.Layer<WorkspaceRoot>`.
387
+ *
388
+ * A parameterized layer factory mints a **fresh reference per call**, and
389
+ * layers memoize by reference — bind the result to a `const` and reuse it
390
+ * rather than calling `layerTest(...)` at each composition site.
391
+ *
392
+ * Pair it with `WorkspaceDiscovery.layerTest` to stand up the whole discovery
393
+ * path without a filesystem; between them there is nothing left for a
394
+ * module-level mock of `@effected/workspaces` to do, and a provided layer
395
+ * keeps the service graph — and its typed errors — intact.
396
+ *
397
+ * @example
398
+ * ```ts
399
+ * import { WorkspaceDiscovery, WorkspaceRoot } from "@effected/workspaces";
400
+ * import { Effect } from "effect";
401
+ *
402
+ * const TestRoot = WorkspaceRoot.layerTest("/repo");
403
+ * const TestDiscovery = WorkspaceDiscovery.layerTest({
404
+ * listPackages: () => Effect.succeed([]),
405
+ * });
406
+ * // program.pipe(Effect.provide(TestRoot), Effect.provide(TestDiscovery))
407
+ * ```
408
+ */
409
+ static readonly layerTest: (root: string) => Layer.Layer<WorkspaceRoot>;
266
410
  }
267
411
  //#endregion
268
412
  //#region src/WorkspaceDiscovery.d.ts
@@ -478,6 +622,7 @@ declare class WorkspaceDiscovery extends WorkspaceDiscovery_base {
478
622
  * path: "/repo/packages/utils",
479
623
  * packageJsonPath: "/repo/packages/utils/package.json",
480
624
  * relativePath: "packages/utils",
625
+ * workspaceRoot: "/repo",
481
626
  * }),
482
627
  * ]),
483
628
  * });
@@ -704,9 +849,13 @@ declare class ConfigDependencyHooks extends ConfigDependencyHooks_base {
704
849
  * `hooks`-source `CatalogAssemblyError`, never a silent skip.
705
850
  *
706
851
  * @remarks
707
- * Node-coupled by design the `node:fs` / `node:path` / `node:url` imports are
708
- * the sanctioned Node-only overlay, matching the other seams here. Only ever
709
- * wired by `WorkspaceCatalogs.layerWithConfigDependencies`.
852
+ * Runtime-coupled by design, not node-exclusive. The `import()` below loads
853
+ * **and executes** a config dependency's pnpmfile in-process code execution,
854
+ * not IO, so no `FileSystem` / `Path` service abstracts it. The `node:path` and
855
+ * `node:url` imports (`join`, `pathToFileURL`) exist only to build the URL that
856
+ * `import()` consumes; node and bun both implement those builtins and dynamic
857
+ * import, so this layer runs on either runtime. Only ever wired by
858
+ * `WorkspaceCatalogs.layerWithConfigDependencies`.
710
859
  */
711
860
  static readonly layerLive: Layer.Layer<ConfigDependencyHooks>;
712
861
  }
@@ -1782,5 +1931,5 @@ interface GetWorkspacePackagesSyncOptions extends WorkspacesSyncOptions {
1782
1931
  */
1783
1932
  declare const getWorkspacePackagesSync: (root: string, options: GetWorkspacePackagesSyncOptions) => ReadonlyArray<WorkspacePackage>;
1784
1933
  //#endregion
1785
- export { type CatalogAssemblyFailure, CatalogSet, ChangeDetectionError, type ChangeDetectionFailure, ChangeDetectionOptions, ChangeDetector, type ChangeDetectorShape, ConfigDependencyHooks, type ConfigDependencyHooksShape, CyclicDependencyError, type DependencyDiff, DependencyGraph, DetectedPackageManager, type GetWorkspacePackagesSyncOptions, LockfileReadError, type LockfileReadFailure, LockfileReader, type LockfileReaderOptions, type LockfileReaderShape, PackageManagerDetectionError, type PackageManagerDetectionFailure, PackageManagerDetector, PackageManagerName, PackageNotFoundError, PackageStateSnapshot, PublishConfig, PublishTarget, PublishabilityDetector, type PublishabilityDetectorShape, type SyncFileSystem, type SyncPath, WORKSPACE_MARKERS, WorkspaceCatalogs, type WorkspaceCatalogsOptions, type WorkspaceCatalogsShape, WorkspaceDiscovery, WorkspaceDiscoveryError, type WorkspaceDiscoveryFailure, type WorkspaceDiscoveryOptions, type WorkspaceDiscoveryShape, WorkspaceInfo, type WorkspaceLookupFailure, WorkspaceManifestError, WorkspacePackage, WorkspacePatternError, WorkspaceRoot, WorkspaceRootNotFoundError, type WorkspaceSnapshotAtFailure, type WorkspaceSnapshotWorktreeFailure, WorkspaceSnapshots, type WorkspaceSnapshotsOptions, type WorkspaceSnapshotsShape, WorkspaceStateSnapshot, Workspaces, type WorkspacesOptions, type WorkspacesServices, type WorkspacesSyncOptions, findWorkspaceRootSync, getWorkspacePackagesSync };
1934
+ export { type CatalogAssemblyFailure, CatalogSet, ChangeDetectionError, type ChangeDetectionFailure, ChangeDetectionOptions, ChangeDetector, type ChangeDetectorShape, ConfigDependencyHooks, type ConfigDependencyHooksShape, CyclicDependencyError, type DependencyDiff, DependencyGraph, DetectedPackageManager, type FindWorkspaceRootOptions, type GetWorkspacePackagesSyncOptions, LockfileReadError, type LockfileReadFailure, LockfileReader, type LockfileReaderOptions, type LockfileReaderShape, PackageManagerDetectionError, type PackageManagerDetectionFailure, PackageManagerDetector, PackageManagerName, PackageNotFoundError, PackageStateSnapshot, PublishConfig, PublishTarget, PublishabilityDetector, type PublishabilityDetectorShape, type SyncFileSystem, type SyncPath, WORKSPACE_MARKERS, WorkspaceCatalogs, type WorkspaceCatalogsOptions, type WorkspaceCatalogsShape, WorkspaceDiscovery, WorkspaceDiscoveryError, type WorkspaceDiscoveryFailure, type WorkspaceDiscoveryOptions, type WorkspaceDiscoveryShape, WorkspaceInfo, type WorkspaceLookupFailure, WorkspaceManifestError, WorkspacePackage, WorkspacePatternError, WorkspaceRoot, WorkspaceRootNotFoundError, type WorkspaceRootShape, type WorkspaceSnapshotAtFailure, type WorkspaceSnapshotWorktreeFailure, WorkspaceSnapshots, type WorkspaceSnapshotsOptions, type WorkspaceSnapshotsShape, WorkspaceStateSnapshot, Workspaces, type WorkspacesOptions, type WorkspacesServices, type WorkspacesSyncOptions, findWorkspaceRootSync, getWorkspacePackagesSync };
1786
1935
  //# sourceMappingURL=index.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/workspaces",
3
- "version": "0.4.1",
3
+ "version": "0.5.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,13 +47,13 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "@effected/git": "0.4.1",
50
- "@effected/glob": "0.1.2",
51
- "@effected/lockfiles": "0.1.5",
52
- "@effected/npm": "0.2.1",
53
- "@effected/package-json": "0.3.1",
54
- "@effected/semver": "0.1.1",
55
- "@effected/walker": "0.2.2",
56
- "@effected/yaml": "0.4.0",
50
+ "@effected/glob": "0.2.0",
51
+ "@effected/lockfiles": "0.1.7",
52
+ "@effected/npm": "0.2.2",
53
+ "@effected/package-json": "0.4.0",
54
+ "@effected/semver": "0.2.0",
55
+ "@effected/walker": "0.3.0",
56
+ "@effected/yaml": "0.5.0",
57
57
  "@pnpm/catalogs.config": "^1100.0.0",
58
58
  "@pnpm/catalogs.protocol-parser": "^1100.0.0",
59
59
  "@pnpm/catalogs.resolver": "^1100.0.0",