flatlock 1.1.0 → 1.2.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.
Files changed (49) hide show
  1. package/README.md +54 -1
  2. package/bin/flatlock-cmp.js +71 -45
  3. package/dist/compare.d.ts +25 -3
  4. package/dist/compare.d.ts.map +1 -1
  5. package/dist/detect.d.ts.map +1 -1
  6. package/dist/index.d.ts +3 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/parsers/index.d.ts +2 -2
  9. package/dist/parsers/npm.d.ts +64 -37
  10. package/dist/parsers/npm.d.ts.map +1 -1
  11. package/dist/parsers/pnpm/detect.d.ts +136 -0
  12. package/dist/parsers/pnpm/detect.d.ts.map +1 -0
  13. package/dist/parsers/pnpm/index.d.ts +120 -0
  14. package/dist/parsers/pnpm/index.d.ts.map +1 -0
  15. package/dist/parsers/pnpm/internal.d.ts +5 -0
  16. package/dist/parsers/pnpm/internal.d.ts.map +1 -0
  17. package/dist/parsers/pnpm/shrinkwrap.d.ts +129 -0
  18. package/dist/parsers/pnpm/shrinkwrap.d.ts.map +1 -0
  19. package/dist/parsers/pnpm/v5.d.ts +139 -0
  20. package/dist/parsers/pnpm/v5.d.ts.map +1 -0
  21. package/dist/parsers/pnpm/v6plus.d.ts +212 -0
  22. package/dist/parsers/pnpm/v6plus.d.ts.map +1 -0
  23. package/dist/parsers/pnpm.d.ts +1 -59
  24. package/dist/parsers/pnpm.d.ts.map +1 -1
  25. package/dist/parsers/types.d.ts +23 -0
  26. package/dist/parsers/types.d.ts.map +1 -0
  27. package/dist/parsers/yarn-berry.d.ts +141 -52
  28. package/dist/parsers/yarn-berry.d.ts.map +1 -1
  29. package/dist/parsers/yarn-classic.d.ts +79 -33
  30. package/dist/parsers/yarn-classic.d.ts.map +1 -1
  31. package/dist/set.d.ts +189 -0
  32. package/dist/set.d.ts.map +1 -0
  33. package/package.json +7 -5
  34. package/src/compare.js +385 -28
  35. package/src/detect.js +3 -4
  36. package/src/index.js +9 -2
  37. package/src/parsers/index.js +10 -2
  38. package/src/parsers/npm.js +64 -16
  39. package/src/parsers/pnpm/detect.js +198 -0
  40. package/src/parsers/pnpm/index.js +289 -0
  41. package/src/parsers/pnpm/internal.js +41 -0
  42. package/src/parsers/pnpm/shrinkwrap.js +241 -0
  43. package/src/parsers/pnpm/v5.js +225 -0
  44. package/src/parsers/pnpm/v6plus.js +290 -0
  45. package/src/parsers/pnpm.js +11 -89
  46. package/src/parsers/types.js +10 -0
  47. package/src/parsers/yarn-berry.js +183 -36
  48. package/src/parsers/yarn-classic.js +81 -21
  49. package/src/set.js +618 -0
package/src/set.js ADDED
@@ -0,0 +1,618 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { parseSyml } from '@yarnpkg/parsers';
3
+ import yaml from 'js-yaml';
4
+ import { detectType, Type } from './detect.js';
5
+ import {
6
+ fromPackageLock,
7
+ fromPnpmLock,
8
+ fromYarnBerryLock,
9
+ fromYarnClassicLock,
10
+ parseYarnBerryKey,
11
+ parseYarnClassic,
12
+ parseYarnClassicKey
13
+ } from './parsers/index.js';
14
+
15
+ /**
16
+ * @typedef {import('./parsers/npm.js').Dependency} Dependency
17
+ * @typedef {import('./detect.js').LockfileType} LockfileType
18
+ */
19
+
20
+ /**
21
+ * @typedef {Object} DependenciesOfOptions
22
+ * @property {string} [workspacePath] - Path to workspace (e.g., 'packages/foo')
23
+ * @property {boolean} [dev=false] - Include devDependencies
24
+ * @property {boolean} [optional=true] - Include optionalDependencies
25
+ * @property {boolean} [peer=false] - Include peerDependencies (default false: peers are provided by consumer)
26
+ */
27
+
28
+ /**
29
+ * @typedef {Object} FromStringOptions
30
+ * @property {string} [path] - Path hint for type detection
31
+ * @property {LockfileType} [type] - Explicit lockfile type
32
+ */
33
+
34
+ /**
35
+ * @typedef {Object} PackageJson
36
+ * @property {Record<string, string>} [dependencies]
37
+ * @property {Record<string, string>} [devDependencies]
38
+ * @property {Record<string, string>} [optionalDependencies]
39
+ * @property {Record<string, string>} [peerDependencies]
40
+ */
41
+
42
+ /**
43
+ * @typedef {Record<string, any>} LockfilePackages
44
+ */
45
+
46
+ /**
47
+ * @typedef {Record<string, any>} LockfileImporters
48
+ */
49
+
50
+ /** Symbol to prevent direct construction */
51
+ const INTERNAL = Symbol('FlatlockSet.internal');
52
+
53
+ /**
54
+ * A Set-like container for lockfile dependencies.
55
+ *
56
+ * Identity is determined by name@version. Two dependencies with the same
57
+ * name and version are considered equal, regardless of integrity or resolved URL.
58
+ *
59
+ * All set operations return new FlatlockSet instances (immutable pattern).
60
+ *
61
+ * NOTE: Set operations (union, intersection, difference) return sets that
62
+ * cannot use dependenciesOf() because they lack lockfile traversal data.
63
+ *
64
+ * @example
65
+ * const set = await FlatlockSet.fromPath('./package-lock.json');
66
+ * console.log(set.size); // 1234
67
+ * console.log(set.has('lodash@4.17.21')); // true
68
+ *
69
+ * // Get dependencies for a specific workspace
70
+ * const pkg = JSON.parse(await readFile('./packages/foo/package.json'));
71
+ * const subset = set.dependenciesOf(pkg, { workspacePath: 'packages/foo' });
72
+ *
73
+ * // Set operations
74
+ * const other = await FlatlockSet.fromPath('./other-lock.json');
75
+ * const common = set.intersection(other);
76
+ */
77
+ export class FlatlockSet {
78
+ /** @type {Map<string, Dependency>} */
79
+ #deps = new Map();
80
+
81
+ /** @type {LockfilePackages | null} Raw lockfile packages for traversal */
82
+ #packages = null;
83
+
84
+ /** @type {LockfileImporters | null} Workspace importers (pnpm) */
85
+ #importers = null;
86
+
87
+ /** @type {LockfileType | null} */
88
+ #type = null;
89
+
90
+ /** @type {boolean} Whether this set supports dependenciesOf */
91
+ #canTraverse = false;
92
+
93
+ /**
94
+ * @param {symbol} internal - Must be INTERNAL symbol
95
+ * @param {Map<string, Dependency>} deps
96
+ * @param {LockfilePackages | null} packages
97
+ * @param {LockfileImporters | null} importers
98
+ * @param {LockfileType | null} type
99
+ */
100
+ constructor(internal, deps, packages, importers, type) {
101
+ if (internal !== INTERNAL) {
102
+ throw new Error(
103
+ 'FlatlockSet cannot be constructed directly. Use FlatlockSet.fromPath() or FlatlockSet.fromString()'
104
+ );
105
+ }
106
+ this.#deps = deps;
107
+ this.#packages = packages;
108
+ this.#importers = importers;
109
+ this.#type = type;
110
+ this.#canTraverse = packages !== null;
111
+ }
112
+
113
+ /**
114
+ * Create FlatlockSet from lockfile path (auto-detect type)
115
+ * @param {string} path - Path to lockfile
116
+ * @param {FromStringOptions} [options] - Parser options
117
+ * @returns {Promise<FlatlockSet>}
118
+ */
119
+ static async fromPath(path, options = {}) {
120
+ const content = await readFile(path, 'utf8');
121
+ return FlatlockSet.fromString(content, { ...options, path });
122
+ }
123
+
124
+ /**
125
+ * Create FlatlockSet from lockfile string
126
+ * @param {string} content - Lockfile content
127
+ * @param {FromStringOptions} [options] - Parser options
128
+ * @returns {FlatlockSet}
129
+ */
130
+ static fromString(content, options = {}) {
131
+ const type = options.type || detectType({ path: options.path, content });
132
+
133
+ if (!type) {
134
+ throw new Error(
135
+ 'Unable to detect lockfile type. ' +
136
+ 'Provide options.type explicitly or ensure content is a valid lockfile format.'
137
+ );
138
+ }
139
+
140
+ // Parse once, extract both deps and raw data
141
+ const { deps, packages, importers } = FlatlockSet.#parseAll(content, type, options);
142
+
143
+ return new FlatlockSet(INTERNAL, deps, packages, importers, type);
144
+ }
145
+
146
+ /**
147
+ * Parse lockfile once, returning both processed deps and raw data
148
+ * @param {string} content
149
+ * @param {LockfileType} type
150
+ * @param {FromStringOptions} options
151
+ * @returns {{ deps: Map<string, Dependency>, packages: LockfilePackages, importers: LockfileImporters | null }}
152
+ */
153
+ static #parseAll(content, type, options) {
154
+ /** @type {Map<string, Dependency>} */
155
+ const deps = new Map();
156
+ /** @type {LockfilePackages} */
157
+ let packages = {};
158
+ /** @type {LockfileImporters | null} */
159
+ let importers = null;
160
+
161
+ switch (type) {
162
+ case Type.NPM: {
163
+ const lockfile = JSON.parse(content);
164
+ packages = lockfile.packages || {};
165
+ // Pass pre-parsed lockfile object to avoid re-parsing
166
+ for (const dep of fromPackageLock(lockfile, options)) {
167
+ deps.set(`${dep.name}@${dep.version}`, dep);
168
+ }
169
+ break;
170
+ }
171
+ case Type.PNPM: {
172
+ /** @type {any} */
173
+ const lockfile = yaml.load(content);
174
+ packages = lockfile.packages || {};
175
+ importers = lockfile.importers || null;
176
+ // Pass pre-parsed lockfile object to avoid re-parsing
177
+ for (const dep of fromPnpmLock(lockfile, options)) {
178
+ deps.set(`${dep.name}@${dep.version}`, dep);
179
+ }
180
+ break;
181
+ }
182
+ case Type.YARN_CLASSIC: {
183
+ const result = parseYarnClassic(content);
184
+ packages = result.object || {};
185
+ // Pass pre-parsed lockfile object to avoid re-parsing
186
+ for (const dep of fromYarnClassicLock(packages, options)) {
187
+ deps.set(`${dep.name}@${dep.version}`, dep);
188
+ }
189
+ break;
190
+ }
191
+ case Type.YARN_BERRY: {
192
+ packages = parseSyml(content);
193
+ // Pass pre-parsed lockfile object to avoid re-parsing
194
+ for (const dep of fromYarnBerryLock(packages, options)) {
195
+ deps.set(`${dep.name}@${dep.version}`, dep);
196
+ }
197
+ break;
198
+ }
199
+ }
200
+
201
+ return { deps, packages, importers };
202
+ }
203
+
204
+ /** @returns {number} */
205
+ get size() {
206
+ return this.#deps.size;
207
+ }
208
+
209
+ /** @returns {LockfileType | null} */
210
+ get type() {
211
+ return this.#type;
212
+ }
213
+
214
+ /** @returns {boolean} */
215
+ get canTraverse() {
216
+ return this.#canTraverse;
217
+ }
218
+
219
+ /**
220
+ * Check if a dependency exists
221
+ * @param {string} nameAtVersion - e.g., "lodash@4.17.21"
222
+ * @returns {boolean}
223
+ */
224
+ has(nameAtVersion) {
225
+ return this.#deps.has(nameAtVersion);
226
+ }
227
+
228
+ /**
229
+ * Get a dependency by name@version
230
+ * @param {string} nameAtVersion
231
+ * @returns {Dependency | undefined}
232
+ */
233
+ get(nameAtVersion) {
234
+ return this.#deps.get(nameAtVersion);
235
+ }
236
+
237
+ /** @returns {IterableIterator<Dependency>} */
238
+ [Symbol.iterator]() {
239
+ return this.#deps.values();
240
+ }
241
+
242
+ /** @returns {IterableIterator<Dependency>} */
243
+ values() {
244
+ return this.#deps.values();
245
+ }
246
+
247
+ /** @returns {IterableIterator<string>} */
248
+ keys() {
249
+ return this.#deps.keys();
250
+ }
251
+
252
+ /** @returns {IterableIterator<[string, Dependency]>} */
253
+ entries() {
254
+ return this.#deps.entries();
255
+ }
256
+
257
+ /**
258
+ * Execute a callback for each dependency
259
+ * @param {(dep: Dependency, key: string, set: FlatlockSet) => void} callback
260
+ * @param {any} [thisArg]
261
+ */
262
+ forEach(callback, thisArg) {
263
+ for (const [key, dep] of this.#deps) {
264
+ callback.call(thisArg, dep, key, this);
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Union of this set with another
270
+ * @param {FlatlockSet} other
271
+ * @returns {FlatlockSet}
272
+ */
273
+ union(other) {
274
+ const deps = new Map(this.#deps);
275
+ for (const [key, dep] of other.#deps) {
276
+ if (!deps.has(key)) {
277
+ deps.set(key, dep);
278
+ }
279
+ }
280
+ return new FlatlockSet(INTERNAL, deps, null, null, null);
281
+ }
282
+
283
+ /**
284
+ * Intersection of this set with another
285
+ * @param {FlatlockSet} other
286
+ * @returns {FlatlockSet}
287
+ */
288
+ intersection(other) {
289
+ const deps = new Map();
290
+ for (const [key, dep] of this.#deps) {
291
+ if (other.has(key)) {
292
+ deps.set(key, dep);
293
+ }
294
+ }
295
+ return new FlatlockSet(INTERNAL, deps, null, null, null);
296
+ }
297
+
298
+ /**
299
+ * Difference: elements in this set but not in other
300
+ * @param {FlatlockSet} other
301
+ * @returns {FlatlockSet}
302
+ */
303
+ difference(other) {
304
+ const deps = new Map();
305
+ for (const [key, dep] of this.#deps) {
306
+ if (!other.has(key)) {
307
+ deps.set(key, dep);
308
+ }
309
+ }
310
+ return new FlatlockSet(INTERNAL, deps, null, null, null);
311
+ }
312
+
313
+ /**
314
+ * Check if this set is a subset of another
315
+ * @param {FlatlockSet} other
316
+ * @returns {boolean}
317
+ */
318
+ isSubsetOf(other) {
319
+ for (const key of this.#deps.keys()) {
320
+ if (!other.has(key)) return false;
321
+ }
322
+ return true;
323
+ }
324
+
325
+ /**
326
+ * Check if this set is a superset of another
327
+ * @param {FlatlockSet} other
328
+ * @returns {boolean}
329
+ */
330
+ isSupersetOf(other) {
331
+ return other.isSubsetOf(this);
332
+ }
333
+
334
+ /**
335
+ * Check if this set has no elements in common with another
336
+ * @param {FlatlockSet} other
337
+ * @returns {boolean}
338
+ */
339
+ isDisjointFrom(other) {
340
+ for (const key of this.#deps.keys()) {
341
+ if (other.has(key)) return false;
342
+ }
343
+ return true;
344
+ }
345
+
346
+ /**
347
+ * Get transitive dependencies of a package.json
348
+ *
349
+ * For monorepos, provide workspacePath to get correct resolution.
350
+ * Without workspacePath, assumes root package (hoisted deps only).
351
+ *
352
+ * NOTE: This method is only available on sets created directly from
353
+ * fromPath/fromString. Sets created via union/intersection/difference
354
+ * cannot use this method (canTraverse will be false).
355
+ *
356
+ * @param {PackageJson} packageJson - Parsed package.json
357
+ * @param {DependenciesOfOptions} [options]
358
+ * @returns {FlatlockSet}
359
+ * @throws {Error} If called on a set that cannot traverse
360
+ */
361
+ dependenciesOf(packageJson, options = {}) {
362
+ if (!packageJson || typeof packageJson !== 'object') {
363
+ throw new TypeError('packageJson must be a non-null object');
364
+ }
365
+
366
+ if (!this.#canTraverse) {
367
+ throw new Error(
368
+ 'dependenciesOf() requires lockfile data. ' +
369
+ 'This set was created via set operations and cannot traverse dependencies. ' +
370
+ 'Use dependenciesOf() on the original set before set operations.'
371
+ );
372
+ }
373
+
374
+ const { workspacePath, dev = false, optional = true, peer = false } = options;
375
+
376
+ // Collect seed dependencies from package.json
377
+ const seeds = this.#collectSeeds(packageJson, { dev, optional, peer });
378
+
379
+ // If pnpm with workspacePath, use importers to get resolved versions
380
+ if (this.#type === Type.PNPM && workspacePath && this.#importers) {
381
+ return this.#dependenciesOfPnpm(seeds, workspacePath, { dev, optional, peer });
382
+ }
383
+
384
+ // BFS traversal for npm/yarn (hoisted resolution)
385
+ return this.#dependenciesOfHoisted(seeds, workspacePath);
386
+ }
387
+
388
+ /**
389
+ * Collect seed dependency names from package.json
390
+ * @param {PackageJson} packageJson
391
+ * @param {{ dev: boolean, optional: boolean, peer: boolean }} options
392
+ * @returns {Set<string>}
393
+ */
394
+ #collectSeeds(packageJson, { dev, optional, peer }) {
395
+ const seeds = new Set();
396
+
397
+ for (const name of Object.keys(packageJson.dependencies || {})) {
398
+ seeds.add(name);
399
+ }
400
+ if (dev) {
401
+ for (const name of Object.keys(packageJson.devDependencies || {})) {
402
+ seeds.add(name);
403
+ }
404
+ }
405
+ if (optional) {
406
+ for (const name of Object.keys(packageJson.optionalDependencies || {})) {
407
+ seeds.add(name);
408
+ }
409
+ }
410
+ if (peer) {
411
+ for (const name of Object.keys(packageJson.peerDependencies || {})) {
412
+ seeds.add(name);
413
+ }
414
+ }
415
+
416
+ return seeds;
417
+ }
418
+
419
+ /**
420
+ * pnpm-specific resolution using importers
421
+ * @param {Set<string>} seeds
422
+ * @param {string} workspacePath
423
+ * @param {{ dev: boolean, optional: boolean, peer: boolean }} options
424
+ * @returns {FlatlockSet}
425
+ */
426
+ #dependenciesOfPnpm(seeds, workspacePath, { dev, optional, peer }) {
427
+ /** @type {Map<string, Dependency>} */
428
+ const result = new Map();
429
+ /** @type {Set<string>} */
430
+ const visited = new Set();
431
+ /** @type {string[]} */
432
+ const queue = [...seeds];
433
+
434
+ // Get resolved versions from importers
435
+ const importer = this.#importers?.[workspacePath] || this.#importers?.['.'] || {};
436
+ const resolvedDeps = {
437
+ ...importer.dependencies,
438
+ ...(dev ? importer.devDependencies : {}),
439
+ ...(optional ? importer.optionalDependencies : {}),
440
+ ...(peer ? importer.peerDependencies : {})
441
+ };
442
+
443
+ while (queue.length > 0) {
444
+ const name = /** @type {string} */ (queue.shift());
445
+ if (visited.has(name)) continue;
446
+ visited.add(name);
447
+
448
+ // Get resolved version from importer or find in deps
449
+ const version = resolvedDeps[name];
450
+ let dep;
451
+
452
+ if (version) {
453
+ // pnpm stores version directly or as specifier
454
+ dep = this.get(`${name}@${version}`) || this.#findByName(name);
455
+ } else {
456
+ dep = this.#findByName(name);
457
+ }
458
+
459
+ if (!dep) continue;
460
+
461
+ const key = `${dep.name}@${dep.version}`;
462
+ result.set(key, dep);
463
+
464
+ // Get transitive deps
465
+ const pkgKey = `/${dep.name}@${dep.version}`;
466
+ const pkgEntry = this.#packages?.[pkgKey];
467
+ if (pkgEntry) {
468
+ for (const transName of Object.keys(pkgEntry.dependencies || {})) {
469
+ if (!visited.has(transName)) queue.push(transName);
470
+ }
471
+ if (optional) {
472
+ for (const transName of Object.keys(pkgEntry.optionalDependencies || {})) {
473
+ if (!visited.has(transName)) queue.push(transName);
474
+ }
475
+ }
476
+ }
477
+ }
478
+
479
+ return new FlatlockSet(INTERNAL, result, null, null, this.#type);
480
+ }
481
+
482
+ /**
483
+ * npm/yarn resolution with hoisting
484
+ * @param {Set<string>} seeds
485
+ * @param {string} [workspacePath]
486
+ * @returns {FlatlockSet}
487
+ */
488
+ #dependenciesOfHoisted(seeds, workspacePath) {
489
+ /** @type {Map<string, Dependency>} */
490
+ const result = new Map();
491
+ /** @type {Set<string>} */
492
+ const visited = new Set();
493
+ /** @type {string[]} */
494
+ const queue = [...seeds];
495
+
496
+ while (queue.length > 0) {
497
+ const name = /** @type {string} */ (queue.shift());
498
+ if (visited.has(name)) continue;
499
+ visited.add(name);
500
+
501
+ // Find package: check workspace-local first, then hoisted
502
+ const dep = this.#findPackage(name, workspacePath);
503
+ if (!dep) continue;
504
+
505
+ const key = `${dep.name}@${dep.version}`;
506
+ result.set(key, dep);
507
+
508
+ // Get transitive deps from raw lockfile
509
+ const entry = this.#getPackageEntry(dep.name, dep.version, workspacePath);
510
+ if (entry) {
511
+ for (const transName of Object.keys(entry.dependencies || {})) {
512
+ if (!visited.has(transName)) queue.push(transName);
513
+ }
514
+ for (const transName of Object.keys(entry.optionalDependencies || {})) {
515
+ if (!visited.has(transName)) queue.push(transName);
516
+ }
517
+ }
518
+ }
519
+
520
+ return new FlatlockSet(INTERNAL, result, null, null, this.#type);
521
+ }
522
+
523
+ /**
524
+ * Find a package by name, checking workspace-local then hoisted
525
+ * @param {string} name
526
+ * @param {string} [workspacePath]
527
+ * @returns {Dependency | undefined}
528
+ */
529
+ #findPackage(name, workspacePath) {
530
+ if (this.#type === Type.NPM && workspacePath) {
531
+ // Check workspace-local node_modules first
532
+ const localKey = `${workspacePath}/node_modules/${name}`;
533
+ const localEntry = this.#packages?.[localKey];
534
+ if (localEntry?.version) {
535
+ return this.get(`${name}@${localEntry.version}`);
536
+ }
537
+ }
538
+
539
+ // Fall back to hoisted (root node_modules)
540
+ return this.#findByName(name);
541
+ }
542
+
543
+ /**
544
+ * Find a dependency by name (returns hoisted/first match)
545
+ * @param {string} name
546
+ * @returns {Dependency | undefined}
547
+ */
548
+ #findByName(name) {
549
+ // For npm, check root node_modules path first
550
+ if (this.#type === Type.NPM) {
551
+ const rootKey = `node_modules/${name}`;
552
+ const entry = this.#packages?.[rootKey];
553
+ if (entry?.version) {
554
+ return this.get(`${name}@${entry.version}`);
555
+ }
556
+ }
557
+
558
+ // Fallback: iterate deps (may return arbitrary version if multiple)
559
+ for (const dep of this.#deps.values()) {
560
+ if (dep.name === name) return dep;
561
+ }
562
+ return undefined;
563
+ }
564
+
565
+ /**
566
+ * Get raw package entry for transitive dep lookup
567
+ * @param {string} name
568
+ * @param {string} version
569
+ * @param {string} [workspacePath]
570
+ * @returns {any}
571
+ */
572
+ #getPackageEntry(name, version, workspacePath) {
573
+ if (!this.#packages) return null;
574
+
575
+ switch (this.#type) {
576
+ case Type.NPM: {
577
+ // Check workspace-local first
578
+ if (workspacePath) {
579
+ const localKey = `${workspacePath}/node_modules/${name}`;
580
+ if (this.#packages[localKey]) return this.#packages[localKey];
581
+ }
582
+ // Fall back to hoisted
583
+ return this.#packages[`node_modules/${name}`] || null;
584
+ }
585
+ case Type.PNPM: {
586
+ return this.#packages[`/${name}@${version}`] || null;
587
+ }
588
+ case Type.YARN_CLASSIC: {
589
+ for (const [key, entry] of Object.entries(this.#packages)) {
590
+ if (entry.version === version) {
591
+ const keyName = parseYarnClassicKey(key);
592
+ if (keyName === name) return entry;
593
+ }
594
+ }
595
+ return null;
596
+ }
597
+ case Type.YARN_BERRY: {
598
+ for (const [key, entry] of Object.entries(this.#packages)) {
599
+ if (entry.version === version) {
600
+ const keyName = parseYarnBerryKey(key);
601
+ if (keyName === name) return entry;
602
+ }
603
+ }
604
+ return null;
605
+ }
606
+ default:
607
+ return null;
608
+ }
609
+ }
610
+
611
+ /**
612
+ * Convert to array
613
+ * @returns {Dependency[]}
614
+ */
615
+ toArray() {
616
+ return [...this.#deps.values()];
617
+ }
618
+ }