flatlock 1.4.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 +135 -20
- package/package.json +1 -1
package/bin/flatcover.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
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';
|
|
17
18
|
import { createReadStream } from 'node:fs';
|
|
18
19
|
import { createInterface } from 'node:readline';
|
|
19
20
|
import { dirname, join } from 'node:path';
|
|
@@ -37,6 +38,8 @@ const { values, positionals } = parseArgs({
|
|
|
37
38
|
concurrency: { type: 'string', default: '20' },
|
|
38
39
|
progress: { type: 'boolean', default: false },
|
|
39
40
|
summary: { type: 'boolean', default: false },
|
|
41
|
+
before: { type: 'string', short: 'b' },
|
|
42
|
+
cache: { type: 'string', short: 'c' },
|
|
40
43
|
help: { type: 'boolean', short: 'h' }
|
|
41
44
|
},
|
|
42
45
|
allowPositionals: true
|
|
@@ -67,7 +70,7 @@ Options:
|
|
|
67
70
|
-s, --specs Include version (name@version or {name,version})
|
|
68
71
|
--json Output as JSON array
|
|
69
72
|
--ndjson Output as newline-delimited JSON (streaming)
|
|
70
|
-
--full Include all metadata (integrity, resolved)
|
|
73
|
+
--full Include all metadata (integrity, resolved, time)
|
|
71
74
|
--dev Include dev dependencies (default: false)
|
|
72
75
|
--peer Include peer dependencies (default: true)
|
|
73
76
|
-h, --help Show this help
|
|
@@ -80,13 +83,17 @@ Coverage options:
|
|
|
80
83
|
--concurrency <n> Concurrent requests (default: 20)
|
|
81
84
|
--progress Show progress on stderr
|
|
82
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
|
|
83
88
|
|
|
84
89
|
Output formats (with --cover):
|
|
85
|
-
(default) CSV
|
|
86
|
-
--
|
|
87
|
-
--
|
|
88
|
-
|
|
89
|
-
|
|
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)
|
|
90
97
|
|
|
91
98
|
Examples:
|
|
92
99
|
# From lockfile
|
|
@@ -97,6 +104,10 @@ Examples:
|
|
|
97
104
|
flatcover --list packages.json --cover --summary
|
|
98
105
|
echo '[{"name":"lodash","version":"4.17.21"}]' > pkgs.json && flatcover -l pkgs.json --cover
|
|
99
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
|
+
|
|
100
111
|
# From stdin (NDJSON) - use '-' to read from stdin
|
|
101
112
|
echo '{"name":"lodash","version":"4.17.21"}' | flatcover - --cover
|
|
102
113
|
cat packages.ndjson | flatcover - --cover --json
|
|
@@ -217,6 +228,68 @@ function encodePackageName(name) {
|
|
|
217
228
|
return name.replace('/', '%2f');
|
|
218
229
|
}
|
|
219
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
|
+
|
|
220
293
|
/**
|
|
221
294
|
* Create undici client with retry support
|
|
222
295
|
* @param {string} registryUrl
|
|
@@ -267,10 +340,10 @@ function createClient(registryUrl, { auth, token }) {
|
|
|
267
340
|
/**
|
|
268
341
|
* Check coverage for all dependencies
|
|
269
342
|
* @param {Array<{ name: string, version: string, integrity?: string, resolved?: string }>} deps
|
|
270
|
-
* @param {{ registry: string, auth?: string, token?: string, progress: boolean }} options
|
|
343
|
+
* @param {{ registry: string, auth?: string, token?: string, progress: boolean, before?: string, cache?: string }} options
|
|
271
344
|
* @returns {AsyncGenerator<{ name: string, version: string, present: boolean, integrity?: string, resolved?: string, error?: string }>}
|
|
272
345
|
*/
|
|
273
|
-
async function* checkCoverage(deps, { registry, auth, token, progress }) {
|
|
346
|
+
async function* checkCoverage(deps, { registry, auth, token, progress, before, cache }) {
|
|
274
347
|
const { client, headers, baseUrl } = createClient(registry, { auth, token });
|
|
275
348
|
|
|
276
349
|
// Group by package name to avoid duplicate requests
|
|
@@ -286,7 +359,7 @@ async function* checkCoverage(deps, { registry, auth, token, progress }) {
|
|
|
286
359
|
|
|
287
360
|
const packages = [...byPackage.entries()];
|
|
288
361
|
let completed = 0;
|
|
289
|
-
const total =
|
|
362
|
+
const total = deps.length;
|
|
290
363
|
|
|
291
364
|
// Process in batches for bounded concurrency
|
|
292
365
|
for (let i = 0; i < packages.length; i += concurrency) {
|
|
@@ -299,10 +372,22 @@ async function* checkCoverage(deps, { registry, auth, token, progress }) {
|
|
|
299
372
|
const path = `${basePath}/${encodedName}`;
|
|
300
373
|
|
|
301
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
|
+
|
|
302
387
|
const response = await client.request({
|
|
303
388
|
method: 'GET',
|
|
304
389
|
path,
|
|
305
|
-
headers
|
|
390
|
+
headers: reqHeaders
|
|
306
391
|
});
|
|
307
392
|
|
|
308
393
|
const chunks = [];
|
|
@@ -316,19 +401,43 @@ async function* checkCoverage(deps, { registry, auth, token, progress }) {
|
|
|
316
401
|
}
|
|
317
402
|
|
|
318
403
|
let packumentVersions = null;
|
|
319
|
-
|
|
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) {
|
|
320
414
|
const body = Buffer.concat(chunks).toString('utf8');
|
|
321
415
|
const packument = JSON.parse(body);
|
|
322
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
|
+
}
|
|
323
426
|
}
|
|
324
427
|
|
|
325
428
|
// Check each version, preserving integrity/resolved from original dep
|
|
326
429
|
const versionResults = [];
|
|
327
430
|
for (const [version, dep] of versionMap) {
|
|
328
|
-
|
|
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
|
+
}
|
|
329
437
|
const result = { name, version, present };
|
|
330
438
|
if (dep.integrity) result.integrity = dep.integrity;
|
|
331
439
|
if (dep.resolved) result.resolved = dep.resolved;
|
|
440
|
+
if (packumentTime && packumentTime[version]) result.time = packumentTime[version];
|
|
332
441
|
versionResults.push(result);
|
|
333
442
|
}
|
|
334
443
|
return versionResults;
|
|
@@ -353,10 +462,10 @@ async function* checkCoverage(deps, { registry, auth, token, progress }) {
|
|
|
353
462
|
for (const packageResults of results) {
|
|
354
463
|
for (const result of packageResults) {
|
|
355
464
|
yield result;
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
465
|
+
completed++;
|
|
466
|
+
if (progress) {
|
|
467
|
+
process.stderr.write(`\r Checking: ${completed}/${total} package specs`);
|
|
468
|
+
}
|
|
360
469
|
}
|
|
361
470
|
}
|
|
362
471
|
}
|
|
@@ -374,7 +483,7 @@ async function* checkCoverage(deps, { registry, auth, token, progress }) {
|
|
|
374
483
|
*/
|
|
375
484
|
function formatDep(dep, { specs, full }) {
|
|
376
485
|
if (full) {
|
|
377
|
-
const obj = { name: dep.name, version: dep.version };
|
|
486
|
+
const obj = { name: dep.name, version: dep.version, spec: `${dep.name}@${dep.version}` };
|
|
378
487
|
if (dep.integrity) obj.integrity = dep.integrity;
|
|
379
488
|
if (dep.resolved) obj.resolved = dep.resolved;
|
|
380
489
|
return obj;
|
|
@@ -432,8 +541,10 @@ async function outputCoverage(results, { json, ndjson, summary, full }) {
|
|
|
432
541
|
if (ndjson) {
|
|
433
542
|
// Stream immediately
|
|
434
543
|
const obj = { name: result.name, version: result.version, present: result.present };
|
|
544
|
+
if (full) obj.spec = `${result.name}@${result.version}`;
|
|
435
545
|
if (full && result.integrity) obj.integrity = result.integrity;
|
|
436
546
|
if (full && result.resolved) obj.resolved = result.resolved;
|
|
547
|
+
if (full && result.time) obj.time = result.time;
|
|
437
548
|
console.log(JSON.stringify(obj));
|
|
438
549
|
} else {
|
|
439
550
|
all.push(result);
|
|
@@ -447,17 +558,19 @@ async function outputCoverage(results, { json, ndjson, summary, full }) {
|
|
|
447
558
|
if (json) {
|
|
448
559
|
const data = all.map(r => {
|
|
449
560
|
const obj = { name: r.name, version: r.version, present: r.present };
|
|
561
|
+
if (full) obj.spec = `${r.name}@${r.version}`;
|
|
450
562
|
if (full && r.integrity) obj.integrity = r.integrity;
|
|
451
563
|
if (full && r.resolved) obj.resolved = r.resolved;
|
|
564
|
+
if (full && r.time) obj.time = r.time;
|
|
452
565
|
return obj;
|
|
453
566
|
});
|
|
454
567
|
console.log(JSON.stringify(data, null, 2));
|
|
455
568
|
} else {
|
|
456
569
|
// CSV output
|
|
457
570
|
if (full) {
|
|
458
|
-
console.log('package,version,present,integrity,resolved');
|
|
571
|
+
console.log('package,version,spec,present,integrity,resolved,time');
|
|
459
572
|
for (const r of all) {
|
|
460
|
-
console.log(`${r.name},${r.version},${r.present},${r.integrity || ''},${r.resolved || ''}`);
|
|
573
|
+
console.log(`${r.name},${r.version},${r.name}@${r.version},${r.present},${r.integrity || ''},${r.resolved || ''},${r.time || ''}`);
|
|
461
574
|
}
|
|
462
575
|
} else {
|
|
463
576
|
console.log('package,version,present');
|
|
@@ -523,7 +636,9 @@ try {
|
|
|
523
636
|
registry: values.registry,
|
|
524
637
|
auth: values.auth,
|
|
525
638
|
token: values.token,
|
|
526
|
-
progress: values.progress
|
|
639
|
+
progress: values.progress,
|
|
640
|
+
before: values.before,
|
|
641
|
+
cache: values.cache
|
|
527
642
|
});
|
|
528
643
|
|
|
529
644
|
await outputCoverage(results, {
|