@effected/workspaces 0.4.1 → 0.5.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/ConfigDependencyHooks.js +7 -3
- package/WorkspaceDiscovery.js +2 -0
- package/WorkspacePackage.js +17 -0
- package/WorkspaceRoot.js +109 -7
- package/WorkspacesSync.js +4 -3
- package/index.d.ts +160 -11
- package/package.json +8 -8
package/ConfigDependencyHooks.js
CHANGED
|
@@ -98,9 +98,13 @@ var ConfigDependencyHooks = class ConfigDependencyHooks extends Context.Service(
|
|
|
98
98
|
* `hooks`-source `CatalogAssemblyError`, never a silent skip.
|
|
99
99
|
*
|
|
100
100
|
* @remarks
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
101
|
+
* Runtime-coupled by design, not node-exclusive. The `import()` below loads
|
|
102
|
+
* **and executes** a config dependency's pnpmfile in-process — code execution,
|
|
103
|
+
* not IO, so no `FileSystem` / `Path` service abstracts it. The `node:path` and
|
|
104
|
+
* `node:url` imports (`join`, `pathToFileURL`) exist only to build the URL that
|
|
105
|
+
* `import()` consumes; node and bun both implement those builtins and dynamic
|
|
106
|
+
* import, so this layer runs on either runtime. Only ever wired by
|
|
107
|
+
* `WorkspaceCatalogs.layerWithConfigDependencies`.
|
|
104
108
|
*/
|
|
105
109
|
static layerLive = Layer.succeed(ConfigDependencyHooks, { inject: (root, configDependencies, seed) => Effect.gen(function* () {
|
|
106
110
|
const names = Object.keys(configDependencies);
|
package/WorkspaceDiscovery.js
CHANGED
|
@@ -184,6 +184,7 @@ var WorkspaceDiscovery = class WorkspaceDiscovery extends Context.Service()("@ef
|
|
|
184
184
|
path: directory,
|
|
185
185
|
packageJsonPath,
|
|
186
186
|
relativePath,
|
|
187
|
+
workspaceRoot: root,
|
|
187
188
|
manifestRecord: raw,
|
|
188
189
|
...typeof raw.private === "boolean" ? { private: raw.private } : {},
|
|
189
190
|
...isStringRecord(raw.dependencies) ? { dependencies: raw.dependencies } : {},
|
|
@@ -341,6 +342,7 @@ var WorkspaceDiscovery = class WorkspaceDiscovery extends Context.Service()("@ef
|
|
|
341
342
|
* path: "/repo/packages/utils",
|
|
342
343
|
* packageJsonPath: "/repo/packages/utils/package.json",
|
|
343
344
|
* relativePath: "packages/utils",
|
|
345
|
+
* workspaceRoot: "/repo",
|
|
344
346
|
* }),
|
|
345
347
|
* ]),
|
|
346
348
|
* });
|
package/WorkspacePackage.js
CHANGED
|
@@ -78,10 +78,12 @@ var WorkspaceManifestError = class extends Schema.TaggedErrorClass()("WorkspaceM
|
|
|
78
78
|
* path: "/repo/packages/utils",
|
|
79
79
|
* packageJsonPath: "/repo/packages/utils/package.json",
|
|
80
80
|
* relativePath: "packages/utils",
|
|
81
|
+
* workspaceRoot: "/repo",
|
|
81
82
|
* });
|
|
82
83
|
*
|
|
83
84
|
* pkg.isRootWorkspace; // false
|
|
84
85
|
* pkg.unscopedName; // "utils"
|
|
86
|
+
* pkg.workspaceRoot; // "/repo"
|
|
85
87
|
* ```
|
|
86
88
|
*
|
|
87
89
|
* @public
|
|
@@ -97,6 +99,21 @@ var WorkspacePackage = class WorkspacePackage extends Schema.Class("WorkspacePac
|
|
|
97
99
|
packageJsonPath: Schema.NonEmptyString,
|
|
98
100
|
/** POSIX path relative to the workspace root; `"."` for the root package. */
|
|
99
101
|
relativePath: Schema.String,
|
|
102
|
+
/**
|
|
103
|
+
* Absolute path to the workspace root this package was discovered under.
|
|
104
|
+
*
|
|
105
|
+
* @remarks
|
|
106
|
+
* Carried, not derived. Whoever built this value already knew the root —
|
|
107
|
+
* `WorkspaceDiscovery` resolved it before enumerating, and the sync entry
|
|
108
|
+
* point is handed it — so dropping it forced every consumer into per-package
|
|
109
|
+
* root arithmetic (counting `relativePath` segments and re-ascending that
|
|
110
|
+
* many `..`). That reconstruction is exact only while `path` and
|
|
111
|
+
* `relativePath` agree, and it re-derives something the kit never had to
|
|
112
|
+
* lose.
|
|
113
|
+
*
|
|
114
|
+
* For the root package this equals `path`, and `relativePath` is `"."`.
|
|
115
|
+
*/
|
|
116
|
+
workspaceRoot: Schema.NonEmptyString,
|
|
100
117
|
/** Whether the package is marked private. */
|
|
101
118
|
private: Schema.Boolean.pipe(Schema.withDecodingDefaultKey(Effect.succeed(false)), Schema.withConstructorDefault(Effect.succeed(false))),
|
|
102
119
|
/** Production dependencies. */
|
package/WorkspaceRoot.js
CHANGED
|
@@ -21,11 +21,22 @@ var WorkspaceRootNotFoundError = class extends Schema.TaggedErrorClass()("Worksp
|
|
|
21
21
|
/** The directory the ascent started from. */
|
|
22
22
|
searchPath: Schema.String,
|
|
23
23
|
/** The marker filenames probed at each ancestor. */
|
|
24
|
-
markers: Schema.Array(Schema.String)
|
|
24
|
+
markers: Schema.Array(Schema.String),
|
|
25
|
+
/**
|
|
26
|
+
* The resolved ceiling the ascent was bounded by, when one was supplied.
|
|
27
|
+
*
|
|
28
|
+
* @remarks
|
|
29
|
+
* Absent means the ascent ran to the filesystem root. Its presence is what
|
|
30
|
+
* lets a caller tell "there is no workspace root anywhere above me" from
|
|
31
|
+
* "there is none below the ceiling I set" — two failures that otherwise
|
|
32
|
+
* render identically.
|
|
33
|
+
*/
|
|
34
|
+
stopAt: Schema.optionalKey(Schema.String)
|
|
25
35
|
}) {
|
|
26
|
-
/** Renders the search path
|
|
36
|
+
/** Renders the search path, probed markers and any ceiling into a one-line message. */
|
|
27
37
|
get message() {
|
|
28
|
-
|
|
38
|
+
const bound = this.stopAt === void 0 ? "" : ` up to ${this.stopAt}`;
|
|
39
|
+
return `No workspace root above ${this.searchPath}${bound} (looked for ${this.markers.join(", ")})`;
|
|
29
40
|
}
|
|
30
41
|
};
|
|
31
42
|
/**
|
|
@@ -48,6 +59,12 @@ const isWorkspaceRoot = (dir) => Effect.gen(function* () {
|
|
|
48
59
|
}).pipe(Effect.orElseSucceed(() => ({})));
|
|
49
60
|
return parsed.workspaces !== void 0 && parsed.workspaces !== null;
|
|
50
61
|
});
|
|
62
|
+
/** Whether `descendant` is `ancestor` itself or lies beneath it, on POSIX or win32 separators. */
|
|
63
|
+
const isAtOrBelow = (descendant, ancestor) => {
|
|
64
|
+
if (descendant === ancestor) return true;
|
|
65
|
+
const prefix = ancestor.endsWith("/") || ancestor.endsWith("\\") ? ancestor : `${ancestor}/`;
|
|
66
|
+
return descendant.startsWith(prefix) || descendant.startsWith(prefix.replace(/\/$/, "\\"));
|
|
67
|
+
};
|
|
51
68
|
/**
|
|
52
69
|
* Locates the workspace root by ascending from a starting directory.
|
|
53
70
|
*
|
|
@@ -62,6 +79,20 @@ const isWorkspaceRoot = (dir) => Effect.gen(function* () {
|
|
|
62
79
|
* });
|
|
63
80
|
* ```
|
|
64
81
|
*
|
|
82
|
+
* @example
|
|
83
|
+
* Bounded: an unmarked fixture directory fails typed instead of escaping into
|
|
84
|
+
* the enclosing repository.
|
|
85
|
+
*
|
|
86
|
+
* ```ts
|
|
87
|
+
* import { WorkspaceRoot } from "@effected/workspaces";
|
|
88
|
+
* import { Effect } from "effect";
|
|
89
|
+
*
|
|
90
|
+
* const program = Effect.gen(function* () {
|
|
91
|
+
* const roots = yield* WorkspaceRoot;
|
|
92
|
+
* return yield* roots.find("/tmp/fixture/packages/a", { stopAt: "/tmp/fixture" });
|
|
93
|
+
* });
|
|
94
|
+
* ```
|
|
95
|
+
*
|
|
65
96
|
* @public
|
|
66
97
|
*/
|
|
67
98
|
var WorkspaceRoot = class WorkspaceRoot extends Context.Service()("@effected/workspaces/WorkspaceRoot") {
|
|
@@ -69,21 +100,92 @@ var WorkspaceRoot = class WorkspaceRoot extends Context.Service()("@effected/wor
|
|
|
69
100
|
static make = Effect.gen(function* () {
|
|
70
101
|
const fs = yield* FileSystem.FileSystem;
|
|
71
102
|
const path = yield* Path.Path;
|
|
72
|
-
const find = Effect.fn("WorkspaceRoot.find")(function* (cwd) {
|
|
103
|
+
const find = Effect.fn("WorkspaceRoot.find")(function* (cwd, options) {
|
|
73
104
|
const start = path.resolve(cwd);
|
|
74
|
-
const
|
|
105
|
+
const ceiling = options?.stopAt === void 0 ? void 0 : path.resolve(options.stopAt);
|
|
106
|
+
const chain = yield* Walker.ascend(start, {
|
|
107
|
+
...ceiling === void 0 ? {} : { stopAt: ceiling },
|
|
108
|
+
...options?.maxDepth === void 0 ? {} : { maxDepth: options.maxDepth }
|
|
109
|
+
});
|
|
75
110
|
const found = yield* Walker.findRoot(chain, isWorkspaceRoot);
|
|
76
111
|
if (Option.isNone(found)) return yield* Effect.fail(new WorkspaceRootNotFoundError({
|
|
77
112
|
searchPath: start,
|
|
78
|
-
markers: WORKSPACE_MARKERS
|
|
113
|
+
markers: WORKSPACE_MARKERS,
|
|
114
|
+
...ceiling === void 0 ? {} : { stopAt: ceiling }
|
|
79
115
|
}));
|
|
80
116
|
yield* Effect.logDebug("Workspace root found").pipe(Effect.annotateLogs("workspace.root", found.value));
|
|
81
117
|
return found.value;
|
|
82
118
|
});
|
|
83
|
-
return { find: (cwd) => find(cwd).pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path)) };
|
|
119
|
+
return { find: (cwd, options) => find(cwd, options).pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path)) };
|
|
84
120
|
});
|
|
85
121
|
/** The live layer. */
|
|
86
122
|
static layer = Layer.effect(WorkspaceRoot, WorkspaceRoot.make);
|
|
123
|
+
/**
|
|
124
|
+
* A test double resolving every `find` to `root`, with no filesystem.
|
|
125
|
+
*
|
|
126
|
+
* @remarks
|
|
127
|
+
* The nine-copies-of-a-four-line-mock case — a `Layer.succeed` over a `find`
|
|
128
|
+
* that ignores its arguments and succeeds with a fixed root is what consumers
|
|
129
|
+
* were writing by hand. The difference is that this double **honours
|
|
130
|
+
* `stopAt`**: a hand-rolled `find` that ignores the ceiling makes a bounded
|
|
131
|
+
* call pass under test and fail against the live service, which is the
|
|
132
|
+
* failure mode the option exists to catch. A `root` above the ceiling fails
|
|
133
|
+
* here exactly as it would live, with the same
|
|
134
|
+
* {@link WorkspaceRootNotFoundError}.
|
|
135
|
+
*
|
|
136
|
+
* The ceiling is `path.resolve`d through the injected `Path` service before
|
|
137
|
+
* the comparison, exactly as the live `make` path does — so a `stopAt`
|
|
138
|
+
* carrying `..` segments bounds the double identically to the live service,
|
|
139
|
+
* not by raw string. This is why `makeTest` yields an `Effect` requiring
|
|
140
|
+
* `Path`: it captures the service once at construction, the same shape as
|
|
141
|
+
* `make`. Consumers reach for {@link WorkspaceRoot.layerTest}, which provides
|
|
142
|
+
* `Path.layer` internally, so the requirement never surfaces at their call
|
|
143
|
+
* site.
|
|
144
|
+
*
|
|
145
|
+
* `maxDepth` is deliberately NOT modelled: the double does not walk, so it has
|
|
146
|
+
* no depth to cap, and pretending otherwise would encode a fiction. A suite
|
|
147
|
+
* exercising the depth guard wants the live service over a fixture tree.
|
|
148
|
+
*
|
|
149
|
+
* @param root - The root every unbounded `find` resolves to.
|
|
150
|
+
*/
|
|
151
|
+
static makeTest = (root) => Effect.gen(function* () {
|
|
152
|
+
const path = yield* Path.Path;
|
|
153
|
+
return { find: (cwd, options) => options?.stopAt !== void 0 && !isAtOrBelow(root, path.resolve(options.stopAt)) ? Effect.fail(new WorkspaceRootNotFoundError({
|
|
154
|
+
searchPath: cwd,
|
|
155
|
+
markers: WORKSPACE_MARKERS,
|
|
156
|
+
stopAt: options.stopAt
|
|
157
|
+
})) : Effect.succeed(root) };
|
|
158
|
+
});
|
|
159
|
+
/**
|
|
160
|
+
* The test layer: {@link WorkspaceRoot.makeTest} with `Path.layer` provided.
|
|
161
|
+
*
|
|
162
|
+
* @remarks
|
|
163
|
+
* `makeTest` requires `Path` to normalize the `stopAt` ceiling; this layer
|
|
164
|
+
* supplies core's `Path.layer` internally, so the requirement never reaches a
|
|
165
|
+
* consumer — the published type stays `Layer.Layer<WorkspaceRoot>`.
|
|
166
|
+
*
|
|
167
|
+
* A parameterized layer factory mints a **fresh reference per call**, and
|
|
168
|
+
* layers memoize by reference — bind the result to a `const` and reuse it
|
|
169
|
+
* rather than calling `layerTest(...)` at each composition site.
|
|
170
|
+
*
|
|
171
|
+
* Pair it with `WorkspaceDiscovery.layerTest` to stand up the whole discovery
|
|
172
|
+
* path without a filesystem; between them there is nothing left for a
|
|
173
|
+
* module-level mock of `@effected/workspaces` to do, and a provided layer
|
|
174
|
+
* keeps the service graph — and its typed errors — intact.
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* ```ts
|
|
178
|
+
* import { WorkspaceDiscovery, WorkspaceRoot } from "@effected/workspaces";
|
|
179
|
+
* import { Effect } from "effect";
|
|
180
|
+
*
|
|
181
|
+
* const TestRoot = WorkspaceRoot.layerTest("/repo");
|
|
182
|
+
* const TestDiscovery = WorkspaceDiscovery.layerTest({
|
|
183
|
+
* listPackages: () => Effect.succeed([]),
|
|
184
|
+
* });
|
|
185
|
+
* // program.pipe(Effect.provide(TestRoot), Effect.provide(TestDiscovery))
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
188
|
+
static layerTest = (root) => Layer.effect(WorkspaceRoot, WorkspaceRoot.makeTest(root)).pipe(Layer.provide(Path.layer));
|
|
87
189
|
};
|
|
88
190
|
|
|
89
191
|
//#endregion
|
package/WorkspacesSync.js
CHANGED
|
@@ -113,7 +113,7 @@ const readPatternsSync = (options, root) => {
|
|
|
113
113
|
return manifestPatternsOf(readJson(fileSystem, path.join(root, "package.json")) ?? {});
|
|
114
114
|
};
|
|
115
115
|
/** Build a `WorkspacePackage` from a directory, or `null` if its manifest is unusable. */
|
|
116
|
-
const readPackageSync = (options, directory, relativePath) => {
|
|
116
|
+
const readPackageSync = (options, root, directory, relativePath) => {
|
|
117
117
|
const packageJsonPath = options.path.join(directory, "package.json");
|
|
118
118
|
const raw = readJson(options.fileSystem, packageJsonPath);
|
|
119
119
|
if (raw === void 0) return null;
|
|
@@ -130,6 +130,7 @@ const readPackageSync = (options, directory, relativePath) => {
|
|
|
130
130
|
path: directory,
|
|
131
131
|
packageJsonPath,
|
|
132
132
|
relativePath,
|
|
133
|
+
workspaceRoot: root,
|
|
133
134
|
private: raw.private === true,
|
|
134
135
|
dependencies: stringRecord(raw.dependencies) ?? {},
|
|
135
136
|
devDependencies: stringRecord(raw.devDependencies) ?? {},
|
|
@@ -233,10 +234,10 @@ const getWorkspacePackagesSync = (root, options) => {
|
|
|
233
234
|
for (const [relativePath, absolute] of [...included.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
|
|
234
235
|
if (relativePath === "." || absolute === root) continue;
|
|
235
236
|
if (globs.excludes.some((exclude) => exclude.matches(relativePath))) continue;
|
|
236
|
-
const pkg = readPackageSync(options, absolute, relativePath);
|
|
237
|
+
const pkg = readPackageSync(options, root, absolute, relativePath);
|
|
237
238
|
if (pkg !== null) members.push(pkg);
|
|
238
239
|
}
|
|
239
|
-
const rootPackage = readPackageSync(options, root, ".");
|
|
240
|
+
const rootPackage = readPackageSync(options, root, root, ".");
|
|
240
241
|
return rootPackage === null ? members : [rootPackage, ...members];
|
|
241
242
|
};
|
|
242
243
|
|
package/index.d.ts
CHANGED
|
@@ -90,6 +90,21 @@ declare const WorkspacePackage_base: Schema.Class<WorkspacePackage, Schema.Struc
|
|
|
90
90
|
readonly packageJsonPath: Schema.NonEmptyString;
|
|
91
91
|
/** POSIX path relative to the workspace root; `"."` for the root package. */
|
|
92
92
|
readonly relativePath: Schema.String;
|
|
93
|
+
/**
|
|
94
|
+
* Absolute path to the workspace root this package was discovered under.
|
|
95
|
+
*
|
|
96
|
+
* @remarks
|
|
97
|
+
* Carried, not derived. Whoever built this value already knew the root —
|
|
98
|
+
* `WorkspaceDiscovery` resolved it before enumerating, and the sync entry
|
|
99
|
+
* point is handed it — so dropping it forced every consumer into per-package
|
|
100
|
+
* root arithmetic (counting `relativePath` segments and re-ascending that
|
|
101
|
+
* many `..`). That reconstruction is exact only while `path` and
|
|
102
|
+
* `relativePath` agree, and it re-derives something the kit never had to
|
|
103
|
+
* lose.
|
|
104
|
+
*
|
|
105
|
+
* For the root package this equals `path`, and `relativePath` is `"."`.
|
|
106
|
+
*/
|
|
107
|
+
readonly workspaceRoot: Schema.NonEmptyString;
|
|
93
108
|
/** Whether the package is marked private. */
|
|
94
109
|
readonly private: Schema.withConstructorDefault<Schema.withDecodingDefaultKey<Schema.Boolean, never>>;
|
|
95
110
|
/** Production dependencies. */
|
|
@@ -135,10 +150,12 @@ declare const WorkspacePackage_base: Schema.Class<WorkspacePackage, Schema.Struc
|
|
|
135
150
|
* path: "/repo/packages/utils",
|
|
136
151
|
* packageJsonPath: "/repo/packages/utils/package.json",
|
|
137
152
|
* relativePath: "packages/utils",
|
|
153
|
+
* workspaceRoot: "/repo",
|
|
138
154
|
* });
|
|
139
155
|
*
|
|
140
156
|
* pkg.isRootWorkspace; // false
|
|
141
157
|
* pkg.unscopedName; // "utils"
|
|
158
|
+
* pkg.workspaceRoot; // "/repo"
|
|
142
159
|
* ```
|
|
143
160
|
*
|
|
144
161
|
* @public
|
|
@@ -212,11 +229,53 @@ declare class WorkspacePackage extends WorkspacePackage_base {
|
|
|
212
229
|
* @public
|
|
213
230
|
*/
|
|
214
231
|
declare const WORKSPACE_MARKERS: ReadonlyArray<string>;
|
|
232
|
+
/**
|
|
233
|
+
* Options for {@link WorkspaceRoot}'s `find`.
|
|
234
|
+
*
|
|
235
|
+
* @remarks
|
|
236
|
+
* Both bounds are passed straight through to `@effected/walker`'s
|
|
237
|
+
* `Walker.ascend`; this package does not re-decide either.
|
|
238
|
+
*
|
|
239
|
+
* @public
|
|
240
|
+
*/
|
|
241
|
+
interface FindWorkspaceRootOptions {
|
|
242
|
+
/**
|
|
243
|
+
* A ceiling directory. The ascent stops after probing it, so an unmarked
|
|
244
|
+
* `stopAt` fails typed as {@link WorkspaceRootNotFoundError} rather than
|
|
245
|
+
* silently escaping into an enclosing repository.
|
|
246
|
+
*
|
|
247
|
+
* @remarks
|
|
248
|
+
* Resolved to an absolute path before comparison, exactly as `cwd` is — a
|
|
249
|
+
* relative or non-normalized ceiling that never string-matched an ancestor
|
|
250
|
+
* would reintroduce the unbounded ascent it was passed to prevent.
|
|
251
|
+
*/
|
|
252
|
+
readonly stopAt?: string;
|
|
253
|
+
/**
|
|
254
|
+
* Hard cap on the number of directories probed.
|
|
255
|
+
*
|
|
256
|
+
* @remarks
|
|
257
|
+
* A non-integer or non-positive value is a **defect**, not a typed failure —
|
|
258
|
+
* it is developer wiring, and walker's guard raises it.
|
|
259
|
+
*
|
|
260
|
+
* @defaultValue 256
|
|
261
|
+
*/
|
|
262
|
+
readonly maxDepth?: number;
|
|
263
|
+
}
|
|
215
264
|
declare const WorkspaceRootNotFoundError_base: Schema.Class<WorkspaceRootNotFoundError, Schema.TaggedStruct<"WorkspaceRootNotFoundError", {
|
|
216
265
|
/** The directory the ascent started from. */
|
|
217
266
|
readonly searchPath: Schema.String;
|
|
218
267
|
/** The marker filenames probed at each ancestor. */
|
|
219
268
|
readonly markers: Schema.$Array<Schema.String>;
|
|
269
|
+
/**
|
|
270
|
+
* The resolved ceiling the ascent was bounded by, when one was supplied.
|
|
271
|
+
*
|
|
272
|
+
* @remarks
|
|
273
|
+
* Absent means the ascent ran to the filesystem root. Its presence is what
|
|
274
|
+
* lets a caller tell "there is no workspace root anywhere above me" from
|
|
275
|
+
* "there is none below the ceiling I set" — two failures that otherwise
|
|
276
|
+
* render identically.
|
|
277
|
+
*/
|
|
278
|
+
readonly stopAt: Schema.optionalKey<Schema.String>;
|
|
220
279
|
}>, import("effect/Cause").YieldableError>;
|
|
221
280
|
/**
|
|
222
281
|
* Raised when no workspace root can be found by ascending from a directory.
|
|
@@ -228,18 +287,32 @@ declare const WorkspaceRootNotFoundError_base: Schema.Class<WorkspaceRootNotFoun
|
|
|
228
287
|
* @public
|
|
229
288
|
*/
|
|
230
289
|
declare class WorkspaceRootNotFoundError extends WorkspaceRootNotFoundError_base {
|
|
231
|
-
/** Renders the search path
|
|
290
|
+
/** Renders the search path, probed markers and any ceiling into a one-line message. */
|
|
232
291
|
get message(): string;
|
|
233
292
|
}
|
|
234
|
-
|
|
293
|
+
/**
|
|
294
|
+
* The {@link WorkspaceRoot} service contract.
|
|
295
|
+
*
|
|
296
|
+
* @remarks
|
|
297
|
+
* Named so a consumer can type its own double — or a `Layer.succeed` — against
|
|
298
|
+
* the contract rather than re-deriving it, exactly as `WorkspaceDiscoveryShape`
|
|
299
|
+
* does. Prefer {@link WorkspaceRoot.layerTest} to hand-rolling one.
|
|
300
|
+
*
|
|
301
|
+
* @public
|
|
302
|
+
*/
|
|
303
|
+
interface WorkspaceRootShape {
|
|
235
304
|
/**
|
|
236
305
|
* The nearest workspace root at or above `cwd`.
|
|
237
306
|
*
|
|
238
307
|
* @param cwd - The directory to start the ascent from; resolved to an
|
|
239
308
|
* absolute path first.
|
|
309
|
+
* @param options - Optional ascent bounds. Unbounded by default, which
|
|
310
|
+
* walks to the filesystem root and can therefore resolve to an enclosing
|
|
311
|
+
* repository's root; pass `stopAt` when the caller knows the ceiling.
|
|
240
312
|
*/
|
|
241
|
-
readonly find: (cwd: string) => Effect.Effect<string, WorkspaceRootNotFoundError>;
|
|
242
|
-
}
|
|
313
|
+
readonly find: (cwd: string, options?: FindWorkspaceRootOptions) => Effect.Effect<string, WorkspaceRootNotFoundError>;
|
|
314
|
+
}
|
|
315
|
+
declare const WorkspaceRoot_base: Context.ServiceClass<WorkspaceRoot, "@effected/workspaces/WorkspaceRoot", WorkspaceRootShape>;
|
|
243
316
|
/**
|
|
244
317
|
* Locates the workspace root by ascending from a starting directory.
|
|
245
318
|
*
|
|
@@ -254,15 +327,86 @@ declare const WorkspaceRoot_base: Context.ServiceClass<WorkspaceRoot, "@effected
|
|
|
254
327
|
* });
|
|
255
328
|
* ```
|
|
256
329
|
*
|
|
330
|
+
* @example
|
|
331
|
+
* Bounded: an unmarked fixture directory fails typed instead of escaping into
|
|
332
|
+
* the enclosing repository.
|
|
333
|
+
*
|
|
334
|
+
* ```ts
|
|
335
|
+
* import { WorkspaceRoot } from "@effected/workspaces";
|
|
336
|
+
* import { Effect } from "effect";
|
|
337
|
+
*
|
|
338
|
+
* const program = Effect.gen(function* () {
|
|
339
|
+
* const roots = yield* WorkspaceRoot;
|
|
340
|
+
* return yield* roots.find("/tmp/fixture/packages/a", { stopAt: "/tmp/fixture" });
|
|
341
|
+
* });
|
|
342
|
+
* ```
|
|
343
|
+
*
|
|
257
344
|
* @public
|
|
258
345
|
*/
|
|
259
346
|
declare class WorkspaceRoot extends WorkspaceRoot_base {
|
|
260
347
|
/** Builds the service over core `FileSystem` and `Path`. */
|
|
261
|
-
static readonly make: Effect.Effect<
|
|
262
|
-
readonly find: (cwd: string) => Effect.Effect<string, WorkspaceRootNotFoundError>;
|
|
263
|
-
}, never, FileSystem.FileSystem | Path.Path>;
|
|
348
|
+
static readonly make: Effect.Effect<WorkspaceRootShape, never, FileSystem.FileSystem | Path.Path>;
|
|
264
349
|
/** The live layer. */
|
|
265
350
|
static readonly layer: Layer.Layer<WorkspaceRoot, never, FileSystem.FileSystem | Path.Path>;
|
|
351
|
+
/**
|
|
352
|
+
* A test double resolving every `find` to `root`, with no filesystem.
|
|
353
|
+
*
|
|
354
|
+
* @remarks
|
|
355
|
+
* The nine-copies-of-a-four-line-mock case — a `Layer.succeed` over a `find`
|
|
356
|
+
* that ignores its arguments and succeeds with a fixed root is what consumers
|
|
357
|
+
* were writing by hand. The difference is that this double **honours
|
|
358
|
+
* `stopAt`**: a hand-rolled `find` that ignores the ceiling makes a bounded
|
|
359
|
+
* call pass under test and fail against the live service, which is the
|
|
360
|
+
* failure mode the option exists to catch. A `root` above the ceiling fails
|
|
361
|
+
* here exactly as it would live, with the same
|
|
362
|
+
* {@link WorkspaceRootNotFoundError}.
|
|
363
|
+
*
|
|
364
|
+
* The ceiling is `path.resolve`d through the injected `Path` service before
|
|
365
|
+
* the comparison, exactly as the live `make` path does — so a `stopAt`
|
|
366
|
+
* carrying `..` segments bounds the double identically to the live service,
|
|
367
|
+
* not by raw string. This is why `makeTest` yields an `Effect` requiring
|
|
368
|
+
* `Path`: it captures the service once at construction, the same shape as
|
|
369
|
+
* `make`. Consumers reach for {@link WorkspaceRoot.layerTest}, which provides
|
|
370
|
+
* `Path.layer` internally, so the requirement never surfaces at their call
|
|
371
|
+
* site.
|
|
372
|
+
*
|
|
373
|
+
* `maxDepth` is deliberately NOT modelled: the double does not walk, so it has
|
|
374
|
+
* no depth to cap, and pretending otherwise would encode a fiction. A suite
|
|
375
|
+
* exercising the depth guard wants the live service over a fixture tree.
|
|
376
|
+
*
|
|
377
|
+
* @param root - The root every unbounded `find` resolves to.
|
|
378
|
+
*/
|
|
379
|
+
static readonly makeTest: (root: string) => Effect.Effect<WorkspaceRootShape, never, Path.Path>;
|
|
380
|
+
/**
|
|
381
|
+
* The test layer: {@link WorkspaceRoot.makeTest} with `Path.layer` provided.
|
|
382
|
+
*
|
|
383
|
+
* @remarks
|
|
384
|
+
* `makeTest` requires `Path` to normalize the `stopAt` ceiling; this layer
|
|
385
|
+
* supplies core's `Path.layer` internally, so the requirement never reaches a
|
|
386
|
+
* consumer — the published type stays `Layer.Layer<WorkspaceRoot>`.
|
|
387
|
+
*
|
|
388
|
+
* A parameterized layer factory mints a **fresh reference per call**, and
|
|
389
|
+
* layers memoize by reference — bind the result to a `const` and reuse it
|
|
390
|
+
* rather than calling `layerTest(...)` at each composition site.
|
|
391
|
+
*
|
|
392
|
+
* Pair it with `WorkspaceDiscovery.layerTest` to stand up the whole discovery
|
|
393
|
+
* path without a filesystem; between them there is nothing left for a
|
|
394
|
+
* module-level mock of `@effected/workspaces` to do, and a provided layer
|
|
395
|
+
* keeps the service graph — and its typed errors — intact.
|
|
396
|
+
*
|
|
397
|
+
* @example
|
|
398
|
+
* ```ts
|
|
399
|
+
* import { WorkspaceDiscovery, WorkspaceRoot } from "@effected/workspaces";
|
|
400
|
+
* import { Effect } from "effect";
|
|
401
|
+
*
|
|
402
|
+
* const TestRoot = WorkspaceRoot.layerTest("/repo");
|
|
403
|
+
* const TestDiscovery = WorkspaceDiscovery.layerTest({
|
|
404
|
+
* listPackages: () => Effect.succeed([]),
|
|
405
|
+
* });
|
|
406
|
+
* // program.pipe(Effect.provide(TestRoot), Effect.provide(TestDiscovery))
|
|
407
|
+
* ```
|
|
408
|
+
*/
|
|
409
|
+
static readonly layerTest: (root: string) => Layer.Layer<WorkspaceRoot>;
|
|
266
410
|
}
|
|
267
411
|
//#endregion
|
|
268
412
|
//#region src/WorkspaceDiscovery.d.ts
|
|
@@ -478,6 +622,7 @@ declare class WorkspaceDiscovery extends WorkspaceDiscovery_base {
|
|
|
478
622
|
* path: "/repo/packages/utils",
|
|
479
623
|
* packageJsonPath: "/repo/packages/utils/package.json",
|
|
480
624
|
* relativePath: "packages/utils",
|
|
625
|
+
* workspaceRoot: "/repo",
|
|
481
626
|
* }),
|
|
482
627
|
* ]),
|
|
483
628
|
* });
|
|
@@ -704,9 +849,13 @@ declare class ConfigDependencyHooks extends ConfigDependencyHooks_base {
|
|
|
704
849
|
* `hooks`-source `CatalogAssemblyError`, never a silent skip.
|
|
705
850
|
*
|
|
706
851
|
* @remarks
|
|
707
|
-
*
|
|
708
|
-
*
|
|
709
|
-
*
|
|
852
|
+
* Runtime-coupled by design, not node-exclusive. The `import()` below loads
|
|
853
|
+
* **and executes** a config dependency's pnpmfile in-process — code execution,
|
|
854
|
+
* not IO, so no `FileSystem` / `Path` service abstracts it. The `node:path` and
|
|
855
|
+
* `node:url` imports (`join`, `pathToFileURL`) exist only to build the URL that
|
|
856
|
+
* `import()` consumes; node and bun both implement those builtins and dynamic
|
|
857
|
+
* import, so this layer runs on either runtime. Only ever wired by
|
|
858
|
+
* `WorkspaceCatalogs.layerWithConfigDependencies`.
|
|
710
859
|
*/
|
|
711
860
|
static readonly layerLive: Layer.Layer<ConfigDependencyHooks>;
|
|
712
861
|
}
|
|
@@ -1782,5 +1931,5 @@ interface GetWorkspacePackagesSyncOptions extends WorkspacesSyncOptions {
|
|
|
1782
1931
|
*/
|
|
1783
1932
|
declare const getWorkspacePackagesSync: (root: string, options: GetWorkspacePackagesSyncOptions) => ReadonlyArray<WorkspacePackage>;
|
|
1784
1933
|
//#endregion
|
|
1785
|
-
export { type CatalogAssemblyFailure, CatalogSet, ChangeDetectionError, type ChangeDetectionFailure, ChangeDetectionOptions, ChangeDetector, type ChangeDetectorShape, ConfigDependencyHooks, type ConfigDependencyHooksShape, CyclicDependencyError, type DependencyDiff, DependencyGraph, DetectedPackageManager, 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 };
|
|
1934
|
+
export { type CatalogAssemblyFailure, CatalogSet, ChangeDetectionError, type ChangeDetectionFailure, ChangeDetectionOptions, ChangeDetector, type ChangeDetectorShape, ConfigDependencyHooks, type ConfigDependencyHooksShape, CyclicDependencyError, type DependencyDiff, DependencyGraph, DetectedPackageManager, type FindWorkspaceRootOptions, 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 WorkspaceRootShape, type WorkspaceSnapshotAtFailure, type WorkspaceSnapshotWorktreeFailure, WorkspaceSnapshots, type WorkspaceSnapshotsOptions, type WorkspaceSnapshotsShape, WorkspaceStateSnapshot, Workspaces, type WorkspacesOptions, type WorkspacesServices, type WorkspacesSyncOptions, findWorkspaceRootSync, getWorkspacePackagesSync };
|
|
1786
1935
|
//# sourceMappingURL=index.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/workspaces",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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": [
|
|
@@ -47,13 +47,13 @@
|
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@effected/git": "0.4.1",
|
|
50
|
-
"@effected/glob": "0.
|
|
51
|
-
"@effected/lockfiles": "0.1.
|
|
52
|
-
"@effected/npm": "0.2.
|
|
53
|
-
"@effected/package-json": "0.
|
|
54
|
-
"@effected/semver": "0.
|
|
55
|
-
"@effected/walker": "0.
|
|
56
|
-
"@effected/yaml": "0.
|
|
50
|
+
"@effected/glob": "0.2.0",
|
|
51
|
+
"@effected/lockfiles": "0.1.6",
|
|
52
|
+
"@effected/npm": "0.2.2",
|
|
53
|
+
"@effected/package-json": "0.4.0",
|
|
54
|
+
"@effected/semver": "0.2.0",
|
|
55
|
+
"@effected/walker": "0.3.0",
|
|
56
|
+
"@effected/yaml": "0.5.0",
|
|
57
57
|
"@pnpm/catalogs.config": "^1100.0.0",
|
|
58
58
|
"@pnpm/catalogs.protocol-parser": "^1100.0.0",
|
|
59
59
|
"@pnpm/catalogs.resolver": "^1100.0.0",
|