@effected/lockfiles 0.12.0 → 0.13.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/ImporterDependency.js +4 -2
- package/Lockfile.js +29 -17
- package/index.d.ts +31 -16
- package/internal/bun.js +4 -5
- package/internal/documents.js +46 -68
- package/internal/pnpm.js +33 -13
- package/internal/pnpmEnv.js +5 -5
- package/internal/shared.js +61 -17
- package/package.json +2 -2
package/ImporterDependency.js
CHANGED
|
@@ -16,8 +16,10 @@ import { DependencyField, DependencySpecifier } from "@effected/npm";
|
|
|
16
16
|
* comparable across refs and printable. pnpm records `{ specifier, version }`
|
|
17
17
|
* per importer dependency, and the parser splits pnpm's peer-disambiguation
|
|
18
18
|
* suffix off into `peerSuffix`, so `version` is always the plain version — or
|
|
19
|
-
* a non-registry resolution (`link:../utils`, `file:...`)
|
|
20
|
-
*
|
|
19
|
+
* a non-registry resolution (`link:../utils`, `file:...`). A `file:`
|
|
20
|
+
* resolution is suffixed like a registry version when the package declares
|
|
21
|
+
* peers and is split the same way; a `link:` is never suffixed and passes
|
|
22
|
+
* through verbatim. bun and npm record resolved versions on their package entries
|
|
21
23
|
* instead, so for those formats `version` is absent and a consumer joins by
|
|
22
24
|
* `name` against `Lockfile.packages`.
|
|
23
25
|
* - `peerSuffix` — pnpm's peer-disambiguation context: the raw parenthesized
|
package/Lockfile.js
CHANGED
|
@@ -44,9 +44,10 @@ var LockfileParseError = class extends Schema.TaggedError()("LockfileParseError"
|
|
|
44
44
|
* lockfile document could be located in it.
|
|
45
45
|
*
|
|
46
46
|
* @remarks
|
|
47
|
-
* `pnpm-lock.yaml` is a YAML **stream**. pnpm 11
|
|
48
|
-
*
|
|
49
|
-
*
|
|
47
|
+
* `pnpm-lock.yaml` is a YAML **stream**. pnpm 11 and 12 write an env
|
|
48
|
+
* preamble document ahead of the lockfile whenever the workspace declares
|
|
49
|
+
* `configDependencies` or `devEngines.packageManager`, so the file holds two
|
|
50
|
+
* documents. The
|
|
50
51
|
* lockfile is the last one — pnpm composes the preamble as a prefix — and a
|
|
51
52
|
* parser that reads only the first document gets the preamble: a document
|
|
52
53
|
* that *validates*, and yields a lockfile with an empty workspace. This error
|
|
@@ -55,20 +56,24 @@ var LockfileParseError = class extends Schema.TaggedError()("LockfileParseError"
|
|
|
55
56
|
* - `format` — which format was being parsed.
|
|
56
57
|
* - `documents` — how many YAML documents the stream carried.
|
|
57
58
|
* - `reason`:
|
|
58
|
-
* - `"noLockfileDocument"` — the stream carries no lockfile document
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
59
|
+
* - `"noLockfileDocument"` — the stream carries no lockfile document:
|
|
60
|
+
* empty content, or a pnpm stream whose lockfile position is empty. That
|
|
61
|
+
* includes an env preamble followed by an empty document unless the
|
|
62
|
+
* caller passes `configOnly`: pnpm writes those same bytes for a
|
|
63
|
+
* config-dependency-only workspace with no root `package.json` *and* for
|
|
64
|
+
* a workspace whose first install failed after its config dependencies
|
|
65
|
+
* were installed, so only the caller can say which it is. With
|
|
66
|
+
* `configOnly`, `Lockfile.parse` reads that stream as an empty lockfile
|
|
67
|
+
* versioned by the preamble; an empty lockfile position with no preamble
|
|
68
|
+
* fails either way.
|
|
62
69
|
* - `"noImporters"` — the located document declares no importers, so it
|
|
63
70
|
* describes no workspace. pnpm always records at least the root importer.
|
|
64
71
|
* - `"unexpectedDocuments"` — the stream carries more documents than the
|
|
65
72
|
* format's framing defines: several in a format that defines none
|
|
66
|
-
* (yarn), or more than the env preamble and the lockfile
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* `Lockfile.parse` reads the last document of a stream of any count and
|
|
71
|
-
* never fails with this reason.
|
|
73
|
+
* (yarn), or more than the env preamble and the lockfile in a pnpm
|
|
74
|
+
* stream. Rather than silently picking one, parsing refuses to guess.
|
|
75
|
+
* `Lockfile.parse` and `PnpmEnvLockfile` share one pnpm splitter, so
|
|
76
|
+
* both enforce the same at-most-two limit.
|
|
72
77
|
*
|
|
73
78
|
* It carries typed fields rather than a `cause`: unlike
|
|
74
79
|
* {@link LockfileParseError}, there is no underlying engine failure to wrap —
|
|
@@ -107,11 +112,11 @@ const materializeFailure = (format, failure) => failure.stage === "framing" ? ne
|
|
|
107
112
|
stage: failure.stage,
|
|
108
113
|
cause: failure.cause
|
|
109
114
|
});
|
|
110
|
-
const dispatch = (format, content) => {
|
|
115
|
+
const dispatch = (format, content, configOnly) => {
|
|
111
116
|
switch (format) {
|
|
112
117
|
case "bun": return parseBun(content);
|
|
113
118
|
case "npm": return parseNpm(content);
|
|
114
|
-
case "pnpm": return parsePnpm(content);
|
|
119
|
+
case "pnpm": return parsePnpm(content, configOnly);
|
|
115
120
|
case "yarn": return parseYarn(content);
|
|
116
121
|
}
|
|
117
122
|
};
|
|
@@ -158,7 +163,14 @@ var Lockfile = class Lockfile extends Schema.Class("Lockfile")({
|
|
|
158
163
|
*
|
|
159
164
|
* @param content - The lockfile text (this package does no IO; the caller
|
|
160
165
|
* reads the file).
|
|
161
|
-
* @param options -
|
|
166
|
+
* @param options - `format` is the lockfile format to parse as.
|
|
167
|
+
* `configOnly` is the caller's assertion that the workspace has **no root
|
|
168
|
+
* `package.json`**, so a pnpm env preamble followed by an empty main
|
|
169
|
+
* document is a config-dependency-only workspace and reads as an empty
|
|
170
|
+
* lockfile versioned by the preamble. Without it that stream fails
|
|
171
|
+
* {@link LockfileFramingError} with `noLockfileDocument`, because the same
|
|
172
|
+
* bytes are also what an interrupted first install leaves behind. It
|
|
173
|
+
* loosens nothing else, and is ignored for non-pnpm formats.
|
|
162
174
|
* @returns An `Effect` succeeding with the {@link Lockfile}, or failing
|
|
163
175
|
* with {@link LockfileParseError} (malformed text or the wrong shape) or
|
|
164
176
|
* {@link LockfileFramingError} (the text parsed, but no lockfile document
|
|
@@ -166,7 +178,7 @@ var Lockfile = class Lockfile extends Schema.Class("Lockfile")({
|
|
|
166
178
|
* multi-document `pnpm-lock.yaml` needs it).
|
|
167
179
|
*/
|
|
168
180
|
static parse = Effect.fn("Lockfile.parse")(function* (content, options) {
|
|
169
|
-
const fields = yield* dispatch(options.format, content).pipe(Effect.mapError((failure) => materializeFailure(options.format, failure)));
|
|
181
|
+
const fields = yield* dispatch(options.format, content, options.configOnly === true).pipe(Effect.mapError((failure) => materializeFailure(options.format, failure)));
|
|
170
182
|
return Lockfile.make({
|
|
171
183
|
format: options.format,
|
|
172
184
|
lockfileVersion: fields.lockfileVersion,
|
package/index.d.ts
CHANGED
|
@@ -77,8 +77,10 @@ declare const ImporterDependency_base: Schema.Class<ImporterDependency, Schema.S
|
|
|
77
77
|
* comparable across refs and printable. pnpm records `{ specifier, version }`
|
|
78
78
|
* per importer dependency, and the parser splits pnpm's peer-disambiguation
|
|
79
79
|
* suffix off into `peerSuffix`, so `version` is always the plain version — or
|
|
80
|
-
* a non-registry resolution (`link:../utils`, `file:...`)
|
|
81
|
-
*
|
|
80
|
+
* a non-registry resolution (`link:../utils`, `file:...`). A `file:`
|
|
81
|
+
* resolution is suffixed like a registry version when the package declares
|
|
82
|
+
* peers and is split the same way; a `link:` is never suffixed and passes
|
|
83
|
+
* through verbatim. bun and npm record resolved versions on their package entries
|
|
82
84
|
* instead, so for those formats `version` is absent and a consumer joins by
|
|
83
85
|
* `name` against `Lockfile.packages`.
|
|
84
86
|
* - `peerSuffix` — pnpm's peer-disambiguation context: the raw parenthesized
|
|
@@ -388,9 +390,10 @@ declare const LockfileFramingError_base: Schema.Class<LockfileFramingError, Sche
|
|
|
388
390
|
* lockfile document could be located in it.
|
|
389
391
|
*
|
|
390
392
|
* @remarks
|
|
391
|
-
* `pnpm-lock.yaml` is a YAML **stream**. pnpm 11
|
|
392
|
-
*
|
|
393
|
-
*
|
|
393
|
+
* `pnpm-lock.yaml` is a YAML **stream**. pnpm 11 and 12 write an env
|
|
394
|
+
* preamble document ahead of the lockfile whenever the workspace declares
|
|
395
|
+
* `configDependencies` or `devEngines.packageManager`, so the file holds two
|
|
396
|
+
* documents. The
|
|
394
397
|
* lockfile is the last one — pnpm composes the preamble as a prefix — and a
|
|
395
398
|
* parser that reads only the first document gets the preamble: a document
|
|
396
399
|
* that *validates*, and yields a lockfile with an empty workspace. This error
|
|
@@ -399,20 +402,24 @@ declare const LockfileFramingError_base: Schema.Class<LockfileFramingError, Sche
|
|
|
399
402
|
* - `format` — which format was being parsed.
|
|
400
403
|
* - `documents` — how many YAML documents the stream carried.
|
|
401
404
|
* - `reason`:
|
|
402
|
-
* - `"noLockfileDocument"` — the stream carries no lockfile document
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
405
|
+
* - `"noLockfileDocument"` — the stream carries no lockfile document:
|
|
406
|
+
* empty content, or a pnpm stream whose lockfile position is empty. That
|
|
407
|
+
* includes an env preamble followed by an empty document unless the
|
|
408
|
+
* caller passes `configOnly`: pnpm writes those same bytes for a
|
|
409
|
+
* config-dependency-only workspace with no root `package.json` *and* for
|
|
410
|
+
* a workspace whose first install failed after its config dependencies
|
|
411
|
+
* were installed, so only the caller can say which it is. With
|
|
412
|
+
* `configOnly`, `Lockfile.parse` reads that stream as an empty lockfile
|
|
413
|
+
* versioned by the preamble; an empty lockfile position with no preamble
|
|
414
|
+
* fails either way.
|
|
406
415
|
* - `"noImporters"` — the located document declares no importers, so it
|
|
407
416
|
* describes no workspace. pnpm always records at least the root importer.
|
|
408
417
|
* - `"unexpectedDocuments"` — the stream carries more documents than the
|
|
409
418
|
* format's framing defines: several in a format that defines none
|
|
410
|
-
* (yarn), or more than the env preamble and the lockfile
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
* `Lockfile.parse` reads the last document of a stream of any count and
|
|
415
|
-
* never fails with this reason.
|
|
419
|
+
* (yarn), or more than the env preamble and the lockfile in a pnpm
|
|
420
|
+
* stream. Rather than silently picking one, parsing refuses to guess.
|
|
421
|
+
* `Lockfile.parse` and `PnpmEnvLockfile` share one pnpm splitter, so
|
|
422
|
+
* both enforce the same at-most-two limit.
|
|
416
423
|
*
|
|
417
424
|
* It carries typed fields rather than a `cause`: unlike
|
|
418
425
|
* {@link LockfileParseError}, there is no underlying engine failure to wrap —
|
|
@@ -462,7 +469,14 @@ export declare class Lockfile extends Lockfile_base {
|
|
|
462
469
|
*
|
|
463
470
|
* @param content - The lockfile text (this package does no IO; the caller
|
|
464
471
|
* reads the file).
|
|
465
|
-
* @param options -
|
|
472
|
+
* @param options - `format` is the lockfile format to parse as.
|
|
473
|
+
* `configOnly` is the caller's assertion that the workspace has **no root
|
|
474
|
+
* `package.json`**, so a pnpm env preamble followed by an empty main
|
|
475
|
+
* document is a config-dependency-only workspace and reads as an empty
|
|
476
|
+
* lockfile versioned by the preamble. Without it that stream fails
|
|
477
|
+
* {@link LockfileFramingError} with `noLockfileDocument`, because the same
|
|
478
|
+
* bytes are also what an interrupted first install leaves behind. It
|
|
479
|
+
* loosens nothing else, and is ignored for non-pnpm formats.
|
|
466
480
|
* @returns An `Effect` succeeding with the {@link Lockfile}, or failing
|
|
467
481
|
* with {@link LockfileParseError} (malformed text or the wrong shape) or
|
|
468
482
|
* {@link LockfileFramingError} (the text parsed, but no lockfile document
|
|
@@ -471,6 +485,7 @@ export declare class Lockfile extends Lockfile_base {
|
|
|
471
485
|
*/
|
|
472
486
|
static readonly parse: (content: string, options: {
|
|
473
487
|
readonly format: LockfileFormat;
|
|
488
|
+
readonly configOnly?: boolean | undefined;
|
|
474
489
|
}) => Effect.Effect<Lockfile, LockfileFramingError | LockfileParseError, never>;
|
|
475
490
|
/**
|
|
476
491
|
* Rewrite pnpm importer-path names to real package names — the explicit
|
package/internal/bun.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BunExtension } from "../BunExtension.js";
|
|
2
2
|
import { LockfileImporter } from "../LockfileImporter.js";
|
|
3
3
|
import { ResolvedPackage } from "../ResolvedPackage.js";
|
|
4
|
-
import { extractWorkspaceDeps, importerDependencies, peerDeclarations, syntaxFailure, toIntegrityHash, validationFailure } from "./shared.js";
|
|
4
|
+
import { extractWorkspaceDeps, importerDependencies, peerDeclarations, splitNameVersion, syntaxFailure, toIntegrityHash, validationFailure } from "./shared.js";
|
|
5
5
|
import { Effect, Exit, Schema } from "effect";
|
|
6
6
|
import { Jsonc } from "@effected/jsonc";
|
|
7
7
|
|
|
@@ -149,10 +149,9 @@ const toFields = (raw) => Effect.gen(function* () {
|
|
|
149
149
|
if (tuple.length < 1) continue;
|
|
150
150
|
const first = tuple[0];
|
|
151
151
|
if (typeof first !== "string") continue;
|
|
152
|
-
const
|
|
153
|
-
if (
|
|
154
|
-
const name =
|
|
155
|
-
const version = first.slice(atIdx + 1);
|
|
152
|
+
const split = splitNameVersion(first);
|
|
153
|
+
if (split === void 0) continue;
|
|
154
|
+
const { name, version } = split;
|
|
156
155
|
if (workspaceNames.has(name)) continue;
|
|
157
156
|
const integrity = yield* toIntegrityHash(tuple.length >= 4 && typeof tuple[3] === "string" ? tuple[3] : void 0);
|
|
158
157
|
const info = tuple.length >= 3 ? readBunPackageInfo(tuple[2]) : void 0;
|
package/internal/documents.js
CHANGED
|
@@ -13,41 +13,59 @@ import { Yaml } from "@effected/yaml";
|
|
|
13
13
|
*/
|
|
14
14
|
const isEmptyDocument = (document) => document === null || document === void 0;
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
16
|
+
* The number of documents pnpm's env-lockfile writer can compose: the env
|
|
17
|
+
* preamble and the main lockfile. See {@link splitPnpmStream}.
|
|
17
18
|
*
|
|
18
|
-
* @
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* is
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
21
|
+
const MAX_PNPM_DOCUMENTS = 2;
|
|
22
|
+
/**
|
|
23
|
+
* Split a `pnpm-lock.yaml` YAML stream into its env preamble and its main
|
|
24
|
+
* lockfile document. This is the one place pnpm's document framing is
|
|
25
|
+
* decided: `Lockfile.parse` projects the main document out of it and
|
|
26
|
+
* `PnpmEnvLockfile` the preamble, so the two readers cannot disagree about
|
|
27
|
+
* which document is which or how many a stream may carry.
|
|
27
28
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
29
|
+
* @remarks
|
|
30
|
+
* pnpm 11 and 12 write `pnpm-lock.yaml` as **up to two YAML documents** when
|
|
31
|
+
* the workspace declares `configDependencies` or `devEngines.packageManager`:
|
|
32
|
+
* an env preamble, then the lockfile proper. The rule for telling them apart
|
|
33
|
+
* is **deterministic, not a heuristic**. It is pnpm's own writer contract:
|
|
34
|
+
* `writeEnvLockfile` emits `${env}---${main}`, composing the preamble as a
|
|
35
|
+
* *prefix*, so the preamble is the **first** of two documents and the
|
|
36
|
+
* lockfile is always the **last**. Both documents declare `lockfileVersion`,
|
|
37
|
+
* `importers` and `packages`, so position is the only discriminator. Content
|
|
38
|
+
* cannot tell them apart, which is how a single-document parse once read the
|
|
39
|
+
* preamble as the lockfile and reported an empty workspace.
|
|
33
40
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
41
|
+
* - One document: no preamble; that document is the lockfile.
|
|
42
|
+
* - Two documents: the first is the preamble, the second the lockfile.
|
|
43
|
+
* - More than two: outside the writer contract, so no position identifies
|
|
44
|
+
* either document. Rather than guess (the preamble feeds integrity
|
|
45
|
+
* verification, where a guess is a trust decision), it fails through the
|
|
46
|
+
* typed framing channel with `unexpectedDocuments`.
|
|
38
47
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
48
|
+
* An empty document composes to `null` and occupies its position as
|
|
49
|
+
* `undefined`. pnpm writes an empty *main* document after a preamble both for
|
|
50
|
+
* a workspace with no root `package.json` and only `configDependencies`, and
|
|
51
|
+
* for a workspace whose first install failed after its config dependencies
|
|
52
|
+
* were installed (measured against pnpm 11.28.0 and 12.7.0; the bytes are
|
|
53
|
+
* identical). Whether that is a lockfile is the caller's decision, not the
|
|
54
|
+
* splitter's.
|
|
42
55
|
*
|
|
43
56
|
* @internal
|
|
44
57
|
*/
|
|
45
|
-
const
|
|
58
|
+
const splitPnpmStream = (content) => Effect.gen(function* () {
|
|
46
59
|
const documents = yield* Yaml.parseAll(content).pipe(Effect.mapError(syntaxFailure));
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
|
|
60
|
+
if (documents.length > MAX_PNPM_DOCUMENTS) return yield* Effect.fail(framingFailure("unexpectedDocuments", documents.length));
|
|
61
|
+
const present = (document) => isEmptyDocument(document) ? void 0 : document;
|
|
62
|
+
return documents.length === MAX_PNPM_DOCUMENTS ? {
|
|
63
|
+
preamble: present(documents[0]),
|
|
64
|
+
main: present(documents[1]),
|
|
65
|
+
documents: documents.length
|
|
66
|
+
} : {
|
|
67
|
+
preamble: void 0,
|
|
68
|
+
main: present(documents[0]),
|
|
51
69
|
documents: documents.length
|
|
52
70
|
};
|
|
53
71
|
});
|
|
@@ -75,46 +93,6 @@ const selectSoleDocument = (content) => Effect.gen(function* () {
|
|
|
75
93
|
documents: documents.length
|
|
76
94
|
};
|
|
77
95
|
});
|
|
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
|
-
});
|
|
118
96
|
|
|
119
97
|
//#endregion
|
|
120
|
-
export {
|
|
98
|
+
export { selectSoleDocument, splitPnpmStream };
|
package/internal/pnpm.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { LockfileImporter } from "../LockfileImporter.js";
|
|
2
2
|
import { ResolvedPackage } from "../ResolvedPackage.js";
|
|
3
|
-
import { extractWorkspaceDeps, framingFailure, gatePnpmVersion, importerDependencies, peerDeclarations, splitPeerSuffix, toIntegrityHash, validationFailure } from "./shared.js";
|
|
3
|
+
import { extractWorkspaceDeps, framingFailure, gatePnpmVersion, importerDependencies, peerDeclarations, splitNameVersion, splitPeerSuffix, toIntegrityHash, validationFailure } from "./shared.js";
|
|
4
4
|
import { PnpmExtension } from "../PnpmExtension.js";
|
|
5
|
-
import {
|
|
5
|
+
import { splitPnpmStream } from "./documents.js";
|
|
6
6
|
import { Effect, Schema } from "effect";
|
|
7
7
|
|
|
8
8
|
//#region src/internal/pnpm.ts
|
|
@@ -46,10 +46,22 @@ const PnpmLockfileRaw = Schema.Struct({
|
|
|
46
46
|
*
|
|
47
47
|
* `pnpm-lock.yaml` is a YAML *stream*, not a single document: a workspace
|
|
48
48
|
* using `configDependencies` gets a config-dependencies preamble document
|
|
49
|
-
* ahead of the lockfile. {@link
|
|
50
|
-
* deterministically (it is the last document);
|
|
51
|
-
*
|
|
52
|
-
*
|
|
49
|
+
* ahead of the lockfile. {@link splitPnpmStream} locates the lockfile
|
|
50
|
+
* deterministically (it is the last document); the preamble is never read as
|
|
51
|
+
* the lockfile.
|
|
52
|
+
*
|
|
53
|
+
* An empty main document after a preamble is **ambiguous**: pnpm 11 and 12
|
|
54
|
+
* write exactly those bytes both for a config-dependency-only workspace with
|
|
55
|
+
* no root `package.json` and for a workspace whose first install failed after
|
|
56
|
+
* its config dependencies were installed. The bytes cannot tell the two apart,
|
|
57
|
+
* so the parser does not decide: only when the caller asserts `configOnly`
|
|
58
|
+
* (it knows the root has no `package.json`) does the stream read as an empty
|
|
59
|
+
* lockfile — no packages, importers or workspace edges, versioned by the
|
|
60
|
+
* preamble's `lockfileVersion` after the preamble passes the same version
|
|
61
|
+
* gate. The preamble's own packages are config dependencies, not the
|
|
62
|
+
* workspace's, so none of them is reported. Without the assertion, and always
|
|
63
|
+
* when there is no preamble to vouch for the stream, an empty main document
|
|
64
|
+
* fails `noLockfileDocument`. The flag loosens nothing else.
|
|
53
65
|
*
|
|
54
66
|
* Workspace packages are keyed by importer *path* with version `"0.0.0"`;
|
|
55
67
|
* `Lockfile#withImporterNames` is the explicit second stage that rewrites
|
|
@@ -57,8 +69,18 @@ const PnpmLockfileRaw = Schema.Struct({
|
|
|
57
69
|
*
|
|
58
70
|
* @internal
|
|
59
71
|
*/
|
|
60
|
-
const parsePnpm = (content) => Effect.gen(function* () {
|
|
61
|
-
const { document, documents } = yield*
|
|
72
|
+
const parsePnpm = (content, configOnly) => Effect.gen(function* () {
|
|
73
|
+
const { preamble, main: document, documents } = yield* splitPnpmStream(content);
|
|
74
|
+
if (document === void 0) {
|
|
75
|
+
if (preamble === void 0 || !configOnly) return yield* Effect.fail(framingFailure("noLockfileDocument", documents));
|
|
76
|
+
return {
|
|
77
|
+
lockfileVersion: yield* gatePnpmVersion(preamble),
|
|
78
|
+
packages: [],
|
|
79
|
+
workspaceDependencies: [],
|
|
80
|
+
importers: [],
|
|
81
|
+
extension: PnpmExtension.make({})
|
|
82
|
+
};
|
|
83
|
+
}
|
|
62
84
|
yield* gatePnpmVersion(document);
|
|
63
85
|
const validated = yield* Schema.decodeUnknownEffect(PnpmLockfileRaw)(document).pipe(Effect.mapError(validationFailure));
|
|
64
86
|
if (Object.keys(validated.importers).length === 0) return yield* Effect.fail(framingFailure("noImporters", documents));
|
|
@@ -291,11 +313,9 @@ const toFields = (raw) => Effect.gen(function* () {
|
|
|
291
313
|
}));
|
|
292
314
|
}
|
|
293
315
|
const emit = (instanceId, meta, edges) => Effect.gen(function* () {
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const name = bare.slice(0, atIndex);
|
|
298
|
-
const version = bare.slice(atIndex + 1);
|
|
316
|
+
const split = splitNameVersion(splitPeerSuffix(instanceId).plain);
|
|
317
|
+
if (split === void 0) return;
|
|
318
|
+
const { name, version } = split;
|
|
299
319
|
const integrity = yield* toIntegrityHash(meta?.resolution?.integrity);
|
|
300
320
|
emitted.add(instanceId);
|
|
301
321
|
packages.push(ResolvedPackage.make({
|
package/internal/pnpmEnv.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ConfigDependencyLock } from "../ConfigDependencyLock.js";
|
|
2
2
|
import { gatePnpmVersion, validationFailure } from "./shared.js";
|
|
3
|
-
import {
|
|
3
|
+
import { splitPnpmStream } from "./documents.js";
|
|
4
4
|
import { PackageManagerLock } from "../PackageManagerLock.js";
|
|
5
5
|
import { Effect, Schema } from "effect";
|
|
6
6
|
import { IntegrityHash } from "@effected/npm";
|
|
@@ -59,10 +59,10 @@ const recordedIntegrity = (raw, subject, name, version) => version === "" ? Effe
|
|
|
59
59
|
* cannot disagree about which document is the preamble or what shape it has.
|
|
60
60
|
*/
|
|
61
61
|
const decodePreamble = (content) => Effect.gen(function* () {
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
-
yield* gatePnpmVersion(
|
|
65
|
-
return yield* Schema.decodeUnknownEffect(PnpmEnvRaw)(
|
|
62
|
+
const { preamble } = yield* splitPnpmStream(content);
|
|
63
|
+
if (preamble === void 0) return void 0;
|
|
64
|
+
yield* gatePnpmVersion(preamble);
|
|
65
|
+
return yield* Schema.decodeUnknownEffect(PnpmEnvRaw)(preamble).pipe(Effect.mapError(validationFailure));
|
|
66
66
|
});
|
|
67
67
|
/**
|
|
68
68
|
* Read the package manager pinned by a `pnpm-lock.yaml`'s env preamble.
|
package/internal/shared.js
CHANGED
|
@@ -42,26 +42,66 @@ const toIntegrityHash = (raw) => {
|
|
|
42
42
|
};
|
|
43
43
|
const decodeSpecifier = Schema.decodeUnknownExit(DependencySpecifier.FromString);
|
|
44
44
|
/**
|
|
45
|
-
* Split pnpm's peer-disambiguation suffix off a recorded string:
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* (`"
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
45
|
+
* Split pnpm's peer-disambiguation suffix off a recorded string: the suffix is
|
|
46
|
+
* the maximal TRAILING run of balanced parenthesized groups, found by scanning
|
|
47
|
+
* back from the end, which is `@pnpm/dependency-path`'s own
|
|
48
|
+
* `indexOfDepPathSuffix` rule. Handles both shapes pnpm records — a bare
|
|
49
|
+
* version (`"1.0.0(effect@4.0.0)"`, importer entries) and a `name@version`
|
|
50
|
+
* snapshot key (`"fdir@6.5.0(picomatch@4.0.4)"`) — and nested chains
|
|
51
|
+
* (`"(a@1(b@2))(c@3)"`) as one suffix.
|
|
52
|
+
*
|
|
53
|
+
* pnpm suffixes a `file:` resolution exactly as it suffixes a registry
|
|
54
|
+
* version whenever the package declares peers, directory and tarball alike
|
|
55
|
+
* (`file:vendor/lib(react@18.3.1)`, measured against pnpm 12.6.0), so the rule
|
|
56
|
+
* applies to protocol versions too. The one exception is `link:`, which pnpm
|
|
57
|
+
* never suffixes, even when the target declares peers: a trailing group there
|
|
58
|
+
* is path text and the string passes through untouched.
|
|
59
|
+
*
|
|
60
|
+
* A parenthesis INSIDE a path is followed by more path, so it is never part of
|
|
61
|
+
* the trailing run (`file:vendor/a(b)/c(react@1.0.0)` splits after `c`). A path
|
|
62
|
+
* that itself ENDS in a group is ambiguous, and it is read the way pnpm reads
|
|
63
|
+
* it: pnpm writes `file:vendor/paren(lib)`'s `packages:` key as
|
|
64
|
+
* `parenlib@file:vendor/paren`, so taking the group as suffix is what keeps the
|
|
65
|
+
* snapshot joined to its own metadata. A string that is nothing but groups
|
|
66
|
+
* yields an empty `plain`, and an unbalanced one passes through untouched.
|
|
67
|
+
*
|
|
68
|
+
* This is the single stripping implementation — do not hand-roll a
|
|
69
|
+
* parenthesis split elsewhere.
|
|
55
70
|
*
|
|
56
71
|
* @internal
|
|
57
72
|
*/
|
|
58
73
|
const splitPeerSuffix = (raw) => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
74
|
+
if (raw.startsWith("link:") || !raw.endsWith(")")) return { plain: raw };
|
|
75
|
+
let open = 1;
|
|
76
|
+
for (let i = raw.length - 2; i >= 0; i--) {
|
|
77
|
+
const char = raw[i];
|
|
78
|
+
if (char === "(") open--;
|
|
79
|
+
else if (char === ")") open++;
|
|
80
|
+
else if (open === 0) return {
|
|
81
|
+
plain: raw.slice(0, i + 1),
|
|
82
|
+
peerSuffix: raw.slice(i + 1)
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return open === 0 ? {
|
|
86
|
+
plain: "",
|
|
87
|
+
peerSuffix: raw
|
|
88
|
+
} : { plain: raw };
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Split a `name@version` key at the separator: the first `@` after a scoped
|
|
92
|
+
* name's leading one. A package name holds no other `@`, while the version
|
|
93
|
+
* part may (`file:../@scope/lib`), so the LAST `@` is the wrong boundary.
|
|
94
|
+
* Returns `undefined` for a key with no separator after its first character
|
|
95
|
+
* (`"@"`, `"@scope/"`, a bare name).
|
|
96
|
+
*
|
|
97
|
+
* @internal
|
|
98
|
+
*/
|
|
99
|
+
const splitNameVersion = (key) => {
|
|
100
|
+
const at = key.indexOf("@", 1);
|
|
101
|
+
if (at === -1) return void 0;
|
|
62
102
|
return {
|
|
63
|
-
|
|
64
|
-
|
|
103
|
+
name: key.slice(0, at),
|
|
104
|
+
version: key.slice(at + 1)
|
|
65
105
|
};
|
|
66
106
|
};
|
|
67
107
|
/**
|
|
@@ -204,9 +244,13 @@ const PnpmVersionProbe = Schema.Struct({ lockfileVersion: Schema.Union([Schema.S
|
|
|
204
244
|
* version: decode {@link PnpmVersionProbe}, then {@link requireLockfileVersion}.
|
|
205
245
|
* Call it BEFORE the shape decode, for the reason the probe documents.
|
|
206
246
|
*
|
|
247
|
+
* Succeeds with the gated version as the model spells it (`String` of the
|
|
248
|
+
* recorded value), for a caller that reports a version without decoding the
|
|
249
|
+
* rest of the document.
|
|
250
|
+
*
|
|
207
251
|
* @internal
|
|
208
252
|
*/
|
|
209
|
-
const gatePnpmVersion = (document) => Schema.decodeUnknownEffect(PnpmVersionProbe)(document).pipe(Effect.mapError(validationFailure), Effect.flatMap((probe) => requireLockfileVersion("pnpm", probe.lockfileVersion)));
|
|
253
|
+
const gatePnpmVersion = (document) => Schema.decodeUnknownEffect(PnpmVersionProbe)(document).pipe(Effect.mapError(validationFailure), Effect.flatMap((probe) => requireLockfileVersion("pnpm", probe.lockfileVersion).pipe(Effect.as(String(probe.lockfileVersion)))));
|
|
210
254
|
/** @internal */
|
|
211
255
|
const syntaxFailure = (cause) => ({
|
|
212
256
|
stage: "syntax",
|
|
@@ -255,4 +299,4 @@ const extractWorkspaceDeps = (workspaces, workspaceNames) => {
|
|
|
255
299
|
};
|
|
256
300
|
|
|
257
301
|
//#endregion
|
|
258
|
-
export { DEP_TYPES, MINIMUM_LOCKFILE_VERSION, extractWorkspaceDeps, framingFailure, gatePnpmVersion, importerDependencies, isWorkspaceSpecifier, peerDeclarations, requireLockfileVersion, splitPeerSuffix, syntaxFailure, toIntegrityHash, validationFailure };
|
|
302
|
+
export { DEP_TYPES, MINIMUM_LOCKFILE_VERSION, extractWorkspaceDeps, framingFailure, gatePnpmVersion, importerDependencies, isWorkspaceSpecifier, peerDeclarations, requireLockfileVersion, splitNameVersion, splitPeerSuffix, syntaxFailure, toIntegrityHash, validationFailure };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/lockfiles",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
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": [
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"@effected/jsonc": "^0.13.0",
|
|
44
|
-
"@effected/npm": "^0.
|
|
44
|
+
"@effected/npm": "^0.18.0",
|
|
45
45
|
"@effected/semver": "^0.9.0",
|
|
46
46
|
"@effected/yaml": "^0.17.0",
|
|
47
47
|
"effect": "4.0.0-rc.117"
|