flatlock 1.2.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 +42 -1
- package/bin/flatcover.js +398 -0
- package/bin/flatlock.js +158 -0
- package/package.json +16 -5
- 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/dist/compare.d.ts +0 -85
- 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 -72
- 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 -109
- package/dist/parsers/npm.d.ts.map +0 -1
- package/dist/parsers/pnpm/detect.d.ts +0 -136
- package/dist/parsers/pnpm/detect.d.ts.map +0 -1
- package/dist/parsers/pnpm/index.d.ts +0 -120
- package/dist/parsers/pnpm/index.d.ts.map +0 -1
- package/dist/parsers/pnpm/internal.d.ts +0 -5
- package/dist/parsers/pnpm/internal.d.ts.map +0 -1
- package/dist/parsers/pnpm/shrinkwrap.d.ts +0 -129
- package/dist/parsers/pnpm/shrinkwrap.d.ts.map +0 -1
- package/dist/parsers/pnpm/v5.d.ts +0 -139
- package/dist/parsers/pnpm/v5.d.ts.map +0 -1
- package/dist/parsers/pnpm/v6plus.d.ts +0 -212
- package/dist/parsers/pnpm/v6plus.d.ts.map +0 -1
- package/dist/parsers/pnpm.d.ts +0 -2
- package/dist/parsers/pnpm.d.ts.map +0 -1
- package/dist/parsers/types.d.ts +0 -23
- package/dist/parsers/types.d.ts.map +0 -1
- package/dist/parsers/yarn-berry.d.ts +0 -154
- package/dist/parsers/yarn-berry.d.ts.map +0 -1
- package/dist/parsers/yarn-classic.d.ts +0 -110
- 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/dist/set.d.ts +0 -189
- package/dist/set.d.ts.map +0 -1
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,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
|
+
}
|
package/bin/flatlock.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* flatlock - Get dependencies from a lockfile
|
|
4
|
+
*
|
|
5
|
+
* For monorepo workspaces, outputs the production dependencies of a workspace.
|
|
6
|
+
* For standalone packages, outputs all production dependencies.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* flatlock <lockfile> # all deps (names only)
|
|
10
|
+
* flatlock <lockfile> --specs # name@version
|
|
11
|
+
* flatlock <lockfile> --json # JSON array
|
|
12
|
+
* flatlock <lockfile> --specs --json # JSON with versions
|
|
13
|
+
* flatlock <lockfile> --specs --ndjson # streaming NDJSON
|
|
14
|
+
* flatlock <lockfile> --full --ndjson # full metadata streaming
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { parseArgs } from 'node:util';
|
|
18
|
+
import { readFileSync } from 'node:fs';
|
|
19
|
+
import { dirname, join } from 'node:path';
|
|
20
|
+
import { FlatlockSet } from '../src/set.js';
|
|
21
|
+
|
|
22
|
+
const { values, positionals } = parseArgs({
|
|
23
|
+
options: {
|
|
24
|
+
workspace: { type: 'string', short: 'w' },
|
|
25
|
+
dev: { type: 'boolean', default: false },
|
|
26
|
+
peer: { type: 'boolean', default: true },
|
|
27
|
+
specs: { type: 'boolean', short: 's', default: false },
|
|
28
|
+
json: { type: 'boolean', default: false },
|
|
29
|
+
ndjson: { type: 'boolean', default: false },
|
|
30
|
+
full: { type: 'boolean', default: false },
|
|
31
|
+
help: { type: 'boolean', short: 'h' }
|
|
32
|
+
},
|
|
33
|
+
allowPositionals: true
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (values.help || positionals.length === 0) {
|
|
37
|
+
console.log(`flatlock - Get dependencies from a lockfile
|
|
38
|
+
|
|
39
|
+
Usage:
|
|
40
|
+
flatlock <lockfile>
|
|
41
|
+
flatlock <lockfile> --workspace <path>
|
|
42
|
+
|
|
43
|
+
Options:
|
|
44
|
+
-w, --workspace <path> Workspace path within monorepo
|
|
45
|
+
-s, --specs Include version (name@version or {name,version})
|
|
46
|
+
--json Output as JSON array
|
|
47
|
+
--ndjson Output as newline-delimited JSON (streaming)
|
|
48
|
+
--full Include all metadata (integrity, resolved)
|
|
49
|
+
--dev Include dev dependencies (default: false)
|
|
50
|
+
--peer Include peer dependencies (default: true)
|
|
51
|
+
-h, --help Show this help
|
|
52
|
+
|
|
53
|
+
Output formats:
|
|
54
|
+
(default) package names, one per line
|
|
55
|
+
--specs package@version, one per line
|
|
56
|
+
--json ["package", ...]
|
|
57
|
+
--specs --json [{"name":"...","version":"..."}, ...]
|
|
58
|
+
--full --json [{"name":"...","version":"...","integrity":"...","resolved":"..."}, ...]
|
|
59
|
+
--ndjson "package" per line
|
|
60
|
+
--specs --ndjson {"name":"...","version":"..."} per line
|
|
61
|
+
--full --ndjson {"name":"...","version":"...","integrity":"...","resolved":"..."} per line
|
|
62
|
+
|
|
63
|
+
Examples:
|
|
64
|
+
flatlock package-lock.json
|
|
65
|
+
flatlock package-lock.json --specs
|
|
66
|
+
flatlock package-lock.json --specs --json
|
|
67
|
+
flatlock package-lock.json --full --ndjson | jq -c 'select(.name | startswith("@babel"))'
|
|
68
|
+
flatlock pnpm-lock.yaml -w packages/core -s --ndjson`);
|
|
69
|
+
process.exit(values.help ? 0 : 1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (values.json && values.ndjson) {
|
|
73
|
+
console.error('Error: --json and --ndjson are mutually exclusive');
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// --full implies --specs
|
|
78
|
+
if (values.full) {
|
|
79
|
+
values.specs = true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const lockfilePath = positionals[0];
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Format a single dependency based on output options
|
|
86
|
+
* @param {{ name: string, version: string, integrity?: string, resolved?: string }} dep
|
|
87
|
+
* @param {{ specs: boolean, full: boolean }} options
|
|
88
|
+
* @returns {string | object}
|
|
89
|
+
*/
|
|
90
|
+
function formatDep(dep, { specs, full }) {
|
|
91
|
+
if (full) {
|
|
92
|
+
const obj = { name: dep.name, version: dep.version };
|
|
93
|
+
if (dep.integrity) obj.integrity = dep.integrity;
|
|
94
|
+
if (dep.resolved) obj.resolved = dep.resolved;
|
|
95
|
+
return obj;
|
|
96
|
+
}
|
|
97
|
+
if (specs) {
|
|
98
|
+
return { name: dep.name, version: dep.version };
|
|
99
|
+
}
|
|
100
|
+
return dep.name;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Output dependencies in the requested format
|
|
105
|
+
* @param {Iterable<{ name: string, version: string, integrity?: string, resolved?: string }>} deps
|
|
106
|
+
* @param {{ specs: boolean, json: boolean, ndjson: boolean, full: boolean }} options
|
|
107
|
+
*/
|
|
108
|
+
function outputDeps(deps, { specs, json, ndjson, full }) {
|
|
109
|
+
const sorted = [...deps].sort((a, b) => a.name.localeCompare(b.name));
|
|
110
|
+
|
|
111
|
+
if (json) {
|
|
112
|
+
const data = sorted.map(d => formatDep(d, { specs, full }));
|
|
113
|
+
console.log(JSON.stringify(data, null, 2));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (ndjson) {
|
|
118
|
+
for (const d of sorted) {
|
|
119
|
+
console.log(JSON.stringify(formatDep(d, { specs, full })));
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Plain text
|
|
125
|
+
for (const d of sorted) {
|
|
126
|
+
console.log(specs ? `${d.name}@${d.version}` : d.name);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const lockfile = await FlatlockSet.fromPath(lockfilePath);
|
|
132
|
+
let deps;
|
|
133
|
+
|
|
134
|
+
if (values.workspace) {
|
|
135
|
+
const repoDir = dirname(lockfilePath);
|
|
136
|
+
const workspacePkgPath = join(repoDir, values.workspace, 'package.json');
|
|
137
|
+
const workspacePkg = JSON.parse(readFileSync(workspacePkgPath, 'utf8'));
|
|
138
|
+
|
|
139
|
+
deps = await lockfile.dependenciesOf(workspacePkg, {
|
|
140
|
+
workspacePath: values.workspace,
|
|
141
|
+
repoDir,
|
|
142
|
+
dev: values.dev,
|
|
143
|
+
peer: values.peer
|
|
144
|
+
});
|
|
145
|
+
} else {
|
|
146
|
+
deps = lockfile;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
outputDeps(deps, {
|
|
150
|
+
specs: values.specs,
|
|
151
|
+
json: values.json,
|
|
152
|
+
ndjson: values.ndjson,
|
|
153
|
+
full: values.full
|
|
154
|
+
});
|
|
155
|
+
} catch (err) {
|
|
156
|
+
console.error(`Error: ${err.message}`);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flatlock",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "The Matlock of lockfile parsers - extracts packages without building dependency graphs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"lockfile",
|
|
@@ -35,12 +35,16 @@
|
|
|
35
35
|
},
|
|
36
36
|
"main": "src/index.js",
|
|
37
37
|
"bin": {
|
|
38
|
-
"flatlock
|
|
38
|
+
"flatlock": "./bin/flatlock.js",
|
|
39
|
+
"flatlock-cmp": "./bin/flatlock-cmp.js",
|
|
40
|
+
"flatcover": "./bin/flatcover.js"
|
|
39
41
|
},
|
|
40
42
|
"files": [
|
|
41
43
|
"src",
|
|
42
44
|
"dist",
|
|
43
|
-
"bin"
|
|
45
|
+
"bin/flatlock.js",
|
|
46
|
+
"bin/flatlock-cmp.js",
|
|
47
|
+
"bin/flatcover.js"
|
|
44
48
|
],
|
|
45
49
|
"scripts": {
|
|
46
50
|
"test": "node --test ./test/*.test.js ./test/**/*.test.js",
|
|
@@ -53,24 +57,31 @@
|
|
|
53
57
|
"format:check": "biome format src test",
|
|
54
58
|
"check": "biome check src test && pnpm run build:types",
|
|
55
59
|
"check:fix": "biome check --write src test",
|
|
60
|
+
"build:ncc": "./bin/ncc.sh",
|
|
56
61
|
"prepublishOnly": "pnpm run build:types"
|
|
57
62
|
},
|
|
58
63
|
"dependencies": {
|
|
59
64
|
"@yarnpkg/lockfile": "^1.1.0",
|
|
60
65
|
"@yarnpkg/parsers": "^3.0.3",
|
|
61
|
-
"js-yaml": "^4.1.1"
|
|
66
|
+
"js-yaml": "^4.1.1",
|
|
67
|
+
"undici": "^7.0.0"
|
|
62
68
|
},
|
|
63
69
|
"devDependencies": {
|
|
64
70
|
"@biomejs/biome": "^2.3.8",
|
|
71
|
+
"@vercel/ncc": "^0.38.4",
|
|
65
72
|
"@types/js-yaml": "^4.0.9",
|
|
66
73
|
"@types/node": "^22.10.2",
|
|
67
74
|
"c8": "^10.1.3",
|
|
75
|
+
"chalk": "^5.6.2",
|
|
76
|
+
"fast-glob": "^3.3.3",
|
|
77
|
+
"jackspeak": "^4.1.1",
|
|
68
78
|
"markdownlint-cli2": "^0.17.2",
|
|
69
79
|
"snyk-nodejs-lockfile-parser": "^1.55.0",
|
|
80
|
+
"tinyexec": "^1.0.2",
|
|
70
81
|
"typescript": "^5.7.2"
|
|
71
82
|
},
|
|
72
83
|
"optionalDependencies": {
|
|
73
|
-
"@cyclonedx/
|
|
84
|
+
"@cyclonedx/cdxgen": "^11.3.3",
|
|
74
85
|
"@npmcli/arborist": "^9.1.9",
|
|
75
86
|
"@pnpm/lockfile.fs": "^1001.0.0",
|
|
76
87
|
"@yarnpkg/core": "^4.5.0"
|
package/src/parsers/index.js
CHANGED
|
@@ -2,9 +2,21 @@
|
|
|
2
2
|
* Re-export all lockfile parsers
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
export { fromPackageLock, parseLockfileKey as parseNpmKey } from './npm.js';
|
|
6
|
-
export { fromPnpmLock, parseLockfileKey as parsePnpmKey } from './pnpm.js';
|
|
7
5
|
export {
|
|
6
|
+
buildWorkspacePackages as buildNpmWorkspacePackages,
|
|
7
|
+
extractWorkspacePaths as extractNpmWorkspacePaths,
|
|
8
|
+
fromPackageLock,
|
|
9
|
+
parseLockfileKey as parseNpmKey
|
|
10
|
+
} from './npm.js';
|
|
11
|
+
export {
|
|
12
|
+
buildWorkspacePackages as buildPnpmWorkspacePackages,
|
|
13
|
+
extractWorkspacePaths as extractPnpmWorkspacePaths,
|
|
14
|
+
fromPnpmLock,
|
|
15
|
+
parseLockfileKey as parsePnpmKey
|
|
16
|
+
} from './pnpm.js';
|
|
17
|
+
export {
|
|
18
|
+
buildWorkspacePackages as buildYarnBerryWorkspacePackages,
|
|
19
|
+
extractWorkspacePaths as extractYarnBerryWorkspacePaths,
|
|
8
20
|
fromYarnBerryLock,
|
|
9
21
|
parseLockfileKey as parseYarnBerryKey,
|
|
10
22
|
parseResolution as parseYarnBerryResolution
|