@effected/workspaces 0.30.4 → 0.32.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/DependencyGraph.js +17 -7
- package/DuplicateCheck.js +8 -7
- package/LayerPolicy.js +1 -1
- package/LockfileReader.js +4 -1
- package/PackageManagerName.js +3 -4
- package/PackedInstall.js +135 -15
- package/PeerCheck.js +6 -2
- package/Publishability.js +10 -14
- package/README.md +4 -4
- package/ReleaseTag.js +6 -5
- package/SourceBoundary.js +1 -1
- package/VersioningStrategy.js +14 -4
- package/WorkspaceCatalogs.js +3 -4
- package/WorkspaceDiscovery.js +7 -7
- package/WorkspacePackage.js +16 -12
- package/WorkspaceRoot.js +4 -6
- package/WorkspaceSnapshots.js +4 -1
- package/WorkspaceStateSnapshot.js +10 -16
- package/Workspaces.js +13 -17
- package/WorkspacesSync.js +1 -3
- package/index.d.ts +152 -146
- package/internal/importerVersions.js +1 -1
- package/node-sync.d.ts +2 -0
- package/package.json +12 -12
- package/testing.d.ts +179 -47
package/DependencyGraph.js
CHANGED
|
@@ -33,8 +33,9 @@ cycle: Schema.Array(Schema.String) }) {
|
|
|
33
33
|
* `dependencies`, `devDependencies`, `peerDependencies` and
|
|
34
34
|
* `optionalDependencies`; a self-edge is dropped.
|
|
35
35
|
*
|
|
36
|
-
* Total accessors never fail
|
|
37
|
-
*
|
|
36
|
+
* Total accessors never fail. The lookups fail with `PackageNotFoundError` for
|
|
37
|
+
* an unknown name, and the ordering operations (`levels`, `sort`, `sortSubset`)
|
|
38
|
+
* fail with {@link CyclicDependencyError} when the graph has a cycle.
|
|
38
39
|
*
|
|
39
40
|
* @example
|
|
40
41
|
* ```ts
|
|
@@ -141,8 +142,11 @@ packages: Schema.Array(WorkspacePackage) }) {
|
|
|
141
142
|
})) : Effect.succeed([...dependents].sort());
|
|
142
143
|
});
|
|
143
144
|
/**
|
|
144
|
-
* `
|
|
145
|
-
* radius of a change. Sorted;
|
|
145
|
+
* Each of `names` plus every package that transitively depends on any of
|
|
146
|
+
* them — the blast radius of a change. Sorted and de-duplicated; the given
|
|
147
|
+
* names are included, whether or not the graph knows them.
|
|
148
|
+
*
|
|
149
|
+
* @param names - The changed package names.
|
|
146
150
|
*/
|
|
147
151
|
affectedBy = Effect.fn("DependencyGraph.affectedBy")((names) => {
|
|
148
152
|
const { reverse } = this.#index();
|
|
@@ -162,9 +166,8 @@ packages: Schema.Array(WorkspacePackage) }) {
|
|
|
162
166
|
* the workspace, level *n* depends only on levels below it.
|
|
163
167
|
*
|
|
164
168
|
* @remarks
|
|
165
|
-
* Kahn's algorithm over the reverse-edge index
|
|
166
|
-
*
|
|
167
|
-
* lexicographically, so the output is deterministic.
|
|
169
|
+
* Kahn's algorithm over the reverse-edge index, linear in the edge count.
|
|
170
|
+
* Each level is sorted lexicographically, so the output is deterministic.
|
|
168
171
|
*/
|
|
169
172
|
levels = Effect.fn("DependencyGraph.levels")(() => Effect.suspend(() => {
|
|
170
173
|
const edges = this.#index();
|
|
@@ -176,6 +179,13 @@ packages: Schema.Array(WorkspacePackage) }) {
|
|
|
176
179
|
/**
|
|
177
180
|
* A topological order over `names` plus their transitive workspace
|
|
178
181
|
* dependencies — the build order for a subset.
|
|
182
|
+
*
|
|
183
|
+
* @remarks
|
|
184
|
+
* Fails with `PackageNotFoundError` for a name the graph does not contain,
|
|
185
|
+
* and with {@link CyclicDependencyError} when the subset's closure has a
|
|
186
|
+
* cycle.
|
|
187
|
+
*
|
|
188
|
+
* @param names - The packages to build; each must be a workspace package.
|
|
179
189
|
*/
|
|
180
190
|
sortSubset = Effect.fn("DependencyGraph.sortSubset")((names) => Effect.suspend(() => {
|
|
181
191
|
const { forward } = this.#index();
|
package/DuplicateCheck.js
CHANGED
|
@@ -85,10 +85,12 @@ var DuplicatedPackage = class extends Schema.Class("DuplicatedPackage")({
|
|
|
85
85
|
*
|
|
86
86
|
* @example
|
|
87
87
|
* ```ts
|
|
88
|
-
* import { DuplicateCheck } from "@effected/workspaces";
|
|
89
88
|
* import { Lockfile } from "@effected/lockfiles";
|
|
89
|
+
* import { DuplicateCheck } from "@effected/workspaces";
|
|
90
90
|
* import { Effect } from "effect";
|
|
91
91
|
*
|
|
92
|
+
* declare const text: string; // the text of a pnpm-lock.yaml
|
|
93
|
+
*
|
|
92
94
|
* const program = Effect.gen(function* () {
|
|
93
95
|
* const lockfile = yield* Lockfile.parse(text, { format: "pnpm" });
|
|
94
96
|
* const report = DuplicateCheck.run(lockfile, { names: DuplicateCheck.kit });
|
|
@@ -119,8 +121,8 @@ var DuplicateCheck = class DuplicateCheck extends Schema.Class("DuplicateCheck")
|
|
|
119
121
|
* In practice this is the **root importer under npm and bun**: neither
|
|
120
122
|
* records a resolved version per importer dependency, and neither emits a
|
|
121
123
|
* package row for the root, so there is nothing to join on. pnpm records a
|
|
122
|
-
* version per importer dependency and is unaffected.
|
|
123
|
-
*
|
|
124
|
+
* version per importer dependency and is unaffected. `PeerCheck` reports the
|
|
125
|
+
* same limitation.
|
|
124
126
|
*
|
|
125
127
|
* Reported rather than silently skipped: a gate that sees no duplicates is
|
|
126
128
|
* entitled to know whether that means "clean" or "not looked at".
|
|
@@ -147,8 +149,7 @@ var DuplicateCheck = class DuplicateCheck extends Schema.Class("DuplicateCheck")
|
|
|
147
149
|
* Exactly those — not `@effect/*`, and not names merely starting with
|
|
148
150
|
* `effect`. A duplicated kit package presents as a `Layer` requirement that
|
|
149
151
|
* looks provided yet cannot be satisfied, at the entry point, naming neither
|
|
150
|
-
* the package nor the skew
|
|
151
|
-
* names both.
|
|
152
|
+
* the package nor the skew; this predicate is the check that names both.
|
|
152
153
|
*
|
|
153
154
|
* @param name - a package name
|
|
154
155
|
* @returns whether the name belongs to the kit
|
|
@@ -222,7 +223,7 @@ var DuplicateCheck = class DuplicateCheck extends Schema.Class("DuplicateCheck")
|
|
|
222
223
|
const dependents = /* @__PURE__ */ new Map();
|
|
223
224
|
const seen = /* @__PURE__ */ new Set();
|
|
224
225
|
const record = (targetId, dependent) => {
|
|
225
|
-
const key = `${targetId}
|
|
226
|
+
const key = `${targetId}\0${renderKey(dependent)}`;
|
|
226
227
|
if (seen.has(key)) return;
|
|
227
228
|
seen.add(key);
|
|
228
229
|
const list = dependents.get(targetId) ?? [];
|
|
@@ -287,7 +288,7 @@ const dependentOf = (from) => from.isWorkspace && from.relativePath !== void 0 ?
|
|
|
287
288
|
version: from.version
|
|
288
289
|
};
|
|
289
290
|
/** @internal */
|
|
290
|
-
const renderKey = (dependent) => dependent._tag === "importer" ? `importer
|
|
291
|
+
const renderKey = (dependent) => dependent._tag === "importer" ? `importer\0${dependent.path}` : `package\0${dependent.name}\0${dependent.version}`;
|
|
291
292
|
|
|
292
293
|
//#endregion
|
|
293
294
|
export { Dependent, DuplicateCheck, DuplicateInstance, DuplicatedPackage, DuplicatedVersion };
|
package/LayerPolicy.js
CHANGED
|
@@ -59,7 +59,7 @@ var LayerPolicyError = class extends Schema.TaggedError()("LayerPolicyError", {
|
|
|
59
59
|
* message names every such key. A typo on an optional key would otherwise be
|
|
60
60
|
* silently dropped: `requiredEdge` would remove the non-vacuity guard and
|
|
61
61
|
* leave the report green. `$schema` is always accepted. A file that carries
|
|
62
|
-
* keys of its own, such as
|
|
62
|
+
* keys of its own, such as a `harness` block, passes them in `allowKeys`.
|
|
63
63
|
*
|
|
64
64
|
* @example
|
|
65
65
|
* ```ts
|
package/LockfileReader.js
CHANGED
|
@@ -112,11 +112,14 @@ var LockfileReader = class LockfileReader extends Context.Service()("@effected/w
|
|
|
112
112
|
};
|
|
113
113
|
});
|
|
114
114
|
/**
|
|
115
|
-
* The live layer
|
|
115
|
+
* The live layer: reads the lockfile of the detected package manager at the
|
|
116
|
+
* workspace root.
|
|
116
117
|
*
|
|
117
118
|
* @remarks
|
|
118
119
|
* Parameterized, so it mints a fresh reference per call — bind it to a
|
|
119
120
|
* `const` and reuse it.
|
|
121
|
+
*
|
|
122
|
+
* @param options - Root resolution (`cwd`, `stopAt`).
|
|
120
123
|
*/
|
|
121
124
|
static layer = (options) => Layer.effect(LockfileReader, LockfileReader.make(options));
|
|
122
125
|
/**
|
package/PackageManagerName.js
CHANGED
|
@@ -82,8 +82,8 @@ const isPlainObject = (value) => typeof value === "object" && value !== null &&
|
|
|
82
82
|
* is not an exact version.
|
|
83
83
|
*
|
|
84
84
|
* Reuses the corepack `name@version+integrity` grammar rather than a second
|
|
85
|
-
* parser, so a `devEngines` version carrying a hash (`11.11.0+sha512
|
|
86
|
-
*
|
|
85
|
+
* parser, so a `devEngines` version carrying a hash (`11.11.0+sha512.…`)
|
|
86
|
+
* normalizes to the same `11.11.0` the
|
|
87
87
|
* top-level field reports — and a *range* (`^11`, `11.x`) yields none, because a
|
|
88
88
|
* range is not a version and corepack will not run one either.
|
|
89
89
|
*/
|
|
@@ -326,8 +326,7 @@ var PackageManagerDetector = class PackageManagerDetector extends Context.Servic
|
|
|
326
326
|
* `WorkspaceDiscovery.info` posture, for the same reason. A stand-in that
|
|
327
327
|
* answered `"pnpm"` would hand a consumer a fact nothing established, and it
|
|
328
328
|
* would contradict the very service it stands in for: the live detector's
|
|
329
|
-
* defining property is that it
|
|
330
|
-
* guess](https://github.com/spencerbeggs/effected) when no evidence matches.
|
|
329
|
+
* defining property is that it refuses to guess when no evidence matches.
|
|
331
330
|
* A double that guesses is worse than no double.
|
|
332
331
|
*
|
|
333
332
|
* Failing typed would be the subtler mistake: `PackageManagerDetectionError`
|
package/PackedInstall.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { WorkspaceDiscovery } from "./WorkspaceDiscovery.js";
|
|
2
2
|
import { PackageManagerName } from "./PackageManagerName.js";
|
|
3
3
|
import { binConflict, binTargetOf, closureOf, consumerFiles, fileOverridesOf, installArgs, overridePath, readPackedManifest, scrubEnv, versionOf } from "./internal/packedInstallPlan.js";
|
|
4
|
-
import { Duration, Effect, FileSystem, Option, Path, Redacted, Result, Schema } from "effect";
|
|
4
|
+
import { Config, Duration, Effect, FileSystem, Option, Path, Redacted, Result, Schema, Stream } from "effect";
|
|
5
5
|
import { Yaml } from "@effected/yaml";
|
|
6
6
|
import { Run } from "@effected/commands";
|
|
7
7
|
import { ChildProcess } from "effect/process";
|
|
@@ -182,7 +182,9 @@ var InstalledConsumer = class extends Schema.Class("InstalledConsumer")({
|
|
|
182
182
|
* Run the installed bin `name` to completion and collect what it wrote.
|
|
183
183
|
*
|
|
184
184
|
* @remarks
|
|
185
|
-
* Spawns {@link InstalledConsumer.command} with stdin
|
|
185
|
+
* Spawns {@link InstalledConsumer.command} with `options.stdin` as its
|
|
186
|
+
* input (the null device when omitted, so a bin never waits on an open
|
|
187
|
+
* pipe): the bin
|
|
186
188
|
* from the consumer's directory, under the install's scrubbed environment
|
|
187
189
|
* with `options.env` layered over it after the scrub, and nothing
|
|
188
190
|
* inherited beyond that. A
|
|
@@ -194,10 +196,10 @@ var InstalledConsumer = class extends Schema.Class("InstalledConsumer")({
|
|
|
194
196
|
*
|
|
195
197
|
* @param name - The bin, as named in `node_modules/.bin`.
|
|
196
198
|
* @param args - Its arguments.
|
|
197
|
-
* @param options - Extra environment, working directory and
|
|
199
|
+
* @param options - Extra environment, working directory, ceiling and stdin.
|
|
198
200
|
*/
|
|
199
201
|
runBin(name, args = [], options = {}) {
|
|
200
|
-
return collectBin(this.command(name, args, options), this.manager, name, options
|
|
202
|
+
return collectBin(this.command(name, args, options), this.manager, name, options);
|
|
201
203
|
}
|
|
202
204
|
/**
|
|
203
205
|
* The command that runs the CARRIER's own bin `name`, whichever package
|
|
@@ -271,7 +273,8 @@ var InstalledConsumer = class extends Schema.Class("InstalledConsumer")({
|
|
|
271
273
|
* the `node_modules/.bin` slot.
|
|
272
274
|
*
|
|
273
275
|
* @remarks
|
|
274
|
-
* Spawns {@link InstalledConsumer.carrierCommand} with stdin
|
|
276
|
+
* Spawns {@link InstalledConsumer.carrierCommand} with `options.stdin` as
|
|
277
|
+
* its input (the null device when omitted), and
|
|
275
278
|
* reports as {@link InstalledConsumer.runBin} does: a non-zero exit is a
|
|
276
279
|
* result, and a bin that cannot spawn or outlives `options.timeout` (one
|
|
277
280
|
* minute by default) fails `BinFailed`. Use it beside `runBin` under
|
|
@@ -282,11 +285,11 @@ var InstalledConsumer = class extends Schema.Class("InstalledConsumer")({
|
|
|
282
285
|
*
|
|
283
286
|
* @param name - The bin, as the carrier's `bin` map names it.
|
|
284
287
|
* @param args - Its arguments.
|
|
285
|
-
* @param options - Extra environment, working directory and
|
|
288
|
+
* @param options - Extra environment, working directory, ceiling and stdin.
|
|
286
289
|
*/
|
|
287
290
|
runCarrierBin(name, args = [], options = {}) {
|
|
288
291
|
const manager = this.manager;
|
|
289
|
-
return this.carrierCommand(name, args, options).pipe(Effect.flatMap((command) => collectBin(command, manager, name, options
|
|
292
|
+
return this.carrierCommand(name, args, options).pipe(Effect.flatMap((command) => collectBin(command, manager, name, options)));
|
|
290
293
|
}
|
|
291
294
|
};
|
|
292
295
|
/** The install's scrubbed environment with the caller's entries layered on after the scrub: a deliberate CI=true must survive it. */
|
|
@@ -296,14 +299,24 @@ const layeredEnv = (consumer, options) => {
|
|
|
296
299
|
else env[key] = value;
|
|
297
300
|
return env;
|
|
298
301
|
};
|
|
299
|
-
/**
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
stdin
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
302
|
+
/** A bin's stdin as `ChildProcess` takes it: absent or `""` is the null device (immediate end of input), anything else is written and closed. */
|
|
303
|
+
const stdinOf = (stdin) => {
|
|
304
|
+
if (stdin === void 0 || stdin === "") return "ignore";
|
|
305
|
+
if (typeof stdin === "string") return Stream.make(new TextEncoder().encode(stdin));
|
|
306
|
+
if (stdin instanceof Uint8Array) return Stream.make(stdin);
|
|
307
|
+
return stdin;
|
|
308
|
+
};
|
|
309
|
+
/** Run a bin's command to completion with `options.stdin` as its input (the null device when omitted), mapping a failed spawn or an expired ceiling to `BinFailed`. */
|
|
310
|
+
const collectBin = (command, manager, name, options) => {
|
|
311
|
+
const timeout = options.timeout ?? DEFAULT_BIN_TIMEOUT;
|
|
312
|
+
return Run.collect(ChildProcess.make(command.command, command.args, {
|
|
313
|
+
...command.options,
|
|
314
|
+
stdin: stdinOf(options.stdin)
|
|
315
|
+
}), { timeout }).pipe(Effect.mapError((cause) => failure("BinFailed", cause._tag === "CommandFailedError" && cause.kind === "timeout" ? `${manager}: ${name} timed out after ${describeDuration(timeout)}` : `${manager}: ${name} could not run`, {
|
|
316
|
+
manager,
|
|
317
|
+
cause
|
|
318
|
+
})));
|
|
319
|
+
};
|
|
307
320
|
/**
|
|
308
321
|
* What a packed install produced.
|
|
309
322
|
*
|
|
@@ -594,6 +607,113 @@ var PackedInstall = class PackedInstall {
|
|
|
594
607
|
...options.packTimeout === void 0 ? {} : { packTimeout: options.packTimeout },
|
|
595
608
|
...extra.perConsumer === void 0 ? {} : { perConsumer: extra.perConsumer }
|
|
596
609
|
})));
|
|
610
|
+
/**
|
|
611
|
+
* Whether the pack source {@link PackedInstall.run} would read is on disk,
|
|
612
|
+
* before any package manager is spawned.
|
|
613
|
+
*
|
|
614
|
+
* @remarks
|
|
615
|
+
* Plans the closure as the run does (`PackedInstall.closure`'s planner) and
|
|
616
|
+
* checks the `package.json` of each closure package under `packFrom`
|
|
617
|
+
* (default `{ directory: "dist/prod/npm/pkg" }`), the same file whose
|
|
618
|
+
* absence makes the run fail `PackSourceMissing`. Under `packFrom: "source"`
|
|
619
|
+
* the packer builds nothing of its own, so nothing is checked and the
|
|
620
|
+
* result is ready. A package a bundler's dev build produced is no
|
|
621
|
+
* substitute: the prod output exists only after the prod build, which a
|
|
622
|
+
* test job's usual `build:dev` pre-step does not run. Pair it with
|
|
623
|
+
* {@link PackedInstall.gate} to skip locally and fail under CI.
|
|
624
|
+
*
|
|
625
|
+
* Fails as `closure` does, plus `Io` when a path cannot be inspected.
|
|
626
|
+
*
|
|
627
|
+
* @example
|
|
628
|
+
* ```ts
|
|
629
|
+
* import { NodeServices } from "@effect/platform-node";
|
|
630
|
+
* import { Workspaces } from "@effected/workspaces";
|
|
631
|
+
* import { PackedInstall } from "@effected/workspaces/testing";
|
|
632
|
+
* import { Effect, Layer } from "effect";
|
|
633
|
+
*
|
|
634
|
+
* const Live = Workspaces.layer({ cwd: "/repo" }).pipe(Layer.provideMerge(NodeServices.layer));
|
|
635
|
+
* const preflight = await Effect.runPromise(
|
|
636
|
+
* PackedInstall.preflight({ carrier: "my-tool", closure: "auto" }).pipe(Effect.provide(Live)),
|
|
637
|
+
* );
|
|
638
|
+
* console.log(preflight.ready, preflight.missing);
|
|
639
|
+
* ```
|
|
640
|
+
*
|
|
641
|
+
* @param options - The run's carrier, closure, overrides and pack source.
|
|
642
|
+
*/
|
|
643
|
+
static preflight = Effect.fn("PackedInstall.preflight")(function* (options) {
|
|
644
|
+
const fs = yield* FileSystem.FileSystem;
|
|
645
|
+
const path = yield* Path.Path;
|
|
646
|
+
const plan = yield* planClosure(options.carrier, options);
|
|
647
|
+
const source = options.packFrom ?? DEFAULT_PACK_FROM;
|
|
648
|
+
if (source === "source") return {
|
|
649
|
+
ready: true,
|
|
650
|
+
missing: []
|
|
651
|
+
};
|
|
652
|
+
const missing = [];
|
|
653
|
+
for (const pkg of plan.closure) {
|
|
654
|
+
const manifest = path.join(pkg.path, source.directory, "package.json");
|
|
655
|
+
if (!(yield* fs.exists(manifest).pipe(Effect.mapError((cause) => failure("Io", `could not inspect ${manifest}`, { cause }))))) missing.push(manifest);
|
|
656
|
+
}
|
|
657
|
+
return {
|
|
658
|
+
ready: missing.length === 0,
|
|
659
|
+
missing
|
|
660
|
+
};
|
|
661
|
+
});
|
|
662
|
+
/**
|
|
663
|
+
* Turns a {@link PackedInstallPreflight} into the suite's decision: run,
|
|
664
|
+
* skip locally, or fail under CI.
|
|
665
|
+
*
|
|
666
|
+
* @remarks
|
|
667
|
+
* `CI` is read through `Config`, so the ambient `ConfigProvider` decides —
|
|
668
|
+
* the process environment at a test file's top level, and whatever a test
|
|
669
|
+
* provides in a test; nothing here reads `process`. `CI` counts as set when
|
|
670
|
+
* it is present and not `""`, `"0"` or `"false"` (case-insensitive), and an
|
|
671
|
+
* unreadable `CI` counts as unset. Ready is always `"run"`;
|
|
672
|
+
* missing is `"skip"` off CI and `"fail"` on it, so a CI job that never ran
|
|
673
|
+
* the prod build cannot pass by silently skipping the proof. The message
|
|
674
|
+
* names every missing path. The test job must run the prod build
|
|
675
|
+
* (`build:prod` for a `@savvy-web/bundler` package) before the packed-install
|
|
676
|
+
* tests: a dev build leaves `dist/prod` absent.
|
|
677
|
+
*
|
|
678
|
+
* @example
|
|
679
|
+
* ```ts
|
|
680
|
+
* import { assert, describe, it } from "@effect/vitest";
|
|
681
|
+
* import { PackedInstall } from "@effected/workspaces/testing";
|
|
682
|
+
* import { Effect } from "effect";
|
|
683
|
+
*
|
|
684
|
+
* declare const preflight: { readonly ready: boolean; readonly missing: ReadonlyArray<string> };
|
|
685
|
+
* const gate = Effect.runSync(PackedInstall.gate(preflight));
|
|
686
|
+
*
|
|
687
|
+
* describe.runIf(gate.action === "run")("packed install", () => {
|
|
688
|
+
* it("installs", () => {});
|
|
689
|
+
* });
|
|
690
|
+
* describe.runIf(gate.action === "fail")("packed install prod build", () => {
|
|
691
|
+
* it("exists", () => assert.fail(gate.message));
|
|
692
|
+
* });
|
|
693
|
+
* ```
|
|
694
|
+
*
|
|
695
|
+
* @param preflight - What {@link PackedInstall.preflight} answered.
|
|
696
|
+
*/
|
|
697
|
+
static gate = (preflight) => Effect.gen(function* () {
|
|
698
|
+
if (preflight.ready) return {
|
|
699
|
+
action: "run",
|
|
700
|
+
message: ""
|
|
701
|
+
};
|
|
702
|
+
const ci = yield* Config.option(Config.String("CI")).pipe(Effect.orElseSucceed(() => Option.none()));
|
|
703
|
+
const underCi = Option.isSome(ci) && ![
|
|
704
|
+
"",
|
|
705
|
+
"0",
|
|
706
|
+
"false"
|
|
707
|
+
].includes(ci.value.toLowerCase());
|
|
708
|
+
const message = `the pack source is missing: ${preflight.missing.join(", ")}; run the prod build (build:prod) before the packed-install tests`;
|
|
709
|
+
return underCi ? {
|
|
710
|
+
action: "fail",
|
|
711
|
+
message: `${message}; it must not be skipped under CI`
|
|
712
|
+
} : {
|
|
713
|
+
action: "skip",
|
|
714
|
+
message
|
|
715
|
+
};
|
|
716
|
+
});
|
|
597
717
|
/** Pack, then install under every available manager. */
|
|
598
718
|
static run = Effect.fn("PackedInstall.run")(function* (options) {
|
|
599
719
|
const fs = yield* FileSystem.FileSystem;
|
package/PeerCheck.js
CHANGED
|
@@ -402,10 +402,12 @@ const judgeJoined = (context, catalogs, peer, declared, optional) => {
|
|
|
402
402
|
*
|
|
403
403
|
* @example
|
|
404
404
|
* ```ts
|
|
405
|
-
* import { PeerCheck } from "@effected/workspaces";
|
|
406
405
|
* import { Lockfile } from "@effected/lockfiles";
|
|
406
|
+
* import { PeerCheck } from "@effected/workspaces";
|
|
407
407
|
* import { Effect } from "effect";
|
|
408
408
|
*
|
|
409
|
+
* declare const text: string; // the text of a pnpm-lock.yaml
|
|
410
|
+
*
|
|
409
411
|
* const program = Effect.gen(function* () {
|
|
410
412
|
* const lockfile = yield* Lockfile.parse(text, { format: "pnpm" });
|
|
411
413
|
* const report = PeerCheck.run(lockfile);
|
|
@@ -508,7 +510,7 @@ var PeerCheck = class PeerCheck extends Schema.Class("PeerCheck")({
|
|
|
508
510
|
* `pnpm peers check` DOES report the linked parent's peers (it reads the
|
|
509
511
|
* manifest on disk): every `link:` edge whose target the caller did not
|
|
510
512
|
* supply therefore fails the report closed rather than letting the
|
|
511
|
-
* invisible parent pass as checked
|
|
513
|
+
* invisible parent pass as checked.
|
|
512
514
|
*
|
|
513
515
|
* Supplying {@link PeerCheckOptions.workspacePackages} closes that gap for
|
|
514
516
|
* every `link:` target the set covers — the root importer's included,
|
|
@@ -543,6 +545,8 @@ var PeerCheck = class PeerCheck extends Schema.Class("PeerCheck")({
|
|
|
543
545
|
* version check, and a plain unparseable version is still skipped.
|
|
544
546
|
*
|
|
545
547
|
* @param lockfile - a lockfile parsed by `@effected/lockfiles`
|
|
548
|
+
* @param options - the workspace's effective rules, manifests and catalogs; see
|
|
549
|
+
* {@link PeerCheckOptions}
|
|
546
550
|
* @returns the report; never fails
|
|
547
551
|
*/
|
|
548
552
|
static run(lockfile, options) {
|
package/Publishability.js
CHANGED
|
@@ -87,16 +87,13 @@ var PublishTarget = class extends Schema.Class("PublishTarget")({
|
|
|
87
87
|
var PublishabilityDetector = class extends Context.Service()("@effected/workspaces/PublishabilityDetector") {
|
|
88
88
|
/**
|
|
89
89
|
* Standard npm publishing semantics, **as a value**. Pure — no filesystem,
|
|
90
|
-
* no platform services.
|
|
90
|
+
* no platform services. Never publishes with provenance (`provenance: false`).
|
|
91
91
|
*
|
|
92
92
|
* @remarks
|
|
93
93
|
* Exposed as a shape and not only as a layer, because a consumer composing
|
|
94
|
-
* *around* these rules cannot reach them through a layer without
|
|
95
|
-
* the very tag it is replacing.
|
|
96
|
-
* `
|
|
97
|
-
* **inside its own implementation of that tag** to get at this function for
|
|
98
|
-
* its pass-through branch; with the value exposed that becomes
|
|
99
|
-
* `PublishabilityDetector.npm.detect(pkg)`.
|
|
94
|
+
* *around* these rules cannot reach them through a layer without
|
|
95
|
+
* re-entering the very tag it is replacing. A custom detector's pass-through
|
|
96
|
+
* branch calls `PublishabilityDetector.npm.detect(pkg)` directly.
|
|
100
97
|
*
|
|
101
98
|
* @example
|
|
102
99
|
* ```ts
|
|
@@ -131,13 +128,12 @@ var PublishabilityDetector = class extends Context.Service()("@effected/workspac
|
|
|
131
128
|
*
|
|
132
129
|
* @remarks
|
|
133
130
|
* Named for its policy rather than called `layer`, deliberately. **No
|
|
134
|
-
* composite in this package provides a publishability detector**:
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* the worst available failure.
|
|
131
|
+
* composite in this package provides a publishability detector**: one that
|
|
132
|
+
* quietly supplied npm semantics would hide the choice and, because
|
|
133
|
+
* `Layer.mergeAll` is last-wins, would let a naively-ordered override such
|
|
134
|
+
* as `Layer.mergeAll(myDetector, Workspaces.layer())` lose to the default in
|
|
135
|
+
* silence. For a service that decides whether a package publishes and to
|
|
136
|
+
* which registry, that silent revert is the worst available failure.
|
|
141
137
|
*
|
|
142
138
|
* The composites do not *require* a detector either — nothing inside them
|
|
143
139
|
* asks a publishability question, so their `R` stays `FileSystem | Path`.
|
package/README.md
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
Monorepo workspace tooling for [Effect](https://effect.website) v4: find the workspace root, enumerate its packages, walk the dependency graph, detect the package manager, resolve pnpm catalogs, read the lockfile, check it for unsatisfied peer dependencies and work out which packages a git range touches. Every capability is a service you provide at the edge and swap in tests. Works with npm, pnpm, yarn Berry and bun.
|
|
9
9
|
|
|
10
|
-
> **Pre
|
|
11
|
-
>
|
|
12
|
-
> `1.0.0`
|
|
13
|
-
>
|
|
10
|
+
> **Pre-`1.0.0`.** This package is part of the `@effected/*` kit, built on stable
|
|
11
|
+
> Effect v4 (`effect` `^4.0.0`) and still in `0.x` development. Stable Effect
|
|
12
|
+
> makes a kit `1.0.0` possible, not automatic. To keep your `effect` and
|
|
13
|
+
> `@effect/*` versions on the line the kit is built and tested against, install
|
|
14
14
|
> [`@effected/pnpm-plugin-effect`](https://www.npmjs.com/package/@effected/pnpm-plugin-effect).
|
|
15
15
|
>
|
|
16
16
|
> **Stability: unstable.** This package's API surface is not yet considered
|
package/ReleaseTag.js
CHANGED
|
@@ -194,11 +194,11 @@ const classifyTag = (tag) => {
|
|
|
194
194
|
* even when `value` carries a prefix, so a consumer comparing versions never
|
|
195
195
|
* has to strip one back off.
|
|
196
196
|
*
|
|
197
|
-
* Formatting
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
197
|
+
* Formatting has **no error channel**: an empty version is rejected by
|
|
198
|
+
* `Schema.NonEmptyString` when the value is constructed. A bad version
|
|
199
|
+
* reaching these statics is developer wiring rather than untrusted input, so it
|
|
200
|
+
* dies as a defect, the same posture as an uncompilable glob literal in
|
|
201
|
+
* `WorkspacePackage.matchesDependency`.
|
|
202
202
|
*
|
|
203
203
|
* @example
|
|
204
204
|
* ```ts
|
|
@@ -227,6 +227,7 @@ var ReleaseTag = class ReleaseTag extends Schema.Class("ReleaseTag")({
|
|
|
227
227
|
*
|
|
228
228
|
* @param version - The version being released. Must not be empty.
|
|
229
229
|
* @param options - Formatting overrides.
|
|
230
|
+
* @returns the single-style {@link ReleaseTag}.
|
|
230
231
|
*/
|
|
231
232
|
static single(version, options) {
|
|
232
233
|
const prefix = options?.versionPrefix ?? "";
|
package/SourceBoundary.js
CHANGED
|
@@ -616,7 +616,7 @@ var SourceBoundary = class SourceBoundary {
|
|
|
616
616
|
});
|
|
617
617
|
/** The shipped positive- and negative-control snippets, at least one of each per rule kind. */
|
|
618
618
|
static fixtures = FIXTURES;
|
|
619
|
-
/** The names of shipped fixtures the scanner
|
|
619
|
+
/** The names of shipped fixtures the scanner gets wrong; `[]` means it still flags and spares what it must. */
|
|
620
620
|
static verifyFixtures = () => FIXTURES.filter((fixture) => SourceBoundary.check("fixture.ts", fixture.source, [fixture.rule]).length > 0 !== fixture.flagged).map((fixture) => `${fixture.flagged ? "missed" : "false positive"}: ${fixture.name}`);
|
|
621
621
|
};
|
|
622
622
|
|
package/VersioningStrategy.js
CHANGED
|
@@ -30,13 +30,13 @@ const VersioningStrategyType = Schema.Literals([
|
|
|
30
30
|
*
|
|
31
31
|
* @example
|
|
32
32
|
* ```ts
|
|
33
|
-
* import { VersioningStrategy } from "@effected/workspaces";
|
|
33
|
+
* import { PublishabilityDetector, VersioningStrategy } from "@effected/workspaces";
|
|
34
34
|
* import { Effect } from "effect";
|
|
35
35
|
*
|
|
36
36
|
* const program = Effect.gen(function* () {
|
|
37
|
-
* const strategy = yield* VersioningStrategy.detect({ fixedGroups });
|
|
38
|
-
* return strategy.tagsFor(
|
|
39
|
-
* });
|
|
37
|
+
* const strategy = yield* VersioningStrategy.detect({ fixedGroups: [["@acme/a", "@acme/b"]] });
|
|
38
|
+
* return strategy.tagsFor([{ name: "@acme/a", version: "1.2.3" }]).map((tag) => tag.value);
|
|
39
|
+
* }).pipe(Effect.provide(PublishabilityDetector.layerNpm));
|
|
40
40
|
* ```
|
|
41
41
|
*
|
|
42
42
|
* @public
|
|
@@ -66,6 +66,9 @@ var VersioningStrategy = class VersioningStrategy extends Schema.Class("Versioni
|
|
|
66
66
|
* Pure and total — no IO, no error channel. `packages` is sorted and
|
|
67
67
|
* de-duplicated first, so a name listed twice cannot inflate a one-package
|
|
68
68
|
* repo into an independent one.
|
|
69
|
+
*
|
|
70
|
+
* @param options - The publishable package names and any fixed groups.
|
|
71
|
+
* @returns the classified {@link VersioningStrategy}.
|
|
69
72
|
*/
|
|
70
73
|
static classify(options) {
|
|
71
74
|
const fixedGroups = options.fixedGroups ?? [];
|
|
@@ -85,6 +88,10 @@ var VersioningStrategy = class VersioningStrategy extends Schema.Class("Versioni
|
|
|
85
88
|
* The publishability question is asked through the service precisely so a
|
|
86
89
|
* consumer with its own rules — honouring a release tool's ignore list, say —
|
|
87
90
|
* swaps the layer instead of filtering afterwards.
|
|
91
|
+
*
|
|
92
|
+
* Requires `WorkspaceDiscovery` and `PublishabilityDetector` (for example
|
|
93
|
+
* `PublishabilityDetector.layerNpm`) in `R`, and fails with
|
|
94
|
+
* `WorkspaceDiscoveryFailure` when discovery does.
|
|
88
95
|
*/
|
|
89
96
|
static detect = Effect.fn("VersioningStrategy.detect")(function* (options) {
|
|
90
97
|
const discovery = yield* WorkspaceDiscovery;
|
|
@@ -114,6 +121,9 @@ var VersioningStrategy = class VersioningStrategy extends Schema.Class("Versioni
|
|
|
114
121
|
* one-line check rather than a field here.
|
|
115
122
|
*
|
|
116
123
|
* An empty batch produces no tags under either style.
|
|
124
|
+
*
|
|
125
|
+
* @param releases - The packages released and their versions.
|
|
126
|
+
* @param options - Tag formatting overrides, such as `versionPrefix`.
|
|
117
127
|
*/
|
|
118
128
|
tagsFor(releases, options) {
|
|
119
129
|
if (releases.length === 0) return [];
|
package/WorkspaceCatalogs.js
CHANGED
|
@@ -578,10 +578,9 @@ var WorkspaceCatalogs = class WorkspaceCatalogs extends Context.Service()("@effe
|
|
|
578
578
|
* `rangeOf` returns `Option.none()` for a dependency no catalog declares, per
|
|
579
579
|
* the contract's convention. A failed catalog *assembly* — an unreadable or
|
|
580
580
|
* malformed `pnpm-workspace.yaml`, a broken config-dependency hook — passes
|
|
581
|
-
* through **typed** as the contract's `CatalogAssemblyError
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
* an unfindable workspace root, is wrapped as `DependencyResolutionError`.
|
|
581
|
+
* through **typed** as the contract's `CatalogAssemblyError`; only the
|
|
582
|
+
* remaining mechanism failure, an unfindable workspace root, is wrapped as
|
|
583
|
+
* `DependencyResolutionError`.
|
|
585
584
|
*/
|
|
586
585
|
static catalogResolver = Layer.effect(CatalogResolver, Effect.gen(function* () {
|
|
587
586
|
const catalogs = yield* WorkspaceCatalogs;
|
package/WorkspaceDiscovery.js
CHANGED
|
@@ -19,7 +19,7 @@ import { DependencyResolutionError, WorkspaceResolver } from "@effected/npm";
|
|
|
19
19
|
* A manifest with no `version` is NOT a failure: pnpm accepts a version-less
|
|
20
20
|
* private package and a private monorepo root without one is the ordinary
|
|
21
21
|
* shape, so the member is discovered with `WorkspacePackage.version` absent.
|
|
22
|
-
*
|
|
22
|
+
* Only ABSENCE is tolerated: a
|
|
23
23
|
* `version` that is present but not a string — or present and `""`, which pnpm
|
|
24
24
|
* never wrote and which would resolve `workspace:^` to a bare `"^"` — is the
|
|
25
25
|
* manifest's shape being wrong and reports `invalidShape`.
|
|
@@ -118,11 +118,9 @@ const patternKindOf = (kind) => kind;
|
|
|
118
118
|
* the lifetime of the layer. A Vitest reporter that builds the layer per call
|
|
119
119
|
* site and never queries it pays nothing.
|
|
120
120
|
*
|
|
121
|
-
* The memo is **success-only
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
* channel. A failure or interrupt is therefore retried on the next call, which
|
|
125
|
-
* is a deliberate behaviour change from the v3 library.
|
|
121
|
+
* The memo is **success-only**: a failure or interrupt is not cached, so an
|
|
122
|
+
* init interrupted by an unrelated timeout cannot brick the layer, and the next
|
|
123
|
+
* call retries.
|
|
126
124
|
*
|
|
127
125
|
* @example
|
|
128
126
|
* ```ts
|
|
@@ -380,12 +378,14 @@ var WorkspaceDiscovery = class WorkspaceDiscovery extends Context.Service()("@ef
|
|
|
380
378
|
};
|
|
381
379
|
});
|
|
382
380
|
/**
|
|
383
|
-
* The live layer.
|
|
381
|
+
* The live layer, discovering the workspace from `options.cwd`.
|
|
384
382
|
*
|
|
385
383
|
* @remarks
|
|
386
384
|
* A parameterized layer factory mints a **fresh reference per call**, and
|
|
387
385
|
* layers memoize by reference — bind the result to a `const` and reuse it
|
|
388
386
|
* rather than calling `layer(...)` at each composition site.
|
|
387
|
+
*
|
|
388
|
+
* @param options - Root resolution and enumeration bounds.
|
|
389
389
|
*/
|
|
390
390
|
static layer = (options) => Layer.effect(WorkspaceDiscovery, WorkspaceDiscovery.make(options));
|
|
391
391
|
/**
|