flatlock 1.3.0 → 1.5.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/bin/flatcover.js +322 -58
- package/bin/flatlock-cmp.js +2 -2
- 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 +72 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/parsers/index.d.ts +5 -0
- package/dist/parsers/index.d.ts.map +1 -0
- package/dist/parsers/npm.d.ts +154 -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 +154 -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 +197 -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 +238 -0
- package/dist/set.d.ts.map +1 -0
- package/package.json +9 -4
- package/src/compare.js +5 -7
package/bin/flatcover.js
CHANGED
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
|
|
15
15
|
import { parseArgs } from 'node:util';
|
|
16
16
|
import { readFileSync } from 'node:fs';
|
|
17
|
+
import { readFile, writeFile, rename, mkdir } from 'node:fs/promises';
|
|
18
|
+
import { createReadStream } from 'node:fs';
|
|
19
|
+
import { createInterface } from 'node:readline';
|
|
17
20
|
import { dirname, join } from 'node:path';
|
|
18
21
|
import { Pool, RetryAgent } from 'undici';
|
|
19
22
|
import { FlatlockSet } from '../src/set.js';
|
|
@@ -21,6 +24,7 @@ import { FlatlockSet } from '../src/set.js';
|
|
|
21
24
|
const { values, positionals } = parseArgs({
|
|
22
25
|
options: {
|
|
23
26
|
workspace: { type: 'string', short: 'w' },
|
|
27
|
+
list: { type: 'string', short: 'l' },
|
|
24
28
|
dev: { type: 'boolean', default: false },
|
|
25
29
|
peer: { type: 'boolean', default: true },
|
|
26
30
|
specs: { type: 'boolean', short: 's', default: false },
|
|
@@ -34,25 +38,39 @@ const { values, positionals } = parseArgs({
|
|
|
34
38
|
concurrency: { type: 'string', default: '20' },
|
|
35
39
|
progress: { type: 'boolean', default: false },
|
|
36
40
|
summary: { type: 'boolean', default: false },
|
|
41
|
+
before: { type: 'string', short: 'b' },
|
|
42
|
+
cache: { type: 'string', short: 'c' },
|
|
37
43
|
help: { type: 'boolean', short: 'h' }
|
|
38
44
|
},
|
|
39
45
|
allowPositionals: true
|
|
40
46
|
});
|
|
41
47
|
|
|
42
|
-
if
|
|
48
|
+
// Check if stdin input is requested via '-' positional argument (Unix convention)
|
|
49
|
+
const useStdin = positionals[0] === '-';
|
|
50
|
+
|
|
51
|
+
// Determine if we have a valid input source
|
|
52
|
+
const hasInputSource = positionals.length > 0 || values.list;
|
|
53
|
+
|
|
54
|
+
if (values.help || !hasInputSource) {
|
|
43
55
|
console.log(`flatcover - Check lockfile package coverage against a registry
|
|
44
56
|
|
|
45
57
|
Usage:
|
|
46
58
|
flatcover <lockfile> --cover
|
|
47
|
-
flatcover
|
|
59
|
+
flatcover --list packages.json --cover
|
|
60
|
+
cat packages.ndjson | flatcover - --cover
|
|
48
61
|
flatcover <lockfile> --cover --registry <url> --auth user:pass
|
|
49
62
|
|
|
63
|
+
Input sources (mutually exclusive):
|
|
64
|
+
<lockfile> Parse lockfile (package-lock.json, pnpm-lock.yaml, yarn.lock)
|
|
65
|
+
-l, --list <file> Read JSON array of {name, version} objects from file
|
|
66
|
+
- Read NDJSON {name, version} objects from stdin (one per line)
|
|
67
|
+
|
|
50
68
|
Options:
|
|
51
|
-
-w, --workspace <path> Workspace path within monorepo
|
|
69
|
+
-w, --workspace <path> Workspace path within monorepo (lockfile mode only)
|
|
52
70
|
-s, --specs Include version (name@version or {name,version})
|
|
53
71
|
--json Output as JSON array
|
|
54
72
|
--ndjson Output as newline-delimited JSON (streaming)
|
|
55
|
-
--full Include all metadata (integrity, resolved)
|
|
73
|
+
--full Include all metadata (integrity, resolved, time)
|
|
56
74
|
--dev Include dev dependencies (default: false)
|
|
57
75
|
--peer Include peer dependencies (default: true)
|
|
58
76
|
-h, --help Show this help
|
|
@@ -65,17 +83,38 @@ Coverage options:
|
|
|
65
83
|
--concurrency <n> Concurrent requests (default: 20)
|
|
66
84
|
--progress Show progress on stderr
|
|
67
85
|
--summary Show coverage summary on stderr
|
|
86
|
+
--before <date> Only count versions published before this ISO date
|
|
87
|
+
-c, --cache <dir> Cache packuments to disk for faster subsequent runs
|
|
68
88
|
|
|
69
89
|
Output formats (with --cover):
|
|
70
|
-
(default) CSV
|
|
71
|
-
--json
|
|
72
|
-
--ndjson
|
|
90
|
+
(default) CSV format (sorted by name, version)
|
|
91
|
+
--json JSON array (sorted by name, version)
|
|
92
|
+
--ndjson Newline-delimited JSON (streaming, unsorted)
|
|
93
|
+
|
|
94
|
+
Output fields:
|
|
95
|
+
(default) name, version, present
|
|
96
|
+
--full Adds: spec, integrity, resolved, time (works with all formats)
|
|
73
97
|
|
|
74
98
|
Examples:
|
|
99
|
+
# From lockfile
|
|
75
100
|
flatcover package-lock.json --cover
|
|
101
|
+
flatcover package-lock.json --cover --full --json
|
|
102
|
+
|
|
103
|
+
# From JSON list file
|
|
104
|
+
flatcover --list packages.json --cover --summary
|
|
105
|
+
echo '[{"name":"lodash","version":"4.17.21"}]' > pkgs.json && flatcover -l pkgs.json --cover
|
|
106
|
+
|
|
107
|
+
# Time-travel reanalysis: capture full output with timestamps
|
|
108
|
+
flatcover package-lock.json --cover --full --json > coverage.json
|
|
109
|
+
# Later, filter locally by publication date without re-fetching registry
|
|
110
|
+
|
|
111
|
+
# From stdin (NDJSON) - use '-' to read from stdin
|
|
112
|
+
echo '{"name":"lodash","version":"4.17.21"}' | flatcover - --cover
|
|
113
|
+
cat packages.ndjson | flatcover - --cover --json
|
|
114
|
+
|
|
115
|
+
# With custom registry
|
|
76
116
|
flatcover package-lock.json --cover --registry https://npm.pkg.github.com --token ghp_xxx
|
|
77
|
-
flatcover pnpm-lock.yaml --cover --auth admin:secret --ndjson
|
|
78
|
-
flatcover pnpm-lock.yaml -w packages/core --cover --summary`);
|
|
117
|
+
flatcover pnpm-lock.yaml --cover --auth admin:secret --ndjson`);
|
|
79
118
|
process.exit(values.help ? 0 : 1);
|
|
80
119
|
}
|
|
81
120
|
|
|
@@ -89,6 +128,19 @@ if (values.auth && values.token) {
|
|
|
89
128
|
process.exit(1);
|
|
90
129
|
}
|
|
91
130
|
|
|
131
|
+
// Validate mutually exclusive input sources
|
|
132
|
+
// Note: useStdin means positionals[0] === '-', so it's already counted in positionals.length
|
|
133
|
+
if (positionals.length > 0 && values.list) {
|
|
134
|
+
console.error('Error: Cannot use both lockfile/stdin and --list');
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// --workspace only works with lockfile input (not stdin or --list)
|
|
139
|
+
if (values.workspace && (useStdin || values.list || !positionals.length)) {
|
|
140
|
+
console.error('Error: --workspace can only be used with lockfile input');
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
|
|
92
144
|
// --full implies --specs
|
|
93
145
|
if (values.full) {
|
|
94
146
|
values.specs = true;
|
|
@@ -102,6 +154,70 @@ if (values.cover) {
|
|
|
102
154
|
const lockfilePath = positionals[0];
|
|
103
155
|
const concurrency = Math.max(1, Math.min(50, Number.parseInt(values.concurrency, 10) || 20));
|
|
104
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Read packages from a JSON list file
|
|
159
|
+
* @param {string} filePath - Path to JSON file containing [{name, version}, ...]
|
|
160
|
+
* @returns {Array<{ name: string, version: string }>}
|
|
161
|
+
*/
|
|
162
|
+
function readJsonList(filePath) {
|
|
163
|
+
const content = readFileSync(filePath, 'utf8');
|
|
164
|
+
const data = JSON.parse(content);
|
|
165
|
+
|
|
166
|
+
if (!Array.isArray(data)) {
|
|
167
|
+
throw new Error('--list file must contain a JSON array');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const packages = [];
|
|
171
|
+
for (const item of data) {
|
|
172
|
+
if (!item.name || !item.version) {
|
|
173
|
+
throw new Error('Each item in --list must have "name" and "version" fields');
|
|
174
|
+
}
|
|
175
|
+
packages.push({
|
|
176
|
+
name: item.name,
|
|
177
|
+
version: item.version,
|
|
178
|
+
integrity: item.integrity,
|
|
179
|
+
resolved: item.resolved
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return packages;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Read packages from stdin as NDJSON
|
|
188
|
+
* @returns {Promise<Array<{ name: string, version: string }>>}
|
|
189
|
+
*/
|
|
190
|
+
async function readStdinNdjson() {
|
|
191
|
+
const packages = [];
|
|
192
|
+
|
|
193
|
+
const rl = createInterface({
|
|
194
|
+
input: process.stdin,
|
|
195
|
+
crlfDelay: Infinity
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
for await (const line of rl) {
|
|
199
|
+
const trimmed = line.trim();
|
|
200
|
+
if (!trimmed) continue;
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const item = JSON.parse(trimmed);
|
|
204
|
+
if (!item.name || !item.version) {
|
|
205
|
+
throw new Error('Each line must have "name" and "version" fields');
|
|
206
|
+
}
|
|
207
|
+
packages.push({
|
|
208
|
+
name: item.name,
|
|
209
|
+
version: item.version,
|
|
210
|
+
integrity: item.integrity,
|
|
211
|
+
resolved: item.resolved
|
|
212
|
+
});
|
|
213
|
+
} catch (err) {
|
|
214
|
+
throw new Error(`Invalid JSON on stdin: ${err.message}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return packages;
|
|
219
|
+
}
|
|
220
|
+
|
|
105
221
|
/**
|
|
106
222
|
* Encode package name for URL (handle scoped packages)
|
|
107
223
|
* @param {string} name - Package name like @babel/core
|
|
@@ -112,6 +228,68 @@ function encodePackageName(name) {
|
|
|
112
228
|
return name.replace('/', '%2f');
|
|
113
229
|
}
|
|
114
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Read cached packument metadata (etag, lastModified)
|
|
233
|
+
* @param {string} cacheDir - Cache directory path
|
|
234
|
+
* @param {string} encodedName - URL-encoded package name
|
|
235
|
+
* @returns {Promise<{ etag?: string, lastModified?: string } | null>}
|
|
236
|
+
*/
|
|
237
|
+
async function readCacheMeta(cacheDir, encodedName) {
|
|
238
|
+
try {
|
|
239
|
+
const metaPath = join(cacheDir, `${encodedName}.meta.json`);
|
|
240
|
+
const content = await readFile(metaPath, 'utf8');
|
|
241
|
+
return JSON.parse(content);
|
|
242
|
+
} catch {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Read cached packument from disk
|
|
249
|
+
* @param {string} cacheDir - Cache directory path
|
|
250
|
+
* @param {string} encodedName - URL-encoded package name
|
|
251
|
+
* @returns {Promise<object | null>}
|
|
252
|
+
*/
|
|
253
|
+
async function readCachedPackument(cacheDir, encodedName) {
|
|
254
|
+
try {
|
|
255
|
+
const cachePath = join(cacheDir, `${encodedName}.json`);
|
|
256
|
+
const content = await readFile(cachePath, 'utf8');
|
|
257
|
+
return JSON.parse(content);
|
|
258
|
+
} catch {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Write packument and metadata to cache atomically
|
|
265
|
+
* @param {string} cacheDir - Cache directory path
|
|
266
|
+
* @param {string} encodedName - URL-encoded package name
|
|
267
|
+
* @param {string} body - Raw packument JSON string
|
|
268
|
+
* @param {{ etag?: string, lastModified?: string }} meta - Cache metadata
|
|
269
|
+
*/
|
|
270
|
+
async function writeCache(cacheDir, encodedName, body, meta) {
|
|
271
|
+
await mkdir(cacheDir, { recursive: true });
|
|
272
|
+
|
|
273
|
+
const cachePath = join(cacheDir, `${encodedName}.json`);
|
|
274
|
+
const metaPath = join(cacheDir, `${encodedName}.meta.json`);
|
|
275
|
+
const pid = process.pid;
|
|
276
|
+
|
|
277
|
+
// Write packument atomically
|
|
278
|
+
const tmpCachePath = `${cachePath}.${pid}.tmp`;
|
|
279
|
+
await writeFile(tmpCachePath, body);
|
|
280
|
+
await rename(tmpCachePath, cachePath);
|
|
281
|
+
|
|
282
|
+
// Write metadata atomically
|
|
283
|
+
const metaObj = {
|
|
284
|
+
etag: meta.etag,
|
|
285
|
+
lastModified: meta.lastModified,
|
|
286
|
+
fetchedAt: new Date().toISOString()
|
|
287
|
+
};
|
|
288
|
+
const tmpMetaPath = `${metaPath}.${pid}.tmp`;
|
|
289
|
+
await writeFile(tmpMetaPath, JSON.stringify(metaObj));
|
|
290
|
+
await rename(tmpMetaPath, metaPath);
|
|
291
|
+
}
|
|
292
|
+
|
|
115
293
|
/**
|
|
116
294
|
* Create undici client with retry support
|
|
117
295
|
* @param {string} registryUrl
|
|
@@ -161,42 +339,55 @@ function createClient(registryUrl, { auth, token }) {
|
|
|
161
339
|
|
|
162
340
|
/**
|
|
163
341
|
* Check coverage for all dependencies
|
|
164
|
-
* @param {Array<{ name: string, version: string }>} deps
|
|
165
|
-
* @param {{ registry: string, auth?: string, token?: string, progress: boolean }} options
|
|
166
|
-
* @returns {AsyncGenerator<{ name: string, version: string, present: boolean, error?: string }>}
|
|
342
|
+
* @param {Array<{ name: string, version: string, integrity?: string, resolved?: string }>} deps
|
|
343
|
+
* @param {{ registry: string, auth?: string, token?: string, progress: boolean, before?: string, cache?: string }} options
|
|
344
|
+
* @returns {AsyncGenerator<{ name: string, version: string, present: boolean, integrity?: string, resolved?: string, error?: string }>}
|
|
167
345
|
*/
|
|
168
|
-
async function* checkCoverage(deps, { registry, auth, token, progress }) {
|
|
346
|
+
async function* checkCoverage(deps, { registry, auth, token, progress, before, cache }) {
|
|
169
347
|
const { client, headers, baseUrl } = createClient(registry, { auth, token });
|
|
170
348
|
|
|
171
349
|
// Group by package name to avoid duplicate requests
|
|
172
|
-
|
|
350
|
+
// Store full dep info (including integrity/resolved) keyed by version
|
|
351
|
+
/** @type {Map<string, Map<string, { name: string, version: string, integrity?: string, resolved?: string }>>} */
|
|
173
352
|
const byPackage = new Map();
|
|
174
353
|
for (const dep of deps) {
|
|
175
354
|
if (!byPackage.has(dep.name)) {
|
|
176
|
-
byPackage.set(dep.name, new
|
|
355
|
+
byPackage.set(dep.name, new Map());
|
|
177
356
|
}
|
|
178
|
-
byPackage.get(dep.name).
|
|
357
|
+
byPackage.get(dep.name).set(dep.version, dep);
|
|
179
358
|
}
|
|
180
359
|
|
|
181
360
|
const packages = [...byPackage.entries()];
|
|
182
361
|
let completed = 0;
|
|
183
|
-
const total =
|
|
362
|
+
const total = deps.length;
|
|
184
363
|
|
|
185
364
|
// Process in batches for bounded concurrency
|
|
186
365
|
for (let i = 0; i < packages.length; i += concurrency) {
|
|
187
366
|
const batch = packages.slice(i, i + concurrency);
|
|
188
367
|
|
|
189
368
|
const results = await Promise.all(
|
|
190
|
-
batch.map(async ([name,
|
|
369
|
+
batch.map(async ([name, versionMap]) => {
|
|
191
370
|
const encodedName = encodePackageName(name);
|
|
192
371
|
const basePath = baseUrl.pathname.replace(/\/$/, '');
|
|
193
372
|
const path = `${basePath}/${encodedName}`;
|
|
194
373
|
|
|
195
374
|
try {
|
|
375
|
+
// Build request headers, adding conditional request headers if cached
|
|
376
|
+
const reqHeaders = { ...headers };
|
|
377
|
+
let cacheMeta = null;
|
|
378
|
+
if (cache) {
|
|
379
|
+
cacheMeta = await readCacheMeta(cache, encodedName);
|
|
380
|
+
if (cacheMeta?.etag) {
|
|
381
|
+
reqHeaders['If-None-Match'] = cacheMeta.etag;
|
|
382
|
+
} else if (cacheMeta?.lastModified) {
|
|
383
|
+
reqHeaders['If-Modified-Since'] = cacheMeta.lastModified;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
196
387
|
const response = await client.request({
|
|
197
388
|
method: 'GET',
|
|
198
389
|
path,
|
|
199
|
-
headers
|
|
390
|
+
headers: reqHeaders
|
|
200
391
|
});
|
|
201
392
|
|
|
202
393
|
const chunks = [];
|
|
@@ -210,27 +401,59 @@ async function* checkCoverage(deps, { registry, auth, token, progress }) {
|
|
|
210
401
|
}
|
|
211
402
|
|
|
212
403
|
let packumentVersions = null;
|
|
213
|
-
|
|
404
|
+
let packumentTime = null;
|
|
405
|
+
|
|
406
|
+
if (response.statusCode === 304 && cache) {
|
|
407
|
+
// Cache hit - read from disk
|
|
408
|
+
const cachedPackument = await readCachedPackument(cache, encodedName);
|
|
409
|
+
if (cachedPackument) {
|
|
410
|
+
packumentVersions = cachedPackument.versions || {};
|
|
411
|
+
packumentTime = cachedPackument.time || {};
|
|
412
|
+
}
|
|
413
|
+
} else if (response.statusCode === 200) {
|
|
214
414
|
const body = Buffer.concat(chunks).toString('utf8');
|
|
215
415
|
const packument = JSON.parse(body);
|
|
216
416
|
packumentVersions = packument.versions || {};
|
|
417
|
+
packumentTime = packument.time || {};
|
|
418
|
+
|
|
419
|
+
// Write to cache if enabled
|
|
420
|
+
if (cache) {
|
|
421
|
+
await writeCache(cache, encodedName, body, {
|
|
422
|
+
etag: response.headers.etag,
|
|
423
|
+
lastModified: response.headers['last-modified']
|
|
424
|
+
});
|
|
425
|
+
}
|
|
217
426
|
}
|
|
218
427
|
|
|
219
|
-
// Check each version
|
|
428
|
+
// Check each version, preserving integrity/resolved from original dep
|
|
220
429
|
const versionResults = [];
|
|
221
|
-
for (const version of
|
|
222
|
-
|
|
223
|
-
|
|
430
|
+
for (const [version, dep] of versionMap) {
|
|
431
|
+
let present = packumentVersions ? !!packumentVersions[version] : false;
|
|
432
|
+
|
|
433
|
+
// Time travel: if --before set, only count if published before that date
|
|
434
|
+
if (present && before && packumentTime[version] >= before) {
|
|
435
|
+
present = false;
|
|
436
|
+
}
|
|
437
|
+
const result = { name, version, present };
|
|
438
|
+
if (dep.integrity) result.integrity = dep.integrity;
|
|
439
|
+
if (dep.resolved) result.resolved = dep.resolved;
|
|
440
|
+
if (packumentTime && packumentTime[version]) result.time = packumentTime[version];
|
|
441
|
+
versionResults.push(result);
|
|
224
442
|
}
|
|
225
443
|
return versionResults;
|
|
226
444
|
} catch (err) {
|
|
227
445
|
// Return error for all versions of this package
|
|
228
|
-
return [...
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
446
|
+
return [...versionMap.values()].map(dep => {
|
|
447
|
+
const result = {
|
|
448
|
+
name: dep.name,
|
|
449
|
+
version: dep.version,
|
|
450
|
+
present: false,
|
|
451
|
+
error: err.message
|
|
452
|
+
};
|
|
453
|
+
if (dep.integrity) result.integrity = dep.integrity;
|
|
454
|
+
if (dep.resolved) result.resolved = dep.resolved;
|
|
455
|
+
return result;
|
|
456
|
+
});
|
|
234
457
|
}
|
|
235
458
|
})
|
|
236
459
|
);
|
|
@@ -239,10 +462,10 @@ async function* checkCoverage(deps, { registry, auth, token, progress }) {
|
|
|
239
462
|
for (const packageResults of results) {
|
|
240
463
|
for (const result of packageResults) {
|
|
241
464
|
yield result;
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
465
|
+
completed++;
|
|
466
|
+
if (progress) {
|
|
467
|
+
process.stderr.write(`\r Checking: ${completed}/${total} package specs`);
|
|
468
|
+
}
|
|
246
469
|
}
|
|
247
470
|
}
|
|
248
471
|
}
|
|
@@ -260,7 +483,7 @@ async function* checkCoverage(deps, { registry, auth, token, progress }) {
|
|
|
260
483
|
*/
|
|
261
484
|
function formatDep(dep, { specs, full }) {
|
|
262
485
|
if (full) {
|
|
263
|
-
const obj = { name: dep.name, version: dep.version };
|
|
486
|
+
const obj = { name: dep.name, version: dep.version, spec: `${dep.name}@${dep.version}` };
|
|
264
487
|
if (dep.integrity) obj.integrity = dep.integrity;
|
|
265
488
|
if (dep.resolved) obj.resolved = dep.resolved;
|
|
266
489
|
return obj;
|
|
@@ -300,10 +523,10 @@ function outputDeps(deps, { specs, json, ndjson, full }) {
|
|
|
300
523
|
|
|
301
524
|
/**
|
|
302
525
|
* Output coverage results
|
|
303
|
-
* @param {AsyncGenerator<{ name: string, version: string, present: boolean, error?: string }>} results
|
|
304
|
-
* @param {{ json: boolean, ndjson: boolean, summary: boolean }} options
|
|
526
|
+
* @param {AsyncGenerator<{ name: string, version: string, present: boolean, integrity?: string, resolved?: string, error?: string }>} results
|
|
527
|
+
* @param {{ json: boolean, ndjson: boolean, summary: boolean, full: boolean }} options
|
|
305
528
|
*/
|
|
306
|
-
async function outputCoverage(results, { json, ndjson, summary }) {
|
|
529
|
+
async function outputCoverage(results, { json, ndjson, summary, full }) {
|
|
307
530
|
const all = [];
|
|
308
531
|
let presentCount = 0;
|
|
309
532
|
let missingCount = 0;
|
|
@@ -317,7 +540,12 @@ async function outputCoverage(results, { json, ndjson, summary }) {
|
|
|
317
540
|
|
|
318
541
|
if (ndjson) {
|
|
319
542
|
// Stream immediately
|
|
320
|
-
|
|
543
|
+
const obj = { name: result.name, version: result.version, present: result.present };
|
|
544
|
+
if (full) obj.spec = `${result.name}@${result.version}`;
|
|
545
|
+
if (full && result.integrity) obj.integrity = result.integrity;
|
|
546
|
+
if (full && result.resolved) obj.resolved = result.resolved;
|
|
547
|
+
if (full && result.time) obj.time = result.time;
|
|
548
|
+
console.log(JSON.stringify(obj));
|
|
321
549
|
} else {
|
|
322
550
|
all.push(result);
|
|
323
551
|
}
|
|
@@ -328,13 +556,27 @@ async function outputCoverage(results, { json, ndjson, summary }) {
|
|
|
328
556
|
all.sort((a, b) => a.name.localeCompare(b.name) || a.version.localeCompare(b.version));
|
|
329
557
|
|
|
330
558
|
if (json) {
|
|
331
|
-
const data = all.map(r =>
|
|
559
|
+
const data = all.map(r => {
|
|
560
|
+
const obj = { name: r.name, version: r.version, present: r.present };
|
|
561
|
+
if (full) obj.spec = `${r.name}@${r.version}`;
|
|
562
|
+
if (full && r.integrity) obj.integrity = r.integrity;
|
|
563
|
+
if (full && r.resolved) obj.resolved = r.resolved;
|
|
564
|
+
if (full && r.time) obj.time = r.time;
|
|
565
|
+
return obj;
|
|
566
|
+
});
|
|
332
567
|
console.log(JSON.stringify(data, null, 2));
|
|
333
568
|
} else {
|
|
334
569
|
// CSV output
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
570
|
+
if (full) {
|
|
571
|
+
console.log('package,version,spec,present,integrity,resolved,time');
|
|
572
|
+
for (const r of all) {
|
|
573
|
+
console.log(`${r.name},${r.version},${r.name}@${r.version},${r.present},${r.integrity || ''},${r.resolved || ''},${r.time || ''}`);
|
|
574
|
+
}
|
|
575
|
+
} else {
|
|
576
|
+
console.log('package,version,present');
|
|
577
|
+
for (const r of all) {
|
|
578
|
+
console.log(`${r.name},${r.version},${r.present}`);
|
|
579
|
+
}
|
|
338
580
|
}
|
|
339
581
|
}
|
|
340
582
|
}
|
|
@@ -350,22 +592,41 @@ async function outputCoverage(results, { json, ndjson, summary }) {
|
|
|
350
592
|
}
|
|
351
593
|
|
|
352
594
|
try {
|
|
353
|
-
const lockfile = await FlatlockSet.fromPath(lockfilePath);
|
|
354
595
|
let deps;
|
|
355
596
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
597
|
+
// Determine input source and load dependencies
|
|
598
|
+
if (useStdin) {
|
|
599
|
+
// Read from stdin (NDJSON)
|
|
600
|
+
deps = await readStdinNdjson();
|
|
601
|
+
if (deps.length === 0) {
|
|
602
|
+
console.error('Error: No packages read from stdin');
|
|
603
|
+
process.exit(1);
|
|
604
|
+
}
|
|
605
|
+
} else if (values.list) {
|
|
606
|
+
// Read from JSON list file
|
|
607
|
+
deps = readJsonList(values.list);
|
|
608
|
+
if (deps.length === 0) {
|
|
609
|
+
console.error('Error: No packages found in --list file');
|
|
610
|
+
process.exit(1);
|
|
611
|
+
}
|
|
367
612
|
} else {
|
|
368
|
-
|
|
613
|
+
// Read from lockfile (existing behavior)
|
|
614
|
+
const lockfile = await FlatlockSet.fromPath(lockfilePath);
|
|
615
|
+
|
|
616
|
+
if (values.workspace) {
|
|
617
|
+
const repoDir = dirname(lockfilePath);
|
|
618
|
+
const workspacePkgPath = join(repoDir, values.workspace, 'package.json');
|
|
619
|
+
const workspacePkg = JSON.parse(readFileSync(workspacePkgPath, 'utf8'));
|
|
620
|
+
|
|
621
|
+
deps = await lockfile.dependenciesOf(workspacePkg, {
|
|
622
|
+
workspacePath: values.workspace,
|
|
623
|
+
repoDir,
|
|
624
|
+
dev: values.dev,
|
|
625
|
+
peer: values.peer
|
|
626
|
+
});
|
|
627
|
+
} else {
|
|
628
|
+
deps = lockfile;
|
|
629
|
+
}
|
|
369
630
|
}
|
|
370
631
|
|
|
371
632
|
if (values.cover) {
|
|
@@ -375,13 +636,16 @@ try {
|
|
|
375
636
|
registry: values.registry,
|
|
376
637
|
auth: values.auth,
|
|
377
638
|
token: values.token,
|
|
378
|
-
progress: values.progress
|
|
639
|
+
progress: values.progress,
|
|
640
|
+
before: values.before,
|
|
641
|
+
cache: values.cache
|
|
379
642
|
});
|
|
380
643
|
|
|
381
644
|
await outputCoverage(results, {
|
|
382
645
|
json: values.json,
|
|
383
646
|
ndjson: values.ndjson,
|
|
384
|
-
summary: values.summary
|
|
647
|
+
summary: values.summary,
|
|
648
|
+
full: values.full
|
|
385
649
|
});
|
|
386
650
|
} else {
|
|
387
651
|
// Standard flatlock mode
|
package/bin/flatlock-cmp.js
CHANGED
|
@@ -187,7 +187,7 @@ Examples:
|
|
|
187
187
|
const wsNote = result.workspaceCount > 0 ? ` (${result.workspaceCount} workspaces excluded)` : '';
|
|
188
188
|
console.log(`✓ ${result.path}${wsNote}`);
|
|
189
189
|
console.log(` count: flatlock=${result.flatlockCount} ${result.source}=${result.comparisonCount}`);
|
|
190
|
-
console.log(` sets: equinumerous`);
|
|
190
|
+
console.log(` sets: equinumerous\n`);
|
|
191
191
|
}
|
|
192
192
|
} else {
|
|
193
193
|
// Determine if this is a "superset" (flatlock found more, expected for pnpm)
|
|
@@ -203,7 +203,7 @@ Examples:
|
|
|
203
203
|
console.log(`⊃ ${result.path}${wsNote}`);
|
|
204
204
|
console.log(` count: flatlock=${result.flatlockCount} ${result.source}=${result.comparisonCount}`);
|
|
205
205
|
console.log(` sets: SUPERSET (+${result.onlyInFlatlock.length} reachable deps)`);
|
|
206
|
-
console.log(` note: flatlock's reachability analysis found transitive deps pnpm omits`);
|
|
206
|
+
console.log(` note: flatlock's reachability analysis found transitive deps pnpm omits\n`);
|
|
207
207
|
}
|
|
208
208
|
} else {
|
|
209
209
|
mismatchCount++;
|
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
/**
|
|
18
|
+
* Check which optional comparison parsers are available
|
|
19
|
+
* @returns {Promise<{ arborist: boolean, cyclonedx: boolean, pnpmLockfileFs: boolean, yarnCore: boolean }>}
|
|
20
|
+
*/
|
|
21
|
+
export function getAvailableParsers(): Promise<{
|
|
22
|
+
arborist: boolean;
|
|
23
|
+
cyclonedx: boolean;
|
|
24
|
+
pnpmLockfileFs: boolean;
|
|
25
|
+
yarnCore: boolean;
|
|
26
|
+
}>;
|
|
27
|
+
export type CompareOptions = {
|
|
28
|
+
/**
|
|
29
|
+
* - Temp directory for Arborist/CycloneDX (npm only)
|
|
30
|
+
*/
|
|
31
|
+
tmpDir?: string;
|
|
32
|
+
/**
|
|
33
|
+
* - Workspace paths for CycloneDX (-w flag)
|
|
34
|
+
*/
|
|
35
|
+
workspace?: string[];
|
|
36
|
+
};
|
|
37
|
+
export type ComparisonResult = {
|
|
38
|
+
/**
|
|
39
|
+
* - Lockfile type
|
|
40
|
+
*/
|
|
41
|
+
type: string;
|
|
42
|
+
/**
|
|
43
|
+
* - Comparison source used (e.g., '@npmcli/arborist', '@cyclonedx/cyclonedx-npm')
|
|
44
|
+
*/
|
|
45
|
+
source?: string;
|
|
46
|
+
/**
|
|
47
|
+
* - Whether flatlock and comparison have same cardinality
|
|
48
|
+
*/
|
|
49
|
+
equinumerous: boolean | null;
|
|
50
|
+
/**
|
|
51
|
+
* - Number of packages found by flatlock
|
|
52
|
+
*/
|
|
53
|
+
flatlockCount: number;
|
|
54
|
+
/**
|
|
55
|
+
* - Number of packages found by comparison parser
|
|
56
|
+
*/
|
|
57
|
+
comparisonCount?: number;
|
|
58
|
+
/**
|
|
59
|
+
* - Number of workspace packages skipped
|
|
60
|
+
*/
|
|
61
|
+
workspaceCount?: number;
|
|
62
|
+
/**
|
|
63
|
+
* - Packages only found by flatlock
|
|
64
|
+
*/
|
|
65
|
+
onlyInFlatlock?: string[];
|
|
66
|
+
/**
|
|
67
|
+
* - Packages only found by comparison parser
|
|
68
|
+
*/
|
|
69
|
+
onlyInComparison?: string[];
|
|
70
|
+
};
|
|
71
|
+
export type PackagesResult = {
|
|
72
|
+
/**
|
|
73
|
+
* - Set of package@version strings
|
|
74
|
+
*/
|
|
75
|
+
packages: Set<string>;
|
|
76
|
+
/**
|
|
77
|
+
* - Number of workspace packages skipped
|
|
78
|
+
*/
|
|
79
|
+
workspaceCount: number;
|
|
80
|
+
/**
|
|
81
|
+
* - Comparison source used
|
|
82
|
+
*/
|
|
83
|
+
source: string;
|
|
84
|
+
};
|
|
85
|
+
//# sourceMappingURL=compare.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compare.d.ts","sourceRoot":"","sources":["../src/compare.js"],"names":[],"mappings":"AAyhBA;;;;;GAKG;AACH,kCAJW,MAAM,YACN,cAAc,GACZ,OAAO,CAAC,gBAAgB,CAAC,CA4CrC;AAED;;;;;GAKG;AACH,sCAJW,MAAM,EAAE,YACR,cAAc,GACZ,cAAc,CAAC,gBAAgB,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAMnE;AAED;;;GAGG;AACH,uCAFa,OAAO,CAAC;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,OAAO,CAAC;IAAC,cAAc,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC,CAgB1G;;;;;aAjhBa,MAAM;;;;gBACN,MAAM,EAAE;;;;;;UAKR,MAAM;;;;aACN,MAAM;;;;kBACN,OAAO,GAAG,IAAI;;;;mBACd,MAAM;;;;sBACN,MAAM;;;;qBACN,MAAM;;;;qBACN,MAAM,EAAE;;;;uBACR,MAAM,EAAE;;;;;;cAKR,GAAG,CAAC,MAAM,CAAC;;;;oBACX,MAAM;;;;YACN,MAAM"}
|