@effected/workspaces 0.28.0-next-20260927071821 → 0.29.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.
@@ -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-20260927071821",
3
+ "version": "0.29.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-20260927071821",
59
- "@effected/npm": "^0.17.0-next-20260927071821",
60
- "@effected/package-json": "^0.18.0-next-20260927071821",
58
+ "@effected/lockfiles": "^0.13.0",
59
+ "@effected/npm": "^0.18.0",
60
+ "@effected/package-json": "^0.18.1",
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
@@ -652,6 +652,11 @@ interface WorkspaceDiscoveryOptions {
652
652
  * directory the caller names, which a single layer-level ceiling cannot
653
653
  * sensibly bound, so they ascend unbounded as before.
654
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
+ *
655
660
  * @defaultValue no ceiling — the ascent runs to the filesystem root.
656
661
  */
657
662
  readonly stopAt?: string | undefined;