@effected/lockfiles 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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 };
@@ -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 };
@@ -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 };
@@ -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 };