flatlock 1.1.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 +95 -1
- package/bin/flatcover.js +398 -0
- package/bin/flatlock-cmp.js +71 -45
- package/bin/flatlock.js +158 -0
- package/package.json +21 -8
- package/src/compare.js +385 -28
- package/src/detect.js +3 -4
- package/src/index.js +9 -2
- package/src/parsers/index.js +24 -4
- package/src/parsers/npm.js +144 -14
- package/src/parsers/pnpm/detect.js +198 -0
- package/src/parsers/pnpm/index.js +359 -0
- package/src/parsers/pnpm/internal.js +41 -0
- package/src/parsers/pnpm/shrinkwrap.js +241 -0
- package/src/parsers/pnpm/v5.js +225 -0
- package/src/parsers/pnpm/v6plus.js +290 -0
- package/src/parsers/pnpm.js +11 -89
- package/src/parsers/types.js +10 -0
- package/src/parsers/yarn-berry.js +271 -36
- package/src/parsers/yarn-classic.js +81 -21
- package/src/set.js +1307 -0
- package/dist/compare.d.ts +0 -63
- 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 -70
- 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 -82
- package/dist/parsers/npm.d.ts.map +0 -1
- package/dist/parsers/pnpm.d.ts +0 -60
- package/dist/parsers/pnpm.d.ts.map +0 -1
- package/dist/parsers/yarn-berry.d.ts +0 -65
- package/dist/parsers/yarn-berry.d.ts.map +0 -1
- package/dist/parsers/yarn-classic.d.ts +0 -64
- 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/src/set.js
ADDED
|
@@ -0,0 +1,1307 @@
|
|
|
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
|
+
buildNpmWorkspacePackages,
|
|
7
|
+
buildPnpmWorkspacePackages,
|
|
8
|
+
buildYarnBerryWorkspacePackages,
|
|
9
|
+
extractNpmWorkspacePaths,
|
|
10
|
+
extractPnpmWorkspacePaths,
|
|
11
|
+
extractYarnBerryWorkspacePaths,
|
|
12
|
+
fromPackageLock,
|
|
13
|
+
fromPnpmLock,
|
|
14
|
+
fromYarnBerryLock,
|
|
15
|
+
fromYarnClassicLock,
|
|
16
|
+
parseYarnBerryKey,
|
|
17
|
+
parseYarnClassic,
|
|
18
|
+
parseYarnClassicKey
|
|
19
|
+
} from './parsers/index.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {import('./parsers/npm.js').Dependency} Dependency
|
|
23
|
+
* @typedef {import('./detect.js').LockfileType} LockfileType
|
|
24
|
+
*/
|
|
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
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {Object} DependenciesOfOptions
|
|
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
|
|
40
|
+
* @property {boolean} [dev=false] - Include devDependencies
|
|
41
|
+
* @property {boolean} [optional=true] - Include optionalDependencies
|
|
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)
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {Object} FromStringOptions
|
|
48
|
+
* @property {string} [path] - Path hint for type detection
|
|
49
|
+
* @property {LockfileType} [type] - Explicit lockfile type
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @typedef {Object} PackageJson
|
|
54
|
+
* @property {Record<string, string>} [dependencies]
|
|
55
|
+
* @property {Record<string, string>} [devDependencies]
|
|
56
|
+
* @property {Record<string, string>} [optionalDependencies]
|
|
57
|
+
* @property {Record<string, string>} [peerDependencies]
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @typedef {Record<string, any>} LockfilePackages
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @typedef {Record<string, any>} LockfileImporters
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
/** Symbol to prevent direct construction */
|
|
69
|
+
const INTERNAL = Symbol('FlatlockSet.internal');
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A Set-like container for lockfile dependencies.
|
|
73
|
+
*
|
|
74
|
+
* Identity is determined by name@version. Two dependencies with the same
|
|
75
|
+
* name and version are considered equal, regardless of integrity or resolved URL.
|
|
76
|
+
*
|
|
77
|
+
* All set operations return new FlatlockSet instances (immutable pattern).
|
|
78
|
+
*
|
|
79
|
+
* NOTE: Set operations (union, intersection, difference) return sets that
|
|
80
|
+
* cannot use dependenciesOf() because they lack lockfile traversal data.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* const set = await FlatlockSet.fromPath('./package-lock.json');
|
|
84
|
+
* console.log(set.size); // 1234
|
|
85
|
+
* console.log(set.has('lodash@4.17.21')); // true
|
|
86
|
+
*
|
|
87
|
+
* // Get dependencies for a specific workspace
|
|
88
|
+
* const pkg = JSON.parse(await readFile('./packages/foo/package.json'));
|
|
89
|
+
* const subset = await set.dependenciesOf(pkg, { workspacePath: 'packages/foo', repoDir: '.' });
|
|
90
|
+
*
|
|
91
|
+
* // Set operations
|
|
92
|
+
* const other = await FlatlockSet.fromPath('./other-lock.json');
|
|
93
|
+
* const common = set.intersection(other);
|
|
94
|
+
*/
|
|
95
|
+
export class FlatlockSet {
|
|
96
|
+
/** @type {Map<string, Dependency>} */
|
|
97
|
+
#deps = new Map();
|
|
98
|
+
|
|
99
|
+
/** @type {LockfilePackages | null} Raw lockfile packages for traversal */
|
|
100
|
+
#packages = null;
|
|
101
|
+
|
|
102
|
+
/** @type {LockfileImporters | null} Workspace importers (pnpm) */
|
|
103
|
+
#importers = null;
|
|
104
|
+
|
|
105
|
+
/** @type {Record<string, any> | null} Snapshots (pnpm v9) */
|
|
106
|
+
#snapshots = null;
|
|
107
|
+
|
|
108
|
+
/** @type {LockfileType | null} */
|
|
109
|
+
#type = null;
|
|
110
|
+
|
|
111
|
+
/** @type {boolean} Whether this set supports dependenciesOf */
|
|
112
|
+
#canTraverse = false;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @param {symbol} internal - Must be INTERNAL symbol
|
|
116
|
+
* @param {Map<string, Dependency>} deps
|
|
117
|
+
* @param {LockfilePackages | null} packages
|
|
118
|
+
* @param {LockfileImporters | null} importers
|
|
119
|
+
* @param {Record<string, any> | null} snapshots
|
|
120
|
+
* @param {LockfileType | null} type
|
|
121
|
+
*/
|
|
122
|
+
constructor(internal, deps, packages, importers, snapshots, type) {
|
|
123
|
+
if (internal !== INTERNAL) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
'FlatlockSet cannot be constructed directly. Use FlatlockSet.fromPath() or FlatlockSet.fromString()'
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
this.#deps = deps;
|
|
129
|
+
this.#packages = packages;
|
|
130
|
+
this.#importers = importers;
|
|
131
|
+
this.#snapshots = snapshots;
|
|
132
|
+
this.#type = type;
|
|
133
|
+
this.#canTraverse = packages !== null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Create FlatlockSet from lockfile path (auto-detect type)
|
|
138
|
+
* @param {string} path - Path to lockfile
|
|
139
|
+
* @param {FromStringOptions} [options] - Parser options
|
|
140
|
+
* @returns {Promise<FlatlockSet>}
|
|
141
|
+
*/
|
|
142
|
+
static async fromPath(path, options = {}) {
|
|
143
|
+
const content = await readFile(path, 'utf8');
|
|
144
|
+
return FlatlockSet.fromString(content, { ...options, path });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Create FlatlockSet from lockfile string
|
|
149
|
+
* @param {string} content - Lockfile content
|
|
150
|
+
* @param {FromStringOptions} [options] - Parser options
|
|
151
|
+
* @returns {FlatlockSet}
|
|
152
|
+
*/
|
|
153
|
+
static fromString(content, options = {}) {
|
|
154
|
+
const type = options.type || detectType({ path: options.path, content });
|
|
155
|
+
|
|
156
|
+
if (!type) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
'Unable to detect lockfile type. ' +
|
|
159
|
+
'Provide options.type explicitly or ensure content is a valid lockfile format.'
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Parse once, extract both deps and raw data
|
|
164
|
+
const { deps, packages, importers, snapshots } = FlatlockSet.#parseAll(content, type, options);
|
|
165
|
+
|
|
166
|
+
return new FlatlockSet(INTERNAL, deps, packages, importers, snapshots, type);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Parse lockfile once, returning both processed deps and raw data
|
|
171
|
+
* @param {string} content
|
|
172
|
+
* @param {LockfileType} type
|
|
173
|
+
* @param {FromStringOptions} options
|
|
174
|
+
* @returns {{ deps: Map<string, Dependency>, packages: LockfilePackages, importers: LockfileImporters | null, snapshots: Record<string, any> | null }}
|
|
175
|
+
*/
|
|
176
|
+
static #parseAll(content, type, options) {
|
|
177
|
+
/** @type {Map<string, Dependency>} */
|
|
178
|
+
const deps = new Map();
|
|
179
|
+
/** @type {LockfilePackages} */
|
|
180
|
+
let packages = {};
|
|
181
|
+
/** @type {LockfileImporters | null} */
|
|
182
|
+
let importers = null;
|
|
183
|
+
/** @type {Record<string, any> | null} */
|
|
184
|
+
let snapshots = null;
|
|
185
|
+
|
|
186
|
+
switch (type) {
|
|
187
|
+
case Type.NPM: {
|
|
188
|
+
const lockfile = JSON.parse(content);
|
|
189
|
+
packages = lockfile.packages || {};
|
|
190
|
+
// Pass pre-parsed lockfile object to avoid re-parsing
|
|
191
|
+
for (const dep of fromPackageLock(lockfile, options)) {
|
|
192
|
+
deps.set(`${dep.name}@${dep.version}`, dep);
|
|
193
|
+
}
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
case Type.PNPM: {
|
|
197
|
+
/** @type {any} */
|
|
198
|
+
const lockfile = yaml.load(content);
|
|
199
|
+
packages = lockfile.packages || {};
|
|
200
|
+
importers = lockfile.importers || null;
|
|
201
|
+
snapshots = lockfile.snapshots || null;
|
|
202
|
+
// Pass pre-parsed lockfile object to avoid re-parsing
|
|
203
|
+
for (const dep of fromPnpmLock(lockfile, options)) {
|
|
204
|
+
deps.set(`${dep.name}@${dep.version}`, dep);
|
|
205
|
+
}
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
case Type.YARN_CLASSIC: {
|
|
209
|
+
const result = parseYarnClassic(content);
|
|
210
|
+
packages = result.object || {};
|
|
211
|
+
// Pass pre-parsed lockfile object to avoid re-parsing
|
|
212
|
+
for (const dep of fromYarnClassicLock(packages, options)) {
|
|
213
|
+
deps.set(`${dep.name}@${dep.version}`, dep);
|
|
214
|
+
}
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
case Type.YARN_BERRY: {
|
|
218
|
+
packages = parseSyml(content);
|
|
219
|
+
// Pass pre-parsed lockfile object to avoid re-parsing
|
|
220
|
+
for (const dep of fromYarnBerryLock(packages, options)) {
|
|
221
|
+
deps.set(`${dep.name}@${dep.version}`, dep);
|
|
222
|
+
}
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return { deps, packages, importers, snapshots };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** @returns {number} */
|
|
231
|
+
get size() {
|
|
232
|
+
return this.#deps.size;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** @returns {LockfileType | null} */
|
|
236
|
+
get type() {
|
|
237
|
+
return this.#type;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** @returns {boolean} */
|
|
241
|
+
get canTraverse() {
|
|
242
|
+
return this.#canTraverse;
|
|
243
|
+
}
|
|
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
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Check if a dependency exists
|
|
283
|
+
* @param {string} nameAtVersion - e.g., "lodash@4.17.21"
|
|
284
|
+
* @returns {boolean}
|
|
285
|
+
*/
|
|
286
|
+
has(nameAtVersion) {
|
|
287
|
+
return this.#deps.has(nameAtVersion);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Get a dependency by name@version
|
|
292
|
+
* @param {string} nameAtVersion
|
|
293
|
+
* @returns {Dependency | undefined}
|
|
294
|
+
*/
|
|
295
|
+
get(nameAtVersion) {
|
|
296
|
+
return this.#deps.get(nameAtVersion);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** @returns {IterableIterator<Dependency>} */
|
|
300
|
+
[Symbol.iterator]() {
|
|
301
|
+
return this.#deps.values();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** @returns {IterableIterator<Dependency>} */
|
|
305
|
+
values() {
|
|
306
|
+
return this.#deps.values();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** @returns {IterableIterator<string>} */
|
|
310
|
+
keys() {
|
|
311
|
+
return this.#deps.keys();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** @returns {IterableIterator<[string, Dependency]>} */
|
|
315
|
+
entries() {
|
|
316
|
+
return this.#deps.entries();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Execute a callback for each dependency
|
|
321
|
+
* @param {(dep: Dependency, key: string, set: FlatlockSet) => void} callback
|
|
322
|
+
* @param {any} [thisArg]
|
|
323
|
+
*/
|
|
324
|
+
forEach(callback, thisArg) {
|
|
325
|
+
for (const [key, dep] of this.#deps) {
|
|
326
|
+
callback.call(thisArg, dep, key, this);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Union of this set with another
|
|
332
|
+
* @param {FlatlockSet} other
|
|
333
|
+
* @returns {FlatlockSet}
|
|
334
|
+
*/
|
|
335
|
+
union(other) {
|
|
336
|
+
const deps = new Map(this.#deps);
|
|
337
|
+
for (const [key, dep] of other.#deps) {
|
|
338
|
+
if (!deps.has(key)) {
|
|
339
|
+
deps.set(key, dep);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return new FlatlockSet(INTERNAL, deps, null, null, null, null);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Intersection of this set with another
|
|
347
|
+
* @param {FlatlockSet} other
|
|
348
|
+
* @returns {FlatlockSet}
|
|
349
|
+
*/
|
|
350
|
+
intersection(other) {
|
|
351
|
+
const deps = new Map();
|
|
352
|
+
for (const [key, dep] of this.#deps) {
|
|
353
|
+
if (other.has(key)) {
|
|
354
|
+
deps.set(key, dep);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return new FlatlockSet(INTERNAL, deps, null, null, null, null);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Difference: elements in this set but not in other
|
|
362
|
+
* @param {FlatlockSet} other
|
|
363
|
+
* @returns {FlatlockSet}
|
|
364
|
+
*/
|
|
365
|
+
difference(other) {
|
|
366
|
+
const deps = new Map();
|
|
367
|
+
for (const [key, dep] of this.#deps) {
|
|
368
|
+
if (!other.has(key)) {
|
|
369
|
+
deps.set(key, dep);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return new FlatlockSet(INTERNAL, deps, null, null, null, null);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Check if this set is a subset of another
|
|
377
|
+
* @param {FlatlockSet} other
|
|
378
|
+
* @returns {boolean}
|
|
379
|
+
*/
|
|
380
|
+
isSubsetOf(other) {
|
|
381
|
+
for (const key of this.#deps.keys()) {
|
|
382
|
+
if (!other.has(key)) return false;
|
|
383
|
+
}
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Check if this set is a superset of another
|
|
389
|
+
* @param {FlatlockSet} other
|
|
390
|
+
* @returns {boolean}
|
|
391
|
+
*/
|
|
392
|
+
isSupersetOf(other) {
|
|
393
|
+
return other.isSubsetOf(this);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Check if this set has no elements in common with another
|
|
398
|
+
* @param {FlatlockSet} other
|
|
399
|
+
* @returns {boolean}
|
|
400
|
+
*/
|
|
401
|
+
isDisjointFrom(other) {
|
|
402
|
+
for (const key of this.#deps.keys()) {
|
|
403
|
+
if (other.has(key)) return false;
|
|
404
|
+
}
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Get transitive dependencies of a package.json
|
|
410
|
+
*
|
|
411
|
+
* For monorepos, provide workspacePath and repoDir to get correct resolution.
|
|
412
|
+
* Without workspacePath, assumes root package (hoisted deps only).
|
|
413
|
+
*
|
|
414
|
+
* NOTE: This method is only available on sets created directly from
|
|
415
|
+
* fromPath/fromString. Sets created via union/intersection/difference
|
|
416
|
+
* cannot use this method (canTraverse will be false).
|
|
417
|
+
*
|
|
418
|
+
* @param {PackageJson} packageJson - Parsed package.json
|
|
419
|
+
* @param {DependenciesOfOptions} [options]
|
|
420
|
+
* @returns {Promise<FlatlockSet>}
|
|
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
|
+
* });
|
|
429
|
+
*/
|
|
430
|
+
async dependenciesOf(packageJson, options = {}) {
|
|
431
|
+
if (!packageJson || typeof packageJson !== 'object') {
|
|
432
|
+
throw new TypeError('packageJson must be a non-null object');
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (!this.#canTraverse) {
|
|
436
|
+
throw new Error(
|
|
437
|
+
'dependenciesOf() requires lockfile data. ' +
|
|
438
|
+
'This set was created via set operations and cannot traverse dependencies. ' +
|
|
439
|
+
'Use dependenciesOf() on the original set before set operations.'
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
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
|
+
}
|
|
450
|
+
|
|
451
|
+
// Collect seed dependencies from package.json
|
|
452
|
+
const seeds = this.#collectSeeds(packageJson, { dev, optional, peer });
|
|
453
|
+
|
|
454
|
+
// If pnpm with workspacePath, use importers to get resolved versions
|
|
455
|
+
if (this.#type === Type.PNPM && workspacePath && this.#importers) {
|
|
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
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
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)
|
|
497
|
+
return this.#dependenciesOfHoisted(seeds, workspacePath);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Collect seed dependency names from package.json
|
|
502
|
+
* @param {PackageJson} packageJson
|
|
503
|
+
* @param {{ dev: boolean, optional: boolean, peer: boolean }} options
|
|
504
|
+
* @returns {Set<string>}
|
|
505
|
+
*/
|
|
506
|
+
#collectSeeds(packageJson, { dev, optional, peer }) {
|
|
507
|
+
const seeds = new Set();
|
|
508
|
+
|
|
509
|
+
for (const name of Object.keys(packageJson.dependencies || {})) {
|
|
510
|
+
seeds.add(name);
|
|
511
|
+
}
|
|
512
|
+
if (dev) {
|
|
513
|
+
for (const name of Object.keys(packageJson.devDependencies || {})) {
|
|
514
|
+
seeds.add(name);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (optional) {
|
|
518
|
+
for (const name of Object.keys(packageJson.optionalDependencies || {})) {
|
|
519
|
+
seeds.add(name);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (peer) {
|
|
523
|
+
for (const name of Object.keys(packageJson.peerDependencies || {})) {
|
|
524
|
+
seeds.add(name);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return seeds;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* pnpm-specific resolution using importers
|
|
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
|
|
541
|
+
* @param {string} workspacePath
|
|
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
|
|
866
|
+
* @returns {FlatlockSet}
|
|
867
|
+
*/
|
|
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
|
+
) {
|
|
982
|
+
/** @type {Map<string, Dependency>} */
|
|
983
|
+
const result = new Map();
|
|
984
|
+
/** @type {Set<string>} */
|
|
985
|
+
const visited = new Set();
|
|
986
|
+
/** @type {string[]} */
|
|
987
|
+
const queue = [...seeds];
|
|
988
|
+
|
|
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
|
+
}
|
|
994
|
+
|
|
995
|
+
while (queue.length > 0) {
|
|
996
|
+
const name = /** @type {string} */ (queue.shift());
|
|
997
|
+
if (visited.has(name)) continue;
|
|
998
|
+
visited.add(name);
|
|
999
|
+
|
|
1000
|
+
let dep;
|
|
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;
|
|
1010
|
+
} else {
|
|
1011
|
+
// Regular npm dependency - find in lockfile
|
|
1012
|
+
dep = this.#findByName(name);
|
|
1013
|
+
if (dep) {
|
|
1014
|
+
entry = this.#getYarnClassicEntry(name, dep.version);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
if (!dep) continue;
|
|
1019
|
+
|
|
1020
|
+
const key = `${dep.name}@${dep.version}`;
|
|
1021
|
+
result.set(key, dep);
|
|
1022
|
+
|
|
1023
|
+
// Get transitive deps from lockfile entry
|
|
1024
|
+
if (entry) {
|
|
1025
|
+
for (const transName of Object.keys(entry.dependencies || {})) {
|
|
1026
|
+
if (!visited.has(transName)) queue.push(transName);
|
|
1027
|
+
}
|
|
1028
|
+
if (optional) {
|
|
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 || {})) {
|
|
1035
|
+
if (!visited.has(transName)) queue.push(transName);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
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;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* npm/yarn resolution with hoisting
|
|
1156
|
+
* @param {Set<string>} seeds
|
|
1157
|
+
* @param {string} [workspacePath]
|
|
1158
|
+
* @returns {FlatlockSet}
|
|
1159
|
+
*/
|
|
1160
|
+
#dependenciesOfHoisted(seeds, workspacePath) {
|
|
1161
|
+
/** @type {Map<string, Dependency>} */
|
|
1162
|
+
const result = new Map();
|
|
1163
|
+
/** @type {Set<string>} */
|
|
1164
|
+
const visited = new Set();
|
|
1165
|
+
/** @type {string[]} */
|
|
1166
|
+
const queue = [...seeds];
|
|
1167
|
+
|
|
1168
|
+
while (queue.length > 0) {
|
|
1169
|
+
const name = /** @type {string} */ (queue.shift());
|
|
1170
|
+
if (visited.has(name)) continue;
|
|
1171
|
+
visited.add(name);
|
|
1172
|
+
|
|
1173
|
+
// Find package: check workspace-local first, then hoisted
|
|
1174
|
+
const dep = this.#findPackage(name, workspacePath);
|
|
1175
|
+
if (!dep) continue;
|
|
1176
|
+
|
|
1177
|
+
const key = `${dep.name}@${dep.version}`;
|
|
1178
|
+
result.set(key, dep);
|
|
1179
|
+
|
|
1180
|
+
// Get transitive deps from raw lockfile
|
|
1181
|
+
const entry = this.#getPackageEntry(dep.name, dep.version, workspacePath);
|
|
1182
|
+
if (entry) {
|
|
1183
|
+
for (const transName of Object.keys(entry.dependencies || {})) {
|
|
1184
|
+
if (!visited.has(transName)) queue.push(transName);
|
|
1185
|
+
}
|
|
1186
|
+
for (const transName of Object.keys(entry.optionalDependencies || {})) {
|
|
1187
|
+
if (!visited.has(transName)) queue.push(transName);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
return new FlatlockSet(INTERNAL, result, null, null, null, this.#type);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
/**
|
|
1196
|
+
* Find a package by name, checking workspace-local then hoisted
|
|
1197
|
+
* @param {string} name
|
|
1198
|
+
* @param {string} [workspacePath]
|
|
1199
|
+
* @returns {Dependency | undefined}
|
|
1200
|
+
*/
|
|
1201
|
+
#findPackage(name, workspacePath) {
|
|
1202
|
+
if (this.#type === Type.NPM && workspacePath) {
|
|
1203
|
+
// Check workspace-local node_modules first
|
|
1204
|
+
const localKey = `${workspacePath}/node_modules/${name}`;
|
|
1205
|
+
const localEntry = this.#packages?.[localKey];
|
|
1206
|
+
if (localEntry?.version) {
|
|
1207
|
+
return this.get(`${name}@${localEntry.version}`);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// Fall back to hoisted (root node_modules)
|
|
1212
|
+
return this.#findByName(name);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
/**
|
|
1216
|
+
* Find a dependency by name (returns hoisted/first match)
|
|
1217
|
+
* @param {string} name
|
|
1218
|
+
* @returns {Dependency | undefined}
|
|
1219
|
+
*/
|
|
1220
|
+
#findByName(name) {
|
|
1221
|
+
// For npm, check root node_modules path first
|
|
1222
|
+
if (this.#type === Type.NPM) {
|
|
1223
|
+
const rootKey = `node_modules/${name}`;
|
|
1224
|
+
const entry = this.#packages?.[rootKey];
|
|
1225
|
+
if (entry?.version) {
|
|
1226
|
+
return this.get(`${name}@${entry.version}`);
|
|
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
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// Fallback: iterate deps (may return arbitrary version if multiple)
|
|
1239
|
+
for (const dep of this.#deps.values()) {
|
|
1240
|
+
if (dep.name === name) return dep;
|
|
1241
|
+
}
|
|
1242
|
+
return undefined;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
/**
|
|
1246
|
+
* Get raw package entry for transitive dep lookup
|
|
1247
|
+
* @param {string} name
|
|
1248
|
+
* @param {string} version
|
|
1249
|
+
* @param {string} [workspacePath]
|
|
1250
|
+
* @returns {any}
|
|
1251
|
+
*/
|
|
1252
|
+
#getPackageEntry(name, version, workspacePath) {
|
|
1253
|
+
if (!this.#packages) return null;
|
|
1254
|
+
|
|
1255
|
+
switch (this.#type) {
|
|
1256
|
+
case Type.NPM: {
|
|
1257
|
+
// Check workspace-local first
|
|
1258
|
+
if (workspacePath) {
|
|
1259
|
+
const localKey = `${workspacePath}/node_modules/${name}`;
|
|
1260
|
+
if (this.#packages[localKey]) return this.#packages[localKey];
|
|
1261
|
+
}
|
|
1262
|
+
// Fall back to hoisted
|
|
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;
|
|
1273
|
+
}
|
|
1274
|
+
case Type.PNPM: {
|
|
1275
|
+
return this.#packages[`/${name}@${version}`] || null;
|
|
1276
|
+
}
|
|
1277
|
+
case Type.YARN_CLASSIC: {
|
|
1278
|
+
for (const [key, entry] of Object.entries(this.#packages)) {
|
|
1279
|
+
if (entry.version === version) {
|
|
1280
|
+
const keyName = parseYarnClassicKey(key);
|
|
1281
|
+
if (keyName === name) return entry;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
return null;
|
|
1285
|
+
}
|
|
1286
|
+
case Type.YARN_BERRY: {
|
|
1287
|
+
for (const [key, entry] of Object.entries(this.#packages)) {
|
|
1288
|
+
if (entry.version === version) {
|
|
1289
|
+
const keyName = parseYarnBerryKey(key);
|
|
1290
|
+
if (keyName === name) return entry;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
return null;
|
|
1294
|
+
}
|
|
1295
|
+
default:
|
|
1296
|
+
return null;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
/**
|
|
1301
|
+
* Convert to array
|
|
1302
|
+
* @returns {Dependency[]}
|
|
1303
|
+
*/
|
|
1304
|
+
toArray() {
|
|
1305
|
+
return [...this.#deps.values()];
|
|
1306
|
+
}
|
|
1307
|
+
}
|