flatlock 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -2
- package/bin/flatlock-cmp.js +109 -356
- package/dist/compare.d.ts +85 -0
- package/dist/compare.d.ts.map +1 -0
- package/dist/detect.d.ts +33 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/index.d.ts +60 -20
- package/dist/index.d.ts.map +1 -1
- package/dist/parsers/index.d.ts +5 -0
- package/dist/parsers/index.d.ts.map +1 -0
- package/dist/parsers/npm.d.ts +109 -0
- package/dist/parsers/npm.d.ts.map +1 -0
- package/dist/parsers/pnpm/detect.d.ts +136 -0
- package/dist/parsers/pnpm/detect.d.ts.map +1 -0
- package/dist/parsers/pnpm/index.d.ts +120 -0
- package/dist/parsers/pnpm/index.d.ts.map +1 -0
- package/dist/parsers/pnpm/internal.d.ts +5 -0
- package/dist/parsers/pnpm/internal.d.ts.map +1 -0
- package/dist/parsers/pnpm/shrinkwrap.d.ts +129 -0
- package/dist/parsers/pnpm/shrinkwrap.d.ts.map +1 -0
- package/dist/parsers/pnpm/v5.d.ts +139 -0
- package/dist/parsers/pnpm/v5.d.ts.map +1 -0
- package/dist/parsers/pnpm/v6plus.d.ts +212 -0
- package/dist/parsers/pnpm/v6plus.d.ts.map +1 -0
- package/dist/parsers/pnpm.d.ts +2 -0
- package/dist/parsers/pnpm.d.ts.map +1 -0
- package/dist/parsers/types.d.ts +23 -0
- package/dist/parsers/types.d.ts.map +1 -0
- package/dist/parsers/yarn-berry.d.ts +154 -0
- package/dist/parsers/yarn-berry.d.ts.map +1 -0
- package/dist/parsers/yarn-classic.d.ts +110 -0
- package/dist/parsers/yarn-classic.d.ts.map +1 -0
- package/dist/result.d.ts +12 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/set.d.ts +189 -0
- package/dist/set.d.ts.map +1 -0
- package/package.json +18 -7
- package/src/compare.js +620 -0
- package/src/detect.js +8 -7
- package/src/index.js +33 -15
- package/src/parsers/index.js +12 -4
- package/src/parsers/npm.js +70 -23
- package/src/parsers/pnpm/detect.js +198 -0
- package/src/parsers/pnpm/index.js +289 -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 +12 -77
- package/src/parsers/types.js +10 -0
- package/src/parsers/yarn-berry.js +187 -38
- package/src/parsers/yarn-classic.js +85 -24
- package/src/result.js +2 -2
- package/src/set.js +618 -0
- package/src/types.d.ts +54 -0
package/README.md
CHANGED
|
@@ -4,14 +4,14 @@ The Matlock of lockfile parsers - cuts through the complexity to get just the fa
|
|
|
4
4
|
|
|
5
5
|
## What makes `flatlock` different?
|
|
6
6
|
|
|
7
|
-

|
|
7
|
+

|
|
8
8
|
|
|
9
9
|
Most lockfile parsers (like `@npmcli/arborist` or `snyk-nodejs-lockfile-parser`) build the full dependency graph with edges representing relationships between packages. This is necessary for dependency resolution but overkill for many use cases.
|
|
10
10
|
|
|
11
11
|
**flatlock** takes a different approach: it extracts a flat stream of packages from any lockfile format. No trees, no graphs, no edges - just packages.
|
|
12
12
|
|
|
13
13
|
```javascript
|
|
14
|
-
import * as
|
|
14
|
+
import * as flatlock from 'flatlock';
|
|
15
15
|
|
|
16
16
|
// Stream packages from any lockfile
|
|
17
17
|
for await (const pkg of flatlock.fromPath('./package-lock.json')) {
|
|
@@ -89,6 +89,59 @@ Each yielded package has:
|
|
|
89
89
|
}
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
+
## FlatlockSet
|
|
93
|
+
|
|
94
|
+
For more advanced use cases, `FlatlockSet` provides Set-like operations on lockfile dependencies:
|
|
95
|
+
|
|
96
|
+
```javascript
|
|
97
|
+
import { FlatlockSet } from 'flatlock';
|
|
98
|
+
|
|
99
|
+
// Create from lockfile
|
|
100
|
+
const set = await FlatlockSet.fromPath('./package-lock.json');
|
|
101
|
+
console.log(set.size); // 1234
|
|
102
|
+
console.log(set.has('lodash@4.17.21')); // true
|
|
103
|
+
|
|
104
|
+
// Set operations (immutable - return new sets)
|
|
105
|
+
const other = await FlatlockSet.fromPath('./other-lock.json');
|
|
106
|
+
const common = set.intersection(other); // packages in both
|
|
107
|
+
const added = other.difference(set); // packages only in other
|
|
108
|
+
const all = set.union(other); // packages in either
|
|
109
|
+
|
|
110
|
+
// Predicates
|
|
111
|
+
set.isSubsetOf(other); // true if all packages in set are in other
|
|
112
|
+
set.isSupersetOf(other); // true if set contains all packages in other
|
|
113
|
+
set.isDisjointFrom(other); // true if no packages in common
|
|
114
|
+
|
|
115
|
+
// Iterate like a Set
|
|
116
|
+
for (const dep of set) {
|
|
117
|
+
console.log(dep.name, dep.version);
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Workspace-Specific SBOMs
|
|
122
|
+
|
|
123
|
+
For monorepos, use `dependenciesOf()` to get only the dependencies of a specific workspace:
|
|
124
|
+
|
|
125
|
+
```javascript
|
|
126
|
+
import { readFile } from 'node:fs/promises';
|
|
127
|
+
import { FlatlockSet } from 'flatlock';
|
|
128
|
+
|
|
129
|
+
const lockfile = await FlatlockSet.fromPath('./package-lock.json');
|
|
130
|
+
const pkg = JSON.parse(await readFile('./packages/api/package.json', 'utf8'));
|
|
131
|
+
|
|
132
|
+
// Get only dependencies reachable from this workspace
|
|
133
|
+
const subset = lockfile.dependenciesOf(pkg, {
|
|
134
|
+
workspacePath: 'packages/api', // for correct resolution in monorepos
|
|
135
|
+
dev: false, // exclude devDependencies
|
|
136
|
+
optional: true, // include optionalDependencies
|
|
137
|
+
peer: false // exclude peerDependencies
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
console.log(`${pkg.name} has ${subset.size} production dependencies`);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Note:** Sets created via `union()`, `intersection()`, or `difference()` cannot use `dependenciesOf()` because they lack the raw lockfile data needed for traversal. Check `set.canTraverse` before calling.
|
|
144
|
+
|
|
92
145
|
## License
|
|
93
146
|
|
|
94
147
|
Apache-2.0
|
package/bin/flatlock-cmp.js
CHANGED
|
@@ -1,277 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { join
|
|
3
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
5
|
import { parseArgs } from 'node:util';
|
|
6
|
-
import { fileURLToPath } from 'node:url';
|
|
7
|
-
import crypto from 'node:crypto';
|
|
8
6
|
import * as flatlock from '../src/index.js';
|
|
9
7
|
|
|
10
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
-
|
|
12
|
-
// Unique run ID for this script execution (7 char hash of timestamp)
|
|
13
|
-
const RUN_ID = Date.now().toString(36).slice(-7);
|
|
14
|
-
const TMP_BASE = join(__dirname, 'tmp', RUN_ID);
|
|
15
|
-
let tmpDirCreated = false;
|
|
16
|
-
|
|
17
|
-
// Comparison parsers (lazy loaded)
|
|
18
|
-
let Arborist, yarnLockfile, parseSyml, yaml;
|
|
19
|
-
|
|
20
|
-
async function loadArborist() {
|
|
21
|
-
if (!Arborist) {
|
|
22
|
-
const mod = await import('@npmcli/arborist');
|
|
23
|
-
Arborist = mod.default;
|
|
24
|
-
}
|
|
25
|
-
return Arborist;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async function loadYarnClassic() {
|
|
29
|
-
if (!yarnLockfile) {
|
|
30
|
-
const mod = await import('@yarnpkg/lockfile');
|
|
31
|
-
yarnLockfile = mod.default || mod;
|
|
32
|
-
}
|
|
33
|
-
return yarnLockfile;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async function loadYarnBerry() {
|
|
37
|
-
if (!parseSyml) {
|
|
38
|
-
const mod = await import('@yarnpkg/parsers');
|
|
39
|
-
parseSyml = mod.parseSyml;
|
|
40
|
-
}
|
|
41
|
-
return parseSyml;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function loadYaml() {
|
|
45
|
-
if (!yaml) {
|
|
46
|
-
const mod = await import('js-yaml');
|
|
47
|
-
yaml = mod.default;
|
|
48
|
-
}
|
|
49
|
-
return yaml;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Ensure the temp directory for this run exists
|
|
54
|
-
*/
|
|
55
|
-
async function ensureTmpDir() {
|
|
56
|
-
if (!tmpDirCreated) {
|
|
57
|
-
await mkdir(TMP_BASE, { recursive: true });
|
|
58
|
-
tmpDirCreated = true;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Cleanup the temp directory for this run
|
|
64
|
-
*/
|
|
65
|
-
async function cleanup() {
|
|
66
|
-
if (tmpDirCreated) {
|
|
67
|
-
try {
|
|
68
|
-
await rm(TMP_BASE, { recursive: true, force: true });
|
|
69
|
-
} catch {
|
|
70
|
-
// Ignore cleanup errors
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Get packages set using @npmcli/arborist
|
|
77
|
-
*
|
|
78
|
-
* Arborist requires a directory with package-lock.json (and optionally package.json).
|
|
79
|
-
* We create a temp directory structure for each lockfile:
|
|
80
|
-
* bin/tmp/<run-id>/<file-hash>/package-lock.json
|
|
81
|
-
*/
|
|
82
|
-
async function getPackagesFromNpm(content, filepath) {
|
|
83
|
-
const ArboristClass = await loadArborist();
|
|
84
|
-
await ensureTmpDir();
|
|
85
|
-
|
|
86
|
-
// Create unique subdir for this lockfile
|
|
87
|
-
const fileId = crypto.createHash('md5').update(filepath).digest('hex').slice(0, 7);
|
|
88
|
-
const tmpDir = join(TMP_BASE, fileId);
|
|
89
|
-
|
|
90
|
-
await mkdir(tmpDir, { recursive: true });
|
|
91
|
-
await writeFile(join(tmpDir, 'package-lock.json'), content);
|
|
92
|
-
|
|
93
|
-
// Create minimal package.json from lockfile root entry
|
|
94
|
-
const lockfile = JSON.parse(content);
|
|
95
|
-
const root = lockfile.packages?.[''] || {};
|
|
96
|
-
const pkg = {
|
|
97
|
-
name: root.name || 'arborist-temp',
|
|
98
|
-
version: root.version || '1.0.0'
|
|
99
|
-
};
|
|
100
|
-
await writeFile(join(tmpDir, 'package.json'), JSON.stringify(pkg));
|
|
101
|
-
|
|
102
|
-
try {
|
|
103
|
-
const arb = new ArboristClass({ path: tmpDir });
|
|
104
|
-
const tree = await arb.loadVirtual();
|
|
105
|
-
|
|
106
|
-
const result = new Set();
|
|
107
|
-
let workspaceCount = 0;
|
|
108
|
-
for (const node of tree.inventory.values()) {
|
|
109
|
-
if (node.isRoot) continue;
|
|
110
|
-
// Skip workspace symlinks (link:true, no version in raw lockfile)
|
|
111
|
-
if (node.isLink) {
|
|
112
|
-
workspaceCount++;
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
// Skip workspace package definitions (not in node_modules)
|
|
116
|
-
// Flatlock only yields packages from node_modules/ paths
|
|
117
|
-
if (node.location && !node.location.includes('node_modules')) {
|
|
118
|
-
workspaceCount++;
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
if (node.name && node.version) {
|
|
122
|
-
result.add(`${node.name}@${node.version}`);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return { packages: result, workspaceCount };
|
|
126
|
-
} finally {
|
|
127
|
-
// Cleanup this specific lockfile's temp dir
|
|
128
|
-
await rm(tmpDir, { recursive: true, force: true });
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Get packages set using @yarnpkg/lockfile (classic)
|
|
134
|
-
*/
|
|
135
|
-
async function getPackagesFromYarnClassic(content) {
|
|
136
|
-
const yarnLock = await loadYarnClassic();
|
|
137
|
-
const parse = yarnLock.parse || yarnLock.default?.parse;
|
|
138
|
-
const { object: lockfile } = parse(content);
|
|
139
|
-
|
|
140
|
-
const result = new Set();
|
|
141
|
-
let workspaceCount = 0;
|
|
142
|
-
for (const [key, pkg] of Object.entries(lockfile)) {
|
|
143
|
-
if (key === '__metadata') continue;
|
|
144
|
-
|
|
145
|
-
// Skip workspace/link entries - flatlock only cares about external dependencies
|
|
146
|
-
const resolved = pkg.resolved || '';
|
|
147
|
-
if (resolved.startsWith('file:') || resolved.startsWith('link:')) {
|
|
148
|
-
workspaceCount++;
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
let name;
|
|
153
|
-
if (key.startsWith('@')) {
|
|
154
|
-
const idx = key.indexOf('@', 1);
|
|
155
|
-
name = key.slice(0, idx);
|
|
156
|
-
} else {
|
|
157
|
-
name = key.split('@')[0];
|
|
158
|
-
}
|
|
159
|
-
if (name && pkg.version) result.add(`${name}@${pkg.version}`);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return { packages: result, workspaceCount };
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Get packages set using @yarnpkg/parsers (berry)
|
|
167
|
-
*/
|
|
168
|
-
async function getPackagesFromYarnBerry(content) {
|
|
169
|
-
const parse = await loadYarnBerry();
|
|
170
|
-
const lockfile = parse(content);
|
|
171
|
-
|
|
172
|
-
const result = new Set();
|
|
173
|
-
let workspaceCount = 0;
|
|
174
|
-
for (const [key, pkg] of Object.entries(lockfile)) {
|
|
175
|
-
if (key === '__metadata') continue;
|
|
176
|
-
|
|
177
|
-
// Skip workspace/link entries - flatlock only cares about external dependencies
|
|
178
|
-
const resolution = pkg.resolution || '';
|
|
179
|
-
if (resolution.startsWith('workspace:') ||
|
|
180
|
-
resolution.startsWith('portal:') ||
|
|
181
|
-
resolution.startsWith('link:')) {
|
|
182
|
-
workspaceCount++;
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
let name;
|
|
187
|
-
if (key.startsWith('@')) {
|
|
188
|
-
const idx = key.indexOf('@', 1);
|
|
189
|
-
name = key.slice(0, idx);
|
|
190
|
-
} else {
|
|
191
|
-
name = key.split('@')[0];
|
|
192
|
-
}
|
|
193
|
-
if (name && pkg.version) result.add(`${name}@${pkg.version}`);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return { packages: result, workspaceCount };
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Get packages set using js-yaml (pnpm)
|
|
201
|
-
*/
|
|
202
|
-
async function getPackagesFromPnpm(content) {
|
|
203
|
-
const y = await loadYaml();
|
|
204
|
-
const lockfile = y.load(content);
|
|
205
|
-
const packages = lockfile.packages || {};
|
|
206
|
-
|
|
207
|
-
const result = new Set();
|
|
208
|
-
let workspaceCount = 0;
|
|
209
|
-
for (const [key, pkg] of Object.entries(packages)) {
|
|
210
|
-
// Skip link/file entries - flatlock only cares about external dependencies
|
|
211
|
-
// Keys can be: link:path, file:path, or @pkg@file:path
|
|
212
|
-
if (key.startsWith('link:') || key.startsWith('file:') ||
|
|
213
|
-
key.includes('@link:') || key.includes('@file:')) {
|
|
214
|
-
workspaceCount++;
|
|
215
|
-
continue;
|
|
216
|
-
}
|
|
217
|
-
// Also skip if resolution.type is 'directory' (workspace)
|
|
218
|
-
if (pkg.resolution?.type === 'directory') {
|
|
219
|
-
workspaceCount++;
|
|
220
|
-
continue;
|
|
221
|
-
}
|
|
222
|
-
// pnpm keys are like /lodash@4.17.21 or /@babel/core@7.0.0
|
|
223
|
-
const match = key.match(/^\/?(@?[^@]+)@(.+)$/);
|
|
224
|
-
if (match) result.add(`${match[1]}@${match[2]}`);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return { packages: result, workspaceCount };
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Get packages set with flatlock
|
|
232
|
-
*/
|
|
233
|
-
async function getPackagesFromFlatlock(filepath) {
|
|
234
|
-
const result = new Set();
|
|
235
|
-
for await (const dep of flatlock.fromPath(filepath)) {
|
|
236
|
-
if (dep.name && dep.version) result.add(`${dep.name}@${dep.version}`);
|
|
237
|
-
}
|
|
238
|
-
return result;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Get comparison parser name for type
|
|
243
|
-
*/
|
|
244
|
-
function getComparisonName(type) {
|
|
245
|
-
switch (type) {
|
|
246
|
-
case 'npm': return '@npmcli/arborist';
|
|
247
|
-
case 'yarn-classic': return '@yarnpkg/lockfile';
|
|
248
|
-
case 'yarn-berry': return '@yarnpkg/parsers';
|
|
249
|
-
case 'pnpm': return 'js-yaml';
|
|
250
|
-
default: return 'unknown';
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Get packages with comparison parser based on type
|
|
256
|
-
*/
|
|
257
|
-
async function getPackagesFromComparison(type, content, filepath) {
|
|
258
|
-
switch (type) {
|
|
259
|
-
case 'npm': return getPackagesFromNpm(content, filepath);
|
|
260
|
-
case 'yarn-classic': return getPackagesFromYarnClassic(content);
|
|
261
|
-
case 'yarn-berry': return getPackagesFromYarnBerry(content);
|
|
262
|
-
case 'pnpm': return getPackagesFromPnpm(content);
|
|
263
|
-
default: return null;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Compare two sets and return differences
|
|
269
|
-
*/
|
|
270
|
-
function compareSets(setA, setB) {
|
|
271
|
-
const onlyInA = new Set([...setA].filter(x => !setB.has(x)));
|
|
272
|
-
const onlyInB = new Set([...setB].filter(x => !setA.has(x)));
|
|
273
|
-
return { onlyInA, onlyInB };
|
|
274
|
-
}
|
|
275
8
|
|
|
276
9
|
/**
|
|
277
10
|
* Convert glob pattern to regex
|
|
@@ -308,52 +41,38 @@ async function findFiles(dir, pattern) {
|
|
|
308
41
|
}
|
|
309
42
|
|
|
310
43
|
/**
|
|
311
|
-
* Process a single lockfile
|
|
44
|
+
* Process a single lockfile using flatlock.compare()
|
|
312
45
|
*/
|
|
313
46
|
async function processFile(filepath, baseDir) {
|
|
314
47
|
try {
|
|
315
|
-
const
|
|
316
|
-
const type = flatlock.detectType({ path: filepath, content });
|
|
48
|
+
const result = await flatlock.compare(filepath);
|
|
317
49
|
const rel = baseDir ? filepath.replace(baseDir + '/', '') : filepath;
|
|
318
|
-
const comparisonName = getComparisonName(type);
|
|
319
50
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
try {
|
|
324
|
-
comparisonResult = await getPackagesFromComparison(type, content, filepath);
|
|
325
|
-
} catch (err) {
|
|
326
|
-
comparisonResult = null;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if (!comparisonResult) {
|
|
51
|
+
if (result.equinumerous === null) {
|
|
52
|
+
// Unsupported type or no comparison available
|
|
330
53
|
return {
|
|
331
|
-
type,
|
|
54
|
+
type: result.type,
|
|
332
55
|
path: rel,
|
|
333
|
-
|
|
334
|
-
flatlockCount:
|
|
56
|
+
source: result.source || 'unknown',
|
|
57
|
+
flatlockCount: result.flatlockCount,
|
|
335
58
|
comparisonCount: null,
|
|
336
59
|
workspaceCount: 0,
|
|
337
|
-
|
|
60
|
+
equinumerous: null,
|
|
338
61
|
onlyInFlatlock: null,
|
|
339
62
|
onlyInComparison: null
|
|
340
63
|
};
|
|
341
64
|
}
|
|
342
65
|
|
|
343
|
-
const { packages: comparisonSet, workspaceCount } = comparisonResult;
|
|
344
|
-
const { onlyInA: onlyInFlatlock, onlyInB: onlyInComparison } = compareSets(flatlockSet, comparisonSet);
|
|
345
|
-
const identical = onlyInFlatlock.size === 0 && onlyInComparison.size === 0;
|
|
346
|
-
|
|
347
66
|
return {
|
|
348
|
-
type,
|
|
67
|
+
type: result.type,
|
|
349
68
|
path: rel,
|
|
350
|
-
|
|
351
|
-
flatlockCount:
|
|
352
|
-
comparisonCount:
|
|
353
|
-
workspaceCount,
|
|
354
|
-
|
|
355
|
-
onlyInFlatlock,
|
|
356
|
-
onlyInComparison
|
|
69
|
+
source: result.source,
|
|
70
|
+
flatlockCount: result.flatlockCount,
|
|
71
|
+
comparisonCount: result.comparisonCount,
|
|
72
|
+
workspaceCount: result.workspaceCount,
|
|
73
|
+
equinumerous: result.equinumerous,
|
|
74
|
+
onlyInFlatlock: result.onlyInFlatlock,
|
|
75
|
+
onlyInComparison: result.onlyInComparison
|
|
357
76
|
};
|
|
358
77
|
} catch (err) {
|
|
359
78
|
const rel = baseDir ? filepath.replace(baseDir + '/', '') : filepath;
|
|
@@ -386,10 +105,21 @@ Options:
|
|
|
386
105
|
-h, --help Show this help
|
|
387
106
|
|
|
388
107
|
Comparison parsers (workspace/link entries excluded from all):
|
|
389
|
-
npm: @npmcli/arborist (
|
|
108
|
+
npm: @npmcli/arborist (preferred) or @cyclonedx/cyclonedx-npm
|
|
390
109
|
yarn-classic: @yarnpkg/lockfile
|
|
391
110
|
yarn-berry: @yarnpkg/parsers
|
|
392
|
-
pnpm: js-yaml
|
|
111
|
+
pnpm: @pnpm/lockfile.fs (preferred) or js-yaml
|
|
112
|
+
|
|
113
|
+
Result types:
|
|
114
|
+
✓ equinumerous Same packages in both (exact match)
|
|
115
|
+
⊃ SUPERSET flatlock found MORE packages (expected for pnpm)
|
|
116
|
+
❌ MISMATCH Unexpected difference (comparison found packages flatlock missed)
|
|
117
|
+
|
|
118
|
+
Note on pnpm supersets:
|
|
119
|
+
flatlock performs full reachability analysis on lockfiles, finding all
|
|
120
|
+
transitive dependencies. pnpm's official tools don't enumerate all reachable
|
|
121
|
+
packages - they omit some transitive deps from their API output. When flatlock
|
|
122
|
+
finds MORE packages than pnpm, this is expected and correct behavior.
|
|
393
123
|
|
|
394
124
|
Examples:
|
|
395
125
|
flatlock-cmp package-lock.json
|
|
@@ -421,94 +151,117 @@ Examples:
|
|
|
421
151
|
let fileCount = 0;
|
|
422
152
|
let errorCount = 0;
|
|
423
153
|
let matchCount = 0;
|
|
154
|
+
let supersetCount = 0; // flatlock found more (expected for pnpm reachability)
|
|
424
155
|
let mismatchCount = 0;
|
|
425
156
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const result = await processFile(file, baseDir);
|
|
157
|
+
for (const file of files) {
|
|
158
|
+
const result = await processFile(file, baseDir);
|
|
429
159
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
continue;
|
|
160
|
+
if (result.error) {
|
|
161
|
+
errorCount++;
|
|
162
|
+
if (!values.quiet) {
|
|
163
|
+
console.log(`\n❌ ERROR: ${result.path}`);
|
|
164
|
+
console.log(` ${result.error}`);
|
|
437
165
|
}
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
438
168
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
169
|
+
fileCount++;
|
|
170
|
+
totalFlatlock += result.flatlockCount;
|
|
171
|
+
totalWorkspaces += result.workspaceCount || 0;
|
|
442
172
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
}
|
|
449
|
-
continue;
|
|
173
|
+
if (result.comparisonCount === null) {
|
|
174
|
+
if (!values.quiet) {
|
|
175
|
+
console.log(`\n⚠️ ${result.path}`);
|
|
176
|
+
console.log(` flatlock: ${result.flatlockCount} packages`);
|
|
177
|
+
console.log(` ${result.source}: unavailable`);
|
|
450
178
|
}
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
451
181
|
|
|
452
|
-
|
|
182
|
+
totalComparison += result.comparisonCount;
|
|
453
183
|
|
|
454
|
-
|
|
455
|
-
|
|
184
|
+
if (result.equinumerous) {
|
|
185
|
+
matchCount++;
|
|
186
|
+
if (!values.quiet) {
|
|
187
|
+
const wsNote = result.workspaceCount > 0 ? ` (${result.workspaceCount} workspaces excluded)` : '';
|
|
188
|
+
console.log(`✓ ${result.path}${wsNote}`);
|
|
189
|
+
console.log(` count: flatlock=${result.flatlockCount} ${result.source}=${result.comparisonCount}`);
|
|
190
|
+
console.log(` sets: equinumerous`);
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
// Determine if this is a "superset" (flatlock found more, expected for pnpm)
|
|
194
|
+
// or a true "mismatch" (comparison found packages flatlock missed)
|
|
195
|
+
const isPnpm = result.type === 'pnpm' || result.path.includes('pnpm-lock');
|
|
196
|
+
const isSuperset = result.onlyInFlatlock.length > 0 && result.onlyInComparison.length === 0;
|
|
197
|
+
|
|
198
|
+
if (isPnpm && isSuperset) {
|
|
199
|
+
// Expected behavior: flatlock's reachability analysis found more packages
|
|
200
|
+
supersetCount++;
|
|
456
201
|
if (!values.quiet) {
|
|
457
202
|
const wsNote = result.workspaceCount > 0 ? ` (${result.workspaceCount} workspaces excluded)` : '';
|
|
458
|
-
console.log(
|
|
459
|
-
console.log(` count: flatlock=${result.flatlockCount} ${result.
|
|
460
|
-
console.log(` sets:
|
|
203
|
+
console.log(`⊃ ${result.path}${wsNote}`);
|
|
204
|
+
console.log(` count: flatlock=${result.flatlockCount} ${result.source}=${result.comparisonCount}`);
|
|
205
|
+
console.log(` sets: SUPERSET (+${result.onlyInFlatlock.length} reachable deps)`);
|
|
206
|
+
console.log(` note: flatlock's reachability analysis found transitive deps pnpm omits`);
|
|
461
207
|
}
|
|
462
208
|
} else {
|
|
463
209
|
mismatchCount++;
|
|
464
210
|
console.log(`\n❌ ${result.path}`);
|
|
465
|
-
console.log(` count: flatlock=${result.flatlockCount} ${result.
|
|
211
|
+
console.log(` count: flatlock=${result.flatlockCount} ${result.source}=${result.comparisonCount}`);
|
|
466
212
|
console.log(` sets: MISMATCH`);
|
|
467
213
|
|
|
468
|
-
if (result.onlyInFlatlock.
|
|
469
|
-
console.log(` only in flatlock (${result.onlyInFlatlock.
|
|
470
|
-
for (const pkg of
|
|
214
|
+
if (result.onlyInFlatlock.length > 0) {
|
|
215
|
+
console.log(` only in flatlock (${result.onlyInFlatlock.length}):`);
|
|
216
|
+
for (const pkg of result.onlyInFlatlock.slice(0, 10)) {
|
|
471
217
|
console.log(` + ${pkg}`);
|
|
472
218
|
}
|
|
473
|
-
if (result.onlyInFlatlock.
|
|
474
|
-
console.log(` ... and ${result.onlyInFlatlock.
|
|
219
|
+
if (result.onlyInFlatlock.length > 10) {
|
|
220
|
+
console.log(` ... and ${result.onlyInFlatlock.length - 10} more`);
|
|
475
221
|
}
|
|
476
222
|
}
|
|
477
223
|
|
|
478
|
-
if (result.onlyInComparison.
|
|
479
|
-
console.log(` only in ${result.
|
|
480
|
-
for (const pkg of
|
|
224
|
+
if (result.onlyInComparison.length > 0) {
|
|
225
|
+
console.log(` only in ${result.source} (${result.onlyInComparison.length}):`);
|
|
226
|
+
for (const pkg of result.onlyInComparison.slice(0, 10)) {
|
|
481
227
|
console.log(` - ${pkg}`);
|
|
482
228
|
}
|
|
483
|
-
if (result.onlyInComparison.
|
|
484
|
-
console.log(` ... and ${result.onlyInComparison.
|
|
229
|
+
if (result.onlyInComparison.length > 10) {
|
|
230
|
+
console.log(` ... and ${result.onlyInComparison.length - 10} more`);
|
|
485
231
|
}
|
|
486
232
|
}
|
|
487
233
|
}
|
|
488
234
|
}
|
|
235
|
+
}
|
|
489
236
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
console.log(` workspaces: ${totalWorkspaces.toString().padStart(8)} excluded (local/workspace refs)`);
|
|
499
|
-
}
|
|
237
|
+
// Summary
|
|
238
|
+
console.log('\n' + '='.repeat(70));
|
|
239
|
+
const summaryParts = [`${fileCount} files`, `${matchCount} equinumerous`];
|
|
240
|
+
if (supersetCount > 0) {
|
|
241
|
+
summaryParts.push(`${supersetCount} supersets`);
|
|
242
|
+
}
|
|
243
|
+
summaryParts.push(`${mismatchCount} mismatches`, `${errorCount} errors`);
|
|
244
|
+
console.log(`SUMMARY: ${summaryParts.join(', ')}`);
|
|
500
245
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
246
|
+
console.log(` flatlock total: ${totalFlatlock.toString().padStart(8)} packages`);
|
|
247
|
+
if (totalComparison > 0) {
|
|
248
|
+
console.log(` comparison total: ${totalComparison.toString().padStart(8)} packages`);
|
|
249
|
+
}
|
|
250
|
+
if (totalWorkspaces > 0) {
|
|
251
|
+
console.log(` workspaces: ${totalWorkspaces.toString().padStart(8)} excluded (local/workspace refs)`);
|
|
252
|
+
}
|
|
253
|
+
if (supersetCount > 0) {
|
|
254
|
+
console.log(` supersets: ${supersetCount.toString().padStart(8)} (flatlock found more via reachability)`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Exit with error only for true mismatches (not supersets)
|
|
258
|
+
// Supersets are expected: flatlock's reachability analysis is more thorough
|
|
259
|
+
if (mismatchCount > 0) {
|
|
260
|
+
process.exit(1);
|
|
507
261
|
}
|
|
508
262
|
}
|
|
509
263
|
|
|
510
|
-
main().catch(
|
|
511
|
-
await cleanup();
|
|
264
|
+
main().catch(err => {
|
|
512
265
|
console.error('Fatal error:', err.message);
|
|
513
266
|
process.exit(1);
|
|
514
267
|
});
|