flatlock 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/flatlock-cmp.js +79 -352
- package/dist/compare.d.ts +63 -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 +58 -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 +82 -0
- package/dist/parsers/npm.d.ts.map +1 -0
- package/dist/parsers/pnpm.d.ts +60 -0
- package/dist/parsers/pnpm.d.ts.map +1 -0
- package/dist/parsers/yarn-berry.d.ts +65 -0
- package/dist/parsers/yarn-berry.d.ts.map +1 -0
- package/dist/parsers/yarn-classic.d.ts +64 -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/package.json +13 -4
- package/src/compare.js +263 -0
- package/src/detect.js +6 -4
- package/src/index.js +24 -13
- package/src/parsers/index.js +4 -4
- package/src/parsers/npm.js +6 -7
- package/src/parsers/pnpm.js +16 -3
- package/src/parsers/yarn-berry.js +8 -6
- package/src/parsers/yarn-classic.js +4 -3
- package/src/result.js +2 -2
- package/src/types.d.ts +54 -0
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ 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
|
|
package/bin/flatlock-cmp.js
CHANGED
|
@@ -1,243 +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
8
|
/**
|
|
242
9
|
* Get comparison parser name for type
|
|
243
10
|
*/
|
|
@@ -251,28 +18,6 @@ function getComparisonName(type) {
|
|
|
251
18
|
}
|
|
252
19
|
}
|
|
253
20
|
|
|
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
|
-
|
|
276
21
|
/**
|
|
277
22
|
* Convert glob pattern to regex
|
|
278
23
|
*/
|
|
@@ -308,30 +53,21 @@ async function findFiles(dir, pattern) {
|
|
|
308
53
|
}
|
|
309
54
|
|
|
310
55
|
/**
|
|
311
|
-
* Process a single lockfile
|
|
56
|
+
* Process a single lockfile using flatlock.compare()
|
|
312
57
|
*/
|
|
313
58
|
async function processFile(filepath, baseDir) {
|
|
314
59
|
try {
|
|
315
|
-
const
|
|
316
|
-
const type = flatlock.detectType({ path: filepath, content });
|
|
60
|
+
const result = await flatlock.compare(filepath);
|
|
317
61
|
const rel = baseDir ? filepath.replace(baseDir + '/', '') : filepath;
|
|
318
|
-
const comparisonName = getComparisonName(type);
|
|
319
|
-
|
|
320
|
-
const flatlockSet = await getPackagesFromFlatlock(filepath);
|
|
321
|
-
let comparisonResult;
|
|
62
|
+
const comparisonName = getComparisonName(result.type);
|
|
322
63
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
} catch (err) {
|
|
326
|
-
comparisonResult = null;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if (!comparisonResult) {
|
|
64
|
+
if (result.identical === null) {
|
|
65
|
+
// Unsupported type or no comparison available
|
|
330
66
|
return {
|
|
331
|
-
type,
|
|
67
|
+
type: result.type,
|
|
332
68
|
path: rel,
|
|
333
69
|
comparisonName,
|
|
334
|
-
flatlockCount:
|
|
70
|
+
flatlockCount: result.flatlockCount,
|
|
335
71
|
comparisonCount: null,
|
|
336
72
|
workspaceCount: 0,
|
|
337
73
|
identical: null,
|
|
@@ -340,20 +76,16 @@ async function processFile(filepath, baseDir) {
|
|
|
340
76
|
};
|
|
341
77
|
}
|
|
342
78
|
|
|
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
79
|
return {
|
|
348
|
-
type,
|
|
80
|
+
type: result.type,
|
|
349
81
|
path: rel,
|
|
350
82
|
comparisonName,
|
|
351
|
-
flatlockCount:
|
|
352
|
-
comparisonCount:
|
|
353
|
-
workspaceCount,
|
|
354
|
-
identical,
|
|
355
|
-
onlyInFlatlock,
|
|
356
|
-
onlyInComparison
|
|
83
|
+
flatlockCount: result.flatlockCount,
|
|
84
|
+
comparisonCount: result.comparisonCount,
|
|
85
|
+
workspaceCount: result.workspaceCount,
|
|
86
|
+
identical: result.identical,
|
|
87
|
+
onlyInFlatlock: result.onlyInFlatlock,
|
|
88
|
+
onlyInComparison: result.onlyInComparison
|
|
357
89
|
};
|
|
358
90
|
} catch (err) {
|
|
359
91
|
const rel = baseDir ? filepath.replace(baseDir + '/', '') : filepath;
|
|
@@ -423,92 +155,87 @@ Examples:
|
|
|
423
155
|
let matchCount = 0;
|
|
424
156
|
let mismatchCount = 0;
|
|
425
157
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const result = await processFile(file, baseDir);
|
|
158
|
+
for (const file of files) {
|
|
159
|
+
const result = await processFile(file, baseDir);
|
|
429
160
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
continue;
|
|
161
|
+
if (result.error) {
|
|
162
|
+
errorCount++;
|
|
163
|
+
if (!values.quiet) {
|
|
164
|
+
console.log(`\n❌ ERROR: ${result.path}`);
|
|
165
|
+
console.log(` ${result.error}`);
|
|
437
166
|
}
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
438
169
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
170
|
+
fileCount++;
|
|
171
|
+
totalFlatlock += result.flatlockCount;
|
|
172
|
+
totalWorkspaces += result.workspaceCount || 0;
|
|
442
173
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
}
|
|
449
|
-
continue;
|
|
174
|
+
if (result.comparisonCount === null) {
|
|
175
|
+
if (!values.quiet) {
|
|
176
|
+
console.log(`\n⚠️ ${result.path}`);
|
|
177
|
+
console.log(` flatlock: ${result.flatlockCount} packages`);
|
|
178
|
+
console.log(` ${result.comparisonName}: unavailable`);
|
|
450
179
|
}
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
451
182
|
|
|
452
|
-
|
|
183
|
+
totalComparison += result.comparisonCount;
|
|
453
184
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
console.log(` count: flatlock=${result.flatlockCount} ${result.comparisonName}=${result.comparisonCount}`);
|
|
460
|
-
console.log(` sets: identical`);
|
|
461
|
-
}
|
|
462
|
-
} else {
|
|
463
|
-
mismatchCount++;
|
|
464
|
-
console.log(`\n❌ ${result.path}`);
|
|
185
|
+
if (result.identical) {
|
|
186
|
+
matchCount++;
|
|
187
|
+
if (!values.quiet) {
|
|
188
|
+
const wsNote = result.workspaceCount > 0 ? ` (${result.workspaceCount} workspaces excluded)` : '';
|
|
189
|
+
console.log(`✓ ${result.path}${wsNote}`);
|
|
465
190
|
console.log(` count: flatlock=${result.flatlockCount} ${result.comparisonName}=${result.comparisonCount}`);
|
|
466
|
-
console.log(` sets:
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
191
|
+
console.log(` sets: identical`);
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
mismatchCount++;
|
|
195
|
+
console.log(`\n❌ ${result.path}`);
|
|
196
|
+
console.log(` count: flatlock=${result.flatlockCount} ${result.comparisonName}=${result.comparisonCount}`);
|
|
197
|
+
console.log(` sets: MISMATCH`);
|
|
198
|
+
|
|
199
|
+
if (result.onlyInFlatlock.length > 0) {
|
|
200
|
+
console.log(` only in flatlock (${result.onlyInFlatlock.length}):`);
|
|
201
|
+
for (const pkg of result.onlyInFlatlock.slice(0, 10)) {
|
|
202
|
+
console.log(` + ${pkg}`);
|
|
476
203
|
}
|
|
204
|
+
if (result.onlyInFlatlock.length > 10) {
|
|
205
|
+
console.log(` ... and ${result.onlyInFlatlock.length - 10} more`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
477
208
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
}
|
|
209
|
+
if (result.onlyInComparison.length > 0) {
|
|
210
|
+
console.log(` only in ${result.comparisonName} (${result.onlyInComparison.length}):`);
|
|
211
|
+
for (const pkg of result.onlyInComparison.slice(0, 10)) {
|
|
212
|
+
console.log(` - ${pkg}`);
|
|
213
|
+
}
|
|
214
|
+
if (result.onlyInComparison.length > 10) {
|
|
215
|
+
console.log(` ... and ${result.onlyInComparison.length - 10} more`);
|
|
486
216
|
}
|
|
487
217
|
}
|
|
488
218
|
}
|
|
219
|
+
}
|
|
489
220
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
221
|
+
// Summary
|
|
222
|
+
console.log('\n' + '='.repeat(70));
|
|
223
|
+
console.log(`SUMMARY: ${fileCount} files, ${matchCount} identical, ${mismatchCount} mismatches, ${errorCount} errors`);
|
|
224
|
+
console.log(` flatlock total: ${totalFlatlock.toString().padStart(8)} packages`);
|
|
225
|
+
if (totalComparison > 0) {
|
|
226
|
+
console.log(` comparison total: ${totalComparison.toString().padStart(8)} packages`);
|
|
227
|
+
}
|
|
228
|
+
if (totalWorkspaces > 0) {
|
|
229
|
+
console.log(` workspaces: ${totalWorkspaces.toString().padStart(8)} excluded (local/workspace refs)`);
|
|
230
|
+
}
|
|
500
231
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
}
|
|
505
|
-
} finally {
|
|
506
|
-
await cleanup();
|
|
232
|
+
// Exit with error if any mismatches
|
|
233
|
+
if (mismatchCount > 0) {
|
|
234
|
+
process.exit(1);
|
|
507
235
|
}
|
|
508
236
|
}
|
|
509
237
|
|
|
510
|
-
main().catch(
|
|
511
|
-
await cleanup();
|
|
238
|
+
main().catch(err => {
|
|
512
239
|
console.error('Fatal error:', err.message);
|
|
513
240
|
process.exit(1);
|
|
514
241
|
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compare flatlock output against established parser for a lockfile
|
|
3
|
+
* @param {string} filepath - Path to lockfile
|
|
4
|
+
* @param {CompareOptions} [options] - Options
|
|
5
|
+
* @returns {Promise<ComparisonResult>}
|
|
6
|
+
*/
|
|
7
|
+
export function compare(filepath: string, options?: CompareOptions): Promise<ComparisonResult>;
|
|
8
|
+
/**
|
|
9
|
+
* Compare multiple lockfiles
|
|
10
|
+
* @param {string[]} filepaths - Paths to lockfiles
|
|
11
|
+
* @param {CompareOptions} [options] - Options
|
|
12
|
+
* @returns {AsyncGenerator<ComparisonResult & { filepath: string }>}
|
|
13
|
+
*/
|
|
14
|
+
export function compareAll(filepaths: string[], options?: CompareOptions): AsyncGenerator<ComparisonResult & {
|
|
15
|
+
filepath: string;
|
|
16
|
+
}>;
|
|
17
|
+
export type CompareOptions = {
|
|
18
|
+
/**
|
|
19
|
+
* - Temp directory for Arborist (npm only)
|
|
20
|
+
*/
|
|
21
|
+
tmpDir?: string;
|
|
22
|
+
};
|
|
23
|
+
export type ComparisonResult = {
|
|
24
|
+
/**
|
|
25
|
+
* - Lockfile type
|
|
26
|
+
*/
|
|
27
|
+
type: string;
|
|
28
|
+
/**
|
|
29
|
+
* - Whether flatlock matches comparison parser
|
|
30
|
+
*/
|
|
31
|
+
identical: boolean | null;
|
|
32
|
+
/**
|
|
33
|
+
* - Number of packages found by flatlock
|
|
34
|
+
*/
|
|
35
|
+
flatlockCount: number;
|
|
36
|
+
/**
|
|
37
|
+
* - Number of packages found by comparison parser
|
|
38
|
+
*/
|
|
39
|
+
comparisonCount?: number;
|
|
40
|
+
/**
|
|
41
|
+
* - Number of workspace packages skipped
|
|
42
|
+
*/
|
|
43
|
+
workspaceCount?: number;
|
|
44
|
+
/**
|
|
45
|
+
* - Packages only found by flatlock
|
|
46
|
+
*/
|
|
47
|
+
onlyInFlatlock?: string[];
|
|
48
|
+
/**
|
|
49
|
+
* - Packages only found by comparison parser
|
|
50
|
+
*/
|
|
51
|
+
onlyInComparison?: string[];
|
|
52
|
+
};
|
|
53
|
+
export type PackagesResult = {
|
|
54
|
+
/**
|
|
55
|
+
* - Set of package@version strings
|
|
56
|
+
*/
|
|
57
|
+
packages: Set<string>;
|
|
58
|
+
/**
|
|
59
|
+
* - Number of workspace packages skipped
|
|
60
|
+
*/
|
|
61
|
+
workspaceCount: number;
|
|
62
|
+
};
|
|
63
|
+
//# sourceMappingURL=compare.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compare.d.ts","sourceRoot":"","sources":["../src/compare.js"],"names":[],"mappings":"AAyMA;;;;;GAKG;AACH,kCAJW,MAAM,YACN,cAAc,GACZ,OAAO,CAAC,gBAAgB,CAAC,CA6CrC;AAED;;;;;GAKG;AACH,sCAJW,MAAM,EAAE,YACR,cAAc,GACZ,cAAc,CAAC,gBAAgB,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAMnE;;;;;aAzPa,MAAM;;;;;;UAKN,MAAM;;;;eACN,OAAO,GAAG,IAAI;;;;mBACd,MAAM;;;;sBACN,MAAM;;;;qBACN,MAAM;;;;qBACN,MAAM,EAAE;;;;uBACR,MAAM,EAAE;;;;;;cAKR,GAAG,CAAC,MAAM,CAAC;;;;oBACX,MAAM"}
|
package/dist/detect.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect lockfile type from content and/or path
|
|
3
|
+
*
|
|
4
|
+
* Content is the primary signal - we actually parse the content to verify
|
|
5
|
+
* it's a valid lockfile of the detected type. This prevents spoofing attacks
|
|
6
|
+
* where malicious content contains detection markers in strings/comments.
|
|
7
|
+
*
|
|
8
|
+
* Path is only used as a fallback hint when content is not provided.
|
|
9
|
+
*
|
|
10
|
+
* @param {Object} options - Detection options
|
|
11
|
+
* @param {string} [options.path] - Path to the lockfile (optional hint)
|
|
12
|
+
* @param {string} [options.content] - Lockfile content (primary signal)
|
|
13
|
+
* @returns {LockfileType}
|
|
14
|
+
* @throws {Error} If unable to detect lockfile type
|
|
15
|
+
*/
|
|
16
|
+
export function detectType({ path, content }?: {
|
|
17
|
+
path?: string | undefined;
|
|
18
|
+
content?: string | undefined;
|
|
19
|
+
}): LockfileType;
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {'npm' | 'pnpm' | 'yarn-classic' | 'yarn-berry'} LockfileType
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Lockfile type constants
|
|
25
|
+
*/
|
|
26
|
+
export const Type: Readonly<{
|
|
27
|
+
NPM: "npm";
|
|
28
|
+
PNPM: "pnpm";
|
|
29
|
+
YARN_CLASSIC: "yarn-classic";
|
|
30
|
+
YARN_BERRY: "yarn-berry";
|
|
31
|
+
}>;
|
|
32
|
+
export type LockfileType = "npm" | "pnpm" | "yarn-classic" | "yarn-berry";
|
|
33
|
+
//# sourceMappingURL=detect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detect.d.ts","sourceRoot":"","sources":["../src/detect.js"],"names":[],"mappings":"AA8FA;;;;;;;;;;;;;;GAcG;AACH,+CALG;IAAyB,IAAI;IACJ,OAAO;CAChC,GAAU,YAAY,CAgDxB;AAtJD;;GAEG;AAEH;;GAEG;AACH;;;;;GAKG;2BAXU,KAAK,GAAG,MAAM,GAAG,cAAc,GAAG,YAAY"}
|