@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/internal/bun.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { BunExtension } from "../BunExtension.js";
|
|
2
|
+
import { LockfileImporter } from "../LockfileImporter.js";
|
|
3
|
+
import { ResolvedPackage } from "../ResolvedPackage.js";
|
|
4
|
+
import { extractWorkspaceDeps, importerDependencies, syntaxFailure, toIntegrityHash, validationFailure } from "./shared.js";
|
|
5
|
+
import { Effect, Schema } from "effect";
|
|
6
|
+
import { Jsonc } from "@effected/jsonc";
|
|
7
|
+
|
|
8
|
+
//#region src/internal/bun.ts
|
|
9
|
+
const DepRecord = Schema.optionalKey(Schema.Record(Schema.String, Schema.String));
|
|
10
|
+
const BunWorkspaceEntry = Schema.Struct({
|
|
11
|
+
name: Schema.optionalKey(Schema.String),
|
|
12
|
+
version: Schema.optionalKey(Schema.String),
|
|
13
|
+
dependencies: DepRecord,
|
|
14
|
+
devDependencies: DepRecord,
|
|
15
|
+
peerDependencies: DepRecord,
|
|
16
|
+
optionalDependencies: DepRecord
|
|
17
|
+
});
|
|
18
|
+
const BunLockfileRaw = Schema.Struct({
|
|
19
|
+
lockfileVersion: Schema.Number,
|
|
20
|
+
workspaces: Schema.optionalKey(Schema.Record(Schema.String, BunWorkspaceEntry)),
|
|
21
|
+
packages: Schema.optionalKey(Schema.Record(Schema.String, Schema.Array(Schema.Unknown))),
|
|
22
|
+
catalog: Schema.optionalKey(Schema.Record(Schema.String, Schema.Unknown)),
|
|
23
|
+
catalogs: Schema.optionalKey(Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Unknown))),
|
|
24
|
+
overrides: Schema.optionalKey(Schema.Record(Schema.String, Schema.String)),
|
|
25
|
+
trustedDependencies: Schema.optionalKey(Schema.Array(Schema.String))
|
|
26
|
+
});
|
|
27
|
+
/**
|
|
28
|
+
* Parse bun `bun.lock` (JSONC) content into the unified field bundle.
|
|
29
|
+
* Resolved packages are tuples whose first element is `"name@version"`;
|
|
30
|
+
* the integrity hash is assumed at tuple index 3 (the permissive v3
|
|
31
|
+
* reading of an under-documented upstream shape).
|
|
32
|
+
*
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
35
|
+
const parseBun = (content) => Effect.gen(function* () {
|
|
36
|
+
const parsed = yield* Jsonc.parse(content).pipe(Effect.mapError(syntaxFailure));
|
|
37
|
+
const validated = yield* Schema.decodeUnknownEffect(BunLockfileRaw)(parsed).pipe(Effect.mapError(validationFailure));
|
|
38
|
+
return yield* toFields(validated);
|
|
39
|
+
});
|
|
40
|
+
const toFields = (raw) => Effect.gen(function* () {
|
|
41
|
+
const packages = [];
|
|
42
|
+
const workspaceNames = /* @__PURE__ */ new Set();
|
|
43
|
+
const workspaceEntries = /* @__PURE__ */ new Map();
|
|
44
|
+
const importers = [];
|
|
45
|
+
if (raw.workspaces) {
|
|
46
|
+
for (const [wsPath, wsEntry] of Object.entries(raw.workspaces)) importers.push(LockfileImporter.make({
|
|
47
|
+
path: wsPath === "" ? "." : wsPath,
|
|
48
|
+
dependencies: importerDependencies(wsEntry, (specifier) => ({ specifier }))
|
|
49
|
+
}));
|
|
50
|
+
for (const [wsPath, wsEntry] of Object.entries(raw.workspaces)) {
|
|
51
|
+
if (wsPath === "") continue;
|
|
52
|
+
const name = wsEntry.name === void 0 || wsEntry.name === "" ? wsPath : wsEntry.name;
|
|
53
|
+
workspaceNames.add(name);
|
|
54
|
+
packages.push(ResolvedPackage.make({
|
|
55
|
+
name,
|
|
56
|
+
version: wsEntry.version ?? "0.0.0",
|
|
57
|
+
isWorkspace: true,
|
|
58
|
+
relativePath: wsPath
|
|
59
|
+
}));
|
|
60
|
+
workspaceEntries.set(name, {
|
|
61
|
+
...wsEntry.dependencies ? { dependencies: wsEntry.dependencies } : {},
|
|
62
|
+
...wsEntry.devDependencies ? { devDependencies: wsEntry.devDependencies } : {},
|
|
63
|
+
...wsEntry.peerDependencies ? { peerDependencies: wsEntry.peerDependencies } : {},
|
|
64
|
+
...wsEntry.optionalDependencies ? { optionalDependencies: wsEntry.optionalDependencies } : {}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (raw.packages) for (const tuple of Object.values(raw.packages)) {
|
|
69
|
+
if (tuple.length < 1) continue;
|
|
70
|
+
const first = tuple[0];
|
|
71
|
+
if (typeof first !== "string") continue;
|
|
72
|
+
const atIdx = first.lastIndexOf("@");
|
|
73
|
+
if (atIdx <= 0) continue;
|
|
74
|
+
const name = first.slice(0, atIdx);
|
|
75
|
+
const version = first.slice(atIdx + 1);
|
|
76
|
+
if (workspaceNames.has(name)) continue;
|
|
77
|
+
const integrity = yield* toIntegrityHash(tuple.length >= 4 && typeof tuple[3] === "string" ? tuple[3] : void 0);
|
|
78
|
+
packages.push(ResolvedPackage.make({
|
|
79
|
+
name,
|
|
80
|
+
version,
|
|
81
|
+
...integrity !== void 0 ? { integrity } : {},
|
|
82
|
+
isWorkspace: false
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
const workspaceDependencies = extractWorkspaceDeps(workspaceEntries, workspaceNames);
|
|
86
|
+
const extension = BunExtension.make({
|
|
87
|
+
...raw.catalog !== void 0 ? { catalog: raw.catalog } : {},
|
|
88
|
+
...raw.catalogs !== void 0 ? { catalogs: raw.catalogs } : {},
|
|
89
|
+
...raw.overrides !== void 0 ? { overrides: raw.overrides } : {},
|
|
90
|
+
...raw.trustedDependencies !== void 0 ? { trustedDependencies: raw.trustedDependencies } : {}
|
|
91
|
+
});
|
|
92
|
+
return {
|
|
93
|
+
lockfileVersion: String(raw.lockfileVersion),
|
|
94
|
+
packages,
|
|
95
|
+
workspaceDependencies,
|
|
96
|
+
importers,
|
|
97
|
+
extension
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
//#endregion
|
|
102
|
+
export { parseBun };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { framingFailure, syntaxFailure } from "./shared.js";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { Yaml } from "@effected/yaml";
|
|
4
|
+
|
|
5
|
+
//#region src/internal/documents.ts
|
|
6
|
+
/**
|
|
7
|
+
* An empty YAML document composes to `null` (`Yaml.parseAll("")` is `[null]`,
|
|
8
|
+
* and the trailing document of an env-only `pnpm-lock.yaml` is `null` too).
|
|
9
|
+
* A *present* scalar — `42` — is not empty; it is a shape error, and must
|
|
10
|
+
* reach validation rather than be reported as a framing failure.
|
|
11
|
+
*
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
const isEmptyDocument = (document) => document === null || document === void 0;
|
|
15
|
+
/**
|
|
16
|
+
* Select the lockfile document from a `pnpm-lock.yaml` YAML stream.
|
|
17
|
+
*
|
|
18
|
+
* @remarks
|
|
19
|
+
* pnpm 11 writes `pnpm-lock.yaml` as **up to two YAML documents** when the
|
|
20
|
+
* workspace uses `configDependencies`: an optional config-dependencies
|
|
21
|
+
* ("env") preamble, then the lockfile proper. The rule for picking the right
|
|
22
|
+
* one is **deterministic, not a heuristic** — it is pnpm's own writer
|
|
23
|
+
* contract. `writeEnvLockfile` emits `${env}---${main}`, composing the
|
|
24
|
+
* preamble as a *prefix*, and `extractMainDocument` reads back everything
|
|
25
|
+
* after the first separator. So the preamble is always first and the lockfile
|
|
26
|
+
* is always **last**.
|
|
27
|
+
*
|
|
28
|
+
* Both documents declare `lockfileVersion`, `importers` and `packages`, so
|
|
29
|
+
* they are not told apart by which keys they carry — only by position. That
|
|
30
|
+
* is exactly why the previous single-document assumption failed *silently*:
|
|
31
|
+
* the preamble validates against the pnpm schema, yielding a `Lockfile`
|
|
32
|
+
* reporting one package and an empty workspace instead of an error.
|
|
33
|
+
*
|
|
34
|
+
* An env-only lockfile (`---` preamble `---` and nothing after it, which
|
|
35
|
+
* pnpm writes when there is no main lockfile yet) has an **empty** trailing
|
|
36
|
+
* document. pnpm reads that as "no lockfile"; so do we, through the typed
|
|
37
|
+
* framing failure — never by silently falling back to the preamble.
|
|
38
|
+
*
|
|
39
|
+
* @internal
|
|
40
|
+
*/
|
|
41
|
+
const selectPnpmDocument = (content) => Effect.gen(function* () {
|
|
42
|
+
const documents = yield* Yaml.parseAll(content).pipe(Effect.mapError(syntaxFailure));
|
|
43
|
+
const document = documents.at(-1);
|
|
44
|
+
if (documents.length === 0 || isEmptyDocument(document)) return yield* Effect.fail(framingFailure("noLockfileDocument", documents.length));
|
|
45
|
+
return {
|
|
46
|
+
document,
|
|
47
|
+
documents: documents.length
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
/**
|
|
51
|
+
* Select the sole document of a YAML lockfile format that defines **no**
|
|
52
|
+
* document framing — yarn Berry's `yarn.lock`.
|
|
53
|
+
*
|
|
54
|
+
* @remarks
|
|
55
|
+
* yarn never writes a multi-document `yarn.lock`, so there is no writer
|
|
56
|
+
* contract to read a "main" document out of one. Rather than silently taking
|
|
57
|
+
* the first document (which is what a single-document parse does, and how the
|
|
58
|
+
* pnpm bug stayed invisible), a stream carrying more than one document fails
|
|
59
|
+
* through the typed framing channel: we refuse to guess where the format
|
|
60
|
+
* defines no rule.
|
|
61
|
+
*
|
|
62
|
+
* @internal
|
|
63
|
+
*/
|
|
64
|
+
const selectSoleDocument = (content) => Effect.gen(function* () {
|
|
65
|
+
const documents = yield* Yaml.parseAll(content).pipe(Effect.mapError(syntaxFailure));
|
|
66
|
+
if (documents.length > 1) return yield* Effect.fail(framingFailure("unexpectedDocuments", documents.length));
|
|
67
|
+
const document = documents.at(0);
|
|
68
|
+
if (documents.length === 0 || isEmptyDocument(document)) return yield* Effect.fail(framingFailure("noLockfileDocument", documents.length));
|
|
69
|
+
return {
|
|
70
|
+
document,
|
|
71
|
+
documents: documents.length
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
//#endregion
|
|
76
|
+
export { selectPnpmDocument, selectSoleDocument };
|
package/internal/npm.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { LockfileImporter } from "../LockfileImporter.js";
|
|
2
|
+
import { ResolvedPackage } from "../ResolvedPackage.js";
|
|
3
|
+
import { extractWorkspaceDeps, importerDependencies, syntaxFailure, toIntegrityHash, validationFailure } from "./shared.js";
|
|
4
|
+
import { Effect, Schema } from "effect";
|
|
5
|
+
|
|
6
|
+
//#region src/internal/npm.ts
|
|
7
|
+
const DepRecord = Schema.optionalKey(Schema.Record(Schema.String, Schema.String));
|
|
8
|
+
const NpmPackageEntry = Schema.Struct({
|
|
9
|
+
name: Schema.optionalKey(Schema.String),
|
|
10
|
+
version: Schema.optionalKey(Schema.String),
|
|
11
|
+
resolved: Schema.optionalKey(Schema.String),
|
|
12
|
+
integrity: Schema.optionalKey(Schema.String),
|
|
13
|
+
link: Schema.optionalKey(Schema.Boolean),
|
|
14
|
+
dev: Schema.optionalKey(Schema.Boolean),
|
|
15
|
+
dependencies: DepRecord,
|
|
16
|
+
devDependencies: DepRecord,
|
|
17
|
+
peerDependencies: DepRecord,
|
|
18
|
+
optionalDependencies: DepRecord
|
|
19
|
+
});
|
|
20
|
+
const NpmLockfileRaw = Schema.Struct({
|
|
21
|
+
name: Schema.optionalKey(Schema.String),
|
|
22
|
+
version: Schema.optionalKey(Schema.String),
|
|
23
|
+
lockfileVersion: Schema.Union([Schema.Number, Schema.String]),
|
|
24
|
+
requires: Schema.optionalKey(Schema.Boolean),
|
|
25
|
+
packages: Schema.Record(Schema.String, NpmPackageEntry)
|
|
26
|
+
});
|
|
27
|
+
const NODE_MODULES_PREFIX = "node_modules/";
|
|
28
|
+
/**
|
|
29
|
+
* Parse npm `package-lock.json` (v2/v3) content into the unified field
|
|
30
|
+
* bundle. Native `JSON.parse` inside `Effect.try` — its throw on hostile
|
|
31
|
+
* input (including V8's `RangeError` on pathological depth) lands typed as
|
|
32
|
+
* `stage: "syntax"`.
|
|
33
|
+
*
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
const parseNpm = (content) => Effect.gen(function* () {
|
|
37
|
+
const raw = yield* Effect.try({
|
|
38
|
+
try: () => JSON.parse(content),
|
|
39
|
+
catch: syntaxFailure
|
|
40
|
+
});
|
|
41
|
+
const validated = yield* Schema.decodeUnknownEffect(NpmLockfileRaw)(raw).pipe(Effect.mapError(validationFailure));
|
|
42
|
+
return yield* toFields(validated);
|
|
43
|
+
});
|
|
44
|
+
const toFields = (raw) => Effect.gen(function* () {
|
|
45
|
+
const packages = [];
|
|
46
|
+
const workspaceNames = /* @__PURE__ */ new Set();
|
|
47
|
+
const workspaceEntries = /* @__PURE__ */ new Map();
|
|
48
|
+
const importers = [];
|
|
49
|
+
const rootEntry = raw.packages[""];
|
|
50
|
+
if (rootEntry) importers.push(LockfileImporter.make({
|
|
51
|
+
path: ".",
|
|
52
|
+
dependencies: importerDependencies(rootEntry, (s) => ({ specifier: s }))
|
|
53
|
+
}));
|
|
54
|
+
for (const [key, entry] of Object.entries(raw.packages)) if (key.startsWith(NODE_MODULES_PREFIX) && entry.link === true) {
|
|
55
|
+
const name = (entry.resolved !== void 0 ? raw.packages[entry.resolved] : void 0)?.name ?? entry.name ?? key.slice(13);
|
|
56
|
+
if (name !== "") workspaceNames.add(name);
|
|
57
|
+
}
|
|
58
|
+
for (const [key, entry] of Object.entries(raw.packages)) {
|
|
59
|
+
if (key === "") continue;
|
|
60
|
+
if (key.startsWith(NODE_MODULES_PREFIX) && entry.link === true) {
|
|
61
|
+
const resolved = entry.resolved;
|
|
62
|
+
const wsEntry = resolved !== void 0 ? raw.packages[resolved] : void 0;
|
|
63
|
+
const name = wsEntry?.name ?? entry.name ?? key.slice(13);
|
|
64
|
+
if (name === "") continue;
|
|
65
|
+
packages.push(ResolvedPackage.make({
|
|
66
|
+
name,
|
|
67
|
+
version: wsEntry?.version ?? "0.0.0",
|
|
68
|
+
isWorkspace: true,
|
|
69
|
+
...resolved !== void 0 ? { relativePath: resolved } : {}
|
|
70
|
+
}));
|
|
71
|
+
if (wsEntry) workspaceEntries.set(name, {
|
|
72
|
+
...wsEntry.dependencies ? { dependencies: wsEntry.dependencies } : {},
|
|
73
|
+
...wsEntry.devDependencies ? { devDependencies: wsEntry.devDependencies } : {},
|
|
74
|
+
...wsEntry.peerDependencies ? { peerDependencies: wsEntry.peerDependencies } : {},
|
|
75
|
+
...wsEntry.optionalDependencies ? { optionalDependencies: wsEntry.optionalDependencies } : {}
|
|
76
|
+
});
|
|
77
|
+
if (resolved !== void 0 && resolved !== "") importers.push(LockfileImporter.make({
|
|
78
|
+
path: resolved,
|
|
79
|
+
dependencies: wsEntry ? importerDependencies(wsEntry, (s) => ({ specifier: s })) : []
|
|
80
|
+
}));
|
|
81
|
+
} else if (key.startsWith(NODE_MODULES_PREFIX)) {
|
|
82
|
+
const name = key.slice(13);
|
|
83
|
+
if (name !== "" && entry.version !== void 0) {
|
|
84
|
+
const integrity = yield* toIntegrityHash(entry.integrity);
|
|
85
|
+
packages.push(ResolvedPackage.make({
|
|
86
|
+
name,
|
|
87
|
+
version: entry.version,
|
|
88
|
+
...integrity !== void 0 ? { integrity } : {},
|
|
89
|
+
isWorkspace: false,
|
|
90
|
+
dependencies: entry.dependencies ?? {}
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const workspaceDependencies = extractWorkspaceDeps(workspaceEntries, workspaceNames);
|
|
96
|
+
return {
|
|
97
|
+
lockfileVersion: String(raw.lockfileVersion),
|
|
98
|
+
packages,
|
|
99
|
+
workspaceDependencies,
|
|
100
|
+
importers
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
//#endregion
|
|
105
|
+
export { parseNpm };
|
package/internal/pnpm.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { LockfileImporter } from "../LockfileImporter.js";
|
|
2
|
+
import { ResolvedPackage } from "../ResolvedPackage.js";
|
|
3
|
+
import { extractWorkspaceDeps, framingFailure, importerDependencies, toIntegrityHash, validationFailure } from "./shared.js";
|
|
4
|
+
import { PnpmExtension } from "../PnpmExtension.js";
|
|
5
|
+
import { selectPnpmDocument } from "./documents.js";
|
|
6
|
+
import { Effect, Schema } from "effect";
|
|
7
|
+
|
|
8
|
+
//#region src/internal/pnpm.ts
|
|
9
|
+
const PnpmImporterDeps = Schema.optionalKey(Schema.Record(Schema.String, Schema.Struct({
|
|
10
|
+
specifier: Schema.String,
|
|
11
|
+
version: Schema.String
|
|
12
|
+
})));
|
|
13
|
+
const PnpmImporter = Schema.Struct({
|
|
14
|
+
dependencies: PnpmImporterDeps,
|
|
15
|
+
devDependencies: PnpmImporterDeps,
|
|
16
|
+
peerDependencies: PnpmImporterDeps,
|
|
17
|
+
optionalDependencies: PnpmImporterDeps
|
|
18
|
+
});
|
|
19
|
+
const PnpmLockfileRaw = Schema.Struct({
|
|
20
|
+
lockfileVersion: Schema.Union([Schema.String, Schema.Number]),
|
|
21
|
+
settings: Schema.optionalKey(Schema.Struct({
|
|
22
|
+
autoInstallPeers: Schema.optionalKey(Schema.Boolean),
|
|
23
|
+
excludeLinksFromLockfile: Schema.optionalKey(Schema.Boolean)
|
|
24
|
+
})),
|
|
25
|
+
overrides: Schema.optionalKey(Schema.Record(Schema.String, Schema.String)),
|
|
26
|
+
catalogs: Schema.optionalKey(Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Union([Schema.String, Schema.Struct({
|
|
27
|
+
specifier: Schema.String,
|
|
28
|
+
version: Schema.String
|
|
29
|
+
})])))),
|
|
30
|
+
importers: Schema.Record(Schema.String, PnpmImporter),
|
|
31
|
+
packages: Schema.optionalKey(Schema.Record(Schema.String, Schema.Struct({ resolution: Schema.optionalKey(Schema.Struct({ integrity: Schema.optionalKey(Schema.String) })) })))
|
|
32
|
+
});
|
|
33
|
+
/**
|
|
34
|
+
* Parse pnpm `pnpm-lock.yaml` content into the unified field bundle.
|
|
35
|
+
*
|
|
36
|
+
* `pnpm-lock.yaml` is a YAML *stream*, not a single document: a workspace
|
|
37
|
+
* using `configDependencies` gets a config-dependencies preamble document
|
|
38
|
+
* ahead of the lockfile. {@link selectPnpmDocument} locates the lockfile
|
|
39
|
+
* deterministically (it is the last document); a stream carrying no lockfile
|
|
40
|
+
* document fails through the typed framing channel rather than silently
|
|
41
|
+
* reporting the preamble as an empty workspace.
|
|
42
|
+
*
|
|
43
|
+
* Workspace packages are keyed by importer *path* with version `"0.0.0"`;
|
|
44
|
+
* `Lockfile#withImporterNames` is the explicit second stage that rewrites
|
|
45
|
+
* them to real names.
|
|
46
|
+
*
|
|
47
|
+
* @internal
|
|
48
|
+
*/
|
|
49
|
+
const parsePnpm = (content) => Effect.gen(function* () {
|
|
50
|
+
const { document, documents } = yield* selectPnpmDocument(content);
|
|
51
|
+
const validated = yield* Schema.decodeUnknownEffect(PnpmLockfileRaw)(document).pipe(Effect.mapError(validationFailure));
|
|
52
|
+
if (Object.keys(validated.importers).length === 0) return yield* Effect.fail(framingFailure("noImporters", documents));
|
|
53
|
+
return yield* toFields(validated);
|
|
54
|
+
});
|
|
55
|
+
const toVersionMap = (deps) => {
|
|
56
|
+
if (!deps) return void 0;
|
|
57
|
+
return Object.fromEntries(Object.entries(deps).map(([name, info]) => [name, info.specifier]));
|
|
58
|
+
};
|
|
59
|
+
const importerDepGroups = (importer) => [
|
|
60
|
+
importer.dependencies,
|
|
61
|
+
importer.devDependencies,
|
|
62
|
+
importer.peerDependencies,
|
|
63
|
+
importer.optionalDependencies
|
|
64
|
+
];
|
|
65
|
+
const toFields = (raw) => Effect.gen(function* () {
|
|
66
|
+
const workspaceEntries = /* @__PURE__ */ new Map();
|
|
67
|
+
const workspaceNames = /* @__PURE__ */ new Set();
|
|
68
|
+
const importers = [];
|
|
69
|
+
for (const [importerPath, importer] of Object.entries(raw.importers)) {
|
|
70
|
+
if (importerPath === "") continue;
|
|
71
|
+
importers.push(LockfileImporter.make({
|
|
72
|
+
path: importerPath,
|
|
73
|
+
dependencies: importerDependencies(importer, (info) => ({
|
|
74
|
+
specifier: info.specifier,
|
|
75
|
+
version: info.version
|
|
76
|
+
}))
|
|
77
|
+
}));
|
|
78
|
+
const deps = toVersionMap(importer.dependencies);
|
|
79
|
+
const devDeps = toVersionMap(importer.devDependencies);
|
|
80
|
+
const peerDeps = toVersionMap(importer.peerDependencies);
|
|
81
|
+
const optDeps = toVersionMap(importer.optionalDependencies);
|
|
82
|
+
workspaceEntries.set(importerPath, {
|
|
83
|
+
...deps ? { dependencies: deps } : {},
|
|
84
|
+
...devDeps ? { devDependencies: devDeps } : {},
|
|
85
|
+
...peerDeps ? { peerDependencies: peerDeps } : {},
|
|
86
|
+
...optDeps ? { optionalDependencies: optDeps } : {}
|
|
87
|
+
});
|
|
88
|
+
for (const group of importerDepGroups(importer)) {
|
|
89
|
+
if (!group) continue;
|
|
90
|
+
for (const [name, info] of Object.entries(group)) if (name !== "" && info.version.startsWith("link:")) workspaceNames.add(name);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
for (const path of Object.keys(raw.importers)) if (path !== "." && path !== "") workspaceNames.add(path);
|
|
94
|
+
const packages = [];
|
|
95
|
+
for (const importerPath of Object.keys(raw.importers)) {
|
|
96
|
+
if (importerPath === "." || importerPath === "") continue;
|
|
97
|
+
packages.push(ResolvedPackage.make({
|
|
98
|
+
name: importerPath,
|
|
99
|
+
version: "0.0.0",
|
|
100
|
+
isWorkspace: true,
|
|
101
|
+
relativePath: importerPath
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
if (raw.packages) for (const [key, pkg] of Object.entries(raw.packages)) {
|
|
105
|
+
const parenIndex = key.indexOf("(");
|
|
106
|
+
const bare = parenIndex === -1 ? key : key.slice(0, parenIndex);
|
|
107
|
+
const atIndex = bare.lastIndexOf("@");
|
|
108
|
+
if (atIndex <= 0) continue;
|
|
109
|
+
const name = bare.slice(0, atIndex);
|
|
110
|
+
const version = bare.slice(atIndex + 1);
|
|
111
|
+
const integrity = yield* toIntegrityHash(pkg.resolution?.integrity);
|
|
112
|
+
packages.push(ResolvedPackage.make({
|
|
113
|
+
name,
|
|
114
|
+
version,
|
|
115
|
+
...integrity !== void 0 ? { integrity } : {},
|
|
116
|
+
isWorkspace: false
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
const workspaceDependencies = extractWorkspaceDeps(workspaceEntries, workspaceNames);
|
|
120
|
+
const extension = PnpmExtension.make({
|
|
121
|
+
...raw.catalogs !== void 0 ? { catalogs: raw.catalogs } : {},
|
|
122
|
+
...raw.overrides !== void 0 ? { overrides: raw.overrides } : {},
|
|
123
|
+
...raw.settings !== void 0 ? { settings: raw.settings } : {}
|
|
124
|
+
});
|
|
125
|
+
return {
|
|
126
|
+
lockfileVersion: String(raw.lockfileVersion),
|
|
127
|
+
packages,
|
|
128
|
+
workspaceDependencies,
|
|
129
|
+
importers,
|
|
130
|
+
extension
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
//#endregion
|
|
135
|
+
export { parsePnpm };
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { ImporterDependency } from "../ImporterDependency.js";
|
|
2
|
+
import { WorkspaceDependency } from "../WorkspaceDependency.js";
|
|
3
|
+
import { Effect, Exit, Schema } from "effect";
|
|
4
|
+
import { DependencySpecifier, IntegrityHash } from "@effected/npm";
|
|
5
|
+
|
|
6
|
+
//#region src/internal/shared.ts
|
|
7
|
+
/**
|
|
8
|
+
* The four dependency sections of a manifest, in a stable order — the shared
|
|
9
|
+
* dependency-sections table (v3's `DEP_SECTIONS`). Each entry is both the
|
|
10
|
+
* manifest field name to read and the `@effected/npm` `DependencyField` it maps
|
|
11
|
+
* to, since the two coincide. Consumed by `extractWorkspaceDeps` and by the
|
|
12
|
+
* pnpm/bun/npm importer builders.
|
|
13
|
+
*
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
const DEP_TYPES = [
|
|
17
|
+
"dependencies",
|
|
18
|
+
"devDependencies",
|
|
19
|
+
"peerDependencies",
|
|
20
|
+
"optionalDependencies"
|
|
21
|
+
];
|
|
22
|
+
/**
|
|
23
|
+
* Coerce a raw lockfile integrity string to the `@effected/npm` `IntegrityHash`
|
|
24
|
+
* brand, which recognizes the SRI (`<algo>-<base64>`), corepack (`<algo>.<hex>`)
|
|
25
|
+
* and yarn (`<cachekey>/<hex>`) textual forms.
|
|
26
|
+
*
|
|
27
|
+
* Absence and corruption are treated differently, and the distinction is the
|
|
28
|
+
* point. An *absent* integrity (input `undefined`) succeeds with `undefined`,
|
|
29
|
+
* so the caller omits the field — a lockfile that records no integrity is
|
|
30
|
+
* legitimate. A *present but unparseable* integrity fails through the same
|
|
31
|
+
* validation channel the shape checks use ({@link validationFailure}), so
|
|
32
|
+
* `Lockfile.parse` surfaces it as a `LockfileParseError` with
|
|
33
|
+
* `stage: "validation"` rather than silently dropping a corrupt value. Never a
|
|
34
|
+
* defect. yarn's `10c0/<hex>` cache checksums are a recognized form, so real
|
|
35
|
+
* yarn/npm/pnpm/bun integrity all still parses; only genuine corruption fails.
|
|
36
|
+
*
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
const toIntegrityHash = (raw) => {
|
|
40
|
+
if (raw === void 0) return Effect.succeed(void 0);
|
|
41
|
+
return Schema.decodeUnknownEffect(IntegrityHash)(raw).pipe(Effect.mapError(validationFailure));
|
|
42
|
+
};
|
|
43
|
+
const decodeSpecifier = Schema.decodeUnknownExit(DependencySpecifier.FromString);
|
|
44
|
+
/**
|
|
45
|
+
* Build one {@link ImporterDependency}, decoding the raw specifier string into
|
|
46
|
+
* the `@effected/npm` `ClassifiedSpecifier` tagged union. Returns `undefined`
|
|
47
|
+
* — a skip, never a throw — when the name is empty or the specifier does not
|
|
48
|
+
* classify (e.g. an empty specifier), per the total-string-surgery discipline.
|
|
49
|
+
*
|
|
50
|
+
* @internal
|
|
51
|
+
*/
|
|
52
|
+
const buildImporterDependency = (name, specifier, depType, version) => {
|
|
53
|
+
if (name === "") return void 0;
|
|
54
|
+
const exit = decodeSpecifier(specifier);
|
|
55
|
+
if (Exit.isFailure(exit)) return void 0;
|
|
56
|
+
return ImporterDependency.make({
|
|
57
|
+
name,
|
|
58
|
+
specifier: exit.value,
|
|
59
|
+
depType,
|
|
60
|
+
...version !== void 0 && version !== "" ? { version } : {}
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Collect an importer's declared dependencies off the shared dependency-sections
|
|
65
|
+
* table. Iterates {@link DEP_TYPES} in order, projecting each section value with
|
|
66
|
+
* `read`; malformed rows are skipped (never thrown). Key-bearing intermediates
|
|
67
|
+
* are the schema-decoded records, whose own-property `Object.entries` iteration
|
|
68
|
+
* neither pollutes nor drops a `__proto__` key.
|
|
69
|
+
*
|
|
70
|
+
* @internal
|
|
71
|
+
*/
|
|
72
|
+
const importerDependencies = (entry, read) => {
|
|
73
|
+
const deps = [];
|
|
74
|
+
for (const field of DEP_TYPES) {
|
|
75
|
+
const section = entry[field];
|
|
76
|
+
if (!section) continue;
|
|
77
|
+
for (const [name, value] of Object.entries(section)) {
|
|
78
|
+
const { specifier, version } = read(value);
|
|
79
|
+
const dep = buildImporterDependency(name, specifier, field, version);
|
|
80
|
+
if (dep !== void 0) deps.push(dep);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return deps;
|
|
84
|
+
};
|
|
85
|
+
/** @internal */
|
|
86
|
+
const syntaxFailure = (cause) => ({
|
|
87
|
+
stage: "syntax",
|
|
88
|
+
cause
|
|
89
|
+
});
|
|
90
|
+
/** @internal */
|
|
91
|
+
const validationFailure = (cause) => ({
|
|
92
|
+
stage: "validation",
|
|
93
|
+
cause
|
|
94
|
+
});
|
|
95
|
+
/** @internal */
|
|
96
|
+
const framingFailure = (reason, documents) => ({
|
|
97
|
+
stage: "framing",
|
|
98
|
+
reason,
|
|
99
|
+
documents
|
|
100
|
+
});
|
|
101
|
+
/**
|
|
102
|
+
* Whether the specifier is a workspace, link or file reference
|
|
103
|
+
* (`"workspace:*"`, `"link:../foo"`, `"file:../bar"`).
|
|
104
|
+
*
|
|
105
|
+
* @internal
|
|
106
|
+
*/
|
|
107
|
+
const isWorkspaceSpecifier = (specifier) => specifier.startsWith("workspace:") || specifier.startsWith("link:") || specifier.startsWith("file:");
|
|
108
|
+
/**
|
|
109
|
+
* Extract inter-workspace dependency edges: for every workspace entry and
|
|
110
|
+
* dependency type, emit an edge for each dependency whose name is itself a
|
|
111
|
+
* workspace. Key-bearing intermediates are `Map`/`Set` — lockfile keys are
|
|
112
|
+
* attacker-adjacent strings (`__proto__`, `constructor`) and must never be
|
|
113
|
+
* assigned onto plain objects here.
|
|
114
|
+
*
|
|
115
|
+
* @internal
|
|
116
|
+
*/
|
|
117
|
+
const extractWorkspaceDeps = (workspaces, workspaceNames) => {
|
|
118
|
+
const deps = [];
|
|
119
|
+
for (const [from, entry] of workspaces) for (const depType of DEP_TYPES) {
|
|
120
|
+
const depMap = entry[depType];
|
|
121
|
+
if (!depMap) continue;
|
|
122
|
+
for (const [name, constraint] of Object.entries(depMap)) if (workspaceNames.has(name)) deps.push(WorkspaceDependency.make({
|
|
123
|
+
from,
|
|
124
|
+
to: name,
|
|
125
|
+
depType,
|
|
126
|
+
constraint
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
return deps;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
//#endregion
|
|
133
|
+
export { DEP_TYPES, extractWorkspaceDeps, framingFailure, importerDependencies, isWorkspaceSpecifier, syntaxFailure, toIntegrityHash, validationFailure };
|
package/internal/yarn.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { ResolvedPackage } from "../ResolvedPackage.js";
|
|
2
|
+
import { extractWorkspaceDeps, toIntegrityHash, validationFailure } from "./shared.js";
|
|
3
|
+
import { selectSoleDocument } from "./documents.js";
|
|
4
|
+
import { Effect, Schema } from "effect";
|
|
5
|
+
|
|
6
|
+
//#region src/internal/yarn.ts
|
|
7
|
+
const YarnLockfileRaw = Schema.Record(Schema.String, Schema.Unknown);
|
|
8
|
+
const DepRecord = Schema.optionalKey(Schema.Record(Schema.String, Schema.String));
|
|
9
|
+
const YarnEntry = Schema.Struct({
|
|
10
|
+
version: Schema.optionalKey(Schema.String),
|
|
11
|
+
resolution: Schema.optionalKey(Schema.String),
|
|
12
|
+
dependencies: DepRecord,
|
|
13
|
+
devDependencies: DepRecord,
|
|
14
|
+
peerDependencies: DepRecord,
|
|
15
|
+
optionalDependencies: DepRecord,
|
|
16
|
+
checksum: Schema.optionalKey(Schema.String),
|
|
17
|
+
languageName: Schema.optionalKey(Schema.String),
|
|
18
|
+
linkType: Schema.optionalKey(Schema.String),
|
|
19
|
+
bin: Schema.optionalKey(Schema.Unknown)
|
|
20
|
+
});
|
|
21
|
+
const YarnMetadata = Schema.Struct({ version: Schema.optionalKey(Schema.Union([Schema.String, Schema.Number])) });
|
|
22
|
+
/**
|
|
23
|
+
* Parse yarn Berry `yarn.lock` content into the unified field bundle.
|
|
24
|
+
*
|
|
25
|
+
* Yarn Berry lockfiles are YAML with a flat key structure where each key
|
|
26
|
+
* encodes package name + resolution descriptor(s) (e.g.
|
|
27
|
+
* `"@scope/name@npm:^1.0.0"`); workspace entries carry `linkType: "soft"`.
|
|
28
|
+
*
|
|
29
|
+
* @internal
|
|
30
|
+
*/
|
|
31
|
+
const parseYarn = (content) => Effect.gen(function* () {
|
|
32
|
+
const { document } = yield* selectSoleDocument(content);
|
|
33
|
+
const raw = yield* Schema.decodeUnknownEffect(YarnLockfileRaw)(document).pipe(Effect.mapError(validationFailure));
|
|
34
|
+
const metadata = raw.__metadata === void 0 ? void 0 : yield* Schema.decodeUnknownEffect(YarnMetadata)(raw.__metadata).pipe(Effect.mapError(validationFailure));
|
|
35
|
+
const lockfileVersion = metadata?.version === void 0 ? "unknown" : String(metadata.version);
|
|
36
|
+
const decoded = /* @__PURE__ */ new Map();
|
|
37
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
38
|
+
if (key === "__metadata") continue;
|
|
39
|
+
const entry = yield* Schema.decodeUnknownEffect(YarnEntry)(value).pipe(Effect.mapError(validationFailure));
|
|
40
|
+
decoded.set(key, entry);
|
|
41
|
+
}
|
|
42
|
+
return yield* toFields(lockfileVersion, decoded);
|
|
43
|
+
});
|
|
44
|
+
const toFields = (lockfileVersion, decoded) => Effect.gen(function* () {
|
|
45
|
+
const packages = [];
|
|
46
|
+
const workspaceNames = /* @__PURE__ */ new Set();
|
|
47
|
+
const workspaceEntries = /* @__PURE__ */ new Map();
|
|
48
|
+
for (const [key, entry] of decoded) if (entry.linkType === "soft") {
|
|
49
|
+
const name = extractYarnPackageName(key);
|
|
50
|
+
if (name !== void 0) workspaceNames.add(name);
|
|
51
|
+
}
|
|
52
|
+
for (const [key, entry] of decoded) {
|
|
53
|
+
const name = extractYarnPackageName(key);
|
|
54
|
+
if (name === void 0) continue;
|
|
55
|
+
const isWorkspace = entry.linkType === "soft";
|
|
56
|
+
const relativePath = isWorkspace ? extractYarnWorkspacePath(key) : void 0;
|
|
57
|
+
const integrity = yield* toIntegrityHash(entry.checksum);
|
|
58
|
+
packages.push(ResolvedPackage.make({
|
|
59
|
+
name,
|
|
60
|
+
version: entry.version ?? "0.0.0",
|
|
61
|
+
...integrity !== void 0 ? { integrity } : {},
|
|
62
|
+
isWorkspace,
|
|
63
|
+
...relativePath !== void 0 ? { relativePath } : {}
|
|
64
|
+
}));
|
|
65
|
+
if (isWorkspace) {
|
|
66
|
+
const deps = cleanYarnDeps(entry.dependencies);
|
|
67
|
+
const devDeps = cleanYarnDeps(entry.devDependencies);
|
|
68
|
+
const peerDeps = cleanYarnDeps(entry.peerDependencies);
|
|
69
|
+
const optDeps = cleanYarnDeps(entry.optionalDependencies);
|
|
70
|
+
workspaceEntries.set(name, {
|
|
71
|
+
...deps ? { dependencies: deps } : {},
|
|
72
|
+
...devDeps ? { devDependencies: devDeps } : {},
|
|
73
|
+
...peerDeps ? { peerDependencies: peerDeps } : {},
|
|
74
|
+
...optDeps ? { optionalDependencies: optDeps } : {}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
lockfileVersion,
|
|
80
|
+
packages,
|
|
81
|
+
workspaceDependencies: extractWorkspaceDeps(workspaceEntries, workspaceNames),
|
|
82
|
+
importers: []
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
/**
|
|
86
|
+
* Extract the package name from a yarn lockfile key. Handles compound keys
|
|
87
|
+
* (`"a@workspace:*, a@workspace:packages/a"`) via the first descriptor and
|
|
88
|
+
* `@patch:` descriptors (which embed `@npm:` inside). Total: malformed keys
|
|
89
|
+
* yield `undefined`.
|
|
90
|
+
*
|
|
91
|
+
* @internal
|
|
92
|
+
*/
|
|
93
|
+
const extractYarnPackageName = (key) => {
|
|
94
|
+
const commaIdx = key.indexOf(", ");
|
|
95
|
+
const descriptor = commaIdx === -1 ? key : key.slice(0, commaIdx);
|
|
96
|
+
const patchIdx = descriptor.indexOf("@patch:");
|
|
97
|
+
if (patchIdx > 0) return descriptor.slice(0, patchIdx);
|
|
98
|
+
const npmIdx = descriptor.lastIndexOf("@npm:");
|
|
99
|
+
const wsIdx = descriptor.lastIndexOf("@workspace:");
|
|
100
|
+
const idx = Math.max(npmIdx, wsIdx);
|
|
101
|
+
if (idx <= 0) return void 0;
|
|
102
|
+
return descriptor.slice(0, idx);
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Extract the workspace-relative path from a yarn lockfile key: the segment
|
|
106
|
+
* after `@workspace:` in the first descriptor carrying a non-`*` path.
|
|
107
|
+
*
|
|
108
|
+
* @internal
|
|
109
|
+
*/
|
|
110
|
+
const extractYarnWorkspacePath = (key) => {
|
|
111
|
+
for (const desc of key.split(", ")) {
|
|
112
|
+
const wsIdx = desc.lastIndexOf("@workspace:");
|
|
113
|
+
if (wsIdx >= 0) {
|
|
114
|
+
const path = desc.slice(wsIdx + 11);
|
|
115
|
+
if (path !== "" && path !== "*") return path;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Strip the `"npm:"` prefix from yarn dependency specifiers.
|
|
121
|
+
*
|
|
122
|
+
* @internal
|
|
123
|
+
*/
|
|
124
|
+
const cleanYarnDeps = (deps) => {
|
|
125
|
+
if (!deps) return void 0;
|
|
126
|
+
return Object.fromEntries(Object.entries(deps).map(([name, value]) => [name, value.startsWith("npm:") ? value.slice(4) : value]));
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
//#endregion
|
|
130
|
+
export { parseYarn };
|