@effected/workspaces 0.25.0 → 0.26.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/LayerPolicy.js +133 -0
- package/PackedInstall.js +282 -0
- package/README.md +195 -0
- package/SourceBoundary.js +540 -0
- package/WorkspaceLayering.js +199 -0
- package/internal/dependencyFields.js +17 -0
- package/internal/packedInstallPlan.js +174 -0
- package/internal/sourceText.js +333 -0
- package/package.json +6 -1
- package/testing.d.ts +1382 -0
- package/testing.js +10 -0
package/testing.d.ts
ADDED
|
@@ -0,0 +1,1382 @@
|
|
|
1
|
+
import { DependencyField, WorkspaceResolver } from "@effected/npm";
|
|
2
|
+
import { Context, Duration, Effect, FileSystem, Layer, Option, Path, Schema } from "effect";
|
|
3
|
+
import { GlobPattern } from "@effected/glob";
|
|
4
|
+
import { WorkspaceManifest } from "@effected/lockfiles";
|
|
5
|
+
import { Package } from "@effected/package-json";
|
|
6
|
+
//#region src/LayerPolicy.d.ts
|
|
7
|
+
declare const LayerPolicyError_base: Schema.Class<LayerPolicyError, Schema.TaggedStruct<"LayerPolicyError", {
|
|
8
|
+
/** `read`: the file could not be read. `json`: it is not JSON. `decode`: it is not a `LayerPolicy`. */
|
|
9
|
+
readonly reason: Schema.Literals<readonly ["read", "json", "decode"]>;
|
|
10
|
+
/** The file, when the policy came from one. */
|
|
11
|
+
readonly path: Schema.optionalKey<Schema.String>;
|
|
12
|
+
/** The originating failure. */
|
|
13
|
+
readonly cause: Schema.Defect;
|
|
14
|
+
}>, import("effect/Cause").YieldableError>;
|
|
15
|
+
/**
|
|
16
|
+
* Raised when a layer policy cannot be read, parsed or decoded.
|
|
17
|
+
*
|
|
18
|
+
* @public
|
|
19
|
+
*/
|
|
20
|
+
export declare class LayerPolicyError extends LayerPolicyError_base {
|
|
21
|
+
/** Renders the failure kind and the file into one line. */
|
|
22
|
+
get message(): string;
|
|
23
|
+
}
|
|
24
|
+
declare const LayerPolicy_base: Schema.Class<LayerPolicy, Schema.Struct<{
|
|
25
|
+
/** Exact package names (never relative paths) per layer, top layer first. */
|
|
26
|
+
readonly layers: Schema.$Array<Schema.$Array<Schema.String>>;
|
|
27
|
+
/** Exact package names (never relative paths) any layer may depend on that never depend on a layer. */
|
|
28
|
+
readonly tooling: Schema.$Array<Schema.String>;
|
|
29
|
+
/** Globs matched against package names (never relative paths) for packages whose own edges are not checked. */
|
|
30
|
+
readonly unconstrained: Schema.$Array<Schema.String>;
|
|
31
|
+
/** The dependency maps to check. Absent means all four. */
|
|
32
|
+
readonly fields: Schema.optionalKey<Schema.$Array<Schema.Literals<readonly ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]>>>;
|
|
33
|
+
/** Edges that must exist, written `"a -> b"`. */
|
|
34
|
+
readonly requiredEdges: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
35
|
+
}>, {}>;
|
|
36
|
+
/**
|
|
37
|
+
* A committed dependency-layering policy (`layers.json`).
|
|
38
|
+
*
|
|
39
|
+
* @remarks
|
|
40
|
+
* `layers` is top-down: an edge may only point from a layer to one BELOW it
|
|
41
|
+
* (a higher index), or into `tooling`. `tooling` packages may depend on each
|
|
42
|
+
* other but never reach a layer. `unconstrained` holds globs (through
|
|
43
|
+
* `@effected/glob`) for packages whose own edges are not checked (a test
|
|
44
|
+
* harness, the private root). `fields` narrows which dependency maps count,
|
|
45
|
+
* defaulting to all four. `requiredEdges` (`"a -> b"`) are edges that must
|
|
46
|
+
* exist in a checked field: the non-vacuity guard against a discovery that
|
|
47
|
+
* quietly drops real edges.
|
|
48
|
+
*
|
|
49
|
+
* Every entry matches a package's `name`, never its `relativePath`: `layers`
|
|
50
|
+
* and `tooling` list exact names, and `unconstrained` globs match names. A
|
|
51
|
+
* private root named `my-monorepo` at `relativePath` `"."` is classified as
|
|
52
|
+
* `"my-monorepo"`; an entry of `"."` or `"packages/*"` classifies nothing.
|
|
53
|
+
*
|
|
54
|
+
* Decoding is strict: a key the policy does not model fails `decode`, and the
|
|
55
|
+
* message names every such key. A typo on an optional key would otherwise be
|
|
56
|
+
* silently dropped: `requiredEdge` would remove the non-vacuity guard and
|
|
57
|
+
* leave the report green. `$schema` is always accepted. A file that carries
|
|
58
|
+
* keys of its own, such as systems' `harness`, passes them in `allowKeys`.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* import { NodeServices } from "@effect/platform-node";
|
|
63
|
+
* import { LayerPolicy } from "@effected/workspaces/testing";
|
|
64
|
+
* import { Effect } from "effect";
|
|
65
|
+
*
|
|
66
|
+
* const policy = LayerPolicy.load("/repo/layers.json").pipe(Effect.provide(NodeServices.layer));
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* @public
|
|
70
|
+
*/
|
|
71
|
+
export declare class LayerPolicy extends LayerPolicy_base {
|
|
72
|
+
/** The dependency maps a check reads: `fields`, or all four. */
|
|
73
|
+
get effectiveFields(): ReadonlyArray<DependencyField>;
|
|
74
|
+
/**
|
|
75
|
+
* Decode a policy from a parsed JSON value.
|
|
76
|
+
*
|
|
77
|
+
* @remarks
|
|
78
|
+
* Every key the policy does not model fails, except `$schema` and the
|
|
79
|
+
* keys named in `options.allowKeys`, which are dropped before decoding.
|
|
80
|
+
* `options.path` names the file in the error.
|
|
81
|
+
*/
|
|
82
|
+
static readonly decode: (input: unknown, options?: {
|
|
83
|
+
readonly path?: string | undefined;
|
|
84
|
+
readonly allowKeys?: ReadonlyArray<string> | undefined;
|
|
85
|
+
} | undefined) => Effect.Effect<LayerPolicy, LayerPolicyError, never>;
|
|
86
|
+
/** Read, parse and decode the policy file at `path`; `options.allowKeys` is passed to `decode`. */
|
|
87
|
+
static readonly load: (path: string, options?: {
|
|
88
|
+
readonly allowKeys?: ReadonlyArray<string> | undefined;
|
|
89
|
+
} | undefined) => Effect.Effect<LayerPolicy, LayerPolicyError, FileSystem.FileSystem>;
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/WorkspacePackage.d.ts
|
|
93
|
+
declare const PublishConfig_base: Schema.Class<PublishConfig, Schema.Struct<{
|
|
94
|
+
/** Scoped-package visibility. Its presence overrides `private`. */
|
|
95
|
+
readonly access: Schema.optionalKey<Schema.Literals<readonly ["public", "restricted"]>>;
|
|
96
|
+
/** The registry to publish to. */
|
|
97
|
+
readonly registry: Schema.optionalKey<Schema.String>;
|
|
98
|
+
/** A subdirectory to publish instead of the package root. */
|
|
99
|
+
readonly directory: Schema.optionalKey<Schema.String>;
|
|
100
|
+
/**
|
|
101
|
+
* Whether workspace links point into `directory` during local development —
|
|
102
|
+
* pnpm symlinks the publish directory instead of the package root, so
|
|
103
|
+
* siblings resolve the built artifact they would install from the registry.
|
|
104
|
+
* Meaningful only alongside `directory`.
|
|
105
|
+
*/
|
|
106
|
+
readonly linkDirectory: Schema.optionalKey<Schema.Boolean>;
|
|
107
|
+
/** The dist-tag to publish under. */
|
|
108
|
+
readonly tag: Schema.optionalKey<Schema.String>;
|
|
109
|
+
}>, {}>;
|
|
110
|
+
/**
|
|
111
|
+
* The `publishConfig` fields workspace tooling reads.
|
|
112
|
+
*
|
|
113
|
+
* @remarks
|
|
114
|
+
* Deliberately narrow. `@effected/package-json` models `publishConfig` as an
|
|
115
|
+
* open `Record<string, unknown>` for round-trip fidelity, which preserves every
|
|
116
|
+
* key but types none of them; this is the typed projection of the handful that
|
|
117
|
+
* decide *where, whether and as what* a package publishes. Unknown keys are
|
|
118
|
+
* ignored, not rejected.
|
|
119
|
+
*
|
|
120
|
+
* @public
|
|
121
|
+
*/
|
|
122
|
+
export declare class PublishConfig extends PublishConfig_base {}
|
|
123
|
+
/**
|
|
124
|
+
* The result of comparing two {@link WorkspacePackage} dependency snapshots.
|
|
125
|
+
*
|
|
126
|
+
* @remarks
|
|
127
|
+
* Comparison runs across all four dependency kinds combined, so a dependency
|
|
128
|
+
* that moves between kinds at the same version does not appear in the diff.
|
|
129
|
+
*
|
|
130
|
+
* @public
|
|
131
|
+
*/
|
|
132
|
+
interface DependencyDiff {
|
|
133
|
+
/** Present in the receiver, absent from the other. */
|
|
134
|
+
readonly added: Record<string, string>;
|
|
135
|
+
/** Present in the other, absent from the receiver. */
|
|
136
|
+
readonly removed: Record<string, string>;
|
|
137
|
+
/** Present in both at different specifiers. */
|
|
138
|
+
readonly changed: Record<string, {
|
|
139
|
+
readonly from: string;
|
|
140
|
+
readonly to: string;
|
|
141
|
+
}>;
|
|
142
|
+
}
|
|
143
|
+
declare const WorkspaceManifestError_base: Schema.Class<WorkspaceManifestError, Schema.TaggedStruct<"WorkspaceManifestError", {
|
|
144
|
+
/** Absolute path to the `package.json` that failed. */
|
|
145
|
+
readonly packageJsonPath: Schema.String;
|
|
146
|
+
/** Whether the file could not be read, or read but not decoded. */
|
|
147
|
+
readonly kind: Schema.Literals<readonly ["read", "decode"]>;
|
|
148
|
+
/** The originating failure, preserved rather than flattened to a string. */
|
|
149
|
+
readonly cause: Schema.Defect;
|
|
150
|
+
}>, import("effect/Cause").YieldableError>;
|
|
151
|
+
/**
|
|
152
|
+
* Raised when a workspace member's `package.json` cannot be read or decoded
|
|
153
|
+
* into the strict `@effected/package-json` `Package` model.
|
|
154
|
+
*
|
|
155
|
+
* @remarks
|
|
156
|
+
* Discovery itself never raises this — it uses the tolerant projection. Only
|
|
157
|
+
* `WorkspacePackage.manifest` does, so opting into the strict model is an
|
|
158
|
+
* explicit, individually recoverable step.
|
|
159
|
+
*
|
|
160
|
+
* @public
|
|
161
|
+
*/
|
|
162
|
+
export declare class WorkspaceManifestError extends WorkspaceManifestError_base {
|
|
163
|
+
/** Renders the path and failure kind into a one-line message. */
|
|
164
|
+
get message(): string;
|
|
165
|
+
}
|
|
166
|
+
declare const WorkspacePackage_base: Schema.Class<WorkspacePackage, Schema.Struct<{
|
|
167
|
+
/** The package name. */
|
|
168
|
+
readonly name: Schema.NonEmptyString;
|
|
169
|
+
/**
|
|
170
|
+
* The raw `version` string — deliberately not semver-validated — or absent
|
|
171
|
+
* when the manifest declares none.
|
|
172
|
+
*
|
|
173
|
+
* @remarks
|
|
174
|
+
* pnpm accepts a version-less private package, and a private monorepo root
|
|
175
|
+
* without a `version` is the ordinary shape, so discovery carries the field
|
|
176
|
+
* exactly as the manifest has it: a non-empty string when present, verbatim
|
|
177
|
+
* and un-validated, absent when the key is absent — never a `"0.0.0"`
|
|
178
|
+
* placeholder and never a present `undefined` key. **Only ABSENCE is
|
|
179
|
+
* tolerated**: a `version` that is present but not a string, or present and
|
|
180
|
+
* `""`, fails discovery as `invalidShape` on both surfaces. `""` in
|
|
181
|
+
* particular is not a pnpm shape and would resolve `workspace:^` to a bare
|
|
182
|
+
* `"^"`.
|
|
183
|
+
* Anything that needs a concrete version (`WorkspaceResolver.versionOf`,
|
|
184
|
+
* a release tag) answers the absence itself rather than inventing one.
|
|
185
|
+
*/
|
|
186
|
+
readonly version: Schema.optionalKey<Schema.String>;
|
|
187
|
+
/** Absolute path to the package directory. */
|
|
188
|
+
readonly path: Schema.NonEmptyString;
|
|
189
|
+
/** Absolute path to the package's `package.json`. */
|
|
190
|
+
readonly packageJsonPath: Schema.NonEmptyString;
|
|
191
|
+
/** POSIX path relative to the workspace root; `"."` for the root package. */
|
|
192
|
+
readonly relativePath: Schema.String;
|
|
193
|
+
/**
|
|
194
|
+
* Absolute path to the workspace root this package was discovered under.
|
|
195
|
+
*
|
|
196
|
+
* @remarks
|
|
197
|
+
* Carried, not derived. Whoever built this value already knew the root —
|
|
198
|
+
* `WorkspaceDiscovery` resolved it before enumerating, and the sync entry
|
|
199
|
+
* point is handed it — so dropping it forced every consumer into per-package
|
|
200
|
+
* root arithmetic (counting `relativePath` segments and re-ascending that
|
|
201
|
+
* many `..`). That reconstruction is exact only while `path` and
|
|
202
|
+
* `relativePath` agree, and it re-derives something the kit never had to
|
|
203
|
+
* lose.
|
|
204
|
+
*
|
|
205
|
+
* For the root package this equals `path`, and `relativePath` is `"."`.
|
|
206
|
+
*/
|
|
207
|
+
readonly workspaceRoot: Schema.NonEmptyString;
|
|
208
|
+
/** Whether the package is marked private. */
|
|
209
|
+
readonly private: Schema.withConstructorDefault<Schema.withDecodingDefaultKey<Schema.Boolean, never>>;
|
|
210
|
+
/** Production dependencies. */
|
|
211
|
+
readonly dependencies: Schema.withConstructorDefault<Schema.withDecodingDefaultKey<Schema.$Record<Schema.String, Schema.String>, never>>;
|
|
212
|
+
/** Development dependencies. */
|
|
213
|
+
readonly devDependencies: Schema.withConstructorDefault<Schema.withDecodingDefaultKey<Schema.$Record<Schema.String, Schema.String>, never>>;
|
|
214
|
+
/** Peer dependencies. */
|
|
215
|
+
readonly peerDependencies: Schema.withConstructorDefault<Schema.withDecodingDefaultKey<Schema.$Record<Schema.String, Schema.String>, never>>;
|
|
216
|
+
/** Optional dependencies. */
|
|
217
|
+
readonly optionalDependencies: Schema.withConstructorDefault<Schema.withDecodingDefaultKey<Schema.$Record<Schema.String, Schema.String>, never>>;
|
|
218
|
+
/** The `publishConfig` block, when present. */
|
|
219
|
+
readonly publishConfig: Schema.optionalKey<typeof PublishConfig>;
|
|
220
|
+
/**
|
|
221
|
+
* The package's `package.json` as read — tolerant access to every field
|
|
222
|
+
* outside the typed discovery slice (`scripts`, `exports`, …) without a
|
|
223
|
+
* second file read.
|
|
224
|
+
*
|
|
225
|
+
* @remarks
|
|
226
|
+
* Values are `unknown` and exactly what discovery parsed; nothing here is
|
|
227
|
+
* validated beyond being a record. For the strict typed model use
|
|
228
|
+
* `manifest()`, which deliberately **re-reads** the file — a point-in-time
|
|
229
|
+
* refresh this captured record cannot provide. Defaults to `{}` for
|
|
230
|
+
* construction sites and previously-serialized values that predate the
|
|
231
|
+
* field.
|
|
232
|
+
*/
|
|
233
|
+
readonly manifestRecord: Schema.withConstructorDefault<Schema.withDecodingDefaultKey<Schema.$Record<Schema.String, Schema.Unknown>, never>>;
|
|
234
|
+
}>, {}>;
|
|
235
|
+
/**
|
|
236
|
+
* A single package inside a workspace: the discovery-relevant slice of its
|
|
237
|
+
* `package.json` plus its filesystem location.
|
|
238
|
+
*
|
|
239
|
+
* @remarks
|
|
240
|
+
* Produced by `WorkspaceDiscovery` for every directory the `packages:` patterns
|
|
241
|
+
* enumerate. The root package is always present with `relativePath` `"."`.
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* ```ts
|
|
245
|
+
* import { WorkspacePackage } from "@effected/workspaces";
|
|
246
|
+
*
|
|
247
|
+
* const pkg = WorkspacePackage.make({
|
|
248
|
+
* name: "@my-org/utils",
|
|
249
|
+
* version: "1.0.0",
|
|
250
|
+
* path: "/repo/packages/utils",
|
|
251
|
+
* packageJsonPath: "/repo/packages/utils/package.json",
|
|
252
|
+
* relativePath: "packages/utils",
|
|
253
|
+
* workspaceRoot: "/repo",
|
|
254
|
+
* });
|
|
255
|
+
*
|
|
256
|
+
* pkg.isRootWorkspace; // false
|
|
257
|
+
* pkg.unscopedName; // "utils"
|
|
258
|
+
* pkg.workspaceRoot; // "/repo"
|
|
259
|
+
* ```
|
|
260
|
+
*
|
|
261
|
+
* @public
|
|
262
|
+
*/
|
|
263
|
+
export declare class WorkspacePackage extends WorkspacePackage_base {
|
|
264
|
+
/** Whether this is the workspace root package. */
|
|
265
|
+
get isRootWorkspace(): boolean;
|
|
266
|
+
/** Whether the package is publishable in principle (not marked private). */
|
|
267
|
+
get isPublic(): boolean;
|
|
268
|
+
/** The npm scope (`@org`), or `Option.none()` for an unscoped name. */
|
|
269
|
+
get scope(): Option.Option<string>;
|
|
270
|
+
/** The name with any scope stripped. */
|
|
271
|
+
get unscopedName(): string;
|
|
272
|
+
/**
|
|
273
|
+
* Every dependency, merged across the four kinds.
|
|
274
|
+
*
|
|
275
|
+
* @remarks
|
|
276
|
+
* Precedence on a name declared in several kinds runs
|
|
277
|
+
* `dependencies` \> `devDependencies` \> `peerDependencies` \>
|
|
278
|
+
* `optionalDependencies`.
|
|
279
|
+
*/
|
|
280
|
+
get allDependencies(): Record<string, string>;
|
|
281
|
+
/** Whether `name` is a production dependency. */
|
|
282
|
+
hasDependency(name: string): boolean;
|
|
283
|
+
/** Whether `name` is a development dependency. */
|
|
284
|
+
hasDevDependency(name: string): boolean;
|
|
285
|
+
/** Whether `name` is a peer dependency. */
|
|
286
|
+
hasPeerDependency(name: string): boolean;
|
|
287
|
+
/** Whether `name` is an optional dependency. */
|
|
288
|
+
hasOptionalDependency(name: string): boolean;
|
|
289
|
+
/** Whether `name` appears in any of the four dependency kinds. */
|
|
290
|
+
hasAnyDependencyOn(name: string): boolean;
|
|
291
|
+
/** The declared specifier for `name`, searched across all four kinds. */
|
|
292
|
+
dependencyVersion(name: string): Option.Option<string>;
|
|
293
|
+
/**
|
|
294
|
+
* Whether any dependency name matches `pattern` — the `minimatch` runtime
|
|
295
|
+
* dependency's one call site, now over `@effected/glob`'s vendored engine.
|
|
296
|
+
*
|
|
297
|
+
* @remarks
|
|
298
|
+
* A `GlobPattern` is total and free to test. A `string` is compiled on every
|
|
299
|
+
* call and an **uncompilable** literal throws: a glob written into a call
|
|
300
|
+
* site is developer wiring, not untrusted input, so it belongs in the defect
|
|
301
|
+
* channel rather than widening the typed channel every caller must branch
|
|
302
|
+
* on. Compile once with `GlobPattern.compile` and pass the result when
|
|
303
|
+
* testing many packages.
|
|
304
|
+
*
|
|
305
|
+
* @param pattern - A compiled pattern, or a source string to compile.
|
|
306
|
+
*/
|
|
307
|
+
matchesDependency(pattern: GlobPattern | string): boolean;
|
|
308
|
+
/** Compare this package's dependencies against `other`'s. */
|
|
309
|
+
dependencyDiff(other: WorkspacePackage): DependencyDiff;
|
|
310
|
+
/**
|
|
311
|
+
* Project to `@effected/lockfiles`' `WorkspaceManifest` — the input shape of
|
|
312
|
+
* `LockfileIntegrity.compare`. Total.
|
|
313
|
+
*/
|
|
314
|
+
toWorkspaceManifest(): WorkspaceManifest;
|
|
315
|
+
/**
|
|
316
|
+
* Read and decode this package's `package.json` into the strict
|
|
317
|
+
* `@effected/package-json` `Package` model — the bridge from the
|
|
318
|
+
* tolerant discovery projection to the fully typed manifest.
|
|
319
|
+
*/
|
|
320
|
+
static readonly manifest: (self: WorkspacePackage) => Effect.Effect<Package, WorkspaceManifestError, FileSystem.FileSystem>;
|
|
321
|
+
/** Instance form of `WorkspacePackage.manifest`. */
|
|
322
|
+
manifest(): Effect.Effect<Package, WorkspaceManifestError, FileSystem.FileSystem>;
|
|
323
|
+
}
|
|
324
|
+
//#endregion
|
|
325
|
+
//#region src/PackageManagerName.d.ts
|
|
326
|
+
/**
|
|
327
|
+
* The four package managers this package understands.
|
|
328
|
+
*
|
|
329
|
+
* @public
|
|
330
|
+
*/
|
|
331
|
+
export declare const PackageManagerName: Schema.Literals<readonly ["npm", "pnpm", "yarn", "bun"]>;
|
|
332
|
+
/**
|
|
333
|
+
* The decoded type of {@link (PackageManagerName:variable)}: `"npm" | "pnpm" | "yarn" | "bun"`.
|
|
334
|
+
*
|
|
335
|
+
* @public
|
|
336
|
+
*/
|
|
337
|
+
export type PackageManagerName = typeof PackageManagerName.Type;
|
|
338
|
+
//#endregion
|
|
339
|
+
//#region src/WorkspaceRoot.d.ts
|
|
340
|
+
/**
|
|
341
|
+
* Options for {@link WorkspaceRoot}'s `find`.
|
|
342
|
+
*
|
|
343
|
+
* @remarks
|
|
344
|
+
* Both bounds are passed straight through to `@effected/walker`'s
|
|
345
|
+
* `Walker.ascend`; this package does not re-decide either.
|
|
346
|
+
*
|
|
347
|
+
* @public
|
|
348
|
+
*/
|
|
349
|
+
interface FindWorkspaceRootOptions {
|
|
350
|
+
/**
|
|
351
|
+
* A ceiling directory. The ascent stops after probing it, so an unmarked
|
|
352
|
+
* `stopAt` fails typed as {@link WorkspaceRootNotFoundError} rather than
|
|
353
|
+
* silently escaping into an enclosing repository.
|
|
354
|
+
*
|
|
355
|
+
* @remarks
|
|
356
|
+
* Resolved to an absolute path before comparison, exactly as `cwd` is — a
|
|
357
|
+
* relative or non-normalized ceiling that never string-matched an ancestor
|
|
358
|
+
* would reintroduce the unbounded ascent it was passed to prevent.
|
|
359
|
+
*/
|
|
360
|
+
readonly stopAt?: string;
|
|
361
|
+
/**
|
|
362
|
+
* Hard cap on the number of directories probed.
|
|
363
|
+
*
|
|
364
|
+
* @remarks
|
|
365
|
+
* A non-integer or non-positive value is a **defect**, not a typed failure —
|
|
366
|
+
* it is developer wiring, and walker's guard raises it.
|
|
367
|
+
*
|
|
368
|
+
* @defaultValue 256
|
|
369
|
+
*/
|
|
370
|
+
readonly maxDepth?: number;
|
|
371
|
+
}
|
|
372
|
+
declare const WorkspaceRootNotFoundError_base: Schema.Class<WorkspaceRootNotFoundError, Schema.TaggedStruct<"WorkspaceRootNotFoundError", {
|
|
373
|
+
/** The directory the ascent started from. */
|
|
374
|
+
readonly searchPath: Schema.String;
|
|
375
|
+
/** The marker filenames probed at each ancestor. */
|
|
376
|
+
readonly markers: Schema.$Array<Schema.String>;
|
|
377
|
+
/**
|
|
378
|
+
* The resolved ceiling the ascent was bounded by, when one was supplied.
|
|
379
|
+
*
|
|
380
|
+
* @remarks
|
|
381
|
+
* Absent means the ascent ran to the filesystem root. Its presence is what
|
|
382
|
+
* lets a caller tell "there is no workspace root anywhere above me" from
|
|
383
|
+
* "there is none below the ceiling I set" — two failures that otherwise
|
|
384
|
+
* render identically.
|
|
385
|
+
*/
|
|
386
|
+
readonly stopAt: Schema.optionalKey<Schema.String>;
|
|
387
|
+
}>, import("effect/Cause").YieldableError>;
|
|
388
|
+
/**
|
|
389
|
+
* Raised when no workspace root can be found by ascending from a directory.
|
|
390
|
+
*
|
|
391
|
+
* @remarks
|
|
392
|
+
* `markers` records what was probed, so the failure names the contract rather
|
|
393
|
+
* than paraphrasing it in prose.
|
|
394
|
+
*
|
|
395
|
+
* @public
|
|
396
|
+
*/
|
|
397
|
+
export declare class WorkspaceRootNotFoundError extends WorkspaceRootNotFoundError_base {
|
|
398
|
+
/** Renders the search path, probed markers and any ceiling into a one-line message. */
|
|
399
|
+
get message(): string;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* The {@link WorkspaceRoot} service contract.
|
|
403
|
+
*
|
|
404
|
+
* @remarks
|
|
405
|
+
* Named so a consumer can type its own double — or a `Layer.succeed` — against
|
|
406
|
+
* the contract rather than re-deriving it, exactly as `WorkspaceDiscoveryShape`
|
|
407
|
+
* does. Prefer {@link WorkspaceRoot.layerTest} to hand-rolling one.
|
|
408
|
+
*
|
|
409
|
+
* @public
|
|
410
|
+
*/
|
|
411
|
+
interface WorkspaceRootShape {
|
|
412
|
+
/**
|
|
413
|
+
* The nearest workspace root at or above `cwd`.
|
|
414
|
+
*
|
|
415
|
+
* @param cwd - The directory to start the ascent from; resolved to an
|
|
416
|
+
* absolute path first.
|
|
417
|
+
* @param options - Optional ascent bounds. Unbounded by default, which
|
|
418
|
+
* walks to the filesystem root and can therefore resolve to an enclosing
|
|
419
|
+
* repository's root; pass `stopAt` when the caller knows the ceiling.
|
|
420
|
+
*/
|
|
421
|
+
readonly find: (cwd: string, options?: FindWorkspaceRootOptions) => Effect.Effect<string, WorkspaceRootNotFoundError>;
|
|
422
|
+
}
|
|
423
|
+
declare const WorkspaceRoot_base: Context.ServiceClass<WorkspaceRoot, "@effected/workspaces/WorkspaceRoot", WorkspaceRootShape>;
|
|
424
|
+
/**
|
|
425
|
+
* Locates the workspace root by ascending from a starting directory.
|
|
426
|
+
*
|
|
427
|
+
* @example
|
|
428
|
+
* ```ts
|
|
429
|
+
* import { WorkspaceRoot } from "@effected/workspaces";
|
|
430
|
+
* import { Effect } from "effect";
|
|
431
|
+
*
|
|
432
|
+
* const program = Effect.gen(function* () {
|
|
433
|
+
* const root = yield* WorkspaceRoot;
|
|
434
|
+
* return yield* root.find("/repo/packages/utils/src");
|
|
435
|
+
* });
|
|
436
|
+
* ```
|
|
437
|
+
*
|
|
438
|
+
* @example
|
|
439
|
+
* Bounded: an unmarked fixture directory fails typed instead of escaping into
|
|
440
|
+
* the enclosing repository.
|
|
441
|
+
*
|
|
442
|
+
* ```ts
|
|
443
|
+
* import { WorkspaceRoot } from "@effected/workspaces";
|
|
444
|
+
* import { Effect } from "effect";
|
|
445
|
+
*
|
|
446
|
+
* const program = Effect.gen(function* () {
|
|
447
|
+
* const roots = yield* WorkspaceRoot;
|
|
448
|
+
* return yield* roots.find("/tmp/fixture/packages/a", { stopAt: "/tmp/fixture" });
|
|
449
|
+
* });
|
|
450
|
+
* ```
|
|
451
|
+
*
|
|
452
|
+
* @public
|
|
453
|
+
*/
|
|
454
|
+
export declare class WorkspaceRoot extends WorkspaceRoot_base {
|
|
455
|
+
/** Builds the service over core `FileSystem` and `Path`. */
|
|
456
|
+
static readonly make: Effect.Effect<WorkspaceRootShape, never, FileSystem.FileSystem | Path.Path>;
|
|
457
|
+
/** The live layer. */
|
|
458
|
+
static readonly layer: Layer.Layer<WorkspaceRoot, never, FileSystem.FileSystem | Path.Path>;
|
|
459
|
+
/**
|
|
460
|
+
* A test double resolving every `find` to `root`, with no filesystem.
|
|
461
|
+
*
|
|
462
|
+
* @remarks
|
|
463
|
+
* The nine-copies-of-a-four-line-mock case — a `Layer.succeed` over a `find`
|
|
464
|
+
* that ignores its arguments and succeeds with a fixed root is what consumers
|
|
465
|
+
* were writing by hand. The difference is that this double **honours
|
|
466
|
+
* `stopAt`**: a hand-rolled `find` that ignores the ceiling makes a bounded
|
|
467
|
+
* call pass under test and fail against the live service, which is the
|
|
468
|
+
* failure mode the option exists to catch. A `root` above the ceiling fails
|
|
469
|
+
* here exactly as it would live, with the same
|
|
470
|
+
* {@link WorkspaceRootNotFoundError}.
|
|
471
|
+
*
|
|
472
|
+
* The ceiling is `path.resolve`d through the injected `Path` service before
|
|
473
|
+
* the comparison, exactly as the live `make` path does — so a `stopAt`
|
|
474
|
+
* carrying `..` segments bounds the double identically to the live service,
|
|
475
|
+
* not by raw string. This is why `makeTest` yields an `Effect` requiring
|
|
476
|
+
* `Path`: it captures the service once at construction, the same shape as
|
|
477
|
+
* `make`. Consumers reach for {@link WorkspaceRoot.layerTest}, which provides
|
|
478
|
+
* `Path.layer` internally, so the requirement never surfaces at their call
|
|
479
|
+
* site.
|
|
480
|
+
*
|
|
481
|
+
* `maxDepth` is deliberately NOT modelled: the double does not walk, so it has
|
|
482
|
+
* no depth to cap, and pretending otherwise would encode a fiction. A suite
|
|
483
|
+
* exercising the depth guard wants the live service over a fixture tree.
|
|
484
|
+
*
|
|
485
|
+
* @param root - The root every unbounded `find` resolves to.
|
|
486
|
+
*/
|
|
487
|
+
static readonly makeTest: (root: string) => Effect.Effect<WorkspaceRootShape, never, Path.Path>;
|
|
488
|
+
/**
|
|
489
|
+
* The test layer: {@link WorkspaceRoot.makeTest} with `Path.layer` provided.
|
|
490
|
+
*
|
|
491
|
+
* @remarks
|
|
492
|
+
* `makeTest` requires `Path` to normalize the `stopAt` ceiling; this layer
|
|
493
|
+
* supplies core's `Path.layer` internally, so the requirement never reaches a
|
|
494
|
+
* consumer — the published type stays `Layer.Layer<WorkspaceRoot>`.
|
|
495
|
+
*
|
|
496
|
+
* A parameterized layer factory mints a **fresh reference per call**, and
|
|
497
|
+
* layers memoize by reference — bind the result to a `const` and reuse it
|
|
498
|
+
* rather than calling `layerTest(...)` at each composition site.
|
|
499
|
+
*
|
|
500
|
+
* Pair it with `WorkspaceDiscovery.layerTest` to stand up the whole discovery
|
|
501
|
+
* path without a filesystem; between them there is nothing left for a
|
|
502
|
+
* module-level mock of `@effected/workspaces` to do, and a provided layer
|
|
503
|
+
* keeps the service graph — and its typed errors — intact.
|
|
504
|
+
*
|
|
505
|
+
* @example
|
|
506
|
+
* ```ts
|
|
507
|
+
* import { WorkspaceDiscovery, WorkspaceRoot } from "@effected/workspaces";
|
|
508
|
+
* import { Effect } from "effect";
|
|
509
|
+
*
|
|
510
|
+
* const TestRoot = WorkspaceRoot.layerTest("/repo");
|
|
511
|
+
* const TestDiscovery = WorkspaceDiscovery.layerTest({
|
|
512
|
+
* listPackages: () => Effect.succeed([]),
|
|
513
|
+
* });
|
|
514
|
+
* // program.pipe(Effect.provide(TestRoot), Effect.provide(TestDiscovery))
|
|
515
|
+
* ```
|
|
516
|
+
*/
|
|
517
|
+
static readonly layerTest: (root: string) => Layer.Layer<WorkspaceRoot>;
|
|
518
|
+
}
|
|
519
|
+
//#endregion
|
|
520
|
+
//#region src/WorkspaceDiscovery.d.ts
|
|
521
|
+
declare const WorkspaceDiscoveryError_base: Schema.Class<WorkspaceDiscoveryError, Schema.TaggedStruct<"WorkspaceDiscoveryError", {
|
|
522
|
+
/** The workspace root discovery was running against. */
|
|
523
|
+
readonly root: Schema.String;
|
|
524
|
+
/** The file that failed. */
|
|
525
|
+
readonly path: Schema.String;
|
|
526
|
+
/** What went wrong with it. */
|
|
527
|
+
readonly kind: Schema.Literals<readonly ["read", "invalidJson", "invalidShape", "invalidYaml", "missingName"]>;
|
|
528
|
+
/** The originating failure, if there was one. */
|
|
529
|
+
readonly cause: Schema.Defect;
|
|
530
|
+
}>, import("effect/Cause").YieldableError>;
|
|
531
|
+
/**
|
|
532
|
+
* Raised when a workspace member's `package.json` cannot be read, parsed, or
|
|
533
|
+
* used — it is missing, malformed, or lacks a `name`.
|
|
534
|
+
*
|
|
535
|
+
* @remarks
|
|
536
|
+
* `kind` is the discriminant a caller branches on; `cause` preserves the
|
|
537
|
+
* originating failure rather than flattening it into a sentence.
|
|
538
|
+
*
|
|
539
|
+
* A manifest with no `version` is NOT a failure: pnpm accepts a version-less
|
|
540
|
+
* private package and a private monorepo root without one is the ordinary
|
|
541
|
+
* shape, so the member is discovered with `WorkspacePackage.version` absent.
|
|
542
|
+
* The former `missingVersion` kind is retired. Only ABSENCE is tolerated: a
|
|
543
|
+
* `version` that is present but not a string — or present and `""`, which pnpm
|
|
544
|
+
* never wrote and which would resolve `workspace:^` to a bare `"^"` — is the
|
|
545
|
+
* manifest's shape being wrong and reports `invalidShape`.
|
|
546
|
+
*
|
|
547
|
+
* @public
|
|
548
|
+
*/
|
|
549
|
+
export declare class WorkspaceDiscoveryError extends WorkspaceDiscoveryError_base {
|
|
550
|
+
/** Renders the failing file and kind into a one-line message. */
|
|
551
|
+
get message(): string;
|
|
552
|
+
}
|
|
553
|
+
declare const WorkspacePatternError_base: Schema.Class<WorkspacePatternError, Schema.TaggedStruct<"WorkspacePatternError", {
|
|
554
|
+
/** The workspace root the patterns were expanded against. */
|
|
555
|
+
readonly root: Schema.String;
|
|
556
|
+
/** The offending pattern, verbatim. */
|
|
557
|
+
readonly pattern: Schema.String;
|
|
558
|
+
/** Why it could not be enumerated. */
|
|
559
|
+
readonly kind: Schema.Literals<readonly ["missingBaseDir", "uncompilable", "depthExceeded", "budgetExceeded", "unreadableDirectory"]>;
|
|
560
|
+
/** A short, structured detail — the missing directory, or the bound exceeded. */
|
|
561
|
+
readonly detail: Schema.String;
|
|
562
|
+
}>, import("effect/Cause").YieldableError>;
|
|
563
|
+
/**
|
|
564
|
+
* Raised when a `packages:` pattern cannot be enumerated: its base directory is
|
|
565
|
+
* absent (usually a typo), the descent exceeded its depth cap, or the visit
|
|
566
|
+
* budget was exhausted.
|
|
567
|
+
*
|
|
568
|
+
* @public
|
|
569
|
+
*/
|
|
570
|
+
export declare class WorkspacePatternError extends WorkspacePatternError_base {
|
|
571
|
+
/** Renders the pattern and failure kind into a one-line message. */
|
|
572
|
+
get message(): string;
|
|
573
|
+
}
|
|
574
|
+
declare const PackageNotFoundError_base: Schema.Class<PackageNotFoundError, Schema.TaggedStruct<"PackageNotFoundError", {
|
|
575
|
+
/** The name that was requested. */
|
|
576
|
+
readonly name: Schema.String;
|
|
577
|
+
/** Every workspace package name that does exist. */
|
|
578
|
+
readonly available: Schema.$Array<Schema.String>;
|
|
579
|
+
}>, import("effect/Cause").YieldableError>;
|
|
580
|
+
/**
|
|
581
|
+
* Raised when a workspace package is requested by a name no member carries.
|
|
582
|
+
*
|
|
583
|
+
* @remarks
|
|
584
|
+
* `available` lists every known member, which is what makes the error
|
|
585
|
+
* actionable — a typo is obvious next to the list it missed.
|
|
586
|
+
*
|
|
587
|
+
* @public
|
|
588
|
+
*/
|
|
589
|
+
export declare class PackageNotFoundError extends PackageNotFoundError_base {
|
|
590
|
+
/** Renders the requested name into a one-line message. */
|
|
591
|
+
get message(): string;
|
|
592
|
+
}
|
|
593
|
+
declare const WorkspaceInfo_base: Schema.Class<WorkspaceInfo, Schema.Struct<{
|
|
594
|
+
/** Absolute path to the workspace root. */
|
|
595
|
+
readonly root: Schema.String;
|
|
596
|
+
/** The `packages:` patterns, verbatim. */
|
|
597
|
+
readonly patterns: Schema.$Array<Schema.String>;
|
|
598
|
+
}>, {}>;
|
|
599
|
+
/**
|
|
600
|
+
* Top-level facts about a workspace: where it is, what manages it, and the
|
|
601
|
+
* patterns that define its membership.
|
|
602
|
+
*
|
|
603
|
+
* @public
|
|
604
|
+
*/
|
|
605
|
+
export declare class WorkspaceInfo extends WorkspaceInfo_base {}
|
|
606
|
+
/**
|
|
607
|
+
* Every failure `WorkspaceDiscovery.getPackage` can surface: the discovery
|
|
608
|
+
* failures plus a name that matches no member.
|
|
609
|
+
*
|
|
610
|
+
* @public
|
|
611
|
+
*/
|
|
612
|
+
type WorkspaceLookupFailure = WorkspaceRootNotFoundError | WorkspaceDiscoveryError | WorkspacePatternError | PackageNotFoundError;
|
|
613
|
+
/**
|
|
614
|
+
* The error channel of the discovery methods that do not look a package up by
|
|
615
|
+
* name — everything except `getPackage`.
|
|
616
|
+
*
|
|
617
|
+
* @public
|
|
618
|
+
*/
|
|
619
|
+
type WorkspaceDiscoveryFailure = Exclude<WorkspaceLookupFailure, PackageNotFoundError>;
|
|
620
|
+
/**
|
|
621
|
+
* Options for the {@link WorkspaceDiscovery} layer.
|
|
622
|
+
*
|
|
623
|
+
* @public
|
|
624
|
+
*/
|
|
625
|
+
interface WorkspaceDiscoveryOptions {
|
|
626
|
+
/**
|
|
627
|
+
* The directory the workspace root is resolved from.
|
|
628
|
+
*
|
|
629
|
+
* @defaultValue `process.cwd()`, read lazily on first use — so a
|
|
630
|
+
* `process.chdir` between providing the layer and the first call is
|
|
631
|
+
* honoured.
|
|
632
|
+
*/
|
|
633
|
+
readonly cwd?: string;
|
|
634
|
+
/** Descent cap for segment-crossing patterns. Defaults to 32. */
|
|
635
|
+
readonly maxDepth?: number;
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* The {@link WorkspaceDiscovery} service shape.
|
|
639
|
+
*
|
|
640
|
+
* @public
|
|
641
|
+
*/
|
|
642
|
+
interface WorkspaceDiscoveryShape {
|
|
643
|
+
/** Facts about the resolved workspace. */
|
|
644
|
+
readonly info: () => Effect.Effect<WorkspaceInfo, WorkspaceDiscoveryFailure>;
|
|
645
|
+
/** Every workspace package, root first, then the rest sorted by relative path. */
|
|
646
|
+
readonly listPackages: () => Effect.Effect<ReadonlyArray<WorkspacePackage>, WorkspaceDiscoveryFailure>;
|
|
647
|
+
/** The discovered packages keyed by their root-relative importer path. */
|
|
648
|
+
readonly importerMap: () => Effect.Effect<ReadonlyMap<string, WorkspacePackage>, WorkspaceDiscoveryFailure>;
|
|
649
|
+
/** A single package by name. */
|
|
650
|
+
readonly getPackage: (name: string) => Effect.Effect<WorkspacePackage, WorkspaceLookupFailure>;
|
|
651
|
+
/** The package owning an absolute file path, by longest-prefix match. */
|
|
652
|
+
readonly resolveFile: (filePath: string) => Effect.Effect<Option.Option<WorkspacePackage>, WorkspaceDiscoveryFailure>;
|
|
653
|
+
/** The distinct packages owning any of `filePaths`. */
|
|
654
|
+
readonly resolveFiles: (filePaths: ReadonlyArray<string>) => Effect.Effect<ReadonlyArray<WorkspacePackage>, WorkspaceDiscoveryFailure>;
|
|
655
|
+
/**
|
|
656
|
+
* Facts about the workspace containing `directory`, discovered against THAT
|
|
657
|
+
* root rather than the layer-bound one.
|
|
658
|
+
*/
|
|
659
|
+
readonly infoIn: (directory: string) => Effect.Effect<WorkspaceInfo, WorkspaceDiscoveryFailure>;
|
|
660
|
+
/**
|
|
661
|
+
* Every package of the workspace containing `directory`, discovered against
|
|
662
|
+
* THAT root rather than the layer-bound one.
|
|
663
|
+
*
|
|
664
|
+
* @remarks
|
|
665
|
+
* For a **long-lived host serving many roots** — an MCP server or a language
|
|
666
|
+
* server that resolves one workspace at startup and then answers calls
|
|
667
|
+
* scoped to a git worktree, a nested repository, or another project
|
|
668
|
+
* entirely. The layer-bound {@link WorkspaceDiscoveryShape.listPackages}
|
|
669
|
+
* answers about the root discovered from `options.cwd`, which such a host
|
|
670
|
+
* has no way to vary per call without building a fresh layer.
|
|
671
|
+
*
|
|
672
|
+
* **This re-reads; it does not re-root.** The tempting cheap fix — take the
|
|
673
|
+
* layer's package list and rewrite each `path` onto the caller's directory —
|
|
674
|
+
* produces correct-looking paths over the ORIGINAL root's manifests, so a
|
|
675
|
+
* worktree whose branch adds, removes or renames a package reports the other
|
|
676
|
+
* branch's membership with no error. Patterns, member manifests, names and
|
|
677
|
+
* versions all come from beneath `directory`'s own root here.
|
|
678
|
+
*
|
|
679
|
+
* `directory` may be the workspace root or anything inside it: the root is
|
|
680
|
+
* resolved by the same upward walk the layer-bound path uses, and results are
|
|
681
|
+
* memoized per RESOLVED root, so many directories in one workspace share one
|
|
682
|
+
* discovery. The memo holds one entry per distinct root for the layer's
|
|
683
|
+
* lifetime; {@link WorkspaceDiscoveryShape.refresh} drops all of them.
|
|
684
|
+
*
|
|
685
|
+
* @param directory - Absolute path to the workspace root, or to any
|
|
686
|
+
* directory inside it.
|
|
687
|
+
*/
|
|
688
|
+
readonly listPackagesIn: (directory: string) => Effect.Effect<ReadonlyArray<WorkspacePackage>, WorkspaceDiscoveryFailure>;
|
|
689
|
+
/**
|
|
690
|
+
* Drop every memoized discovery — the layer-bound one and each per-root memo
|
|
691
|
+
* — so the next call re-reads the filesystem.
|
|
692
|
+
*/
|
|
693
|
+
readonly refresh: () => Effect.Effect<void>;
|
|
694
|
+
/**
|
|
695
|
+
* Drop only the memo for the workspace containing `directory`, leaving the
|
|
696
|
+
* layer-bound memo and every other root's untouched.
|
|
697
|
+
*
|
|
698
|
+
* @remarks
|
|
699
|
+
* The precise counterpart to {@link WorkspaceDiscoveryShape.refresh} for a
|
|
700
|
+
* host serving several roots: refreshing one worktree because it changed
|
|
701
|
+
* should not discard sibling worktrees that did not, which is all `refresh`
|
|
702
|
+
* can do.
|
|
703
|
+
*
|
|
704
|
+
* **Fails typed on a directory in no workspace**, exactly as
|
|
705
|
+
* {@link WorkspaceDiscoveryShape.listPackagesIn} does for the same input —
|
|
706
|
+
* the three per-root methods answer a bad path the same way, and a caller
|
|
707
|
+
* that would rather treat it as a no-op writes `Effect.ignore`. Refreshing a
|
|
708
|
+
* root that HAS no memo is an ordinary no-op and not an error.
|
|
709
|
+
*
|
|
710
|
+
* @param directory - Absolute path to the workspace root, or to any
|
|
711
|
+
* directory inside it.
|
|
712
|
+
*/
|
|
713
|
+
readonly refreshIn: (directory: string) => Effect.Effect<void, WorkspaceRootNotFoundError>;
|
|
714
|
+
}
|
|
715
|
+
declare const WorkspaceDiscovery_base: Context.ServiceClass<WorkspaceDiscovery, "@effected/workspaces/WorkspaceDiscovery", WorkspaceDiscoveryShape>;
|
|
716
|
+
/**
|
|
717
|
+
* Discovers the packages of a workspace.
|
|
718
|
+
*
|
|
719
|
+
* @remarks
|
|
720
|
+
* Layer construction is O(1): the root walk, pattern read, enumeration and
|
|
721
|
+
* per-package decode all happen on the first method call and are memoized for
|
|
722
|
+
* the lifetime of the layer. A Vitest reporter that builds the layer per call
|
|
723
|
+
* site and never queries it pays nothing.
|
|
724
|
+
*
|
|
725
|
+
* The memo is **success-only**. `Effect.cached` memoizes the first `Exit`,
|
|
726
|
+
* *including an interrupt* — an init interrupted by an unrelated timeout would
|
|
727
|
+
* otherwise brick the layer permanently with a cause outside its declared error
|
|
728
|
+
* channel. A failure or interrupt is therefore retried on the next call, which
|
|
729
|
+
* is a deliberate behaviour change from the v3 library.
|
|
730
|
+
*
|
|
731
|
+
* @example
|
|
732
|
+
* ```ts
|
|
733
|
+
* import { WorkspaceDiscovery } from "@effected/workspaces";
|
|
734
|
+
* import { Effect } from "effect";
|
|
735
|
+
*
|
|
736
|
+
* const program = Effect.gen(function* () {
|
|
737
|
+
* const discovery = yield* WorkspaceDiscovery;
|
|
738
|
+
* const packages = yield* discovery.listPackages();
|
|
739
|
+
* return packages.map((p) => p.name);
|
|
740
|
+
* });
|
|
741
|
+
* ```
|
|
742
|
+
*
|
|
743
|
+
* @public
|
|
744
|
+
*/
|
|
745
|
+
export declare class WorkspaceDiscovery extends WorkspaceDiscovery_base {
|
|
746
|
+
/**
|
|
747
|
+
* Builds the service. Root resolution is one explicit concern: `cwd` is an
|
|
748
|
+
* option here, never an ambient `process.cwd()` read inside a method.
|
|
749
|
+
*/
|
|
750
|
+
static readonly make: (options?: WorkspaceDiscoveryOptions) => Effect.Effect<WorkspaceDiscoveryShape, never, WorkspaceRoot | FileSystem.FileSystem | Path.Path>;
|
|
751
|
+
/**
|
|
752
|
+
* The live layer.
|
|
753
|
+
*
|
|
754
|
+
* @remarks
|
|
755
|
+
* A parameterized layer factory mints a **fresh reference per call**, and
|
|
756
|
+
* layers memoize by reference — bind the result to a `const` and reuse it
|
|
757
|
+
* rather than calling `layer(...)` at each composition site.
|
|
758
|
+
*/
|
|
759
|
+
static readonly layer: (options?: WorkspaceDiscoveryOptions) => Layer.Layer<WorkspaceDiscovery, never, WorkspaceRoot | FileSystem.FileSystem | Path.Path>;
|
|
760
|
+
/**
|
|
761
|
+
* An in-memory test double of the service shape, with every method
|
|
762
|
+
* defaulted so a test stubs only what it exercises.
|
|
763
|
+
*
|
|
764
|
+
* @remarks
|
|
765
|
+
* The defaults model an **empty workspace**, and the derived methods run
|
|
766
|
+
* over the *effective* `listPackages` — the override when one is supplied —
|
|
767
|
+
* so stubbing only `listPackages` yields a consistent double:
|
|
768
|
+
*
|
|
769
|
+
* - `listPackages` — succeeds with `[]`.
|
|
770
|
+
* - `importerMap` — derived: the packages keyed by `relativePath`.
|
|
771
|
+
* - `getPackage` — derived: a name lookup that fails with the service's own
|
|
772
|
+
* typed {@link PackageNotFoundError} on a miss, exactly as the live
|
|
773
|
+
* implementation does.
|
|
774
|
+
* - `resolveFile` / `resolveFiles` — derived: longest-prefix ownership over
|
|
775
|
+
* `pkg.path`, POSIX-terminated (`"/"`); supply a win32 double explicitly
|
|
776
|
+
* if your fixture paths are win32.
|
|
777
|
+
* - `refresh` — a no-op (`Effect.void`); there is nothing memoized to drop.
|
|
778
|
+
* - `info` — **dies** with an explanatory defect. No honest default exists
|
|
779
|
+
* (a fabricated root path would leak into consumer path logic), so an
|
|
780
|
+
* unstubbed `info()` call is a test-wiring mistake and fails loudly as a
|
|
781
|
+
* defect rather than succeeding with a lie or failing with a dishonest
|
|
782
|
+
* typed error. A defect is not absorbed by `Effect.catch` or any
|
|
783
|
+
* typed-error handler — deliberately, so code under test with a
|
|
784
|
+
* best-effort `catch` cannot make the mandatory stub look optional; the
|
|
785
|
+
* unstubbed call still fails the test.
|
|
786
|
+
*
|
|
787
|
+
* @example
|
|
788
|
+
* ```ts
|
|
789
|
+
* import { WorkspaceDiscovery, WorkspacePackage } from "@effected/workspaces";
|
|
790
|
+
* import { Effect } from "effect";
|
|
791
|
+
*
|
|
792
|
+
* const double = WorkspaceDiscovery.makeTest({
|
|
793
|
+
* listPackages: () =>
|
|
794
|
+
* Effect.succeed([
|
|
795
|
+
* WorkspacePackage.make({
|
|
796
|
+
* name: "@my-org/utils",
|
|
797
|
+
* version: "1.0.0",
|
|
798
|
+
* path: "/repo/packages/utils",
|
|
799
|
+
* packageJsonPath: "/repo/packages/utils/package.json",
|
|
800
|
+
* relativePath: "packages/utils",
|
|
801
|
+
* workspaceRoot: "/repo",
|
|
802
|
+
* }),
|
|
803
|
+
* ]),
|
|
804
|
+
* });
|
|
805
|
+
* // `getPackage`, `importerMap`, `resolveFile(s)` now answer consistently.
|
|
806
|
+
* ```
|
|
807
|
+
*/
|
|
808
|
+
static readonly makeTest: (overrides?: Partial<WorkspaceDiscoveryShape>) => WorkspaceDiscoveryShape;
|
|
809
|
+
/**
|
|
810
|
+
* The test layer: {@link WorkspaceDiscovery.makeTest} behind
|
|
811
|
+
* `Layer.succeed`, so a suite provides only the methods it exercises.
|
|
812
|
+
*
|
|
813
|
+
* @remarks
|
|
814
|
+
* A parameterized layer factory mints a **fresh reference per call**, and
|
|
815
|
+
* layers memoize by reference — bind the result to a `const` and reuse it
|
|
816
|
+
* rather than calling `layerTest(...)` at each composition site.
|
|
817
|
+
*
|
|
818
|
+
* @example
|
|
819
|
+
* ```ts
|
|
820
|
+
* import { WorkspaceDiscovery } from "@effected/workspaces";
|
|
821
|
+
* import { Effect } from "effect";
|
|
822
|
+
*
|
|
823
|
+
* const TestDiscovery = WorkspaceDiscovery.layerTest({
|
|
824
|
+
* listPackages: () => Effect.succeed([]),
|
|
825
|
+
* });
|
|
826
|
+
* // program.pipe(Effect.provide(TestDiscovery))
|
|
827
|
+
* ```
|
|
828
|
+
*/
|
|
829
|
+
static readonly layerTest: (overrides?: Partial<WorkspaceDiscoveryShape>) => Layer.Layer<WorkspaceDiscovery>;
|
|
830
|
+
/**
|
|
831
|
+
* The real implementation of `@effected/npm`'s `WorkspaceResolver` contract
|
|
832
|
+
* — the one `@effected/package-json` declares but cannot fill.
|
|
833
|
+
*
|
|
834
|
+
* @remarks
|
|
835
|
+
* `versionOf` returns `Option.none()` for a name that is not a workspace
|
|
836
|
+
* member, per the contract's convention; the `DependencyResolutionError`
|
|
837
|
+
* channel is reserved for a failure of the resolution *mechanism* (an
|
|
838
|
+
* unfindable root, an unreadable manifest), never an ordinary miss.
|
|
839
|
+
*
|
|
840
|
+
* A member that declares **no `version`** is neither: it is a known member
|
|
841
|
+
* with nothing for `workspace:` to resolve to. Answering `none` would read
|
|
842
|
+
* as "not a member" downstream, so it fails typed instead, naming the
|
|
843
|
+
* specifier — the same channel a consumer already handles for an
|
|
844
|
+
* unresolvable workspace.
|
|
845
|
+
*
|
|
846
|
+
* @example
|
|
847
|
+
* ```ts
|
|
848
|
+
* import { Package } from "@effected/package-json";
|
|
849
|
+
* import { WorkspaceDiscovery } from "@effected/workspaces";
|
|
850
|
+
* import { Layer } from "effect";
|
|
851
|
+
*
|
|
852
|
+
* const resolvers = WorkspaceDiscovery.workspaceResolver.pipe(
|
|
853
|
+
* Layer.provide(WorkspaceDiscovery.layer()),
|
|
854
|
+
* );
|
|
855
|
+
* // `Package.resolve` now resolves `workspace:*` for real.
|
|
856
|
+
* ```
|
|
857
|
+
*/
|
|
858
|
+
static readonly workspaceResolver: Layer.Layer<WorkspaceResolver, never, WorkspaceDiscovery>;
|
|
859
|
+
}
|
|
860
|
+
//#endregion
|
|
861
|
+
//#region src/PackedInstall.d.ts
|
|
862
|
+
/**
|
|
863
|
+
* Where each closure package is packed from.
|
|
864
|
+
*
|
|
865
|
+
* @remarks
|
|
866
|
+
* `{ directory }` runs `npm pack` in that subdirectory of each package, whose
|
|
867
|
+
* manifest must already be publish-ready. The default,
|
|
868
|
+
* `{ directory: "dist/prod/npm/pkg" }`, is the effected bundler's prod npm
|
|
869
|
+
* output: the same file list a release publishes.
|
|
870
|
+
*
|
|
871
|
+
* `"source"` runs `pnpm pack` in the package directory: pnpm honours
|
|
872
|
+
* `publishConfig.directory` and rewrites `workspace:` and `catalog:`
|
|
873
|
+
* specifiers. Two costs. Under the effected bundler `publishConfig.directory`
|
|
874
|
+
* names the DEV build, so source mode proves the dev artifact, not the
|
|
875
|
+
* published one. And pnpm can rewrite a `workspace:` specifier only in a
|
|
876
|
+
* workspace that has been `pnpm install`ed: in a never-installed one the pack
|
|
877
|
+
* fails with `ERR_PNPM_CANNOT_RESOLVE_WORKSPACE_PROTOCOL`, which
|
|
878
|
+
* `PackedInstall.run` reports as a `PackFailed` error naming the missing
|
|
879
|
+
* install.
|
|
880
|
+
*
|
|
881
|
+
* @public
|
|
882
|
+
*/
|
|
883
|
+
type PackSource = "source" | {
|
|
884
|
+
readonly directory: string;
|
|
885
|
+
};
|
|
886
|
+
/**
|
|
887
|
+
* Options for {@link PackedInstall.run}.
|
|
888
|
+
*
|
|
889
|
+
* @public
|
|
890
|
+
*/
|
|
891
|
+
interface PackedInstallOptions {
|
|
892
|
+
/** The carrier: a direct dependency of each scratch consumer, beside only `consumerDependencies`. */
|
|
893
|
+
readonly carrier: string;
|
|
894
|
+
/** The other workspace packages to pack and override; `"auto"` is the carrier's transitive runtime workspace dependencies. */
|
|
895
|
+
readonly closure: ReadonlyArray<string> | "auto";
|
|
896
|
+
/**
|
|
897
|
+
* The package managers to install with; each is used only if it answers
|
|
898
|
+
* `--version`. A manager listed twice is installed once; an empty list
|
|
899
|
+
* fails `NoManagerAvailable`.
|
|
900
|
+
*/
|
|
901
|
+
readonly managers: ReadonlyArray<PackageManagerName>;
|
|
902
|
+
/** `"all"` fails if any listed manager is unavailable; `"any"` (the default) needs one. */
|
|
903
|
+
readonly require?: "all" | "any" | undefined;
|
|
904
|
+
/**
|
|
905
|
+
* Where to pack from. Defaults to `{ directory: "dist/prod/npm/pkg" }`, the
|
|
906
|
+
* published artifact; see {@link PackSource} for what `"source"` costs.
|
|
907
|
+
*/
|
|
908
|
+
readonly packFrom?: PackSource | undefined;
|
|
909
|
+
/** Bin names every consumer must expose, executable, in `node_modules/.bin`. */
|
|
910
|
+
readonly bins: ReadonlyArray<string>;
|
|
911
|
+
/** The environment for every spawn: pass `process.env` from the test file. The parent manager's context is stripped. */
|
|
912
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
913
|
+
/**
|
|
914
|
+
* Extra consumer dependencies, name to range or `file:` tarball spec.
|
|
915
|
+
*
|
|
916
|
+
* @remarks
|
|
917
|
+
* Declare here every package the consumer's own code imports directly
|
|
918
|
+
* other than the carrier. pnpm's isolated layout links only the consumer's
|
|
919
|
+
* declared dependencies at its top level, so a closure package reached only
|
|
920
|
+
* through the carrier's peers or the overrides resolves inside the carrier
|
|
921
|
+
* but fails `ERR_MODULE_NOT_FOUND` when imported from the consumer root,
|
|
922
|
+
* while npm and bun hoist it and pass. That is a property of the consumer,
|
|
923
|
+
* not of the pack.
|
|
924
|
+
*
|
|
925
|
+
* An entry naming a packed package (the carrier or a closure member) is
|
|
926
|
+
* written as that package's `file:` tarball, whatever spec you pass, so
|
|
927
|
+
* any range will do: the packed tarball always wins. npm fails an install
|
|
928
|
+
* whose direct spec differs from its override (`EOVERRIDE`), so a range
|
|
929
|
+
* written through unchanged would break every npm consumer.
|
|
930
|
+
*/
|
|
931
|
+
readonly consumerDependencies?: Readonly<Record<string, string>> | undefined;
|
|
932
|
+
/** Ceiling on each install. Defaults to four minutes. Expiry fails `InstallFailed` with a message naming the manager and this duration. */
|
|
933
|
+
readonly installTimeout?: Duration.Input | undefined;
|
|
934
|
+
}
|
|
935
|
+
declare const PackedInstallError_base: Schema.Class<PackedInstallError, Schema.TaggedStruct<"PackedInstallError", {
|
|
936
|
+
/** What failed. */
|
|
937
|
+
readonly reason: Schema.Literals<readonly ["UnsupportedPlatform", "NoManagerAvailable", "ManagerUnavailable", "UnknownPackage", "PackSourceMissing", "PackFailed", "UnresolvedProtocol", "InstallFailed", "MissingBin", "Discovery", "Io"]>;
|
|
938
|
+
/** One line, naming the package or manager involved. */
|
|
939
|
+
readonly message: Schema.String;
|
|
940
|
+
/** The package manager involved. */
|
|
941
|
+
readonly manager: Schema.optionalKey<Schema.Literals<readonly ["npm", "pnpm", "yarn", "bun"]>>;
|
|
942
|
+
/** The workspace package involved. */
|
|
943
|
+
readonly package: Schema.optionalKey<Schema.String>;
|
|
944
|
+
/** The tail of the failing command's output. */
|
|
945
|
+
readonly output: Schema.optionalKey<Schema.String>;
|
|
946
|
+
/** The originating failure. */
|
|
947
|
+
readonly cause: Schema.optionalKey<Schema.Defect>;
|
|
948
|
+
}>, import("effect/Cause").YieldableError>;
|
|
949
|
+
/**
|
|
950
|
+
* Why a packed install could not be proven.
|
|
951
|
+
*
|
|
952
|
+
* @public
|
|
953
|
+
*/
|
|
954
|
+
export declare class PackedInstallError extends PackedInstallError_base {}
|
|
955
|
+
declare const InstalledConsumer_base: Schema.Class<InstalledConsumer, Schema.Struct<{
|
|
956
|
+
/** The package manager that installed it. */
|
|
957
|
+
readonly manager: Schema.Literals<readonly ["npm", "pnpm", "yarn", "bun"]>;
|
|
958
|
+
/** The version it reported and was pinned to. */
|
|
959
|
+
readonly managerVersion: Schema.String;
|
|
960
|
+
/** The consumer project directory (realpath'd). */
|
|
961
|
+
readonly directory: Schema.String;
|
|
962
|
+
}>, {}>;
|
|
963
|
+
/**
|
|
964
|
+
* One scratch project, outside the workspace, with the carrier installed.
|
|
965
|
+
*
|
|
966
|
+
* @public
|
|
967
|
+
*/
|
|
968
|
+
export declare class InstalledConsumer extends InstalledConsumer_base {
|
|
969
|
+
/** The installed bin `name`, in `node_modules/.bin`. POSIX: `PackedInstall` runs only there. */
|
|
970
|
+
binPath(name: string): string;
|
|
971
|
+
}
|
|
972
|
+
declare const PackedInstallResult_base: Schema.Class<PackedInstallResult, Schema.Struct<{
|
|
973
|
+
/** One consumer per available manager, in the order requested. */
|
|
974
|
+
readonly consumers: Schema.$Array<typeof InstalledConsumer>;
|
|
975
|
+
/** The requested managers that did not answer `--version`. */
|
|
976
|
+
readonly unavailable: Schema.$Array<Schema.Literals<readonly ["npm", "pnpm", "yarn", "bun"]>>;
|
|
977
|
+
/** Every packed package: name to absolute tarball path. */
|
|
978
|
+
readonly tarballs: Schema.$Record<Schema.String, Schema.String>;
|
|
979
|
+
}>, {}>;
|
|
980
|
+
/**
|
|
981
|
+
* What a packed install produced.
|
|
982
|
+
*
|
|
983
|
+
* @public
|
|
984
|
+
*/
|
|
985
|
+
export declare class PackedInstallResult extends PackedInstallResult_base {}
|
|
986
|
+
/**
|
|
987
|
+
* Prove a carrier's bins reach a consumer that is not part of the workspace,
|
|
988
|
+
* once per available package manager.
|
|
989
|
+
*
|
|
990
|
+
* @remarks
|
|
991
|
+
* Packs the carrier and its closure into a scoped, realpath'd scratch
|
|
992
|
+
* directory (so `file:` specs and the install cwd agree on macOS). Then, for
|
|
993
|
+
* each manager that answers `--version` from that directory (so a corepack
|
|
994
|
+
* pin in the repo cannot refuse it), it writes a consumer whose only direct
|
|
995
|
+
* dependency is the carrier tarball, steers the closure to its tarballs
|
|
996
|
+
* through the manager's own override field, pins the probed version (as
|
|
997
|
+
* `packageManager`, or for pnpm as `devEngines.packageManager` with `onFail:
|
|
998
|
+
* "ignore"`, since pnpm resolves a `packageManager` pin from the registry even
|
|
999
|
+
* when it names the running version), installs with lifecycle scripts skipped,
|
|
1000
|
+
* and checks every
|
|
1001
|
+
* expected bin is present and executable. A packed manifest that still
|
|
1002
|
+
* carries `workspace:`, `catalog:`, `link:` or a relative `file:` specifier
|
|
1003
|
+
* fails `UnresolvedProtocol` before any install.
|
|
1004
|
+
*
|
|
1005
|
+
* It asserts nothing about what the bins DO: run them from the test through
|
|
1006
|
+
* `InstalledConsumer.binPath`, for an MCP bin with `McpProbe` from
|
|
1007
|
+
* `@effected/mcp/testing`, inside the same scope, because the scratch
|
|
1008
|
+
* directory is removed when the scope closes. POSIX only.
|
|
1009
|
+
*
|
|
1010
|
+
* @example
|
|
1011
|
+
* ```ts
|
|
1012
|
+
* import { NodeServices } from "@effect/platform-node";
|
|
1013
|
+
* import { Run } from "@effected/commands";
|
|
1014
|
+
* import { Workspaces } from "@effected/workspaces";
|
|
1015
|
+
* import { PackedInstall } from "@effected/workspaces/testing";
|
|
1016
|
+
* import { Effect, Layer } from "effect";
|
|
1017
|
+
* import { ChildProcess } from "effect/unstable/process";
|
|
1018
|
+
*
|
|
1019
|
+
* const Live = Workspaces.layer({ cwd: "/repo" }).pipe(Layer.provideMerge(NodeServices.layer));
|
|
1020
|
+
*
|
|
1021
|
+
* // Use the consumers INSIDE the scope: closing it removes the scratch
|
|
1022
|
+
* // directory, so a binPath returned out of Effect.scoped points at nothing.
|
|
1023
|
+
* const program = Effect.gen(function* () {
|
|
1024
|
+
* const result = yield* PackedInstall.run({
|
|
1025
|
+
* carrier: "my-tool",
|
|
1026
|
+
* closure: "auto",
|
|
1027
|
+
* managers: ["npm", "pnpm"],
|
|
1028
|
+
* bins: ["my-tool"],
|
|
1029
|
+
* env: process.env,
|
|
1030
|
+
* });
|
|
1031
|
+
* const env = PackedInstall.scrubEnv(process.env);
|
|
1032
|
+
* for (const consumer of result.consumers) {
|
|
1033
|
+
* const version = yield* Run.text(
|
|
1034
|
+
* ChildProcess.make(consumer.binPath("my-tool"), ["--version"], { env, extendEnv: false }),
|
|
1035
|
+
* );
|
|
1036
|
+
* console.log(consumer.manager, version.trim());
|
|
1037
|
+
* }
|
|
1038
|
+
* }).pipe(Effect.scoped, Effect.provide(Live));
|
|
1039
|
+
* ```
|
|
1040
|
+
*
|
|
1041
|
+
* @public
|
|
1042
|
+
*/
|
|
1043
|
+
export declare class PackedInstall {
|
|
1044
|
+
private constructor();
|
|
1045
|
+
/** `env` without undefined values or the parent manager's context: the environment the installs run under. Reuse it to run the installed bins. */
|
|
1046
|
+
static readonly scrubEnv: (env: Readonly<Record<string, string | undefined>>) => Record<string, string>;
|
|
1047
|
+
/** Pack, then install under every available manager. */
|
|
1048
|
+
static readonly run: (options: PackedInstallOptions) => Effect.Effect<PackedInstallResult, PackedInstallError, import("effect/unstable/process/ChildProcessSpawner").ChildProcessSpawner | FileSystem.FileSystem | Path.Path | import("effect/Scope").Scope | WorkspaceDiscovery>;
|
|
1049
|
+
}
|
|
1050
|
+
//#endregion
|
|
1051
|
+
//#region src/SourceBoundary.d.ts
|
|
1052
|
+
/**
|
|
1053
|
+
* One rule a source file must keep.
|
|
1054
|
+
*
|
|
1055
|
+
* @remarks
|
|
1056
|
+
* `"process"` forbids a read of the global `process`. `"node:process"` forbids
|
|
1057
|
+
* importing `node:process` or `process`. `"stdout-write"` forbids a
|
|
1058
|
+
* `stdout.write` on anything, and a `stdout.end(chunk)`, which writes its
|
|
1059
|
+
* chunk before closing; an `end()` with no chunk is spared. It matches the
|
|
1060
|
+
* name `stdout`, so a renamed receiver (`const { stdout: o } = process`) is
|
|
1061
|
+
* not seen: the `"process"` rule is what backstops it, so waiving `"process"`
|
|
1062
|
+
* for a file waives that backstop too. `"console"` forbids any reference to the
|
|
1063
|
+
* global `console`. `"console-stdout"` forbids a reference to the global
|
|
1064
|
+
* `console` except a member access to one of the methods Node's console
|
|
1065
|
+
* writes to stderr: `error`, `warn`, `trace` and `assert`. A bare or aliased
|
|
1066
|
+
* reference (`const c = console`, `f(console)`, `console[m]`) is still
|
|
1067
|
+
* flagged, since it can reach the stdout methods. Neither console rule flags
|
|
1068
|
+
* core's `Console` service. `{ forbidImports }` forbids an import whose specifier
|
|
1069
|
+
* equals an entry, is a subpath of one, or starts with an entry's text before a
|
|
1070
|
+
* trailing `*` (so `"node:*"` and `"@effect/platform*"` are prefixes).
|
|
1071
|
+
*
|
|
1072
|
+
* `"node:*"` matches only the `node:` spelling: a bare built-in such as `"fs"`
|
|
1073
|
+
* is not caught. To forbid both, spread Node's own list from the test file,
|
|
1074
|
+
* `{ forbidImports: ["node:*", ...builtinModules] }` with `builtinModules`
|
|
1075
|
+
* from `node:module`. That also forbids npm packages that share a built-in's
|
|
1076
|
+
* name (`events`, `buffer`, `punycode`). No built-in list ships here: it
|
|
1077
|
+
* would drift with Node releases.
|
|
1078
|
+
*
|
|
1079
|
+
* @public
|
|
1080
|
+
*/
|
|
1081
|
+
type BoundaryRule = "process" | "node:process" | "stdout-write" | "console" | "console-stdout" | {
|
|
1082
|
+
readonly forbidImports: ReadonlyArray<string>;
|
|
1083
|
+
};
|
|
1084
|
+
/**
|
|
1085
|
+
* Options for the `process` rule.
|
|
1086
|
+
*
|
|
1087
|
+
* @public
|
|
1088
|
+
*/
|
|
1089
|
+
interface ReferenceOptions {
|
|
1090
|
+
/**
|
|
1091
|
+
* Exact tokens exempt from the `process` rule, each starting at the
|
|
1092
|
+
* reference (`"process.env.MY_CONSTANT"`). `process.env.__PACKAGE_VERSION__`
|
|
1093
|
+
* is always exempt: the bundler substitutes it at build time.
|
|
1094
|
+
*/
|
|
1095
|
+
readonly ignoreTokens?: ReadonlyArray<string> | undefined;
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* A snippet with the verdict one rule must reach on it.
|
|
1099
|
+
*
|
|
1100
|
+
* @public
|
|
1101
|
+
*/
|
|
1102
|
+
interface BoundaryFixture {
|
|
1103
|
+
/** What the snippet exercises. */
|
|
1104
|
+
readonly name: string;
|
|
1105
|
+
/** The source text. */
|
|
1106
|
+
readonly source: string;
|
|
1107
|
+
/** The rule it is checked against. */
|
|
1108
|
+
readonly rule: BoundaryRule;
|
|
1109
|
+
/** Whether that rule must flag it. */
|
|
1110
|
+
readonly flagged: boolean;
|
|
1111
|
+
}
|
|
1112
|
+
declare const Offence_base: Schema.Class<Offence, Schema.Struct<{
|
|
1113
|
+
/** The file, relative to the scanned root, with `/` separators. */
|
|
1114
|
+
readonly file: Schema.String;
|
|
1115
|
+
/** The 1-based line. */
|
|
1116
|
+
readonly line: Schema.Number;
|
|
1117
|
+
/** The 1-based column, in UTF-16 code units. */
|
|
1118
|
+
readonly column: Schema.Number;
|
|
1119
|
+
/** The rule broken. */
|
|
1120
|
+
readonly rule: Schema.Literals<readonly ["process", "node:process", "stdout-write", "console", "console-stdout", "forbidImports"]>;
|
|
1121
|
+
/** What matched: the identifier, the call, or the import specifier. */
|
|
1122
|
+
readonly detail: Schema.String;
|
|
1123
|
+
}>, {}>;
|
|
1124
|
+
/**
|
|
1125
|
+
* One place a source file breaks a {@link BoundaryRule}.
|
|
1126
|
+
*
|
|
1127
|
+
* @public
|
|
1128
|
+
*/
|
|
1129
|
+
export declare class Offence extends Offence_base {
|
|
1130
|
+
/** `file:line:column rule detail`, the form an assertion message reads best in. */
|
|
1131
|
+
get label(): string;
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* The rule an {@link Offence} names: every string {@link BoundaryRule}, plus
|
|
1135
|
+
* `"forbidImports"` for any `{ forbidImports }` rule. These are the keys of
|
|
1136
|
+
* {@link ScanOptions.allowRules}.
|
|
1137
|
+
*
|
|
1138
|
+
* @public
|
|
1139
|
+
*/
|
|
1140
|
+
type OffenceRule = Offence["rule"];
|
|
1141
|
+
declare const SourceScan_base: Schema.Class<SourceScan, Schema.Struct<{
|
|
1142
|
+
/** Every source file visited, relative to the root with `/` separators, sorted. Assert it is non-empty. */
|
|
1143
|
+
readonly files: Schema.$Array<Schema.String>;
|
|
1144
|
+
/** The visited files an `allow` glob exempted from every rule. */
|
|
1145
|
+
readonly allowed: Schema.$Array<Schema.String>;
|
|
1146
|
+
/** Every offence, sorted by file, then line, then column. */
|
|
1147
|
+
readonly offences: Schema.$Array<typeof Offence>;
|
|
1148
|
+
/**
|
|
1149
|
+
* Every offence an `allowRules` glob waived, sorted like `offences`. Assert
|
|
1150
|
+
* it is exactly what you meant to waive: a waiver that no longer waives
|
|
1151
|
+
* anything, or waives more than intended, shows up here.
|
|
1152
|
+
*/
|
|
1153
|
+
readonly waived: Schema.$Array<typeof Offence>;
|
|
1154
|
+
}>, {}>;
|
|
1155
|
+
/**
|
|
1156
|
+
* What a scan read and found.
|
|
1157
|
+
*
|
|
1158
|
+
* @public
|
|
1159
|
+
*/
|
|
1160
|
+
export declare class SourceScan extends SourceScan_base {
|
|
1161
|
+
/** One `file:line:column rule detail` label per offence: `[]` means clean. */
|
|
1162
|
+
get violations(): ReadonlyArray<string>;
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Options for {@link SourceBoundary.scan}.
|
|
1166
|
+
*
|
|
1167
|
+
* @public
|
|
1168
|
+
*/
|
|
1169
|
+
interface ScanOptions extends ReferenceOptions {
|
|
1170
|
+
/** The directory to scan. */
|
|
1171
|
+
readonly root: string;
|
|
1172
|
+
/** The rules every non-allowed file must keep. */
|
|
1173
|
+
readonly rules: ReadonlyArray<BoundaryRule>;
|
|
1174
|
+
/** Globs, relative to `root` with `/` separators, naming files exempt from every rule. */
|
|
1175
|
+
readonly allow?: ReadonlyArray<string> | undefined;
|
|
1176
|
+
/**
|
|
1177
|
+
* Globs per rule, matched like `allow`, naming files exempt from that one
|
|
1178
|
+
* rule. A matching file is still checked against every other rule, and
|
|
1179
|
+
* each offence a glob waives is reported in `SourceScan.waived`
|
|
1180
|
+
* rather than dropped. The `"forbidImports"` key covers every
|
|
1181
|
+
* `{ forbidImports }` rule. A file `allow` matches is exempt from every
|
|
1182
|
+
* rule, so nothing in it is waived.
|
|
1183
|
+
*/
|
|
1184
|
+
readonly allowRules?: { readonly [R in OffenceRule]?: ReadonlyArray<string> | undefined; } | undefined;
|
|
1185
|
+
/**
|
|
1186
|
+
* File extensions to scan. Defaults to `.ts`, `.mts`, `.cts`, `.js`,
|
|
1187
|
+
* `.mjs` and `.cjs`; declaration files are always skipped.
|
|
1188
|
+
*/
|
|
1189
|
+
readonly extensions?: ReadonlyArray<string> | undefined;
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1192
|
+
* Source-text boundary checks: which files read `process`, import a forbidden
|
|
1193
|
+
* module, or write to stdout or the console.
|
|
1194
|
+
*
|
|
1195
|
+
* @remarks
|
|
1196
|
+
* Every check runs over one lexer pass that separates code from comments,
|
|
1197
|
+
* strings, template text and regex bodies, so `process` in prose, in a string
|
|
1198
|
+
* or in an embedded script's template text never counts, and a `/*` inside a
|
|
1199
|
+
* string never hides the code behind it. A `/` after an operand divides (an
|
|
1200
|
+
* identifier, `)`, `]`, a postfix `++`/`--` or a non-null `!`), while one after
|
|
1201
|
+
* an operator, a keyword or the `)` of an `if`/`while`/`for`/`with` condition
|
|
1202
|
+
* opens a regex. An object-literal key or type member named `process`
|
|
1203
|
+
* (`{ process: 1 }`) is not a read.
|
|
1204
|
+
*
|
|
1205
|
+
* It is a lexer, not a type checker, and these misses are known:
|
|
1206
|
+
*
|
|
1207
|
+
* - there is no scope analysis, so ANY local binding named `process` or
|
|
1208
|
+
* `console` is flagged like the global: a parameter
|
|
1209
|
+
* (`(process: Handle) => process.kill()`), a variable, a label, or an
|
|
1210
|
+
* unannotated class field (`process = 1`). An annotated class field
|
|
1211
|
+
* (`process: T`) reads as a type member and is spared. Prefer renaming the
|
|
1212
|
+
* binding; otherwise waive that one rule for the file with an
|
|
1213
|
+
* `allowRules` glob, which leaves every other rule in force and reports
|
|
1214
|
+
* what it waived. An `allow` glob exempts the file from every rule;
|
|
1215
|
+
*
|
|
1216
|
+
* - a computed access through a string key (`globalThis["process"]`) and a
|
|
1217
|
+
* destructuring of a global (`const { process: p } = globalThis`) are not seen;
|
|
1218
|
+
*
|
|
1219
|
+
* - a regex literal directly after a block-closing `}` reads as a division, so
|
|
1220
|
+
* a quote or `/*` inside it can hide the code after it;
|
|
1221
|
+
*
|
|
1222
|
+
* - JSX text reads as code;
|
|
1223
|
+
*
|
|
1224
|
+
* - `forbidImports: ["node:*"]` does not catch a bare built-in such as
|
|
1225
|
+
* `"fs"` (see {@link BoundaryRule}).
|
|
1226
|
+
*
|
|
1227
|
+
* Assert on {@link SourceBoundary.verifyFixtures} beside your own scan: it
|
|
1228
|
+
* proves the scanner you are trusting still flags what it must and spares
|
|
1229
|
+
* what it must.
|
|
1230
|
+
*
|
|
1231
|
+
* @example
|
|
1232
|
+
* ```ts
|
|
1233
|
+
* import { SourceBoundary } from "@effected/workspaces/testing";
|
|
1234
|
+
*
|
|
1235
|
+
* const offences = SourceBoundary.check("src/a.ts", "const { env } = process;", ["process"]);
|
|
1236
|
+
* const clean = SourceBoundary.verifyFixtures().length === 0;
|
|
1237
|
+
* console.log(offences.map((offence) => offence.label), clean);
|
|
1238
|
+
* // => [ 'src/a.ts:1:17 process process' ] true
|
|
1239
|
+
* ```
|
|
1240
|
+
*
|
|
1241
|
+
* @public
|
|
1242
|
+
*/
|
|
1243
|
+
export declare class SourceBoundary {
|
|
1244
|
+
private constructor();
|
|
1245
|
+
/** `text` with every comment blanked to spaces; strings, templates and regexes untouched, length and line breaks kept. */
|
|
1246
|
+
static readonly stripComments: (text: string) => string;
|
|
1247
|
+
/** Whether `text` reads the global `process` in code (see {@link BoundaryRule}). */
|
|
1248
|
+
static readonly referencesProcess: (text: string, options?: ReferenceOptions) => boolean;
|
|
1249
|
+
/** Every module specifier `text` imports: static, side-effect, re-export, type-only, `import(...)` with a literal, and `require(...)`. */
|
|
1250
|
+
static readonly importSpecifiers: (text: string) => ReadonlyArray<string>;
|
|
1251
|
+
/** Whether `text` imports the Node built-in `module`, in either its `node:` or bare form. */
|
|
1252
|
+
static readonly importsNode: (text: string, module: string) => boolean;
|
|
1253
|
+
/** Every place `text` breaks one of `rules`, in source order, attributed to `file`. */
|
|
1254
|
+
static readonly check: (file: string, text: string, rules: ReadonlyArray<BoundaryRule>, options?: ReferenceOptions) => ReadonlyArray<Offence>;
|
|
1255
|
+
/**
|
|
1256
|
+
* Check every source file under `root` against `rules`.
|
|
1257
|
+
*
|
|
1258
|
+
* @remarks
|
|
1259
|
+
* Walks with an explicit stack, visiting each real directory once (via
|
|
1260
|
+
* `realPath`), so a symlink loop terminates and a linked directory is not
|
|
1261
|
+
* scanned twice. `node_modules` is never entered. Paths come back relative
|
|
1262
|
+
* and `/`-separated whatever the platform's separator, and that is also
|
|
1263
|
+
* what `allow` globs match against. A missing root fails; it never scans
|
|
1264
|
+
* nothing. A dangling symlink under the root is skipped, having nothing to
|
|
1265
|
+
* scan; any other entry that cannot be read fails the scan.
|
|
1266
|
+
*
|
|
1267
|
+
* @example
|
|
1268
|
+
* ```ts
|
|
1269
|
+
* import { NodeServices } from "@effect/platform-node";
|
|
1270
|
+
* import { SourceBoundary } from "@effected/workspaces/testing";
|
|
1271
|
+
* import { Effect } from "effect";
|
|
1272
|
+
*
|
|
1273
|
+
* const scan = SourceBoundary.scan({
|
|
1274
|
+
* root: "/repo/packages/engine/src",
|
|
1275
|
+
* rules: ["process", "node:process", { forbidImports: ["node:*", "@effect/platform*"] }],
|
|
1276
|
+
* }).pipe(Effect.provide(NodeServices.layer));
|
|
1277
|
+
* ```
|
|
1278
|
+
*/
|
|
1279
|
+
static readonly scan: (options: ScanOptions) => Effect.Effect<SourceScan, import("@effected/glob").GlobPatternError | import("effect/PlatformError").PlatformError, FileSystem.FileSystem | Path.Path>;
|
|
1280
|
+
/** The shipped positive- and negative-control snippets, at least one of each per rule kind. */
|
|
1281
|
+
static readonly fixtures: ReadonlyArray<BoundaryFixture>;
|
|
1282
|
+
/** The names of shipped fixtures the scanner now gets wrong; `[]` means it still flags and spares what it must. */
|
|
1283
|
+
static readonly verifyFixtures: () => ReadonlyArray<string>;
|
|
1284
|
+
}
|
|
1285
|
+
//#endregion
|
|
1286
|
+
//#region src/WorkspaceLayering.d.ts
|
|
1287
|
+
declare const LayerEdge_base: Schema.Class<LayerEdge, Schema.Struct<{
|
|
1288
|
+
/** The dependent package. */
|
|
1289
|
+
readonly from: Schema.String;
|
|
1290
|
+
/** The package depended on. */
|
|
1291
|
+
readonly to: Schema.String;
|
|
1292
|
+
/** The manifest map that declares it. */
|
|
1293
|
+
readonly field: Schema.Literals<readonly ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]>;
|
|
1294
|
+
}>, {}>;
|
|
1295
|
+
/**
|
|
1296
|
+
* One dependency edge between two workspace packages, in one field.
|
|
1297
|
+
*
|
|
1298
|
+
* @public
|
|
1299
|
+
*/
|
|
1300
|
+
export declare class LayerEdge extends LayerEdge_base {
|
|
1301
|
+
/** `from -> to (field)`. */
|
|
1302
|
+
get label(): string;
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* The input to {@link WorkspaceLayering.check}: package names, and per-field edges between them.
|
|
1306
|
+
*
|
|
1307
|
+
* @public
|
|
1308
|
+
*/
|
|
1309
|
+
interface LayeringGraph {
|
|
1310
|
+
/** Every workspace package name, the root included. */
|
|
1311
|
+
readonly names: ReadonlyArray<string>;
|
|
1312
|
+
/** One edge per declaring field. */
|
|
1313
|
+
readonly edges: ReadonlyArray<LayerEdge>;
|
|
1314
|
+
}
|
|
1315
|
+
declare const LayeringReport_base: Schema.Class<LayeringReport, Schema.Struct<{
|
|
1316
|
+
/** Packages the policy declares more than once, or declares and also matches with an unconstrained glob. */
|
|
1317
|
+
readonly duplicates: Schema.$Array<Schema.String>;
|
|
1318
|
+
/** Workspace packages the policy does not classify. */
|
|
1319
|
+
readonly unclassified: Schema.$Array<Schema.String>;
|
|
1320
|
+
/** Edges that break the policy, each with why. */
|
|
1321
|
+
readonly offenders: Schema.$Array<Schema.Struct<{
|
|
1322
|
+
readonly edge: typeof LayerEdge;
|
|
1323
|
+
readonly reason: Schema.Literals<readonly ["upward", "sameLayer", "toolingReachesLayer", "intoUnconstrained", "intoUnclassified"]>;
|
|
1324
|
+
}>>;
|
|
1325
|
+
/** The members of every dependency cycle in the checked fields, or none. */
|
|
1326
|
+
readonly cycle: Schema.Option<Schema.$Array<Schema.String>>;
|
|
1327
|
+
/** Packages the policy names that the workspace does not contain. */
|
|
1328
|
+
readonly missingDeclared: Schema.$Array<Schema.String>;
|
|
1329
|
+
/** Required edges absent from the checked fields. */
|
|
1330
|
+
readonly missingRequiredEdges: Schema.$Array<Schema.String>;
|
|
1331
|
+
/** Edges in the checked fields; `0` is itself a violation. */
|
|
1332
|
+
readonly edgeCount: Schema.Number;
|
|
1333
|
+
}>, {}>;
|
|
1334
|
+
/**
|
|
1335
|
+
* What a layering check found.
|
|
1336
|
+
*
|
|
1337
|
+
* @public
|
|
1338
|
+
*/
|
|
1339
|
+
export declare class LayeringReport extends LayeringReport_base {
|
|
1340
|
+
/** One line per violation: `[]` means the graph honours the policy and the check was not vacuous. */
|
|
1341
|
+
get violations(): ReadonlyArray<string>;
|
|
1342
|
+
}
|
|
1343
|
+
/**
|
|
1344
|
+
* Holds a workspace's package graph to a committed {@link LayerPolicy}.
|
|
1345
|
+
*
|
|
1346
|
+
* @remarks
|
|
1347
|
+
* `check` is pure, so positive-control fixture graphs need no filesystem.
|
|
1348
|
+
* `edgesOf` recomputes one edge per declaring field, because
|
|
1349
|
+
* `DependencyGraph` merges the four fields into one adjacency and a policy
|
|
1350
|
+
* may check only some of them. An edge exists wherever a dependency NAME is
|
|
1351
|
+
* a workspace package, whatever its specifier protocol.
|
|
1352
|
+
*
|
|
1353
|
+
* @example
|
|
1354
|
+
* ```ts
|
|
1355
|
+
* import { NodeServices } from "@effect/platform-node";
|
|
1356
|
+
* import { Workspaces } from "@effected/workspaces";
|
|
1357
|
+
* import { LayerPolicy, WorkspaceLayering } from "@effected/workspaces/testing";
|
|
1358
|
+
* import { Effect, Layer } from "effect";
|
|
1359
|
+
*
|
|
1360
|
+
* const Live = Workspaces.layer({ cwd: "/repo" }).pipe(Layer.provideMerge(NodeServices.layer));
|
|
1361
|
+
*
|
|
1362
|
+
* const violations = Effect.gen(function* () {
|
|
1363
|
+
* const policy = yield* LayerPolicy.load("/repo/layers.json");
|
|
1364
|
+
* const report = yield* WorkspaceLayering.checkWorkspace(policy);
|
|
1365
|
+
* return report.violations;
|
|
1366
|
+
* }).pipe(Effect.provide(Live));
|
|
1367
|
+
* ```
|
|
1368
|
+
*
|
|
1369
|
+
* @public
|
|
1370
|
+
*/
|
|
1371
|
+
export declare class WorkspaceLayering {
|
|
1372
|
+
private constructor();
|
|
1373
|
+
/** Check `graph` against `policy`, reading only the policy's fields. Pure. */
|
|
1374
|
+
static readonly check: (graph: LayeringGraph, policy: LayerPolicy) => LayeringReport;
|
|
1375
|
+
/** One edge per declaring field between workspace packages; self-edges dropped. */
|
|
1376
|
+
static readonly edgesOf: (packages: ReadonlyArray<WorkspacePackage>) => ReadonlyArray<LayerEdge>;
|
|
1377
|
+
/** Discover the workspace and check it against `policy`. */
|
|
1378
|
+
static readonly checkWorkspace: (policy: LayerPolicy) => Effect.Effect<LayeringReport, WorkspaceDiscoveryFailure, WorkspaceDiscovery>;
|
|
1379
|
+
}
|
|
1380
|
+
//#endregion
|
|
1381
|
+
export type { BoundaryFixture, BoundaryRule, DependencyDiff, FindWorkspaceRootOptions, LayeringGraph, OffenceRule, PackSource, PackedInstallOptions, ReferenceOptions, ScanOptions, WorkspaceDiscoveryFailure, WorkspaceDiscoveryOptions, WorkspaceDiscoveryShape, WorkspaceLookupFailure, WorkspaceRootShape };
|
|
1382
|
+
//# sourceMappingURL=testing.d.ts.map
|