flatlock 1.1.0 → 1.3.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 +95 -1
- package/bin/flatcover.js +398 -0
- package/bin/flatlock-cmp.js +71 -45
- package/bin/flatlock.js +158 -0
- package/package.json +21 -8
- package/src/compare.js +385 -28
- package/src/detect.js +3 -4
- package/src/index.js +9 -2
- package/src/parsers/index.js +24 -4
- package/src/parsers/npm.js +144 -14
- package/src/parsers/pnpm/detect.js +198 -0
- package/src/parsers/pnpm/index.js +359 -0
- package/src/parsers/pnpm/internal.js +41 -0
- package/src/parsers/pnpm/shrinkwrap.js +241 -0
- package/src/parsers/pnpm/v5.js +225 -0
- package/src/parsers/pnpm/v6plus.js +290 -0
- package/src/parsers/pnpm.js +11 -89
- package/src/parsers/types.js +10 -0
- package/src/parsers/yarn-berry.js +271 -36
- package/src/parsers/yarn-classic.js +81 -21
- package/src/set.js +1307 -0
- package/dist/compare.d.ts +0 -63
- package/dist/compare.d.ts.map +0 -1
- package/dist/detect.d.ts +0 -33
- package/dist/detect.d.ts.map +0 -1
- package/dist/index.d.ts +0 -70
- package/dist/index.d.ts.map +0 -1
- package/dist/parsers/index.d.ts +0 -5
- package/dist/parsers/index.d.ts.map +0 -1
- package/dist/parsers/npm.d.ts +0 -82
- package/dist/parsers/npm.d.ts.map +0 -1
- package/dist/parsers/pnpm.d.ts +0 -60
- package/dist/parsers/pnpm.d.ts.map +0 -1
- package/dist/parsers/yarn-berry.d.ts +0 -65
- package/dist/parsers/yarn-berry.d.ts.map +0 -1
- package/dist/parsers/yarn-classic.d.ts +0 -64
- package/dist/parsers/yarn-classic.d.ts.map +0 -1
- package/dist/result.d.ts +0 -12
- package/dist/result.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Most lockfile parsers (like `@npmcli/arborist` or `snyk-nodejs-lockfile-parser`)
|
|
|
11
11
|
**flatlock** takes a different approach: it extracts a flat stream of packages from any lockfile format. No trees, no graphs, no edges - just packages.
|
|
12
12
|
|
|
13
13
|
```javascript
|
|
14
|
-
import * as
|
|
14
|
+
import * as flatlock from 'flatlock';
|
|
15
15
|
|
|
16
16
|
// Stream packages from any lockfile
|
|
17
17
|
for await (const pkg of flatlock.fromPath('./package-lock.json')) {
|
|
@@ -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
|
|
@@ -89,6 +112,77 @@ Each yielded package has:
|
|
|
89
112
|
}
|
|
90
113
|
```
|
|
91
114
|
|
|
115
|
+
## FlatlockSet
|
|
116
|
+
|
|
117
|
+
For more advanced use cases, `FlatlockSet` provides Set-like operations on lockfile dependencies:
|
|
118
|
+
|
|
119
|
+
```javascript
|
|
120
|
+
import { FlatlockSet } from 'flatlock';
|
|
121
|
+
|
|
122
|
+
// Create from lockfile
|
|
123
|
+
const set = await FlatlockSet.fromPath('./package-lock.json');
|
|
124
|
+
console.log(set.size); // 1234
|
|
125
|
+
console.log(set.has('lodash@4.17.21')); // true
|
|
126
|
+
|
|
127
|
+
// Set operations (immutable - return new sets)
|
|
128
|
+
const other = await FlatlockSet.fromPath('./other-lock.json');
|
|
129
|
+
const common = set.intersection(other); // packages in both
|
|
130
|
+
const added = other.difference(set); // packages only in other
|
|
131
|
+
const all = set.union(other); // packages in either
|
|
132
|
+
|
|
133
|
+
// Predicates
|
|
134
|
+
set.isSubsetOf(other); // true if all packages in set are in other
|
|
135
|
+
set.isSupersetOf(other); // true if set contains all packages in other
|
|
136
|
+
set.isDisjointFrom(other); // true if no packages in common
|
|
137
|
+
|
|
138
|
+
// Iterate like a Set
|
|
139
|
+
for (const dep of set) {
|
|
140
|
+
console.log(dep.name, dep.version);
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Workspace-Specific SBOMs
|
|
145
|
+
|
|
146
|
+
For monorepos, use `dependenciesOf()` to get only the dependencies of a specific workspace:
|
|
147
|
+
|
|
148
|
+
```javascript
|
|
149
|
+
import { readFile } from 'node:fs/promises';
|
|
150
|
+
import { FlatlockSet } from 'flatlock';
|
|
151
|
+
|
|
152
|
+
const lockfile = await FlatlockSet.fromPath('./package-lock.json');
|
|
153
|
+
const pkg = JSON.parse(await readFile('./packages/api/package.json', 'utf8'));
|
|
154
|
+
|
|
155
|
+
// Get only dependencies reachable from this workspace
|
|
156
|
+
const subset = await lockfile.dependenciesOf(pkg, {
|
|
157
|
+
workspacePath: 'packages/api', // for correct resolution in monorepos
|
|
158
|
+
repoDir: '.', // reads workspace package.json files for accurate traversal
|
|
159
|
+
dev: false, // exclude devDependencies
|
|
160
|
+
optional: true, // include optionalDependencies
|
|
161
|
+
peer: false // exclude peerDependencies
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
console.log(`${pkg.name} has ${subset.size} production dependencies`);
|
|
165
|
+
```
|
|
166
|
+
|
|
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.
|
|
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
|
+
|
|
92
186
|
## License
|
|
93
187
|
|
|
94
188
|
Apache-2.0
|
package/bin/flatcover.js
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
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 { dirname, join } from 'node:path';
|
|
18
|
+
import { Pool, RetryAgent } from 'undici';
|
|
19
|
+
import { FlatlockSet } from '../src/set.js';
|
|
20
|
+
|
|
21
|
+
const { values, positionals } = parseArgs({
|
|
22
|
+
options: {
|
|
23
|
+
workspace: { type: 'string', short: 'w' },
|
|
24
|
+
dev: { type: 'boolean', default: false },
|
|
25
|
+
peer: { type: 'boolean', default: true },
|
|
26
|
+
specs: { type: 'boolean', short: 's', default: false },
|
|
27
|
+
json: { type: 'boolean', default: false },
|
|
28
|
+
ndjson: { type: 'boolean', default: false },
|
|
29
|
+
full: { type: 'boolean', default: false },
|
|
30
|
+
cover: { type: 'boolean', default: false },
|
|
31
|
+
registry: { type: 'string', default: 'https://registry.npmjs.org' },
|
|
32
|
+
auth: { type: 'string' },
|
|
33
|
+
token: { type: 'string' },
|
|
34
|
+
concurrency: { type: 'string', default: '20' },
|
|
35
|
+
progress: { type: 'boolean', default: false },
|
|
36
|
+
summary: { type: 'boolean', default: false },
|
|
37
|
+
help: { type: 'boolean', short: 'h' }
|
|
38
|
+
},
|
|
39
|
+
allowPositionals: true
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (values.help || positionals.length === 0) {
|
|
43
|
+
console.log(`flatcover - Check lockfile package coverage against a registry
|
|
44
|
+
|
|
45
|
+
Usage:
|
|
46
|
+
flatcover <lockfile> --cover
|
|
47
|
+
flatcover <lockfile> --cover --registry <url>
|
|
48
|
+
flatcover <lockfile> --cover --registry <url> --auth user:pass
|
|
49
|
+
|
|
50
|
+
Options:
|
|
51
|
+
-w, --workspace <path> Workspace path within monorepo
|
|
52
|
+
-s, --specs Include version (name@version or {name,version})
|
|
53
|
+
--json Output as JSON array
|
|
54
|
+
--ndjson Output as newline-delimited JSON (streaming)
|
|
55
|
+
--full Include all metadata (integrity, resolved)
|
|
56
|
+
--dev Include dev dependencies (default: false)
|
|
57
|
+
--peer Include peer dependencies (default: true)
|
|
58
|
+
-h, --help Show this help
|
|
59
|
+
|
|
60
|
+
Coverage options:
|
|
61
|
+
--cover Enable registry coverage checking
|
|
62
|
+
--registry <url> Registry URL (default: https://registry.npmjs.org)
|
|
63
|
+
--auth <user:pass> Basic authentication credentials
|
|
64
|
+
--token <token> Bearer token for authentication
|
|
65
|
+
--concurrency <n> Concurrent requests (default: 20)
|
|
66
|
+
--progress Show progress on stderr
|
|
67
|
+
--summary Show coverage summary on stderr
|
|
68
|
+
|
|
69
|
+
Output formats (with --cover):
|
|
70
|
+
(default) CSV: package,version,present
|
|
71
|
+
--json [{"name":"...","version":"...","present":true}, ...]
|
|
72
|
+
--ndjson {"name":"...","version":"...","present":true} per line
|
|
73
|
+
|
|
74
|
+
Examples:
|
|
75
|
+
flatcover package-lock.json --cover
|
|
76
|
+
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`);
|
|
79
|
+
process.exit(values.help ? 0 : 1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (values.json && values.ndjson) {
|
|
83
|
+
console.error('Error: --json and --ndjson are mutually exclusive');
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (values.auth && values.token) {
|
|
88
|
+
console.error('Error: --auth and --token are mutually exclusive');
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// --full implies --specs
|
|
93
|
+
if (values.full) {
|
|
94
|
+
values.specs = true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// --cover implies --specs (need versions to check)
|
|
98
|
+
if (values.cover) {
|
|
99
|
+
values.specs = true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const lockfilePath = positionals[0];
|
|
103
|
+
const concurrency = Math.max(1, Math.min(50, Number.parseInt(values.concurrency, 10) || 20));
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Encode package name for URL (handle scoped packages)
|
|
107
|
+
* @param {string} name - Package name like @babel/core
|
|
108
|
+
* @returns {string} URL-safe name like @babel%2fcore
|
|
109
|
+
*/
|
|
110
|
+
function encodePackageName(name) {
|
|
111
|
+
// Scoped packages: @scope/name -> @scope%2fname
|
|
112
|
+
return name.replace('/', '%2f');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Create undici client with retry support
|
|
117
|
+
* @param {string} registryUrl
|
|
118
|
+
* @param {{ auth?: string, token?: string }} options
|
|
119
|
+
* @returns {{ client: RetryAgent, headers: Record<string, string>, baseUrl: URL }}
|
|
120
|
+
*/
|
|
121
|
+
function createClient(registryUrl, { auth, token }) {
|
|
122
|
+
const baseUrl = new URL(registryUrl);
|
|
123
|
+
|
|
124
|
+
const pool = new Pool(baseUrl.origin, {
|
|
125
|
+
connections: Math.min(concurrency, 50),
|
|
126
|
+
pipelining: 1, // Conservative - most proxies don't support HTTP pipelining
|
|
127
|
+
keepAliveTimeout: 30000,
|
|
128
|
+
keepAliveMaxTimeout: 60000
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const client = new RetryAgent(pool, {
|
|
132
|
+
maxRetries: 3,
|
|
133
|
+
minTimeout: 1000,
|
|
134
|
+
maxTimeout: 10000,
|
|
135
|
+
timeoutFactor: 2,
|
|
136
|
+
retryAfter: true, // Respect Retry-After header
|
|
137
|
+
statusCodes: [429, 500, 502, 503, 504],
|
|
138
|
+
errorCodes: [
|
|
139
|
+
'ECONNRESET',
|
|
140
|
+
'ECONNREFUSED',
|
|
141
|
+
'ENOTFOUND',
|
|
142
|
+
'ENETUNREACH',
|
|
143
|
+
'ETIMEDOUT',
|
|
144
|
+
'UND_ERR_SOCKET'
|
|
145
|
+
]
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const headers = {
|
|
149
|
+
Accept: 'application/json',
|
|
150
|
+
'User-Agent': 'flatcover/1.0.0'
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
if (auth) {
|
|
154
|
+
headers.Authorization = `Basic ${Buffer.from(auth).toString('base64')}`;
|
|
155
|
+
} else if (token) {
|
|
156
|
+
headers.Authorization = `Bearer ${token}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { client, headers, baseUrl };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 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 }>}
|
|
167
|
+
*/
|
|
168
|
+
async function* checkCoverage(deps, { registry, auth, token, progress }) {
|
|
169
|
+
const { client, headers, baseUrl } = createClient(registry, { auth, token });
|
|
170
|
+
|
|
171
|
+
// Group by package name to avoid duplicate requests
|
|
172
|
+
/** @type {Map<string, Set<string>>} */
|
|
173
|
+
const byPackage = new Map();
|
|
174
|
+
for (const dep of deps) {
|
|
175
|
+
if (!byPackage.has(dep.name)) {
|
|
176
|
+
byPackage.set(dep.name, new Set());
|
|
177
|
+
}
|
|
178
|
+
byPackage.get(dep.name).add(dep.version);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const packages = [...byPackage.entries()];
|
|
182
|
+
let completed = 0;
|
|
183
|
+
const total = packages.length;
|
|
184
|
+
|
|
185
|
+
// Process in batches for bounded concurrency
|
|
186
|
+
for (let i = 0; i < packages.length; i += concurrency) {
|
|
187
|
+
const batch = packages.slice(i, i + concurrency);
|
|
188
|
+
|
|
189
|
+
const results = await Promise.all(
|
|
190
|
+
batch.map(async ([name, versions]) => {
|
|
191
|
+
const encodedName = encodePackageName(name);
|
|
192
|
+
const basePath = baseUrl.pathname.replace(/\/$/, '');
|
|
193
|
+
const path = `${basePath}/${encodedName}`;
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
const response = await client.request({
|
|
197
|
+
method: 'GET',
|
|
198
|
+
path,
|
|
199
|
+
headers
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const chunks = [];
|
|
203
|
+
for await (const chunk of response.body) {
|
|
204
|
+
chunks.push(chunk);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (response.statusCode === 401 || response.statusCode === 403) {
|
|
208
|
+
console.error(`Error: Authentication failed for ${name} (${response.statusCode})`);
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let packumentVersions = null;
|
|
213
|
+
if (response.statusCode === 200) {
|
|
214
|
+
const body = Buffer.concat(chunks).toString('utf8');
|
|
215
|
+
const packument = JSON.parse(body);
|
|
216
|
+
packumentVersions = packument.versions || {};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Check each version
|
|
220
|
+
const versionResults = [];
|
|
221
|
+
for (const version of versions) {
|
|
222
|
+
const present = packumentVersions ? !!packumentVersions[version] : false;
|
|
223
|
+
versionResults.push({ name, version, present });
|
|
224
|
+
}
|
|
225
|
+
return versionResults;
|
|
226
|
+
} catch (err) {
|
|
227
|
+
// Return error for all versions of this package
|
|
228
|
+
return [...versions].map(version => ({
|
|
229
|
+
name,
|
|
230
|
+
version,
|
|
231
|
+
present: false,
|
|
232
|
+
error: err.message
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
// Flatten and yield results
|
|
239
|
+
for (const packageResults of results) {
|
|
240
|
+
for (const result of packageResults) {
|
|
241
|
+
yield result;
|
|
242
|
+
}
|
|
243
|
+
completed++;
|
|
244
|
+
if (progress) {
|
|
245
|
+
process.stderr.write(`\r Checking: ${completed}/${total} packages`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (progress) {
|
|
251
|
+
process.stderr.write('\n');
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Format a single dependency based on output options
|
|
257
|
+
* @param {{ name: string, version: string, integrity?: string, resolved?: string }} dep
|
|
258
|
+
* @param {{ specs: boolean, full: boolean }} options
|
|
259
|
+
* @returns {string | object}
|
|
260
|
+
*/
|
|
261
|
+
function formatDep(dep, { specs, full }) {
|
|
262
|
+
if (full) {
|
|
263
|
+
const obj = { name: dep.name, version: dep.version };
|
|
264
|
+
if (dep.integrity) obj.integrity = dep.integrity;
|
|
265
|
+
if (dep.resolved) obj.resolved = dep.resolved;
|
|
266
|
+
return obj;
|
|
267
|
+
}
|
|
268
|
+
if (specs) {
|
|
269
|
+
return { name: dep.name, version: dep.version };
|
|
270
|
+
}
|
|
271
|
+
return dep.name;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Output dependencies in the requested format (non-cover mode)
|
|
276
|
+
* @param {Iterable<{ name: string, version: string, integrity?: string, resolved?: string }>} deps
|
|
277
|
+
* @param {{ specs: boolean, json: boolean, ndjson: boolean, full: boolean }} options
|
|
278
|
+
*/
|
|
279
|
+
function outputDeps(deps, { specs, json, ndjson, full }) {
|
|
280
|
+
const sorted = [...deps].sort((a, b) => a.name.localeCompare(b.name));
|
|
281
|
+
|
|
282
|
+
if (json) {
|
|
283
|
+
const data = sorted.map(d => formatDep(d, { specs, full }));
|
|
284
|
+
console.log(JSON.stringify(data, null, 2));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (ndjson) {
|
|
289
|
+
for (const d of sorted) {
|
|
290
|
+
console.log(JSON.stringify(formatDep(d, { specs, full })));
|
|
291
|
+
}
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Plain text
|
|
296
|
+
for (const d of sorted) {
|
|
297
|
+
console.log(specs ? `${d.name}@${d.version}` : d.name);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Output coverage results
|
|
303
|
+
* @param {AsyncGenerator<{ name: string, version: string, present: boolean, error?: string }>} results
|
|
304
|
+
* @param {{ json: boolean, ndjson: boolean, summary: boolean }} options
|
|
305
|
+
*/
|
|
306
|
+
async function outputCoverage(results, { json, ndjson, summary }) {
|
|
307
|
+
const all = [];
|
|
308
|
+
let presentCount = 0;
|
|
309
|
+
let missingCount = 0;
|
|
310
|
+
|
|
311
|
+
for await (const result of results) {
|
|
312
|
+
if (result.present) {
|
|
313
|
+
presentCount++;
|
|
314
|
+
} else {
|
|
315
|
+
missingCount++;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (ndjson) {
|
|
319
|
+
// Stream immediately
|
|
320
|
+
console.log(JSON.stringify({ name: result.name, version: result.version, present: result.present }));
|
|
321
|
+
} else {
|
|
322
|
+
all.push(result);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (!ndjson) {
|
|
327
|
+
// Sort by name, then version
|
|
328
|
+
all.sort((a, b) => a.name.localeCompare(b.name) || a.version.localeCompare(b.version));
|
|
329
|
+
|
|
330
|
+
if (json) {
|
|
331
|
+
const data = all.map(r => ({ name: r.name, version: r.version, present: r.present }));
|
|
332
|
+
console.log(JSON.stringify(data, null, 2));
|
|
333
|
+
} else {
|
|
334
|
+
// CSV output
|
|
335
|
+
console.log('package,version,present');
|
|
336
|
+
for (const r of all) {
|
|
337
|
+
console.log(`${r.name},${r.version},${r.present}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (summary) {
|
|
343
|
+
const total = presentCount + missingCount;
|
|
344
|
+
const percentage = total > 0 ? ((presentCount / total) * 100).toFixed(1) : 0;
|
|
345
|
+
process.stderr.write(`\nCoverage: ${presentCount}/${total} (${percentage}%) packages present\n`);
|
|
346
|
+
if (missingCount > 0) {
|
|
347
|
+
process.stderr.write(`Missing: ${missingCount} packages\n`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
const lockfile = await FlatlockSet.fromPath(lockfilePath);
|
|
354
|
+
let deps;
|
|
355
|
+
|
|
356
|
+
if (values.workspace) {
|
|
357
|
+
const repoDir = dirname(lockfilePath);
|
|
358
|
+
const workspacePkgPath = join(repoDir, values.workspace, 'package.json');
|
|
359
|
+
const workspacePkg = JSON.parse(readFileSync(workspacePkgPath, 'utf8'));
|
|
360
|
+
|
|
361
|
+
deps = await lockfile.dependenciesOf(workspacePkg, {
|
|
362
|
+
workspacePath: values.workspace,
|
|
363
|
+
repoDir,
|
|
364
|
+
dev: values.dev,
|
|
365
|
+
peer: values.peer
|
|
366
|
+
});
|
|
367
|
+
} else {
|
|
368
|
+
deps = lockfile;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (values.cover) {
|
|
372
|
+
// Coverage mode
|
|
373
|
+
const sorted = [...deps].sort((a, b) => a.name.localeCompare(b.name));
|
|
374
|
+
const results = checkCoverage(sorted, {
|
|
375
|
+
registry: values.registry,
|
|
376
|
+
auth: values.auth,
|
|
377
|
+
token: values.token,
|
|
378
|
+
progress: values.progress
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
await outputCoverage(results, {
|
|
382
|
+
json: values.json,
|
|
383
|
+
ndjson: values.ndjson,
|
|
384
|
+
summary: values.summary
|
|
385
|
+
});
|
|
386
|
+
} else {
|
|
387
|
+
// Standard flatlock mode
|
|
388
|
+
outputDeps(deps, {
|
|
389
|
+
specs: values.specs,
|
|
390
|
+
json: values.json,
|
|
391
|
+
ndjson: values.ndjson,
|
|
392
|
+
full: values.full
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
} catch (err) {
|
|
396
|
+
console.error(`Error: ${err.message}`);
|
|
397
|
+
process.exit(1);
|
|
398
|
+
}
|