@effected/workspaces 0.18.3 → 0.19.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/README.md CHANGED
@@ -193,7 +193,7 @@ const packages = root === null ? [] : getWorkspacePackagesSync(root, nodeSyncOps
193
193
  // packages: the discovered workspace packages, empty when there is no root
194
194
  ```
195
195
 
196
- Both entry points take their path positionally, so the bag usually passes through verbatim; spread it to add `getWorkspacePackagesSync`'s traversal extras — `{ ...nodeSyncOps, maxDepth }`. The bindings are a separate subpath deliberately: the main entry imports nothing platform-shaped, and re-exporting them from it would drag `node:*` into every consumer, including the ones supplying their own operations. `nodePath` is the running platform's `node:path`, so on Windows the paths handed back are win32 paths.
196
+ Both entry points take their path positionally, so the bag usually passes through verbatim; spread it to add `getWorkspacePackagesSync`'s traversal extras — `{ ...nodeSyncOps, maxDepth, onSkip }`. The function is total, so it cannot fail on a manifest it cannot use — but it never drops one silently either: `onSkip` receives a `WorkspaceDiscoverySkip` (`root`, `path`, `kind`, `cause`) for every manifest left out, with the same `kind` the Effect surface would have failed with, so "no packages" and "packages rejected" stay distinguishable. A manifest with no `version` is not skipped on either surface; it is a member with `version` absent, as pnpm treats it. The bindings are a separate subpath deliberately: the main entry imports nothing platform-shaped, and re-exporting them from it would drag `node:*` into every consumer, including the ones supplying their own operations. `nodePath` is the running platform's `node:path`, so on Windows the paths handed back are win32 paths.
197
197
 
198
198
  Write the operations yourself when Node's built-ins are not the platform you mean — a Bun or Deno binding, a test fake, or `node:path/win32` to pin a dialect rather than follow the running platform. Each one is a one-liner:
199
199
 
@@ -293,7 +293,7 @@ A name miss in the derived `getPackage` fails with the service's own typed `Pack
293
293
  - `Workspaces.resolverLayer` / `Workspaces.resolveManifest` — the one-call manifest-resolution path: a fresh, unmemoized layer per call so root discovery follows your cwd, and one-shot resolution of a whole `Manifest` against the real workspace.
294
294
  - `WorkspaceRoot` — root discovery from a `cwd`, over `WORKSPACE_MARKERS`.
295
295
  - `WorkspaceDiscovery` — package enumeration with a bounded descent for segment-crossing `packages/**` patterns, per-package lookup and the `makeTest` / `layerTest` in-memory test doubles.
296
- - `WorkspacePackage` — a deliberately tolerant manifest model, so one member with an odd version cannot fail discovery for the whole repo. `manifestRecord` keeps the as-read `package.json` for tolerant access to fields outside the typed slice without a second read; `WorkspacePackage.manifest(pkg)` re-reads and is the opt-in bridge to `@effected/package-json`'s strict `Package`.
296
+ - `WorkspacePackage` — a deliberately tolerant manifest model, so one member with an odd version cannot fail discovery for the whole repo. `version` is optional and carried exactly as the manifest has it — absent for the ordinary version-less private root or member, never a `"0.0.0"` placeholder; a `version` that is present but not a string is `invalidShape`. `manifestRecord` keeps the as-read `package.json` for tolerant access to fields outside the typed slice without a second read; `WorkspacePackage.manifest(pkg)` re-reads and is the opt-in bridge to `@effected/package-json`'s strict `Package`.
297
297
  - `DependencyGraph` — a value class over discovered packages: `levels()` for parallel build tiers, the flattened topological order, `toMermaid()` for a deterministic Mermaid `flowchart TD` of the whole graph, and `CyclicDependencyError` — naming the cycle's actual members — when there is no order.
298
298
  - `PackageManagerDetector` — npm, pnpm, yarn or bun from lockfiles and the `packageManager` field.
299
299
  - `WorkspaceCatalogs` — pnpm catalog assembly and `catalog:` resolution, on pnpm's own catalog packages; `releaseAgeGate()` assembles the effective `@effected/npm` `ReleaseAgeGate` from inline `pnpm-workspace.yaml` release-age keys and replayed hook contributions, strictest-wins, in the same pass as the catalogs.
@@ -9,12 +9,20 @@ import { DependencyResolutionError, WorkspaceResolver } from "@effected/npm";
9
9
  //#region src/WorkspaceDiscovery.ts
10
10
  /**
11
11
  * Raised when a workspace member's `package.json` cannot be read, parsed, or
12
- * used — it is missing, malformed, or lacks a `name` or `version`.
12
+ * used — it is missing, malformed, or lacks a `name`.
13
13
  *
14
14
  * @remarks
15
15
  * `kind` is the discriminant a caller branches on; `cause` preserves the
16
16
  * originating failure rather than flattening it into a sentence.
17
17
  *
18
+ * A manifest with no `version` is NOT a failure: pnpm accepts a version-less
19
+ * private package and a private monorepo root without one is the ordinary
20
+ * shape, so the member is discovered with `WorkspacePackage.version` absent.
21
+ * The former `missingVersion` kind is retired. Only ABSENCE is tolerated: a
22
+ * `version` that is present but not a string — or present and `""`, which pnpm
23
+ * never wrote and which would resolve `workspace:^` to a bare `"^"` — is the
24
+ * manifest's shape being wrong and reports `invalidShape`.
25
+ *
18
26
  * @public
19
27
  */
20
28
  var WorkspaceDiscoveryError = class extends Schema.TaggedError()("WorkspaceDiscoveryError", {
@@ -28,8 +36,7 @@ var WorkspaceDiscoveryError = class extends Schema.TaggedError()("WorkspaceDisco
28
36
  "invalidJson",
29
37
  "invalidShape",
30
38
  "invalidYaml",
31
- "missingName",
32
- "missingVersion"
39
+ "missingName"
33
40
  ]),
34
41
  /** The originating failure, if there was one. */
35
42
  cause: Schema.Defect()
@@ -172,15 +179,21 @@ var WorkspaceDiscovery = class WorkspaceDiscovery extends Context.Service()("@ef
172
179
  cause: void 0
173
180
  }));
174
181
  const version = raw.version;
175
- if (typeof version !== "string" || version.length === 0) return yield* Effect.fail(new WorkspaceDiscoveryError({
182
+ if (version !== void 0 && typeof version !== "string") return yield* Effect.fail(new WorkspaceDiscoveryError({
176
183
  root,
177
184
  path: packageJsonPath,
178
- kind: "missingVersion",
179
- cause: void 0
185
+ kind: "invalidShape",
186
+ cause: /* @__PURE__ */ new Error(`version must be a string, got ${typeof version}`)
187
+ }));
188
+ if (version === "") return yield* Effect.fail(new WorkspaceDiscoveryError({
189
+ root,
190
+ path: packageJsonPath,
191
+ kind: "invalidShape",
192
+ cause: /* @__PURE__ */ new Error("version must be a non-empty string")
180
193
  }));
181
194
  return yield* Schema.decodeUnknownEffect(WorkspacePackage)({
182
195
  name,
183
- version,
196
+ ...version !== void 0 ? { version } : {},
184
197
  path: directory,
185
198
  packageJsonPath,
186
199
  relativePath,
@@ -494,6 +507,12 @@ var WorkspaceDiscovery = class WorkspaceDiscovery extends Context.Service()("@ef
494
507
  * channel is reserved for a failure of the resolution *mechanism* (an
495
508
  * unfindable root, an unreadable manifest), never an ordinary miss.
496
509
  *
510
+ * A member that declares **no `version`** is neither: it is a known member
511
+ * with nothing for `workspace:` to resolve to. Answering `none` would read
512
+ * as "not a member" downstream, so it fails typed instead, naming the
513
+ * specifier — the same channel a consumer already handles for an
514
+ * unresolvable workspace.
515
+ *
497
516
  * @example
498
517
  * ```ts
499
518
  * import { Package } from "@effected/package-json";
@@ -517,10 +536,21 @@ var WorkspaceDiscovery = class WorkspaceDiscovery extends Context.Service()("@ef
517
536
  versionIndexes.set(all, index);
518
537
  return index;
519
538
  };
520
- return { versionOf: (packageName) => discovery.listPackages().pipe(Effect.map((all) => Option.fromUndefinedOr(versionsByName(all).get(packageName))), Effect.mapError((cause) => new DependencyResolutionError({
521
- specifier: `workspace:${packageName}`,
522
- cause
523
- }))) };
539
+ return { versionOf: (packageName) => {
540
+ const specifier = `workspace:${packageName}`;
541
+ return discovery.listPackages().pipe(Effect.mapError((cause) => new DependencyResolutionError({
542
+ specifier,
543
+ cause
544
+ })), Effect.flatMap((all) => {
545
+ const index = versionsByName(all);
546
+ if (!index.has(packageName)) return Effect.succeed(Option.none());
547
+ const version = index.get(packageName);
548
+ return version === void 0 ? Effect.fail(new DependencyResolutionError({
549
+ specifier,
550
+ cause: /* @__PURE__ */ new Error(`Workspace member "${packageName}" declares no version`)
551
+ })) : Effect.succeed(Option.some(version));
552
+ }));
553
+ } };
524
554
  }));
525
555
  };
526
556
  const isStringRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) && Object.values(value).every((entry) => typeof entry === "string");
@@ -91,8 +91,24 @@ var WorkspaceManifestError = class extends Schema.TaggedError()("WorkspaceManife
91
91
  var WorkspacePackage = class WorkspacePackage extends Schema.Class("WorkspacePackage")({
92
92
  /** The package name. */
93
93
  name: Schema.NonEmptyString,
94
- /** The raw `version` string — deliberately not semver-validated. */
95
- version: Schema.String,
94
+ /**
95
+ * The raw `version` string — deliberately not semver-validated — or absent
96
+ * when the manifest declares none.
97
+ *
98
+ * @remarks
99
+ * pnpm accepts a version-less private package, and a private monorepo root
100
+ * without a `version` is the ordinary shape, so discovery carries the field
101
+ * exactly as the manifest has it: a non-empty string when present, verbatim
102
+ * and un-validated, absent when the key is absent — never a `"0.0.0"`
103
+ * placeholder and never a present `undefined` key. **Only ABSENCE is
104
+ * tolerated**: a `version` that is present but not a string, or present and
105
+ * `""`, fails discovery as `invalidShape` on both surfaces. `""` in
106
+ * particular is not a pnpm shape and would resolve `workspace:^` to a bare
107
+ * `"^"`.
108
+ * Anything that needs a concrete version (`WorkspaceResolver.versionOf`,
109
+ * a release tag) answers the absence itself rather than inventing one.
110
+ */
111
+ version: Schema.optionalKey(Schema.String),
96
112
  /** Absolute path to the package directory. */
97
113
  path: Schema.NonEmptyString,
98
114
  /** Absolute path to the package's `package.json`. */
@@ -197,7 +197,7 @@ var WorkspaceSnapshots = class WorkspaceSnapshots extends Context.Service()("@ef
197
197
  const importerVersions = yield* catalogsService.importerVersions();
198
198
  const snapshotPackages = packages.map((pkg) => PackageStateSnapshot.make({
199
199
  name: pkg.name,
200
- version: pkg.version,
200
+ version: pkg.version ?? "",
201
201
  relativePath: pkg.relativePath,
202
202
  dependencies: pkg.dependencies,
203
203
  devDependencies: pkg.devDependencies,
@@ -1,7 +1,7 @@
1
1
  import { unanimousVersionOf } from "./internal/importerVersions.js";
2
2
  import { CatalogSet } from "./WorkspaceCatalogs.js";
3
3
  import { Effect, Exit, Layer, Option, Schema } from "effect";
4
- import { CatalogResolver, DependencySpecifier, WorkspaceResolver } from "@effected/npm";
4
+ import { CatalogResolver, DependencyResolutionError, DependencySpecifier, WorkspaceResolver } from "@effected/npm";
5
5
 
6
6
  //#region src/WorkspaceStateSnapshot.ts
7
7
  const EMPTY = Object.freeze(Object.create(null));
@@ -23,7 +23,23 @@ const DependencyMap = Schema.Record(Schema.String, Schema.String).pipe(Schema.wi
23
23
  var PackageStateSnapshot = class extends Schema.Class("PackageStateSnapshot")({
24
24
  /** The package name. */
25
25
  name: Schema.NonEmptyString,
26
- /** The raw `version` string, as recorded at the captured moment. */
26
+ /**
27
+ * The raw `version` string, as recorded at the captured moment — or `""` for
28
+ * a manifest that declared none.
29
+ *
30
+ * @remarks
31
+ * `WorkspacePackage.version` is optional and a version-less member is an
32
+ * ordinary pnpm shape, but this field stays a plain required string because a
33
+ * snapshot is a serialized value someone stores and diffs: an absent key and
34
+ * a present one would read as a change the moment one side of a diff was
35
+ * captured by a different code path. **`""` is therefore the sentinel for
36
+ * "the manifest declared no version", not a version anyone can use** — both
37
+ * `snapshotOf` at a ref and the worktree snapshot write it, so the two sides
38
+ * agree. Every resolution surface treats it as absence:
39
+ * {@link WorkspaceStateSnapshot.resolve} answers `Option.none()` for a
40
+ * `workspace:` specifier against it, because `some("")` would rewrite
41
+ * `workspace:^` as a bare `"^"`.
42
+ */
27
43
  version: Schema.String,
28
44
  /** POSIX path relative to the workspace root; `"."` for the root package. */
29
45
  relativePath: Schema.String,
@@ -137,7 +153,17 @@ var WorkspaceStateSnapshot = class WorkspaceStateSnapshot extends Schema.Class("
137
153
  if (this.#packageIndex === void 0) this.#packageIndex = new Map(this.packages.map((pkg) => [pkg.name, pkg]));
138
154
  return this.#packageIndex;
139
155
  }
140
- /** Every captured package's name → version. Total; O(1) after the first call. */
156
+ /**
157
+ * Every captured package's name → version. Total; O(1) after the first call.
158
+ *
159
+ * @remarks
160
+ * Values are `PackageStateSnapshot.version` verbatim, so a member whose
161
+ * manifest declared no version maps to the **`""` sentinel** rather than
162
+ * being absent from the map — presence here answers membership, not
163
+ * "has a usable version". A caller reading a version out of this map owes
164
+ * the `""` check itself; {@link WorkspaceStateSnapshot.resolve} already
165
+ * makes it.
166
+ */
141
167
  get versions() {
142
168
  return this.#versions();
143
169
  }
@@ -222,7 +248,10 @@ var WorkspaceStateSnapshot = class WorkspaceStateSnapshot extends Schema.Class("
222
248
  const fromCatalogs = this.#catalogRange(dependency, classified.name);
223
249
  return Option.isSome(fromCatalogs) ? fromCatalogs : onUnresolvedCatalog();
224
250
  }
225
- case "workspace": return Option.fromUndefinedOr(this.#versions().get(dependency));
251
+ case "workspace": {
252
+ const version = this.#versions().get(dependency);
253
+ return version === void 0 || version === "" ? Option.none() : Option.some(version);
254
+ }
226
255
  default: return Option.none();
227
256
  }
228
257
  }
@@ -368,7 +397,15 @@ var WorkspaceStateSnapshot = class WorkspaceStateSnapshot extends Schema.Class("
368
397
  * `workspace:` specifiers as of this ref. Built once per instance and cached.
369
398
  */
370
399
  get workspaceResolver() {
371
- if (this.#workspaceResolver === void 0) this.#workspaceResolver = Layer.succeed(WorkspaceResolver, { versionOf: (packageName) => Effect.succeed(Option.fromUndefinedOr(this.#versions().get(packageName))) });
400
+ if (this.#workspaceResolver === void 0) this.#workspaceResolver = Layer.succeed(WorkspaceResolver, { versionOf: (packageName) => {
401
+ const version = this.#versions().get(packageName);
402
+ if (version === void 0) return Effect.succeed(Option.none());
403
+ if (version === "") return Effect.fail(new DependencyResolutionError({
404
+ specifier: `workspace:${packageName}`,
405
+ cause: /* @__PURE__ */ new Error(`Workspace member "${packageName}" declares no version`)
406
+ }));
407
+ return Effect.succeed(Option.some(version));
408
+ } });
372
409
  return this.#workspaceResolver;
373
410
  }
374
411
  /** Both snapshot-scoped resolver layers merged. Built once per instance and cached. */
package/WorkspacesSync.js CHANGED
@@ -7,6 +7,38 @@ import { Yaml } from "@effected/yaml";
7
7
 
8
8
  //#region src/WorkspacesSync.ts
9
9
  /**
10
+ * Read one `package.json` into a plain record, or say why not. Never throws.
11
+ *
12
+ * The three failures are kept apart — a throwing consumer `readFile`, a JSON
13
+ * syntax error, and valid JSON that is not an object — because the skip report
14
+ * carries the distinction; the two other callers of {@link readJson} do not
15
+ * need it and keep the collapsed form.
16
+ */
17
+ const readManifest = (fileSystem, file) => {
18
+ let text;
19
+ try {
20
+ text = fileSystem.readFile(file);
21
+ } catch (cause) {
22
+ return {
23
+ kind: "read",
24
+ cause
25
+ };
26
+ }
27
+ let parsed;
28
+ try {
29
+ parsed = JSON.parse(text);
30
+ } catch (cause) {
31
+ return {
32
+ kind: "invalidJson",
33
+ cause
34
+ };
35
+ }
36
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? { raw: parsed } : {
37
+ kind: "invalidShape",
38
+ cause: /* @__PURE__ */ new Error("package.json is not a JSON object")
39
+ };
40
+ };
41
+ /**
10
42
  * Read and JSON-parse a file into a plain object, or `undefined`. Never throws.
11
43
  *
12
44
  * The non-object check is load-bearing, not defensive noise. `JSON.parse`
@@ -143,21 +175,47 @@ const readPatternsSync = (options, root) => {
143
175
  }
144
176
  return manifestPatternsOf(readJson(fileSystem, path.join(root, "package.json")) ?? {});
145
177
  };
146
- /** Build a `WorkspacePackage` from a directory, or `null` if its manifest is unusable. */
178
+ /**
179
+ * Build a `WorkspacePackage` from a directory, or a {@link WorkspaceDiscoverySkip}
180
+ * saying why its manifest is unusable — never `null`, so a skip cannot go
181
+ * unreported.
182
+ */
147
183
  const readPackageSync = (options, root, directory, relativePath) => {
148
184
  const packageJsonPath = options.path.join(directory, "package.json");
149
- const raw = readJson(options.fileSystem, packageJsonPath);
150
- if (raw === void 0) return null;
185
+ const read = readManifest(options.fileSystem, packageJsonPath);
186
+ if (!("raw" in read)) return {
187
+ root,
188
+ path: packageJsonPath,
189
+ kind: read.kind,
190
+ cause: read.cause
191
+ };
192
+ const raw = read.raw;
151
193
  const name = raw.name;
194
+ if (typeof name !== "string" || name.length === 0) return {
195
+ root,
196
+ path: packageJsonPath,
197
+ kind: "missingName",
198
+ cause: void 0
199
+ };
152
200
  const version = raw.version;
153
- if (typeof name !== "string" || name.length === 0) return null;
154
- if (typeof version !== "string" || version.length === 0) return null;
201
+ if (version !== void 0 && typeof version !== "string") return {
202
+ root,
203
+ path: packageJsonPath,
204
+ kind: "invalidShape",
205
+ cause: /* @__PURE__ */ new Error(`version must be a string, got ${typeof version}`)
206
+ };
207
+ if (version === "") return {
208
+ root,
209
+ path: packageJsonPath,
210
+ kind: "invalidShape",
211
+ cause: /* @__PURE__ */ new Error("version must be a non-empty string")
212
+ };
155
213
  const stringRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) && Object.values(value).every((entry) => typeof entry === "string") ? value : void 0;
156
214
  const publishConfig = raw.publishConfig;
157
215
  const config = publishConfig !== null && typeof publishConfig === "object" ? Effect.runSyncExit(Schema.decodeUnknownEffect(PublishConfig)(publishConfig)) : void 0;
158
216
  return WorkspacePackage.make({
159
217
  name,
160
- version,
218
+ ...version !== void 0 ? { version } : {},
161
219
  path: directory,
162
220
  packageJsonPath,
163
221
  relativePath,
@@ -194,6 +252,13 @@ const readPackageSync = (options, root, directory, relativePath) => {
194
252
  * caller mistakes: a `maxDepth` that is not a positive integer throws, matching
195
253
  * the enumerator's defect.
196
254
  *
255
+ * **A skip is never silent.** Every manifest left out is reported through
256
+ * `onSkip` with its path and the same `kind` the Effect surface would have
257
+ * failed with (a `WorkspaceDiscoverySkip`), so "no packages" and "packages
258
+ * rejected" stay distinguishable. A manifest with no `version` is NOT skipped:
259
+ * pnpm accepts a version-less private package, so it is a member with
260
+ * `version` absent — on both surfaces.
261
+ *
197
262
  * @param root - The workspace root, from {@link findWorkspaceRootSync}.
198
263
  * @param options - The consumer-supplied operations and traversal bounds; see
199
264
  * {@link GetWorkspacePackagesSyncOptions}.
@@ -261,13 +326,18 @@ const getWorkspacePackagesSync = (root, options) => {
261
326
  }
262
327
  }
263
328
  const members = [];
329
+ const admit = (read) => {
330
+ if (read instanceof WorkspacePackage) members.push(read);
331
+ else options.onSkip?.(read);
332
+ };
264
333
  for (const [relativePath, absolute] of [...included.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
265
334
  if (relativePath === "." || absolute === root) continue;
266
- const pkg = readPackageSync(options, root, absolute, relativePath);
267
- if (pkg !== null) members.push(pkg);
335
+ admit(readPackageSync(options, root, absolute, relativePath));
268
336
  }
269
337
  const rootPackage = readPackageSync(options, root, root, ".");
270
- return rootPackage === null ? members : [rootPackage, ...members];
338
+ if (rootPackage instanceof WorkspacePackage) return [rootPackage, ...members];
339
+ options.onSkip?.(rootPackage);
340
+ return members;
271
341
  };
272
342
 
273
343
  //#endregion
package/index.d.ts CHANGED
@@ -83,8 +83,24 @@ declare class WorkspaceManifestError extends WorkspaceManifestError_base {
83
83
  declare const WorkspacePackage_base: Schema.Class<WorkspacePackage, Schema.Struct<{
84
84
  /** The package name. */
85
85
  readonly name: Schema.NonEmptyString;
86
- /** The raw `version` string — deliberately not semver-validated. */
87
- readonly version: Schema.String;
86
+ /**
87
+ * The raw `version` string — deliberately not semver-validated — or absent
88
+ * when the manifest declares none.
89
+ *
90
+ * @remarks
91
+ * pnpm accepts a version-less private package, and a private monorepo root
92
+ * without a `version` is the ordinary shape, so discovery carries the field
93
+ * exactly as the manifest has it: a non-empty string when present, verbatim
94
+ * and un-validated, absent when the key is absent — never a `"0.0.0"`
95
+ * placeholder and never a present `undefined` key. **Only ABSENCE is
96
+ * tolerated**: a `version` that is present but not a string, or present and
97
+ * `""`, fails discovery as `invalidShape` on both surfaces. `""` in
98
+ * particular is not a pnpm shape and would resolve `workspace:^` to a bare
99
+ * `"^"`.
100
+ * Anything that needs a concrete version (`WorkspaceResolver.versionOf`,
101
+ * a release tag) answers the absence itself rather than inventing one.
102
+ */
103
+ readonly version: Schema.optionalKey<Schema.String>;
88
104
  /** Absolute path to the package directory. */
89
105
  readonly path: Schema.NonEmptyString;
90
106
  /** Absolute path to the package's `package.json`. */
@@ -417,18 +433,26 @@ declare const WorkspaceDiscoveryError_base: Schema.Class<WorkspaceDiscoveryError
417
433
  /** The file that failed. */
418
434
  readonly path: Schema.String;
419
435
  /** What went wrong with it. */
420
- readonly kind: Schema.Literals<readonly ["read", "invalidJson", "invalidShape", "invalidYaml", "missingName", "missingVersion"]>;
436
+ readonly kind: Schema.Literals<readonly ["read", "invalidJson", "invalidShape", "invalidYaml", "missingName"]>;
421
437
  /** The originating failure, if there was one. */
422
438
  readonly cause: Schema.Defect;
423
439
  }>, import("effect/Cause").YieldableError>;
424
440
  /**
425
441
  * Raised when a workspace member's `package.json` cannot be read, parsed, or
426
- * used — it is missing, malformed, or lacks a `name` or `version`.
442
+ * used — it is missing, malformed, or lacks a `name`.
427
443
  *
428
444
  * @remarks
429
445
  * `kind` is the discriminant a caller branches on; `cause` preserves the
430
446
  * originating failure rather than flattening it into a sentence.
431
447
  *
448
+ * A manifest with no `version` is NOT a failure: pnpm accepts a version-less
449
+ * private package and a private monorepo root without one is the ordinary
450
+ * shape, so the member is discovered with `WorkspacePackage.version` absent.
451
+ * The former `missingVersion` kind is retired. Only ABSENCE is tolerated: a
452
+ * `version` that is present but not a string — or present and `""`, which pnpm
453
+ * never wrote and which would resolve `workspace:^` to a bare `"^"` — is the
454
+ * manifest's shape being wrong and reports `invalidShape`.
455
+ *
432
456
  * @public
433
457
  */
434
458
  declare class WorkspaceDiscoveryError extends WorkspaceDiscoveryError_base {
@@ -722,6 +746,12 @@ declare class WorkspaceDiscovery extends WorkspaceDiscovery_base {
722
746
  * channel is reserved for a failure of the resolution *mechanism* (an
723
747
  * unfindable root, an unreadable manifest), never an ordinary miss.
724
748
  *
749
+ * A member that declares **no `version`** is neither: it is a known member
750
+ * with nothing for `workspace:` to resolve to. Answering `none` would read
751
+ * as "not a member" downstream, so it fails typed instead, naming the
752
+ * specifier — the same channel a consumer already handles for an
753
+ * unresolvable workspace.
754
+ *
725
755
  * @example
726
756
  * ```ts
727
757
  * import { Package } from "@effected/package-json";
@@ -2689,7 +2719,23 @@ declare class WorkspaceCatalogs extends WorkspaceCatalogs_base {
2689
2719
  declare const PackageStateSnapshot_base: Schema.Class<PackageStateSnapshot, Schema.Struct<{
2690
2720
  /** The package name. */
2691
2721
  readonly name: Schema.NonEmptyString;
2692
- /** The raw `version` string, as recorded at the captured moment. */
2722
+ /**
2723
+ * The raw `version` string, as recorded at the captured moment — or `""` for
2724
+ * a manifest that declared none.
2725
+ *
2726
+ * @remarks
2727
+ * `WorkspacePackage.version` is optional and a version-less member is an
2728
+ * ordinary pnpm shape, but this field stays a plain required string because a
2729
+ * snapshot is a serialized value someone stores and diffs: an absent key and
2730
+ * a present one would read as a change the moment one side of a diff was
2731
+ * captured by a different code path. **`""` is therefore the sentinel for
2732
+ * "the manifest declared no version", not a version anyone can use** — both
2733
+ * `snapshotOf` at a ref and the worktree snapshot write it, so the two sides
2734
+ * agree. Every resolution surface treats it as absence:
2735
+ * {@link WorkspaceStateSnapshot.resolve} answers `Option.none()` for a
2736
+ * `workspace:` specifier against it, because `some("")` would rewrite
2737
+ * `workspace:^` as a bare `"^"`.
2738
+ */
2693
2739
  readonly version: Schema.String;
2694
2740
  /** POSIX path relative to the workspace root; `"."` for the root package. */
2695
2741
  readonly relativePath: Schema.String;
@@ -2800,7 +2846,17 @@ declare const WorkspaceStateSnapshot_base: Schema.Class<WorkspaceStateSnapshot,
2800
2846
  */
2801
2847
  declare class WorkspaceStateSnapshot extends WorkspaceStateSnapshot_base {
2802
2848
  #private;
2803
- /** Every captured package's name → version. Total; O(1) after the first call. */
2849
+ /**
2850
+ * Every captured package's name → version. Total; O(1) after the first call.
2851
+ *
2852
+ * @remarks
2853
+ * Values are `PackageStateSnapshot.version` verbatim, so a member whose
2854
+ * manifest declared no version maps to the **`""` sentinel** rather than
2855
+ * being absent from the map — presence here answers membership, not
2856
+ * "has a usable version". A caller reading a version out of this map owes
2857
+ * the `""` check itself; {@link WorkspaceStateSnapshot.resolve} already
2858
+ * makes it.
2859
+ */
2804
2860
  get versions(): ReadonlyMap<string, string>;
2805
2861
  /** A single captured package by name, or `Option.none()`. Total. */
2806
2862
  package(name: string): Option.Option<PackageStateSnapshot>;
@@ -3631,6 +3687,61 @@ interface WorkspacesSyncOptions {
3631
3687
  /** The synchronous path implementation (Node: the `node:path` module itself). */
3632
3688
  readonly path: SyncPath;
3633
3689
  }
3690
+ /**
3691
+ * Why {@link getWorkspacePackagesSync} left a manifest out of its result — the
3692
+ * `WorkspaceDiscoveryError.kind` values a single manifest read can produce.
3693
+ *
3694
+ * @remarks
3695
+ * The same vocabulary the Effect surface fails with, deliberately: a member the
3696
+ * async `listPackages()` rejects as `missingName` is the member the sync facade
3697
+ * reports as `missingName`. `invalidYaml` is excluded because it describes the
3698
+ * `pnpm-workspace.yaml` read, not a manifest. There is no `missingVersion` —
3699
+ * a version-less manifest is a member on both surfaces.
3700
+ *
3701
+ * The vocabulary is shared; the CHECKS are shared only as far as this list.
3702
+ * Both surfaces perform the same five: the file read (`read`), `JSON.parse`
3703
+ * (`invalidJson`), the parsed value being a non-null non-array object
3704
+ * (`invalidShape`), a non-empty string `name` (`missingName`), and a `version`
3705
+ * that is either absent or a non-empty string (`invalidShape`). Beyond them
3706
+ * they diverge, and one divergence is deliberate: a malformed `publishConfig`
3707
+ * FAILS the async surface, because it reaches the decode, while the sync facade
3708
+ * drops the field and keeps the member. A consumer that must reject such a
3709
+ * manifest has to use the Effect surface.
3710
+ *
3711
+ * @public
3712
+ */
3713
+ type WorkspaceDiscoverySkipKind = Exclude<WorkspaceDiscoveryError["kind"], "invalidYaml">;
3714
+ /**
3715
+ * One manifest {@link getWorkspacePackagesSync} skipped, reported through
3716
+ * {@link GetWorkspacePackagesSyncOptions.onSkip}.
3717
+ *
3718
+ * @remarks
3719
+ * The sync facade is total, so it cannot fail the way `WorkspaceDiscovery`
3720
+ * does — but a skipped member must still be observable. Before this record
3721
+ * existed the skip was silent, and a hand-written fixture with one unusable
3722
+ * manifest enumerated as a plausible empty array indistinguishable from "no
3723
+ * workspaces configured" (issue #605). The fields mirror
3724
+ * `WorkspaceDiscoveryError`: `root`, `path`, `kind`, and on `cause` either the
3725
+ * caught throwable (`read` / `invalidJson`) or an `Error` carrying the same
3726
+ * sentence the Effect surface fails with (`invalidShape`). Only `missingName`
3727
+ * has `cause: undefined` — the field is simply absent, and nothing threw.
3728
+ *
3729
+ * @public
3730
+ */
3731
+ interface WorkspaceDiscoverySkip {
3732
+ /** The workspace root the enumeration ran against. */
3733
+ readonly root: string;
3734
+ /** Absolute path to the `package.json` that was skipped. */
3735
+ readonly path: string;
3736
+ /** Why it was skipped. */
3737
+ readonly kind: WorkspaceDiscoverySkipKind;
3738
+ /**
3739
+ * The thrown value for `read` and `invalidJson`, an `Error` whose message
3740
+ * matches the Effect surface's for `invalidShape`, and `undefined` for
3741
+ * `missingName` alone.
3742
+ */
3743
+ readonly cause: unknown;
3744
+ }
3634
3745
  /**
3635
3746
  * The nearest workspace root at or above `cwd`, or `null`.
3636
3747
  *
@@ -3689,6 +3800,21 @@ interface GetWorkspacePackagesSyncOptions extends WorkspacesSyncOptions {
3689
3800
  * @defaultValue 32
3690
3801
  */
3691
3802
  readonly maxDepth?: number;
3803
+ /**
3804
+ * Called once for every manifest the enumeration found and could not use,
3805
+ * with the file and the reason — so a skipped member is observable even
3806
+ * though this function is total and has no error channel to raise it on.
3807
+ *
3808
+ * @remarks
3809
+ * Omit it and skips are simply not reported, which is the pre-existing
3810
+ * behaviour; nothing is logged in its place. The callback is invoked
3811
+ * synchronously, before the result is returned, in enumeration order —
3812
+ * members first, then the root, which is read last even though it is
3813
+ * returned first. Its
3814
+ * result is discarded, and a throw from it propagates: the facade is total
3815
+ * over *data*, not over caller mistakes, exactly as a bad `maxDepth` is.
3816
+ */
3817
+ readonly onSkip?: ((skip: WorkspaceDiscoverySkip) => void) | undefined;
3692
3818
  }
3693
3819
  /**
3694
3820
  * Every workspace package under `root`, root package first.
@@ -3713,6 +3839,13 @@ interface GetWorkspacePackagesSyncOptions extends WorkspacesSyncOptions {
3713
3839
  * caller mistakes: a `maxDepth` that is not a positive integer throws, matching
3714
3840
  * the enumerator's defect.
3715
3841
  *
3842
+ * **A skip is never silent.** Every manifest left out is reported through
3843
+ * `onSkip` with its path and the same `kind` the Effect surface would have
3844
+ * failed with (a `WorkspaceDiscoverySkip`), so "no packages" and "packages
3845
+ * rejected" stay distinguishable. A manifest with no `version` is NOT skipped:
3846
+ * pnpm accepts a version-less private package, so it is a member with
3847
+ * `version` absent — on both surfaces.
3848
+ *
3716
3849
  * @param root - The workspace root, from {@link findWorkspaceRootSync}.
3717
3850
  * @param options - The consumer-supplied operations and traversal bounds; see
3718
3851
  * {@link GetWorkspacePackagesSyncOptions}.
@@ -3740,5 +3873,5 @@ interface GetWorkspacePackagesSyncOptions extends WorkspacesSyncOptions {
3740
3873
  */
3741
3874
  declare const getWorkspacePackagesSync: (root: string, options: GetWorkspacePackagesSyncOptions) => ReadonlyArray<WorkspacePackage>;
3742
3875
  //#endregion
3743
- export { type CatalogAssemblyFailure, CatalogSet, ChangeDetectionError, type ChangeDetectionFailure, ChangeDetectionOptions, ChangeDetector, type ChangeDetectorShape, type ClassifyOptions, ConfigDependencyHooks, type ConfigDependencyHooksShape, CyclicDependencyError, type DependencyDiff, DependencyGraph, DetectedPackageManager, type FindWorkspaceRootOptions, type GetWorkspacePackagesSyncOptions, type HookInjection, type ImporterVersions, LockfileReadError, type LockfileReadFailure, LockfileReader, type LockfileReaderOptions, type LockfileReaderShape, NoPeerDependencyRules, PackageManagerDetectionError, type PackageManagerDetectionFailure, PackageManagerDetector, type PackageManagerDetectorShape, PackageManagerEvidence, PackageManagerName, PackageNotFoundError, type PackageRelease, PackageStateSnapshot, PeerCheck, type PeerCheckOptions, type PeerDependencyRules, PeerParent, PublishConfig, PublishTarget, PublishabilityDetector, type PublishabilityDetectorShape, ReleaseTag, type SyncDirectoryEntry, type SyncFileSystem, type SyncPath, type TagClassification, type TagFormatOptions, TagStyle, TrackingTag, type TrackingTagOptions, UnsatisfiedPeer, type UnverifiedReason, type VersioningDetectOptions, VersioningStrategy, VersioningStrategyType, 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 WorkspacesGitOptions, type WorkspacesOptions, type WorkspacesServices, type WorkspacesSyncOptions, classifyTag, findWorkspaceRootSync, getWorkspacePackagesSync };
3876
+ export { type CatalogAssemblyFailure, CatalogSet, ChangeDetectionError, type ChangeDetectionFailure, ChangeDetectionOptions, ChangeDetector, type ChangeDetectorShape, type ClassifyOptions, ConfigDependencyHooks, type ConfigDependencyHooksShape, CyclicDependencyError, type DependencyDiff, DependencyGraph, DetectedPackageManager, type FindWorkspaceRootOptions, type GetWorkspacePackagesSyncOptions, type HookInjection, type ImporterVersions, LockfileReadError, type LockfileReadFailure, LockfileReader, type LockfileReaderOptions, type LockfileReaderShape, NoPeerDependencyRules, PackageManagerDetectionError, type PackageManagerDetectionFailure, PackageManagerDetector, type PackageManagerDetectorShape, PackageManagerEvidence, PackageManagerName, PackageNotFoundError, type PackageRelease, PackageStateSnapshot, PeerCheck, type PeerCheckOptions, type PeerDependencyRules, PeerParent, PublishConfig, PublishTarget, PublishabilityDetector, type PublishabilityDetectorShape, ReleaseTag, type SyncDirectoryEntry, type SyncFileSystem, type SyncPath, type TagClassification, type TagFormatOptions, TagStyle, TrackingTag, type TrackingTagOptions, UnsatisfiedPeer, type UnverifiedReason, type VersioningDetectOptions, VersioningStrategy, VersioningStrategyType, WORKSPACE_MARKERS, WorkspaceCatalogs, type WorkspaceCatalogsOptions, type WorkspaceCatalogsShape, WorkspaceDiscovery, WorkspaceDiscoveryError, type WorkspaceDiscoveryFailure, type WorkspaceDiscoveryOptions, type WorkspaceDiscoveryShape, type WorkspaceDiscoverySkip, type WorkspaceDiscoverySkipKind, WorkspaceInfo, type WorkspaceLookupFailure, WorkspaceManifestError, WorkspacePackage, WorkspacePatternError, WorkspaceRoot, WorkspaceRootNotFoundError, type WorkspaceRootShape, type WorkspaceSnapshotAtFailure, type WorkspaceSnapshotWorktreeFailure, WorkspaceSnapshots, type WorkspaceSnapshotsOptions, type WorkspaceSnapshotsShape, WorkspaceStateSnapshot, Workspaces, type WorkspacesGitOptions, type WorkspacesOptions, type WorkspacesServices, type WorkspacesSyncOptions, classifyTag, findWorkspaceRootSync, getWorkspacePackagesSync };
3744
3877
  //# sourceMappingURL=index.d.ts.map
package/node-sync.d.ts CHANGED
@@ -1,7 +1,8 @@
1
+ import "@effected/npm";
2
+ import "effect";
1
3
  import "@effected/glob";
2
4
  import "@effected/lockfiles";
3
5
  import "@effected/package-json";
4
- import "effect";
5
6
  //#region src/WorkspacesSync.d.ts
6
7
  /**
7
8
  * The synchronous file operations the sync entry points need, supplied by the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/workspaces",
3
- "version": "0.18.3",
3
+ "version": "0.19.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": [
@@ -52,7 +52,7 @@
52
52
  "@effected/lockfiles": "^0.7.1",
53
53
  "@effected/npm": "^0.12.1",
54
54
  "@effected/package-json": "^0.13.0",
55
- "@effected/semver": "^0.5.0",
55
+ "@effected/semver": "^0.5.1",
56
56
  "@effected/walker": "^0.5.0",
57
57
  "@effected/yaml": "^0.12.0",
58
58
  "@pnpm/catalogs.config": "^1100.0.5",