@effected/lockfiles 0.10.0 → 0.12.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.
@@ -0,0 +1,36 @@
1
+ import { Schema } from "effect";
2
+
3
+ //#region src/ConfigDependencyLock.ts
4
+ /**
5
+ * One config dependency a `pnpm-lock.yaml` records, with the integrity pnpm
6
+ * recorded for it — read from the lockfile's env preamble by
7
+ * `PnpmEnvLockfile.configDependencies`.
8
+ *
9
+ * @remarks
10
+ * pnpm 11 and 12 record each `configDependencies` entry of
11
+ * `pnpm-workspace.yaml` as the root importer's `configDependencies` in the env
12
+ * preamble document, and its integrity in the preamble's `packages:` section.
13
+ * A workspace that writes its `configDependencies` as bare versions keeps the
14
+ * integrity only here, which makes the lockfile the checksum store for its
15
+ * config dependencies, as it is for the package manager
16
+ * (`PackageManagerLock`).
17
+ *
18
+ * - `name` — the config dependency's package name.
19
+ * - `specifier` — the specifier pnpm recorded, verbatim. pnpm records the bare
20
+ * version even when `pnpm-workspace.yaml` declares the legacy inline
21
+ * `<version>+<integrity>` form.
22
+ * - `version` — the exact version pnpm resolved the specifier to.
23
+ * - `integrity` — the SRI integrity (`sha512-<base64>`) of
24
+ * `<name>@<version>`.
25
+ *
26
+ * @public
27
+ */
28
+ var ConfigDependencyLock = class extends Schema.Class("ConfigDependencyLock")({
29
+ name: Schema.String,
30
+ specifier: Schema.String,
31
+ version: Schema.String,
32
+ integrity: Schema.String
33
+ }) {};
34
+
35
+ //#endregion
36
+ export { ConfigDependencyLock };
package/Lockfile.js CHANGED
@@ -61,9 +61,14 @@ var LockfileParseError = class extends Schema.TaggedError()("LockfileParseError"
61
61
  * such a file as having no lockfile.
62
62
  * - `"noImporters"` — the located document declares no importers, so it
63
63
  * describes no workspace. pnpm always records at least the root importer.
64
- * - `"unexpectedDocuments"` — the stream carries several documents in a
65
- * format that defines no document framing (yarn). Rather than silently
66
- * taking the first, parsing refuses to guess.
64
+ * - `"unexpectedDocuments"` — the stream carries more documents than the
65
+ * format's framing defines: several in a format that defines none
66
+ * (yarn), or more than the env preamble and the lockfile when reading a
67
+ * pnpm env preamble. Rather than silently picking one, parsing refuses to
68
+ * guess. For pnpm the limit is the env reader's alone:
69
+ * `PnpmEnvLockfile` enforces at most two documents, while
70
+ * `Lockfile.parse` reads the last document of a stream of any count and
71
+ * never fails with this reason.
67
72
  *
68
73
  * It carries typed fields rather than a `cause`: unlike
69
74
  * {@link LockfileParseError}, there is no underlying engine failure to wrap —
@@ -81,10 +86,27 @@ var LockfileFramingError = class extends Schema.TaggedError()("LockfileFramingEr
81
86
  documents: Schema.Int
82
87
  }) {
83
88
  get message() {
84
- const detail = this.reason === "noImporters" ? "the lockfile document declares no importers, so it describes no workspace" : this.reason === "unexpectedDocuments" ? `expected a single YAML document but the content carries ${this.documents}` : `the content carries no lockfile document (${this.documents} YAML document(s) found)`;
89
+ const detail = this.reason === "noImporters" ? "the lockfile document declares no importers, so it describes no workspace" : this.reason === "unexpectedDocuments" ? this.format === "pnpm" ? `expected at most two YAML documents (an env preamble and the lockfile) but the content carries ${this.documents}` : `expected a single YAML document but the content carries ${this.documents}` : `the content carries no lockfile document (${this.documents} YAML document(s) found)`;
85
90
  return `Failed to parse ${this.format} lockfile: ${detail}`;
86
91
  }
87
92
  };
93
+ /**
94
+ * Materialize an internal {@link ParseFailure} record into the public error
95
+ * union — the one mapping every public fallible boundary shares, so
96
+ * `Lockfile.parse` and `PnpmEnvLockfile.packageManager` cannot disagree about
97
+ * which failure is which.
98
+ *
99
+ * @internal
100
+ */
101
+ const materializeFailure = (format, failure) => failure.stage === "framing" ? new LockfileFramingError({
102
+ format,
103
+ reason: failure.reason,
104
+ documents: failure.documents
105
+ }) : new LockfileParseError({
106
+ format,
107
+ stage: failure.stage,
108
+ cause: failure.cause
109
+ });
88
110
  const dispatch = (format, content) => {
89
111
  switch (format) {
90
112
  case "bun": return parseBun(content);
@@ -144,15 +166,7 @@ var Lockfile = class Lockfile extends Schema.Class("Lockfile")({
144
166
  * multi-document `pnpm-lock.yaml` needs it).
145
167
  */
146
168
  static parse = Effect.fn("Lockfile.parse")(function* (content, options) {
147
- const fields = yield* dispatch(options.format, content).pipe(Effect.mapError((failure) => failure.stage === "framing" ? new LockfileFramingError({
148
- format: options.format,
149
- reason: failure.reason,
150
- documents: failure.documents
151
- }) : new LockfileParseError({
152
- format: options.format,
153
- stage: failure.stage,
154
- cause: failure.cause
155
- })));
169
+ const fields = yield* dispatch(options.format, content).pipe(Effect.mapError((failure) => materializeFailure(options.format, failure)));
156
170
  return Lockfile.make({
157
171
  format: options.format,
158
172
  lockfileVersion: fields.lockfileVersion,
@@ -288,4 +302,4 @@ var Lockfile = class Lockfile extends Schema.Class("Lockfile")({
288
302
  };
289
303
 
290
304
  //#endregion
291
- export { Lockfile, LockfileFramingError, LockfileParseError };
305
+ export { Lockfile, LockfileFramingError, LockfileParseError, materializeFailure };
@@ -0,0 +1,42 @@
1
+ import { Schema } from "effect";
2
+
3
+ //#region src/PackageManagerLock.ts
4
+ /**
5
+ * The package manager a `pnpm-lock.yaml` pins, with the integrity pnpm
6
+ * recorded for it — read from the lockfile's env preamble by
7
+ * `PnpmEnvLockfile.packageManager`.
8
+ *
9
+ * @remarks
10
+ * pnpm records the package manager a workspace declares in
11
+ * `devEngines.packageManager` as the root importer's
12
+ * `packageManagerDependencies` in the env preamble document, and resolves it
13
+ * like any other dependency: the preamble's `packages:` section carries its
14
+ * integrity, and pnpm refuses to run a manager whose identity does not match.
15
+ * That makes the lockfile, not the `+sha512.<hex>` suffix on the declared
16
+ * version, the checksum store for the manager itself.
17
+ *
18
+ * - `name` — the package-manager package, always `"pnpm"`.
19
+ * - `specifier` — the declared specifier, verbatim. It may still carry the
20
+ * corepack-style `+sha512.<hex>` suffix the manifest declared it with.
21
+ * - `version` — the exact version pnpm resolved the specifier to.
22
+ * - `integrity` — the SRI integrity (`sha512-<base64>`) of the
23
+ * `pnpm@<version>` package.
24
+ * - `nativeIntegrity` — SRI integrity per native package, keyed by package
25
+ * name with no version (`"@pnpm/exe.linux-x64"`): every package the lockfile
26
+ * records as an optional dependency of `pnpm@<version>`. pnpm 12 records one
27
+ * `@pnpm/exe.<target>` entry per platform there; pnpm 11 records none, so
28
+ * the record is empty — pnpm 11 hangs its platform binaries off a separate
29
+ * `@pnpm/exe` wrapper entry instead, which this model does not carry.
30
+ *
31
+ * @public
32
+ */
33
+ var PackageManagerLock = class extends Schema.Class("PackageManagerLock")({
34
+ name: Schema.Literal("pnpm"),
35
+ specifier: Schema.String,
36
+ version: Schema.String,
37
+ integrity: Schema.String,
38
+ nativeIntegrity: Schema.Record(Schema.String, Schema.String)
39
+ }) {};
40
+
41
+ //#endregion
42
+ export { PackageManagerLock };
@@ -0,0 +1,37 @@
1
+ import { materializeFailure } from "./Lockfile.js";
2
+ import { readPnpmConfigDependencies, readPnpmPackageManager } from "./internal/pnpmEnv.js";
3
+ import { Effect, Option } from "effect";
4
+
5
+ //#region src/PnpmEnvLockfile.ts
6
+ /** Both readers surface an internal `ParseFailure` the one way `Lockfile.parse` does. */
7
+ const materialize = Effect.mapError((failure) => materializeFailure("pnpm", failure));
8
+ const packageManager = Effect.fn("PnpmEnvLockfile.packageManager")((content) => readPnpmPackageManager(content).pipe(materialize, Effect.map(Option.fromUndefinedOr)));
9
+ const configDependencies = Effect.fn("PnpmEnvLockfile.configDependencies")((content) => readPnpmConfigDependencies(content).pipe(materialize));
10
+ /**
11
+ * Readers over the env ("preamble") document of a `pnpm-lock.yaml` — see
12
+ * {@link PnpmEnvLockfileReaders}.
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * import { PnpmEnvLockfile } from "@effected/lockfiles";
17
+ * import { Effect, Option } from "effect";
18
+ *
19
+ * const program = Effect.gen(function* () {
20
+ * const lock = yield* PnpmEnvLockfile.packageManager(content);
21
+ * const configDependencies = yield* PnpmEnvLockfile.configDependencies(content);
22
+ * return {
23
+ * pnpm: Option.map(lock, (pm) => pm.integrity),
24
+ * plugin: configDependencies.get("@effected/pnpm-plugin-effect")?.integrity,
25
+ * };
26
+ * });
27
+ * ```
28
+ *
29
+ * @public
30
+ */
31
+ const PnpmEnvLockfile = {
32
+ packageManager,
33
+ configDependencies
34
+ };
35
+
36
+ //#endregion
37
+ export { PnpmEnvLockfile };
package/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Effect, Option, Schema } from "effect";
2
+ import "@effected/npm";
2
3
  //#region src/BunExtension.d.ts
3
4
  declare const BunExtension_base: Schema.Class<BunExtension, Schema.Struct<{
4
5
  readonly _tag: Schema.tag<"bun">;
@@ -21,6 +22,39 @@ declare const BunExtension_base: Schema.Class<BunExtension, Schema.Struct<{
21
22
  */
22
23
  export declare class BunExtension extends BunExtension_base {}
23
24
  //#endregion
25
+ //#region src/ConfigDependencyLock.d.ts
26
+ declare const ConfigDependencyLock_base: Schema.Class<ConfigDependencyLock, Schema.Struct<{
27
+ readonly name: Schema.String;
28
+ readonly specifier: Schema.String;
29
+ readonly version: Schema.String;
30
+ readonly integrity: Schema.String;
31
+ }>, {}>;
32
+ /**
33
+ * One config dependency a `pnpm-lock.yaml` records, with the integrity pnpm
34
+ * recorded for it — read from the lockfile's env preamble by
35
+ * `PnpmEnvLockfile.configDependencies`.
36
+ *
37
+ * @remarks
38
+ * pnpm 11 and 12 record each `configDependencies` entry of
39
+ * `pnpm-workspace.yaml` as the root importer's `configDependencies` in the env
40
+ * preamble document, and its integrity in the preamble's `packages:` section.
41
+ * A workspace that writes its `configDependencies` as bare versions keeps the
42
+ * integrity only here, which makes the lockfile the checksum store for its
43
+ * config dependencies, as it is for the package manager
44
+ * (`PackageManagerLock`).
45
+ *
46
+ * - `name` — the config dependency's package name.
47
+ * - `specifier` — the specifier pnpm recorded, verbatim. pnpm records the bare
48
+ * version even when `pnpm-workspace.yaml` declares the legacy inline
49
+ * `<version>+<integrity>` form.
50
+ * - `version` — the exact version pnpm resolved the specifier to.
51
+ * - `integrity` — the SRI integrity (`sha512-<base64>`) of
52
+ * `<name>@<version>`.
53
+ *
54
+ * @public
55
+ */
56
+ export declare class ConfigDependencyLock extends ConfigDependencyLock_base {}
57
+ //#endregion
24
58
  //#region src/ImporterDependency.d.ts
25
59
  declare const ImporterDependency_base: Schema.Class<ImporterDependency, Schema.Struct<{
26
60
  readonly name: Schema.NonEmptyString;
@@ -60,77 +94,6 @@ declare const ImporterDependency_base: Schema.Class<ImporterDependency, Schema.S
60
94
  */
61
95
  export declare class ImporterDependency extends ImporterDependency_base {}
62
96
  //#endregion
63
- //#region src/LockfileFormat.d.ts
64
- /**
65
- * The lockfile formats this package parses: bun's `bun.lock` (JSONC), npm's
66
- * `package-lock.json` (JSON), pnpm's `pnpm-lock.yaml` and yarn Berry's
67
- * `yarn.lock` (both YAML).
68
- *
69
- * @remarks
70
- * The literal names the *lockfile format*, not the package manager that
71
- * happens to write it — this package models lockfiles. Yarn support is
72
- * Berry only; classic (v1) `yarn.lock` is not YAML and fails
73
- * `Lockfile.parse` with a typed `LockfileParseError`.
74
- *
75
- * The format literal says nothing about which *versions* of that format are
76
- * supported: pnpm `lockfileVersion` 9+ and npm `lockfileVersion` 3+ parse,
77
- * and older ones fail typed. See `Lockfile.parse`.
78
- *
79
- * @public
80
- */
81
- export declare const LockfileFormat: Schema.Literals<readonly ["bun", "npm", "pnpm", "yarn"]>;
82
- /**
83
- * The union of supported lockfile format names.
84
- *
85
- * @public
86
- */
87
- export type LockfileFormat = typeof LockfileFormat.Type;
88
- /**
89
- * The conventional lockfile filename for a format: `"bun.lock"`,
90
- * `"package-lock.json"`, `"pnpm-lock.yaml"` or `"yarn.lock"`.
91
- *
92
- * @remarks
93
- * The primary name only — the first element of {@link filenamesFor}, which is
94
- * what detection that must also see the genuine alternates should use.
95
- *
96
- * @public
97
- */
98
- export declare const filenameFor: (format: LockfileFormat) => string;
99
- /**
100
- * Every filename a format is genuinely written under, primary first.
101
- *
102
- * @remarks
103
- * Real detection needs more than the conventional name: npm honours
104
- * `npm-shrinkwrap.json` (and prefers it over `package-lock.json` when both
105
- * exist — the ordering here is conventional-name-first, not npm's own
106
- * precedence), and bun has shipped two formats, the current text `bun.lock`
107
- * and the older **binary** `bun.lockb`. pnpm and yarn each have exactly one.
108
- *
109
- * Only genuine *lockfile* spellings appear here. Files that merely influence
110
- * what an install resolves to — `pnpm-workspace.yaml`, `.pnpmfile.cjs`, yarn's
111
- * PnP artifacts — are a consumer's cache policy, not alternates of the
112
- * lockfile.
113
- *
114
- * Two deliberate asymmetries with the rest of this module: `bun.lockb` is a
115
- * detection alternate but not a parse target (`Lockfile.parse` takes text; the
116
- * binary format fails typed), and {@link fromFilename} keeps answering for the
117
- * primary names only.
118
- *
119
- * @public
120
- */
121
- export declare const filenamesFor: (format: LockfileFormat) => readonly [string, ...ReadonlyArray<string>];
122
- /**
123
- * The format a lockfile filename identifies, if any.
124
- *
125
- * @remarks
126
- * Matches exact conventional filenames only (`"bun.lock"`,
127
- * `"package-lock.json"`, `"pnpm-lock.yaml"`, `"yarn.lock"`) — paths, other
128
- * spellings and the {@link filenamesFor} alternates return `Option.none()`.
129
- *
130
- * @public
131
- */
132
- export declare const fromFilename: (name: string) => Option.Option<LockfileFormat>;
133
- //#endregion
134
97
  //#region src/LockfileImporter.d.ts
135
98
  declare const LockfileImporter_base: Schema.Class<LockfileImporter, Schema.Struct<{
136
99
  readonly path: Schema.NonEmptyString;
@@ -317,6 +280,77 @@ declare const WorkspaceDependency_base: Schema.Class<WorkspaceDependency, Schema
317
280
  */
318
281
  export declare class WorkspaceDependency extends WorkspaceDependency_base {}
319
282
  //#endregion
283
+ //#region src/LockfileFormat.d.ts
284
+ /**
285
+ * The lockfile formats this package parses: bun's `bun.lock` (JSONC), npm's
286
+ * `package-lock.json` (JSON), pnpm's `pnpm-lock.yaml` and yarn Berry's
287
+ * `yarn.lock` (both YAML).
288
+ *
289
+ * @remarks
290
+ * The literal names the *lockfile format*, not the package manager that
291
+ * happens to write it — this package models lockfiles. Yarn support is
292
+ * Berry only; classic (v1) `yarn.lock` is not YAML and fails
293
+ * `Lockfile.parse` with a typed `LockfileParseError`.
294
+ *
295
+ * The format literal says nothing about which *versions* of that format are
296
+ * supported: pnpm `lockfileVersion` 9+ and npm `lockfileVersion` 3+ parse,
297
+ * and older ones fail typed. See `Lockfile.parse`.
298
+ *
299
+ * @public
300
+ */
301
+ export declare const LockfileFormat: Schema.Literals<readonly ["bun", "npm", "pnpm", "yarn"]>;
302
+ /**
303
+ * The union of supported lockfile format names.
304
+ *
305
+ * @public
306
+ */
307
+ export type LockfileFormat = typeof LockfileFormat.Type;
308
+ /**
309
+ * The conventional lockfile filename for a format: `"bun.lock"`,
310
+ * `"package-lock.json"`, `"pnpm-lock.yaml"` or `"yarn.lock"`.
311
+ *
312
+ * @remarks
313
+ * The primary name only — the first element of {@link filenamesFor}, which is
314
+ * what detection that must also see the genuine alternates should use.
315
+ *
316
+ * @public
317
+ */
318
+ export declare const filenameFor: (format: LockfileFormat) => string;
319
+ /**
320
+ * Every filename a format is genuinely written under, primary first.
321
+ *
322
+ * @remarks
323
+ * Real detection needs more than the conventional name: npm honours
324
+ * `npm-shrinkwrap.json` (and prefers it over `package-lock.json` when both
325
+ * exist — the ordering here is conventional-name-first, not npm's own
326
+ * precedence), and bun has shipped two formats, the current text `bun.lock`
327
+ * and the older **binary** `bun.lockb`. pnpm and yarn each have exactly one.
328
+ *
329
+ * Only genuine *lockfile* spellings appear here. Files that merely influence
330
+ * what an install resolves to — `pnpm-workspace.yaml`, `.pnpmfile.cjs`, yarn's
331
+ * PnP artifacts — are a consumer's cache policy, not alternates of the
332
+ * lockfile.
333
+ *
334
+ * Two deliberate asymmetries with the rest of this module: `bun.lockb` is a
335
+ * detection alternate but not a parse target (`Lockfile.parse` takes text; the
336
+ * binary format fails typed), and {@link fromFilename} keeps answering for the
337
+ * primary names only.
338
+ *
339
+ * @public
340
+ */
341
+ export declare const filenamesFor: (format: LockfileFormat) => readonly [string, ...ReadonlyArray<string>];
342
+ /**
343
+ * The format a lockfile filename identifies, if any.
344
+ *
345
+ * @remarks
346
+ * Matches exact conventional filenames only (`"bun.lock"`,
347
+ * `"package-lock.json"`, `"pnpm-lock.yaml"`, `"yarn.lock"`) — paths, other
348
+ * spellings and the {@link filenamesFor} alternates return `Option.none()`.
349
+ *
350
+ * @public
351
+ */
352
+ export declare const fromFilename: (name: string) => Option.Option<LockfileFormat>;
353
+ //#endregion
320
354
  //#region src/Lockfile.d.ts
321
355
  declare const LockfileParseError_base: Schema.Class<LockfileParseError, Schema.TaggedStruct<"LockfileParseError", {
322
356
  readonly format: Schema.Literals<readonly ["bun", "npm", "pnpm", "yarn"]>;
@@ -371,9 +405,14 @@ declare const LockfileFramingError_base: Schema.Class<LockfileFramingError, Sche
371
405
  * such a file as having no lockfile.
372
406
  * - `"noImporters"` — the located document declares no importers, so it
373
407
  * describes no workspace. pnpm always records at least the root importer.
374
- * - `"unexpectedDocuments"` — the stream carries several documents in a
375
- * format that defines no document framing (yarn). Rather than silently
376
- * taking the first, parsing refuses to guess.
408
+ * - `"unexpectedDocuments"` — the stream carries more documents than the
409
+ * format's framing defines: several in a format that defines none
410
+ * (yarn), or more than the env preamble and the lockfile when reading a
411
+ * pnpm env preamble. Rather than silently picking one, parsing refuses to
412
+ * guess. For pnpm the limit is the env reader's alone:
413
+ * `PnpmEnvLockfile` enforces at most two documents, while
414
+ * `Lockfile.parse` reads the last document of a stream of any count and
415
+ * never fails with this reason.
377
416
  *
378
417
  * It carries typed fields rather than a `cause`: unlike
379
418
  * {@link LockfileParseError}, there is no underlying engine failure to wrap —
@@ -568,6 +607,131 @@ export declare class LockfileIntegrity extends LockfileIntegrity_base {
568
607
  static compare(lockfile: Lockfile, manifests: ReadonlyArray<WorkspaceManifest>): LockfileIntegrity;
569
608
  }
570
609
  //#endregion
610
+ //#region src/PackageManagerLock.d.ts
611
+ declare const PackageManagerLock_base: Schema.Class<PackageManagerLock, Schema.Struct<{
612
+ readonly name: Schema.Literal<"pnpm">;
613
+ readonly specifier: Schema.String;
614
+ readonly version: Schema.String;
615
+ readonly integrity: Schema.String;
616
+ readonly nativeIntegrity: Schema.$Record<Schema.String, Schema.String>;
617
+ }>, {}>;
618
+ /**
619
+ * The package manager a `pnpm-lock.yaml` pins, with the integrity pnpm
620
+ * recorded for it — read from the lockfile's env preamble by
621
+ * `PnpmEnvLockfile.packageManager`.
622
+ *
623
+ * @remarks
624
+ * pnpm records the package manager a workspace declares in
625
+ * `devEngines.packageManager` as the root importer's
626
+ * `packageManagerDependencies` in the env preamble document, and resolves it
627
+ * like any other dependency: the preamble's `packages:` section carries its
628
+ * integrity, and pnpm refuses to run a manager whose identity does not match.
629
+ * That makes the lockfile, not the `+sha512.<hex>` suffix on the declared
630
+ * version, the checksum store for the manager itself.
631
+ *
632
+ * - `name` — the package-manager package, always `"pnpm"`.
633
+ * - `specifier` — the declared specifier, verbatim. It may still carry the
634
+ * corepack-style `+sha512.<hex>` suffix the manifest declared it with.
635
+ * - `version` — the exact version pnpm resolved the specifier to.
636
+ * - `integrity` — the SRI integrity (`sha512-<base64>`) of the
637
+ * `pnpm@<version>` package.
638
+ * - `nativeIntegrity` — SRI integrity per native package, keyed by package
639
+ * name with no version (`"@pnpm/exe.linux-x64"`): every package the lockfile
640
+ * records as an optional dependency of `pnpm@<version>`. pnpm 12 records one
641
+ * `@pnpm/exe.<target>` entry per platform there; pnpm 11 records none, so
642
+ * the record is empty — pnpm 11 hangs its platform binaries off a separate
643
+ * `@pnpm/exe` wrapper entry instead, which this model does not carry.
644
+ *
645
+ * @public
646
+ */
647
+ export declare class PackageManagerLock extends PackageManagerLock_base {}
648
+ //#endregion
649
+ //#region src/PnpmEnvLockfile.d.ts
650
+ /**
651
+ * Readers over the env ("preamble") document of a `pnpm-lock.yaml`.
652
+ *
653
+ * @remarks
654
+ * A `pnpm-lock.yaml` is a YAML stream. When a workspace declares
655
+ * `devEngines.packageManager` or `configDependencies`, pnpm writes an env
656
+ * preamble document ahead of the lockfile proper: the preamble is always the
657
+ * **first** of two documents, the lockfile always the **last**.
658
+ * `Lockfile.parse` reads the last; these readers read the first.
659
+ *
660
+ * @public
661
+ */
662
+ interface PnpmEnvLockfileReaders {
663
+ /**
664
+ * Read the package manager a `pnpm-lock.yaml` pins, with its recorded
665
+ * integrity. Pure: takes the lockfile text, performs no IO.
666
+ *
667
+ * @param content - The `pnpm-lock.yaml` text.
668
+ * @returns `Option.none()` when the lockfile records no package manager —
669
+ * a single-document lockfile (no preamble; pnpm writes none for a
670
+ * workspace declaring neither `devEngines.packageManager` nor
671
+ * `configDependencies`), or a preamble whose root importer declares no
672
+ * `pnpm` in `packageManagerDependencies`. Otherwise `Option.some` of the
673
+ * {@link PackageManagerLock}.
674
+ *
675
+ * Fails with {@link LockfileParseError} when the text is not well-formed
676
+ * YAML (`stage: "syntax"`), or when the preamble is malformed or records a
677
+ * claim it cannot back (`stage: "validation"`): `pnpm@<version>` with no
678
+ * `packages` entry, snapshot or SRI integrity, or a native optional
679
+ * dependency with no SRI integrity. A lockfile that names a version it
680
+ * cannot account for is a failure, never `none`. Fails with
681
+ * {@link LockfileFramingError} (`reason: "unexpectedDocuments"`) when the
682
+ * stream carries more than two documents, since no position then
683
+ * identifies the preamble.
684
+ */
685
+ readonly packageManager: (content: string) => Effect.Effect<Option.Option<PackageManagerLock>, LockfileParseError | LockfileFramingError>;
686
+ /**
687
+ * Read the config dependencies a `pnpm-lock.yaml` records, each with its
688
+ * recorded integrity. Pure: takes the lockfile text, performs no IO.
689
+ *
690
+ * @remarks
691
+ * The integrity source for a `configDependencies` entry written as a bare
692
+ * version: pnpm 11 and 12 keep it here rather than inline in
693
+ * `pnpm-workspace.yaml`. Read by `@effected/workspaces` to verify a config
694
+ * dependency it fetches at a version this checkout never installed.
695
+ *
696
+ * @param content - The `pnpm-lock.yaml` text.
697
+ * @returns A map keyed by config-dependency name. Empty when the lockfile
698
+ * records none — a single-document lockfile (no preamble), or a preamble
699
+ * whose root importer declares no `configDependencies`.
700
+ *
701
+ * Fails exactly as {@link PnpmEnvLockfileReaders.packageManager} does, with
702
+ * {@link LockfileParseError} for broken YAML (`stage: "syntax"`) or a
703
+ * malformed preamble (`stage: "validation"`), and with
704
+ * {@link LockfileFramingError} (`reason: "unexpectedDocuments"`) for more
705
+ * than two documents. A recorded entry the lockfile cannot back fails at
706
+ * `stage: "validation"`: an empty version, or a `<name>@<version>` with no
707
+ * `packages` entry, no `resolution.integrity`, or an integrity that is not
708
+ * SRI. An entry is never silently dropped.
709
+ */
710
+ readonly configDependencies: (content: string) => Effect.Effect<ReadonlyMap<string, ConfigDependencyLock>, LockfileParseError | LockfileFramingError>;
711
+ }
712
+ /**
713
+ * Readers over the env ("preamble") document of a `pnpm-lock.yaml` — see
714
+ * {@link PnpmEnvLockfileReaders}.
715
+ *
716
+ * @example
717
+ * ```typescript
718
+ * import { PnpmEnvLockfile } from "@effected/lockfiles";
719
+ * import { Effect, Option } from "effect";
720
+ *
721
+ * const program = Effect.gen(function* () {
722
+ * const lock = yield* PnpmEnvLockfile.packageManager(content);
723
+ * const configDependencies = yield* PnpmEnvLockfile.configDependencies(content);
724
+ * return {
725
+ * pnpm: Option.map(lock, (pm) => pm.integrity),
726
+ * plugin: configDependencies.get("@effected/pnpm-plugin-effect")?.integrity,
727
+ * };
728
+ * });
729
+ * ```
730
+ *
731
+ * @public
732
+ */
733
+ export declare const PnpmEnvLockfile: PnpmEnvLockfileReaders;
734
+ //#endregion
571
735
  //#region src/UnsupportedLockfileVersion.d.ts
572
736
  /**
573
737
  * The cause a {@link LockfileParseError} carries when a lockfile predates the
@@ -633,5 +797,5 @@ interface UnsupportedLockfileVersion {
633
797
  */
634
798
  export declare const isUnsupportedLockfileVersion: (cause: unknown) => cause is UnsupportedLockfileVersion;
635
799
  //#endregion
636
- export type { PnpmCatalogs, UnsupportedLockfileVersion };
800
+ export type { PnpmCatalogs, PnpmEnvLockfileReaders, UnsupportedLockfileVersion };
637
801
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { BunExtension } from "./BunExtension.js";
2
+ import { ConfigDependencyLock } from "./ConfigDependencyLock.js";
2
3
  import { ImporterDependency } from "./ImporterDependency.js";
3
4
  import { LockfileImporter } from "./LockfileImporter.js";
4
5
  import { ResolvedPackage } from "./ResolvedPackage.js";
@@ -7,6 +8,8 @@ import { PnpmExtension } from "./PnpmExtension.js";
7
8
  import { LockfileFormat, filenameFor, filenamesFor, fromFilename } from "./LockfileFormat.js";
8
9
  import { Lockfile, LockfileFramingError, LockfileParseError } from "./Lockfile.js";
9
10
  import { LockfileIntegrity, WorkspaceManifest } from "./LockfileIntegrity.js";
11
+ import { PackageManagerLock } from "./PackageManagerLock.js";
12
+ import { PnpmEnvLockfile } from "./PnpmEnvLockfile.js";
10
13
  import { isUnsupportedLockfileVersion } from "./UnsupportedLockfileVersion.js";
11
14
 
12
- export { BunExtension, ImporterDependency, Lockfile, LockfileFormat, LockfileFramingError, LockfileImporter, LockfileIntegrity, LockfileParseError, PnpmExtension, ResolvedPackage, WorkspaceDependency, WorkspaceManifest, filenameFor, filenamesFor, fromFilename, isUnsupportedLockfileVersion };
15
+ export { BunExtension, ConfigDependencyLock, ImporterDependency, Lockfile, LockfileFormat, LockfileFramingError, LockfileImporter, LockfileIntegrity, LockfileParseError, PackageManagerLock, PnpmEnvLockfile, PnpmExtension, ResolvedPackage, WorkspaceDependency, WorkspaceManifest, filenameFor, filenamesFor, fromFilename, isUnsupportedLockfileVersion };
@@ -36,6 +36,10 @@ const isEmptyDocument = (document) => document === null || document === void 0;
36
36
  * document. pnpm reads that as "no lockfile"; so do we, through the typed
37
37
  * framing failure — never by silently falling back to the preamble.
38
38
  *
39
+ * It takes the last document of a stream of **any** count and never fails
40
+ * with `unexpectedDocuments`; only {@link selectPnpmEnvDocument}, the env
41
+ * reader's selector, enforces the at-most-two limit.
42
+ *
39
43
  * @internal
40
44
  */
41
45
  const selectPnpmDocument = (content) => Effect.gen(function* () {
@@ -71,6 +75,46 @@ const selectSoleDocument = (content) => Effect.gen(function* () {
71
75
  documents: documents.length
72
76
  };
73
77
  });
78
+ /**
79
+ * The number of documents pnpm's env-lockfile writer can compose: the env
80
+ * preamble and the main lockfile. See {@link selectPnpmEnvDocument}.
81
+ *
82
+ * @internal
83
+ */
84
+ const MAX_PNPM_DOCUMENTS = 2;
85
+ /**
86
+ * Select the env ("preamble") document from a `pnpm-lock.yaml` YAML stream,
87
+ * or `undefined` when the stream carries none.
88
+ *
89
+ * @remarks
90
+ * The same position rule as {@link selectPnpmDocument}, read from the other
91
+ * end: pnpm's `writeEnvLockfile` composes `${env}---${main}`, so the preamble
92
+ * is the **first** of exactly two documents. A single-document stream has no
93
+ * preamble — that is how pnpm writes a lockfile for a workspace that declares
94
+ * neither `configDependencies` nor `devEngines.packageManager` — and neither
95
+ * does an empty one. An empty first document is no preamble either: there is
96
+ * nothing in it to read.
97
+ *
98
+ * A stream carrying **more** than two documents is outside the writer
99
+ * contract, so no position identifies the preamble in it. Rather than guess —
100
+ * this selection feeds integrity verification, where a guess is a trust
101
+ * decision — it fails through the typed framing channel. That limit is this
102
+ * selector's alone: {@link selectPnpmDocument}, behind `Lockfile.parse`, takes
103
+ * the last document of a stream of any count.
104
+ *
105
+ * @internal
106
+ */
107
+ const selectPnpmEnvDocument = (content) => Effect.gen(function* () {
108
+ const documents = yield* Yaml.parseAll(content).pipe(Effect.mapError(syntaxFailure));
109
+ if (documents.length > MAX_PNPM_DOCUMENTS) return yield* Effect.fail(framingFailure("unexpectedDocuments", documents.length));
110
+ if (documents.length < MAX_PNPM_DOCUMENTS) return void 0;
111
+ const document = documents.at(0);
112
+ if (isEmptyDocument(document)) return void 0;
113
+ return {
114
+ document,
115
+ documents: documents.length
116
+ };
117
+ });
74
118
 
75
119
  //#endregion
76
- export { selectPnpmDocument, selectSoleDocument };
120
+ export { selectPnpmDocument, selectPnpmEnvDocument, selectSoleDocument };
package/internal/pnpm.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { LockfileImporter } from "../LockfileImporter.js";
2
2
  import { ResolvedPackage } from "../ResolvedPackage.js";
3
- import { extractWorkspaceDeps, framingFailure, importerDependencies, peerDeclarations, requireLockfileVersion, splitPeerSuffix, toIntegrityHash, validationFailure } from "./shared.js";
3
+ import { extractWorkspaceDeps, framingFailure, gatePnpmVersion, importerDependencies, peerDeclarations, splitPeerSuffix, toIntegrityHash, validationFailure } from "./shared.js";
4
4
  import { PnpmExtension } from "../PnpmExtension.js";
5
5
  import { selectPnpmDocument } from "./documents.js";
6
6
  import { Effect, Schema } from "effect";
@@ -17,18 +17,6 @@ const PnpmImporter = Schema.Struct({
17
17
  optionalDependencies: PnpmImporterDeps,
18
18
  publishDirectory: Schema.optionalKey(Schema.String)
19
19
  });
20
- /**
21
- * The version gate's own input: `lockfileVersion` and nothing else.
22
- *
23
- * The gate has to read the version *before* the shape decode, because the
24
- * shape it decodes against is the shape of a supported version. `importers` is
25
- * a required key here and a pre-v9 single-project lockfile has none — it
26
- * records its dependencies at the top level — so a shape-first order reports a
27
- * lockfile we reject for being too old as merely malformed instead.
28
- *
29
- * @internal
30
- */
31
- const PnpmVersionProbe = Schema.Struct({ lockfileVersion: Schema.Union([Schema.String, Schema.Number]) });
32
20
  const PnpmLockfileRaw = Schema.Struct({
33
21
  lockfileVersion: Schema.Union([Schema.String, Schema.Number]),
34
22
  settings: Schema.optionalKey(Schema.Struct({
@@ -71,8 +59,7 @@ const PnpmLockfileRaw = Schema.Struct({
71
59
  */
72
60
  const parsePnpm = (content) => Effect.gen(function* () {
73
61
  const { document, documents } = yield* selectPnpmDocument(content);
74
- const probe = yield* Schema.decodeUnknownEffect(PnpmVersionProbe)(document).pipe(Effect.mapError(validationFailure));
75
- yield* requireLockfileVersion("pnpm", probe.lockfileVersion);
62
+ yield* gatePnpmVersion(document);
76
63
  const validated = yield* Schema.decodeUnknownEffect(PnpmLockfileRaw)(document).pipe(Effect.mapError(validationFailure));
77
64
  if (Object.keys(validated.importers).length === 0) return yield* Effect.fail(framingFailure("noImporters", documents));
78
65
  return yield* toFields(validated);
@@ -0,0 +1,135 @@
1
+ import { ConfigDependencyLock } from "../ConfigDependencyLock.js";
2
+ import { gatePnpmVersion, validationFailure } from "./shared.js";
3
+ import { selectPnpmEnvDocument } from "./documents.js";
4
+ import { PackageManagerLock } from "../PackageManagerLock.js";
5
+ import { Effect, Schema } from "effect";
6
+ import { IntegrityHash } from "@effected/npm";
7
+
8
+ //#region src/internal/pnpmEnv.ts
9
+ const PnpmEnvImporterDeps = Schema.optionalKey(Schema.Record(Schema.String, Schema.Struct({
10
+ specifier: Schema.String,
11
+ version: Schema.String
12
+ })));
13
+ const PnpmEnvRaw = Schema.Struct({
14
+ importers: Schema.optionalKey(Schema.Record(Schema.String, Schema.Struct({
15
+ packageManagerDependencies: PnpmEnvImporterDeps,
16
+ configDependencies: PnpmEnvImporterDeps
17
+ }))),
18
+ packages: Schema.optionalKey(Schema.Record(Schema.String, Schema.Struct({ resolution: Schema.optionalKey(Schema.Struct({ integrity: Schema.optionalKey(Schema.String) })) }))),
19
+ snapshots: Schema.optionalKey(Schema.Record(Schema.String, Schema.Struct({ optionalDependencies: Schema.optionalKey(Schema.Record(Schema.String, Schema.String)) })))
20
+ });
21
+ /** The package-manager package this reader resolves. */
22
+ const PNPM = "pnpm";
23
+ /** The importer pnpm records `packageManagerDependencies` under. */
24
+ const ROOT_IMPORTER = ".";
25
+ /**
26
+ * Own-property read of a decoded record. The records come from YAML, so a key
27
+ * such as `constructor` or `__proto__` must never be answered by
28
+ * `Object.prototype`.
29
+ */
30
+ const own = (record, key) => record !== void 0 && Object.hasOwn(record, key) ? record[key] : void 0;
31
+ /**
32
+ * A located, well-formed preamble that does not hold what it promises: the
33
+ * importer names a package the `packages:` section cannot account for. A
34
+ * shape failure of a located document, so it rides the validation channel.
35
+ */
36
+ const unaccounted = (message) => validationFailure(/* @__PURE__ */ new Error(`pnpm-lock.yaml env preamble: ${message}`));
37
+ /**
38
+ * The SRI integrity of `packages["<key>"]`, failing when the entry, its
39
+ * integrity, or the SRI form of that integrity is missing. Never `undefined`:
40
+ * the caller only asks for keys the lockfile's own graph names.
41
+ */
42
+ const integrityOf = (raw, key) => {
43
+ const integrity = own(raw.packages, key)?.resolution?.integrity;
44
+ if (integrity === void 0) return Effect.fail(unaccounted(`packages[${JSON.stringify(key)}] records no resolution.integrity`));
45
+ if (!IntegrityHash.isSri(integrity)) return Effect.fail(unaccounted(`packages[${JSON.stringify(key)}].resolution.integrity is not an SRI integrity string`));
46
+ return Effect.succeed(integrity);
47
+ };
48
+ /**
49
+ * The SRI integrity of the `<name>@<version>` a preamble entry records,
50
+ * failing first when the version is empty: an empty version names no
51
+ * `packages` entry, and saying so beats reporting the malformed key.
52
+ * `subject` is the entry as the empty-version message names it.
53
+ */
54
+ const recordedIntegrity = (raw, subject, name, version) => version === "" ? Effect.fail(unaccounted(`${subject} is recorded with an empty version`)) : integrityOf(raw, `${name}@${version}`);
55
+ /**
56
+ * Locate and decode the env preamble of a `pnpm-lock.yaml`: `undefined` when
57
+ * the stream carries none, typed when it is malformed or below the supported
58
+ * lockfile version. The one decode every preamble reader shares, so they
59
+ * cannot disagree about which document is the preamble or what shape it has.
60
+ */
61
+ const decodePreamble = (content) => Effect.gen(function* () {
62
+ const selected = yield* selectPnpmEnvDocument(content);
63
+ if (selected === void 0) return void 0;
64
+ yield* gatePnpmVersion(selected.document);
65
+ return yield* Schema.decodeUnknownEffect(PnpmEnvRaw)(selected.document).pipe(Effect.mapError(validationFailure));
66
+ });
67
+ /**
68
+ * Read the package manager pinned by a `pnpm-lock.yaml`'s env preamble.
69
+ *
70
+ * @remarks
71
+ * `undefined` means the lockfile records no package manager: no preamble, or
72
+ * a preamble whose root importer declares no `pnpm` package-manager
73
+ * dependency. Everything past that point is a claim the lockfile made, so a
74
+ * claim it cannot back — a missing `pnpm@<version>` entry, a missing snapshot,
75
+ * a missing or non-SRI integrity for pnpm or any native it lists — fails typed
76
+ * rather than degrading to "nothing recorded".
77
+ *
78
+ * Native packages come from the lockfile's own graph — the optional
79
+ * dependencies of the `pnpm@<version>` snapshot — not from a name pattern.
80
+ *
81
+ * @internal
82
+ */
83
+ const readPnpmPackageManager = (content) => Effect.gen(function* () {
84
+ const raw = yield* decodePreamble(content);
85
+ if (raw === void 0) return void 0;
86
+ const declared = own(own(raw.importers, ROOT_IMPORTER)?.packageManagerDependencies, PNPM);
87
+ if (declared === void 0) return void 0;
88
+ const integrity = yield* recordedIntegrity(raw, PNPM, PNPM, declared.version);
89
+ const key = `${PNPM}@${declared.version}`;
90
+ const snapshot = own(raw.snapshots, key);
91
+ if (snapshot === void 0) return yield* Effect.fail(unaccounted(`snapshots[${JSON.stringify(key)}] is missing`));
92
+ const natives = [];
93
+ for (const [name, version] of Object.entries(snapshot.optionalDependencies ?? {})) natives.push([name, yield* integrityOf(raw, `${name}@${version}`)]);
94
+ return PackageManagerLock.make({
95
+ name: PNPM,
96
+ specifier: declared.specifier,
97
+ version: declared.version,
98
+ integrity,
99
+ nativeIntegrity: Object.fromEntries(natives)
100
+ });
101
+ });
102
+ /**
103
+ * Read the config dependencies recorded by a `pnpm-lock.yaml`'s env preamble,
104
+ * keyed by name, each with the integrity pnpm recorded for it.
105
+ *
106
+ * @remarks
107
+ * An empty map means the lockfile records none: no preamble, or a preamble
108
+ * whose root importer declares no `configDependencies`. As with
109
+ * {@link readPnpmPackageManager}, every entry the importer names is a claim,
110
+ * so an entry whose `<name>@<version>` has no `packages` entry, no integrity,
111
+ * a non-SRI integrity, or an empty version fails typed rather than being
112
+ * dropped. The map is built from own keys only, so a hostile name such as
113
+ * `__proto__` is an ordinary entry.
114
+ *
115
+ * @internal
116
+ */
117
+ const readPnpmConfigDependencies = (content) => Effect.gen(function* () {
118
+ const raw = yield* decodePreamble(content);
119
+ const declared = own(raw?.importers, ROOT_IMPORTER)?.configDependencies;
120
+ const locks = /* @__PURE__ */ new Map();
121
+ if (raw === void 0 || declared === void 0) return locks;
122
+ for (const [name, entry] of Object.entries(declared)) {
123
+ const integrity = yield* recordedIntegrity(raw, `config dependency ${JSON.stringify(name)}`, name, entry.version);
124
+ locks.set(name, ConfigDependencyLock.make({
125
+ name,
126
+ specifier: entry.specifier,
127
+ version: entry.version,
128
+ integrity
129
+ }));
130
+ }
131
+ return locks;
132
+ });
133
+
134
+ //#endregion
135
+ export { readPnpmConfigDependencies, readPnpmPackageManager };
@@ -187,6 +187,26 @@ const requireLockfileVersion = (format, raw) => {
187
187
  };
188
188
  return Effect.fail(validationFailure(cause));
189
189
  };
190
+ /**
191
+ * The pnpm version gate's own input: `lockfileVersion` and nothing else.
192
+ *
193
+ * The gate has to read the version *before* the shape decode, because the
194
+ * shape it decodes against is the shape of a supported version. `importers` is
195
+ * a required key in the lockfile shape and a pre-v9 single-project lockfile has
196
+ * none — it records its dependencies at the top level — so a shape-first order
197
+ * reports a lockfile we reject for being too old as merely malformed instead.
198
+ *
199
+ * @internal
200
+ */
201
+ const PnpmVersionProbe = Schema.Struct({ lockfileVersion: Schema.Union([Schema.String, Schema.Number]) });
202
+ /**
203
+ * Gate a located pnpm document (lockfile or env preamble) on its format
204
+ * version: decode {@link PnpmVersionProbe}, then {@link requireLockfileVersion}.
205
+ * Call it BEFORE the shape decode, for the reason the probe documents.
206
+ *
207
+ * @internal
208
+ */
209
+ const gatePnpmVersion = (document) => Schema.decodeUnknownEffect(PnpmVersionProbe)(document).pipe(Effect.mapError(validationFailure), Effect.flatMap((probe) => requireLockfileVersion("pnpm", probe.lockfileVersion)));
190
210
  /** @internal */
191
211
  const syntaxFailure = (cause) => ({
192
212
  stage: "syntax",
@@ -235,4 +255,4 @@ const extractWorkspaceDeps = (workspaces, workspaceNames) => {
235
255
  };
236
256
 
237
257
  //#endregion
238
- export { DEP_TYPES, MINIMUM_LOCKFILE_VERSION, extractWorkspaceDeps, framingFailure, importerDependencies, isWorkspaceSpecifier, peerDeclarations, requireLockfileVersion, splitPeerSuffix, syntaxFailure, toIntegrityHash, validationFailure };
258
+ export { DEP_TYPES, MINIMUM_LOCKFILE_VERSION, extractWorkspaceDeps, framingFailure, gatePnpmVersion, importerDependencies, isWorkspaceSpecifier, peerDeclarations, requireLockfileVersion, splitPeerSuffix, syntaxFailure, toIntegrityHash, validationFailure };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/lockfiles",
3
- "version": "0.10.0",
3
+ "version": "0.12.0-next-20260927062352",
4
4
  "private": false,
5
5
  "description": "Pure lockfile parsing for bun, npm, pnpm and yarn Berry into one unified Effect schema model, with pure integrity checking against workspace manifests.",
6
6
  "keywords": [
@@ -40,11 +40,11 @@
40
40
  "./package.json": "./package.json"
41
41
  },
42
42
  "peerDependencies": {
43
- "@effected/jsonc": "^0.12.0",
44
- "@effected/npm": "^0.15.0",
45
- "@effected/semver": "^0.8.0",
46
- "@effected/yaml": "^0.16.0",
47
- "effect": "4.0.0-rc.116"
43
+ "@effected/jsonc": "^0.13.0",
44
+ "@effected/npm": "^0.17.0-next-20260927062352",
45
+ "@effected/semver": "^0.9.0",
46
+ "@effected/yaml": "^0.17.0",
47
+ "effect": "4.0.0-rc.117"
48
48
  },
49
49
  "engines": {
50
50
  "node": ">=24.11.0"