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