@effected/lockfiles 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # @effected/lockfiles
2
+
3
+ [![npm](https://img.shields.io/npm/v/@effected%2Flockfiles?label=npm&color=cb3837)](https://www.npmjs.com/package/@effected/lockfiles)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-4caf50.svg)](https://opensource.org/licenses/MIT)
5
+ [![Node.js %3E%3D24.11.0](https://img.shields.io/badge/Node.js-%3E%3D24.11.0-5fa04e.svg)](https://nodejs.org/)
6
+ [![TypeScript 6.0](https://img.shields.io/badge/TypeScript-6.0-3178c6.svg)](https://www.typescriptlang.org/)
7
+
8
+ Lockfile parsing for [Effect](https://effect.website) v4: bun (`bun.lock`), npm (`package-lock.json` v2/v3), pnpm (`pnpm-lock.yaml`) and yarn Berry (`yarn.lock`) all normalized into one `Lockfile` schema model, plus pure integrity checking of that model against workspace manifests. Four formats, one model, no IO and no external runtime dependencies.
9
+
10
+ > **Pre-release.** This package is part of the `@effected/*` kit, in pre-`1.0.0`
11
+ > development against a single pinned Effect v4 beta. Packages graduate to
12
+ > `1.0.0` once Effect `4.0.0` ships. To hold your own `effect` versions at
13
+ > exactly the ones the kit is built and tested against, install
14
+ > [`@effected/pnpm-plugin-effect`](https://www.npmjs.com/package/@effected/pnpm-plugin-effect).
15
+ >
16
+ > **Stability: unstable.** This package's API surface is not yet considered
17
+ > complete and may change across `0.x` releases. Pin an exact version — even a
18
+ > package marked *stable* before `1.0.0` can introduce a breaking change by
19
+ > accident, and an exact pin turns that into a type-check error rather than a
20
+ > runtime surprise. Full policy: [release strategy](https://github.com/spencerbeggs/effected#release-strategy).
21
+
22
+ ## Why @effected/lockfiles
23
+
24
+ Every package manager writes its lockfile in a different dialect — JSONC for bun, JSON for npm, YAML for pnpm and yarn — and each encodes packages, workspace edges and integrity data differently. Tooling that wants to answer "which version of `typescript` is resolved here" ends up with four code paths and four sets of bugs. This package normalizes all four into one model, so the question is asked once regardless of which package manager produced the file.
25
+
26
+ Every entrypoint takes content as a **string**. The package performs no IO at all: reading files, finding workspace roots and detecting which package manager a repo uses belong to its consumers, and keeping them out means the parser is a pure function you can drive from a fixture, a network response or a git blob. Malformed input always exits through a typed error channel, never as a defect, and the two ways a lockfile can be unusable are distinct tags rather than one blurry `reason` string. Yarn support is Berry only, and that is enforced: classic v1 content fails typed instead of being mis-normalized into something that looks plausible.
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ npm install @effected/lockfiles @effected/jsonc @effected/semver @effected/yaml effect
32
+ ```
33
+
34
+ ```bash
35
+ pnpm add @effected/lockfiles @effected/jsonc @effected/semver @effected/yaml effect
36
+ ```
37
+
38
+ Requires Node.js >=24.11.0. `effect` v4, `@effected/jsonc`, `@effected/semver` and `@effected/yaml` are peer dependencies — the JSONC and YAML engines and the SemVer range checker all arrive through those siblings, so nothing outside `effect` and `@effected/*` reaches your tree. Package managers that install peers automatically will pull them in; add them to your manifest explicitly if yours does not.
39
+
40
+ ## Quick start
41
+
42
+ ```ts
43
+ import { Lockfile, LockfileIntegrity, WorkspaceManifest } from "@effected/lockfiles";
44
+ import { Effect } from "effect";
45
+
46
+ declare const content: string; // lockfile text, read by the caller
47
+
48
+ const program = Effect.gen(function* () {
49
+ // The only fallible boundary in the package.
50
+ const lockfile = yield* Lockfile.parse(content, { format: "pnpm" });
51
+
52
+ // pnpm workspace packages come back keyed by importer path; rewrite them
53
+ // once you have read the manifests. Total and pure — no error channel.
54
+ const named = lockfile.withImporterNames(new Map([["packages/core", "@acme/core"]]));
55
+
56
+ // Total lookups over the model.
57
+ const versions = named.packagesNamed("typescript").map((p) => p.version);
58
+
59
+ // Pure integrity checking — no Effect, no error channel, no IO.
60
+ const report = LockfileIntegrity.compare(named, [
61
+ WorkspaceManifest.make({ name: "@acme/core", dependencies: { lodash: "^4.17.0" } }),
62
+ ]);
63
+
64
+ return { versions, workspaces: named.workspacePackages.length, valid: report.valid };
65
+ });
66
+
67
+ Effect.runPromise(program).then(console.log);
68
+ // { versions: [...resolved typescript versions], workspaces: <count>, valid: true | false }
69
+ ```
70
+
71
+ ## Formats
72
+
73
+ `format` is a literal, and `filenameFor` / `fromFilename` map between the literal and the file on disk so a consumer that detected a package manager never has to hard-code a filename.
74
+
75
+ | Format | Filename | Dialect | Notes |
76
+ | ------ | -------- | ------- | ----- |
77
+ | `"bun"` | `bun.lock` | JSONC | Trusted dependencies preserved on `BunExtension` |
78
+ | `"npm"` | `package-lock.json` | JSON | v2 and v3 |
79
+ | `"pnpm"` | `pnpm-lock.yaml` | YAML | Catalogs preserved on `PnpmExtension`; see below |
80
+ | `"yarn"` | `yarn.lock` | YAML | Berry only — classic v1 fails typed |
81
+
82
+ ## Errors
83
+
84
+ `Lockfile.parse` is the only fallible entrypoint, and it fails two ways.
85
+
86
+ | Tag | Means | Fields |
87
+ | --- | ----- | ------ |
88
+ | `LockfileParseError` | The content is not a valid lockfile of that format. | `format`, `stage` (`"syntax"` when the text itself did not parse, `"validation"` when it parsed but did not have the expected shape), `cause` (structural, never stringified) |
89
+ | `LockfileFramingError` | The text parsed, but no single lockfile document could be located in it. | `format`, `documents`, `reason` (`"noLockfileDocument"`, `"noImporters"`, `"unexpectedDocuments"`) |
90
+
91
+ The framing error exists because `pnpm-lock.yaml` is a YAML **stream**, not a YAML document. pnpm 11 writes a config-dependencies preamble ahead of the lockfile whenever a workspace uses `configDependencies`, so the file carries two documents — and the preamble declares `lockfileVersion`, `importers` and `packages` too, so a parser that reads only the first document gets a lockfile that *validates* and describes an empty workspace. That is a wrong answer shaped exactly like a right one. The rule here is positional and deterministic rather than a heuristic: pnpm composes the preamble as a prefix, so the lockfile is the **last** document. A stream carrying no lockfile document fails typed. It never degrades into an empty `Lockfile`.
92
+
93
+ yarn defines no document framing at all, so multi-document `yarn.lock` content fails with `"unexpectedDocuments"` rather than being silently truncated to its first document. Where a format states no rule, this package refuses to guess.
94
+
95
+ ## Features
96
+
97
+ - `Lockfile.parse(content, { format })` — the package's only fallible boundary; fails with `LockfileParseError` or `LockfileFramingError`.
98
+ - `Lockfile#withImporterNames(names)` — the pure second stage for pnpm: rewrites importer-path names and both ends of each dependency edge once the consumer has read the manifests.
99
+ - `Lockfile#packagesNamed(name)` and `Lockfile#workspacePackages` — total lookups over the model.
100
+ - `LockfileFormat` with `filenameFor` / `fromFilename` — the format literal and its mapping to lockfile filenames.
101
+ - `LockfileIntegrity.compare(lockfile, manifests)` — total, pure integrity checking with no error channel; the report carries `valid`, `missingWorkspaces`, `extraWorkspaces` and `unsatisfiedConstraints`. Constraint checking is best-effort by design: `workspace:` / `link:` / `file:` specifiers and rows whose range does not parse as SemVer are skipped.
102
+ - `WorkspaceManifest` — the manifest input shape for integrity checking: a package name plus four optional dependency records. Deliberately a plain value rather than a strict manifest model, so consumers can derive it from anything.
103
+ - `ResolvedPackage` — name, version, optional integrity hash, workspace flag and relative path, dependency map.
104
+ - `WorkspaceDependency` and `DependencyType` — a `from`/`to` edge between workspace packages with its dependency type and constraint.
105
+ - `PnpmExtension` and `BunExtension` — format-specific data such as pnpm catalogs and bun trusted dependencies, preserved on the model's optional `extension` field.
106
+
107
+ ## License
108
+
109
+ [MIT](LICENSE)
@@ -0,0 +1,40 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { IntegrityHash } from "@effected/npm";
3
+
4
+ //#region src/ResolvedPackage.ts
5
+ const EMPTY_DEPENDENCIES = {};
6
+ /**
7
+ * A package resolved from a lockfile.
8
+ *
9
+ * @remarks
10
+ * The common shape every format's entries normalize into:
11
+ *
12
+ * - `name` — the resolved package name. For pnpm *workspace* packages
13
+ * straight out of `Lockfile.parse` this is the importer path until
14
+ * `Lockfile#withImporterNames` rewrites it.
15
+ * - `version` — the resolved version string (pnpm workspace packages carry
16
+ * `"0.0.0"`; the lockfile does not record their real versions).
17
+ * - `integrity` — optional `@effected/npm` `IntegrityHash`, covering npm/pnpm
18
+ * `sha512-...` SRI and yarn Berry's `10c0/...` cache checksums (the yarn
19
+ * textual form). An *absent* integrity is omitted, so a `ResolvedPackage` may
20
+ * carry none; a *present but unparseable* integrity fails the parse typed at
21
+ * validation rather than being silently dropped.
22
+ * - `isWorkspace` — `true` for workspace-local packages.
23
+ * - `relativePath` — the workspace-relative directory for workspace
24
+ * packages, when the lockfile records one.
25
+ * - `dependencies` — the package's own dependency map, defaulting to `{}`
26
+ * both at construction and when decoding serialized data.
27
+ *
28
+ * @public
29
+ */
30
+ var ResolvedPackage = class extends Schema.Class("ResolvedPackage")({
31
+ name: Schema.NonEmptyString,
32
+ version: Schema.String,
33
+ integrity: Schema.optionalKey(IntegrityHash),
34
+ isWorkspace: Schema.Boolean,
35
+ relativePath: Schema.optionalKey(Schema.String),
36
+ dependencies: Schema.Record(Schema.String, Schema.String).pipe(Schema.withDecodingDefaultKey(Effect.succeed(EMPTY_DEPENDENCIES)), Schema.withConstructorDefault(Effect.succeed(EMPTY_DEPENDENCIES)))
37
+ }) {};
38
+
39
+ //#endregion
40
+ export { ResolvedPackage };
@@ -0,0 +1,27 @@
1
+ import { Schema } from "effect";
2
+ import { DependencyField } from "@effected/npm";
3
+
4
+ //#region src/WorkspaceDependency.ts
5
+ /**
6
+ * A directed dependency edge between two workspace packages as recorded in
7
+ * the lockfile.
8
+ *
9
+ * @remarks
10
+ * - `from` — the workspace package declaring the dependency. For pnpm this
11
+ * is the importer path until `Lockfile#withImporterNames` rewrites it.
12
+ * - `to` — the workspace package depended upon.
13
+ * - `depType` — which dependency map holds the edge, spelled with
14
+ * `@effected/npm`'s kit-wide `DependencyField` vocabulary.
15
+ * - `constraint` — the declared specifier (e.g. `"workspace:*"`, `"^1.0.0"`).
16
+ *
17
+ * @public
18
+ */
19
+ var WorkspaceDependency = class extends Schema.Class("WorkspaceDependency")({
20
+ from: Schema.NonEmptyString,
21
+ to: Schema.NonEmptyString,
22
+ depType: DependencyField,
23
+ constraint: Schema.String
24
+ }) {};
25
+
26
+ //#endregion
27
+ export { WorkspaceDependency };
package/index.d.ts ADDED
@@ -0,0 +1,457 @@
1
+ import { Effect, Option, Schema } from "effect";
2
+ //#region src/BunExtension.d.ts
3
+ declare const BunExtension_base: Schema.Class<BunExtension, Schema.Struct<{
4
+ readonly _tag: Schema.tag<"bun">;
5
+ readonly catalog: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Unknown>>;
6
+ readonly catalogs: Schema.optionalKey<Schema.$Record<Schema.String, Schema.$Record<Schema.String, Schema.Unknown>>>;
7
+ readonly overrides: Schema.optionalKey<Schema.$Record<Schema.String, Schema.String>>;
8
+ readonly trustedDependencies: Schema.optionalKey<Schema.$Array<Schema.String>>;
9
+ }>, {}>;
10
+ /**
11
+ * Extension data specific to bun lockfiles, attached to `Lockfile.extension`
12
+ * when the format is `"bun"`.
13
+ *
14
+ * @remarks
15
+ * - `catalog` — the default (unnamed) catalog.
16
+ * - `catalogs` — named catalog definitions.
17
+ * - `overrides` — the version override map.
18
+ * - `trustedDependencies` — packages allowed to run install scripts.
19
+ *
20
+ * @public
21
+ */
22
+ declare class BunExtension extends BunExtension_base {}
23
+ //#endregion
24
+ //#region src/ImporterDependency.d.ts
25
+ declare const ImporterDependency_base: Schema.Class<ImporterDependency, Schema.Struct<{
26
+ readonly name: Schema.NonEmptyString;
27
+ readonly specifier: Schema.Codec<import("@effected/npm").ClassifiedSpecifier, string, never, never>;
28
+ readonly version: Schema.optionalKey<Schema.String>;
29
+ readonly depType: Schema.Literals<readonly ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]>;
30
+ }>, {}>;
31
+ /**
32
+ * One declared dependency of one workspace importer, as the lockfile records it.
33
+ *
34
+ * @remarks
35
+ * - `name` — the dependency's package name.
36
+ * - `specifier` — the declared range, typed through `@effected/npm`'s
37
+ * `DependencySpecifier.FromString` codec: the decoded value is a tag-matchable
38
+ * `ClassifiedSpecifier` (`catalog:` / `workspace:` / range / dist-tag / raw),
39
+ * while encoding round-trips the **exact original string byte-for-byte** — the
40
+ * guarantee a before/after lockfile diff relies on.
41
+ * - `version` — the concrete resolved version, **populated by pnpm only**. pnpm
42
+ * records `{ specifier, version }` per importer dependency; bun and npm record
43
+ * resolved versions on their package entries instead, so for those formats
44
+ * `version` is absent and a consumer joins by `name` against
45
+ * `Lockfile.packages`.
46
+ * - `depType` — which dependency map declared it, spelled with `@effected/npm`'s
47
+ * kit-wide `DependencyField` vocabulary.
48
+ *
49
+ * @public
50
+ */
51
+ declare class ImporterDependency extends ImporterDependency_base {}
52
+ //#endregion
53
+ //#region src/LockfileFormat.d.ts
54
+ /**
55
+ * The lockfile formats this package parses: bun's `bun.lock` (JSONC), npm's
56
+ * `package-lock.json` (v2/v3 JSON), pnpm's `pnpm-lock.yaml` and yarn Berry's
57
+ * `yarn.lock` (both YAML).
58
+ *
59
+ * @remarks
60
+ * The literal names the *lockfile format*, not the package manager that
61
+ * happens to write it — this package models lockfiles. Yarn support is
62
+ * Berry only; classic (v1) `yarn.lock` is not YAML and fails
63
+ * `Lockfile.parse` with a typed `LockfileParseError`.
64
+ *
65
+ * @public
66
+ */
67
+ declare const LockfileFormat: Schema.Literals<readonly ["bun", "npm", "pnpm", "yarn"]>;
68
+ /**
69
+ * The union of supported lockfile format names.
70
+ *
71
+ * @public
72
+ */
73
+ type LockfileFormat = typeof LockfileFormat.Type;
74
+ /**
75
+ * The conventional lockfile filename for a format: `"bun.lock"`,
76
+ * `"package-lock.json"`, `"pnpm-lock.yaml"` or `"yarn.lock"`.
77
+ *
78
+ * @public
79
+ */
80
+ declare const filenameFor: (format: LockfileFormat) => string;
81
+ /**
82
+ * The format a lockfile filename identifies, if any.
83
+ *
84
+ * @remarks
85
+ * Matches exact conventional filenames only (`"bun.lock"`,
86
+ * `"package-lock.json"`, `"pnpm-lock.yaml"`, `"yarn.lock"`) — paths and
87
+ * other spellings return `Option.none()`.
88
+ *
89
+ * @public
90
+ */
91
+ declare const fromFilename: (name: string) => Option.Option<LockfileFormat>;
92
+ //#endregion
93
+ //#region src/LockfileImporter.d.ts
94
+ declare const LockfileImporter_base: Schema.Class<LockfileImporter, Schema.Struct<{
95
+ readonly path: Schema.NonEmptyString;
96
+ readonly dependencies: Schema.$Array<typeof ImporterDependency>;
97
+ }>, {}>;
98
+ /**
99
+ * One workspace importer's declared dependencies, as the lockfile records them.
100
+ *
101
+ * @remarks
102
+ * - `path` — the importer path relative to the workspace root, `"."` for the
103
+ * root package (never empty — a `NonEmptyString`) — the same keys as
104
+ * `WorkspaceDiscovery.importerMap()` in
105
+ * `@effected/workspaces`. This is the stable join key: `Lockfile#importer`
106
+ * looks importers up by it, and `Lockfile#withImporterNames` deliberately
107
+ * leaves importers untouched because the path — not a package name — keys
108
+ * them.
109
+ * - `dependencies` — each declared dependency as an {@link ImporterDependency}.
110
+ *
111
+ * Populated by the pnpm, bun and npm parsers. yarn does not record importers,
112
+ * so a yarn lockfile always yields an empty `importers` array.
113
+ *
114
+ * @public
115
+ */
116
+ declare class LockfileImporter extends LockfileImporter_base {}
117
+ //#endregion
118
+ //#region src/PnpmExtension.d.ts
119
+ /**
120
+ * The pnpm `catalogs:` record shape as it appears in `pnpm-lock.yaml`:
121
+ * catalog name → package name → pinned version string or
122
+ * `{ specifier, version }` pair.
123
+ *
124
+ * @remarks
125
+ * Exported so consumers (e.g. a `CatalogSet.fromLockfileCatalogs`) can type
126
+ * against the lockfile's own shape instead of re-declaring it.
127
+ *
128
+ * @public
129
+ */
130
+ type PnpmCatalogs = Record<string, Record<string, string | {
131
+ readonly specifier: string;
132
+ readonly version: string;
133
+ }>>;
134
+ declare const PnpmExtension_base: Schema.Class<PnpmExtension, Schema.Struct<{
135
+ readonly _tag: Schema.tag<"pnpm">;
136
+ readonly catalogs: Schema.optionalKey<Schema.$Record<Schema.String, Schema.$Record<Schema.String, Schema.Union<readonly [Schema.String, Schema.Struct<{
137
+ readonly specifier: Schema.String;
138
+ readonly version: Schema.String;
139
+ }>]>>>>;
140
+ readonly overrides: Schema.optionalKey<Schema.$Record<Schema.String, Schema.String>>;
141
+ readonly settings: Schema.optionalKey<Schema.Struct<{
142
+ readonly autoInstallPeers: Schema.optionalKey<Schema.Boolean>;
143
+ readonly excludeLinksFromLockfile: Schema.optionalKey<Schema.Boolean>;
144
+ }>>;
145
+ }>, {}>;
146
+ /**
147
+ * Extension data specific to pnpm lockfiles, attached to `Lockfile.extension`
148
+ * when the format is `"pnpm"`.
149
+ *
150
+ * @remarks
151
+ * - `catalogs` — pnpm catalog definitions ({@link PnpmCatalogs}).
152
+ * - `overrides` — the version override map recorded in the lockfile header.
153
+ * - `settings` — pnpm settings recorded in the lockfile header.
154
+ *
155
+ * @public
156
+ */
157
+ declare class PnpmExtension extends PnpmExtension_base {}
158
+ //#endregion
159
+ //#region src/ResolvedPackage.d.ts
160
+ declare const ResolvedPackage_base: Schema.Class<ResolvedPackage, Schema.Struct<{
161
+ readonly name: Schema.NonEmptyString;
162
+ readonly version: Schema.String;
163
+ readonly integrity: Schema.optionalKey<Schema.brand<Schema.String, "IntegrityHash"> & {
164
+ isSri: (value: string) => boolean;
165
+ isCorepack: (value: string) => boolean;
166
+ isYarnChecksum: (value: string) => boolean;
167
+ isValid: (value: string) => boolean;
168
+ algorithmOf: (value: string) => import("effect/Option").Option<import("@effected/npm").IntegrityAlgorithm>;
169
+ decode: (input: string) => Effect.Effect<import("@effected/npm").IntegrityHashBrand, import("@effected/npm").InvalidIntegrityHashError>;
170
+ }>;
171
+ readonly isWorkspace: Schema.Boolean;
172
+ readonly relativePath: Schema.optionalKey<Schema.String>;
173
+ readonly dependencies: Schema.withConstructorDefault<Schema.withDecodingDefaultKey<Schema.$Record<Schema.String, Schema.String>, never>>;
174
+ }>, {}>;
175
+ /**
176
+ * A package resolved from a lockfile.
177
+ *
178
+ * @remarks
179
+ * The common shape every format's entries normalize into:
180
+ *
181
+ * - `name` — the resolved package name. For pnpm *workspace* packages
182
+ * straight out of `Lockfile.parse` this is the importer path until
183
+ * `Lockfile#withImporterNames` rewrites it.
184
+ * - `version` — the resolved version string (pnpm workspace packages carry
185
+ * `"0.0.0"`; the lockfile does not record their real versions).
186
+ * - `integrity` — optional `@effected/npm` `IntegrityHash`, covering npm/pnpm
187
+ * `sha512-...` SRI and yarn Berry's `10c0/...` cache checksums (the yarn
188
+ * textual form). An *absent* integrity is omitted, so a `ResolvedPackage` may
189
+ * carry none; a *present but unparseable* integrity fails the parse typed at
190
+ * validation rather than being silently dropped.
191
+ * - `isWorkspace` — `true` for workspace-local packages.
192
+ * - `relativePath` — the workspace-relative directory for workspace
193
+ * packages, when the lockfile records one.
194
+ * - `dependencies` — the package's own dependency map, defaulting to `{}`
195
+ * both at construction and when decoding serialized data.
196
+ *
197
+ * @public
198
+ */
199
+ declare class ResolvedPackage extends ResolvedPackage_base {}
200
+ //#endregion
201
+ //#region src/WorkspaceDependency.d.ts
202
+ declare const WorkspaceDependency_base: Schema.Class<WorkspaceDependency, Schema.Struct<{
203
+ readonly from: Schema.NonEmptyString;
204
+ readonly to: Schema.NonEmptyString;
205
+ readonly depType: Schema.Literals<readonly ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]>;
206
+ readonly constraint: Schema.String;
207
+ }>, {}>;
208
+ /**
209
+ * A directed dependency edge between two workspace packages as recorded in
210
+ * the lockfile.
211
+ *
212
+ * @remarks
213
+ * - `from` — the workspace package declaring the dependency. For pnpm this
214
+ * is the importer path until `Lockfile#withImporterNames` rewrites it.
215
+ * - `to` — the workspace package depended upon.
216
+ * - `depType` — which dependency map holds the edge, spelled with
217
+ * `@effected/npm`'s kit-wide `DependencyField` vocabulary.
218
+ * - `constraint` — the declared specifier (e.g. `"workspace:*"`, `"^1.0.0"`).
219
+ *
220
+ * @public
221
+ */
222
+ declare class WorkspaceDependency extends WorkspaceDependency_base {}
223
+ //#endregion
224
+ //#region src/Lockfile.d.ts
225
+ declare const LockfileParseError_base: Schema.Class<LockfileParseError, Schema.TaggedStruct<"LockfileParseError", {
226
+ readonly format: Schema.Literals<readonly ["bun", "npm", "pnpm", "yarn"]>;
227
+ readonly stage: Schema.Literals<readonly ["syntax", "validation"]>;
228
+ readonly cause: Schema.Defect;
229
+ }>, import("effect/Cause").YieldableError>;
230
+ /**
231
+ * Failure of `Lockfile.parse`: the given content is not a valid lockfile of
232
+ * the requested format.
233
+ *
234
+ * @remarks
235
+ * - `format` — which format was being parsed.
236
+ * - `stage` — `"syntax"` when the text itself failed to parse (YAML, JSON,
237
+ * JSONC), `"validation"` when the text parsed but did not have the
238
+ * format's expected shape.
239
+ * - `cause` — the underlying jsonc/yaml/JSON/Schema error, preserved
240
+ * structurally.
241
+ *
242
+ * Malformed input always exits through this typed failure, never as a
243
+ * defect. Parse takes content, not a path — the caller that did the IO owns
244
+ * any path context.
245
+ *
246
+ * @public
247
+ */
248
+ declare class LockfileParseError extends LockfileParseError_base {
249
+ get message(): string;
250
+ }
251
+ declare const LockfileFramingError_base: Schema.Class<LockfileFramingError, Schema.TaggedStruct<"LockfileFramingError", {
252
+ readonly format: Schema.Literals<readonly ["bun", "npm", "pnpm", "yarn"]>;
253
+ readonly reason: Schema.Literals<readonly ["noLockfileDocument", "noImporters", "unexpectedDocuments"]>;
254
+ readonly documents: Schema.Int;
255
+ }>, import("effect/Cause").YieldableError>;
256
+ /**
257
+ * Failure of `Lockfile.parse`: the content parsed as text, but no single
258
+ * lockfile document could be located in it.
259
+ *
260
+ * @remarks
261
+ * `pnpm-lock.yaml` is a YAML **stream**. pnpm 11 writes a
262
+ * config-dependencies preamble document ahead of the lockfile whenever the
263
+ * workspace uses `configDependencies`, so the file holds two documents. The
264
+ * lockfile is the last one — pnpm composes the preamble as a prefix — and a
265
+ * parser that reads only the first document gets the preamble: a document
266
+ * that *validates*, and yields a lockfile with an empty workspace. This error
267
+ * exists so that case can never again succeed quietly.
268
+ *
269
+ * - `format` — which format was being parsed.
270
+ * - `documents` — how many YAML documents the stream carried.
271
+ * - `reason`:
272
+ * - `"noLockfileDocument"` — the stream carries no lockfile document. An
273
+ * env-only `pnpm-lock.yaml` (a config-dependencies preamble and nothing
274
+ * after it) reads this way, as does empty content. pnpm itself treats
275
+ * such a file as having no lockfile.
276
+ * - `"noImporters"` — the located document declares no importers, so it
277
+ * describes no workspace. pnpm always records at least the root importer.
278
+ * - `"unexpectedDocuments"` — the stream carries several documents in a
279
+ * format that defines no document framing (yarn). Rather than silently
280
+ * taking the first, parsing refuses to guess.
281
+ *
282
+ * It carries typed fields rather than a `cause`: unlike
283
+ * {@link LockfileParseError}, there is no underlying engine failure to wrap —
284
+ * the text parsed fine.
285
+ *
286
+ * @public
287
+ */
288
+ declare class LockfileFramingError extends LockfileFramingError_base {
289
+ get message(): string;
290
+ }
291
+ declare const Lockfile_base: Schema.Class<Lockfile, Schema.Struct<{
292
+ readonly format: Schema.Literals<readonly ["bun", "npm", "pnpm", "yarn"]>;
293
+ readonly lockfileVersion: Schema.String;
294
+ readonly packages: Schema.$Array<typeof ResolvedPackage>;
295
+ readonly workspaceDependencies: Schema.$Array<typeof WorkspaceDependency>;
296
+ readonly importers: Schema.withConstructorDefault<Schema.withDecodingDefaultKey<Schema.$Array<typeof LockfileImporter>, never>>;
297
+ readonly extension: Schema.optionalKey<Schema.Union<readonly [typeof PnpmExtension, typeof BunExtension]>>;
298
+ }>, {}>;
299
+ /**
300
+ * The unified lockfile model all four formats normalize into.
301
+ *
302
+ * @remarks
303
+ * - `format` — which lockfile format produced the data.
304
+ * - `lockfileVersion` — the lockfile format version string.
305
+ * - `packages` — every resolved package.
306
+ * - `workspaceDependencies` — inter-workspace dependency edges.
307
+ * - `importers` — each workspace importer's declared dependencies
308
+ * ({@link LockfileImporter}), keyed by importer path. Populated by the pnpm,
309
+ * bun and npm parsers; always empty for yarn, which records no importers.
310
+ * - `extension` — format-specific residue (`PnpmExtension` or
311
+ * `BunExtension`) when the format records any.
312
+ *
313
+ * For pnpm, `Lockfile.parse` emits the honest importer-path-keyed model
314
+ * (workspace packages named by importer path, version `"0.0.0"`);
315
+ * {@link Lockfile.withImporterNames} is the explicit, pure second stage
316
+ * that rewrites those names once the caller has read the workspace
317
+ * manifests. npm, yarn and bun lockfiles carry real names and need no
318
+ * second stage.
319
+ *
320
+ * @public
321
+ */
322
+ declare class Lockfile extends Lockfile_base {
323
+ #private;
324
+ /**
325
+ * Parse lockfile content of a known format into the unified model — the
326
+ * package's only fallible boundary.
327
+ *
328
+ * @param content - The lockfile text (this package does no IO; the caller
329
+ * reads the file).
330
+ * @param options - The lockfile format to parse as.
331
+ * @returns An `Effect` succeeding with the {@link Lockfile}, or failing
332
+ * with {@link LockfileParseError} (malformed text or the wrong shape) or
333
+ * {@link LockfileFramingError} (the text parsed, but no lockfile document
334
+ * could be located in the stream — see that error for why a
335
+ * multi-document `pnpm-lock.yaml` needs it).
336
+ */
337
+ static readonly parse: (content: string, options: {
338
+ readonly format: LockfileFormat;
339
+ }) => Effect.Effect<Lockfile, LockfileFramingError | LockfileParseError, never>;
340
+ /**
341
+ * Rewrite pnpm importer-path names to real package names — the explicit
342
+ * second stage of pnpm parsing. Total and pure.
343
+ *
344
+ * @remarks
345
+ * Workspace packages whose `relativePath` appears in `names` are renamed;
346
+ * dependency edge ends are rewritten through the same map. Entries not in
347
+ * the map keep their path name, and non-pnpm lockfiles are unaffected (no
348
+ * key matches). Versions are not touched — pnpm workspace packages keep
349
+ * `"0.0.0"` (the lockfile does not record their real versions).
350
+ *
351
+ * @param names - Importer path → real package name.
352
+ * @returns A new {@link Lockfile} with names rewritten.
353
+ */
354
+ withImporterNames(names: ReadonlyMap<string, string>): Lockfile;
355
+ /**
356
+ * Every resolved package with the given name — one entry per resolved
357
+ * version. Backed by a lazily built index, so repeated lookups are O(1).
358
+ *
359
+ * @param name - The package name to look up.
360
+ * @returns The matching packages, empty when the name is not in the
361
+ * lockfile.
362
+ */
363
+ packagesNamed(name: string): ReadonlyArray<ResolvedPackage>;
364
+ /**
365
+ * The importer at the given path — `"."` for the workspace root — or
366
+ * `Option.none()` when the lockfile records no importer there. Backed by a
367
+ * lazily built index, so repeated lookups are O(1). The index is a `Map`,
368
+ * so an attacker-adjacent path (`__proto__`, `constructor`) neither pollutes
369
+ * nor collides.
370
+ *
371
+ * @param path - The importer path to look up.
372
+ * @returns The matching {@link LockfileImporter}, or `Option.none()`.
373
+ */
374
+ importer(path: string): Option.Option<LockfileImporter>;
375
+ /** The workspace-local packages. */
376
+ get workspacePackages(): ReadonlyArray<ResolvedPackage>;
377
+ }
378
+ //#endregion
379
+ //#region src/LockfileIntegrity.d.ts
380
+ declare const WorkspaceManifest_base: Schema.Class<WorkspaceManifest, Schema.Struct<{
381
+ readonly name: Schema.NonEmptyString;
382
+ readonly dependencies: Schema.optionalKey<Schema.$Record<Schema.String, Schema.String>>;
383
+ readonly devDependencies: Schema.optionalKey<Schema.$Record<Schema.String, Schema.String>>;
384
+ readonly peerDependencies: Schema.optionalKey<Schema.$Record<Schema.String, Schema.String>>;
385
+ readonly optionalDependencies: Schema.optionalKey<Schema.$Record<Schema.String, Schema.String>>;
386
+ }>, {}>;
387
+ /**
388
+ * The minimal manifest shape {@link LockfileIntegrity.compare} checks a
389
+ * lockfile against: a package name plus the four optional dependency maps.
390
+ *
391
+ * @remarks
392
+ * Deliberately *not* a `@effected/package-json` type — this package takes
393
+ * manifests as plain values so its consumers own the manifest IO (and may
394
+ * derive these from any richer model).
395
+ *
396
+ * @public
397
+ */
398
+ declare class WorkspaceManifest extends WorkspaceManifest_base {}
399
+ declare const LockfileIntegrity_base: Schema.Class<LockfileIntegrity, Schema.Struct<{
400
+ readonly valid: Schema.Boolean;
401
+ readonly missingWorkspaces: Schema.$Array<Schema.String>;
402
+ readonly extraWorkspaces: Schema.$Array<Schema.String>;
403
+ readonly unsatisfiedConstraints: Schema.$Array<Schema.Struct<{
404
+ readonly workspace: Schema.String;
405
+ readonly dependency: Schema.String;
406
+ readonly constraint: Schema.String;
407
+ readonly resolved: Schema.String;
408
+ readonly depType: Schema.Literals<readonly ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]>;
409
+ }>>;
410
+ }>, {}>;
411
+ /**
412
+ * Result of checking a parsed lockfile against the workspace's declared
413
+ * manifests.
414
+ *
415
+ * @remarks
416
+ * A data type, not an error: it reports *what* mismatches exist without
417
+ * failing anything.
418
+ *
419
+ * - `valid` — `true` when the lockfile is fully consistent.
420
+ * - `missingWorkspaces` — workspace names present in the manifests but
421
+ * absent from the lockfile.
422
+ * - `extraWorkspaces` — workspace names in the lockfile with no matching
423
+ * manifest.
424
+ * - `unsatisfiedConstraints` — declared constraints the lockfile's resolved
425
+ * versions do not satisfy.
426
+ *
427
+ * @public
428
+ */
429
+ declare class LockfileIntegrity extends LockfileIntegrity_base {
430
+ /**
431
+ * Check a lockfile's consistency against workspace manifests — a total,
432
+ * pure function: no Effect, no error channel, no IO.
433
+ *
434
+ * @remarks
435
+ * Constraint checking is best-effort by design: `workspace:` / `link:` /
436
+ * `file:` specifiers and rows whose range (or every resolved version) does
437
+ * not parse as SemVer are skipped, exactly as in the v3 implementation.
438
+ * A lockfile may resolve the same package at several versions; a
439
+ * constraint is satisfied when *any* resolved version matches, and an
440
+ * unsatisfied row reports every candidate in `resolved`. The
441
+ * caller reads the manifests (this package does no IO). For pnpm, apply
442
+ * `Lockfile#withImporterNames` first so workspace names align with
443
+ * manifest names.
444
+ *
445
+ * (Named `compare`, not `check`: every v4 `Schema.Class` already carries
446
+ * a `static check(...checks)` for attaching schema checks, and statics
447
+ * cannot be shadowed with an incompatible signature.)
448
+ *
449
+ * @param lockfile - The parsed lockfile.
450
+ * @param manifests - The workspace manifests to compare against.
451
+ * @returns The integrity report.
452
+ */
453
+ static compare(lockfile: Lockfile, manifests: ReadonlyArray<WorkspaceManifest>): LockfileIntegrity;
454
+ }
455
+ //#endregion
456
+ export { BunExtension, ImporterDependency, Lockfile, LockfileFormat, LockfileFramingError, LockfileImporter, LockfileIntegrity, LockfileParseError, type PnpmCatalogs, PnpmExtension, ResolvedPackage, WorkspaceDependency, WorkspaceManifest, filenameFor, fromFilename };
457
+ //# sourceMappingURL=index.d.ts.map
package/index.js ADDED
@@ -0,0 +1,11 @@
1
+ import { BunExtension } from "./BunExtension.js";
2
+ import { ImporterDependency } from "./ImporterDependency.js";
3
+ import { LockfileImporter } from "./LockfileImporter.js";
4
+ import { ResolvedPackage } from "./ResolvedPackage.js";
5
+ import { WorkspaceDependency } from "./WorkspaceDependency.js";
6
+ import { PnpmExtension } from "./PnpmExtension.js";
7
+ import { LockfileFormat, filenameFor, fromFilename } from "./LockfileFormat.js";
8
+ import { Lockfile, LockfileFramingError, LockfileParseError } from "./Lockfile.js";
9
+ import { LockfileIntegrity, WorkspaceManifest } from "./LockfileIntegrity.js";
10
+
11
+ export { BunExtension, ImporterDependency, Lockfile, LockfileFormat, LockfileFramingError, LockfileImporter, LockfileIntegrity, LockfileParseError, PnpmExtension, ResolvedPackage, WorkspaceDependency, WorkspaceManifest, filenameFor, fromFilename };