@effected/workspaces 0.15.0 → 0.16.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/PeerCheck.js CHANGED
@@ -308,7 +308,11 @@ const collect = (importerPath, roots, byId, rows, seen, allowed) => {
308
308
  * `"react-dom@18.0.0>react"` suppresses a `react-dom@18.3.1` instance too.
309
309
  * Replicating that is not optional: matching on the version would suppress a
310
310
  * strictly smaller set than pnpm does, and every row in the difference is a
311
- * false positive.
311
+ * false positive. Measured against pnpm 11.22.0, one axis at a time: a rule
312
+ * keyed at a version the installed parent does not have still suppresses, and
313
+ * so does one keyed at a wildly different version, while a rule keyed on an
314
+ * ANCESTOR of the declaring package suppresses nothing. The parent is the
315
+ * declarer, matched by name.
312
316
  *
313
317
  * Both spellings occur in the wild and both must work — parent-versioned, as
314
318
  * `pnpm:export` materializes into `pnpm-workspace.yaml`, and unversioned, as a
package/README.md CHANGED
@@ -218,6 +218,25 @@ const packages = root === null ? [] : getWorkspacePackagesSync(root, options);
218
218
  // packages: the discovered workspace packages, empty when there is no root
219
219
  ```
220
220
 
221
+ Those four operations are the whole requirement. A fifth, `readDirectoryWithTypes`, is optional: supply it and package enumeration reads a directory's entries and their types in one call instead of a `readDirectory` plus an `isDirectory` per entry, which on a large workspace is a syscall per file. `nodeFileSystem` already implements it over `readdirSync(p, { withFileTypes: true })`, so the `node-sync` bindings get the fast path for free. Omit it and enumeration falls back to the four required operations with identical results — a cost optimization, never a behavior switch.
222
+
223
+ Each entry reports `name`, `isDirectory` and `isSymbolicLink` as a `SyncDirectoryEntry`, which Node's `Dirent` satisfies once its predicate methods are called. The link flag is load-bearing: a `Dirent` describes the entry itself, so a symlink pointing at a directory reports `isDirectory: false`, while the `stat`-based path resolves the link and calls the same entry a directory. Enumeration re-resolves links through `isDirectory` rather than trusting the flag, which is what keeps a workspace with symlinked packages discovered identically on both paths.
224
+
225
+ ```ts
226
+ import { readdirSync } from "node:fs";
227
+ import type { SyncDirectoryEntry } from "@effected/workspaces";
228
+
229
+ const readDirectoryWithTypes = (p: string): ReadonlyArray<SyncDirectoryEntry> =>
230
+ readdirSync(p, { withFileTypes: true }).map((entry) => ({
231
+ name: entry.name,
232
+ isDirectory: entry.isDirectory(),
233
+ isSymbolicLink: entry.isSymbolicLink(),
234
+ }));
235
+ // pass alongside the four required operations: { ...options.fileSystem, readDirectoryWithTypes }
236
+ ```
237
+
238
+ For a test fake, `@effected/memfs`' `MemoryFileSystem.syncFileSystem(volume)` satisfies `SyncFileSystem` structurally — neither package imports the other — so a config-time discovery path can be exercised against a virtual workspace with nothing on disk.
239
+
221
240
  Windows correctness is therefore the operations you pass, and nothing else. Both entry points drive one traversal state machine (the same dequeue order, depth rule, visit budget and `node_modules` prune), so the sync and Effect surfaces can never disagree about what a pattern means. The one deliberate difference is at a bound: the Effect enumerator fails typed, the sync one truncates. Prefer the Effect API everywhere you can run one.
222
241
 
223
242
  ## Error handling
@@ -286,7 +305,7 @@ A name miss in the derived `getPackage` fails with the service's own typed `Pack
286
305
  - `ReleaseTag` / `TrackingTag` — release-tag formatting (`ReleaseTag.single` / `.scoped`, strict SemVer by default with no `v` prefix) and the floating major/minor alias derivation GitHub Actions-style consumers expect (`v1`, `v1.2`), plus `classifyTag` to tell a release tag from a tracking alias.
287
306
  - `VersioningStrategy` — classify a workspace as `single`, `fixed-group` or `independent` from package names and fixed groups, or detect it live against `PublishabilityDetector`, and produce the release tags for a batch with `tagsFor`.
288
307
  - `findWorkspaceRootSync` / `getWorkspacePackagesSync` — the synchronous escape hatch for config-time callers that cannot await, over file and path operations you supply.
289
- - `@effected/workspaces/node-sync` — a second entry point holding the Node bindings for those operations (`nodeFileSystem`, `nodePath` and the `nodeSyncOps` bag), kept off the main entry so `node:*` never reaches a consumer that supplies its own.
308
+ - `@effected/workspaces/node-sync` — a second entry point holding the Node bindings for those operations (`nodeFileSystem`, `nodePath` and the `nodeSyncOps` bag), kept off the main entry so `node:*` never reaches a consumer that supplies its own. `nodeFileSystem` implements the optional `readDirectoryWithTypes` fast path, so the bindings enumerate a workspace in one `readdirSync` per directory.
290
309
 
291
310
  ## License
292
311
 
package/WorkspacesSync.js CHANGED
@@ -35,6 +35,37 @@ const isDirectory = (fileSystem, dir) => {
35
35
  return false;
36
36
  }
37
37
  };
38
+ /**
39
+ * The entries of `absoluteDir`, each carrying whether it is a directory, or
40
+ * `undefined` when the directory could not be read at all (the degraded-skip
41
+ * case). Uses {@link SyncFileSystem.readDirectoryWithTypes} when the consumer
42
+ * supplied it and the four-operation fallback otherwise — the two agree by
43
+ * construction, including on symbolic links, which are always re-resolved
44
+ * through `isDirectory` because a `Dirent` describes the link and not its
45
+ * target.
46
+ */
47
+ const readResolvedEntries = (options, absoluteDir) => {
48
+ const { fileSystem, path } = options;
49
+ const withTypes = fileSystem.readDirectoryWithTypes;
50
+ if (withTypes !== void 0) try {
51
+ return withTypes(absoluteDir).map((entry) => ({
52
+ name: entry.name,
53
+ directory: entry.isSymbolicLink ? isDirectory(fileSystem, path.join(absoluteDir, entry.name)) : entry.isDirectory
54
+ }));
55
+ } catch {
56
+ return;
57
+ }
58
+ let names;
59
+ try {
60
+ names = fileSystem.readDirectory(absoluteDir);
61
+ } catch {
62
+ return;
63
+ }
64
+ return names.map((name) => ({
65
+ name,
66
+ directory: isDirectory(fileSystem, path.join(absoluteDir, name))
67
+ }));
68
+ };
38
69
  /** Whether `dir` holds a `package.json`. */
39
70
  const isPackage = (options, dir) => options.fileSystem.exists(options.path.join(dir, "package.json"));
40
71
  /**
@@ -212,17 +243,13 @@ const getWorkspacePackagesSync = (root, options) => {
212
243
  let stopped = false;
213
244
  for (let current = traversal.next(); current !== void 0 && !stopped; current = traversal.next()) {
214
245
  if (traversal.charge() !== void 0) break;
215
- let entries = [];
216
- try {
217
- entries = fileSystem.readDirectory(current.absolute);
218
- } catch {
219
- continue;
220
- }
246
+ const entries = readResolvedEntries(options, current.absolute);
247
+ if (entries === void 0) continue;
221
248
  for (const entry of entries) {
222
- if (isPruned(entry)) continue;
223
- const relative = joinRelative(current.relative, entry);
224
- const absolute = path.join(current.absolute, entry);
225
- if (!isDirectory(fileSystem, absolute)) continue;
249
+ if (isPruned(entry.name)) continue;
250
+ const relative = joinRelative(current.relative, entry.name);
251
+ const absolute = path.join(current.absolute, entry.name);
252
+ if (!entry.directory) continue;
226
253
  if (wildcard.crossesSegments && !traversal.admits(current)) {
227
254
  stopped = true;
228
255
  break;
package/index.d.ts CHANGED
@@ -3227,6 +3227,53 @@ interface SyncFileSystem {
3227
3227
  readonly readDirectory: (path: string) => ReadonlyArray<string>;
3228
3228
  /** Whether `path` is a directory. May throw; a throw reads as `false`. */
3229
3229
  readonly isDirectory: (path: string) => boolean;
3230
+ /**
3231
+ * Optional fast path: the entries inside `path` with their types already
3232
+ * known, in ONE call.
3233
+ *
3234
+ * @remarks
3235
+ * Package enumeration otherwise costs a `readDirectory` plus one
3236
+ * `isDirectory` per entry — the readdir-then-stat-per-entry shape, which on
3237
+ * a large workspace is a syscall per file. Supplying this collapses that to
3238
+ * a single `readdirSync(path, { withFileTypes: true })`; `nodeFileSystem`
3239
+ * does. Omit it and enumeration falls back to the four required operations
3240
+ * with identical results, so this is purely a cost optimization and never a
3241
+ * behavior switch.
3242
+ *
3243
+ * May throw; a throw skips the directory, exactly like `readDirectory`.
3244
+ */
3245
+ readonly readDirectoryWithTypes?: ((path: string) => ReadonlyArray<SyncDirectoryEntry>) | undefined;
3246
+ }
3247
+ /**
3248
+ * One directory entry with its type resolved, as the optional
3249
+ * {@link SyncFileSystem.readDirectoryWithTypes} fast path reports it. Node's
3250
+ * `Dirent` satisfies it after mapping its predicate methods to booleans.
3251
+ *
3252
+ * @remarks
3253
+ * `isSymbolicLink` is not decoration. A `Dirent` describes the entry ITSELF, so
3254
+ * a symbolic link pointing at a directory reports `isDirectory: false` — while
3255
+ * the `stat`-based slow path, which resolves the link, calls the same entry a
3256
+ * directory. Enumeration therefore re-resolves links through `isDirectory`
3257
+ * rather than trusting `isDirectory` on a link, which is what keeps the fast
3258
+ * and slow paths in agreement on a workspace whose packages are symlinked.
3259
+ *
3260
+ * Which behavior is *correct* is domain-dependent, so the flag is reported
3261
+ * rather than resolved away. Enumeration follows links because a symlinked
3262
+ * package is still a package. A test-file discovery walk usually must NOT: in a
3263
+ * pnpm workspace `node_modules` is a farm of links into the content-addressed
3264
+ * store, and following them walks the whole store or hits a cycle. A consumer
3265
+ * building its own walker on this shape wants `isDirectory` verbatim — there,
3266
+ * not following is the requirement, not the hazard.
3267
+ *
3268
+ * @public
3269
+ */
3270
+ interface SyncDirectoryEntry {
3271
+ /** The entry's own name, not a path. */
3272
+ readonly name: string;
3273
+ /** Whether the entry itself is a directory. `false` for a symbolic link, even one targeting a directory. */
3274
+ readonly isDirectory: boolean;
3275
+ /** Whether the entry itself is a symbolic link. */
3276
+ readonly isSymbolicLink: boolean;
3230
3277
  }
3231
3278
  /**
3232
3279
  * The synchronous path operations the sync entry points need, supplied by the
@@ -3382,5 +3429,5 @@ interface GetWorkspacePackagesSyncOptions extends WorkspacesSyncOptions {
3382
3429
  */
3383
3430
  declare const getWorkspacePackagesSync: (root: string, options: GetWorkspacePackagesSyncOptions) => ReadonlyArray<WorkspacePackage>;
3384
3431
  //#endregion
3385
- 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 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 WorkspacesOptions, type WorkspacesServices, type WorkspacesSyncOptions, classifyTag, findWorkspaceRootSync, getWorkspacePackagesSync };
3432
+ 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 WorkspacesOptions, type WorkspacesServices, type WorkspacesSyncOptions, classifyTag, findWorkspaceRootSync, getWorkspacePackagesSync };
3386
3433
  //# sourceMappingURL=index.d.ts.map
package/node-sync.d.ts CHANGED
@@ -36,6 +36,53 @@ interface SyncFileSystem {
36
36
  readonly readDirectory: (path: string) => ReadonlyArray<string>;
37
37
  /** Whether `path` is a directory. May throw; a throw reads as `false`. */
38
38
  readonly isDirectory: (path: string) => boolean;
39
+ /**
40
+ * Optional fast path: the entries inside `path` with their types already
41
+ * known, in ONE call.
42
+ *
43
+ * @remarks
44
+ * Package enumeration otherwise costs a `readDirectory` plus one
45
+ * `isDirectory` per entry — the readdir-then-stat-per-entry shape, which on
46
+ * a large workspace is a syscall per file. Supplying this collapses that to
47
+ * a single `readdirSync(path, { withFileTypes: true })`; `nodeFileSystem`
48
+ * does. Omit it and enumeration falls back to the four required operations
49
+ * with identical results, so this is purely a cost optimization and never a
50
+ * behavior switch.
51
+ *
52
+ * May throw; a throw skips the directory, exactly like `readDirectory`.
53
+ */
54
+ readonly readDirectoryWithTypes?: ((path: string) => ReadonlyArray<SyncDirectoryEntry>) | undefined;
55
+ }
56
+ /**
57
+ * One directory entry with its type resolved, as the optional
58
+ * {@link SyncFileSystem.readDirectoryWithTypes} fast path reports it. Node's
59
+ * `Dirent` satisfies it after mapping its predicate methods to booleans.
60
+ *
61
+ * @remarks
62
+ * `isSymbolicLink` is not decoration. A `Dirent` describes the entry ITSELF, so
63
+ * a symbolic link pointing at a directory reports `isDirectory: false` — while
64
+ * the `stat`-based slow path, which resolves the link, calls the same entry a
65
+ * directory. Enumeration therefore re-resolves links through `isDirectory`
66
+ * rather than trusting `isDirectory` on a link, which is what keeps the fast
67
+ * and slow paths in agreement on a workspace whose packages are symlinked.
68
+ *
69
+ * Which behavior is *correct* is domain-dependent, so the flag is reported
70
+ * rather than resolved away. Enumeration follows links because a symlinked
71
+ * package is still a package. A test-file discovery walk usually must NOT: in a
72
+ * pnpm workspace `node_modules` is a farm of links into the content-addressed
73
+ * store, and following them walks the whole store or hits a cycle. A consumer
74
+ * building its own walker on this shape wants `isDirectory` verbatim — there,
75
+ * not following is the requirement, not the hazard.
76
+ *
77
+ * @public
78
+ */
79
+ interface SyncDirectoryEntry {
80
+ /** The entry's own name, not a path. */
81
+ readonly name: string;
82
+ /** Whether the entry itself is a directory. `false` for a symbolic link, even one targeting a directory. */
83
+ readonly isDirectory: boolean;
84
+ /** Whether the entry itself is a symbolic link. */
85
+ readonly isSymbolicLink: boolean;
39
86
  }
40
87
  /**
41
88
  * The synchronous path operations the sync entry points need, supplied by the
@@ -114,5 +161,5 @@ declare const nodePath: SyncPath;
114
161
  */
115
162
  declare const nodeSyncOps: WorkspacesSyncOptions;
116
163
  //#endregion
117
- export { type SyncFileSystem, type SyncPath, type WorkspacesSyncOptions, nodeFileSystem, nodePath, nodeSyncOps };
164
+ export { type SyncDirectoryEntry, type SyncFileSystem, type SyncPath, type WorkspacesSyncOptions, nodeFileSystem, nodePath, nodeSyncOps };
118
165
  //# sourceMappingURL=node-sync.d.ts.map
package/node-sync.js CHANGED
@@ -41,7 +41,12 @@ const nodeFileSystem = {
41
41
  exists: existsSync,
42
42
  readFile: (p) => readFileSync(p, "utf8"),
43
43
  readDirectory: (p) => readdirSync(p),
44
- isDirectory: (p) => statSync(p).isDirectory()
44
+ isDirectory: (p) => statSync(p).isDirectory(),
45
+ readDirectoryWithTypes: (p) => readdirSync(p, { withFileTypes: true }).map((entry) => ({
46
+ name: entry.name,
47
+ isDirectory: entry.isDirectory(),
48
+ isSymbolicLink: entry.isSymbolicLink()
49
+ }))
45
50
  };
46
51
  /**
47
52
  * `SyncPath` as the running platform's `node:path` — win32 semantics on
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/workspaces",
3
- "version": "0.15.0",
3
+ "version": "0.16.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": [
@@ -49,8 +49,8 @@
49
49
  "@effected/commands": "^0.5.0",
50
50
  "@effected/git": "^0.9.0",
51
51
  "@effected/glob": "^0.4.0",
52
- "@effected/lockfiles": "^0.6.0",
53
- "@effected/npm": "^0.11.0",
52
+ "@effected/lockfiles": "^0.6.1",
53
+ "@effected/npm": "^0.11.1",
54
54
  "@effected/package-json": "^0.10.2",
55
55
  "@effected/semver": "^0.5.0",
56
56
  "@effected/walker": "^0.5.0",
@@ -5,7 +5,7 @@
5
5
  "toolPackages": [
6
6
  {
7
7
  "packageName": "@microsoft/api-extractor",
8
- "packageVersion": "7.58.12"
8
+ "packageVersion": "7.58.13"
9
9
  }
10
10
  ]
11
11
  }