@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.
@@ -0,0 +1,25 @@
1
+ import { Schema } from "effect";
2
+
3
+ //#region src/BunExtension.ts
4
+ /**
5
+ * Extension data specific to bun lockfiles, attached to `Lockfile.extension`
6
+ * when the format is `"bun"`.
7
+ *
8
+ * @remarks
9
+ * - `catalog` — the default (unnamed) catalog.
10
+ * - `catalogs` — named catalog definitions.
11
+ * - `overrides` — the version override map.
12
+ * - `trustedDependencies` — packages allowed to run install scripts.
13
+ *
14
+ * @public
15
+ */
16
+ var BunExtension = class extends Schema.Class("BunExtension")({
17
+ _tag: Schema.tag("bun"),
18
+ catalog: Schema.optionalKey(Schema.Record(Schema.String, Schema.Unknown)),
19
+ catalogs: Schema.optionalKey(Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Unknown))),
20
+ overrides: Schema.optionalKey(Schema.Record(Schema.String, Schema.String)),
21
+ trustedDependencies: Schema.optionalKey(Schema.Array(Schema.String))
22
+ }) {};
23
+
24
+ //#endregion
25
+ export { BunExtension };
@@ -0,0 +1,33 @@
1
+ import { Schema } from "effect";
2
+ import { DependencyField, DependencySpecifier } from "@effected/npm";
3
+
4
+ //#region src/ImporterDependency.ts
5
+ /**
6
+ * One declared dependency of one workspace importer, as the lockfile records it.
7
+ *
8
+ * @remarks
9
+ * - `name` — the dependency's package name.
10
+ * - `specifier` — the declared range, typed through `@effected/npm`'s
11
+ * `DependencySpecifier.FromString` codec: the decoded value is a tag-matchable
12
+ * `ClassifiedSpecifier` (`catalog:` / `workspace:` / range / dist-tag / raw),
13
+ * while encoding round-trips the **exact original string byte-for-byte** — the
14
+ * guarantee a before/after lockfile diff relies on.
15
+ * - `version` — the concrete resolved version, **populated by pnpm only**. pnpm
16
+ * records `{ specifier, version }` per importer dependency; bun and npm record
17
+ * resolved versions on their package entries instead, so for those formats
18
+ * `version` is absent and a consumer joins by `name` against
19
+ * `Lockfile.packages`.
20
+ * - `depType` — which dependency map declared it, spelled with `@effected/npm`'s
21
+ * kit-wide `DependencyField` vocabulary.
22
+ *
23
+ * @public
24
+ */
25
+ var ImporterDependency = class extends Schema.Class("ImporterDependency")({
26
+ name: Schema.NonEmptyString,
27
+ specifier: DependencySpecifier.FromString,
28
+ version: Schema.optionalKey(Schema.String),
29
+ depType: DependencyField
30
+ }) {};
31
+
32
+ //#endregion
33
+ export { ImporterDependency };
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 C. Spencer Beggs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/Lockfile.js ADDED
@@ -0,0 +1,258 @@
1
+ import { BunExtension } from "./BunExtension.js";
2
+ import { LockfileImporter } from "./LockfileImporter.js";
3
+ import { ResolvedPackage } from "./ResolvedPackage.js";
4
+ import { WorkspaceDependency } from "./WorkspaceDependency.js";
5
+ import { parseBun } from "./internal/bun.js";
6
+ import { parseNpm } from "./internal/npm.js";
7
+ import { PnpmExtension } from "./PnpmExtension.js";
8
+ import { parsePnpm } from "./internal/pnpm.js";
9
+ import { parseYarn } from "./internal/yarn.js";
10
+ import { LockfileFormat } from "./LockfileFormat.js";
11
+ import { Effect, Option, Schema } from "effect";
12
+
13
+ //#region src/Lockfile.ts
14
+ const EMPTY_IMPORTERS = [];
15
+ /**
16
+ * Failure of `Lockfile.parse`: the given content is not a valid lockfile of
17
+ * the requested format.
18
+ *
19
+ * @remarks
20
+ * - `format` — which format was being parsed.
21
+ * - `stage` — `"syntax"` when the text itself failed to parse (YAML, JSON,
22
+ * JSONC), `"validation"` when the text parsed but did not have the
23
+ * format's expected shape.
24
+ * - `cause` — the underlying jsonc/yaml/JSON/Schema error, preserved
25
+ * structurally.
26
+ *
27
+ * Malformed input always exits through this typed failure, never as a
28
+ * defect. Parse takes content, not a path — the caller that did the IO owns
29
+ * any path context.
30
+ *
31
+ * @public
32
+ */
33
+ var LockfileParseError = class extends Schema.TaggedErrorClass()("LockfileParseError", {
34
+ format: LockfileFormat,
35
+ stage: Schema.Literals(["syntax", "validation"]),
36
+ cause: Schema.Defect()
37
+ }) {
38
+ get message() {
39
+ return this.stage === "syntax" ? `Failed to parse ${this.format} lockfile: the content is not well-formed` : `Failed to parse ${this.format} lockfile: the content does not have the expected ${this.format} shape`;
40
+ }
41
+ };
42
+ /**
43
+ * Failure of `Lockfile.parse`: the content parsed as text, but no single
44
+ * lockfile document could be located in it.
45
+ *
46
+ * @remarks
47
+ * `pnpm-lock.yaml` is a YAML **stream**. pnpm 11 writes a
48
+ * config-dependencies preamble document ahead of the lockfile whenever the
49
+ * workspace uses `configDependencies`, so the file holds two documents. The
50
+ * lockfile is the last one — pnpm composes the preamble as a prefix — and a
51
+ * parser that reads only the first document gets the preamble: a document
52
+ * that *validates*, and yields a lockfile with an empty workspace. This error
53
+ * exists so that case can never again succeed quietly.
54
+ *
55
+ * - `format` — which format was being parsed.
56
+ * - `documents` — how many YAML documents the stream carried.
57
+ * - `reason`:
58
+ * - `"noLockfileDocument"` — the stream carries no lockfile document. An
59
+ * env-only `pnpm-lock.yaml` (a config-dependencies preamble and nothing
60
+ * after it) reads this way, as does empty content. pnpm itself treats
61
+ * such a file as having no lockfile.
62
+ * - `"noImporters"` — the located document declares no importers, so it
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.
67
+ *
68
+ * It carries typed fields rather than a `cause`: unlike
69
+ * {@link LockfileParseError}, there is no underlying engine failure to wrap —
70
+ * the text parsed fine.
71
+ *
72
+ * @public
73
+ */
74
+ var LockfileFramingError = class extends Schema.TaggedErrorClass()("LockfileFramingError", {
75
+ format: LockfileFormat,
76
+ reason: Schema.Literals([
77
+ "noLockfileDocument",
78
+ "noImporters",
79
+ "unexpectedDocuments"
80
+ ]),
81
+ documents: Schema.Int
82
+ }) {
83
+ 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)`;
85
+ return `Failed to parse ${this.format} lockfile: ${detail}`;
86
+ }
87
+ };
88
+ const dispatch = (format, content) => {
89
+ switch (format) {
90
+ case "bun": return parseBun(content);
91
+ case "npm": return parseNpm(content);
92
+ case "pnpm": return parsePnpm(content);
93
+ case "yarn": return parseYarn(content);
94
+ }
95
+ };
96
+ /**
97
+ * The unified lockfile model all four formats normalize into.
98
+ *
99
+ * @remarks
100
+ * - `format` — which lockfile format produced the data.
101
+ * - `lockfileVersion` — the lockfile format version string.
102
+ * - `packages` — every resolved package.
103
+ * - `workspaceDependencies` — inter-workspace dependency edges.
104
+ * - `importers` — each workspace importer's declared dependencies
105
+ * ({@link LockfileImporter}), keyed by importer path. Populated by the pnpm,
106
+ * bun and npm parsers; always empty for yarn, which records no importers.
107
+ * - `extension` — format-specific residue (`PnpmExtension` or
108
+ * `BunExtension`) when the format records any.
109
+ *
110
+ * For pnpm, `Lockfile.parse` emits the honest importer-path-keyed model
111
+ * (workspace packages named by importer path, version `"0.0.0"`);
112
+ * {@link Lockfile.withImporterNames} is the explicit, pure second stage
113
+ * that rewrites those names once the caller has read the workspace
114
+ * manifests. npm, yarn and bun lockfiles carry real names and need no
115
+ * second stage.
116
+ *
117
+ * @public
118
+ */
119
+ var Lockfile = class Lockfile extends Schema.Class("Lockfile")({
120
+ format: LockfileFormat,
121
+ lockfileVersion: Schema.String,
122
+ packages: Schema.Array(ResolvedPackage),
123
+ workspaceDependencies: Schema.Array(WorkspaceDependency),
124
+ importers: Schema.Array(LockfileImporter).pipe(Schema.withDecodingDefaultKey(Effect.succeed([])), Schema.withConstructorDefault(Effect.succeed(EMPTY_IMPORTERS))),
125
+ extension: Schema.optionalKey(Schema.Union([PnpmExtension, BunExtension]))
126
+ }) {
127
+ /** Lazily built name → packages index; deliberately outside the schema, never encodes. */
128
+ #nameIndex;
129
+ /** Lazily built importer-path → importer index; deliberately outside the schema, never encodes. */
130
+ #importerIndex;
131
+ /**
132
+ * Parse lockfile content of a known format into the unified model — the
133
+ * package's only fallible boundary.
134
+ *
135
+ * @param content - The lockfile text (this package does no IO; the caller
136
+ * reads the file).
137
+ * @param options - The lockfile format to parse as.
138
+ * @returns An `Effect` succeeding with the {@link Lockfile}, or failing
139
+ * with {@link LockfileParseError} (malformed text or the wrong shape) or
140
+ * {@link LockfileFramingError} (the text parsed, but no lockfile document
141
+ * could be located in the stream — see that error for why a
142
+ * multi-document `pnpm-lock.yaml` needs it).
143
+ */
144
+ static parse = Effect.fn("Lockfile.parse")(function* (content, options) {
145
+ const fields = yield* dispatch(options.format, content).pipe(Effect.mapError((failure) => failure.stage === "framing" ? new LockfileFramingError({
146
+ format: options.format,
147
+ reason: failure.reason,
148
+ documents: failure.documents
149
+ }) : new LockfileParseError({
150
+ format: options.format,
151
+ stage: failure.stage,
152
+ cause: failure.cause
153
+ })));
154
+ return Lockfile.make({
155
+ format: options.format,
156
+ lockfileVersion: fields.lockfileVersion,
157
+ packages: fields.packages,
158
+ workspaceDependencies: fields.workspaceDependencies,
159
+ importers: fields.importers,
160
+ ...fields.extension !== void 0 ? { extension: fields.extension } : {}
161
+ });
162
+ });
163
+ /**
164
+ * Rewrite pnpm importer-path names to real package names — the explicit
165
+ * second stage of pnpm parsing. Total and pure.
166
+ *
167
+ * @remarks
168
+ * Workspace packages whose `relativePath` appears in `names` are renamed;
169
+ * dependency edge ends are rewritten through the same map. Entries not in
170
+ * the map keep their path name, and non-pnpm lockfiles are unaffected (no
171
+ * key matches). Versions are not touched — pnpm workspace packages keep
172
+ * `"0.0.0"` (the lockfile does not record their real versions).
173
+ *
174
+ * @param names - Importer path → real package name.
175
+ * @returns A new {@link Lockfile} with names rewritten.
176
+ */
177
+ withImporterNames(names) {
178
+ const packages = this.packages.map((pkg) => {
179
+ if (!pkg.isWorkspace || pkg.relativePath === void 0) return pkg;
180
+ const realName = names.get(pkg.relativePath);
181
+ if (realName === void 0 || realName === "" || realName === pkg.name) return pkg;
182
+ return ResolvedPackage.make({
183
+ name: realName,
184
+ version: pkg.version,
185
+ ...pkg.integrity !== void 0 ? { integrity: pkg.integrity } : {},
186
+ isWorkspace: pkg.isWorkspace,
187
+ relativePath: pkg.relativePath,
188
+ dependencies: pkg.dependencies
189
+ });
190
+ });
191
+ const workspaceDependencies = this.workspaceDependencies.map((dep) => {
192
+ const mappedFrom = names.get(dep.from);
193
+ const mappedTo = names.get(dep.to);
194
+ const from = mappedFrom === void 0 || mappedFrom === "" ? dep.from : mappedFrom;
195
+ const to = mappedTo === void 0 || mappedTo === "" ? dep.to : mappedTo;
196
+ if (from === dep.from && to === dep.to) return dep;
197
+ return WorkspaceDependency.make({
198
+ from,
199
+ to,
200
+ depType: dep.depType,
201
+ constraint: dep.constraint
202
+ });
203
+ });
204
+ return Lockfile.make({
205
+ format: this.format,
206
+ lockfileVersion: this.lockfileVersion,
207
+ packages,
208
+ workspaceDependencies,
209
+ importers: this.importers,
210
+ ...this.extension !== void 0 ? { extension: this.extension } : {}
211
+ });
212
+ }
213
+ /**
214
+ * Every resolved package with the given name — one entry per resolved
215
+ * version. Backed by a lazily built index, so repeated lookups are O(1).
216
+ *
217
+ * @param name - The package name to look up.
218
+ * @returns The matching packages, empty when the name is not in the
219
+ * lockfile.
220
+ */
221
+ packagesNamed(name) {
222
+ if (this.#nameIndex === void 0) {
223
+ const index = /* @__PURE__ */ new Map();
224
+ for (const pkg of this.packages) {
225
+ const bucket = index.get(pkg.name);
226
+ if (bucket === void 0) index.set(pkg.name, [pkg]);
227
+ else bucket.push(pkg);
228
+ }
229
+ this.#nameIndex = index;
230
+ }
231
+ return this.#nameIndex.get(name) ?? [];
232
+ }
233
+ /**
234
+ * The importer at the given path — `"."` for the workspace root — or
235
+ * `Option.none()` when the lockfile records no importer there. Backed by a
236
+ * lazily built index, so repeated lookups are O(1). The index is a `Map`,
237
+ * so an attacker-adjacent path (`__proto__`, `constructor`) neither pollutes
238
+ * nor collides.
239
+ *
240
+ * @param path - The importer path to look up.
241
+ * @returns The matching {@link LockfileImporter}, or `Option.none()`.
242
+ */
243
+ importer(path) {
244
+ if (this.#importerIndex === void 0) {
245
+ const index = /* @__PURE__ */ new Map();
246
+ for (const imp of this.importers) index.set(imp.path, imp);
247
+ this.#importerIndex = index;
248
+ }
249
+ return Option.fromUndefinedOr(this.#importerIndex.get(path));
250
+ }
251
+ /** The workspace-local packages. */
252
+ get workspacePackages() {
253
+ return this.packages.filter((pkg) => pkg.isWorkspace);
254
+ }
255
+ };
256
+
257
+ //#endregion
258
+ export { Lockfile, LockfileFramingError, LockfileParseError };
@@ -0,0 +1,52 @@
1
+ import { Option, Schema } from "effect";
2
+
3
+ //#region src/LockfileFormat.ts
4
+ /**
5
+ * The lockfile formats this package parses: bun's `bun.lock` (JSONC), npm's
6
+ * `package-lock.json` (v2/v3 JSON), pnpm's `pnpm-lock.yaml` and yarn Berry's
7
+ * `yarn.lock` (both YAML).
8
+ *
9
+ * @remarks
10
+ * The literal names the *lockfile format*, not the package manager that
11
+ * happens to write it — this package models lockfiles. Yarn support is
12
+ * Berry only; classic (v1) `yarn.lock` is not YAML and fails
13
+ * `Lockfile.parse` with a typed `LockfileParseError`.
14
+ *
15
+ * @public
16
+ */
17
+ const LockfileFormat = Schema.Literals([
18
+ "bun",
19
+ "npm",
20
+ "pnpm",
21
+ "yarn"
22
+ ]);
23
+ const FILENAMES = {
24
+ bun: "bun.lock",
25
+ npm: "package-lock.json",
26
+ pnpm: "pnpm-lock.yaml",
27
+ yarn: "yarn.lock"
28
+ };
29
+ /**
30
+ * The conventional lockfile filename for a format: `"bun.lock"`,
31
+ * `"package-lock.json"`, `"pnpm-lock.yaml"` or `"yarn.lock"`.
32
+ *
33
+ * @public
34
+ */
35
+ const filenameFor = (format) => FILENAMES[format];
36
+ /**
37
+ * The format a lockfile filename identifies, if any.
38
+ *
39
+ * @remarks
40
+ * Matches exact conventional filenames only (`"bun.lock"`,
41
+ * `"package-lock.json"`, `"pnpm-lock.yaml"`, `"yarn.lock"`) — paths and
42
+ * other spellings return `Option.none()`.
43
+ *
44
+ * @public
45
+ */
46
+ const fromFilename = (name) => {
47
+ for (const format of LockfileFormat.literals) if (FILENAMES[format] === name) return Option.some(format);
48
+ return Option.none();
49
+ };
50
+
51
+ //#endregion
52
+ export { LockfileFormat, filenameFor, fromFilename };
@@ -0,0 +1,29 @@
1
+ import { ImporterDependency } from "./ImporterDependency.js";
2
+ import { Schema } from "effect";
3
+
4
+ //#region src/LockfileImporter.ts
5
+ /**
6
+ * One workspace importer's declared dependencies, as the lockfile records them.
7
+ *
8
+ * @remarks
9
+ * - `path` — the importer path relative to the workspace root, `"."` for the
10
+ * root package (never empty — a `NonEmptyString`) — the same keys as
11
+ * `WorkspaceDiscovery.importerMap()` in
12
+ * `@effected/workspaces`. This is the stable join key: `Lockfile#importer`
13
+ * looks importers up by it, and `Lockfile#withImporterNames` deliberately
14
+ * leaves importers untouched because the path — not a package name — keys
15
+ * them.
16
+ * - `dependencies` — each declared dependency as an {@link ImporterDependency}.
17
+ *
18
+ * Populated by the pnpm, bun and npm parsers. yarn does not record importers,
19
+ * so a yarn lockfile always yields an empty `importers` array.
20
+ *
21
+ * @public
22
+ */
23
+ var LockfileImporter = class extends Schema.Class("LockfileImporter")({
24
+ path: Schema.NonEmptyString,
25
+ dependencies: Schema.Array(ImporterDependency)
26
+ }) {};
27
+
28
+ //#endregion
29
+ export { LockfileImporter };
@@ -0,0 +1,123 @@
1
+ import { DEP_TYPES, isWorkspaceSpecifier } from "./internal/shared.js";
2
+ import { Exit, Schema } from "effect";
3
+ import { DependencyField } from "@effected/npm";
4
+ import { Range, SemVer } from "@effected/semver";
5
+
6
+ //#region src/LockfileIntegrity.ts
7
+ /**
8
+ * The minimal manifest shape {@link LockfileIntegrity.compare} checks a
9
+ * lockfile against: a package name plus the four optional dependency maps.
10
+ *
11
+ * @remarks
12
+ * Deliberately *not* a `@effected/package-json` type — this package takes
13
+ * manifests as plain values so its consumers own the manifest IO (and may
14
+ * derive these from any richer model).
15
+ *
16
+ * @public
17
+ */
18
+ var WorkspaceManifest = class extends Schema.Class("WorkspaceManifest")({
19
+ name: Schema.NonEmptyString,
20
+ dependencies: Schema.optionalKey(Schema.Record(Schema.String, Schema.String)),
21
+ devDependencies: Schema.optionalKey(Schema.Record(Schema.String, Schema.String)),
22
+ peerDependencies: Schema.optionalKey(Schema.Record(Schema.String, Schema.String)),
23
+ optionalDependencies: Schema.optionalKey(Schema.Record(Schema.String, Schema.String))
24
+ }) {};
25
+ const decodeRange = Schema.decodeUnknownExit(Range.FromString);
26
+ const decodeSemVer = Schema.decodeUnknownExit(SemVer.FromString);
27
+ /**
28
+ * Result of checking a parsed lockfile against the workspace's declared
29
+ * manifests.
30
+ *
31
+ * @remarks
32
+ * A data type, not an error: it reports *what* mismatches exist without
33
+ * failing anything.
34
+ *
35
+ * - `valid` — `true` when the lockfile is fully consistent.
36
+ * - `missingWorkspaces` — workspace names present in the manifests but
37
+ * absent from the lockfile.
38
+ * - `extraWorkspaces` — workspace names in the lockfile with no matching
39
+ * manifest.
40
+ * - `unsatisfiedConstraints` — declared constraints the lockfile's resolved
41
+ * versions do not satisfy.
42
+ *
43
+ * @public
44
+ */
45
+ var LockfileIntegrity = class LockfileIntegrity extends Schema.Class("LockfileIntegrity")({
46
+ valid: Schema.Boolean,
47
+ missingWorkspaces: Schema.Array(Schema.String),
48
+ extraWorkspaces: Schema.Array(Schema.String),
49
+ unsatisfiedConstraints: Schema.Array(Schema.Struct({
50
+ workspace: Schema.String,
51
+ dependency: Schema.String,
52
+ constraint: Schema.String,
53
+ resolved: Schema.String,
54
+ depType: DependencyField
55
+ }))
56
+ }) {
57
+ /**
58
+ * Check a lockfile's consistency against workspace manifests — a total,
59
+ * pure function: no Effect, no error channel, no IO.
60
+ *
61
+ * @remarks
62
+ * Constraint checking is best-effort by design: `workspace:` / `link:` /
63
+ * `file:` specifiers and rows whose range (or every resolved version) does
64
+ * not parse as SemVer are skipped, exactly as in the v3 implementation.
65
+ * A lockfile may resolve the same package at several versions; a
66
+ * constraint is satisfied when *any* resolved version matches, and an
67
+ * unsatisfied row reports every candidate in `resolved`. The
68
+ * caller reads the manifests (this package does no IO). For pnpm, apply
69
+ * `Lockfile#withImporterNames` first so workspace names align with
70
+ * manifest names.
71
+ *
72
+ * (Named `compare`, not `check`: every v4 `Schema.Class` already carries
73
+ * a `static check(...checks)` for attaching schema checks, and statics
74
+ * cannot be shadowed with an incompatible signature.)
75
+ *
76
+ * @param lockfile - The parsed lockfile.
77
+ * @param manifests - The workspace manifests to compare against.
78
+ * @returns The integrity report.
79
+ */
80
+ static compare(lockfile, manifests) {
81
+ const workspacePackages = lockfile.packages.filter((p) => p.isWorkspace && p.relativePath !== void 0);
82
+ const lockfileWsNames = new Set(workspacePackages.map((p) => p.name));
83
+ const manifestNames = new Set(manifests.map((m) => m.name));
84
+ const missingWorkspaces = [...manifestNames].filter((n) => !lockfileWsNames.has(n));
85
+ const extraWorkspaces = [...lockfileWsNames].filter((n) => !manifestNames.has(n));
86
+ const resolvedIndex = /* @__PURE__ */ new Map();
87
+ for (const p of lockfile.packages) {
88
+ const versions = resolvedIndex.get(p.name);
89
+ if (versions === void 0) resolvedIndex.set(p.name, [p.version]);
90
+ else versions.push(p.version);
91
+ }
92
+ const unsatisfiedConstraints = [];
93
+ for (const manifest of manifests) for (const depType of DEP_TYPES) {
94
+ const depMap = manifest[depType];
95
+ if (!depMap) continue;
96
+ for (const [dependency, constraint] of Object.entries(depMap)) {
97
+ if (isWorkspaceSpecifier(constraint)) continue;
98
+ const candidates = resolvedIndex.get(dependency);
99
+ if (candidates === void 0) continue;
100
+ const rangeExit = decodeRange(constraint);
101
+ if (Exit.isFailure(rangeExit)) continue;
102
+ const versions = candidates.map((candidate) => decodeSemVer(candidate)).filter(Exit.isSuccess);
103
+ if (versions.length === 0) continue;
104
+ if (!versions.some((v) => rangeExit.value.test(v.value))) unsatisfiedConstraints.push({
105
+ workspace: manifest.name,
106
+ dependency,
107
+ constraint,
108
+ resolved: candidates.join(", "),
109
+ depType
110
+ });
111
+ }
112
+ }
113
+ return LockfileIntegrity.make({
114
+ valid: missingWorkspaces.length === 0 && extraWorkspaces.length === 0 && unsatisfiedConstraints.length === 0,
115
+ missingWorkspaces,
116
+ extraWorkspaces,
117
+ unsatisfiedConstraints
118
+ });
119
+ }
120
+ };
121
+
122
+ //#endregion
123
+ export { LockfileIntegrity, WorkspaceManifest };
@@ -0,0 +1,29 @@
1
+ import { Schema } from "effect";
2
+
3
+ //#region src/PnpmExtension.ts
4
+ /**
5
+ * Extension data specific to pnpm lockfiles, attached to `Lockfile.extension`
6
+ * when the format is `"pnpm"`.
7
+ *
8
+ * @remarks
9
+ * - `catalogs` — pnpm catalog definitions ({@link PnpmCatalogs}).
10
+ * - `overrides` — the version override map recorded in the lockfile header.
11
+ * - `settings` — pnpm settings recorded in the lockfile header.
12
+ *
13
+ * @public
14
+ */
15
+ var PnpmExtension = class extends Schema.Class("PnpmExtension")({
16
+ _tag: Schema.tag("pnpm"),
17
+ catalogs: Schema.optionalKey(Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Union([Schema.String, Schema.Struct({
18
+ specifier: Schema.String,
19
+ version: Schema.String
20
+ })])))),
21
+ overrides: Schema.optionalKey(Schema.Record(Schema.String, Schema.String)),
22
+ settings: Schema.optionalKey(Schema.Struct({
23
+ autoInstallPeers: Schema.optionalKey(Schema.Boolean),
24
+ excludeLinksFromLockfile: Schema.optionalKey(Schema.Boolean)
25
+ }))
26
+ }) {};
27
+
28
+ //#endregion
29
+ export { PnpmExtension };