@effected/workspaces 0.26.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.
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 };