@effected/workspaces 0.1.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.
@@ -0,0 +1,338 @@
1
+ import { enumerate } from "./internal/enumerate.js";
2
+ import { readPatterns } from "./internal/patterns.js";
3
+ import { WorkspacePackage } from "./WorkspacePackage.js";
4
+ import { WorkspaceRoot } from "./WorkspaceRoot.js";
5
+ import { Context, Duration, Effect, Exit, FileSystem, Layer, Option, Path, Schema } from "effect";
6
+ import { GlobSet } from "@effected/glob";
7
+ import { DependencyResolutionError, WorkspaceResolver } from "@effected/npm";
8
+
9
+ //#region src/WorkspaceDiscovery.ts
10
+ /**
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`.
13
+ *
14
+ * @remarks
15
+ * `kind` is the discriminant a caller branches on; `cause` preserves the
16
+ * originating failure rather than flattening it into a sentence.
17
+ *
18
+ * @public
19
+ */
20
+ var WorkspaceDiscoveryError = class extends Schema.TaggedErrorClass()("WorkspaceDiscoveryError", {
21
+ /** The workspace root discovery was running against. */
22
+ root: Schema.String,
23
+ /** The file that failed. */
24
+ path: Schema.String,
25
+ /** What went wrong with it. */
26
+ kind: Schema.Literals([
27
+ "read",
28
+ "invalidJson",
29
+ "invalidShape",
30
+ "invalidYaml",
31
+ "missingName",
32
+ "missingVersion"
33
+ ]),
34
+ /** The originating failure, if there was one. */
35
+ cause: Schema.Defect()
36
+ }) {
37
+ /** Renders the failing file and kind into a one-line message. */
38
+ get message() {
39
+ return `Workspace discovery failed at ${this.path} (${this.kind})`;
40
+ }
41
+ };
42
+ /**
43
+ * Raised when a `packages:` pattern cannot be enumerated: its base directory is
44
+ * absent (usually a typo), the descent exceeded its depth cap, or the visit
45
+ * budget was exhausted.
46
+ *
47
+ * @public
48
+ */
49
+ var WorkspacePatternError = class extends Schema.TaggedErrorClass()("WorkspacePatternError", {
50
+ /** The workspace root the patterns were expanded against. */
51
+ root: Schema.String,
52
+ /** The offending pattern, verbatim. */
53
+ pattern: Schema.String,
54
+ /** Why it could not be enumerated. */
55
+ kind: Schema.Literals([
56
+ "missingBaseDir",
57
+ "uncompilable",
58
+ "depthExceeded",
59
+ "budgetExceeded",
60
+ "unreadableDirectory"
61
+ ]),
62
+ /** A short, structured detail — the missing directory, or the bound exceeded. */
63
+ detail: Schema.String
64
+ }) {
65
+ /** Renders the pattern and failure kind into a one-line message. */
66
+ get message() {
67
+ return `Workspace pattern "${this.pattern}" could not be enumerated (${this.kind}: ${this.detail})`;
68
+ }
69
+ };
70
+ /**
71
+ * Raised when a workspace package is requested by a name no member carries.
72
+ *
73
+ * @remarks
74
+ * `available` lists every known member, which is what makes the error
75
+ * actionable — a typo is obvious next to the list it missed.
76
+ *
77
+ * @public
78
+ */
79
+ var PackageNotFoundError = class extends Schema.TaggedErrorClass()("PackageNotFoundError", {
80
+ /** The name that was requested. */
81
+ name: Schema.String,
82
+ /** Every workspace package name that does exist. */
83
+ available: Schema.Array(Schema.String)
84
+ }) {
85
+ /** Renders the requested name into a one-line message. */
86
+ get message() {
87
+ return `No workspace package named "${this.name}"`;
88
+ }
89
+ };
90
+ /**
91
+ * Top-level facts about a workspace: where it is, what manages it, and the
92
+ * patterns that define its membership.
93
+ *
94
+ * @public
95
+ */
96
+ var WorkspaceInfo = class extends Schema.Class("WorkspaceInfo")({
97
+ /** Absolute path to the workspace root. */
98
+ root: Schema.String,
99
+ /** The `packages:` patterns, verbatim. */
100
+ patterns: Schema.Array(Schema.String)
101
+ }) {};
102
+ /** The enumeration failure kinds map straight onto the pattern-error kinds. */
103
+ const patternKindOf = (kind) => kind;
104
+ /**
105
+ * Discovers the packages of a workspace.
106
+ *
107
+ * @remarks
108
+ * Layer construction is O(1): the root walk, pattern read, enumeration and
109
+ * per-package decode all happen on the first method call and are memoized for
110
+ * the lifetime of the layer. A Vitest reporter that builds the layer per call
111
+ * site and never queries it pays nothing.
112
+ *
113
+ * The memo is **success-only**. `Effect.cached` memoizes the first `Exit`,
114
+ * *including an interrupt* — an init interrupted by an unrelated timeout would
115
+ * otherwise brick the layer permanently with a cause outside its declared error
116
+ * channel. A failure or interrupt is therefore retried on the next call, which
117
+ * is a deliberate behaviour change from the v3 library.
118
+ *
119
+ * @example
120
+ * ```ts
121
+ * import { WorkspaceDiscovery } from "@effected/workspaces";
122
+ * import { Effect } from "effect";
123
+ *
124
+ * const program = Effect.gen(function* () {
125
+ * const discovery = yield* WorkspaceDiscovery;
126
+ * const packages = yield* discovery.listPackages();
127
+ * return packages.map((p) => p.name);
128
+ * });
129
+ * ```
130
+ *
131
+ * @public
132
+ */
133
+ var WorkspaceDiscovery = class WorkspaceDiscovery extends Context.Service()("@effected/workspaces/WorkspaceDiscovery") {
134
+ /**
135
+ * Builds the service. Root resolution is one explicit concern: `cwd` is an
136
+ * option here, never an ambient `process.cwd()` read inside a method.
137
+ */
138
+ static make = (options) => Effect.gen(function* () {
139
+ const roots = yield* WorkspaceRoot;
140
+ const fs = yield* FileSystem.FileSystem;
141
+ const path = yield* Path.Path;
142
+ /** Read one `package.json` into the tolerant discovery projection. */
143
+ const readPackage = (root, directory, relativePath) => Effect.gen(function* () {
144
+ const packageJsonPath = path.join(directory, "package.json");
145
+ const content = yield* fs.readFileString(packageJsonPath).pipe(Effect.mapError((cause) => new WorkspaceDiscoveryError({
146
+ root,
147
+ path: packageJsonPath,
148
+ kind: "read",
149
+ cause
150
+ })));
151
+ const parsed = yield* Effect.try({
152
+ try: () => JSON.parse(content),
153
+ catch: (cause) => new WorkspaceDiscoveryError({
154
+ root,
155
+ path: packageJsonPath,
156
+ kind: "invalidJson",
157
+ cause
158
+ })
159
+ });
160
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return yield* Effect.fail(new WorkspaceDiscoveryError({
161
+ root,
162
+ path: packageJsonPath,
163
+ kind: "invalidShape",
164
+ cause: /* @__PURE__ */ new Error("package.json is not a JSON object")
165
+ }));
166
+ const raw = parsed;
167
+ const name = raw.name;
168
+ if (typeof name !== "string" || name.length === 0) return yield* Effect.fail(new WorkspaceDiscoveryError({
169
+ root,
170
+ path: packageJsonPath,
171
+ kind: "missingName",
172
+ cause: void 0
173
+ }));
174
+ const version = raw.version;
175
+ if (typeof version !== "string" || version.length === 0) return yield* Effect.fail(new WorkspaceDiscoveryError({
176
+ root,
177
+ path: packageJsonPath,
178
+ kind: "missingVersion",
179
+ cause: void 0
180
+ }));
181
+ return yield* Schema.decodeUnknownEffect(WorkspacePackage)({
182
+ name,
183
+ version,
184
+ path: directory,
185
+ packageJsonPath,
186
+ relativePath,
187
+ ...typeof raw.private === "boolean" ? { private: raw.private } : {},
188
+ ...isStringRecord(raw.dependencies) ? { dependencies: raw.dependencies } : {},
189
+ ...isStringRecord(raw.devDependencies) ? { devDependencies: raw.devDependencies } : {},
190
+ ...isStringRecord(raw.peerDependencies) ? { peerDependencies: raw.peerDependencies } : {},
191
+ ...isStringRecord(raw.optionalDependencies) ? { optionalDependencies: raw.optionalDependencies } : {},
192
+ ...raw.publishConfig !== void 0 && raw.publishConfig !== null ? { publishConfig: raw.publishConfig } : {}
193
+ }).pipe(Effect.catchTag("SchemaError", (cause) => new WorkspaceDiscoveryError({
194
+ root,
195
+ path: packageJsonPath,
196
+ kind: "invalidShape",
197
+ cause
198
+ })));
199
+ });
200
+ const discover = Effect.gen(function* () {
201
+ const root = yield* Effect.suspend(() => roots.find(options?.cwd ?? process.cwd()));
202
+ const patterns = yield* readPatterns(root).pipe(Effect.mapError((failure) => new WorkspaceDiscoveryError({
203
+ root,
204
+ path: failure.path,
205
+ kind: failure.kind,
206
+ cause: failure.cause
207
+ })));
208
+ const directories = yield* enumerate(root, yield* GlobSet.compile(patterns).pipe(Effect.mapError((error) => new WorkspacePatternError({
209
+ root,
210
+ pattern: error.pattern,
211
+ kind: "uncompilable",
212
+ detail: error.message
213
+ }))), { maxDepth: options?.maxDepth ?? 32 }).pipe(Effect.mapError((failure) => new WorkspacePatternError({
214
+ root,
215
+ pattern: failure.pattern,
216
+ kind: patternKindOf(failure.kind),
217
+ detail: failure.detail
218
+ })));
219
+ const members = yield* Effect.forEach(directories.filter((entry) => entry.relativePath !== "." && entry.path !== root), (entry) => readPackage(root, entry.path, entry.relativePath), { concurrency: 10 });
220
+ const packages = [yield* readPackage(root, root, "."), ...members];
221
+ yield* Effect.logDebug("Workspace packages discovered").pipe(Effect.annotateLogs({
222
+ "workspace.root": root,
223
+ "workspace.packages.count": packages.length
224
+ }));
225
+ return {
226
+ info: WorkspaceInfo.make({
227
+ root,
228
+ patterns
229
+ }),
230
+ packages
231
+ };
232
+ }).pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path));
233
+ const [resolveOnce, invalidate] = yield* Effect.cachedInvalidateWithTTL(discover, Duration.infinity);
234
+ const memo = Effect.onExit(resolveOnce, (exit) => Exit.isSuccess(exit) ? Effect.void : invalidate);
235
+ const packages = memo.pipe(Effect.map((state) => state.packages));
236
+ /**
237
+ * The longest-prefix index, built once per package list.
238
+ *
239
+ * Keyed on the package array's identity: the memo hands back the *same*
240
+ * array on every call, so this is a hit for the whole life of the memo, and
241
+ * `refresh()` produces a fresh array — a cache miss — which is exactly the
242
+ * invalidation we want. No staleness is representable.
243
+ */
244
+ const ownerIndexes = /* @__PURE__ */ new WeakMap();
245
+ const owners = (all) => {
246
+ const cached = ownerIndexes.get(all);
247
+ if (cached !== void 0) return cached;
248
+ const index = all.map((pkg) => ({
249
+ prefix: pkg.path.endsWith(path.sep) ? pkg.path : pkg.path + path.sep,
250
+ package: pkg
251
+ })).sort((a, b) => b.prefix.length - a.prefix.length);
252
+ ownerIndexes.set(all, index);
253
+ return index;
254
+ };
255
+ const ownerOf = (filePath, index) => {
256
+ for (const entry of index) if (filePath.startsWith(entry.prefix)) return Option.some(entry.package);
257
+ return Option.none();
258
+ };
259
+ return {
260
+ info: Effect.fn("WorkspaceDiscovery.info")(function* () {
261
+ return (yield* memo).info;
262
+ }),
263
+ listPackages: Effect.fn("WorkspaceDiscovery.listPackages")(function* () {
264
+ return yield* packages;
265
+ }),
266
+ importerMap: Effect.fn("WorkspaceDiscovery.importerMap")(function* () {
267
+ const all = yield* packages;
268
+ return new Map(all.map((pkg) => [pkg.relativePath, pkg]));
269
+ }),
270
+ getPackage: Effect.fn("WorkspaceDiscovery.getPackage")(function* (name) {
271
+ const all = yield* packages;
272
+ const found = all.find((pkg) => pkg.name === name);
273
+ if (found !== void 0) return found;
274
+ return yield* Effect.fail(new PackageNotFoundError({
275
+ name,
276
+ available: all.map((pkg) => pkg.name)
277
+ }));
278
+ }),
279
+ resolveFile: Effect.fn("WorkspaceDiscovery.resolveFile")(function* (filePath) {
280
+ const all = yield* packages;
281
+ return ownerOf(filePath, owners(all));
282
+ }),
283
+ resolveFiles: Effect.fn("WorkspaceDiscovery.resolveFiles")(function* (filePaths) {
284
+ const all = yield* packages;
285
+ const index = owners(all);
286
+ const seen = /* @__PURE__ */ new Map();
287
+ for (const filePath of filePaths) {
288
+ const owner = ownerOf(filePath, index);
289
+ if (Option.isSome(owner)) seen.set(owner.value.name, owner.value);
290
+ }
291
+ return [...seen.values()].sort((a, b) => a.name.localeCompare(b.name));
292
+ }),
293
+ refresh: () => invalidate
294
+ };
295
+ });
296
+ /**
297
+ * The live layer.
298
+ *
299
+ * @remarks
300
+ * A parameterized layer factory mints a **fresh reference per call**, and
301
+ * layers memoize by reference — bind the result to a `const` and reuse it
302
+ * rather than calling `layer(...)` at each composition site.
303
+ */
304
+ static layer = (options) => Layer.effect(WorkspaceDiscovery, WorkspaceDiscovery.make(options));
305
+ /**
306
+ * The real implementation of `@effected/npm`'s `WorkspaceResolver` contract
307
+ * — the one `@effected/package-json` declares but cannot fill.
308
+ *
309
+ * @remarks
310
+ * `versionOf` returns `Option.none()` for a name that is not a workspace
311
+ * member, per the contract's convention; the `DependencyResolutionError`
312
+ * channel is reserved for a failure of the resolution *mechanism* (an
313
+ * unfindable root, an unreadable manifest), never an ordinary miss.
314
+ *
315
+ * @example
316
+ * ```ts
317
+ * import { Package } from "@effected/package-json";
318
+ * import { WorkspaceDiscovery } from "@effected/workspaces";
319
+ * import { Layer } from "effect";
320
+ *
321
+ * const resolvers = WorkspaceDiscovery.workspaceResolver.pipe(
322
+ * Layer.provide(WorkspaceDiscovery.layer()),
323
+ * );
324
+ * // `Package.resolve` now resolves `workspace:*` for real.
325
+ * ```
326
+ */
327
+ static workspaceResolver = Layer.effect(WorkspaceResolver, Effect.gen(function* () {
328
+ const discovery = yield* WorkspaceDiscovery;
329
+ return { versionOf: (packageName) => discovery.listPackages().pipe(Effect.map((all) => Option.fromUndefinedOr(all.find((pkg) => pkg.name === packageName)?.version)), Effect.mapError((cause) => new DependencyResolutionError({
330
+ specifier: `workspace:${packageName}`,
331
+ cause
332
+ }))) };
333
+ }));
334
+ };
335
+ const isStringRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) && Object.values(value).every((entry) => typeof entry === "string");
336
+
337
+ //#endregion
338
+ export { PackageNotFoundError, WorkspaceDiscovery, WorkspaceDiscoveryError, WorkspaceInfo, WorkspacePatternError };
@@ -0,0 +1,247 @@
1
+ import { Effect, FileSystem, Option, Schema } from "effect";
2
+ import { GlobPattern } from "@effected/glob";
3
+ import { WorkspaceManifest } from "@effected/lockfiles";
4
+ import { Package } from "@effected/package-json";
5
+
6
+ //#region src/WorkspacePackage.ts
7
+ const EMPTY = Object.freeze(Object.create(null));
8
+ /**
9
+ * The `publishConfig` fields workspace tooling reads.
10
+ *
11
+ * @remarks
12
+ * Deliberately narrow. `@effected/package-json` models `publishConfig` as an
13
+ * open `Record<string, unknown>` for round-trip fidelity, which preserves every
14
+ * key but types none of them; this is the typed projection of the four that
15
+ * decide *where and whether* a package publishes. Unknown keys are ignored, not
16
+ * rejected.
17
+ *
18
+ * @public
19
+ */
20
+ var PublishConfig = class extends Schema.Class("PublishConfig")({
21
+ /** Scoped-package visibility. Its presence overrides `private`. */
22
+ access: Schema.optionalKey(Schema.Literals(["public", "restricted"])),
23
+ /** The registry to publish to. */
24
+ registry: Schema.optionalKey(Schema.String),
25
+ /** A subdirectory to publish instead of the package root. */
26
+ directory: Schema.optionalKey(Schema.String),
27
+ /** The dist-tag to publish under. */
28
+ tag: Schema.optionalKey(Schema.String)
29
+ }) {};
30
+ const DependencyMap = Schema.Record(Schema.String, Schema.String).pipe(Schema.withDecodingDefaultKey(Effect.succeed(EMPTY)), Schema.withConstructorDefault(Effect.succeed(EMPTY)));
31
+ /**
32
+ * Raised when a workspace member's `package.json` cannot be read or decoded
33
+ * into the strict `@effected/package-json` `Package` model.
34
+ *
35
+ * @remarks
36
+ * Discovery itself never raises this — it uses the tolerant projection. Only
37
+ * `WorkspacePackage.manifest` does, so opting into the strict model is an
38
+ * explicit, individually recoverable step.
39
+ *
40
+ * @public
41
+ */
42
+ var WorkspaceManifestError = class extends Schema.TaggedErrorClass()("WorkspaceManifestError", {
43
+ /** Absolute path to the `package.json` that failed. */
44
+ packageJsonPath: Schema.String,
45
+ /** Whether the file could not be read, or read but not decoded. */
46
+ kind: Schema.Literals(["read", "decode"]),
47
+ /** The originating failure, preserved rather than flattened to a string. */
48
+ cause: Schema.Defect()
49
+ }) {
50
+ /** Renders the path and failure kind into a one-line message. */
51
+ get message() {
52
+ return `Failed to ${this.kind} package.json at ${this.packageJsonPath}`;
53
+ }
54
+ };
55
+ /**
56
+ * A single package inside a workspace: the discovery-relevant slice of its
57
+ * `package.json` plus its filesystem location.
58
+ *
59
+ * @remarks
60
+ * Produced by `WorkspaceDiscovery` for every directory the `packages:` patterns
61
+ * enumerate. The root package is always present with `relativePath` `"."`.
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * import { WorkspacePackage } from "@effected/workspaces";
66
+ *
67
+ * const pkg = WorkspacePackage.make({
68
+ * name: "@my-org/utils",
69
+ * version: "1.0.0",
70
+ * path: "/repo/packages/utils",
71
+ * packageJsonPath: "/repo/packages/utils/package.json",
72
+ * relativePath: "packages/utils",
73
+ * });
74
+ *
75
+ * pkg.isRootWorkspace; // false
76
+ * pkg.unscopedName; // "utils"
77
+ * ```
78
+ *
79
+ * @public
80
+ */
81
+ var WorkspacePackage = class WorkspacePackage extends Schema.Class("WorkspacePackage")({
82
+ /** The package name. */
83
+ name: Schema.NonEmptyString,
84
+ /** The raw `version` string — deliberately not semver-validated. */
85
+ version: Schema.String,
86
+ /** Absolute path to the package directory. */
87
+ path: Schema.NonEmptyString,
88
+ /** Absolute path to the package's `package.json`. */
89
+ packageJsonPath: Schema.NonEmptyString,
90
+ /** POSIX path relative to the workspace root; `"."` for the root package. */
91
+ relativePath: Schema.String,
92
+ /** Whether the package is marked private. */
93
+ private: Schema.Boolean.pipe(Schema.withDecodingDefaultKey(Effect.succeed(false)), Schema.withConstructorDefault(Effect.succeed(false))),
94
+ /** Production dependencies. */
95
+ dependencies: DependencyMap,
96
+ /** Development dependencies. */
97
+ devDependencies: DependencyMap,
98
+ /** Peer dependencies. */
99
+ peerDependencies: DependencyMap,
100
+ /** Optional dependencies. */
101
+ optionalDependencies: DependencyMap,
102
+ /** The `publishConfig` block, when present. */
103
+ publishConfig: Schema.optionalKey(PublishConfig)
104
+ }) {
105
+ /** Whether this is the workspace root package. */
106
+ get isRootWorkspace() {
107
+ return this.relativePath === ".";
108
+ }
109
+ /** Whether the package is publishable in principle (not marked private). */
110
+ get isPublic() {
111
+ return !this.private;
112
+ }
113
+ /** The npm scope (`@org`), or `Option.none()` for an unscoped name. */
114
+ get scope() {
115
+ const match = /^(@[^/]+)\//.exec(this.name);
116
+ return match === null ? Option.none() : Option.some(match[1]);
117
+ }
118
+ /** The name with any scope stripped. */
119
+ get unscopedName() {
120
+ const slash = this.name.indexOf("/");
121
+ return this.name.startsWith("@") && slash !== -1 ? this.name.slice(slash + 1) : this.name;
122
+ }
123
+ /**
124
+ * Every dependency, merged across the four kinds.
125
+ *
126
+ * @remarks
127
+ * Precedence on a name declared in several kinds runs
128
+ * `dependencies` \> `devDependencies` \> `peerDependencies` \>
129
+ * `optionalDependencies`.
130
+ */
131
+ get allDependencies() {
132
+ return {
133
+ ...this.optionalDependencies,
134
+ ...this.peerDependencies,
135
+ ...this.devDependencies,
136
+ ...this.dependencies
137
+ };
138
+ }
139
+ /** Whether `name` is a production dependency. */
140
+ hasDependency(name) {
141
+ return Object.hasOwn(this.dependencies, name);
142
+ }
143
+ /** Whether `name` is a development dependency. */
144
+ hasDevDependency(name) {
145
+ return Object.hasOwn(this.devDependencies, name);
146
+ }
147
+ /** Whether `name` is a peer dependency. */
148
+ hasPeerDependency(name) {
149
+ return Object.hasOwn(this.peerDependencies, name);
150
+ }
151
+ /** Whether `name` is an optional dependency. */
152
+ hasOptionalDependency(name) {
153
+ return Object.hasOwn(this.optionalDependencies, name);
154
+ }
155
+ /** Whether `name` appears in any of the four dependency kinds. */
156
+ hasAnyDependencyOn(name) {
157
+ return this.hasDependency(name) || this.hasDevDependency(name) || this.hasPeerDependency(name) || this.hasOptionalDependency(name);
158
+ }
159
+ /** The declared specifier for `name`, searched across all four kinds. */
160
+ dependencyVersion(name) {
161
+ const own = (map) => Object.hasOwn(map, name) ? map[name] : void 0;
162
+ const version = own(this.dependencies) ?? own(this.devDependencies) ?? own(this.peerDependencies) ?? own(this.optionalDependencies);
163
+ return version === void 0 ? Option.none() : Option.some(version);
164
+ }
165
+ /**
166
+ * Whether any dependency name matches `pattern` — the `minimatch` runtime
167
+ * dependency's one call site, now over `@effected/glob`'s vendored engine.
168
+ *
169
+ * @remarks
170
+ * A `GlobPattern` is total and free to test. A `string` is compiled on every
171
+ * call and an **uncompilable** literal throws: a glob written into a call
172
+ * site is developer wiring, not untrusted input, so it belongs in the defect
173
+ * channel rather than widening the typed channel every caller must branch
174
+ * on. Compile once with `GlobPattern.compile` and pass the result when
175
+ * testing many packages.
176
+ *
177
+ * @param pattern - A compiled pattern, or a source string to compile.
178
+ */
179
+ matchesDependency(pattern) {
180
+ const compiled = typeof pattern === "string" ? GlobPattern.make({ source: pattern }) : pattern;
181
+ return Object.keys(this.allDependencies).some((dependency) => compiled.matches(dependency));
182
+ }
183
+ /** Compare this package's dependencies against `other`'s. */
184
+ dependencyDiff(other) {
185
+ const mine = this.allDependencies;
186
+ const theirs = other.allDependencies;
187
+ const added = {};
188
+ const removed = {};
189
+ const changed = {};
190
+ for (const [name, version] of Object.entries(mine)) if (!Object.hasOwn(theirs, name)) added[name] = version;
191
+ else if (theirs[name] !== version) changed[name] = {
192
+ from: theirs[name],
193
+ to: version
194
+ };
195
+ for (const [name, version] of Object.entries(theirs)) if (!Object.hasOwn(mine, name)) removed[name] = version;
196
+ return {
197
+ added,
198
+ removed,
199
+ changed
200
+ };
201
+ }
202
+ /**
203
+ * Project to `@effected/lockfiles`' `WorkspaceManifest` — the input shape of
204
+ * `LockfileIntegrity.compare`. Total.
205
+ */
206
+ toWorkspaceManifest() {
207
+ return WorkspaceManifest.make({
208
+ name: this.name,
209
+ dependencies: this.dependencies,
210
+ devDependencies: this.devDependencies,
211
+ peerDependencies: this.peerDependencies,
212
+ optionalDependencies: this.optionalDependencies
213
+ });
214
+ }
215
+ /**
216
+ * Read and decode this package's `package.json` into the strict
217
+ * `@effected/package-json` `Package` model — the bridge from the
218
+ * tolerant discovery projection to the fully typed manifest.
219
+ */
220
+ static manifest = Effect.fn("WorkspacePackage.manifest")(function* (self) {
221
+ const content = yield* (yield* FileSystem.FileSystem).readFileString(self.packageJsonPath).pipe(Effect.mapError((cause) => new WorkspaceManifestError({
222
+ packageJsonPath: self.packageJsonPath,
223
+ kind: "read",
224
+ cause
225
+ })));
226
+ const raw = yield* Effect.try({
227
+ try: () => JSON.parse(content),
228
+ catch: (cause) => new WorkspaceManifestError({
229
+ packageJsonPath: self.packageJsonPath,
230
+ kind: "decode",
231
+ cause
232
+ })
233
+ });
234
+ return yield* Package.decode(raw).pipe(Effect.mapError((cause) => new WorkspaceManifestError({
235
+ packageJsonPath: self.packageJsonPath,
236
+ kind: "decode",
237
+ cause
238
+ })));
239
+ });
240
+ /** Instance form of `WorkspacePackage.manifest`. */
241
+ manifest() {
242
+ return WorkspacePackage.manifest(this);
243
+ }
244
+ };
245
+
246
+ //#endregion
247
+ export { PublishConfig, WorkspaceManifestError, WorkspacePackage };
@@ -0,0 +1,90 @@
1
+ import { Context, Effect, FileSystem, Layer, Option, Path, Schema } from "effect";
2
+ import { Walker } from "@effected/walker";
3
+
4
+ //#region src/WorkspaceRoot.ts
5
+ /**
6
+ * The marker filenames {@link WorkspaceRoot} probes for, in priority order.
7
+ *
8
+ * @public
9
+ */
10
+ const WORKSPACE_MARKERS = ["pnpm-workspace.yaml", "package.json"];
11
+ /**
12
+ * Raised when no workspace root can be found by ascending from a directory.
13
+ *
14
+ * @remarks
15
+ * `markers` records what was probed, so the failure names the contract rather
16
+ * than paraphrasing it in prose.
17
+ *
18
+ * @public
19
+ */
20
+ var WorkspaceRootNotFoundError = class extends Schema.TaggedErrorClass()("WorkspaceRootNotFoundError", {
21
+ /** The directory the ascent started from. */
22
+ searchPath: Schema.String,
23
+ /** The marker filenames probed at each ancestor. */
24
+ markers: Schema.Array(Schema.String)
25
+ }) {
26
+ /** Renders the search path and probed markers into a one-line message. */
27
+ get message() {
28
+ return `No workspace root above ${this.searchPath} (looked for ${this.markers.join(", ")})`;
29
+ }
30
+ };
31
+ /**
32
+ * Whether `dir` is a workspace root: it holds a `pnpm-workspace.yaml`, or a
33
+ * `package.json` with a `workspaces` field.
34
+ *
35
+ * A malformed root `package.json` is "not a root", not an error — the ascent
36
+ * continues past it, matching walker's absorption contract.
37
+ */
38
+ const isWorkspaceRoot = (dir) => Effect.gen(function* () {
39
+ const fs = yield* FileSystem.FileSystem;
40
+ const path = yield* Path.Path;
41
+ if (yield* fs.exists(path.join(dir, "pnpm-workspace.yaml")).pipe(Effect.orElseSucceed(() => false))) return true;
42
+ const packageJson = path.join(dir, "package.json");
43
+ if (!(yield* fs.exists(packageJson).pipe(Effect.orElseSucceed(() => false)))) return false;
44
+ const content = yield* fs.readFileString(packageJson).pipe(Effect.orElseSucceed(() => "{}"));
45
+ const parsed = yield* Effect.try({
46
+ try: () => JSON.parse(content),
47
+ catch: () => void 0
48
+ }).pipe(Effect.orElseSucceed(() => ({})));
49
+ return parsed.workspaces !== void 0 && parsed.workspaces !== null;
50
+ });
51
+ /**
52
+ * Locates the workspace root by ascending from a starting directory.
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * import { WorkspaceRoot } from "@effected/workspaces";
57
+ * import { Effect } from "effect";
58
+ *
59
+ * const program = Effect.gen(function* () {
60
+ * const root = yield* WorkspaceRoot;
61
+ * return yield* root.find("/repo/packages/utils/src");
62
+ * });
63
+ * ```
64
+ *
65
+ * @public
66
+ */
67
+ var WorkspaceRoot = class WorkspaceRoot extends Context.Service()("@effected/workspaces/WorkspaceRoot") {
68
+ /** Builds the service over core `FileSystem` and `Path`. */
69
+ static make = Effect.gen(function* () {
70
+ const fs = yield* FileSystem.FileSystem;
71
+ const path = yield* Path.Path;
72
+ const find = Effect.fn("WorkspaceRoot.find")(function* (cwd) {
73
+ const start = path.resolve(cwd);
74
+ const chain = yield* Walker.ascend(start);
75
+ const found = yield* Walker.findRoot(chain, isWorkspaceRoot);
76
+ if (Option.isNone(found)) return yield* Effect.fail(new WorkspaceRootNotFoundError({
77
+ searchPath: start,
78
+ markers: WORKSPACE_MARKERS
79
+ }));
80
+ yield* Effect.logDebug("Workspace root found").pipe(Effect.annotateLogs("workspace.root", found.value));
81
+ return found.value;
82
+ });
83
+ return { find: (cwd) => find(cwd).pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path)) };
84
+ });
85
+ /** The live layer. */
86
+ static layer = Layer.effect(WorkspaceRoot, WorkspaceRoot.make);
87
+ };
88
+
89
+ //#endregion
90
+ export { WORKSPACE_MARKERS, WorkspaceRoot, WorkspaceRootNotFoundError };
Binary file