flatlock 1.2.0 → 1.4.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 +42 -1
- package/bin/flatcover.js +547 -0
- package/bin/flatlock-cmp.js +2 -2
- package/bin/flatlock.js +158 -0
- package/dist/compare.d.ts.map +1 -1
- package/dist/parsers/index.d.ts +3 -3
- package/dist/parsers/npm.d.ts +45 -0
- package/dist/parsers/npm.d.ts.map +1 -1
- package/dist/parsers/pnpm/index.d.ts +34 -0
- package/dist/parsers/pnpm/index.d.ts.map +1 -1
- package/dist/parsers/yarn-berry.d.ts +43 -0
- package/dist/parsers/yarn-berry.d.ts.map +1 -1
- package/dist/set.d.ts +55 -6
- package/dist/set.d.ts.map +1 -1
- package/package.json +21 -5
- package/src/compare.js +5 -7
- package/src/parsers/index.js +14 -2
- package/src/parsers/npm.js +82 -0
- package/src/parsers/pnpm/index.js +70 -0
- package/src/parsers/yarn-berry.js +88 -0
- package/src/set.js +730 -41
package/README.md
CHANGED
|
@@ -38,6 +38,29 @@ for await (const pkg of flatlock.fromPath('./package-lock.json')) {
|
|
|
38
38
|
- **yarn classic**: yarn.lock v1
|
|
39
39
|
- **yarn berry**: yarn.lock v2+
|
|
40
40
|
|
|
41
|
+
## CLI Tools
|
|
42
|
+
|
|
43
|
+
Three command-line tools are included for common workflows:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Extract dependencies from any lockfile
|
|
47
|
+
npx flatlock package-lock.json --specs --json
|
|
48
|
+
|
|
49
|
+
# Verify parser accuracy against official tools
|
|
50
|
+
npx flatlock-cmp --dir ./fixtures --glob "**/*lock*"
|
|
51
|
+
|
|
52
|
+
# Check registry availability of all dependencies
|
|
53
|
+
npx flatcover package-lock.json --summary
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
| Command | Purpose |
|
|
57
|
+
|---------|---------|
|
|
58
|
+
| `flatlock` | Extract dependencies as plain text, JSON, or NDJSON |
|
|
59
|
+
| `flatlock-cmp` | Compare output against @npmcli/arborist, @yarnpkg/lockfile, @pnpm/lockfile.fs |
|
|
60
|
+
| `flatcover` | Verify packages exist on registry (useful for private registry migrations) |
|
|
61
|
+
|
|
62
|
+
Run any command with `--help` for full options.
|
|
63
|
+
|
|
41
64
|
## API
|
|
42
65
|
|
|
43
66
|
```javascript
|
|
@@ -130,8 +153,9 @@ const lockfile = await FlatlockSet.fromPath('./package-lock.json');
|
|
|
130
153
|
const pkg = JSON.parse(await readFile('./packages/api/package.json', 'utf8'));
|
|
131
154
|
|
|
132
155
|
// Get only dependencies reachable from this workspace
|
|
133
|
-
const subset = lockfile.dependenciesOf(pkg, {
|
|
156
|
+
const subset = await lockfile.dependenciesOf(pkg, {
|
|
134
157
|
workspacePath: 'packages/api', // for correct resolution in monorepos
|
|
158
|
+
repoDir: '.', // reads workspace package.json files for accurate traversal
|
|
135
159
|
dev: false, // exclude devDependencies
|
|
136
160
|
optional: true, // include optionalDependencies
|
|
137
161
|
peer: false // exclude peerDependencies
|
|
@@ -142,6 +166,23 @@ console.log(`${pkg.name} has ${subset.size} production dependencies`);
|
|
|
142
166
|
|
|
143
167
|
**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
168
|
|
|
169
|
+
## Compare API
|
|
170
|
+
|
|
171
|
+
Verify flatlock output against official package manager parsers:
|
|
172
|
+
|
|
173
|
+
```javascript
|
|
174
|
+
// Both import styles work - use whichever you prefer
|
|
175
|
+
import { compare } from 'flatlock';
|
|
176
|
+
import { compare } from 'flatlock/compare';
|
|
177
|
+
|
|
178
|
+
const result = await compare('./package-lock.json');
|
|
179
|
+
console.log(result.equinumerous); // true if counts match
|
|
180
|
+
console.log(result.flatlockCount); // packages found by flatlock
|
|
181
|
+
console.log(result.comparisonCount); // packages found by official parser
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The dedicated `flatlock/compare` entry point exists for tools that want explicit imports, but the main export works identically.
|
|
185
|
+
|
|
145
186
|
## License
|
|
146
187
|
|
|
147
188
|
Apache-2.0
|
package/bin/flatcover.js
ADDED
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* flatcover - Check lockfile package coverage against a registry
|
|
4
|
+
*
|
|
5
|
+
* Checks if packages from a lockfile exist in a npm registry.
|
|
6
|
+
* Outputs CSV by default: package,version,present
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* flatcover <lockfile> --cover # check against npmjs.org
|
|
10
|
+
* flatcover <lockfile> --cover --registry <url> # custom registry
|
|
11
|
+
* flatcover <lockfile> --cover --registry <url> --auth u:p # with basic auth
|
|
12
|
+
* flatcover <lockfile> --cover --ndjson # streaming output
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { parseArgs } from 'node:util';
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
import { createReadStream } from 'node:fs';
|
|
18
|
+
import { createInterface } from 'node:readline';
|
|
19
|
+
import { dirname, join } from 'node:path';
|
|
20
|
+
import { Pool, RetryAgent } from 'undici';
|
|
21
|
+
import { FlatlockSet } from '../src/set.js';
|
|
22
|
+
|
|
23
|
+
const { values, positionals } = parseArgs({
|
|
24
|
+
options: {
|
|
25
|
+
workspace: { type: 'string', short: 'w' },
|
|
26
|
+
list: { type: 'string', short: 'l' },
|
|
27
|
+
dev: { type: 'boolean', default: false },
|
|
28
|
+
peer: { type: 'boolean', default: true },
|
|
29
|
+
specs: { type: 'boolean', short: 's', default: false },
|
|
30
|
+
json: { type: 'boolean', default: false },
|
|
31
|
+
ndjson: { type: 'boolean', default: false },
|
|
32
|
+
full: { type: 'boolean', default: false },
|
|
33
|
+
cover: { type: 'boolean', default: false },
|
|
34
|
+
registry: { type: 'string', default: 'https://registry.npmjs.org' },
|
|
35
|
+
auth: { type: 'string' },
|
|
36
|
+
token: { type: 'string' },
|
|
37
|
+
concurrency: { type: 'string', default: '20' },
|
|
38
|
+
progress: { type: 'boolean', default: false },
|
|
39
|
+
summary: { type: 'boolean', default: false },
|
|
40
|
+
help: { type: 'boolean', short: 'h' }
|
|
41
|
+
},
|
|
42
|
+
allowPositionals: true
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Check if stdin input is requested via '-' positional argument (Unix convention)
|
|
46
|
+
const useStdin = positionals[0] === '-';
|
|
47
|
+
|
|
48
|
+
// Determine if we have a valid input source
|
|
49
|
+
const hasInputSource = positionals.length > 0 || values.list;
|
|
50
|
+
|
|
51
|
+
if (values.help || !hasInputSource) {
|
|
52
|
+
console.log(`flatcover - Check lockfile package coverage against a registry
|
|
53
|
+
|
|
54
|
+
Usage:
|
|
55
|
+
flatcover <lockfile> --cover
|
|
56
|
+
flatcover --list packages.json --cover
|
|
57
|
+
cat packages.ndjson | flatcover - --cover
|
|
58
|
+
flatcover <lockfile> --cover --registry <url> --auth user:pass
|
|
59
|
+
|
|
60
|
+
Input sources (mutually exclusive):
|
|
61
|
+
<lockfile> Parse lockfile (package-lock.json, pnpm-lock.yaml, yarn.lock)
|
|
62
|
+
-l, --list <file> Read JSON array of {name, version} objects from file
|
|
63
|
+
- Read NDJSON {name, version} objects from stdin (one per line)
|
|
64
|
+
|
|
65
|
+
Options:
|
|
66
|
+
-w, --workspace <path> Workspace path within monorepo (lockfile mode only)
|
|
67
|
+
-s, --specs Include version (name@version or {name,version})
|
|
68
|
+
--json Output as JSON array
|
|
69
|
+
--ndjson Output as newline-delimited JSON (streaming)
|
|
70
|
+
--full Include all metadata (integrity, resolved)
|
|
71
|
+
--dev Include dev dependencies (default: false)
|
|
72
|
+
--peer Include peer dependencies (default: true)
|
|
73
|
+
-h, --help Show this help
|
|
74
|
+
|
|
75
|
+
Coverage options:
|
|
76
|
+
--cover Enable registry coverage checking
|
|
77
|
+
--registry <url> Registry URL (default: https://registry.npmjs.org)
|
|
78
|
+
--auth <user:pass> Basic authentication credentials
|
|
79
|
+
--token <token> Bearer token for authentication
|
|
80
|
+
--concurrency <n> Concurrent requests (default: 20)
|
|
81
|
+
--progress Show progress on stderr
|
|
82
|
+
--summary Show coverage summary on stderr
|
|
83
|
+
|
|
84
|
+
Output formats (with --cover):
|
|
85
|
+
(default) CSV: package,version,present
|
|
86
|
+
--full CSV: package,version,present,integrity,resolved
|
|
87
|
+
--json [{"name":"...","version":"...","present":true}, ...]
|
|
88
|
+
--full --json Adds "integrity" and "resolved" fields to JSON
|
|
89
|
+
--ndjson {"name":"...","version":"...","present":true} per line
|
|
90
|
+
|
|
91
|
+
Examples:
|
|
92
|
+
# From lockfile
|
|
93
|
+
flatcover package-lock.json --cover
|
|
94
|
+
flatcover package-lock.json --cover --full --json
|
|
95
|
+
|
|
96
|
+
# From JSON list file
|
|
97
|
+
flatcover --list packages.json --cover --summary
|
|
98
|
+
echo '[{"name":"lodash","version":"4.17.21"}]' > pkgs.json && flatcover -l pkgs.json --cover
|
|
99
|
+
|
|
100
|
+
# From stdin (NDJSON) - use '-' to read from stdin
|
|
101
|
+
echo '{"name":"lodash","version":"4.17.21"}' | flatcover - --cover
|
|
102
|
+
cat packages.ndjson | flatcover - --cover --json
|
|
103
|
+
|
|
104
|
+
# With custom registry
|
|
105
|
+
flatcover package-lock.json --cover --registry https://npm.pkg.github.com --token ghp_xxx
|
|
106
|
+
flatcover pnpm-lock.yaml --cover --auth admin:secret --ndjson`);
|
|
107
|
+
process.exit(values.help ? 0 : 1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (values.json && values.ndjson) {
|
|
111
|
+
console.error('Error: --json and --ndjson are mutually exclusive');
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (values.auth && values.token) {
|
|
116
|
+
console.error('Error: --auth and --token are mutually exclusive');
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Validate mutually exclusive input sources
|
|
121
|
+
// Note: useStdin means positionals[0] === '-', so it's already counted in positionals.length
|
|
122
|
+
if (positionals.length > 0 && values.list) {
|
|
123
|
+
console.error('Error: Cannot use both lockfile/stdin and --list');
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// --workspace only works with lockfile input (not stdin or --list)
|
|
128
|
+
if (values.workspace && (useStdin || values.list || !positionals.length)) {
|
|
129
|
+
console.error('Error: --workspace can only be used with lockfile input');
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// --full implies --specs
|
|
134
|
+
if (values.full) {
|
|
135
|
+
values.specs = true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// --cover implies --specs (need versions to check)
|
|
139
|
+
if (values.cover) {
|
|
140
|
+
values.specs = true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const lockfilePath = positionals[0];
|
|
144
|
+
const concurrency = Math.max(1, Math.min(50, Number.parseInt(values.concurrency, 10) || 20));
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Read packages from a JSON list file
|
|
148
|
+
* @param {string} filePath - Path to JSON file containing [{name, version}, ...]
|
|
149
|
+
* @returns {Array<{ name: string, version: string }>}
|
|
150
|
+
*/
|
|
151
|
+
function readJsonList(filePath) {
|
|
152
|
+
const content = readFileSync(filePath, 'utf8');
|
|
153
|
+
const data = JSON.parse(content);
|
|
154
|
+
|
|
155
|
+
if (!Array.isArray(data)) {
|
|
156
|
+
throw new Error('--list file must contain a JSON array');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const packages = [];
|
|
160
|
+
for (const item of data) {
|
|
161
|
+
if (!item.name || !item.version) {
|
|
162
|
+
throw new Error('Each item in --list must have "name" and "version" fields');
|
|
163
|
+
}
|
|
164
|
+
packages.push({
|
|
165
|
+
name: item.name,
|
|
166
|
+
version: item.version,
|
|
167
|
+
integrity: item.integrity,
|
|
168
|
+
resolved: item.resolved
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return packages;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Read packages from stdin as NDJSON
|
|
177
|
+
* @returns {Promise<Array<{ name: string, version: string }>>}
|
|
178
|
+
*/
|
|
179
|
+
async function readStdinNdjson() {
|
|
180
|
+
const packages = [];
|
|
181
|
+
|
|
182
|
+
const rl = createInterface({
|
|
183
|
+
input: process.stdin,
|
|
184
|
+
crlfDelay: Infinity
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
for await (const line of rl) {
|
|
188
|
+
const trimmed = line.trim();
|
|
189
|
+
if (!trimmed) continue;
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const item = JSON.parse(trimmed);
|
|
193
|
+
if (!item.name || !item.version) {
|
|
194
|
+
throw new Error('Each line must have "name" and "version" fields');
|
|
195
|
+
}
|
|
196
|
+
packages.push({
|
|
197
|
+
name: item.name,
|
|
198
|
+
version: item.version,
|
|
199
|
+
integrity: item.integrity,
|
|
200
|
+
resolved: item.resolved
|
|
201
|
+
});
|
|
202
|
+
} catch (err) {
|
|
203
|
+
throw new Error(`Invalid JSON on stdin: ${err.message}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return packages;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Encode package name for URL (handle scoped packages)
|
|
212
|
+
* @param {string} name - Package name like @babel/core
|
|
213
|
+
* @returns {string} URL-safe name like @babel%2fcore
|
|
214
|
+
*/
|
|
215
|
+
function encodePackageName(name) {
|
|
216
|
+
// Scoped packages: @scope/name -> @scope%2fname
|
|
217
|
+
return name.replace('/', '%2f');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Create undici client with retry support
|
|
222
|
+
* @param {string} registryUrl
|
|
223
|
+
* @param {{ auth?: string, token?: string }} options
|
|
224
|
+
* @returns {{ client: RetryAgent, headers: Record<string, string>, baseUrl: URL }}
|
|
225
|
+
*/
|
|
226
|
+
function createClient(registryUrl, { auth, token }) {
|
|
227
|
+
const baseUrl = new URL(registryUrl);
|
|
228
|
+
|
|
229
|
+
const pool = new Pool(baseUrl.origin, {
|
|
230
|
+
connections: Math.min(concurrency, 50),
|
|
231
|
+
pipelining: 1, // Conservative - most proxies don't support HTTP pipelining
|
|
232
|
+
keepAliveTimeout: 30000,
|
|
233
|
+
keepAliveMaxTimeout: 60000
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const client = new RetryAgent(pool, {
|
|
237
|
+
maxRetries: 3,
|
|
238
|
+
minTimeout: 1000,
|
|
239
|
+
maxTimeout: 10000,
|
|
240
|
+
timeoutFactor: 2,
|
|
241
|
+
retryAfter: true, // Respect Retry-After header
|
|
242
|
+
statusCodes: [429, 500, 502, 503, 504],
|
|
243
|
+
errorCodes: [
|
|
244
|
+
'ECONNRESET',
|
|
245
|
+
'ECONNREFUSED',
|
|
246
|
+
'ENOTFOUND',
|
|
247
|
+
'ENETUNREACH',
|
|
248
|
+
'ETIMEDOUT',
|
|
249
|
+
'UND_ERR_SOCKET'
|
|
250
|
+
]
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const headers = {
|
|
254
|
+
Accept: 'application/json',
|
|
255
|
+
'User-Agent': 'flatcover/1.0.0'
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
if (auth) {
|
|
259
|
+
headers.Authorization = `Basic ${Buffer.from(auth).toString('base64')}`;
|
|
260
|
+
} else if (token) {
|
|
261
|
+
headers.Authorization = `Bearer ${token}`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return { client, headers, baseUrl };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Check coverage for all dependencies
|
|
269
|
+
* @param {Array<{ name: string, version: string, integrity?: string, resolved?: string }>} deps
|
|
270
|
+
* @param {{ registry: string, auth?: string, token?: string, progress: boolean }} options
|
|
271
|
+
* @returns {AsyncGenerator<{ name: string, version: string, present: boolean, integrity?: string, resolved?: string, error?: string }>}
|
|
272
|
+
*/
|
|
273
|
+
async function* checkCoverage(deps, { registry, auth, token, progress }) {
|
|
274
|
+
const { client, headers, baseUrl } = createClient(registry, { auth, token });
|
|
275
|
+
|
|
276
|
+
// Group by package name to avoid duplicate requests
|
|
277
|
+
// Store full dep info (including integrity/resolved) keyed by version
|
|
278
|
+
/** @type {Map<string, Map<string, { name: string, version: string, integrity?: string, resolved?: string }>>} */
|
|
279
|
+
const byPackage = new Map();
|
|
280
|
+
for (const dep of deps) {
|
|
281
|
+
if (!byPackage.has(dep.name)) {
|
|
282
|
+
byPackage.set(dep.name, new Map());
|
|
283
|
+
}
|
|
284
|
+
byPackage.get(dep.name).set(dep.version, dep);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const packages = [...byPackage.entries()];
|
|
288
|
+
let completed = 0;
|
|
289
|
+
const total = packages.length;
|
|
290
|
+
|
|
291
|
+
// Process in batches for bounded concurrency
|
|
292
|
+
for (let i = 0; i < packages.length; i += concurrency) {
|
|
293
|
+
const batch = packages.slice(i, i + concurrency);
|
|
294
|
+
|
|
295
|
+
const results = await Promise.all(
|
|
296
|
+
batch.map(async ([name, versionMap]) => {
|
|
297
|
+
const encodedName = encodePackageName(name);
|
|
298
|
+
const basePath = baseUrl.pathname.replace(/\/$/, '');
|
|
299
|
+
const path = `${basePath}/${encodedName}`;
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
const response = await client.request({
|
|
303
|
+
method: 'GET',
|
|
304
|
+
path,
|
|
305
|
+
headers
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const chunks = [];
|
|
309
|
+
for await (const chunk of response.body) {
|
|
310
|
+
chunks.push(chunk);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (response.statusCode === 401 || response.statusCode === 403) {
|
|
314
|
+
console.error(`Error: Authentication failed for ${name} (${response.statusCode})`);
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
let packumentVersions = null;
|
|
319
|
+
if (response.statusCode === 200) {
|
|
320
|
+
const body = Buffer.concat(chunks).toString('utf8');
|
|
321
|
+
const packument = JSON.parse(body);
|
|
322
|
+
packumentVersions = packument.versions || {};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Check each version, preserving integrity/resolved from original dep
|
|
326
|
+
const versionResults = [];
|
|
327
|
+
for (const [version, dep] of versionMap) {
|
|
328
|
+
const present = packumentVersions ? !!packumentVersions[version] : false;
|
|
329
|
+
const result = { name, version, present };
|
|
330
|
+
if (dep.integrity) result.integrity = dep.integrity;
|
|
331
|
+
if (dep.resolved) result.resolved = dep.resolved;
|
|
332
|
+
versionResults.push(result);
|
|
333
|
+
}
|
|
334
|
+
return versionResults;
|
|
335
|
+
} catch (err) {
|
|
336
|
+
// Return error for all versions of this package
|
|
337
|
+
return [...versionMap.values()].map(dep => {
|
|
338
|
+
const result = {
|
|
339
|
+
name: dep.name,
|
|
340
|
+
version: dep.version,
|
|
341
|
+
present: false,
|
|
342
|
+
error: err.message
|
|
343
|
+
};
|
|
344
|
+
if (dep.integrity) result.integrity = dep.integrity;
|
|
345
|
+
if (dep.resolved) result.resolved = dep.resolved;
|
|
346
|
+
return result;
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
})
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
// Flatten and yield results
|
|
353
|
+
for (const packageResults of results) {
|
|
354
|
+
for (const result of packageResults) {
|
|
355
|
+
yield result;
|
|
356
|
+
}
|
|
357
|
+
completed++;
|
|
358
|
+
if (progress) {
|
|
359
|
+
process.stderr.write(`\r Checking: ${completed}/${total} packages`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (progress) {
|
|
365
|
+
process.stderr.write('\n');
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Format a single dependency based on output options
|
|
371
|
+
* @param {{ name: string, version: string, integrity?: string, resolved?: string }} dep
|
|
372
|
+
* @param {{ specs: boolean, full: boolean }} options
|
|
373
|
+
* @returns {string | object}
|
|
374
|
+
*/
|
|
375
|
+
function formatDep(dep, { specs, full }) {
|
|
376
|
+
if (full) {
|
|
377
|
+
const obj = { name: dep.name, version: dep.version };
|
|
378
|
+
if (dep.integrity) obj.integrity = dep.integrity;
|
|
379
|
+
if (dep.resolved) obj.resolved = dep.resolved;
|
|
380
|
+
return obj;
|
|
381
|
+
}
|
|
382
|
+
if (specs) {
|
|
383
|
+
return { name: dep.name, version: dep.version };
|
|
384
|
+
}
|
|
385
|
+
return dep.name;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Output dependencies in the requested format (non-cover mode)
|
|
390
|
+
* @param {Iterable<{ name: string, version: string, integrity?: string, resolved?: string }>} deps
|
|
391
|
+
* @param {{ specs: boolean, json: boolean, ndjson: boolean, full: boolean }} options
|
|
392
|
+
*/
|
|
393
|
+
function outputDeps(deps, { specs, json, ndjson, full }) {
|
|
394
|
+
const sorted = [...deps].sort((a, b) => a.name.localeCompare(b.name));
|
|
395
|
+
|
|
396
|
+
if (json) {
|
|
397
|
+
const data = sorted.map(d => formatDep(d, { specs, full }));
|
|
398
|
+
console.log(JSON.stringify(data, null, 2));
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (ndjson) {
|
|
403
|
+
for (const d of sorted) {
|
|
404
|
+
console.log(JSON.stringify(formatDep(d, { specs, full })));
|
|
405
|
+
}
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Plain text
|
|
410
|
+
for (const d of sorted) {
|
|
411
|
+
console.log(specs ? `${d.name}@${d.version}` : d.name);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Output coverage results
|
|
417
|
+
* @param {AsyncGenerator<{ name: string, version: string, present: boolean, integrity?: string, resolved?: string, error?: string }>} results
|
|
418
|
+
* @param {{ json: boolean, ndjson: boolean, summary: boolean, full: boolean }} options
|
|
419
|
+
*/
|
|
420
|
+
async function outputCoverage(results, { json, ndjson, summary, full }) {
|
|
421
|
+
const all = [];
|
|
422
|
+
let presentCount = 0;
|
|
423
|
+
let missingCount = 0;
|
|
424
|
+
|
|
425
|
+
for await (const result of results) {
|
|
426
|
+
if (result.present) {
|
|
427
|
+
presentCount++;
|
|
428
|
+
} else {
|
|
429
|
+
missingCount++;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (ndjson) {
|
|
433
|
+
// Stream immediately
|
|
434
|
+
const obj = { name: result.name, version: result.version, present: result.present };
|
|
435
|
+
if (full && result.integrity) obj.integrity = result.integrity;
|
|
436
|
+
if (full && result.resolved) obj.resolved = result.resolved;
|
|
437
|
+
console.log(JSON.stringify(obj));
|
|
438
|
+
} else {
|
|
439
|
+
all.push(result);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (!ndjson) {
|
|
444
|
+
// Sort by name, then version
|
|
445
|
+
all.sort((a, b) => a.name.localeCompare(b.name) || a.version.localeCompare(b.version));
|
|
446
|
+
|
|
447
|
+
if (json) {
|
|
448
|
+
const data = all.map(r => {
|
|
449
|
+
const obj = { name: r.name, version: r.version, present: r.present };
|
|
450
|
+
if (full && r.integrity) obj.integrity = r.integrity;
|
|
451
|
+
if (full && r.resolved) obj.resolved = r.resolved;
|
|
452
|
+
return obj;
|
|
453
|
+
});
|
|
454
|
+
console.log(JSON.stringify(data, null, 2));
|
|
455
|
+
} else {
|
|
456
|
+
// CSV output
|
|
457
|
+
if (full) {
|
|
458
|
+
console.log('package,version,present,integrity,resolved');
|
|
459
|
+
for (const r of all) {
|
|
460
|
+
console.log(`${r.name},${r.version},${r.present},${r.integrity || ''},${r.resolved || ''}`);
|
|
461
|
+
}
|
|
462
|
+
} else {
|
|
463
|
+
console.log('package,version,present');
|
|
464
|
+
for (const r of all) {
|
|
465
|
+
console.log(`${r.name},${r.version},${r.present}`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (summary) {
|
|
472
|
+
const total = presentCount + missingCount;
|
|
473
|
+
const percentage = total > 0 ? ((presentCount / total) * 100).toFixed(1) : 0;
|
|
474
|
+
process.stderr.write(`\nCoverage: ${presentCount}/${total} (${percentage}%) packages present\n`);
|
|
475
|
+
if (missingCount > 0) {
|
|
476
|
+
process.stderr.write(`Missing: ${missingCount} packages\n`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
try {
|
|
482
|
+
let deps;
|
|
483
|
+
|
|
484
|
+
// Determine input source and load dependencies
|
|
485
|
+
if (useStdin) {
|
|
486
|
+
// Read from stdin (NDJSON)
|
|
487
|
+
deps = await readStdinNdjson();
|
|
488
|
+
if (deps.length === 0) {
|
|
489
|
+
console.error('Error: No packages read from stdin');
|
|
490
|
+
process.exit(1);
|
|
491
|
+
}
|
|
492
|
+
} else if (values.list) {
|
|
493
|
+
// Read from JSON list file
|
|
494
|
+
deps = readJsonList(values.list);
|
|
495
|
+
if (deps.length === 0) {
|
|
496
|
+
console.error('Error: No packages found in --list file');
|
|
497
|
+
process.exit(1);
|
|
498
|
+
}
|
|
499
|
+
} else {
|
|
500
|
+
// Read from lockfile (existing behavior)
|
|
501
|
+
const lockfile = await FlatlockSet.fromPath(lockfilePath);
|
|
502
|
+
|
|
503
|
+
if (values.workspace) {
|
|
504
|
+
const repoDir = dirname(lockfilePath);
|
|
505
|
+
const workspacePkgPath = join(repoDir, values.workspace, 'package.json');
|
|
506
|
+
const workspacePkg = JSON.parse(readFileSync(workspacePkgPath, 'utf8'));
|
|
507
|
+
|
|
508
|
+
deps = await lockfile.dependenciesOf(workspacePkg, {
|
|
509
|
+
workspacePath: values.workspace,
|
|
510
|
+
repoDir,
|
|
511
|
+
dev: values.dev,
|
|
512
|
+
peer: values.peer
|
|
513
|
+
});
|
|
514
|
+
} else {
|
|
515
|
+
deps = lockfile;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (values.cover) {
|
|
520
|
+
// Coverage mode
|
|
521
|
+
const sorted = [...deps].sort((a, b) => a.name.localeCompare(b.name));
|
|
522
|
+
const results = checkCoverage(sorted, {
|
|
523
|
+
registry: values.registry,
|
|
524
|
+
auth: values.auth,
|
|
525
|
+
token: values.token,
|
|
526
|
+
progress: values.progress
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
await outputCoverage(results, {
|
|
530
|
+
json: values.json,
|
|
531
|
+
ndjson: values.ndjson,
|
|
532
|
+
summary: values.summary,
|
|
533
|
+
full: values.full
|
|
534
|
+
});
|
|
535
|
+
} else {
|
|
536
|
+
// Standard flatlock mode
|
|
537
|
+
outputDeps(deps, {
|
|
538
|
+
specs: values.specs,
|
|
539
|
+
json: values.json,
|
|
540
|
+
ndjson: values.ndjson,
|
|
541
|
+
full: values.full
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
} catch (err) {
|
|
545
|
+
console.error(`Error: ${err.message}`);
|
|
546
|
+
process.exit(1);
|
|
547
|
+
}
|
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++;
|