@effected/workspaces 0.28.0-next-20260927062352 → 0.28.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.
@@ -482,7 +482,19 @@ var ConfigDependencyHooks = class ConfigDependencyHooks extends Context.Service(
482
482
  * with `reason: "integrityUnavailable"` — nothing is fetched in either case
483
483
  * — and a fetch that fails with `reason: "fetchFailed"`, keeping the
484
484
  * not-installed remediation. The fetch needs `pnpm` 11 or 12 on `PATH` and
485
- * registry access, and is bounded at two minutes. An empty `configDependencies`, or one whose dependencies all
485
+ * registry access, and is bounded at two minutes.
486
+ *
487
+ * The scratch workspace fetches through the same registries a
488
+ * `pnpm install` in the workspace would: the workspace root's `.npmrc` (scoped
489
+ * registries, mirrors, auth) is copied into it as-is, with `${VAR}`
490
+ * references left for pnpm to expand, never read or logged, and removed
491
+ * with the scratch; and the root `pnpm-workspace.yaml`'s `registry` and
492
+ * `registries` keys are carried into the scratch's. That is the CURRENT
493
+ * checkout's registry config for both sides of a diff — a base ref's
494
+ * `.npmrc` is not read through git — and relative paths inside the `.npmrc`
495
+ * (a `cafile=./ca.pem`) resolve against the scratch, not the root.
496
+ *
497
+ * An empty `configDependencies`, or one whose dependencies all
486
498
  * ship no pnpmfile, returns the seed without spawning anything; a `..` path
487
499
  * segment in a dependency name fails typed **before** any spawn; a
488
500
  * load failure in the child — a syntax error, a throwing top level, an
package/LockfileReader.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { findLayerRoot } from "./internal/layerRoot.js";
1
2
  import { WorkspaceRoot } from "./WorkspaceRoot.js";
2
3
  import { WorkspaceDiscovery } from "./WorkspaceDiscovery.js";
3
4
  import { PackageManagerDetector } from "./PackageManagerName.js";
@@ -60,7 +61,7 @@ var LockfileReader = class LockfileReader extends Context.Service()("@effected/w
60
61
  const fs = yield* FileSystem.FileSystem;
61
62
  const path = yield* Path.Path;
62
63
  const init = Effect.gen(function* () {
63
- const root = yield* Effect.suspend(() => roots.find(options?.cwd ?? process.cwd()));
64
+ const root = yield* findLayerRoot(roots, options);
64
65
  const format = (yield* detector.detect(root)).name;
65
66
  const lockfilePath = path.join(root, filenameFor(format));
66
67
  const content = yield* fs.readFileString(lockfilePath).pipe(Effect.mapError((cause) => new LockfileReadError({
@@ -1,3 +1,4 @@
1
+ import { findLayerRoot } from "./internal/layerRoot.js";
1
2
  import { WorkspaceRoot } from "./WorkspaceRoot.js";
2
3
  import { inlineCatalogs, merge, normalize, rangeOf } from "./internal/catalogs.js";
3
4
  import { ConfigDependencyHooks, NoPeerDependencyRules } from "./ConfigDependencyHooks.js";
@@ -342,7 +343,7 @@ var WorkspaceCatalogs = class WorkspaceCatalogs extends Context.Service()("@effe
342
343
  }));
343
344
  }));
344
345
  const assemble = Effect.gen(function* () {
345
- const root = yield* Effect.suspend(() => roots.find(options?.cwd ?? process.cwd()));
346
+ const root = yield* findLayerRoot(roots, options);
346
347
  const lockfileOutputs = yield* lockfiles.read().pipe(Effect.map((lockfile) => ({
347
348
  catalogs: CatalogSet.fromLockfile(lockfile),
348
349
  importerVersions: importerVersionsOf(lockfile)
@@ -1,4 +1,5 @@
1
1
  import { enumerate } from "./internal/enumerate.js";
2
+ import { findLayerRoot } from "./internal/layerRoot.js";
2
3
  import { readPatterns } from "./internal/patterns.js";
3
4
  import { WorkspacePackage } from "./WorkspacePackage.js";
4
5
  import { WorkspaceRoot } from "./WorkspaceRoot.js";
@@ -256,7 +257,7 @@ var WorkspaceDiscovery = class WorkspaceDiscovery extends Context.Service()("@ef
256
257
  };
257
258
  }).pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path));
258
259
  const discover = Effect.gen(function* () {
259
- const root = yield* Effect.suspend(() => roots.find(options?.cwd ?? process.cwd()));
260
+ const root = yield* findLayerRoot(roots, options);
260
261
  return yield* discoverAt(root);
261
262
  });
262
263
  const [resolveOnce, invalidate] = yield* Effect.cachedInvalidateWithTTL(discover, Duration.infinity);
package/WorkspaceRoot.js CHANGED
@@ -8,6 +8,8 @@ import { Walker } from "@effected/walker";
8
8
  * @public
9
9
  */
10
10
  const WORKSPACE_MARKERS = ["pnpm-workspace.yaml", "package.json"];
11
+ /** How the not-found message names the `package.json` marker: only one with a `workspaces` field counts. */
12
+ const PACKAGE_JSON_MARKER_PROSE = "package.json with a \"workspaces\" field";
11
13
  /**
12
14
  * Raised when no workspace root can be found by ascending from a directory.
13
15
  *
@@ -36,7 +38,8 @@ var WorkspaceRootNotFoundError = class extends Schema.TaggedError()("WorkspaceRo
36
38
  /** Renders the search path, probed markers and any ceiling into a one-line message. */
37
39
  get message() {
38
40
  const bound = this.stopAt === void 0 ? "" : ` up to ${this.stopAt}`;
39
- return `No workspace root above ${this.searchPath}${bound} (looked for ${this.markers.join(", ")})`;
41
+ const looked = this.markers.map((marker) => marker === "package.json" ? PACKAGE_JSON_MARKER_PROSE : marker);
42
+ return `No workspace root above ${this.searchPath}${bound} (looked for ${looked.join(", ")})`;
40
43
  }
41
44
  };
42
45
  /**
@@ -1,3 +1,4 @@
1
+ import { findLayerRoot } from "./internal/layerRoot.js";
1
2
  import { manifestPatternsOf, pnpmPatternsOf } from "./internal/patterns.js";
2
3
  import { WorkspaceRoot } from "./WorkspaceRoot.js";
3
4
  import { WorkspaceDiscovery } from "./WorkspaceDiscovery.js";
@@ -210,7 +211,7 @@ var WorkspaceSnapshots = class WorkspaceSnapshots extends Context.Service()("@ef
210
211
  const atCaches = /* @__PURE__ */ new Map();
211
212
  return {
212
213
  at: Effect.fn("WorkspaceSnapshots.at")(function* (ref) {
213
- const root = yield* Effect.suspend(() => roots.find(options?.cwd ?? process.cwd()));
214
+ const root = yield* findLayerRoot(roots, options);
214
215
  const key = `${root}\0${ref}`;
215
216
  let memo = atCaches.get(key);
216
217
  if (memo === void 0) {
package/Workspaces.js CHANGED
@@ -48,7 +48,8 @@ const localExecLayer = (options) => Layer.effect(LocalExec, Effect.gen(function*
48
48
  const detector = yield* PackageManagerDetector;
49
49
  return { context: Effect.gen(function* () {
50
50
  const cwd = options?.cwd ?? globalThis.process?.cwd?.() ?? "/";
51
- const root = yield* roots.find(cwd).pipe(Effect.asSome, Effect.orElseSucceed(Option.none));
51
+ const stopAt = options?.stopAt;
52
+ const root = yield* roots.find(cwd, stopAt === void 0 ? void 0 : { stopAt }).pipe(Effect.asSome, Effect.orElseSucceed(Option.none));
52
53
  if (Option.isNone(root)) return Option.none();
53
54
  const detected = yield* detector.detect(root.value).pipe(Effect.asSome, Effect.catchTag("PackageManagerDetectionError", () => Effect.succeed(Option.none())), Effect.mapError((cause) => new LocalExecError({
54
55
  directory: root.value,
@@ -289,7 +290,10 @@ var Workspaces = class {
289
290
  * verbatim.
290
291
  *
291
292
  * `directory` is the resolved **workspace root**, not the caller's cwd: a
292
- * project-local launcher has to run where the workspace is.
293
+ * project-local launcher has to run where the workspace is. `stopAt` caps
294
+ * that ascent exactly as {@link WorkspacesOptions.stopAt} does, and a
295
+ * ceiling with no root at or below it is the same `Option.none()` as no
296
+ * root at all.
293
297
  *
294
298
  * A consumer with no monorepo never needs this layer, and therefore never
295
299
  * installs this package — `LocalExec.layerNone` and `LocalExec.layerFor`
package/index.d.ts CHANGED
@@ -541,6 +541,33 @@ interface WorkspaceDiscoveryOptions {
541
541
  * honoured.
542
542
  */
543
543
  readonly cwd?: string;
544
+ /**
545
+ * A ceiling for the layer-bound root ascent from `cwd`, passed straight
546
+ * through to the `stopAt` of {@link WorkspaceRoot}'s `find`.
547
+ *
548
+ * @remarks
549
+ * Inclusive: the ceiling itself is probed, so a `cwd` that is its own
550
+ * workspace root still resolves with `stopAt: cwd`. A relative ceiling is
551
+ * resolved against the process working directory, exactly as `cwd` is. When
552
+ * no root is found at or below the ceiling, the methods fail with
553
+ * {@link WorkspaceRootNotFoundError} carrying the resolved `stopAt`, rather
554
+ * than adopting an enclosing directory's workspace — pass `stopAt: cwd` for
555
+ * a checkout nested inside someone else's monorepo.
556
+ *
557
+ * Applies ONLY to the layer-bound methods (`info`, `listPackages`,
558
+ * `importerMap`, `getPackage`, `resolveFile`, `resolveFiles`). The per-call
559
+ * `infoIn`, `listPackagesIn` and `refreshIn` resolve from an arbitrary
560
+ * directory the caller names, which a single layer-level ceiling cannot
561
+ * sensibly bound, so they ascend unbounded as before.
562
+ *
563
+ * `LockfileReader`, `WorkspaceCatalogs` and `WorkspaceSnapshots` each
564
+ * take the same option; give them the same
565
+ * value when wiring by hand, or let a `Workspaces.*` composite forward one
566
+ * `stopAt` to all of them, so no service adopts a root another refused.
567
+ *
568
+ * @defaultValue no ceiling — the ascent runs to the filesystem root.
569
+ */
570
+ readonly stopAt?: string | undefined;
544
571
  /** Descent cap for segment-crossing patterns. Defaults to 32. */
545
572
  readonly maxDepth?: number;
546
573
  }
@@ -1230,7 +1257,19 @@ export declare class ConfigDependencyHooks extends ConfigDependencyHooks_base {
1230
1257
  * with `reason: "integrityUnavailable"` — nothing is fetched in either case
1231
1258
  * — and a fetch that fails with `reason: "fetchFailed"`, keeping the
1232
1259
  * not-installed remediation. The fetch needs `pnpm` 11 or 12 on `PATH` and
1233
- * registry access, and is bounded at two minutes. An empty `configDependencies`, or one whose dependencies all
1260
+ * registry access, and is bounded at two minutes.
1261
+ *
1262
+ * The scratch workspace fetches through the same registries a
1263
+ * `pnpm install` in the workspace would: the workspace root's `.npmrc` (scoped
1264
+ * registries, mirrors, auth) is copied into it as-is, with `${VAR}`
1265
+ * references left for pnpm to expand, never read or logged, and removed
1266
+ * with the scratch; and the root `pnpm-workspace.yaml`'s `registry` and
1267
+ * `registries` keys are carried into the scratch's. That is the CURRENT
1268
+ * checkout's registry config for both sides of a diff — a base ref's
1269
+ * `.npmrc` is not read through git — and relative paths inside the `.npmrc`
1270
+ * (a `cafile=./ca.pem`) resolve against the scratch, not the root.
1271
+ *
1272
+ * An empty `configDependencies`, or one whose dependencies all
1234
1273
  * ship no pnpmfile, returns the seed without spawning anything; a `..` path
1235
1274
  * segment in a dependency name fails typed **before** any spawn; a
1236
1275
  * load failure in the child — a syntax error, a throwing top level, an
@@ -2032,6 +2071,21 @@ interface LockfileReaderOptions {
2032
2071
  * @defaultValue `process.cwd()`, read lazily on first use.
2033
2072
  */
2034
2073
  readonly cwd?: string;
2074
+ /**
2075
+ * A ceiling for the root ascent from `cwd`, passed straight through to the
2076
+ * `stopAt` of {@link WorkspaceRoot}'s `find`.
2077
+ *
2078
+ * @remarks
2079
+ * Inclusive, and resolved to an absolute path exactly as `cwd` is. When no
2080
+ * root is found at or below the ceiling, `read` fails with
2081
+ * {@link WorkspaceRootNotFoundError} carrying the resolved `stopAt` rather
2082
+ * than adopting an enclosing directory's workspace. Pass the same value as
2083
+ * {@link WorkspaceDiscoveryOptions.stopAt} so every service agrees on the
2084
+ * root; the `Workspaces.*` composites forward one `stopAt` to all of them.
2085
+ *
2086
+ * @defaultValue no ceiling — the ascent runs to the filesystem root.
2087
+ */
2088
+ readonly stopAt?: string | undefined;
2035
2089
  }
2036
2090
  declare const LockfileReader_base: Context.ServiceClass<LockfileReader, "@effected/workspaces/LockfileReader", LockfileReaderShape>;
2037
2091
  /**
@@ -3109,6 +3163,21 @@ interface WorkspaceCatalogsOptions {
3109
3163
  * @defaultValue `process.cwd()`, read lazily on first use.
3110
3164
  */
3111
3165
  readonly cwd?: string;
3166
+ /**
3167
+ * A ceiling for the root ascent from `cwd`, passed straight through to the
3168
+ * `stopAt` of {@link WorkspaceRoot}'s `find`.
3169
+ *
3170
+ * @remarks
3171
+ * Inclusive, and resolved to an absolute path exactly as `cwd` is. When no
3172
+ * root is found at or below the ceiling, catalog assembly fails with
3173
+ * {@link WorkspaceRootNotFoundError} carrying the resolved `stopAt` rather
3174
+ * than adopting an enclosing directory's workspace. Pass the same value as
3175
+ * {@link WorkspaceDiscoveryOptions.stopAt} so every service agrees on the
3176
+ * root; the `Workspaces.*` composites forward one `stopAt` to all of them.
3177
+ *
3178
+ * @defaultValue no ceiling — the ascent runs to the filesystem root.
3179
+ */
3180
+ readonly stopAt?: string | undefined;
3112
3181
  }
3113
3182
  declare const WorkspaceCatalogs_base: Context.ServiceClass<WorkspaceCatalogs, "@effected/workspaces/WorkspaceCatalogs", WorkspaceCatalogsShape>;
3114
3183
  /**
@@ -3662,6 +3731,21 @@ interface WorkspaceSnapshotsOptions {
3662
3731
  * first call is honoured.
3663
3732
  */
3664
3733
  readonly cwd?: string;
3734
+ /**
3735
+ * A ceiling for the root ascent from `cwd`, passed straight through to the
3736
+ * `stopAt` of {@link WorkspaceRoot}'s `find`.
3737
+ *
3738
+ * @remarks
3739
+ * Inclusive, and resolved to an absolute path exactly as `cwd` is. When no
3740
+ * root is found at or below the ceiling, `at(ref)` fails with
3741
+ * {@link WorkspaceRootNotFoundError} carrying the resolved `stopAt` rather
3742
+ * than adopting an enclosing directory's workspace. Pass the same value as
3743
+ * {@link WorkspaceDiscoveryOptions.stopAt} so every service agrees on the
3744
+ * root; the `Workspaces.*` composites forward one `stopAt` to all of them.
3745
+ *
3746
+ * @defaultValue no ceiling — the ascent runs to the filesystem root.
3747
+ */
3748
+ readonly stopAt?: string | undefined;
3665
3749
  /**
3666
3750
  * Catalogs every snapshot this service produces carries as its
3667
3751
  * `seededCatalogs` — consulted only where the snapshot's own catalogs cannot
@@ -3829,9 +3913,17 @@ export declare class WorkspaceSnapshots extends WorkspaceSnapshots_base {
3829
3913
  /**
3830
3914
  * Options shared by the composite layers.
3831
3915
  *
3916
+ * @remarks
3917
+ * Derived from the option shapes of the services a composite builds —
3918
+ * {@link WorkspaceDiscoveryOptions}, {@link LockfileReaderOptions} and
3919
+ * {@link WorkspaceCatalogsOptions} — and handed to each of them unchanged, so a
3920
+ * root-resolving option one service grows cannot silently stop at the
3921
+ * composite. `cwd` and `stopAt` are re-declared here only to document them as
3922
+ * the one concern they are across the composite.
3923
+ *
3832
3924
  * @public
3833
3925
  */
3834
- interface WorkspacesOptions {
3926
+ interface WorkspacesOptions extends WorkspaceDiscoveryOptions, LockfileReaderOptions, WorkspaceCatalogsOptions {
3835
3927
  /**
3836
3928
  * The directory every root-consuming service resolves the workspace root
3837
3929
  * from — one explicit concern, applied uniformly.
@@ -3839,8 +3931,23 @@ interface WorkspacesOptions {
3839
3931
  * @defaultValue `process.cwd()`, read lazily on first use.
3840
3932
  */
3841
3933
  readonly cwd?: string;
3842
- /** Descent cap for segment-crossing `packages:` patterns. Defaults to 32. */
3843
- readonly maxDepth?: number;
3934
+ /**
3935
+ * A ceiling for the root ascent from `cwd`, forwarded to EVERY
3936
+ * root-resolving service the composite builds — {@link WorkspaceDiscovery},
3937
+ * {@link LockfileReader}, {@link WorkspaceCatalogs} and, on the git
3938
+ * composites, {@link WorkspaceSnapshots}.
3939
+ *
3940
+ * @remarks
3941
+ * Inclusive and resolved to an absolute path, as {@link WorkspaceRoot}'s
3942
+ * `find` does. Pass `stopAt: cwd` for a checkout nested inside someone
3943
+ * else's workspace: every service then fails with
3944
+ * {@link WorkspaceRootNotFoundError} consistently, instead of discovery
3945
+ * refusing the enclosing workspace while the lockfile and catalog reads
3946
+ * adopt it. A checkout that is itself a workspace root still resolves.
3947
+ *
3948
+ * @defaultValue no ceiling — the ascent runs to the filesystem root.
3949
+ */
3950
+ readonly stopAt?: string | undefined;
3844
3951
  }
3845
3952
  /**
3846
3953
  * Options for the composites that additionally provide {@link WorkspaceSnapshots}
@@ -4086,7 +4193,10 @@ export declare class Workspaces {
4086
4193
  * verbatim.
4087
4194
  *
4088
4195
  * `directory` is the resolved **workspace root**, not the caller's cwd: a
4089
- * project-local launcher has to run where the workspace is.
4196
+ * project-local launcher has to run where the workspace is. `stopAt` caps
4197
+ * that ascent exactly as {@link WorkspacesOptions.stopAt} does, and a
4198
+ * ceiling with no root at or below it is the same `Option.none()` as no
4199
+ * root at all.
4090
4200
  *
4091
4201
  * A consumer with no monorepo never needs this layer, and therefore never
4092
4202
  * installs this package — `LocalExec.layerNone` and `LocalExec.layerFor`
@@ -4115,6 +4225,7 @@ export declare class Workspaces {
4115
4225
  */
4116
4226
  static readonly localExecLayer: (options?: {
4117
4227
  readonly cwd?: string;
4228
+ readonly stopAt?: string | undefined;
4118
4229
  }) => Layer.Layer<LocalExec, never, PackageManagerDetector | WorkspaceRoot>;
4119
4230
  /**
4120
4231
  * Resolve every `catalog:` and `workspace:` specifier in one `Manifest`
@@ -1,10 +1,11 @@
1
1
  import { carries, manifestVersion, messageOf, sideLabel } from "./configDependencyShared.js";
2
2
  import { splitConfigDependencySpec } from "./configDependencySpecGrammar.js";
3
- import { Duration, Effect } from "effect";
3
+ import { Duration, Effect, Predicate } from "effect";
4
4
  import { IntegrityHash } from "@effected/npm";
5
+ import { Yaml } from "@effected/yaml";
5
6
  import { Run } from "@effected/commands";
6
7
  import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
7
- import { mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
8
+ import { copyFile, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
8
9
  import { tmpdir } from "node:os";
9
10
  import { basename, dirname, join } from "node:path";
10
11
 
@@ -47,12 +48,36 @@ const expectedIntegrity = (request) => Effect.gen(function* () {
47
48
  if (integrity === void 0) return yield* Effect.fail(fetchFailure("integrityUnavailable", `config dependency ${key} must be fetched, but ${sideLabel(side)} records no integrity for it (no inline integrity in pnpm-workspace.yaml and no pnpm-lock.yaml entry for that version), so a fetched copy could not be verified and nothing was fetched.`));
48
49
  return integrity;
49
50
  });
51
+ /** The {@link RegistrySettings} of a parsed `pnpm-workspace.yaml` document. */
52
+ const registrySettingsOf = (document) => {
53
+ if (!Predicate.isObject(document)) return { registries: {} };
54
+ const registries = {};
55
+ if (Predicate.isObject(document.registries)) {
56
+ for (const [scope, url] of Object.entries(document.registries)) if (typeof url === "string") registries[scope] = url;
57
+ }
58
+ return typeof document.registry === "string" ? {
59
+ registry: document.registry,
60
+ registries
61
+ } : { registries };
62
+ };
50
63
  /**
51
- * The scratch workspace's `pnpm-workspace.yaml`. Every scalar is a
64
+ * The scratch workspace's `pnpm-workspace.yaml`: the one config dependency,
65
+ * plus the declaring workspace's registry settings. Every key and scalar is a
52
66
  * JSON-quoted string, which YAML reads verbatim, so no declared text can
53
67
  * change the document's structure.
54
68
  */
55
- const scratchWorkspaceYaml = (name, version) => `configDependencies:\n ${JSON.stringify(name)}: ${JSON.stringify(version)}\n`;
69
+ const scratchWorkspaceYaml = (name, version, settings = { registries: {} }) => {
70
+ const lines = ["configDependencies:", ` ${JSON.stringify(name)}: ${JSON.stringify(version)}`];
71
+ if (settings.registry !== void 0) lines.push(`registry: ${JSON.stringify(settings.registry)}`);
72
+ const scopes = Object.entries(settings.registries);
73
+ if (scopes.length > 0) {
74
+ lines.push("registries:");
75
+ for (const [scope, url] of scopes) lines.push(` ${JSON.stringify(scope)}: ${JSON.stringify(url)}`);
76
+ }
77
+ return `${lines.join("\n")}\n`;
78
+ };
79
+ /** Whether `error` is a node filesystem "no such file" failure. */
80
+ const isNotFound = (error) => Predicate.isObject(error) && error.code === "ENOENT";
56
81
  /**
57
82
  * The scratch workspace's `pnpm-lock.yaml`: the env preamble pnpm 11 and 12
58
83
  * write for a lone config dependency, pinned to `integrity`, followed by the
@@ -123,14 +148,22 @@ const makeFetchConfigDependency = (spawner) => (request) => Effect.scoped(Effect
123
148
  catch: (cause) => failed(`${what} failed: ${messageOf(cause)}`, cause)
124
149
  });
125
150
  const integrity = yield* expectedIntegrity(request);
151
+ const rootYaml = yield* io("reading the workspace pnpm-workspace.yaml", () => readFile(join(request.root, "pnpm-workspace.yaml"), "utf8").catch((error) => {
152
+ if (isNotFound(error)) return void 0;
153
+ throw error;
154
+ }));
155
+ const settings = rootYaml === void 0 ? registrySettingsOf(void 0) : registrySettingsOf(yield* Yaml.parse(rootYaml).pipe(Effect.mapError((cause) => failed(`reading the workspace pnpm-workspace.yaml failed: ${messageOf(cause)}`, cause))));
126
156
  const scratch = yield* Effect.acquireRelease(io("creating a scratch workspace", () => mkdtemp(join(tmpdir(), "effected-config-dependency-"))), (dir) => Effect.promise(() => rm(dir, {
127
157
  recursive: true,
128
158
  force: true
129
159
  }).catch(() => void 0)));
130
160
  const lockfile = scratchLockfile(name, version, integrity);
131
161
  yield* io("writing the scratch workspace", async () => {
132
- await writeFile(join(scratch, "pnpm-workspace.yaml"), scratchWorkspaceYaml(name, version));
162
+ await writeFile(join(scratch, "pnpm-workspace.yaml"), scratchWorkspaceYaml(name, version, settings));
133
163
  await writeFile(join(scratch, "pnpm-lock.yaml"), lockfile);
164
+ await copyFile(join(request.root, ".npmrc"), join(scratch, ".npmrc")).catch((error) => {
165
+ if (!isNotFound(error)) throw error;
166
+ });
134
167
  });
135
168
  const command = ChildProcess.make("pnpm", fetchArgs(scratch, request.stores), { cwd: scratch });
136
169
  yield* Run.text(command, { timeout: FETCH_TIMEOUT }).pipe(Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), Effect.mapError((cause) => failed(`fetching ${key} with \`pnpm ${fetchArgs("<scratch>", request.stores).join(" ")}\` failed: ${messageOf(cause)}`, cause)));
@@ -143,4 +176,4 @@ const makeFetchConfigDependency = (spawner) => (request) => Effect.scoped(Effect
143
176
  }));
144
177
 
145
178
  //#endregion
146
- export { expectedIntegrity, fetchArgs, makeFetchConfigDependency, storeDirArgument };
179
+ export { expectedIntegrity, fetchArgs, makeFetchConfigDependency, registrySettingsOf, scratchWorkspaceYaml, storeDirArgument };
@@ -266,6 +266,7 @@ const resolveDirectory = (root, entry, stores, locks, options) => Effect.gen(fun
266
266
  if (options.fetch === void 0) return yield* Effect.fail(notInstalled(Option.none()));
267
267
  return {
268
268
  dir: yield* options.fetch({
269
+ root,
269
270
  name,
270
271
  version: declared,
271
272
  spec: entry.spec,
@@ -0,0 +1,14 @@
1
+ import { Effect } from "effect";
2
+
3
+ //#region src/internal/layerRoot.ts
4
+ /**
5
+ * Resolve the workspace root a layer's options name. `Effect.suspend` so the
6
+ * ambient `process.cwd()` is read at first use, not at layer construction.
7
+ */
8
+ const findLayerRoot = (roots, options) => Effect.suspend(() => {
9
+ const stopAt = options?.stopAt;
10
+ return roots.find(options?.cwd ?? process.cwd(), stopAt === void 0 ? void 0 : { stopAt });
11
+ });
12
+
13
+ //#endregion
14
+ export { findLayerRoot };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/workspaces",
3
- "version": "0.28.0-next-20260927062352",
3
+ "version": "0.28.0",
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": [
@@ -55,9 +55,9 @@
55
55
  "@effected/git": "^0.17.0",
56
56
  "@effected/glob": "^0.8.0",
57
57
  "@effected/jsonc": "^0.13.0",
58
- "@effected/lockfiles": "^0.12.0-next-20260927062352",
59
- "@effected/npm": "^0.17.0-next-20260927062352",
60
- "@effected/package-json": "^0.18.0-next-20260927062352",
58
+ "@effected/lockfiles": "^0.12.0",
59
+ "@effected/npm": "^0.17.0",
60
+ "@effected/package-json": "^0.18.0",
61
61
  "@effected/semver": "^0.9.0",
62
62
  "@effected/walker": "^0.12.0",
63
63
  "@effected/yaml": "^0.17.0",
package/testing.d.ts CHANGED
@@ -633,6 +633,33 @@ interface WorkspaceDiscoveryOptions {
633
633
  * honoured.
634
634
  */
635
635
  readonly cwd?: string;
636
+ /**
637
+ * A ceiling for the layer-bound root ascent from `cwd`, passed straight
638
+ * through to the `stopAt` of {@link WorkspaceRoot}'s `find`.
639
+ *
640
+ * @remarks
641
+ * Inclusive: the ceiling itself is probed, so a `cwd` that is its own
642
+ * workspace root still resolves with `stopAt: cwd`. A relative ceiling is
643
+ * resolved against the process working directory, exactly as `cwd` is. When
644
+ * no root is found at or below the ceiling, the methods fail with
645
+ * {@link WorkspaceRootNotFoundError} carrying the resolved `stopAt`, rather
646
+ * than adopting an enclosing directory's workspace — pass `stopAt: cwd` for
647
+ * a checkout nested inside someone else's monorepo.
648
+ *
649
+ * Applies ONLY to the layer-bound methods (`info`, `listPackages`,
650
+ * `importerMap`, `getPackage`, `resolveFile`, `resolveFiles`). The per-call
651
+ * `infoIn`, `listPackagesIn` and `refreshIn` resolve from an arbitrary
652
+ * directory the caller names, which a single layer-level ceiling cannot
653
+ * sensibly bound, so they ascend unbounded as before.
654
+ *
655
+ * `LockfileReader`, `WorkspaceCatalogs` and `WorkspaceSnapshots` each
656
+ * take the same option; give them the same
657
+ * value when wiring by hand, or let a `Workspaces.*` composite forward one
658
+ * `stopAt` to all of them, so no service adopts a root another refused.
659
+ *
660
+ * @defaultValue no ceiling — the ascent runs to the filesystem root.
661
+ */
662
+ readonly stopAt?: string | undefined;
636
663
  /** Descent cap for segment-crossing patterns. Defaults to 32. */
637
664
  readonly maxDepth?: number;
638
665
  }