@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/BunExtension.js +25 -0
- package/ImporterDependency.js +33 -0
- package/LICENSE +21 -0
- package/Lockfile.js +258 -0
- package/LockfileFormat.js +52 -0
- package/LockfileImporter.js +29 -0
- package/LockfileIntegrity.js +123 -0
- package/PnpmExtension.js +29 -0
- package/README.md +109 -0
- package/ResolvedPackage.js +40 -0
- package/WorkspaceDependency.js +27 -0
- package/index.d.ts +457 -0
- package/index.js +11 -0
- package/internal/bun.js +102 -0
- package/internal/documents.js +76 -0
- package/internal/npm.js +105 -0
- package/internal/pnpm.js +135 -0
- package/internal/shared.js +133 -0
- package/internal/yarn.js +130 -0
- package/package.json +51 -0
- package/tsdoc-metadata.json +11 -0
package/BunExtension.js
ADDED
|
@@ -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 };
|
package/PnpmExtension.js
ADDED
|
@@ -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 };
|