@effected/workspaces 0.2.1 → 0.3.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/Publishability.js CHANGED
@@ -51,6 +51,37 @@ var PublishTarget = class extends Schema.Class("PublishTarget")({
51
51
  * });
52
52
  * ```
53
53
  *
54
+ * @example
55
+ * The shape's error channel is `never` — **degrade or die**. An override
56
+ * backed by something fallible (a policy service, a registry probe) folds its
57
+ * failure structurally over `{ readonly message: string }` — matching every
58
+ * `Error`, every Effect schema error class, and anything else carrying a
59
+ * message — and either degrades to a safe answer or dies:
60
+ *
61
+ * ```ts
62
+ * import { PublishabilityDetector, PublishTarget } from "@effected/workspaces";
63
+ * import { Effect, Layer } from "effect";
64
+ *
65
+ * declare const lookupPolicy: (
66
+ * name: string,
67
+ * ) => Effect.Effect<ReadonlyArray<PublishTarget>, { readonly message: string }>;
68
+ *
69
+ * const fromPolicyService = Layer.succeed(PublishabilityDetector, {
70
+ * detect: (pkg) =>
71
+ * lookupPolicy(pkg.name).pipe(
72
+ * Effect.catch((error) =>
73
+ * Effect.die(new Error(`publishability policy lookup failed for ${pkg.name}: ${error.message}`)),
74
+ * ),
75
+ * ),
76
+ * });
77
+ * ```
78
+ *
79
+ * A lookup failure that should *not* abort the run degrades instead —
80
+ * `Effect.catch(() => Effect.succeed([]))` reads as "unknown means
81
+ * unpublishable" — but pick one deliberately; silently swallowing the failure
82
+ * into a wrong "publishes to npm" answer is the one option the contract
83
+ * forbids.
84
+ *
54
85
  * @public
55
86
  */
56
87
  var PublishabilityDetector = class PublishabilityDetector extends Context.Service()("@effected/workspaces/PublishabilityDetector") {
package/README.md CHANGED
@@ -39,6 +39,8 @@ pnpm add @effected/workspaces effect
39
39
 
40
40
  Requires Node.js >=24.11.0. `effect` v4 is a peer dependency. You provide a `FileSystem` and `Path` implementation at the edge — `@effect/platform-node` or `@effect/platform-bun`.
41
41
 
42
+ All `@effected/*` packages are ESM-only: the exports maps publish only `import` conditions, so `require()` — including tools that resolve in CJS mode — fails with Node's `ERR_PACKAGE_PATH_NOT_EXPORTED` rather than loading a CJS build that does not exist. Import from an ES module.
43
+
42
44
  pnpm's catalog semantics come from pnpm's own `@pnpm/catalogs.*` packages, which install as regular dependencies. Reimplementing them would mean owning a moving spec with no oracle, so they are used directly and confined to a single internal module.
43
45
 
44
46
  ## Quick start
@@ -12,9 +12,9 @@ const EMPTY_MANIFEST = Object.freeze(Object.create(null));
12
12
  * @remarks
13
13
  * Deliberately narrow. `@effected/package-json` models `publishConfig` as an
14
14
  * open `Record<string, unknown>` for round-trip fidelity, which preserves every
15
- * key but types none of them; this is the typed projection of the four that
16
- * decide *where and whether* a package publishes. Unknown keys are ignored, not
17
- * rejected.
15
+ * key but types none of them; this is the typed projection of the handful that
16
+ * decide *where, whether and as what* a package publishes. Unknown keys are
17
+ * ignored, not rejected.
18
18
  *
19
19
  * @public
20
20
  */
@@ -25,6 +25,13 @@ var PublishConfig = class extends Schema.Class("PublishConfig")({
25
25
  registry: Schema.optionalKey(Schema.String),
26
26
  /** A subdirectory to publish instead of the package root. */
27
27
  directory: Schema.optionalKey(Schema.String),
28
+ /**
29
+ * Whether workspace links point into `directory` during local development —
30
+ * pnpm symlinks the publish directory instead of the package root, so
31
+ * siblings resolve the built artifact they would install from the registry.
32
+ * Meaningful only alongside `directory`.
33
+ */
34
+ linkDirectory: Schema.optionalKey(Schema.Boolean),
28
35
  /** The dist-tag to publish under. */
29
36
  tag: Schema.optionalKey(Schema.String)
30
37
  }) {};
package/index.d.ts CHANGED
@@ -13,6 +13,13 @@ declare const PublishConfig_base: Schema.Class<PublishConfig, Schema.Struct<{
13
13
  readonly registry: Schema.optionalKey<Schema.String>;
14
14
  /** A subdirectory to publish instead of the package root. */
15
15
  readonly directory: Schema.optionalKey<Schema.String>;
16
+ /**
17
+ * Whether workspace links point into `directory` during local development —
18
+ * pnpm symlinks the publish directory instead of the package root, so
19
+ * siblings resolve the built artifact they would install from the registry.
20
+ * Meaningful only alongside `directory`.
21
+ */
22
+ readonly linkDirectory: Schema.optionalKey<Schema.Boolean>;
16
23
  /** The dist-tag to publish under. */
17
24
  readonly tag: Schema.optionalKey<Schema.String>;
18
25
  }>, {}>;
@@ -22,9 +29,9 @@ declare const PublishConfig_base: Schema.Class<PublishConfig, Schema.Struct<{
22
29
  * @remarks
23
30
  * Deliberately narrow. `@effected/package-json` models `publishConfig` as an
24
31
  * open `Record<string, unknown>` for round-trip fidelity, which preserves every
25
- * key but types none of them; this is the typed projection of the four that
26
- * decide *where and whether* a package publishes. Unknown keys are ignored, not
27
- * rejected.
32
+ * key but types none of them; this is the typed projection of the handful that
33
+ * decide *where, whether and as what* a package publishes. Unknown keys are
34
+ * ignored, not rejected.
28
35
  *
29
36
  * @public
30
37
  */
@@ -977,10 +984,26 @@ declare const PublishTarget_base: Schema.Class<PublishTarget, Schema.Struct<{
977
984
  * @public
978
985
  */
979
986
  declare class PublishTarget extends PublishTarget_base {}
980
- declare const PublishabilityDetector_base: Context.ServiceClass<PublishabilityDetector, "@effected/workspaces/PublishabilityDetector", {
987
+ /**
988
+ * The {@link PublishabilityDetector} service shape.
989
+ *
990
+ * @remarks
991
+ * The error channel is deliberately `never`: every consumer of the service —
992
+ * a release planner iterating a whole workspace — treats "does this publish"
993
+ * as a total question, so an overriding layer whose lookup can fail must
994
+ * **degrade or die**. Fold a recoverable failure into a safe answer (usually
995
+ * the empty target list), or `Effect.orDie` it into the defect channel; it
996
+ * cannot widen the channel the contract declares. See
997
+ * {@link PublishabilityDetector} for the adapter an overriding consumer
998
+ * writes.
999
+ *
1000
+ * @public
1001
+ */
1002
+ interface PublishabilityDetectorShape {
981
1003
  /** The publish targets for a package; empty means it does not publish. */
982
1004
  readonly detect: (pkg: WorkspacePackage) => Effect.Effect<ReadonlyArray<PublishTarget>>;
983
- }>;
1005
+ }
1006
+ declare const PublishabilityDetector_base: Context.ServiceClass<PublishabilityDetector, "@effected/workspaces/PublishabilityDetector", PublishabilityDetectorShape>;
984
1007
  /**
985
1008
  * Decides whether a workspace package publishes, and to where.
986
1009
  *
@@ -1012,6 +1035,37 @@ declare const PublishabilityDetector_base: Context.ServiceClass<PublishabilityDe
1012
1035
  * });
1013
1036
  * ```
1014
1037
  *
1038
+ * @example
1039
+ * The shape's error channel is `never` — **degrade or die**. An override
1040
+ * backed by something fallible (a policy service, a registry probe) folds its
1041
+ * failure structurally over `{ readonly message: string }` — matching every
1042
+ * `Error`, every Effect schema error class, and anything else carrying a
1043
+ * message — and either degrades to a safe answer or dies:
1044
+ *
1045
+ * ```ts
1046
+ * import { PublishabilityDetector, PublishTarget } from "@effected/workspaces";
1047
+ * import { Effect, Layer } from "effect";
1048
+ *
1049
+ * declare const lookupPolicy: (
1050
+ * name: string,
1051
+ * ) => Effect.Effect<ReadonlyArray<PublishTarget>, { readonly message: string }>;
1052
+ *
1053
+ * const fromPolicyService = Layer.succeed(PublishabilityDetector, {
1054
+ * detect: (pkg) =>
1055
+ * lookupPolicy(pkg.name).pipe(
1056
+ * Effect.catch((error) =>
1057
+ * Effect.die(new Error(`publishability policy lookup failed for ${pkg.name}: ${error.message}`)),
1058
+ * ),
1059
+ * ),
1060
+ * });
1061
+ * ```
1062
+ *
1063
+ * A lookup failure that should *not* abort the run degrades instead —
1064
+ * `Effect.catch(() => Effect.succeed([]))` reads as "unknown means
1065
+ * unpublishable" — but pick one deliberately; silently swallowing the failure
1066
+ * into a wrong "publishes to npm" answer is the one option the contract
1067
+ * forbids.
1068
+ *
1015
1069
  * @public
1016
1070
  */
1017
1071
  declare class PublishabilityDetector extends PublishabilityDetector_base {
@@ -1668,5 +1722,5 @@ interface GetWorkspacePackagesSyncOptions extends WorkspacesSyncOptions {
1668
1722
  */
1669
1723
  declare const getWorkspacePackagesSync: (root: string, options: GetWorkspacePackagesSyncOptions) => ReadonlyArray<WorkspacePackage>;
1670
1724
  //#endregion
1671
- export { type CatalogAssemblyFailure, CatalogSet, ChangeDetectionError, type ChangeDetectionFailure, ChangeDetectionOptions, ChangeDetector, type ChangeDetectorShape, ConfigDependencyHooks, type ConfigDependencyHooksShape, CyclicDependencyError, type DependencyDiff, DependencyGraph, DetectedPackageManager, type FindWorkspaceRootSyncOptions, type GetWorkspacePackagesSyncOptions, LockfileReadError, type LockfileReadFailure, LockfileReader, type LockfileReaderOptions, type LockfileReaderShape, PackageManagerDetectionError, type PackageManagerDetectionFailure, PackageManagerDetector, PackageManagerName, PackageNotFoundError, PackageStateSnapshot, PublishConfig, PublishTarget, PublishabilityDetector, type SyncFileSystem, type SyncPath, WORKSPACE_MARKERS, WorkspaceCatalogs, type WorkspaceCatalogsOptions, type WorkspaceCatalogsShape, WorkspaceDiscovery, WorkspaceDiscoveryError, type WorkspaceDiscoveryFailure, type WorkspaceDiscoveryOptions, type WorkspaceDiscoveryShape, WorkspaceInfo, type WorkspaceLookupFailure, WorkspaceManifestError, WorkspacePackage, WorkspacePatternError, WorkspaceRoot, WorkspaceRootNotFoundError, type WorkspaceSnapshotAtFailure, type WorkspaceSnapshotWorktreeFailure, WorkspaceSnapshots, type WorkspaceSnapshotsOptions, type WorkspaceSnapshotsShape, WorkspaceStateSnapshot, Workspaces, type WorkspacesOptions, type WorkspacesServices, type WorkspacesSyncOptions, findWorkspaceRootSync, getWorkspacePackagesSync };
1725
+ export { type CatalogAssemblyFailure, CatalogSet, ChangeDetectionError, type ChangeDetectionFailure, ChangeDetectionOptions, ChangeDetector, type ChangeDetectorShape, ConfigDependencyHooks, type ConfigDependencyHooksShape, CyclicDependencyError, type DependencyDiff, DependencyGraph, DetectedPackageManager, type FindWorkspaceRootSyncOptions, type GetWorkspacePackagesSyncOptions, LockfileReadError, type LockfileReadFailure, LockfileReader, type LockfileReaderOptions, type LockfileReaderShape, PackageManagerDetectionError, type PackageManagerDetectionFailure, PackageManagerDetector, PackageManagerName, PackageNotFoundError, PackageStateSnapshot, PublishConfig, PublishTarget, PublishabilityDetector, type PublishabilityDetectorShape, type SyncFileSystem, type SyncPath, WORKSPACE_MARKERS, WorkspaceCatalogs, type WorkspaceCatalogsOptions, type WorkspaceCatalogsShape, WorkspaceDiscovery, WorkspaceDiscoveryError, type WorkspaceDiscoveryFailure, type WorkspaceDiscoveryOptions, type WorkspaceDiscoveryShape, WorkspaceInfo, type WorkspaceLookupFailure, WorkspaceManifestError, WorkspacePackage, WorkspacePatternError, WorkspaceRoot, WorkspaceRootNotFoundError, type WorkspaceSnapshotAtFailure, type WorkspaceSnapshotWorktreeFailure, WorkspaceSnapshots, type WorkspaceSnapshotsOptions, type WorkspaceSnapshotsShape, WorkspaceStateSnapshot, Workspaces, type WorkspacesOptions, type WorkspacesServices, type WorkspacesSyncOptions, findWorkspaceRootSync, getWorkspacePackagesSync };
1672
1726
  //# sourceMappingURL=index.d.ts.map
package/node-sync.d.ts ADDED
@@ -0,0 +1,116 @@
1
+ import "@effected/glob";
2
+ import "@effected/lockfiles";
3
+ import "@effected/package-json";
4
+ import "effect";
5
+ //#region src/WorkspacesSync.d.ts
6
+ /**
7
+ * The synchronous file operations the sync entry points need, supplied by the
8
+ * consumer. Node's built-ins satisfy it directly:
9
+ *
10
+ * ```ts
11
+ * import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
12
+ *
13
+ * const fileSystem: SyncFileSystem = {
14
+ * exists: existsSync,
15
+ * readFile: (p) => readFileSync(p, "utf8"),
16
+ * readDirectory: (p) => readdirSync(p),
17
+ * isDirectory: (p) => statSync(p).isDirectory(),
18
+ * };
19
+ * ```
20
+ *
21
+ * `exists` must return a boolean and not throw (Node's `existsSync` never
22
+ * does). The other three may throw — `statSync` on a missing path, a
23
+ * permission error mid-read — and every throw is absorbed into the documented
24
+ * degraded-skip semantics: a throwing `readFile` reads as an unusable
25
+ * manifest, a throwing `readDirectory` as an unreadable directory, a throwing
26
+ * `isDirectory` as "not a directory". Nothing propagates.
27
+ *
28
+ * @public
29
+ */
30
+ interface SyncFileSystem {
31
+ /** Whether a file or directory exists at `path`. Must not throw. */
32
+ readonly exists: (path: string) => boolean;
33
+ /** Read the file at `path` as text. May throw; a throw degrades to a skip. */
34
+ readonly readFile: (path: string) => string;
35
+ /** The entry names inside the directory at `path`. May throw; a throw skips the directory. */
36
+ readonly readDirectory: (path: string) => ReadonlyArray<string>;
37
+ /** Whether `path` is a directory. May throw; a throw reads as `false`. */
38
+ readonly isDirectory: (path: string) => boolean;
39
+ }
40
+ /**
41
+ * The synchronous path operations the sync entry points need, supplied by the
42
+ * consumer. Deliberately a structural subset of `node:path`, so the built-in
43
+ * module (and its `win32` / `posix` variants, or a Bun / Deno equivalent)
44
+ * satisfies it verbatim:
45
+ *
46
+ * ```ts
47
+ * import * as path from "node:path";
48
+ *
49
+ * const options: WorkspacesSyncOptions = {
50
+ * fileSystem: { exists: existsSync, readFile: (p) => readFileSync(p, "utf8"), readDirectory: (p) => readdirSync(p), isDirectory: (p) => statSync(p).isDirectory() },
51
+ * path, // node:path IS a SyncPath
52
+ * };
53
+ * ```
54
+ *
55
+ * These operations shape only the ABSOLUTE paths handed back to the consumer
56
+ * (and to its own `fileSystem`); workspace-relative pattern matching is POSIX
57
+ * by the `packages:` contract and never routes through here. Windows
58
+ * correctness comes from supplying a win32-appropriate implementation, not
59
+ * from anything in this module.
60
+ *
61
+ * @public
62
+ */
63
+ interface SyncPath {
64
+ /** Join segments with the implementation's separator (like `path.join`). */
65
+ readonly join: (...segments: ReadonlyArray<string>) => string;
66
+ /** The directory portion of `p` (like `path.dirname`). */
67
+ readonly dirname: (p: string) => string;
68
+ /** Resolve segments to an absolute path (rightmost-wins, like `path.resolve`). */
69
+ readonly resolve: (...segments: ReadonlyArray<string>) => string;
70
+ }
71
+ /**
72
+ * The consumer-supplied operations backing one sync call: the file operations
73
+ * and the path implementation. Both are required — this package never imports
74
+ * `node:*` and never assumes posix, so the platform binding is entirely the
75
+ * caller's.
76
+ *
77
+ * @public
78
+ */
79
+ interface WorkspacesSyncOptions {
80
+ /** The synchronous file operations (Node: `existsSync` / `readFileSync` / `readdirSync` / `statSync`). */
81
+ readonly fileSystem: SyncFileSystem;
82
+ /** The synchronous path implementation (Node: the `node:path` module itself). */
83
+ readonly path: SyncPath;
84
+ }
85
+ //#endregion
86
+ //#region src/node-sync.d.ts
87
+ /**
88
+ * `SyncFileSystem` over `node:fs`.
89
+ *
90
+ * @remarks
91
+ * `existsSync` never throws, satisfying `exists`'s must-not-throw contract;
92
+ * the other three may throw and every throw lands in the sync entry points'
93
+ * documented degraded-skip semantics.
94
+ *
95
+ * @public
96
+ */
97
+ declare const nodeFileSystem: SyncFileSystem;
98
+ /**
99
+ * `SyncPath` as the running platform's `node:path` — win32 semantics on
100
+ * Windows, posix elsewhere. Pass `node:path/win32` or `node:path/posix`
101
+ * yourself to pin a dialect.
102
+ *
103
+ * @public
104
+ */
105
+ declare const nodePath: SyncPath;
106
+ /**
107
+ * The complete Node-bound options bag for `findWorkspaceRootSync` and
108
+ * `getWorkspacePackagesSync` — {@link nodeFileSystem} plus {@link nodePath}.
109
+ * Spread it to add the per-call extras: `{ ...nodeSyncOps, cwd }`.
110
+ *
111
+ * @public
112
+ */
113
+ declare const nodeSyncOps: WorkspacesSyncOptions;
114
+ //#endregion
115
+ export { type SyncFileSystem, type SyncPath, type WorkspacesSyncOptions, nodeFileSystem, nodePath, nodeSyncOps };
116
+ //# sourceMappingURL=node-sync.d.ts.map
package/node-sync.js ADDED
@@ -0,0 +1,67 @@
1
+ import * as path from "node:path";
2
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
3
+
4
+ //#region src/node-sync.ts
5
+ /**
6
+ * The Node.js binding for the sync entry points — ready-made `SyncFileSystem`
7
+ * and `SyncPath` operations over `node:fs` / `node:path`, so adopting
8
+ * `findWorkspaceRootSync` / `getWorkspacePackagesSync` is one import instead
9
+ * of four hand-wired one-liners.
10
+ *
11
+ * Deliberately a **separate subpath** (`@effected/workspaces/node-sync`), not
12
+ * part of the main entry: the main entry imports nothing platform-shaped, and
13
+ * re-exporting these from it would drag `node:*` imports into every consumer —
14
+ * including the ones supplying their own operations (a win32-explicit `path`,
15
+ * a Bun or Deno binding, a test fake). Import this module only where Node's
16
+ * built-ins are the platform you mean; `nodePath` is the running platform's
17
+ * `node:path`, so on Windows the paths handed back are win32 paths.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * import { findWorkspaceRootSync, getWorkspacePackagesSync } from "@effected/workspaces";
22
+ * import { nodeSyncOps } from "@effected/workspaces/node-sync";
23
+ *
24
+ * const root = findWorkspaceRootSync(nodeSyncOps);
25
+ * const packages = root === null ? [] : getWorkspacePackagesSync(root, nodeSyncOps);
26
+ * ```
27
+ *
28
+ * @packageDocumentation
29
+ */
30
+ /**
31
+ * `SyncFileSystem` over `node:fs`.
32
+ *
33
+ * @remarks
34
+ * `existsSync` never throws, satisfying `exists`'s must-not-throw contract;
35
+ * the other three may throw and every throw lands in the sync entry points'
36
+ * documented degraded-skip semantics.
37
+ *
38
+ * @public
39
+ */
40
+ const nodeFileSystem = {
41
+ exists: existsSync,
42
+ readFile: (p) => readFileSync(p, "utf8"),
43
+ readDirectory: (p) => readdirSync(p),
44
+ isDirectory: (p) => statSync(p).isDirectory()
45
+ };
46
+ /**
47
+ * `SyncPath` as the running platform's `node:path` — win32 semantics on
48
+ * Windows, posix elsewhere. Pass `node:path/win32` or `node:path/posix`
49
+ * yourself to pin a dialect.
50
+ *
51
+ * @public
52
+ */
53
+ const nodePath = path;
54
+ /**
55
+ * The complete Node-bound options bag for `findWorkspaceRootSync` and
56
+ * `getWorkspacePackagesSync` — {@link nodeFileSystem} plus {@link nodePath}.
57
+ * Spread it to add the per-call extras: `{ ...nodeSyncOps, cwd }`.
58
+ *
59
+ * @public
60
+ */
61
+ const nodeSyncOps = {
62
+ fileSystem: nodeFileSystem,
63
+ path: nodePath
64
+ };
65
+
66
+ //#endregion
67
+ export { nodeFileSystem, nodePath, nodeSyncOps };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/workspaces",
3
- "version": "0.2.1",
3
+ "version": "0.3.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": [
@@ -37,16 +37,20 @@
37
37
  "types": "./index.d.ts",
38
38
  "import": "./index.js"
39
39
  },
40
+ "./node-sync": {
41
+ "types": "./node-sync.d.ts",
42
+ "import": "./node-sync.js"
43
+ },
40
44
  "./package.json": "./package.json"
41
45
  },
42
46
  "dependencies": {
43
- "@effected/git": "0.2.0",
47
+ "@effected/git": "0.3.0",
44
48
  "@effected/glob": "0.1.0",
45
- "@effected/lockfiles": "0.1.1",
49
+ "@effected/lockfiles": "0.1.2",
46
50
  "@effected/npm": "0.2.0",
47
- "@effected/package-json": "0.2.0",
48
- "@effected/walker": "0.1.0",
49
- "@effected/yaml": "0.1.0",
51
+ "@effected/package-json": "0.3.0",
52
+ "@effected/walker": "0.2.0",
53
+ "@effected/yaml": "0.2.0",
50
54
  "@pnpm/catalogs.config": "^1100.0.0",
51
55
  "@pnpm/catalogs.protocol-parser": "^1100.0.0",
52
56
  "@pnpm/catalogs.resolver": "^1100.0.0",