flatlock 1.2.0 → 1.3.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/README.md +42 -1
- package/bin/flatcover.js +398 -0
- package/bin/flatlock.js +158 -0
- package/package.json +16 -5
- package/src/parsers/index.js +14 -2
- package/src/parsers/npm.js +82 -0
- package/src/parsers/pnpm/index.js +70 -0
- package/src/parsers/yarn-berry.js +88 -0
- package/src/set.js +730 -41
- package/dist/compare.d.ts +0 -85
- package/dist/compare.d.ts.map +0 -1
- package/dist/detect.d.ts +0 -33
- package/dist/detect.d.ts.map +0 -1
- package/dist/index.d.ts +0 -72
- package/dist/index.d.ts.map +0 -1
- package/dist/parsers/index.d.ts +0 -5
- package/dist/parsers/index.d.ts.map +0 -1
- package/dist/parsers/npm.d.ts +0 -109
- package/dist/parsers/npm.d.ts.map +0 -1
- package/dist/parsers/pnpm/detect.d.ts +0 -136
- package/dist/parsers/pnpm/detect.d.ts.map +0 -1
- package/dist/parsers/pnpm/index.d.ts +0 -120
- package/dist/parsers/pnpm/index.d.ts.map +0 -1
- package/dist/parsers/pnpm/internal.d.ts +0 -5
- package/dist/parsers/pnpm/internal.d.ts.map +0 -1
- package/dist/parsers/pnpm/shrinkwrap.d.ts +0 -129
- package/dist/parsers/pnpm/shrinkwrap.d.ts.map +0 -1
- package/dist/parsers/pnpm/v5.d.ts +0 -139
- package/dist/parsers/pnpm/v5.d.ts.map +0 -1
- package/dist/parsers/pnpm/v6plus.d.ts +0 -212
- package/dist/parsers/pnpm/v6plus.d.ts.map +0 -1
- package/dist/parsers/pnpm.d.ts +0 -2
- package/dist/parsers/pnpm.d.ts.map +0 -1
- package/dist/parsers/types.d.ts +0 -23
- package/dist/parsers/types.d.ts.map +0 -1
- package/dist/parsers/yarn-berry.d.ts +0 -154
- package/dist/parsers/yarn-berry.d.ts.map +0 -1
- package/dist/parsers/yarn-classic.d.ts +0 -110
- package/dist/parsers/yarn-classic.d.ts.map +0 -1
- package/dist/result.d.ts +0 -12
- package/dist/result.d.ts.map +0 -1
- package/dist/set.d.ts +0 -189
- package/dist/set.d.ts.map +0 -1
package/src/set.js
CHANGED
|
@@ -3,6 +3,12 @@ import { parseSyml } from '@yarnpkg/parsers';
|
|
|
3
3
|
import yaml from 'js-yaml';
|
|
4
4
|
import { detectType, Type } from './detect.js';
|
|
5
5
|
import {
|
|
6
|
+
buildNpmWorkspacePackages,
|
|
7
|
+
buildPnpmWorkspacePackages,
|
|
8
|
+
buildYarnBerryWorkspacePackages,
|
|
9
|
+
extractNpmWorkspacePaths,
|
|
10
|
+
extractPnpmWorkspacePaths,
|
|
11
|
+
extractYarnBerryWorkspacePaths,
|
|
6
12
|
fromPackageLock,
|
|
7
13
|
fromPnpmLock,
|
|
8
14
|
fromYarnBerryLock,
|
|
@@ -17,12 +23,24 @@ import {
|
|
|
17
23
|
* @typedef {import('./detect.js').LockfileType} LockfileType
|
|
18
24
|
*/
|
|
19
25
|
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {Object} WorkspacePackage
|
|
28
|
+
* @property {string} name - Package name (e.g., '@vue/shared')
|
|
29
|
+
* @property {string} version - Package version (e.g., '3.5.26')
|
|
30
|
+
* @property {Record<string, string>} [dependencies] - Production dependencies (for yarn berry workspace traversal)
|
|
31
|
+
* @property {Record<string, string>} [devDependencies] - Dev dependencies
|
|
32
|
+
* @property {Record<string, string>} [optionalDependencies] - Optional dependencies
|
|
33
|
+
* @property {Record<string, string>} [peerDependencies] - Peer dependencies
|
|
34
|
+
*/
|
|
35
|
+
|
|
20
36
|
/**
|
|
21
37
|
* @typedef {Object} DependenciesOfOptions
|
|
22
38
|
* @property {string} [workspacePath] - Path to workspace (e.g., 'packages/foo')
|
|
39
|
+
* @property {string} [repoDir] - Path to repository root for reading workspace package.json files
|
|
23
40
|
* @property {boolean} [dev=false] - Include devDependencies
|
|
24
41
|
* @property {boolean} [optional=true] - Include optionalDependencies
|
|
25
42
|
* @property {boolean} [peer=false] - Include peerDependencies (default false: peers are provided by consumer)
|
|
43
|
+
* @property {Record<string, WorkspacePackage>} [workspacePackages] - Map of workspace path to package info for resolving workspace links (auto-built if repoDir provided)
|
|
26
44
|
*/
|
|
27
45
|
|
|
28
46
|
/**
|
|
@@ -68,7 +86,7 @@ const INTERNAL = Symbol('FlatlockSet.internal');
|
|
|
68
86
|
*
|
|
69
87
|
* // Get dependencies for a specific workspace
|
|
70
88
|
* const pkg = JSON.parse(await readFile('./packages/foo/package.json'));
|
|
71
|
-
* const subset = set.dependenciesOf(pkg, { workspacePath: 'packages/foo' });
|
|
89
|
+
* const subset = await set.dependenciesOf(pkg, { workspacePath: 'packages/foo', repoDir: '.' });
|
|
72
90
|
*
|
|
73
91
|
* // Set operations
|
|
74
92
|
* const other = await FlatlockSet.fromPath('./other-lock.json');
|
|
@@ -84,6 +102,9 @@ export class FlatlockSet {
|
|
|
84
102
|
/** @type {LockfileImporters | null} Workspace importers (pnpm) */
|
|
85
103
|
#importers = null;
|
|
86
104
|
|
|
105
|
+
/** @type {Record<string, any> | null} Snapshots (pnpm v9) */
|
|
106
|
+
#snapshots = null;
|
|
107
|
+
|
|
87
108
|
/** @type {LockfileType | null} */
|
|
88
109
|
#type = null;
|
|
89
110
|
|
|
@@ -95,9 +116,10 @@ export class FlatlockSet {
|
|
|
95
116
|
* @param {Map<string, Dependency>} deps
|
|
96
117
|
* @param {LockfilePackages | null} packages
|
|
97
118
|
* @param {LockfileImporters | null} importers
|
|
119
|
+
* @param {Record<string, any> | null} snapshots
|
|
98
120
|
* @param {LockfileType | null} type
|
|
99
121
|
*/
|
|
100
|
-
constructor(internal, deps, packages, importers, type) {
|
|
122
|
+
constructor(internal, deps, packages, importers, snapshots, type) {
|
|
101
123
|
if (internal !== INTERNAL) {
|
|
102
124
|
throw new Error(
|
|
103
125
|
'FlatlockSet cannot be constructed directly. Use FlatlockSet.fromPath() or FlatlockSet.fromString()'
|
|
@@ -106,6 +128,7 @@ export class FlatlockSet {
|
|
|
106
128
|
this.#deps = deps;
|
|
107
129
|
this.#packages = packages;
|
|
108
130
|
this.#importers = importers;
|
|
131
|
+
this.#snapshots = snapshots;
|
|
109
132
|
this.#type = type;
|
|
110
133
|
this.#canTraverse = packages !== null;
|
|
111
134
|
}
|
|
@@ -138,9 +161,9 @@ export class FlatlockSet {
|
|
|
138
161
|
}
|
|
139
162
|
|
|
140
163
|
// Parse once, extract both deps and raw data
|
|
141
|
-
const { deps, packages, importers } = FlatlockSet.#parseAll(content, type, options);
|
|
164
|
+
const { deps, packages, importers, snapshots } = FlatlockSet.#parseAll(content, type, options);
|
|
142
165
|
|
|
143
|
-
return new FlatlockSet(INTERNAL, deps, packages, importers, type);
|
|
166
|
+
return new FlatlockSet(INTERNAL, deps, packages, importers, snapshots, type);
|
|
144
167
|
}
|
|
145
168
|
|
|
146
169
|
/**
|
|
@@ -148,7 +171,7 @@ export class FlatlockSet {
|
|
|
148
171
|
* @param {string} content
|
|
149
172
|
* @param {LockfileType} type
|
|
150
173
|
* @param {FromStringOptions} options
|
|
151
|
-
* @returns {{ deps: Map<string, Dependency>, packages: LockfilePackages, importers: LockfileImporters | null }}
|
|
174
|
+
* @returns {{ deps: Map<string, Dependency>, packages: LockfilePackages, importers: LockfileImporters | null, snapshots: Record<string, any> | null }}
|
|
152
175
|
*/
|
|
153
176
|
static #parseAll(content, type, options) {
|
|
154
177
|
/** @type {Map<string, Dependency>} */
|
|
@@ -157,6 +180,8 @@ export class FlatlockSet {
|
|
|
157
180
|
let packages = {};
|
|
158
181
|
/** @type {LockfileImporters | null} */
|
|
159
182
|
let importers = null;
|
|
183
|
+
/** @type {Record<string, any> | null} */
|
|
184
|
+
let snapshots = null;
|
|
160
185
|
|
|
161
186
|
switch (type) {
|
|
162
187
|
case Type.NPM: {
|
|
@@ -173,6 +198,7 @@ export class FlatlockSet {
|
|
|
173
198
|
const lockfile = yaml.load(content);
|
|
174
199
|
packages = lockfile.packages || {};
|
|
175
200
|
importers = lockfile.importers || null;
|
|
201
|
+
snapshots = lockfile.snapshots || null;
|
|
176
202
|
// Pass pre-parsed lockfile object to avoid re-parsing
|
|
177
203
|
for (const dep of fromPnpmLock(lockfile, options)) {
|
|
178
204
|
deps.set(`${dep.name}@${dep.version}`, dep);
|
|
@@ -198,7 +224,7 @@ export class FlatlockSet {
|
|
|
198
224
|
}
|
|
199
225
|
}
|
|
200
226
|
|
|
201
|
-
return { deps, packages, importers };
|
|
227
|
+
return { deps, packages, importers, snapshots };
|
|
202
228
|
}
|
|
203
229
|
|
|
204
230
|
/** @returns {number} */
|
|
@@ -216,6 +242,42 @@ export class FlatlockSet {
|
|
|
216
242
|
return this.#canTraverse;
|
|
217
243
|
}
|
|
218
244
|
|
|
245
|
+
/**
|
|
246
|
+
* Get workspace paths from lockfile.
|
|
247
|
+
* Supports npm, pnpm, and yarn berry lockfiles.
|
|
248
|
+
* @returns {string[]} Array of workspace paths (e.g., ['packages/foo', 'packages/bar'])
|
|
249
|
+
*/
|
|
250
|
+
getWorkspacePaths() {
|
|
251
|
+
switch (this.#type) {
|
|
252
|
+
case Type.NPM:
|
|
253
|
+
return extractNpmWorkspacePaths(this.#packages ? { packages: this.#packages } : {});
|
|
254
|
+
case Type.PNPM:
|
|
255
|
+
return extractPnpmWorkspacePaths(this.#importers ? { importers: this.#importers } : {});
|
|
256
|
+
case Type.YARN_BERRY:
|
|
257
|
+
return extractYarnBerryWorkspacePaths(this.#packages || {});
|
|
258
|
+
default:
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Build workspace packages map by reading package.json files.
|
|
265
|
+
* @param {string} repoDir - Path to repository root
|
|
266
|
+
* @returns {Promise<Record<string, WorkspacePackage>>}
|
|
267
|
+
*/
|
|
268
|
+
async #buildWorkspacePackages(repoDir) {
|
|
269
|
+
switch (this.#type) {
|
|
270
|
+
case Type.NPM:
|
|
271
|
+
return buildNpmWorkspacePackages({ packages: this.#packages || {} }, repoDir);
|
|
272
|
+
case Type.PNPM:
|
|
273
|
+
return buildPnpmWorkspacePackages({ importers: this.#importers || {} }, repoDir);
|
|
274
|
+
case Type.YARN_BERRY:
|
|
275
|
+
return buildYarnBerryWorkspacePackages(this.#packages || {}, repoDir);
|
|
276
|
+
default:
|
|
277
|
+
return {};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
219
281
|
/**
|
|
220
282
|
* Check if a dependency exists
|
|
221
283
|
* @param {string} nameAtVersion - e.g., "lodash@4.17.21"
|
|
@@ -277,7 +339,7 @@ export class FlatlockSet {
|
|
|
277
339
|
deps.set(key, dep);
|
|
278
340
|
}
|
|
279
341
|
}
|
|
280
|
-
return new FlatlockSet(INTERNAL, deps, null, null, null);
|
|
342
|
+
return new FlatlockSet(INTERNAL, deps, null, null, null, null);
|
|
281
343
|
}
|
|
282
344
|
|
|
283
345
|
/**
|
|
@@ -292,7 +354,7 @@ export class FlatlockSet {
|
|
|
292
354
|
deps.set(key, dep);
|
|
293
355
|
}
|
|
294
356
|
}
|
|
295
|
-
return new FlatlockSet(INTERNAL, deps, null, null, null);
|
|
357
|
+
return new FlatlockSet(INTERNAL, deps, null, null, null, null);
|
|
296
358
|
}
|
|
297
359
|
|
|
298
360
|
/**
|
|
@@ -307,7 +369,7 @@ export class FlatlockSet {
|
|
|
307
369
|
deps.set(key, dep);
|
|
308
370
|
}
|
|
309
371
|
}
|
|
310
|
-
return new FlatlockSet(INTERNAL, deps, null, null, null);
|
|
372
|
+
return new FlatlockSet(INTERNAL, deps, null, null, null, null);
|
|
311
373
|
}
|
|
312
374
|
|
|
313
375
|
/**
|
|
@@ -346,7 +408,7 @@ export class FlatlockSet {
|
|
|
346
408
|
/**
|
|
347
409
|
* Get transitive dependencies of a package.json
|
|
348
410
|
*
|
|
349
|
-
* For monorepos, provide workspacePath to get correct resolution.
|
|
411
|
+
* For monorepos, provide workspacePath and repoDir to get correct resolution.
|
|
350
412
|
* Without workspacePath, assumes root package (hoisted deps only).
|
|
351
413
|
*
|
|
352
414
|
* NOTE: This method is only available on sets created directly from
|
|
@@ -355,10 +417,17 @@ export class FlatlockSet {
|
|
|
355
417
|
*
|
|
356
418
|
* @param {PackageJson} packageJson - Parsed package.json
|
|
357
419
|
* @param {DependenciesOfOptions} [options]
|
|
358
|
-
* @returns {FlatlockSet}
|
|
420
|
+
* @returns {Promise<FlatlockSet>}
|
|
359
421
|
* @throws {Error} If called on a set that cannot traverse
|
|
422
|
+
*
|
|
423
|
+
* @example
|
|
424
|
+
* // Simple usage with repoDir (recommended)
|
|
425
|
+
* const deps = await lockfile.dependenciesOf(pkg, {
|
|
426
|
+
* workspacePath: 'packages/foo',
|
|
427
|
+
* repoDir: '/path/to/repo'
|
|
428
|
+
* });
|
|
360
429
|
*/
|
|
361
|
-
dependenciesOf(packageJson, options = {}) {
|
|
430
|
+
async dependenciesOf(packageJson, options = {}) {
|
|
362
431
|
if (!packageJson || typeof packageJson !== 'object') {
|
|
363
432
|
throw new TypeError('packageJson must be a non-null object');
|
|
364
433
|
}
|
|
@@ -371,17 +440,60 @@ export class FlatlockSet {
|
|
|
371
440
|
);
|
|
372
441
|
}
|
|
373
442
|
|
|
374
|
-
const { workspacePath, dev = false, optional = true, peer = false } = options;
|
|
443
|
+
const { workspacePath, repoDir, dev = false, optional = true, peer = false } = options;
|
|
444
|
+
|
|
445
|
+
// Build workspacePackages if repoDir provided and not already supplied
|
|
446
|
+
let { workspacePackages } = options;
|
|
447
|
+
if (!workspacePackages && repoDir && workspacePath) {
|
|
448
|
+
workspacePackages = await this.#buildWorkspacePackages(repoDir);
|
|
449
|
+
}
|
|
375
450
|
|
|
376
451
|
// Collect seed dependencies from package.json
|
|
377
452
|
const seeds = this.#collectSeeds(packageJson, { dev, optional, peer });
|
|
378
453
|
|
|
379
454
|
// If pnpm with workspacePath, use importers to get resolved versions
|
|
380
455
|
if (this.#type === Type.PNPM && workspacePath && this.#importers) {
|
|
381
|
-
return this.#dependenciesOfPnpm(seeds, workspacePath, {
|
|
456
|
+
return this.#dependenciesOfPnpm(seeds, workspacePath, {
|
|
457
|
+
dev,
|
|
458
|
+
optional,
|
|
459
|
+
peer,
|
|
460
|
+
...(workspacePackages && { workspacePackages })
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// If yarn berry with workspace context, use workspace-aware resolution
|
|
465
|
+
// Auto-extract workspacePackages from lockfile if not provided
|
|
466
|
+
if (this.#type === Type.YARN_BERRY && workspacePath) {
|
|
467
|
+
const wsPackages = workspacePackages || this.#extractYarnBerryWorkspaces();
|
|
468
|
+
return this.#dependenciesOfYarnBerry(seeds, packageJson, {
|
|
469
|
+
dev,
|
|
470
|
+
optional,
|
|
471
|
+
peer,
|
|
472
|
+
workspacePackages: wsPackages
|
|
473
|
+
});
|
|
382
474
|
}
|
|
383
475
|
|
|
384
|
-
//
|
|
476
|
+
// If yarn classic with workspace packages, use workspace-aware resolution
|
|
477
|
+
if (this.#type === Type.YARN_CLASSIC && workspacePackages) {
|
|
478
|
+
return this.#dependenciesOfYarnClassic(seeds, packageJson, {
|
|
479
|
+
dev,
|
|
480
|
+
optional,
|
|
481
|
+
peer,
|
|
482
|
+
workspacePackages
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// If npm with workspace packages and workspacePath, use workspace-aware resolution
|
|
487
|
+
if (this.#type === Type.NPM && workspacePackages && workspacePath) {
|
|
488
|
+
return this.#dependenciesOfNpm(seeds, workspacePath, {
|
|
489
|
+
dev,
|
|
490
|
+
optional,
|
|
491
|
+
peer,
|
|
492
|
+
workspacePackages
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// BFS traversal for npm/yarn-classic (hoisted resolution)
|
|
385
497
|
return this.#dependenciesOfHoisted(seeds, workspacePath);
|
|
386
498
|
}
|
|
387
499
|
|
|
@@ -418,12 +530,455 @@ export class FlatlockSet {
|
|
|
418
530
|
|
|
419
531
|
/**
|
|
420
532
|
* pnpm-specific resolution using importers
|
|
421
|
-
*
|
|
533
|
+
*
|
|
534
|
+
* pnpm monorepos have workspace packages linked via link: protocol.
|
|
535
|
+
* To get the full dependency tree, we need to:
|
|
536
|
+
* 1. Follow workspace links recursively, emitting workspace packages
|
|
537
|
+
* 2. Collect external deps from each visited workspace
|
|
538
|
+
* 3. Traverse the packages/snapshots section for transitive deps
|
|
539
|
+
*
|
|
540
|
+
* @param {Set<string>} _seeds - Unused, we derive from importers
|
|
422
541
|
* @param {string} workspacePath
|
|
423
|
-
* @param {{ dev: boolean, optional: boolean, peer: boolean }} options
|
|
542
|
+
* @param {{ dev: boolean, optional: boolean, peer: boolean, workspacePackages?: Record<string, {name: string, version: string}> }} options
|
|
543
|
+
* @returns {FlatlockSet}
|
|
544
|
+
*/
|
|
545
|
+
#dependenciesOfPnpm(_seeds, workspacePath, { dev, optional, peer, workspacePackages }) {
|
|
546
|
+
/** @type {Map<string, Dependency>} */
|
|
547
|
+
const result = new Map();
|
|
548
|
+
|
|
549
|
+
// Phase 1: Follow workspace links, emit workspace packages, collect external deps
|
|
550
|
+
const externalDeps = new Set();
|
|
551
|
+
const visitedWorkspaces = new Set();
|
|
552
|
+
const workspaceQueue = [workspacePath];
|
|
553
|
+
|
|
554
|
+
while (workspaceQueue.length > 0) {
|
|
555
|
+
const ws = /** @type {string} */ (workspaceQueue.shift());
|
|
556
|
+
if (visitedWorkspaces.has(ws)) continue;
|
|
557
|
+
visitedWorkspaces.add(ws);
|
|
558
|
+
|
|
559
|
+
// If we have workspace package info, emit this workspace as a dependency
|
|
560
|
+
// (except for the starting workspace - dependenciesOf returns deps, not self)
|
|
561
|
+
if (workspacePackages && ws !== workspacePath) {
|
|
562
|
+
const wsPkg = workspacePackages[ws];
|
|
563
|
+
if (wsPkg) {
|
|
564
|
+
result.set(`${wsPkg.name}@${wsPkg.version}`, {
|
|
565
|
+
name: wsPkg.name,
|
|
566
|
+
version: wsPkg.version
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const importer = this.#importers?.[ws];
|
|
572
|
+
if (!importer) continue;
|
|
573
|
+
|
|
574
|
+
// Collect dependencies from importer
|
|
575
|
+
// v9 format: { specifier: '...', version: '...' }
|
|
576
|
+
// older format: version string directly
|
|
577
|
+
const depSections = [importer.dependencies];
|
|
578
|
+
if (dev) depSections.push(importer.devDependencies);
|
|
579
|
+
if (optional) depSections.push(importer.optionalDependencies);
|
|
580
|
+
if (peer) depSections.push(importer.peerDependencies);
|
|
581
|
+
|
|
582
|
+
for (const deps of depSections) {
|
|
583
|
+
if (!deps) continue;
|
|
584
|
+
for (const [name, spec] of Object.entries(deps)) {
|
|
585
|
+
// Handle v9 object format or older string format
|
|
586
|
+
const version =
|
|
587
|
+
typeof spec === 'object' && spec !== null
|
|
588
|
+
? /** @type {{version?: string}} */ (spec).version
|
|
589
|
+
: /** @type {string} */ (spec);
|
|
590
|
+
|
|
591
|
+
if (!version) continue;
|
|
592
|
+
|
|
593
|
+
if (version.startsWith('link:')) {
|
|
594
|
+
// Workspace link - resolve and follow
|
|
595
|
+
const linkedPath = version.slice(5); // Remove 'link:'
|
|
596
|
+
const resolvedPath = this.#resolveRelativePath(ws, linkedPath);
|
|
597
|
+
if (!visitedWorkspaces.has(resolvedPath)) {
|
|
598
|
+
workspaceQueue.push(resolvedPath);
|
|
599
|
+
}
|
|
600
|
+
} else {
|
|
601
|
+
// External dependency
|
|
602
|
+
externalDeps.add(name);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Phase 2: Traverse external deps and their transitive dependencies
|
|
609
|
+
// In v9, dependencies are in snapshots; in older versions, they're in packages
|
|
610
|
+
const depsSource = this.#snapshots || this.#packages || {};
|
|
611
|
+
const visitedDeps = new Set();
|
|
612
|
+
const depQueue = [...externalDeps];
|
|
613
|
+
|
|
614
|
+
while (depQueue.length > 0) {
|
|
615
|
+
const name = /** @type {string} */ (depQueue.shift());
|
|
616
|
+
if (visitedDeps.has(name)) continue;
|
|
617
|
+
visitedDeps.add(name);
|
|
618
|
+
|
|
619
|
+
// Find this package in our deps map
|
|
620
|
+
const dep = this.#findByName(name);
|
|
621
|
+
if (dep) {
|
|
622
|
+
result.set(`${dep.name}@${dep.version}`, dep);
|
|
623
|
+
|
|
624
|
+
// Find transitive deps from snapshots/packages
|
|
625
|
+
// Keys are like "name@version" or "@scope/name@version" or with peer deps suffix
|
|
626
|
+
// In pnpm v9, same package can have multiple entries with different peer configurations
|
|
627
|
+
// e.g., "ts-api-utils@1.2.1(typescript@4.9.5)" and "ts-api-utils@1.2.1(typescript@5.3.3)"
|
|
628
|
+
// We must process ALL matching entries to capture deps from all peer variants
|
|
629
|
+
for (const [key, pkg] of Object.entries(depsSource)) {
|
|
630
|
+
const keyPackageName = this.#extractPnpmPackageName(key);
|
|
631
|
+
if (keyPackageName === name) {
|
|
632
|
+
// Found a package entry, get its dependencies
|
|
633
|
+
for (const transName of Object.keys(pkg.dependencies || {})) {
|
|
634
|
+
if (!visitedDeps.has(transName)) {
|
|
635
|
+
depQueue.push(transName);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
if (optional) {
|
|
639
|
+
for (const transName of Object.keys(pkg.optionalDependencies || {})) {
|
|
640
|
+
if (!visitedDeps.has(transName)) {
|
|
641
|
+
depQueue.push(transName);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
// NOTE: No break - continue processing all peer variants of this package
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return new FlatlockSet(INTERNAL, result, null, null, null, this.#type);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* npm-specific resolution with workspace support
|
|
656
|
+
*
|
|
657
|
+
* npm monorepos have workspace packages that are symlinked from node_modules.
|
|
658
|
+
* Packages can have nested node_modules with different versions.
|
|
659
|
+
*
|
|
660
|
+
* @param {Set<string>} _seeds - Seed dependency names (unused, derived from lockfile)
|
|
661
|
+
* @param {string} workspacePath - Path to workspace (e.g., 'workspaces/arborist')
|
|
662
|
+
* @param {{ dev: boolean, optional: boolean, peer: boolean, workspacePackages: Record<string, {name: string, version: string}> }} options
|
|
663
|
+
* @returns {FlatlockSet}
|
|
664
|
+
*/
|
|
665
|
+
#dependenciesOfNpm(_seeds, workspacePath, { dev, optional, peer, workspacePackages }) {
|
|
666
|
+
/** @type {Map<string, Dependency>} */
|
|
667
|
+
const result = new Map();
|
|
668
|
+
|
|
669
|
+
// Build name -> workspace path mapping
|
|
670
|
+
const nameToWorkspace = new Map();
|
|
671
|
+
for (const [wsPath, pkg] of Object.entries(workspacePackages)) {
|
|
672
|
+
nameToWorkspace.set(pkg.name, wsPath);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Queue entries: { name, contextPath } where contextPath is where to look for nested node_modules
|
|
676
|
+
// contextPath is either a workspace path or a node_modules package path
|
|
677
|
+
const queue = [];
|
|
678
|
+
const visited = new Set(); // Track "name@contextPath" to handle same package at different contexts
|
|
679
|
+
|
|
680
|
+
// Phase 1: Process workspace packages
|
|
681
|
+
const visitedWorkspaces = new Set();
|
|
682
|
+
const workspaceQueue = [workspacePath];
|
|
683
|
+
|
|
684
|
+
while (workspaceQueue.length > 0) {
|
|
685
|
+
const wsPath = /** @type {string} */ (workspaceQueue.shift());
|
|
686
|
+
if (visitedWorkspaces.has(wsPath)) continue;
|
|
687
|
+
visitedWorkspaces.add(wsPath);
|
|
688
|
+
|
|
689
|
+
const wsEntry = this.#packages?.[wsPath];
|
|
690
|
+
if (!wsEntry) continue;
|
|
691
|
+
|
|
692
|
+
// Emit this workspace package (except starting workspace)
|
|
693
|
+
// Name comes from workspacePackages map since lockfile may not have it
|
|
694
|
+
if (wsPath !== workspacePath) {
|
|
695
|
+
const wsPkg = workspacePackages[wsPath];
|
|
696
|
+
if (wsPkg?.name && wsPkg?.version) {
|
|
697
|
+
result.set(`${wsPkg.name}@${wsPkg.version}`, {
|
|
698
|
+
name: wsPkg.name,
|
|
699
|
+
version: wsPkg.version
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Collect dependencies
|
|
705
|
+
const depSections = [wsEntry.dependencies];
|
|
706
|
+
if (dev) depSections.push(wsEntry.devDependencies);
|
|
707
|
+
if (optional) depSections.push(wsEntry.optionalDependencies);
|
|
708
|
+
if (peer) depSections.push(wsEntry.peerDependencies);
|
|
709
|
+
|
|
710
|
+
for (const deps of depSections) {
|
|
711
|
+
if (!deps) continue;
|
|
712
|
+
for (const name of Object.keys(deps)) {
|
|
713
|
+
if (nameToWorkspace.has(name)) {
|
|
714
|
+
const linkedWsPath = nameToWorkspace.get(name);
|
|
715
|
+
if (!visitedWorkspaces.has(linkedWsPath)) {
|
|
716
|
+
workspaceQueue.push(linkedWsPath);
|
|
717
|
+
}
|
|
718
|
+
} else {
|
|
719
|
+
// Add to queue with workspace context
|
|
720
|
+
queue.push({ name, contextPath: wsPath });
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Phase 2: Traverse external dependencies with context-aware resolution
|
|
727
|
+
while (queue.length > 0) {
|
|
728
|
+
const { name, contextPath } = /** @type {{name: string, contextPath: string}} */ (
|
|
729
|
+
queue.shift()
|
|
730
|
+
);
|
|
731
|
+
const visitKey = `${name}@${contextPath}`;
|
|
732
|
+
if (visited.has(visitKey)) continue;
|
|
733
|
+
visited.add(visitKey);
|
|
734
|
+
|
|
735
|
+
// Check if this is a workspace package
|
|
736
|
+
if (nameToWorkspace.has(name)) {
|
|
737
|
+
const wsPath = nameToWorkspace.get(name);
|
|
738
|
+
const wsEntry = this.#packages?.[wsPath];
|
|
739
|
+
if (wsEntry?.name && wsEntry?.version) {
|
|
740
|
+
result.set(`${wsEntry.name}@${wsEntry.version}`, {
|
|
741
|
+
name: wsEntry.name,
|
|
742
|
+
version: wsEntry.version
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Resolve package using npm's resolution algorithm:
|
|
749
|
+
// 1. Check nested node_modules at contextPath
|
|
750
|
+
// 2. Walk up the path checking each parent's node_modules
|
|
751
|
+
// 3. Fall back to root node_modules
|
|
752
|
+
let entry = null;
|
|
753
|
+
let pkgPath = null;
|
|
754
|
+
|
|
755
|
+
// Try nested node_modules at context path
|
|
756
|
+
const nestedKey = `${contextPath}/node_modules/${name}`;
|
|
757
|
+
if (this.#packages?.[nestedKey]) {
|
|
758
|
+
entry = this.#packages[nestedKey];
|
|
759
|
+
pkgPath = nestedKey;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Walk up context path looking for the package
|
|
763
|
+
if (!entry) {
|
|
764
|
+
const parts = contextPath.split('/');
|
|
765
|
+
while (parts.length > 0) {
|
|
766
|
+
const parentKey = `${parts.join('/')}/node_modules/${name}`;
|
|
767
|
+
if (this.#packages?.[parentKey]) {
|
|
768
|
+
entry = this.#packages[parentKey];
|
|
769
|
+
pkgPath = parentKey;
|
|
770
|
+
break;
|
|
771
|
+
}
|
|
772
|
+
parts.pop();
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Fall back to root node_modules
|
|
777
|
+
if (!entry) {
|
|
778
|
+
const rootKey = `node_modules/${name}`;
|
|
779
|
+
if (this.#packages?.[rootKey]) {
|
|
780
|
+
entry = this.#packages[rootKey];
|
|
781
|
+
pkgPath = rootKey;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if (!entry?.version) continue;
|
|
786
|
+
|
|
787
|
+
// Follow symlinks for workspace packages
|
|
788
|
+
if (entry.link && entry.resolved) {
|
|
789
|
+
const resolvedEntry = this.#packages?.[entry.resolved];
|
|
790
|
+
if (resolvedEntry?.version) {
|
|
791
|
+
entry = resolvedEntry;
|
|
792
|
+
pkgPath = entry.resolved;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
result.set(`${name}@${entry.version}`, { name, version: entry.version });
|
|
797
|
+
|
|
798
|
+
// Queue transitive dependencies with this package's path as context
|
|
799
|
+
// The context should be the directory containing this package's node_modules
|
|
800
|
+
const depContext = pkgPath;
|
|
801
|
+
|
|
802
|
+
for (const transName of Object.keys(entry.dependencies || {})) {
|
|
803
|
+
queue.push({ name: transName, contextPath: depContext });
|
|
804
|
+
}
|
|
805
|
+
if (optional) {
|
|
806
|
+
for (const transName of Object.keys(entry.optionalDependencies || {})) {
|
|
807
|
+
queue.push({ name: transName, contextPath: depContext });
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
return new FlatlockSet(INTERNAL, result, null, null, null, this.#type);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Extract package name from a pnpm snapshot/packages key.
|
|
817
|
+
* Handles formats like:
|
|
818
|
+
* - name@version
|
|
819
|
+
* - @scope/name@version
|
|
820
|
+
* - name@version(peer@peerVersion)
|
|
821
|
+
* - @scope/name@version(peer@peerVersion)
|
|
822
|
+
* @param {string} key - The snapshot key
|
|
823
|
+
* @returns {string} The package name
|
|
824
|
+
*/
|
|
825
|
+
#extractPnpmPackageName(key) {
|
|
826
|
+
// For scoped packages (@scope/name), find the second @
|
|
827
|
+
if (key.startsWith('@')) {
|
|
828
|
+
const secondAt = key.indexOf('@', 1);
|
|
829
|
+
return secondAt === -1 ? key : key.slice(0, secondAt);
|
|
830
|
+
}
|
|
831
|
+
// For unscoped packages, find the first @
|
|
832
|
+
const firstAt = key.indexOf('@');
|
|
833
|
+
return firstAt === -1 ? key : key.slice(0, firstAt);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Resolve a relative path from a workspace path
|
|
838
|
+
* @param {string} from - Current workspace path (e.g., 'packages/vue')
|
|
839
|
+
* @param {string} relative - Relative path (e.g., '../compiler-dom')
|
|
840
|
+
* @returns {string} Resolved path (e.g., 'packages/compiler-dom')
|
|
841
|
+
*/
|
|
842
|
+
#resolveRelativePath(from, relative) {
|
|
843
|
+
const parts = from.split('/');
|
|
844
|
+
const relParts = relative.split('/');
|
|
845
|
+
|
|
846
|
+
for (const p of relParts) {
|
|
847
|
+
if (p === '..') {
|
|
848
|
+
parts.pop();
|
|
849
|
+
} else if (p !== '.') {
|
|
850
|
+
parts.push(p);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
return parts.join('/');
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Yarn berry-specific resolution with workspace support
|
|
859
|
+
*
|
|
860
|
+
* Yarn berry workspace packages use `workspace:*` or `workspace:^` specifiers.
|
|
861
|
+
* These need to be resolved to actual package versions from workspacePackages.
|
|
862
|
+
*
|
|
863
|
+
* @param {Set<string>} _seeds - Seed dependency names (unused, derived from packageJson)
|
|
864
|
+
* @param {PackageJson} packageJson - The workspace's package.json
|
|
865
|
+
* @param {{ dev: boolean, optional: boolean, peer: boolean, workspacePackages: Record<string, {name: string, version: string}> }} options
|
|
424
866
|
* @returns {FlatlockSet}
|
|
425
867
|
*/
|
|
426
|
-
#
|
|
868
|
+
#dependenciesOfYarnBerry(_seeds, packageJson, { dev, optional, peer, workspacePackages }) {
|
|
869
|
+
/** @type {Map<string, Dependency>} */
|
|
870
|
+
const result = new Map();
|
|
871
|
+
/** @type {Set<string>} */
|
|
872
|
+
const visited = new Set(); // Track by name@version
|
|
873
|
+
|
|
874
|
+
// Build a map of package name -> workspace path for quick lookup
|
|
875
|
+
const nameToWorkspace = new Map();
|
|
876
|
+
for (const [wsPath, pkg] of Object.entries(workspacePackages)) {
|
|
877
|
+
nameToWorkspace.set(pkg.name, { path: wsPath, ...pkg });
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Get dependency specifiers from package.json
|
|
881
|
+
const rootSpecs = {
|
|
882
|
+
...packageJson.dependencies,
|
|
883
|
+
...(dev ? packageJson.devDependencies : {}),
|
|
884
|
+
...(optional ? packageJson.optionalDependencies : {}),
|
|
885
|
+
...(peer ? packageJson.peerDependencies : {})
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
// Queue items are {name, spec} pairs
|
|
889
|
+
/** @type {Array<{name: string, spec: string}>} */
|
|
890
|
+
const queue = Object.entries(rootSpecs).map(([name, spec]) => ({ name, spec }));
|
|
891
|
+
|
|
892
|
+
while (queue.length > 0) {
|
|
893
|
+
const { name, spec } = /** @type {{name: string, spec: string}} */ (queue.shift());
|
|
894
|
+
|
|
895
|
+
const isWorkspaceDep = typeof spec === 'string' && spec.startsWith('workspace:');
|
|
896
|
+
|
|
897
|
+
let dep;
|
|
898
|
+
let entry;
|
|
899
|
+
|
|
900
|
+
if (isWorkspaceDep && nameToWorkspace.has(name)) {
|
|
901
|
+
// Use workspace package info
|
|
902
|
+
const wsPkg = nameToWorkspace.get(name);
|
|
903
|
+
dep = { name: wsPkg.name, version: wsPkg.version };
|
|
904
|
+
entry = this.#getYarnWorkspaceEntry(name);
|
|
905
|
+
} else if (nameToWorkspace.has(name)) {
|
|
906
|
+
// It's a workspace package referenced transitively
|
|
907
|
+
const wsPkg = nameToWorkspace.get(name);
|
|
908
|
+
dep = { name: wsPkg.name, version: wsPkg.version };
|
|
909
|
+
entry = this.#getYarnWorkspaceEntry(name);
|
|
910
|
+
} else {
|
|
911
|
+
// Regular npm dependency - use spec to find correct version
|
|
912
|
+
entry = this.#getYarnBerryEntryBySpec(name, spec);
|
|
913
|
+
if (entry) {
|
|
914
|
+
dep = { name, version: entry.version };
|
|
915
|
+
} else {
|
|
916
|
+
// Fallback to first match
|
|
917
|
+
dep = this.#findByName(name);
|
|
918
|
+
if (dep) {
|
|
919
|
+
entry = this.#getYarnBerryEntry(name, dep.version);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
if (!dep) continue;
|
|
925
|
+
|
|
926
|
+
const key = `${dep.name}@${dep.version}`;
|
|
927
|
+
if (visited.has(key)) continue;
|
|
928
|
+
visited.add(key);
|
|
929
|
+
|
|
930
|
+
result.set(key, dep);
|
|
931
|
+
|
|
932
|
+
// Get transitive deps
|
|
933
|
+
// For workspace packages, ALWAYS use workspacePackages dependencies (from package.json)
|
|
934
|
+
// instead of lockfile entry (which merges prod + dev deps).
|
|
935
|
+
// Even if the workspace has no deps (null/empty), we should NOT fall back to lockfile.
|
|
936
|
+
const wsInfo = nameToWorkspace.get(name);
|
|
937
|
+
|
|
938
|
+
if (wsInfo) {
|
|
939
|
+
// This is a workspace package - use package.json deps (respects prod/dev separation)
|
|
940
|
+
const depsToTraverse = {
|
|
941
|
+
...(wsInfo.dependencies || {}),
|
|
942
|
+
...(dev ? wsInfo.devDependencies || {} : {}),
|
|
943
|
+
...(optional ? wsInfo.optionalDependencies || {} : {}),
|
|
944
|
+
...(peer ? wsInfo.peerDependencies || {} : {})
|
|
945
|
+
};
|
|
946
|
+
for (const [transName, transSpec] of Object.entries(depsToTraverse)) {
|
|
947
|
+
queue.push({ name: transName, spec: transSpec });
|
|
948
|
+
}
|
|
949
|
+
} else if (entry) {
|
|
950
|
+
// Non-workspace package - use lockfile entry
|
|
951
|
+
for (const [transName, transSpec] of Object.entries(entry.dependencies || {})) {
|
|
952
|
+
queue.push({ name: transName, spec: transSpec });
|
|
953
|
+
}
|
|
954
|
+
if (optional) {
|
|
955
|
+
for (const [transName, transSpec] of Object.entries(entry.optionalDependencies || {})) {
|
|
956
|
+
queue.push({ name: transName, spec: transSpec });
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
return new FlatlockSet(INTERNAL, result, null, null, null, this.#type);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* Yarn classic-specific resolution with workspace support
|
|
967
|
+
*
|
|
968
|
+
* Yarn classic workspace packages are NOT in the lockfile - they're resolved
|
|
969
|
+
* from the filesystem. So when a dependency isn't found in the lockfile,
|
|
970
|
+
* we check if it's a workspace package.
|
|
971
|
+
*
|
|
972
|
+
* @param {Set<string>} seeds - Seed dependency names from package.json
|
|
973
|
+
* @param {PackageJson} _packageJson - The workspace's package.json (unused)
|
|
974
|
+
* @param {{ dev: boolean, optional: boolean, peer: boolean, workspacePackages: Record<string, {name: string, version: string}> }} options
|
|
975
|
+
* @returns {FlatlockSet}
|
|
976
|
+
*/
|
|
977
|
+
#dependenciesOfYarnClassic(
|
|
978
|
+
seeds,
|
|
979
|
+
_packageJson,
|
|
980
|
+
{ dev: _dev, optional, peer, workspacePackages }
|
|
981
|
+
) {
|
|
427
982
|
/** @type {Map<string, Dependency>} */
|
|
428
983
|
const result = new Map();
|
|
429
984
|
/** @type {Set<string>} */
|
|
@@ -431,29 +986,33 @@ export class FlatlockSet {
|
|
|
431
986
|
/** @type {string[]} */
|
|
432
987
|
const queue = [...seeds];
|
|
433
988
|
|
|
434
|
-
//
|
|
435
|
-
const
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
...(optional ? importer.optionalDependencies : {}),
|
|
440
|
-
...(peer ? importer.peerDependencies : {})
|
|
441
|
-
};
|
|
989
|
+
// Build a map of package name -> workspace info for quick lookup
|
|
990
|
+
const nameToWorkspace = new Map();
|
|
991
|
+
for (const [wsPath, pkg] of Object.entries(workspacePackages)) {
|
|
992
|
+
nameToWorkspace.set(pkg.name, { path: wsPath, ...pkg });
|
|
993
|
+
}
|
|
442
994
|
|
|
443
995
|
while (queue.length > 0) {
|
|
444
996
|
const name = /** @type {string} */ (queue.shift());
|
|
445
997
|
if (visited.has(name)) continue;
|
|
446
998
|
visited.add(name);
|
|
447
999
|
|
|
448
|
-
// Get resolved version from importer or find in deps
|
|
449
|
-
const version = resolvedDeps[name];
|
|
450
1000
|
let dep;
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
1001
|
+
let entry;
|
|
1002
|
+
|
|
1003
|
+
// Check if this is a workspace package
|
|
1004
|
+
if (nameToWorkspace.has(name)) {
|
|
1005
|
+
// Use workspace package info
|
|
1006
|
+
const wsPkg = nameToWorkspace.get(name);
|
|
1007
|
+
dep = { name: wsPkg.name, version: wsPkg.version };
|
|
1008
|
+
// Workspace packages don't have lockfile entries in yarn classic
|
|
1009
|
+
entry = null;
|
|
455
1010
|
} else {
|
|
1011
|
+
// Regular npm dependency - find in lockfile
|
|
456
1012
|
dep = this.#findByName(name);
|
|
1013
|
+
if (dep) {
|
|
1014
|
+
entry = this.#getYarnClassicEntry(name, dep.version);
|
|
1015
|
+
}
|
|
457
1016
|
}
|
|
458
1017
|
|
|
459
1018
|
if (!dep) continue;
|
|
@@ -461,22 +1020,135 @@ export class FlatlockSet {
|
|
|
461
1020
|
const key = `${dep.name}@${dep.version}`;
|
|
462
1021
|
result.set(key, dep);
|
|
463
1022
|
|
|
464
|
-
// Get transitive deps
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
if (pkgEntry) {
|
|
468
|
-
for (const transName of Object.keys(pkgEntry.dependencies || {})) {
|
|
1023
|
+
// Get transitive deps from lockfile entry
|
|
1024
|
+
if (entry) {
|
|
1025
|
+
for (const transName of Object.keys(entry.dependencies || {})) {
|
|
469
1026
|
if (!visited.has(transName)) queue.push(transName);
|
|
470
1027
|
}
|
|
471
1028
|
if (optional) {
|
|
472
|
-
for (const transName of Object.keys(
|
|
1029
|
+
for (const transName of Object.keys(entry.optionalDependencies || {})) {
|
|
1030
|
+
if (!visited.has(transName)) queue.push(transName);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
if (peer) {
|
|
1034
|
+
for (const transName of Object.keys(entry.peerDependencies || {})) {
|
|
473
1035
|
if (!visited.has(transName)) queue.push(transName);
|
|
474
1036
|
}
|
|
475
1037
|
}
|
|
476
1038
|
}
|
|
477
1039
|
}
|
|
478
1040
|
|
|
479
|
-
return new FlatlockSet(INTERNAL, result, null, null, this.#type);
|
|
1041
|
+
return new FlatlockSet(INTERNAL, result, null, null, null, this.#type);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/**
|
|
1045
|
+
* Find a yarn classic entry by name and version
|
|
1046
|
+
* @param {string} name
|
|
1047
|
+
* @param {string} version
|
|
1048
|
+
* @returns {any}
|
|
1049
|
+
*/
|
|
1050
|
+
#getYarnClassicEntry(name, version) {
|
|
1051
|
+
if (!this.#packages) return null;
|
|
1052
|
+
for (const [key, entry] of Object.entries(this.#packages)) {
|
|
1053
|
+
if (entry.version === version) {
|
|
1054
|
+
const keyName = parseYarnClassicKey(key);
|
|
1055
|
+
if (keyName === name) return entry;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
return null;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Find a yarn berry workspace entry by package name
|
|
1063
|
+
* @param {string} name
|
|
1064
|
+
* @returns {any}
|
|
1065
|
+
*/
|
|
1066
|
+
#getYarnWorkspaceEntry(name) {
|
|
1067
|
+
if (!this.#packages) return null;
|
|
1068
|
+
for (const [key, entry] of Object.entries(this.#packages)) {
|
|
1069
|
+
if (
|
|
1070
|
+
key.includes('@workspace:') &&
|
|
1071
|
+
(key.startsWith(`${name}@`) || key.includes(`/${name}@`))
|
|
1072
|
+
) {
|
|
1073
|
+
return entry;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
return null;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
/**
|
|
1080
|
+
* Find a yarn berry npm entry by name and version
|
|
1081
|
+
* @param {string} name
|
|
1082
|
+
* @param {string} version
|
|
1083
|
+
* @returns {any}
|
|
1084
|
+
*/
|
|
1085
|
+
#getYarnBerryEntry(name, version) {
|
|
1086
|
+
if (!this.#packages) return null;
|
|
1087
|
+
// Yarn berry keys are like "@babel/types@npm:^7.24.0" and resolution is "@babel/types@npm:7.24.0"
|
|
1088
|
+
for (const [key, entry] of Object.entries(this.#packages)) {
|
|
1089
|
+
if (entry.version === version && key.includes(`${name}@`)) {
|
|
1090
|
+
return entry;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
return null;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* Find a yarn berry entry by spec (e.g., "npm:^3.1.0")
|
|
1098
|
+
* Yarn berry keys contain the spec like "p-limit@npm:^3.1.0"
|
|
1099
|
+
* @param {string} name
|
|
1100
|
+
* @param {string} spec - The spec like "npm:^3.1.0" or "^3.1.0"
|
|
1101
|
+
* @returns {any}
|
|
1102
|
+
*/
|
|
1103
|
+
#getYarnBerryEntryBySpec(name, spec) {
|
|
1104
|
+
if (!this.#packages || !spec) return null;
|
|
1105
|
+
|
|
1106
|
+
// Normalize spec - yarn specs may or may not have npm: prefix
|
|
1107
|
+
// Key format: "p-limit@npm:^3.0.2, p-limit@npm:^3.1.0"
|
|
1108
|
+
const normalizedSpec = spec.startsWith('npm:') ? spec : `npm:${spec}`;
|
|
1109
|
+
const searchKey = `${name}@${normalizedSpec}`;
|
|
1110
|
+
|
|
1111
|
+
for (const [key, entry] of Object.entries(this.#packages)) {
|
|
1112
|
+
// Key can have multiple specs comma-separated
|
|
1113
|
+
if (key.includes(searchKey)) {
|
|
1114
|
+
return entry;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
return null;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/**
|
|
1121
|
+
* Extract workspace packages from yarn berry lockfile.
|
|
1122
|
+
* Yarn berry lockfiles contain workspace entries with `@workspace:` protocol.
|
|
1123
|
+
* @returns {Record<string, {name: string, version: string}>}
|
|
1124
|
+
*/
|
|
1125
|
+
#extractYarnBerryWorkspaces() {
|
|
1126
|
+
/** @type {Record<string, {name: string, version: string}>} */
|
|
1127
|
+
const workspacePackages = {};
|
|
1128
|
+
|
|
1129
|
+
for (const [key, entry] of Object.entries(this.#packages || {})) {
|
|
1130
|
+
if (!key.includes('@workspace:')) continue;
|
|
1131
|
+
|
|
1132
|
+
// Handle potentially multiple descriptors (comma-separated)
|
|
1133
|
+
const descriptors = key.split(', ');
|
|
1134
|
+
for (const descriptor of descriptors) {
|
|
1135
|
+
if (!descriptor.includes('@workspace:')) continue;
|
|
1136
|
+
|
|
1137
|
+
// Find @workspace: and extract path after it
|
|
1138
|
+
const wsIndex = descriptor.indexOf('@workspace:');
|
|
1139
|
+
const path = descriptor.slice(wsIndex + '@workspace:'.length);
|
|
1140
|
+
|
|
1141
|
+
// Extract name - everything before @workspace:
|
|
1142
|
+
const name = descriptor.slice(0, wsIndex);
|
|
1143
|
+
|
|
1144
|
+
workspacePackages[path] = {
|
|
1145
|
+
name,
|
|
1146
|
+
version: entry.version || '0.0.0'
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
return workspacePackages;
|
|
480
1152
|
}
|
|
481
1153
|
|
|
482
1154
|
/**
|
|
@@ -517,7 +1189,7 @@ export class FlatlockSet {
|
|
|
517
1189
|
}
|
|
518
1190
|
}
|
|
519
1191
|
|
|
520
|
-
return new FlatlockSet(INTERNAL, result, null, null, this.#type);
|
|
1192
|
+
return new FlatlockSet(INTERNAL, result, null, null, null, this.#type);
|
|
521
1193
|
}
|
|
522
1194
|
|
|
523
1195
|
/**
|
|
@@ -553,6 +1225,14 @@ export class FlatlockSet {
|
|
|
553
1225
|
if (entry?.version) {
|
|
554
1226
|
return this.get(`${name}@${entry.version}`);
|
|
555
1227
|
}
|
|
1228
|
+
// Follow workspace symlinks: link:true with resolved points to workspace
|
|
1229
|
+
if (entry?.link && entry?.resolved) {
|
|
1230
|
+
const workspaceEntry = this.#packages?.[entry.resolved];
|
|
1231
|
+
if (workspaceEntry?.version) {
|
|
1232
|
+
// Return a synthetic dependency for the workspace package
|
|
1233
|
+
return { name, version: workspaceEntry.version, link: true };
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
556
1236
|
}
|
|
557
1237
|
|
|
558
1238
|
// Fallback: iterate deps (may return arbitrary version if multiple)
|
|
@@ -580,7 +1260,16 @@ export class FlatlockSet {
|
|
|
580
1260
|
if (this.#packages[localKey]) return this.#packages[localKey];
|
|
581
1261
|
}
|
|
582
1262
|
// Fall back to hoisted
|
|
583
|
-
|
|
1263
|
+
const hoistedKey = `node_modules/${name}`;
|
|
1264
|
+
const hoistedEntry = this.#packages[hoistedKey];
|
|
1265
|
+
if (hoistedEntry) {
|
|
1266
|
+
// Follow workspace symlinks to get the actual package entry
|
|
1267
|
+
if (hoistedEntry.link && hoistedEntry.resolved) {
|
|
1268
|
+
return this.#packages[hoistedEntry.resolved] || hoistedEntry;
|
|
1269
|
+
}
|
|
1270
|
+
return hoistedEntry;
|
|
1271
|
+
}
|
|
1272
|
+
return null;
|
|
584
1273
|
}
|
|
585
1274
|
case Type.PNPM: {
|
|
586
1275
|
return this.#packages[`/${name}@${version}`] || null;
|