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.
@@ -5,18 +5,6 @@ import { join } from 'node:path';
5
5
  import { parseArgs } from 'node:util';
6
6
  import * as flatlock from '../src/index.js';
7
7
 
8
- /**
9
- * Get comparison parser name for type
10
- */
11
- function getComparisonName(type) {
12
- switch (type) {
13
- case 'npm': return '@npmcli/arborist';
14
- case 'yarn-classic': return '@yarnpkg/lockfile';
15
- case 'yarn-berry': return '@yarnpkg/parsers';
16
- case 'pnpm': return 'js-yaml';
17
- default: return 'unknown';
18
- }
19
- }
20
8
 
21
9
  /**
22
10
  * Convert glob pattern to regex
@@ -59,18 +47,17 @@ async function processFile(filepath, baseDir) {
59
47
  try {
60
48
  const result = await flatlock.compare(filepath);
61
49
  const rel = baseDir ? filepath.replace(baseDir + '/', '') : filepath;
62
- const comparisonName = getComparisonName(result.type);
63
50
 
64
- if (result.identical === null) {
51
+ if (result.equinumerous === null) {
65
52
  // Unsupported type or no comparison available
66
53
  return {
67
54
  type: result.type,
68
55
  path: rel,
69
- comparisonName,
56
+ source: result.source || 'unknown',
70
57
  flatlockCount: result.flatlockCount,
71
58
  comparisonCount: null,
72
59
  workspaceCount: 0,
73
- identical: null,
60
+ equinumerous: null,
74
61
  onlyInFlatlock: null,
75
62
  onlyInComparison: null
76
63
  };
@@ -79,11 +66,11 @@ async function processFile(filepath, baseDir) {
79
66
  return {
80
67
  type: result.type,
81
68
  path: rel,
82
- comparisonName,
69
+ source: result.source,
83
70
  flatlockCount: result.flatlockCount,
84
71
  comparisonCount: result.comparisonCount,
85
72
  workspaceCount: result.workspaceCount,
86
- identical: result.identical,
73
+ equinumerous: result.equinumerous,
87
74
  onlyInFlatlock: result.onlyInFlatlock,
88
75
  onlyInComparison: result.onlyInComparison
89
76
  };
@@ -118,10 +105,21 @@ Options:
118
105
  -h, --help Show this help
119
106
 
120
107
  Comparison parsers (workspace/link entries excluded from all):
121
- npm: @npmcli/arborist (loadVirtual)
108
+ npm: @npmcli/arborist (preferred) or @cyclonedx/cyclonedx-npm
122
109
  yarn-classic: @yarnpkg/lockfile
123
110
  yarn-berry: @yarnpkg/parsers
124
- pnpm: js-yaml
111
+ pnpm: @pnpm/lockfile.fs (preferred) or js-yaml
112
+
113
+ Result types:
114
+ ✓ equinumerous Same packages in both (exact match)
115
+ ⊃ SUPERSET flatlock found MORE packages (expected for pnpm)
116
+ ❌ MISMATCH Unexpected difference (comparison found packages flatlock missed)
117
+
118
+ Note on pnpm supersets:
119
+ flatlock performs full reachability analysis on lockfiles, finding all
120
+ transitive dependencies. pnpm's official tools don't enumerate all reachable
121
+ packages - they omit some transitive deps from their API output. When flatlock
122
+ finds MORE packages than pnpm, this is expected and correct behavior.
125
123
 
126
124
  Examples:
127
125
  flatlock-cmp package-lock.json
@@ -153,6 +151,7 @@ Examples:
153
151
  let fileCount = 0;
154
152
  let errorCount = 0;
155
153
  let matchCount = 0;
154
+ let supersetCount = 0; // flatlock found more (expected for pnpm reachability)
156
155
  let mismatchCount = 0;
157
156
 
158
157
  for (const file of files) {
@@ -175,44 +174,61 @@ Examples:
175
174
  if (!values.quiet) {
176
175
  console.log(`\n⚠️ ${result.path}`);
177
176
  console.log(` flatlock: ${result.flatlockCount} packages`);
178
- console.log(` ${result.comparisonName}: unavailable`);
177
+ console.log(` ${result.source}: unavailable`);
179
178
  }
180
179
  continue;
181
180
  }
182
181
 
183
182
  totalComparison += result.comparisonCount;
184
183
 
185
- if (result.identical) {
184
+ if (result.equinumerous) {
186
185
  matchCount++;
187
186
  if (!values.quiet) {
188
187
  const wsNote = result.workspaceCount > 0 ? ` (${result.workspaceCount} workspaces excluded)` : '';
189
188
  console.log(`✓ ${result.path}${wsNote}`);
190
- console.log(` count: flatlock=${result.flatlockCount} ${result.comparisonName}=${result.comparisonCount}`);
191
- console.log(` sets: identical`);
189
+ console.log(` count: flatlock=${result.flatlockCount} ${result.source}=${result.comparisonCount}`);
190
+ console.log(` sets: equinumerous`);
192
191
  }
193
192
  } else {
194
- mismatchCount++;
195
- console.log(`\n❌ ${result.path}`);
196
- console.log(` count: flatlock=${result.flatlockCount} ${result.comparisonName}=${result.comparisonCount}`);
197
- console.log(` sets: MISMATCH`);
198
-
199
- if (result.onlyInFlatlock.length > 0) {
200
- console.log(` only in flatlock (${result.onlyInFlatlock.length}):`);
201
- for (const pkg of result.onlyInFlatlock.slice(0, 10)) {
202
- console.log(` + ${pkg}`);
203
- }
204
- if (result.onlyInFlatlock.length > 10) {
205
- console.log(` ... and ${result.onlyInFlatlock.length - 10} more`);
193
+ // Determine if this is a "superset" (flatlock found more, expected for pnpm)
194
+ // or a true "mismatch" (comparison found packages flatlock missed)
195
+ const isPnpm = result.type === 'pnpm' || result.path.includes('pnpm-lock');
196
+ const isSuperset = result.onlyInFlatlock.length > 0 && result.onlyInComparison.length === 0;
197
+
198
+ if (isPnpm && isSuperset) {
199
+ // Expected behavior: flatlock's reachability analysis found more packages
200
+ supersetCount++;
201
+ if (!values.quiet) {
202
+ const wsNote = result.workspaceCount > 0 ? ` (${result.workspaceCount} workspaces excluded)` : '';
203
+ console.log(`⊃ ${result.path}${wsNote}`);
204
+ console.log(` count: flatlock=${result.flatlockCount} ${result.source}=${result.comparisonCount}`);
205
+ console.log(` sets: SUPERSET (+${result.onlyInFlatlock.length} reachable deps)`);
206
+ console.log(` note: flatlock's reachability analysis found transitive deps pnpm omits`);
206
207
  }
207
- }
208
+ } else {
209
+ mismatchCount++;
210
+ console.log(`\n❌ ${result.path}`);
211
+ console.log(` count: flatlock=${result.flatlockCount} ${result.source}=${result.comparisonCount}`);
212
+ console.log(` sets: MISMATCH`);
208
213
 
209
- if (result.onlyInComparison.length > 0) {
210
- console.log(` only in ${result.comparisonName} (${result.onlyInComparison.length}):`);
211
- for (const pkg of result.onlyInComparison.slice(0, 10)) {
212
- console.log(` - ${pkg}`);
214
+ if (result.onlyInFlatlock.length > 0) {
215
+ console.log(` only in flatlock (${result.onlyInFlatlock.length}):`);
216
+ for (const pkg of result.onlyInFlatlock.slice(0, 10)) {
217
+ console.log(` + ${pkg}`);
218
+ }
219
+ if (result.onlyInFlatlock.length > 10) {
220
+ console.log(` ... and ${result.onlyInFlatlock.length - 10} more`);
221
+ }
213
222
  }
214
- if (result.onlyInComparison.length > 10) {
215
- console.log(` ... and ${result.onlyInComparison.length - 10} more`);
223
+
224
+ if (result.onlyInComparison.length > 0) {
225
+ console.log(` only in ${result.source} (${result.onlyInComparison.length}):`);
226
+ for (const pkg of result.onlyInComparison.slice(0, 10)) {
227
+ console.log(` - ${pkg}`);
228
+ }
229
+ if (result.onlyInComparison.length > 10) {
230
+ console.log(` ... and ${result.onlyInComparison.length - 10} more`);
231
+ }
216
232
  }
217
233
  }
218
234
  }
@@ -220,7 +236,13 @@ Examples:
220
236
 
221
237
  // Summary
222
238
  console.log('\n' + '='.repeat(70));
223
- console.log(`SUMMARY: ${fileCount} files, ${matchCount} identical, ${mismatchCount} mismatches, ${errorCount} errors`);
239
+ const summaryParts = [`${fileCount} files`, `${matchCount} equinumerous`];
240
+ if (supersetCount > 0) {
241
+ summaryParts.push(`${supersetCount} supersets`);
242
+ }
243
+ summaryParts.push(`${mismatchCount} mismatches`, `${errorCount} errors`);
244
+ console.log(`SUMMARY: ${summaryParts.join(', ')}`);
245
+
224
246
  console.log(` flatlock total: ${totalFlatlock.toString().padStart(8)} packages`);
225
247
  if (totalComparison > 0) {
226
248
  console.log(` comparison total: ${totalComparison.toString().padStart(8)} packages`);
@@ -228,8 +250,12 @@ Examples:
228
250
  if (totalWorkspaces > 0) {
229
251
  console.log(` workspaces: ${totalWorkspaces.toString().padStart(8)} excluded (local/workspace refs)`);
230
252
  }
253
+ if (supersetCount > 0) {
254
+ console.log(` supersets: ${supersetCount.toString().padStart(8)} (flatlock found more via reachability)`);
255
+ }
231
256
 
232
- // Exit with error if any mismatches
257
+ // Exit with error only for true mismatches (not supersets)
258
+ // Supersets are expected: flatlock's reachability analysis is more thorough
233
259
  if (mismatchCount > 0) {
234
260
  process.exit(1);
235
261
  }
@@ -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.1.0",
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,15 +35,19 @@
35
35
  },
36
36
  "main": "src/index.js",
37
37
  "bin": {
38
- "flatlock-cmp": "./bin/flatlock-cmp.js"
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
- "test": "node --test ./test/*.test.js",
50
+ "test": "node --test ./test/*.test.js ./test/**/*.test.js",
47
51
  "test:coverage": "c8 node --test ./test/*.test.js",
48
52
  "build:types": "tsc",
49
53
  "lint": "biome lint src test",
@@ -53,29 +57,38 @@
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",
65
- "@pnpm/lockfile-file": "^9.0.0",
71
+ "@vercel/ncc": "^0.38.4",
66
72
  "@types/js-yaml": "^4.0.9",
67
73
  "@types/node": "^22.10.2",
68
74
  "c8": "^10.1.3",
75
+ "chalk": "^5.6.2",
76
+ "fast-glob": "^3.3.3",
77
+ "jackspeak": "^4.1.1",
69
78
  "markdownlint-cli2": "^0.17.2",
70
79
  "snyk-nodejs-lockfile-parser": "^1.55.0",
80
+ "tinyexec": "^1.0.2",
71
81
  "typescript": "^5.7.2"
72
82
  },
73
83
  "optionalDependencies": {
74
- "@npmcli/arborist": "^9.1.9"
84
+ "@cyclonedx/cdxgen": "^11.3.3",
85
+ "@npmcli/arborist": "^9.1.9",
86
+ "@pnpm/lockfile.fs": "^1001.0.0",
87
+ "@yarnpkg/core": "^4.5.0"
75
88
  },
76
89
  "packageManager": "pnpm@10.25.0",
77
90
  "engines": {
78
- "node": ">=20"
91
+ "node": ">=22"
79
92
  },
80
93
  "c8": {
81
94
  "all": true,