@effected/workspaces 0.27.0 → 0.28.0-next-20260927062352

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.
@@ -1,4 +1,5 @@
1
1
  import { normalize } from "./internal/catalogs.js";
2
+ import { makeFetchConfigDependency } from "./internal/configDependencyFetch.js";
2
3
  import { lookupPnpmfiles, resolvePnpmfiles } from "./internal/configDependencyResolution.js";
3
4
  import { Context, Duration, Effect, Layer, Predicate, Schema } from "effect";
4
5
  import { CatalogAssemblyError } from "@effected/npm";
@@ -381,9 +382,10 @@ var ConfigDependencyHooks = class ConfigDependencyHooks extends Context.Service(
381
382
  * `links/<name>/<declared>/*\/node_modules/<name>` (the store is located
382
383
  * from `node_modules/.modules.yaml`, the realpath of any `.pnpm-config`
383
384
  * entry, then `$PNPM_HOME` / `$XDG_DATA_HOME` / the platform default), else
384
- * a typed error naming the package, the declared version, what is
385
- * installed, the stores searched, and the remediation
386
- * (`pnpm add --config <name>@<version>` in a throwaway workspace). In the
385
+ * a typed error with `reason: "notInstalled"` naming the package, the
386
+ * declared version, what is installed, the stores searched, and the
387
+ * remediation (`pnpm add --config <name>@<version>` in a throwaway
388
+ * workspace, or the subprocess layer, which fetches). In the
387
389
  * chosen directory the first of `pnpmfile.mjs`, `pnpmfile.cjs`,
388
390
  * `pnpmfile.js` present in one directory listing is loaded; a listed but
389
391
  * unreadable file fails typed at import time, never as "ships no hook".
@@ -396,9 +398,9 @@ var ConfigDependencyHooks = class ConfigDependencyHooks extends Context.Service(
396
398
  * wired by `WorkspaceCatalogs.layerWithConfigDependencies` and the
397
399
  * `Workspaces` composites of the same name.
398
400
  */
399
- static layerLive = Layer.succeed(ConfigDependencyHooks, { inject: (root, configDependencies, seed, rules) => Effect.gen(function* () {
401
+ static layerLive = Layer.succeed(ConfigDependencyHooks, { inject: (root, configDependencies, seed, rules, context) => Effect.gen(function* () {
400
402
  if (Object.keys(configDependencies).length === 0) return untouched(seed, rules);
401
- const pnpmfiles = yield* resolvePnpmfiles(root, configDependencies);
403
+ const pnpmfiles = yield* resolvePnpmfiles(root, configDependencies, { side: context });
402
404
  return yield* replayInProcess(pnpmfiles, seed, rules);
403
405
  }) });
404
406
  /**
@@ -463,9 +465,24 @@ var ConfigDependencyHooks = class ConfigDependencyHooks extends Context.Service(
463
465
  *
464
466
  * The declared-version resolution is the PARENT's: the same ladder as
465
467
  * `layerLive` (`.pnpm-config` when it holds the declared version, else the
466
- * pnpm store, else a typed fail-closed error) runs here, and the child
467
- * receives the resolved `[name, fileUrl]` pairs as one JSON argv
468
- * argument. An empty `configDependencies`, or one whose dependencies all
468
+ * pnpm store) runs here, and the child receives the resolved
469
+ * `[name, fileUrl]` pairs as one JSON argv argument.
470
+ *
471
+ * One rung more than `layerLive`: a version held nowhere is FETCHED into
472
+ * the store rather than failing, so a diff whose base side declares a
473
+ * version this checkout never installed still replays that version. pnpm
474
+ * runs `install --frozen-lockfile` in a scratch workspace (removed
475
+ * afterwards) whose lockfile pins the integrity the declaring side recorded
476
+ * — the inline `<version>+<integrity>` spec, else that side's
477
+ * `pnpm-lock.yaml` env preamble ({@link HookReplayContext.lockfile}) — so
478
+ * pnpm itself refuses a download that does not match. It writes to the
479
+ * first store the ladder searched, so the next replay finds the version
480
+ * there. The replay records `source: "fetched"`. Fail-closed: two recorded
481
+ * integrities that disagree fail with `reason: "integrityMismatch"`, none
482
+ * with `reason: "integrityUnavailable"` — nothing is fetched in either case
483
+ * — and a fetch that fails with `reason: "fetchFailed"`, keeping the
484
+ * not-installed remediation. The fetch needs `pnpm` 11 or 12 on `PATH` and
485
+ * registry access, and is bounded at two minutes. An empty `configDependencies`, or one whose dependencies all
469
486
  * ship no pnpmfile, returns the seed without spawning anything; a `..` path
470
487
  * segment in a dependency name fails typed **before** any spawn; a
471
488
  * load failure in the child — a syntax error, a throwing top level, an
@@ -498,9 +515,13 @@ var ConfigDependencyHooks = class ConfigDependencyHooks extends Context.Service(
498
515
  */
499
516
  static layerSubprocess = Layer.effect(ConfigDependencyHooks, Effect.gen(function* () {
500
517
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
501
- return { inject: (root, configDependencies, seed, rules) => Effect.gen(function* () {
518
+ const fetch = makeFetchConfigDependency(spawner);
519
+ return { inject: (root, configDependencies, seed, rules, context) => Effect.gen(function* () {
502
520
  if (Object.keys(configDependencies).length === 0) return untouched(seed, rules);
503
- const pnpmfiles = yield* resolvePnpmfiles(root, configDependencies);
521
+ const pnpmfiles = yield* resolvePnpmfiles(root, configDependencies, {
522
+ side: context,
523
+ fetch
524
+ });
504
525
  const loadable = pnpmfiles.flatMap(({ name, path }) => path === void 0 ? [] : [[name, pathToFileURL(path).href]]);
505
526
  if (loadable.length === 0) return untouched(seed, rules, replaysOf(pnpmfiles));
506
527
  const command = ChildProcess.make("node", [
@@ -0,0 +1,170 @@
1
+ import { splitConfigDependencySpec } from "./internal/configDependencySpecGrammar.js";
2
+ import { Effect, Exit, Option, Result, Schema, SchemaIssue, SchemaTransformation } from "effect";
3
+ import { SriIntegrityHash } from "@effected/npm";
4
+ import { SemVer } from "@effected/semver";
5
+
6
+ //#region src/ConfigDependencySpec.ts
7
+ /**
8
+ * Indicates that a string could not be parsed as a pnpm `configDependencies`
9
+ * spec (`<version>[+<integrity>]`).
10
+ *
11
+ * Raised by {@link ConfigDependencySpec.parse} and
12
+ * {@link ConfigDependencySpec.parseResult}; the decode direction of
13
+ * {@link ConfigDependencySpec.FromString} reports the same failure through a
14
+ * generic `Schema` parse error carrying the same message. The offending string
15
+ * is preserved on `input`; `reason` says which half failed.
16
+ *
17
+ * @public
18
+ */
19
+ var InvalidConfigDependencySpecError = class extends Schema.TaggedError()("InvalidConfigDependencySpecError", {
20
+ /** The raw spec string that failed validation. */
21
+ input: Schema.String,
22
+ /**
23
+ * Which half of the spec failed: `version` (the text before the first
24
+ * `+` is not an exact SemVer version — ranges, partial versions,
25
+ * dist-tags and padded values all land here) or `integrity` (the text
26
+ * after the first `+` is not an SRI `<algo>-<base64>` hash).
27
+ */
28
+ reason: Schema.Literals(["version", "integrity"])
29
+ }) {
30
+ get message() {
31
+ return this.reason === "version" ? `Invalid config dependency spec "${this.input}": version must be an exact SemVer version (ranges, partial versions and dist-tags are not allowed)` : `Invalid config dependency spec "${this.input}": integrity must be an SRI <algo>-<base64> hash`;
32
+ }
33
+ };
34
+ const specVersion = SemVer.pipe(Schema.check(Schema.makeFilter((version) => version.build.length === 0 ? void 0 : "Expected a version without build metadata")));
35
+ /**
36
+ * A pnpm `configDependencies` spec from `pnpm-workspace.yaml`: an exact
37
+ * `version` and, on the legacy inline form only, an SRI `integrity`.
38
+ *
39
+ * @remarks
40
+ * pnpm 11 and later write the **bare** form — `pnpm add --config` records
41
+ * `0.11.1`, and the integrity lives in the lockfile. The inline form
42
+ * `0.11.1+sha512-<base64>` is deprecated but still read, and workspaces in the
43
+ * wild still carry it. {@link ConfigDependencySpec.bare} renders the bare form
44
+ * (a tool normalizing the field writes this); `toString()` re-renders the form
45
+ * that was parsed, so a tool that must not rewrite the field round-trips it
46
+ * byte-identically.
47
+ *
48
+ * The first `+` always begins the integrity, never semver build metadata, so
49
+ * `0.11.1+sha512-…` is version `0.11.1` plus integrity `sha512-…`; a tail that
50
+ * is not an SRI hash fails with `reason: "integrity"` rather than falling back
51
+ * to build-metadata parsing.
52
+ *
53
+ * `version` is `@effected/semver`'s `SemVer`: prerelease versions are
54
+ * accepted, ranges (`^0.11.1`), partial versions, dist-tags, a `v` prefix and
55
+ * padded values are not.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * import { ConfigDependencySpec } from "@effected/workspaces";
60
+ * import { Effect } from "effect";
61
+ *
62
+ * const program = Effect.gen(function* () {
63
+ * const spec = yield* ConfigDependencySpec.parse("0.11.1+sha512-m35m…==");
64
+ * return [spec.bare, spec.hasIntegrity] as const; // ["0.11.1", true]
65
+ * });
66
+ * ```
67
+ *
68
+ * @public
69
+ */
70
+ var ConfigDependencySpec = class ConfigDependencySpec extends Schema.Class("ConfigDependencySpec")({
71
+ /**
72
+ * The exact declared version. Never carries build metadata — the spec
73
+ * grammar cannot express it (see the class remarks), and a version
74
+ * constructed with build identifiers is rejected at construction.
75
+ */
76
+ version: specVersion,
77
+ /**
78
+ * The inline integrity (e.g. `sha512-m35m…==`): `@effected/npm`'s
79
+ * `SriIntegrityHash`, the shared restriction of the `IntegrityHash` brand
80
+ * to the SRI `<algo>-<base64>` form. `None` on the bare form pnpm 11+
81
+ * writes.
82
+ */
83
+ integrity: Schema.Option(SriIntegrityHash)
84
+ }) {
85
+ /**
86
+ * Schema transformation between the `<version>[+<integrity>]` string and a
87
+ * {@link ConfigDependencySpec}. Decoding parses via
88
+ * {@link ConfigDependencySpec.parseResult} (the surfaces share one grammar);
89
+ * encoding prints `toString()`, the form that was parsed.
90
+ */
91
+ static FromString = Schema.String.pipe(Schema.decodeTo(Schema.instanceOf(ConfigDependencySpec), SchemaTransformation.transformEffect({
92
+ decode: (input) => {
93
+ const parsed = ConfigDependencySpec.parseResult(input);
94
+ return Result.isSuccess(parsed) ? Effect.succeed(parsed.success) : Effect.fail(new SchemaIssue.InvalidValue({ message: parsed.failure.message }, input));
95
+ },
96
+ encode: (spec) => Effect.succeed(spec.toString())
97
+ })));
98
+ /**
99
+ * Parse a `configDependencies` spec, synchronously, returning a `Result`
100
+ * instead of an `Effect`.
101
+ *
102
+ * @remarks
103
+ * The text before the first `+` must be an exact SemVer version that
104
+ * renders back to itself byte for byte (`SemVer.isPinnable`, plus a
105
+ * canonical-spelling check so `toString()` round-trips); the text after it,
106
+ * when present, must be an SRI `<algo>-<base64>` hash. An empty tail
107
+ * (`0.11.1+`) is an integrity failure, not a bare spec.
108
+ * {@link ConfigDependencySpec.parse} is defined in terms of this function.
109
+ *
110
+ * @param input - the spec string to parse
111
+ * @returns a `Result` succeeding with the parsed {@link ConfigDependencySpec},
112
+ * or failing with {@link InvalidConfigDependencySpecError} naming the half
113
+ * that failed.
114
+ */
115
+ static parseResult(input) {
116
+ const parts = splitConfigDependencySpec(input);
117
+ const version = SemVer.parseResult(parts.version);
118
+ if (Result.isFailure(version) || !SemVer.isPinnable(parts.version) || version.success.toString() !== parts.version) return Result.fail(new InvalidConfigDependencySpecError({
119
+ input,
120
+ reason: "version"
121
+ }));
122
+ if (parts.integrity === void 0) return Result.succeed(ConfigDependencySpec.make({
123
+ version: version.success,
124
+ integrity: Option.none()
125
+ }));
126
+ const integrity = Schema.decodeUnknownExit(SriIntegrityHash)(parts.integrity);
127
+ if (Exit.isFailure(integrity)) return Result.fail(new InvalidConfigDependencySpecError({
128
+ input,
129
+ reason: "integrity"
130
+ }));
131
+ return Result.succeed(ConfigDependencySpec.make({
132
+ version: version.success,
133
+ integrity: Option.some(integrity.value)
134
+ }));
135
+ }
136
+ /**
137
+ * Parse a `configDependencies` spec. Defined in terms of
138
+ * {@link ConfigDependencySpec.parseResult} — synchronous callers can use
139
+ * that variant directly.
140
+ *
141
+ * @param input - the spec string to parse
142
+ * @returns the parsed {@link ConfigDependencySpec}. Fails with
143
+ * {@link InvalidConfigDependencySpecError} when `input` is not a valid spec.
144
+ */
145
+ static parse = Effect.fn("ConfigDependencySpec.parse")((input) => Effect.fromResult(ConfigDependencySpec.parseResult(input)));
146
+ /** Whether the spec carries an inline integrity (the legacy form). */
147
+ get hasIntegrity() {
148
+ return Option.isSome(this.integrity);
149
+ }
150
+ /**
151
+ * The bare `<version>` form — what pnpm 11+ writes to `configDependencies`
152
+ * — with any inline integrity dropped.
153
+ */
154
+ get bare() {
155
+ return this.version.toString();
156
+ }
157
+ /**
158
+ * The spec as parsed: `<version>` or `<version>+<integrity>`. The encode
159
+ * direction of {@link ConfigDependencySpec.FromString} prints exactly this.
160
+ */
161
+ toString() {
162
+ return Option.match(this.integrity, {
163
+ onNone: () => this.bare,
164
+ onSome: (integrity) => `${this.bare}+${integrity}`
165
+ });
166
+ }
167
+ };
168
+
169
+ //#endregion
170
+ export { ConfigDependencySpec, InvalidConfigDependencySpecError };
@@ -7,6 +7,7 @@ import { configDependenciesOf, inlinePeerDependencyRules } from "./internal/work
7
7
  import { Context, Duration, Effect, Exit, FileSystem, Layer, Option, Path, PlatformError, Schema } from "effect";
8
8
  import { CatalogAssemblyError, CatalogResolver, DependencyResolutionError, PartialReleaseAgeGate, ReleaseAgeGate } from "@effected/npm";
9
9
  import { Yaml } from "@effected/yaml";
10
+ import { filenameFor } from "@effected/lockfiles";
10
11
 
11
12
  //#region src/WorkspaceCatalogs.ts
12
13
  /**
@@ -289,10 +290,13 @@ const unstubbed = (method) => Effect.die(/* @__PURE__ */ new Error(`WorkspaceCat
289
290
  * @remarks
290
291
  * Rules are SEEDED, not merged afterwards: the workspace file's block goes
291
292
  * into the threaded config so the hooks merge onto it, exactly as pnpm seeds
292
- * its own config and takes back what the hooks return. Not part of the public
293
- * surface; `WorkspaceSnapshots` imports it directly.
293
+ * its own config and takes back what the hooks return. `context` is the
294
+ * document's own side — its lockfile and, at a ref, the ref — so a fetched
295
+ * config dependency is verified against the record of the side that declared
296
+ * it. Not part of the public surface; `WorkspaceSnapshots` imports it
297
+ * directly.
294
298
  */
295
- const injectFromDocument = (hooks, root, document, inline) => Effect.map(hooks.inject(root, configDependenciesOf(document), inline.entries, inlinePeerDependencyRules(document)), (injection) => ({
299
+ const injectFromDocument = (hooks, root, document, inline, context) => Effect.map(hooks.inject(root, configDependenciesOf(document), inline.entries, inlinePeerDependencyRules(document), context), (injection) => ({
296
300
  injected: CatalogSet.fromCatalogs(injection.catalogs),
297
301
  injection
298
302
  }));
@@ -370,7 +374,8 @@ var WorkspaceCatalogs = class WorkspaceCatalogs extends Context.Service()("@effe
370
374
  yield* validatePnpmWorkspaceCatalogs(document);
371
375
  inline = CatalogSet.fromCatalogs(inlineCatalogs(catalogBlocksOf(document)));
372
376
  inlineGate = yield* inlineReleaseAge(document);
373
- const replayed = yield* injectFromDocument(hooks, root, document, inline);
377
+ const lockfileText = Object.keys(configDependenciesOf(document)).length === 0 ? void 0 : yield* fs.readFileString(path.join(root, filenameFor("pnpm"))).pipe(Effect.orElseSucceed(() => void 0));
378
+ const replayed = yield* injectFromDocument(hooks, root, document, inline, { lockfile: lockfileText });
374
379
  injected = replayed.injected;
375
380
  hookGate = replayed.injection.releaseAge;
376
381
  peerDependencyRules = replayed.injection.peerDependencyRules;
@@ -127,18 +127,20 @@ var WorkspaceSnapshots = class WorkspaceSnapshots extends Context.Service()("@ef
127
127
  const catalogsService = yield* WorkspaceCatalogs;
128
128
  const hooks = yield* ConfigDependencyHooks;
129
129
  const seeded = (snapshot) => options?.seedCatalogs === void 0 ? snapshot : snapshot.withSeededCatalogs(options.seedCatalogs);
130
+ /** A manager's lockfile text at the ref, `none` when the ref has none. */
131
+ const lockfileText = (root, ref, format) => git.show(root, ref, `./${filenameFor(format)}`);
130
132
  /**
131
133
  * What a manager's lockfile records at the ref: its catalog set and its
132
134
  * importer versions, from ONE parse. Empty on both counts when the
133
135
  * lockfile is absent or malformed.
134
136
  */
135
- const lockfileRecord = (root, ref, format) => git.show(root, ref, `./${filenameFor(format)}`).pipe(Effect.flatMap((content) => Option.match(content, {
137
+ const lockfileRecord = (content, format) => Option.match(content, {
136
138
  onNone: () => Effect.succeed(EMPTY_LOCKFILE_RECORD),
137
139
  onSome: (text) => Lockfile.parse(text, { format }).pipe(Effect.map((lockfile) => ({
138
140
  catalogs: CatalogSet.fromLockfile(lockfile),
139
141
  importerVersions: importerVersionsOf(lockfile)
140
142
  })), Effect.catch(() => Effect.succeed(EMPTY_LOCKFILE_RECORD)))
141
- })));
143
+ });
142
144
  const computeAt = (root, ref) => Effect.gen(function* () {
143
145
  const [pnpmWorkspaceText, rootManifestText] = yield* Effect.all([git.show(root, ref, "./pnpm-workspace.yaml"), git.show(root, ref, "./package.json")], { concurrency: 2 });
144
146
  const rootManifest = Option.match(rootManifestText, {
@@ -159,15 +161,19 @@ var WorkspaceSnapshots = class WorkspaceSnapshots extends Context.Service()("@ef
159
161
  const pnpmPatterns = pnpmPatternsOf(document);
160
162
  patterns = pnpmPatterns.length > 0 ? pnpmPatterns : manifestPatternsOf(rootManifest);
161
163
  inline = yield* CatalogSet.fromWorkspaceYaml(pnpmWorkspaceText.value);
162
- const replayed = yield* injectFromDocument(hooks, root, document, inline);
164
+ const lockfile = yield* lockfileText(root, ref, "pnpm");
165
+ const replayed = yield* injectFromDocument(hooks, root, document, inline, {
166
+ lockfile: Option.getOrUndefined(lockfile),
167
+ ref
168
+ });
163
169
  injected = replayed.injected;
164
170
  hookReplays = replayed.injection.replays;
165
- recorded = yield* lockfileRecord(root, ref, "pnpm");
171
+ recorded = yield* lockfileRecord(lockfile, "pnpm");
166
172
  } else {
167
173
  patterns = manifestPatternsOf(rootManifest);
168
174
  inline = bunInlineCatalogs(rootManifest);
169
175
  injected = CatalogSet.empty();
170
- recorded = yield* lockfileRecord(root, ref, "bun");
176
+ recorded = yield* lockfileRecord(yield* lockfileText(root, ref, "bun"), "bun");
171
177
  }
172
178
  const catalogs = CatalogSet.merge(recorded.catalogs, inline, injected);
173
179
  const globs = yield* GlobSet.compile(patterns).pipe(Effect.mapError((error) => new CatalogAssemblyError({
package/index.d.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import { Git, GitCommandError, NotARepositoryError, UnknownRefError } from "@effected/git";
2
- import { Context, Effect, FileSystem, Layer, Option, Path, Schema } from "effect";
2
+ import { Context, Effect, FileSystem, Layer, Option, Path, Result, Schema } from "effect";
3
3
  import { CatalogAssemblyError, CatalogResolver, DependencyResolutionError, Manifest, PartialReleaseAgeGate, ReleaseAgeGate, UnresolvedDependencyError, WorkspaceResolver } from "@effected/npm";
4
4
  import { GlobPattern } from "@effected/glob";
5
5
  import { Lockfile, LockfileFramingError, LockfileIntegrity, LockfileParseError, ResolvedPackage, WorkspaceManifest } from "@effected/lockfiles";
6
6
  import { Package } from "@effected/package-json";
7
7
  import { ChildProcessSpawner } from "effect/unstable/process";
8
+ import { SemVer } from "@effected/semver";
8
9
  import { LocalExec } from "@effected/commands";
9
10
  //#region src/WorkspacePackage.d.ts
10
11
  declare const PublishConfig_base: Schema.Class<PublishConfig, Schema.Struct<{
@@ -934,12 +935,43 @@ interface PeerDependencyRules {
934
935
  export declare const NoPeerDependencyRules: PeerDependencyRules;
935
936
  /**
936
937
  * Where a replayed config dependency's declared version was found: the
937
- * `node_modules/.pnpm-config` copy, the pnpm store's `links/` tree, or a
938
+ * `node_modules/.pnpm-config` copy, the pnpm store's `links/` tree, the store
939
+ * after {@link ConfigDependencyHooks.layerSubprocess} fetched it, or a
938
940
  * {@link ConfigDependencyHooks.layerFrom} entry.
939
941
  *
940
942
  * @public
941
943
  */
942
- type HookReplaySource = "installed" | "store" | "supplied";
944
+ type HookReplaySource = "installed" | "store" | "fetched" | "supplied";
945
+ /**
946
+ * What the side whose `configDependencies` are being replayed recorded beyond
947
+ * that map — the input the fetch rung needs to verify what it fetches.
948
+ *
949
+ * @remarks
950
+ * Both fields are optional, and omitting the whole context is always valid:
951
+ * nothing here changes which version is replayed. `lockfile` matters only when
952
+ * a replaying layer must fetch a version installed nowhere, and only
953
+ * {@link ConfigDependencyHooks.layerSubprocess} fetches. Without it, a bare
954
+ * `configDependencies` spec has no integrity to verify a fetch against, so the
955
+ * fetch fails closed with `reason: "integrityUnavailable"`.
956
+ *
957
+ * `WorkspaceCatalogs` passes the working tree's lockfile, and
958
+ * `WorkspaceSnapshots.at(ref)` passes the lockfile and the ref it read at, so
959
+ * each side of a diff is verified against its own record.
960
+ *
961
+ * @public
962
+ */
963
+ interface HookReplayContext {
964
+ /**
965
+ * The declaring side's `pnpm-lock.yaml` text, whose env preamble records each
966
+ * config dependency's integrity. `undefined` when that side has no lockfile.
967
+ */
968
+ readonly lockfile?: string | undefined;
969
+ /**
970
+ * The git ref whose `pnpm-workspace.yaml` declared the `configDependencies`,
971
+ * used to name the side in error messages. `undefined` for the working tree.
972
+ */
973
+ readonly ref?: string | undefined;
974
+ }
943
975
  /**
944
976
  * Which version of a config dependency a replay actually loaded, and where
945
977
  * that version came from.
@@ -949,7 +981,9 @@ type HookReplaySource = "installed" | "store" | "supplied";
949
981
  * WHAT the hooks injected but which pinned version injected it — the
950
982
  * evidence that a "range moved" row came from a config-dependency bump and
951
983
  * not from a hook edit. `installed` is the `node_modules/.pnpm-config` copy,
952
- * `store` the pnpm store's `links/` tree, `supplied` a
984
+ * `store` the pnpm store's `links/` tree, `fetched` the store copy
985
+ * {@link ConfigDependencyHooks.layerSubprocess} fetched, verified, because
986
+ * neither held the declared version, and `supplied` a
953
987
  * {@link ConfigDependencyHooks.layerFrom} entry.
954
988
  *
955
989
  * @public
@@ -1048,8 +1082,12 @@ interface ConfigDependencyHooksShape {
1048
1082
  * threaded config so hooks merge onto them rather than replacing them.
1049
1083
  * Omitted means "the workspace file declares none", which is different
1050
1084
  * from "nobody looked" — the caller owns that distinction.
1085
+ * @param context - What the declaring side recorded beyond the map: its
1086
+ * lockfile, which verifies a fetched version, and its ref, which names it
1087
+ * in errors. Only {@link ConfigDependencyHooks.layerSubprocess} reads the
1088
+ * lockfile; omitting the context never changes which version is replayed.
1051
1089
  */
1052
- readonly inject: (root: string, configDependencies: Readonly<Record<string, string>>, seed: Readonly<Record<string, Readonly<Record<string, string>>>>, rules?: PeerDependencyRules) => Effect.Effect<HookInjection, CatalogAssemblyError>;
1090
+ readonly inject: (root: string, configDependencies: Readonly<Record<string, string>>, seed: Readonly<Record<string, Readonly<Record<string, string>>>>, rules?: PeerDependencyRules, context?: HookReplayContext) => Effect.Effect<HookInjection, CatalogAssemblyError>;
1053
1091
  }
1054
1092
  declare const ConfigDependencyHooks_base: Context.ServiceClass<ConfigDependencyHooks, "@effected/workspaces/ConfigDependencyHooks", ConfigDependencyHooksShape>;
1055
1093
  /**
@@ -1100,9 +1138,10 @@ export declare class ConfigDependencyHooks extends ConfigDependencyHooks_base {
1100
1138
  * `links/<name>/<declared>/*\/node_modules/<name>` (the store is located
1101
1139
  * from `node_modules/.modules.yaml`, the realpath of any `.pnpm-config`
1102
1140
  * entry, then `$PNPM_HOME` / `$XDG_DATA_HOME` / the platform default), else
1103
- * a typed error naming the package, the declared version, what is
1104
- * installed, the stores searched, and the remediation
1105
- * (`pnpm add --config <name>@<version>` in a throwaway workspace). In the
1141
+ * a typed error with `reason: "notInstalled"` naming the package, the
1142
+ * declared version, what is installed, the stores searched, and the
1143
+ * remediation (`pnpm add --config <name>@<version>` in a throwaway
1144
+ * workspace, or the subprocess layer, which fetches). In the
1106
1145
  * chosen directory the first of `pnpmfile.mjs`, `pnpmfile.cjs`,
1107
1146
  * `pnpmfile.js` present in one directory listing is loaded; a listed but
1108
1147
  * unreadable file fails typed at import time, never as "ships no hook".
@@ -1174,9 +1213,24 @@ export declare class ConfigDependencyHooks extends ConfigDependencyHooks_base {
1174
1213
  *
1175
1214
  * The declared-version resolution is the PARENT's: the same ladder as
1176
1215
  * `layerLive` (`.pnpm-config` when it holds the declared version, else the
1177
- * pnpm store, else a typed fail-closed error) runs here, and the child
1178
- * receives the resolved `[name, fileUrl]` pairs as one JSON argv
1179
- * argument. An empty `configDependencies`, or one whose dependencies all
1216
+ * pnpm store) runs here, and the child receives the resolved
1217
+ * `[name, fileUrl]` pairs as one JSON argv argument.
1218
+ *
1219
+ * One rung more than `layerLive`: a version held nowhere is FETCHED into
1220
+ * the store rather than failing, so a diff whose base side declares a
1221
+ * version this checkout never installed still replays that version. pnpm
1222
+ * runs `install --frozen-lockfile` in a scratch workspace (removed
1223
+ * afterwards) whose lockfile pins the integrity the declaring side recorded
1224
+ * — the inline `<version>+<integrity>` spec, else that side's
1225
+ * `pnpm-lock.yaml` env preamble ({@link HookReplayContext.lockfile}) — so
1226
+ * pnpm itself refuses a download that does not match. It writes to the
1227
+ * first store the ladder searched, so the next replay finds the version
1228
+ * there. The replay records `source: "fetched"`. Fail-closed: two recorded
1229
+ * integrities that disagree fail with `reason: "integrityMismatch"`, none
1230
+ * with `reason: "integrityUnavailable"` — nothing is fetched in either case
1231
+ * — and a fetch that fails with `reason: "fetchFailed"`, keeping the
1232
+ * not-installed remediation. The fetch needs `pnpm` 11 or 12 on `PATH` and
1233
+ * registry access, and is bounded at two minutes. An empty `configDependencies`, or one whose dependencies all
1180
1234
  * ship no pnpmfile, returns the seed without spawning anything; a `..` path
1181
1235
  * segment in a dependency name fails typed **before** any spawn; a
1182
1236
  * load failure in the child — a syntax error, a throwing top level, an
@@ -1210,6 +1264,157 @@ export declare class ConfigDependencyHooks extends ConfigDependencyHooks_base {
1210
1264
  static readonly layerSubprocess: Layer.Layer<ConfigDependencyHooks, never, ChildProcessSpawner.ChildProcessSpawner>;
1211
1265
  }
1212
1266
  //#endregion
1267
+ //#region src/ConfigDependencySpec.d.ts
1268
+ declare const InvalidConfigDependencySpecError_base: Schema.Class<InvalidConfigDependencySpecError, Schema.TaggedStruct<"InvalidConfigDependencySpecError", {
1269
+ /** The raw spec string that failed validation. */
1270
+ readonly input: Schema.String;
1271
+ /**
1272
+ * Which half of the spec failed: `version` (the text before the first
1273
+ * `+` is not an exact SemVer version — ranges, partial versions,
1274
+ * dist-tags and padded values all land here) or `integrity` (the text
1275
+ * after the first `+` is not an SRI `<algo>-<base64>` hash).
1276
+ */
1277
+ readonly reason: Schema.Literals<readonly ["version", "integrity"]>;
1278
+ }>, import("effect/Cause").YieldableError>;
1279
+ /**
1280
+ * Indicates that a string could not be parsed as a pnpm `configDependencies`
1281
+ * spec (`<version>[+<integrity>]`).
1282
+ *
1283
+ * Raised by {@link ConfigDependencySpec.parse} and
1284
+ * {@link ConfigDependencySpec.parseResult}; the decode direction of
1285
+ * {@link ConfigDependencySpec.FromString} reports the same failure through a
1286
+ * generic `Schema` parse error carrying the same message. The offending string
1287
+ * is preserved on `input`; `reason` says which half failed.
1288
+ *
1289
+ * @public
1290
+ */
1291
+ export declare class InvalidConfigDependencySpecError extends InvalidConfigDependencySpecError_base {
1292
+ get message(): string;
1293
+ }
1294
+ declare const ConfigDependencySpec_base: Schema.Class<ConfigDependencySpec, Schema.Struct<{
1295
+ /**
1296
+ * The exact declared version. Never carries build metadata — the spec
1297
+ * grammar cannot express it (see the class remarks), and a version
1298
+ * constructed with build identifiers is rejected at construction.
1299
+ */
1300
+ readonly version: Schema.decodeTo<Schema.declareConstructor<SemVer, {
1301
+ readonly major: number;
1302
+ readonly minor: number;
1303
+ readonly patch: number;
1304
+ readonly prerelease: readonly (string | number)[];
1305
+ readonly build: readonly string[];
1306
+ }, readonly [Schema.Struct<{
1307
+ readonly major: Schema.Number;
1308
+ readonly minor: Schema.Number;
1309
+ readonly patch: Schema.Number;
1310
+ readonly prerelease: Schema.$Array<Schema.Union<readonly [Schema.String, Schema.Number]>>;
1311
+ readonly build: Schema.$Array<Schema.String>;
1312
+ }>], {
1313
+ readonly major: number;
1314
+ readonly minor: number;
1315
+ readonly patch: number;
1316
+ readonly prerelease: readonly (string | number)[];
1317
+ readonly build: readonly string[];
1318
+ }>, Schema.Struct<{
1319
+ readonly major: Schema.Number;
1320
+ readonly minor: Schema.Number;
1321
+ readonly patch: Schema.Number;
1322
+ readonly prerelease: Schema.$Array<Schema.Union<readonly [Schema.String, Schema.Number]>>;
1323
+ readonly build: Schema.$Array<Schema.String>;
1324
+ }>, never, never>;
1325
+ /**
1326
+ * The inline integrity (e.g. `sha512-m35m…==`): `@effected/npm`'s
1327
+ * `SriIntegrityHash`, the shared restriction of the `IntegrityHash` brand
1328
+ * to the SRI `<algo>-<base64>` form. `None` on the bare form pnpm 11+
1329
+ * writes.
1330
+ */
1331
+ readonly integrity: Schema.Option<Schema.brand<Schema.String, "IntegrityHash">>;
1332
+ }>, {}>;
1333
+ /**
1334
+ * A pnpm `configDependencies` spec from `pnpm-workspace.yaml`: an exact
1335
+ * `version` and, on the legacy inline form only, an SRI `integrity`.
1336
+ *
1337
+ * @remarks
1338
+ * pnpm 11 and later write the **bare** form — `pnpm add --config` records
1339
+ * `0.11.1`, and the integrity lives in the lockfile. The inline form
1340
+ * `0.11.1+sha512-<base64>` is deprecated but still read, and workspaces in the
1341
+ * wild still carry it. {@link ConfigDependencySpec.bare} renders the bare form
1342
+ * (a tool normalizing the field writes this); `toString()` re-renders the form
1343
+ * that was parsed, so a tool that must not rewrite the field round-trips it
1344
+ * byte-identically.
1345
+ *
1346
+ * The first `+` always begins the integrity, never semver build metadata, so
1347
+ * `0.11.1+sha512-…` is version `0.11.1` plus integrity `sha512-…`; a tail that
1348
+ * is not an SRI hash fails with `reason: "integrity"` rather than falling back
1349
+ * to build-metadata parsing.
1350
+ *
1351
+ * `version` is `@effected/semver`'s `SemVer`: prerelease versions are
1352
+ * accepted, ranges (`^0.11.1`), partial versions, dist-tags, a `v` prefix and
1353
+ * padded values are not.
1354
+ *
1355
+ * @example
1356
+ * ```ts
1357
+ * import { ConfigDependencySpec } from "@effected/workspaces";
1358
+ * import { Effect } from "effect";
1359
+ *
1360
+ * const program = Effect.gen(function* () {
1361
+ * const spec = yield* ConfigDependencySpec.parse("0.11.1+sha512-m35m…==");
1362
+ * return [spec.bare, spec.hasIntegrity] as const; // ["0.11.1", true]
1363
+ * });
1364
+ * ```
1365
+ *
1366
+ * @public
1367
+ */
1368
+ export declare class ConfigDependencySpec extends ConfigDependencySpec_base {
1369
+ /**
1370
+ * Schema transformation between the `<version>[+<integrity>]` string and a
1371
+ * {@link ConfigDependencySpec}. Decoding parses via
1372
+ * {@link ConfigDependencySpec.parseResult} (the surfaces share one grammar);
1373
+ * encoding prints `toString()`, the form that was parsed.
1374
+ */
1375
+ static readonly FromString: Schema.Codec<ConfigDependencySpec, string>;
1376
+ /**
1377
+ * Parse a `configDependencies` spec, synchronously, returning a `Result`
1378
+ * instead of an `Effect`.
1379
+ *
1380
+ * @remarks
1381
+ * The text before the first `+` must be an exact SemVer version that
1382
+ * renders back to itself byte for byte (`SemVer.isPinnable`, plus a
1383
+ * canonical-spelling check so `toString()` round-trips); the text after it,
1384
+ * when present, must be an SRI `<algo>-<base64>` hash. An empty tail
1385
+ * (`0.11.1+`) is an integrity failure, not a bare spec.
1386
+ * {@link ConfigDependencySpec.parse} is defined in terms of this function.
1387
+ *
1388
+ * @param input - the spec string to parse
1389
+ * @returns a `Result` succeeding with the parsed {@link ConfigDependencySpec},
1390
+ * or failing with {@link InvalidConfigDependencySpecError} naming the half
1391
+ * that failed.
1392
+ */
1393
+ static parseResult(input: string): Result.Result<ConfigDependencySpec, InvalidConfigDependencySpecError>;
1394
+ /**
1395
+ * Parse a `configDependencies` spec. Defined in terms of
1396
+ * {@link ConfigDependencySpec.parseResult} — synchronous callers can use
1397
+ * that variant directly.
1398
+ *
1399
+ * @param input - the spec string to parse
1400
+ * @returns the parsed {@link ConfigDependencySpec}. Fails with
1401
+ * {@link InvalidConfigDependencySpecError} when `input` is not a valid spec.
1402
+ */
1403
+ static readonly parse: (input: string) => Effect.Effect<ConfigDependencySpec, InvalidConfigDependencySpecError, never>;
1404
+ /** Whether the spec carries an inline integrity (the legacy form). */
1405
+ get hasIntegrity(): boolean;
1406
+ /**
1407
+ * The bare `<version>` form — what pnpm 11+ writes to `configDependencies`
1408
+ * — with any inline integrity dropped.
1409
+ */
1410
+ get bare(): string;
1411
+ /**
1412
+ * The spec as parsed: `<version>` or `<version>+<integrity>`. The encode
1413
+ * direction of {@link ConfigDependencySpec.FromString} prints exactly this.
1414
+ */
1415
+ toString(): string;
1416
+ }
1417
+ //#endregion
1213
1418
  //#region src/DependencyGraph.d.ts
1214
1419
  declare const CyclicDependencyError_base: Schema.Class<CyclicDependencyError, Schema.TaggedStruct<"CyclicDependencyError", {
1215
1420
  /** The packages participating in the cycle. */
@@ -4312,5 +4517,5 @@ interface GetWorkspacePackagesSyncOptions extends WorkspacesSyncOptions {
4312
4517
  */
4313
4518
  export declare const getWorkspacePackagesSync: (root: string, options: GetWorkspacePackagesSyncOptions) => ReadonlyArray<WorkspacePackage>;
4314
4519
  //#endregion
4315
- export type { CatalogAssemblyFailure, ChangeDetectionFailure, ChangeDetectorShape, ClassifyOptions, ConfigDependencyHooksShape, DependencyDiff, DuplicateCheckOptions, FindWorkspaceRootOptions, GetWorkspacePackagesSyncOptions, HookInjection, HookReplay, HookReplaySource, ImporterVersions, LockfileReadFailure, LockfileReaderOptions, LockfileReaderShape, PackageManagerDetectionFailure, PackageManagerDetectorShape, PackageRelease, PeerCheckOptions, PeerDependencyRules, PublishabilityDetectorShape, SyncDirectoryEntry, SyncFileSystem, SyncPath, TagClassification, TagFormatOptions, TrackingTagOptions, UnverifiedReason, VersioningDetectOptions, WorkspaceCatalogsOptions, WorkspaceCatalogsShape, WorkspaceDiscoveryFailure, WorkspaceDiscoveryOptions, WorkspaceDiscoveryShape, WorkspaceDiscoverySkip, WorkspaceDiscoverySkipKind, WorkspaceLookupFailure, WorkspaceRootShape, WorkspaceSnapshotAtFailure, WorkspaceSnapshotWorktreeFailure, WorkspaceSnapshotsOptions, WorkspaceSnapshotsShape, WorkspacesGitOptions, WorkspacesOptions, WorkspacesServices, WorkspacesSyncOptions };
4520
+ export type { CatalogAssemblyFailure, ChangeDetectionFailure, ChangeDetectorShape, ClassifyOptions, ConfigDependencyHooksShape, DependencyDiff, DuplicateCheckOptions, FindWorkspaceRootOptions, GetWorkspacePackagesSyncOptions, HookInjection, HookReplay, HookReplayContext, HookReplaySource, ImporterVersions, LockfileReadFailure, LockfileReaderOptions, LockfileReaderShape, PackageManagerDetectionFailure, PackageManagerDetectorShape, PackageRelease, PeerCheckOptions, PeerDependencyRules, PublishabilityDetectorShape, SyncDirectoryEntry, SyncFileSystem, SyncPath, TagClassification, TagFormatOptions, TrackingTagOptions, UnverifiedReason, VersioningDetectOptions, WorkspaceCatalogsOptions, WorkspaceCatalogsShape, WorkspaceDiscoveryFailure, WorkspaceDiscoveryOptions, WorkspaceDiscoveryShape, WorkspaceDiscoverySkip, WorkspaceDiscoverySkipKind, WorkspaceLookupFailure, WorkspaceRootShape, WorkspaceSnapshotAtFailure, WorkspaceSnapshotWorktreeFailure, WorkspaceSnapshotsOptions, WorkspaceSnapshotsShape, WorkspacesGitOptions, WorkspacesOptions, WorkspacesServices, WorkspacesSyncOptions };
4316
4521
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -4,6 +4,7 @@ import { PackageNotFoundError, WorkspaceDiscovery, WorkspaceDiscoveryError, Work
4
4
  import { CyclicDependencyError, DependencyGraph } from "./DependencyGraph.js";
5
5
  import { ChangeDetectionError, ChangeDetectionOptions, ChangeDetector } from "./ChangeDetector.js";
6
6
  import { ConfigDependencyHooks, NoPeerDependencyRules } from "./ConfigDependencyHooks.js";
7
+ import { ConfigDependencySpec, InvalidConfigDependencySpecError } from "./ConfigDependencySpec.js";
7
8
  import { Dependent, DuplicateCheck, DuplicateInstance, DuplicatedPackage, DuplicatedVersion } from "./DuplicateCheck.js";
8
9
  import { DetectedPackageManager, PackageManagerDetectionError, PackageManagerDetector, PackageManagerEvidence, PackageManagerName } from "./PackageManagerName.js";
9
10
  import { LockfileReadError, LockfileReader } from "./LockfileReader.js";
@@ -17,4 +18,4 @@ import { WorkspaceSnapshots } from "./WorkspaceSnapshots.js";
17
18
  import { Workspaces } from "./Workspaces.js";
18
19
  import { findWorkspaceRootSync, getWorkspacePackagesSync } from "./WorkspacesSync.js";
19
20
 
20
- export { CatalogSet, ChangeDetectionError, ChangeDetectionOptions, ChangeDetector, ConfigDependencyHooks, CyclicDependencyError, DependencyGraph, Dependent, DetectedPackageManager, DuplicateCheck, DuplicateInstance, DuplicatedPackage, DuplicatedVersion, LockfileReadError, LockfileReader, NoPeerDependencyRules, PackageManagerDetectionError, PackageManagerDetector, PackageManagerEvidence, PackageManagerName, PackageNotFoundError, PackageStateSnapshot, PeerCheck, PeerParent, PublishConfig, PublishTarget, PublishabilityDetector, ReleaseTag, TagStyle, TrackingTag, UnsatisfiedPeer, VersioningStrategy, VersioningStrategyType, WORKSPACE_MARKERS, WorkspaceCatalogs, WorkspaceDiscovery, WorkspaceDiscoveryError, WorkspaceInfo, WorkspaceManifestError, WorkspacePackage, WorkspacePatternError, WorkspaceRoot, WorkspaceRootNotFoundError, WorkspaceSnapshots, WorkspaceStateSnapshot, Workspaces, classifyTag, findWorkspaceRootSync, getWorkspacePackagesSync };
21
+ export { CatalogSet, ChangeDetectionError, ChangeDetectionOptions, ChangeDetector, ConfigDependencyHooks, ConfigDependencySpec, CyclicDependencyError, DependencyGraph, Dependent, DetectedPackageManager, DuplicateCheck, DuplicateInstance, DuplicatedPackage, DuplicatedVersion, InvalidConfigDependencySpecError, LockfileReadError, LockfileReader, NoPeerDependencyRules, PackageManagerDetectionError, PackageManagerDetector, PackageManagerEvidence, PackageManagerName, PackageNotFoundError, PackageStateSnapshot, PeerCheck, PeerParent, PublishConfig, PublishTarget, PublishabilityDetector, ReleaseTag, TagStyle, TrackingTag, UnsatisfiedPeer, VersioningStrategy, VersioningStrategyType, WORKSPACE_MARKERS, WorkspaceCatalogs, WorkspaceDiscovery, WorkspaceDiscoveryError, WorkspaceInfo, WorkspaceManifestError, WorkspacePackage, WorkspacePatternError, WorkspaceRoot, WorkspaceRootNotFoundError, WorkspaceSnapshots, WorkspaceStateSnapshot, Workspaces, classifyTag, findWorkspaceRootSync, getWorkspacePackagesSync };
@@ -0,0 +1,146 @@
1
+ import { carries, manifestVersion, messageOf, sideLabel } from "./configDependencyShared.js";
2
+ import { splitConfigDependencySpec } from "./configDependencySpecGrammar.js";
3
+ import { Duration, Effect } from "effect";
4
+ import { IntegrityHash } from "@effected/npm";
5
+ import { Run } from "@effected/commands";
6
+ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
7
+ import { mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
8
+ import { tmpdir } from "node:os";
9
+ import { basename, dirname, join } from "node:path";
10
+
11
+ //#region src/internal/configDependencyFetch.ts
12
+ const fetchFailure = (reason, message, cause) => cause === void 0 ? {
13
+ reason,
14
+ message
15
+ } : {
16
+ reason,
17
+ message,
18
+ cause
19
+ };
20
+ /**
21
+ * The ceiling on one fetch. It downloads one small tarball; two minutes covers
22
+ * a slow registry without letting a hung pnpm stall catalog assembly forever.
23
+ */
24
+ const FETCH_TIMEOUT = Duration.minutes(2);
25
+ /**
26
+ * The integrity the fetched copy must match, decided before anything is
27
+ * spawned: the inline spec integrity (legacy) and the declaring side's
28
+ * lockfile preamble entry for exactly `<name>@<version>`. Both present and
29
+ * different, neither present, an inline integrity that is not SRI, or a
30
+ * lockfile that cannot be read all fail typed. A lockfile that records the
31
+ * name at a different version is not a source for this version.
32
+ *
33
+ * @remarks
34
+ * An unreadable lockfile fails closed even when the spec carries an inline
35
+ * integrity: the lockfile is the declaring side's own checksum store, and a
36
+ * store that cannot be read cannot be confirmed to agree with the inline pin.
37
+ */
38
+ const expectedIntegrity = (request) => Effect.gen(function* () {
39
+ const { name, version, side } = request;
40
+ const key = `${name}@${version}`;
41
+ const inline = splitConfigDependencySpec(request.spec).integrity;
42
+ if (inline !== void 0 && !IntegrityHash.isSri(inline)) return yield* Effect.fail(fetchFailure("integrityUnavailable", `config dependency ${key} declared by ${sideLabel(side)} carries an inline integrity that is not an SRI hash, so a fetched copy cannot be verified and nothing was fetched.`));
43
+ const lock = (yield* request.locks.pipe(Effect.mapError((cause) => fetchFailure("integrityUnavailable", `config dependency ${key} must be fetched, but the pnpm-lock.yaml of ${sideLabel(side)} cannot be read (${messageOf(cause)}), so its recorded integrity cannot be checked and nothing was fetched.`, cause))))?.get(name);
44
+ const locked = lock !== void 0 && lock.version === version ? lock.integrity : void 0;
45
+ if (inline !== void 0 && locked !== void 0 && inline !== locked) return yield* Effect.fail(fetchFailure("integrityMismatch", `config dependency ${key} declared by ${sideLabel(side)} has two different recorded integrities: pnpm-workspace.yaml pins ${inline} and pnpm-lock.yaml records ${locked}. Nothing was fetched; reconcile the two (a fresh \`pnpm install\` rewrites the lockfile) before retrying.`));
46
+ const integrity = inline ?? locked;
47
+ if (integrity === void 0) return yield* Effect.fail(fetchFailure("integrityUnavailable", `config dependency ${key} must be fetched, but ${sideLabel(side)} records no integrity for it (no inline integrity in pnpm-workspace.yaml and no pnpm-lock.yaml entry for that version), so a fetched copy could not be verified and nothing was fetched.`));
48
+ return integrity;
49
+ });
50
+ /**
51
+ * The scratch workspace's `pnpm-workspace.yaml`. Every scalar is a
52
+ * JSON-quoted string, which YAML reads verbatim, so no declared text can
53
+ * change the document's structure.
54
+ */
55
+ const scratchWorkspaceYaml = (name, version) => `configDependencies:\n ${JSON.stringify(name)}: ${JSON.stringify(version)}\n`;
56
+ /**
57
+ * The scratch workspace's `pnpm-lock.yaml`: the env preamble pnpm 11 and 12
58
+ * write for a lone config dependency, pinned to `integrity`, followed by the
59
+ * empty lockfile document they write when there are no projects.
60
+ */
61
+ const scratchLockfile = (name, version, integrity) => {
62
+ const key = JSON.stringify(`${name}@${version}`);
63
+ return [
64
+ "---",
65
+ "lockfileVersion: '9.0'",
66
+ "",
67
+ "importers:",
68
+ "",
69
+ " .:",
70
+ " configDependencies:",
71
+ ` ${JSON.stringify(name)}:`,
72
+ ` specifier: ${JSON.stringify(version)}`,
73
+ ` version: ${JSON.stringify(version)}`,
74
+ "",
75
+ "packages:",
76
+ "",
77
+ ` ${key}:`,
78
+ ` resolution: {integrity: ${JSON.stringify(integrity)}}`,
79
+ "",
80
+ "snapshots:",
81
+ "",
82
+ ` ${key}: {}`,
83
+ "",
84
+ "---",
85
+ ""
86
+ ].join("\n");
87
+ };
88
+ /**
89
+ * The `--store-dir` to hand pnpm so it writes into the store the ladder
90
+ * searched first. Discovered stores are the versioned directories
91
+ * (`<root>/v11`), and pnpm appends that segment itself, so a versioned
92
+ * directory is passed as its parent. `undefined` (pnpm's own default) when no
93
+ * store was discovered.
94
+ */
95
+ const storeDirArgument = (stores) => {
96
+ const first = stores[0];
97
+ if (first === void 0) return void 0;
98
+ return /^v\d+$/.test(basename(first)) ? dirname(first) : first;
99
+ };
100
+ /** The pnpm argv for the verified fetch. */
101
+ const fetchArgs = (scratch, stores) => {
102
+ const storeDir = storeDirArgument(stores);
103
+ return [
104
+ "install",
105
+ "--frozen-lockfile",
106
+ "--dir",
107
+ scratch,
108
+ ...storeDir === void 0 ? [] : ["--store-dir", storeDir]
109
+ ];
110
+ };
111
+ /** Whether `resolved` sits under a store's `links/<name>/<version>/` tree. */
112
+ const isStoreEntry = (resolved, name, version) => resolved.split(/[/\\]/).join("/").includes(`/links/${name}/${version}/`);
113
+ /**
114
+ * Build the fetch rung over a spawner. Wired only by
115
+ * `ConfigDependencyHooks.layerSubprocess`.
116
+ */
117
+ const makeFetchConfigDependency = (spawner) => (request) => Effect.scoped(Effect.gen(function* () {
118
+ const { name, version } = request;
119
+ const key = `${name}@${version}`;
120
+ const failed = (message, cause) => fetchFailure("fetchFailed", message, cause);
121
+ const io = (what, run) => Effect.tryPromise({
122
+ try: run,
123
+ catch: (cause) => failed(`${what} failed: ${messageOf(cause)}`, cause)
124
+ });
125
+ const integrity = yield* expectedIntegrity(request);
126
+ const scratch = yield* Effect.acquireRelease(io("creating a scratch workspace", () => mkdtemp(join(tmpdir(), "effected-config-dependency-"))), (dir) => Effect.promise(() => rm(dir, {
127
+ recursive: true,
128
+ force: true
129
+ }).catch(() => void 0)));
130
+ const lockfile = scratchLockfile(name, version, integrity);
131
+ yield* io("writing the scratch workspace", async () => {
132
+ await writeFile(join(scratch, "pnpm-workspace.yaml"), scratchWorkspaceYaml(name, version));
133
+ await writeFile(join(scratch, "pnpm-lock.yaml"), lockfile);
134
+ });
135
+ const command = ChildProcess.make("pnpm", fetchArgs(scratch, request.stores), { cwd: scratch });
136
+ yield* Run.text(command, { timeout: FETCH_TIMEOUT }).pipe(Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), Effect.mapError((cause) => failed(`fetching ${key} with \`pnpm ${fetchArgs("<scratch>", request.stores).join(" ")}\` failed: ${messageOf(cause)}`, cause)));
137
+ if ((yield* io("reading back the scratch lockfile", () => readFile(join(scratch, "pnpm-lock.yaml"), "utf8"))) !== lockfile) return yield* Effect.fail(failed(`fetching ${key}: pnpm rewrote the integrity-pinned scratch lockfile instead of installing from it, so the installed copy was not verified against the recorded integrity and is not used.`));
138
+ const linked = join(scratch, "node_modules", ".pnpm-config", name);
139
+ const resolved = yield* io(`resolving ${linked}`, () => realpath(linked));
140
+ const manifest = yield* manifestVersion(name, resolved).pipe(Effect.mapError((error) => failed(`the fetched ${key} has an unreadable package.json: ${messageOf(error.cause)}`, error.cause)));
141
+ if (!carries(manifest, version) || !isStoreEntry(resolved, name, version)) return yield* Effect.fail(failed(`fetching ${key}: pnpm linked ${resolved}, which is not a store copy of that version (its manifest reads ${manifest._tag === "version" ? manifest.version : "no version"}).`));
142
+ return resolved;
143
+ }));
144
+
145
+ //#endregion
146
+ export { expectedIntegrity, fetchArgs, makeFetchConfigDependency, storeDirArgument };
@@ -1,6 +1,9 @@
1
+ import { carries, hooksError, ioOrNone, manifestVersion, sideLabel } from "./configDependencyShared.js";
2
+ import { splitConfigDependencySpec } from "./configDependencySpecGrammar.js";
1
3
  import { Array, Duration, Effect, Exit, Option, Predicate, Result } from "effect";
2
- import { CatalogAssemblyError, PackageManagerCache } from "@effected/npm";
4
+ import { PackageManagerCache } from "@effected/npm";
3
5
  import { Yaml } from "@effected/yaml";
6
+ import { PnpmEnvLockfile } from "@effected/lockfiles";
4
7
  import { readFile, readdir, realpath } from "node:fs/promises";
5
8
  import { homedir } from "node:os";
6
9
  import { basename, dirname, join } from "node:path";
@@ -15,11 +18,13 @@ const PNPMFILE_CANDIDATES = [
15
18
  /**
16
19
  * The version part of a `configDependencies` spec: the text before the first
17
20
  * `+` (`0.9.0+sha512-…` → `0.9.0`); a bare `0.9.0` is returned whole.
21
+ *
22
+ * Deliberately lenient — it goes through the same split as the public
23
+ * `ConfigDependencySpec` but validates neither half. See
24
+ * `configDependencySpecGrammar.ts` for why replay must not adopt the strict
25
+ * model's rejections.
18
26
  */
19
- const declaredVersionOf = (spec) => {
20
- const plus = spec.indexOf("+");
21
- return plus === -1 ? spec : spec.slice(0, plus);
22
- };
27
+ const declaredVersionOf = (spec) => splitConfigDependencySpec(spec).version;
23
28
  /**
24
29
  * Whether a config-dependency `name` carries a `..` path segment. A scoped name
25
30
  * legitimately contains `/` (`@scope/pkg`), so only a `..` *segment* is rejected —
@@ -27,12 +32,6 @@ const declaredVersionOf = (spec) => {
27
32
  * dynamic `import()`.
28
33
  */
29
34
  const hasTraversalSegment = (name) => name.split(/[/\\]/).includes("..");
30
- /** The typed `hooks`-source failure every rung of the ladder reports through. */
31
- const hooksError = (path, cause) => new CatalogAssemblyError({
32
- source: "hooks",
33
- path,
34
- cause
35
- });
36
35
  /**
37
36
  * The declared `(name, version)` pairs of a `configDependencies` map, in
38
37
  * declaration order, with EVERY name validated before any path is built from
@@ -42,36 +41,12 @@ const hooksError = (path, cause) => new CatalogAssemblyError({
42
41
  const declaredEntries = (configDependencies) => {
43
42
  const entries = Object.entries(configDependencies).map(([name, spec]) => ({
44
43
  name,
45
- version: declaredVersionOf(spec)
44
+ version: declaredVersionOf(spec),
45
+ spec
46
46
  }));
47
47
  const traversal = entries.find((entry) => hasTraversalSegment(entry.name));
48
48
  return traversal === void 0 ? Effect.succeed(entries) : Effect.fail(hooksError(traversal.name, /* @__PURE__ */ new Error(`config dependency name has a '..' path segment: ${traversal.name}`)));
49
49
  };
50
- /** Whether a `node:fs` rejection means "nothing there" (as opposed to a real IO failure). */
51
- const isAbsent = (cause) => Predicate.isObject(cause) && (cause.code === "ENOENT" || cause.code === "ENOTDIR");
52
- /**
53
- * Run a `node:fs/promises` call, mapping an absent target to `Option.none()`
54
- * and ANY other rejection (`EACCES`, `EIO`, …) to a typed `hooks` error
55
- * attributed to `path` — never a silent skip.
56
- */
57
- const ioOrNone = (path, run) => Effect.tryPromise({
58
- try: run,
59
- catch: (cause) => cause
60
- }).pipe(Effect.map(Option.some), Effect.catch((cause) => isAbsent(cause) ? Effect.succeed(Option.none()) : Effect.fail(hooksError(path, cause))));
61
- /**
62
- * The `version` of the `package.json` in `dir`: `None` when the manifest is
63
- * absent, `Some("")` when present but carrying no string version (so it can
64
- * never match a declared version), typed on any other IO failure or on
65
- * unparseable JSON — a manifest that exists but cannot be read is not
66
- * evidence of absence.
67
- */
68
- const manifestVersion = (name, dir) => ioOrNone(name, () => readFile(join(dir, "package.json"), "utf8")).pipe(Effect.flatMap((text) => {
69
- if (Option.isNone(text)) return Effect.succeed(Option.none());
70
- return Effect.try({
71
- try: () => JSON.parse(text.value),
72
- catch: (cause) => hooksError(name, cause)
73
- }).pipe(Effect.map((parsed) => Option.some(Predicate.isObject(parsed) && typeof parsed.version === "string" ? parsed.version : "")));
74
- }));
75
50
  /** Directory entries of `dir`, or `[]` when it is absent; typed on any other failure. */
76
51
  const entriesOf = (path, dir) => ioOrNone(path, () => readdir(dir)).pipe(Effect.map(Option.getOrElse(() => [])));
77
52
  /** The `<root>/node_modules/.pnpm-config` directory. */
@@ -206,7 +181,7 @@ const findInStores = (name, declared, stores) => Effect.gen(function* () {
206
181
  for (const hash of yield* entriesOf(name, versionDir)) {
207
182
  const dir = join(versionDir, hash, "node_modules", name);
208
183
  const version = yield* manifestVersion(name, dir);
209
- if (Option.isSome(version) && version.value === declared) matches.push(dir);
184
+ if (carries(version, declared)) matches.push(dir);
210
185
  }
211
186
  if (matches.length > 0) return {
212
187
  store,
@@ -220,12 +195,23 @@ const findInStores = (name, declared, stores) => Effect.gen(function* () {
220
195
  });
221
196
  /** The fail-closed message for a version ONE store holds more than once: which store, which copies, how to disambiguate. */
222
197
  const ambiguousMessage = (name, declared, store, matches) => `config dependency ${name}@${declared} is ambiguous: the pnpm store at ${store} holds ${matches.length} copies of that version (${matches.join(", ")}) and the store records no integrity to tell them apart, so none is replayed. Remove the stale copies, or install ${name}@${declared} in this workspace so node_modules/.pnpm-config answers instead.`;
223
- /** The fail-closed message: what was declared, what is installed, where we looked, and how to fix it. */
224
- const notInstalledMessage = (name, declared, installed, stores) => {
225
- return `config dependency ${name}@${declared} is not installed: node_modules/.pnpm-config/${name} holds ${Option.match(installed, {
226
- onNone: () => "nothing",
227
- onSome: (version) => version === "" ? "a package with no version" : `version ${version}`
228
- })}, and no store copy of ${name}@${declared} was found (${stores.length === 0 ? "no pnpm store directory could be located" : `searched ${stores.join(", ")}`}). Run \`pnpm add --config ${name}@${declared}\` in a throwaway workspace to populate the store, then retry.`;
198
+ /**
199
+ * The fail-closed message: what was declared, by which side, what is
200
+ * installed, where we looked, why that happens, and how to fix it. `fetch`
201
+ * is the fetch rung's failure when one ran.
202
+ */
203
+ const notInstalledMessage = (name, declared, installed, stores, side, fetch) => {
204
+ const holds = installed._tag === "absent" ? "nothing" : installed._tag === "unversioned" ? "a package with no version" : `version ${installed.version}`;
205
+ const searched = stores.length === 0 ? "no pnpm store directory could be located" : `searched ${stores.join(", ")}`;
206
+ return [
207
+ `config dependency ${name}@${declared}${side.ref === void 0 ? "" : ` (declared at ${sideLabel(side)})`} is not installed: node_modules/.pnpm-config/${name} holds ${holds}, and no store copy of ${name}@${declared} was found (${searched}).`,
208
+ installed._tag === "version" && installed.version !== declared ? "This side declares a different version from the installed one, as the base side of a diff across a config-dependency bump does: that version was never installed in this checkout." : "",
209
+ Option.match(fetch, {
210
+ onNone: () => "This replay layer does not fetch; ConfigDependencyHooks.layerSubprocess, which the *Subprocess Workspaces composites use, fetches the declared version, verified, into the store.",
211
+ onSome: (failure) => failure.reason === "fetchFailed" ? `Fetching it into the store failed: ${failure.message}` : failure.message
212
+ }),
213
+ Option.exists(fetch, (failure) => failure.reason === "integrityMismatch") ? "" : `Run \`pnpm add --config ${name}@${declared}\` in a throwaway workspace to populate the store, then retry.`
214
+ ].filter((sentence) => sentence !== "").join(" ");
229
215
  };
230
216
  /**
231
217
  * The first pnpmfile candidate present in `dir`, from ONE directory listing.
@@ -238,14 +224,26 @@ const pnpmfileIn = (name, dir) => entriesOf(name, dir).pipe(Effect.map((entries)
238
224
  /**
239
225
  * Resolve one declared config dependency to the directory holding its
240
226
  * declared version: `.pnpm-config` when it holds exactly that version, else
241
- * the store, else a typed fail-closed error. `stores` is the memoized
242
- * discovery, run at most once per {@link resolvePnpmfiles} call and only on
243
- * the first `.pnpm-config` miss.
227
+ * the store, else the fetch rung when one is wired, else a typed fail-closed
228
+ * error. `stores` is the memoized discovery, run at most once per
229
+ * {@link resolvePnpmfiles} call and only on the first `.pnpm-config` miss;
230
+ * `locks` is the declaring side's memoized lockfile decode, run at most once
231
+ * per call and only by the fetch rung.
232
+ *
233
+ * @remarks
234
+ * A fetch failure is folded into the not-installed message, so the error
235
+ * still says what was declared, what is installed and where the ladder
236
+ * looked, and it keeps the fetch rung's own `reason` (`integrityMismatch`,
237
+ * `integrityUnavailable`, `fetchFailed`). The `pnpm add --config`
238
+ * remediation is kept except for `integrityMismatch`, where the pins must be
239
+ * reconciled first.
244
240
  */
245
- const resolveDirectory = (root, name, declared, stores) => Effect.gen(function* () {
241
+ const resolveDirectory = (root, entry, stores, locks, options) => Effect.gen(function* () {
242
+ const { name, version: declared } = entry;
243
+ const side = options.side ?? {};
246
244
  const installedDir = join(pnpmConfigDir(root), name);
247
245
  const installed = yield* manifestVersion(name, installedDir);
248
- if (Option.isSome(installed) && installed.value === declared) return {
246
+ if (carries(installed, declared)) return {
249
247
  dir: installedDir,
250
248
  source: "installed"
251
249
  };
@@ -256,23 +254,51 @@ const resolveDirectory = (root, name, declared, stores) => Effect.gen(function*
256
254
  dir: only,
257
255
  source: "store"
258
256
  };
259
- if (only !== void 0) return yield* Effect.fail(hooksError(name, new Error(ambiguousMessage(name, declared, store, matches))));
260
- return yield* Effect.fail(hooksError(name, new Error(notInstalledMessage(name, declared, installed, searched))));
257
+ if (only !== void 0) return yield* Effect.fail(hooksError(name, new Error(ambiguousMessage(name, declared, store, matches)), "ambiguous"));
258
+ const notInstalled = (fetch) => {
259
+ const message = notInstalledMessage(name, declared, installed, searched, side, fetch);
260
+ const cause = Option.getOrUndefined(fetch)?.cause;
261
+ return hooksError(name, cause === void 0 ? new Error(message) : new Error(message, { cause }), Option.match(fetch, {
262
+ onNone: () => "notInstalled",
263
+ onSome: (failure) => failure.reason
264
+ }));
265
+ };
266
+ if (options.fetch === void 0) return yield* Effect.fail(notInstalled(Option.none()));
267
+ return {
268
+ dir: yield* options.fetch({
269
+ name,
270
+ version: declared,
271
+ spec: entry.spec,
272
+ stores: searched,
273
+ side,
274
+ locks
275
+ }).pipe(Effect.mapError((failure) => notInstalled(Option.some(failure)))),
276
+ source: "fetched"
277
+ };
261
278
  });
262
279
  /**
280
+ * A lazy memo of `effect`, scoped to one {@link resolvePnpmfiles} call: the
281
+ * house success-only memo (never `Effect.cached`, which would pin an
282
+ * interrupt), so a failed or interrupted run is retried by the next user.
283
+ */
284
+ const memoizeSuccess = (effect) => Effect.map(Effect.cachedInvalidateWithTTL(effect, Duration.infinity), ([once, invalidate]) => Effect.onExit(once, (exit) => Exit.isSuccess(exit) ? Effect.void : invalidate));
285
+ /**
263
286
  * Resolve every declared config dependency at its declared version, in
264
287
  * declaration order. A dependency that ships no pnpmfile is still returned
265
288
  * (resolved, `path` undefined — the one legitimate skip at replay time); a
266
289
  * `..` segment in a name, an unresolvable declared version, or any
267
290
  * non-absent IO failure fails typed as a `hooks`-source
268
- * `CatalogAssemblyError`.
291
+ * `CatalogAssemblyError`. `options.fetch`, when wired, is tried for a version
292
+ * found nowhere before that fails.
269
293
  */
270
- const resolvePnpmfiles = (root, configDependencies) => Effect.gen(function* () {
294
+ const resolvePnpmfiles = (root, configDependencies, options = {}) => Effect.gen(function* () {
271
295
  const entries = yield* declaredEntries(configDependencies);
272
- const [discoverOnce, invalidate] = yield* Effect.cachedInvalidateWithTTL(discoverStores(root), Duration.infinity);
273
- const stores = Effect.onExit(discoverOnce, (exit) => Exit.isSuccess(exit) ? Effect.void : invalidate);
274
- return yield* Effect.forEach(entries, ({ name, version }) => Effect.gen(function* () {
275
- const { dir, source } = yield* resolveDirectory(root, name, version, stores);
296
+ const stores = yield* memoizeSuccess(discoverStores(root));
297
+ const lockfile = options.side?.lockfile;
298
+ const locks = lockfile === void 0 ? Effect.succeed(void 0) : yield* memoizeSuccess(PnpmEnvLockfile.configDependencies(lockfile));
299
+ return yield* Effect.forEach(entries, (entry) => Effect.gen(function* () {
300
+ const { name, version } = entry;
301
+ const { dir, source } = yield* resolveDirectory(root, entry, stores, locks, options);
276
302
  const pnpmfile = yield* pnpmfileIn(name, dir);
277
303
  return {
278
304
  name,
@@ -0,0 +1,51 @@
1
+ import { Effect, Option, Predicate } from "effect";
2
+ import { CatalogAssemblyError } from "@effected/npm";
3
+ import { readFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+
6
+ //#region src/internal/configDependencyShared.ts
7
+ /** The typed `hooks`-source failure every rung of the ladder reports through. */
8
+ const hooksError = (path, cause, reason) => new CatalogAssemblyError({
9
+ source: "hooks",
10
+ path,
11
+ cause,
12
+ ...reason === void 0 ? {} : { reason }
13
+ });
14
+ /** The message of a cause, for splicing into ours. */
15
+ const messageOf = (cause) => cause instanceof Error ? cause.message : String(cause);
16
+ /** Where the declared spec came from, for messages: `the working tree` or `ref <ref>`. */
17
+ const sideLabel = (side) => side.ref === void 0 ? "the working tree" : `ref ${side.ref}`;
18
+ /** Whether a `node:fs` rejection means "nothing there" (as opposed to a real IO failure). */
19
+ const isAbsent = (cause) => Predicate.isObject(cause) && (cause.code === "ENOENT" || cause.code === "ENOTDIR");
20
+ /**
21
+ * Run a `node:fs/promises` call, mapping an absent target to `Option.none()`
22
+ * and ANY other rejection (`EACCES`, `EIO`, …) to a typed `hooks` error
23
+ * attributed to `path` — never a silent skip.
24
+ */
25
+ const ioOrNone = (path, run) => Effect.tryPromise({
26
+ try: run,
27
+ catch: (cause) => cause
28
+ }).pipe(Effect.map(Option.some), Effect.catch((cause) => isAbsent(cause) ? Effect.succeed(Option.none()) : Effect.fail(hooksError(path, cause))));
29
+ const ABSENT = { _tag: "absent" };
30
+ const UNVERSIONED = { _tag: "unversioned" };
31
+ /** Whether a manifest carries exactly `declared`. Only a `version` state can. */
32
+ const carries = (manifest, declared) => manifest._tag === "version" && manifest.version === declared;
33
+ /**
34
+ * The version state of the `package.json` in `dir`: `absent` when there is
35
+ * no manifest, `unversioned` when it carries no non-empty string `version`,
36
+ * typed on any other IO failure or on unparseable JSON — a manifest that
37
+ * exists but cannot be read is not evidence of absence.
38
+ */
39
+ const manifestVersion = (name, dir) => ioOrNone(name, () => readFile(join(dir, "package.json"), "utf8")).pipe(Effect.flatMap((text) => {
40
+ if (Option.isNone(text)) return Effect.succeed(ABSENT);
41
+ return Effect.try({
42
+ try: () => JSON.parse(text.value),
43
+ catch: (cause) => hooksError(name, cause)
44
+ }).pipe(Effect.map((parsed) => Predicate.isObject(parsed) && typeof parsed.version === "string" && parsed.version !== "" ? {
45
+ _tag: "version",
46
+ version: parsed.version
47
+ } : UNVERSIONED));
48
+ }));
49
+
50
+ //#endregion
51
+ export { carries, hooksError, ioOrNone, manifestVersion, messageOf, sideLabel };
@@ -0,0 +1,15 @@
1
+ //#region src/internal/configDependencySpecGrammar.ts
2
+ /** Split a spec on its first `+`: `0.9.0+sha512-…` → `{ version: "0.9.0", integrity: "sha512-…" }`. */
3
+ const splitConfigDependencySpec = (spec) => {
4
+ const plus = spec.indexOf("+");
5
+ return plus === -1 ? {
6
+ version: spec,
7
+ integrity: void 0
8
+ } : {
9
+ version: spec.slice(0, plus),
10
+ integrity: spec.slice(plus + 1)
11
+ };
12
+ };
13
+
14
+ //#endregion
15
+ export { splitConfigDependencySpec };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/workspaces",
3
- "version": "0.27.0",
3
+ "version": "0.28.0-next-20260927062352",
4
4
  "private": false,
5
5
  "description": "Monorepo workspace tooling as Effect services — root discovery, package enumeration, the dependency graph, package-manager detection, pnpm catalog resolution, lockfile IO and git-based change detection.",
6
6
  "keywords": [
@@ -55,9 +55,9 @@
55
55
  "@effected/git": "^0.17.0",
56
56
  "@effected/glob": "^0.8.0",
57
57
  "@effected/jsonc": "^0.13.0",
58
- "@effected/lockfiles": "^0.11.0",
59
- "@effected/npm": "^0.16.0",
60
- "@effected/package-json": "^0.17.0",
58
+ "@effected/lockfiles": "^0.12.0-next-20260927062352",
59
+ "@effected/npm": "^0.17.0-next-20260927062352",
60
+ "@effected/package-json": "^0.18.0-next-20260927062352",
61
61
  "@effected/semver": "^0.9.0",
62
62
  "@effected/walker": "^0.12.0",
63
63
  "@effected/yaml": "^0.17.0",