dependency-change-report 1.4.9 → 1.4.11

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 CHANGED
@@ -15,10 +15,14 @@ A tool to analyze dependency changes between different versions of a Node.js pro
15
15
 
16
16
  ### Using npx (Recommended)
17
17
 
18
- No installation required! Run directly with npx:
18
+ No installation required! Run directly with npx from within your git repository:
19
19
 
20
20
  ```bash
21
- npx dependency-change-report <github-repo> <older-version> <newer-version> [working-dir]
21
+ # Auto-detect versions
22
+ npx dependency-change-report auto
23
+
24
+ # Or compare specific versions
25
+ npx dependency-change-report compare v1.0.0 v2.0.0
22
26
  ```
23
27
 
24
28
  ### Global Installation (Alternative)
@@ -32,39 +36,69 @@ npm install -g dependency-change-report
32
36
  Then run with:
33
37
 
34
38
  ```bash
35
- dependency-change-report <github-repo> <older-version> <newer-version> [working-dir]
39
+ # Auto-detect versions
40
+ dependency-change-report auto
41
+
42
+ # Or compare specific versions
43
+ dependency-change-report compare v1.0.0 v2.0.0
36
44
  ```
37
45
 
38
46
  ## Usage
39
47
 
40
48
  ### Command Line Interface
41
49
 
42
- Generate a dependency report:
50
+ The tool provides two main commands:
51
+
52
+ #### Auto Command (Recommended)
53
+
54
+ Automatically detects versions and generates reports:
43
55
 
44
56
  ```bash
45
- # Using npx (recommended)
46
- npx dependency-change-report <github-repo> <older-version> <newer-version> [working-dir]
57
+ # From within a git repository
58
+ dependency-change-report auto
47
59
 
48
- # If installed globally
49
- dependency-change-report <github-repo> <older-version> <newer-version> [working-dir]
60
+ # Or specify a repository URL
61
+ dependency-change-report auto <github-repo>
62
+ ```
63
+
64
+ #### Compare Command
65
+
66
+ Compare specific versions:
67
+
68
+ ```bash
69
+ # From within a git repository (repo URL auto-detected)
70
+ dependency-change-report compare <older-version> <newer-version>
71
+
72
+ # Or specify a repository URL explicitly
73
+ dependency-change-report compare --repo <github-repo> <older-version> <newer-version>
50
74
  ```
51
75
 
52
76
  The tool automatically generates three report formats:
53
77
  - `report.json` - Raw data in JSON format
54
78
  - `report.html` - Web-friendly HTML report
55
- - `report.txt` - Slack-friendly text report
79
+ - `report.md` - Markdown report (perfect for PR comments)
80
+ - `report.txt` - Plain text report
56
81
 
57
82
  ### Examples
58
83
 
59
84
  ```bash
60
- # Generate a report comparing v1.0.0 and v2.0.0 of a repository
61
- npx dependency-change-report git@github.com:user/repo.git v1.0.0 v2.0.0
85
+ # Auto-detect versions and generate all reports (from within a git repo)
86
+ dependency-change-report auto
87
+
88
+ # Compare specific versions (from within a git repo)
89
+ dependency-change-report compare v1.0.0 v2.0.0
90
+
91
+ # Compare specific versions with explicit repo URL
92
+ dependency-change-report compare --repo https://github.com/user/repo v1.0.0 v2.0.0
93
+
94
+ # Generate only HTML and Markdown reports
95
+ dependency-change-report auto --html --markdown
62
96
 
63
- # Generate a report with a specific working directory
64
- npx dependency-change-report git@github.com:user/repo.git v1.0.0 v2.0.0 /tmp/analysis
97
+ # Ignore dev dependencies
98
+ dependency-change-report compare v1.0.0 v2.0.0 --ignore-dev
65
99
 
66
- # Filter nested dependencies by namespace (e.g., @holepunch)
67
- npx dependency-change-report git@github.com:user/repo.git v1.0.0 v2.0.0 . @holepunch
100
+ # Save reports to a specific directory
101
+ dependency-change-report auto --output-dir ./reports
68
102
  ```
69
103
 
70
104
  ### Programmatic Usage
package/cli.mjs CHANGED
@@ -14,42 +14,62 @@ import { mkdir } from 'fs/promises';
14
14
  const compare = command(
15
15
  'compare',
16
16
  flag('--ignore-dev', 'ignore dev dependencies'),
17
+ flag('--debug-tree', 'output debug information about dependency tree filtering'),
17
18
  flag('--working-dir [path]', 'the working dir for the report. If not provided, then temp dir is used'),
18
19
  flag('--output-dir [path]', 'directory to save reports. If not provided, reports are saved in working dir'),
19
20
  flag('--html', 'generate a html report'),
20
21
  flag('--markdown', 'generate a markdown report'),
21
22
  flag('--text', 'generate a text only report'),
22
- arg('<repo>', 'repo url'),
23
+ flag('--repo [url]', 'repo url (optional if in git directory)'),
23
24
  arg('<older>', 'the older tag, commit, or branch'),
24
- arg('[newer]', 'the newer tag, commit, or branch'),
25
+ arg('<newer>', 'the newer tag, commit, or branch'),
25
26
  async () => {
26
27
  try {
27
- let repoUrl = compare.args.repo;
28
+ let repoUrl = compare.flags.repo;
29
+
30
+ // If no repo provided, try to get it from git remote
31
+ if (!repoUrl) {
32
+ try {
33
+ const result = await executeCommand('git', ['remote', 'get-url', 'origin'], process.cwd(), 10000, 'detecting git remote');
34
+ if (!result) {
35
+ throw new Error('Git command returned no output');
36
+ }
37
+ repoUrl = result.trim();
38
+ if (!repoUrl) {
39
+ throw new Error('Git remote URL is empty');
40
+ }
41
+ console.log(`Detected git remote: ${repoUrl}`);
42
+ } catch (error) {
43
+ console.error(`Failed to detect git remote: ${error.message}`);
44
+ throw new Error('No repo URL provided and could not detect git remote. Either provide a repo URL or run from within a git repository.');
45
+ }
46
+ }
47
+
28
48
  const olderVersion = compare.args.older;
29
49
  const newerVersion = compare.args.newer;
30
-
50
+
31
51
  // Use temp directory if working-dir not specified
32
52
  let workingDir = compare.flags['working-dir'];
33
53
  if (!workingDir) {
34
54
  const paths = envPaths('dependency-change-report');
35
55
  workingDir = paths.temp;
36
56
  }
37
-
57
+
38
58
  // Detect if running in GitHub Actions
39
59
  const isGitHubActions = process.env.GITHUB_ACTIONS === 'true';
40
-
60
+
41
61
  // Note: No need for GitHub token authentication when using worktrees
42
62
  // since we use the already-authenticated repository
43
63
  if (isGitHubActions) {
44
64
  console.log('GitHub Actions detected - using authenticated repository');
45
65
  }
46
-
66
+
47
67
  // Set up output directory
48
68
  let outputDir = compare.flags.outputDir;
49
69
  if (!outputDir) {
50
70
  outputDir = workingDir; // Default to working directory
51
71
  }
52
-
72
+
53
73
  // Ensure output directory exists
54
74
  try {
55
75
  await mkdir(outputDir, { recursive: true });
@@ -57,17 +77,17 @@ const compare = command(
57
77
  console.error(`Failed to create output directory ${outputDir}: ${error.message}`);
58
78
  throw error;
59
79
  }
60
-
80
+
61
81
  console.log(`Analyzing dependency changes for ${repoUrl} between older version (${olderVersion}) and newer version (${newerVersion})`);
62
-
63
- const report = await analyzeDependencyChanges(repoUrl, olderVersion, newerVersion, workingDir, null, compare.flags.ignoreDev);
64
-
82
+
83
+ const report = await analyzeDependencyChanges(repoUrl, olderVersion, newerVersion, workingDir, null, compare.flags.ignoreDev, compare.flags.debugTree);
84
+
65
85
  console.log('\nSummary:');
66
86
  console.log(`Added dependencies: ${report.changes.added.length}`);
67
87
  console.log(`Upgraded dependencies: ${report.changes.upgraded.length}`);
68
88
  console.log(`Removed dependencies: ${report.changes.removed.length}`);
69
89
  console.log(`Modified dependencies (namespace changes): ${report.changes.modified ? report.changes.modified.length : 0}`);
70
-
90
+
71
91
  // Display nested dependency information if available
72
92
  if (report.changes.nested) {
73
93
  console.log('\nNested Dependencies:');
@@ -75,24 +95,24 @@ const compare = command(
75
95
  console.log(`Upgraded nested dependencies: ${report.changes.nested.upgraded.length}`);
76
96
  console.log(`Removed nested dependencies: ${report.changes.nested.removed.length}`);
77
97
  }
78
-
98
+
79
99
  const changelogCount = Object.keys(report.changelogs).length;
80
100
  const errorCount = Object.keys(report.errors).length;
81
101
  console.log(`Generated changelogs for ${changelogCount} upgraded dependencies`);
82
102
  console.log(`Encountered errors with ${errorCount} dependencies`);
83
-
103
+
84
104
  // Generate additional report formats
85
105
  console.log('\nGenerating additional report formats...');
86
-
106
+
87
107
  const reportJsonPath = report.reportPath;
88
-
108
+
89
109
  // Generate GitHub Actions-friendly filenames if detected
90
110
  let baseFilename = 'report';
91
111
  if (isGitHubActions) {
92
112
  const eventName = process.env.GITHUB_EVENT_NAME;
93
113
  let prNumber = process.env.GITHUB_PR_NUMBER;
94
114
  const sha = process.env.GITHUB_SHA?.substring(0, 7);
95
-
115
+
96
116
  // Extract PR number from GITHUB_REF_NAME if not in GITHUB_PR_NUMBER
97
117
  if (!prNumber && process.env.GITHUB_REF_NAME) {
98
118
  const refName = process.env.GITHUB_REF_NAME;
@@ -101,27 +121,27 @@ const compare = command(
101
121
  prNumber = prMatch[1];
102
122
  }
103
123
  }
104
-
124
+
105
125
  if (eventName === 'pull_request' && prNumber) {
106
126
  baseFilename = `dependency-report-PR-${prNumber}`;
107
127
  } else if (sha) {
108
128
  baseFilename = `dependency-report-${sha}`;
109
129
  }
110
130
  }
111
-
131
+
112
132
  if (compare.flags.html || compare.flags.markdown || compare.flags.text) {
113
133
  if (compare.flags.html) {
114
134
  const htmlPath = join(outputDir, `${baseFilename}.html`);
115
135
  await generateHtmlReport(reportJsonPath, htmlPath);
116
136
  console.log(`🌐 HTML: ${htmlPath}`);
117
137
  }
118
-
138
+
119
139
  if (compare.flags.markdown) {
120
140
  const markdownPath = join(outputDir, `${baseFilename}.md`);
121
141
  await generateMarkdownReport(reportJsonPath, markdownPath);
122
142
  console.log(`📝 Markdown: ${markdownPath}`);
123
143
  }
124
-
144
+
125
145
  if (compare.flags.text) {
126
146
  const textPath = join(outputDir, `${baseFilename}.txt`);
127
147
  await generateTextReport(reportJsonPath, textPath);
@@ -132,16 +152,16 @@ const compare = command(
132
152
  const htmlPath = join(outputDir, `${baseFilename}.html`);
133
153
  const markdownPath = join(outputDir, `${baseFilename}.md`);
134
154
  const textPath = join(outputDir, `${baseFilename}.txt`);
135
-
155
+
136
156
  await generateHtmlReport(reportJsonPath, htmlPath);
137
157
  await generateMarkdownReport(reportJsonPath, markdownPath);
138
158
  await generateTextReport(reportJsonPath, textPath);
139
-
159
+
140
160
  console.log(`🌐 HTML: ${htmlPath}`);
141
161
  console.log(`📝 Markdown: ${markdownPath}`);
142
162
  console.log(`📝 Text: ${textPath}`);
143
163
  }
144
-
164
+
145
165
  // Output GitHub Actions commands if detected
146
166
  if (isGitHubActions) {
147
167
  const hasChanges = report.changes.added.length > 0 || report.changes.upgraded.length > 0 || report.changes.removed.length > 0;
@@ -151,10 +171,10 @@ const compare = command(
151
171
  console.log(`::set-output name=removed-count::${report.changes.removed.length}`);
152
172
  console.log(`::set-output name=report-dir::${outputDir}`);
153
173
  }
154
-
174
+
155
175
  console.log('\nReport generated successfully!');
156
176
  console.log(`📄 JSON: ${report.reportPath}`);
157
-
177
+
158
178
  // Display repository information for added dependencies
159
179
  if (report.changes.added.length > 0) {
160
180
  console.log('\nAdded dependencies with repositories:');
@@ -174,6 +194,7 @@ const compare = command(
174
194
  const auto = command(
175
195
  'auto',
176
196
  flag('--ignore-dev', 'ignore dev dependencies'),
197
+ flag('--debug-tree', 'output debug information about dependency tree filtering'),
177
198
  flag('--working-dir [path]', 'the working dir for the report. If not provided, then temp dir is used'),
178
199
  flag('--output-dir [path]', 'directory to save reports. If not provided, reports are saved in working dir'),
179
200
  flag('--html', 'generate a html report'),
@@ -183,7 +204,7 @@ const auto = command(
183
204
  async () => {
184
205
  try {
185
206
  let repoUrl = auto.args.repo;
186
-
207
+
187
208
  // If no repo provided, try to get it from git remote
188
209
  if (!repoUrl) {
189
210
  try {
@@ -201,29 +222,29 @@ const auto = command(
201
222
  throw new Error('No repo URL provided and could not detect git remote. Either provide a repo URL or run from within a git repository.');
202
223
  }
203
224
  }
204
-
225
+
205
226
  // Use temp directory if working-dir not specified
206
227
  let workingDir = auto.flags['working-dir'];
207
228
  if (!workingDir) {
208
229
  const paths = envPaths('dependency-change-report');
209
230
  workingDir = paths.temp;
210
231
  }
211
-
232
+
212
233
  // Detect if running in GitHub Actions
213
234
  const isGitHubActions = process.env.GITHUB_ACTIONS === 'true';
214
-
235
+
215
236
  // Note: No need for GitHub token authentication when using worktrees
216
237
  // since we use the already-authenticated repository
217
238
  if (isGitHubActions) {
218
239
  console.log('GitHub Actions detected - using authenticated repository');
219
240
  }
220
-
241
+
221
242
  // Set up output directory
222
243
  let outputDir = auto.flags.outputDir;
223
244
  if (!outputDir) {
224
245
  outputDir = workingDir; // Default to working directory
225
246
  }
226
-
247
+
227
248
  // Ensure output directory exists
228
249
  try {
229
250
  await mkdir(outputDir, { recursive: true });
@@ -231,22 +252,22 @@ const auto = command(
231
252
  console.error(`Failed to create output directory ${outputDir}: ${error.message}`);
232
253
  throw error;
233
254
  }
234
-
255
+
235
256
  console.log(`Auto-detecting versions for ${repoUrl}...`);
236
-
257
+
237
258
  // Detect versions automatically
238
259
  const { newer, older } = await detectVersions('.');
239
-
260
+
240
261
  console.log(`Analyzing dependency changes between ${older} and ${newer}`);
241
-
242
- const report = await analyzeDependencyChanges(repoUrl, older, newer, workingDir, null, auto.flags.ignoreDev);
243
-
262
+
263
+ const report = await analyzeDependencyChanges(repoUrl, older, newer, workingDir, null, auto.flags.ignoreDev, auto.flags.debugTree);
264
+
244
265
  console.log('\nSummary:');
245
266
  console.log(`Added dependencies: ${report.changes.added.length}`);
246
267
  console.log(`Upgraded dependencies: ${report.changes.upgraded.length}`);
247
268
  console.log(`Removed dependencies: ${report.changes.removed.length}`);
248
269
  console.log(`Modified dependencies (namespace changes): ${report.changes.modified ? report.changes.modified.length : 0}`);
249
-
270
+
250
271
  // Display nested dependency information if available
251
272
  if (report.changes.nested) {
252
273
  console.log('\nNested Dependencies:');
@@ -254,24 +275,24 @@ const auto = command(
254
275
  console.log(`Upgraded nested dependencies: ${report.changes.nested.upgraded.length}`);
255
276
  console.log(`Removed nested dependencies: ${report.changes.nested.removed.length}`);
256
277
  }
257
-
278
+
258
279
  const changelogCount = Object.keys(report.changelogs).length;
259
280
  const errorCount = Object.keys(report.errors).length;
260
281
  console.log(`Generated changelogs for ${changelogCount} upgraded dependencies`);
261
282
  console.log(`Encountered errors with ${errorCount} dependencies`);
262
-
283
+
263
284
  // Generate additional report formats
264
285
  console.log('\nGenerating additional report formats...');
265
-
286
+
266
287
  const reportJsonPath = report.reportPath;
267
-
288
+
268
289
  // Generate GitHub Actions-friendly filenames if detected
269
290
  let baseFilename = 'report';
270
291
  if (isGitHubActions) {
271
292
  const eventName = process.env.GITHUB_EVENT_NAME;
272
293
  let prNumber = process.env.GITHUB_PR_NUMBER;
273
294
  const sha = process.env.GITHUB_SHA?.substring(0, 7);
274
-
295
+
275
296
  // Extract PR number from GITHUB_REF_NAME if not in GITHUB_PR_NUMBER
276
297
  if (!prNumber && process.env.GITHUB_REF_NAME) {
277
298
  const refName = process.env.GITHUB_REF_NAME;
@@ -280,27 +301,27 @@ const auto = command(
280
301
  prNumber = prMatch[1];
281
302
  }
282
303
  }
283
-
304
+
284
305
  if (eventName === 'pull_request' && prNumber) {
285
306
  baseFilename = `dependency-report-PR-${prNumber}`;
286
307
  } else if (sha) {
287
308
  baseFilename = `dependency-report-${sha}`;
288
309
  }
289
310
  }
290
-
311
+
291
312
  if (auto.flags.html || auto.flags.markdown || auto.flags.text) {
292
313
  if (auto.flags.html) {
293
314
  const htmlPath = join(outputDir, `${baseFilename}.html`);
294
315
  await generateHtmlReport(reportJsonPath, htmlPath);
295
316
  console.log(`🌐 HTML: ${htmlPath}`);
296
317
  }
297
-
318
+
298
319
  if (auto.flags.markdown) {
299
320
  const markdownPath = join(outputDir, `${baseFilename}.md`);
300
321
  await generateMarkdownReport(reportJsonPath, markdownPath);
301
322
  console.log(`📝 Markdown: ${markdownPath}`);
302
323
  }
303
-
324
+
304
325
  if (auto.flags.text) {
305
326
  const textPath = join(outputDir, `${baseFilename}.txt`);
306
327
  await generateTextReport(reportJsonPath, textPath);
@@ -311,16 +332,16 @@ const auto = command(
311
332
  const htmlPath = join(outputDir, `${baseFilename}.html`);
312
333
  const markdownPath = join(outputDir, `${baseFilename}.md`);
313
334
  const textPath = join(outputDir, `${baseFilename}.txt`);
314
-
335
+
315
336
  await generateHtmlReport(reportJsonPath, htmlPath);
316
337
  await generateMarkdownReport(reportJsonPath, markdownPath);
317
338
  await generateTextReport(reportJsonPath, textPath);
318
-
339
+
319
340
  console.log(`🌐 HTML: ${htmlPath}`);
320
341
  console.log(`📝 Markdown: ${markdownPath}`);
321
342
  console.log(`📝 Text: ${textPath}`);
322
343
  }
323
-
344
+
324
345
  // Output GitHub Actions commands if detected
325
346
  if (isGitHubActions) {
326
347
  const hasChanges = report.changes.added.length > 0 || report.changes.upgraded.length > 0 || report.changes.removed.length > 0;
@@ -330,19 +351,87 @@ const auto = command(
330
351
  console.log(`::set-output name=removed-count::${report.changes.removed.length}`);
331
352
  console.log(`::set-output name=report-dir::${outputDir}`);
332
353
  }
333
-
354
+
334
355
  console.log('\nReport generated successfully!');
335
356
  console.log(`📄 JSON: ${report.reportPath}`);
336
-
357
+
337
358
  } catch (error) {
338
359
  console.error(`Error: ${error.message}`);
339
360
  process.exit(1);
340
361
  }
341
362
  }
342
363
  )
343
- const cmd = command('dependency-change-report', summary('show dependency changes between versions'), compare, auto )
364
+ // Default action when no subcommand is provided
365
+ const defaultAction = async () => {
366
+ console.log('🔍 Dependency Change Report\n');
367
+
368
+ // Try to detect if we're in a git repository
369
+ let repoUrl = null;
370
+ let isInGitRepo = false;
371
+
372
+ try {
373
+ const result = await executeCommand('git', ['remote', 'get-url', 'origin'], process.cwd(), 10000, 'detecting git remote');
374
+ repoUrl = result?.trim();
375
+ isInGitRepo = !!repoUrl;
376
+ } catch (error) {
377
+ // Not in a git repo or no remote
378
+ }
379
+
380
+ if (isInGitRepo) {
381
+ console.log(`✅ Detected git repository: ${repoUrl}\n`);
382
+
383
+ // Try to detect versions
384
+ try {
385
+ const { newer, older } = await detectVersions('.');
386
+ console.log(`✅ Detected versions:`);
387
+ console.log(` Older: ${older}`);
388
+ console.log(` Newer: ${newer}\n`);
389
+
390
+ console.log('📋 Ready to analyze! Run one of these commands:\n');
391
+ console.log(' # Auto-detect versions and generate all reports:');
392
+ console.log(' dependency-change-report auto --ignore-dev\n');
393
+ console.log(' # Or specify versions explicitly:');
394
+ console.log(` dependency-change-report compare --ignore-dev ${older} ${newer}\n`);
395
+ console.log(' # Generate specific report formats:');
396
+ console.log(` dependency-change-report auto --html --markdown\n`);
397
+
398
+ } catch (error) {
399
+ console.log(`⚠️ Could not auto-detect versions: ${error.message}\n`);
400
+ console.log('📋 Run with explicit versions:\n');
401
+ console.log(' dependency-change-report compare <older-version> <newer-version>\n');
402
+ console.log(' Example:');
403
+ console.log(' dependency-change-report compare v1.0.0 v2.0.0\n');
404
+ }
405
+
406
+ } else {
407
+ console.log('⚠️ Not in a git repository or no remote configured\n');
408
+ console.log('📋 Run from a git repository:\n');
409
+ console.log(' cd /path/to/your/repo');
410
+ console.log(' dependency-change-report auto\n');
411
+ console.log('📋 Or specify a repository URL:\n');
412
+ console.log(' dependency-change-report compare https://github.com/user/repo v1.0.0 v2.0.0\n');
413
+ }
414
+
415
+ console.log('💡 Additional options:');
416
+ console.log(' --ignore-dev Ignore dev dependencies');
417
+ console.log(' --debug-tree Show debug info about dependency filtering');
418
+ console.log(' --output-dir <path> Save reports to specific directory');
419
+ console.log(' --html Generate HTML report only');
420
+ console.log(' --markdown Generate Markdown report only');
421
+ console.log(' --text Generate text report only\n');
422
+
423
+ console.log('📚 For more help:');
424
+ console.log(' dependency-change-report --help');
425
+ };
426
+
427
+ const cmd = command('dependency-change-report', summary('show dependency changes between versions'), compare, auto)
344
428
  const init = async () => {
345
- cmd.parse()
429
+ // If no arguments provided (just the command name), run default action
430
+ if (process.argv.length === 2) {
431
+ await defaultAction();
432
+ } else {
433
+ cmd.parse();
434
+ }
346
435
  }
347
436
 
348
437
  // Run the main function
@@ -112,7 +112,7 @@ const processSingleDependency = async (dep, newerVersionDir, reposDir, multibar,
112
112
  repoUrl: cleanRepoUrl,
113
113
  oldVersion: dep.oldVersion,
114
114
  newVersion: dep.newVersion,
115
- error: "No commits found between versions"
115
+ error: `No commits found between versions ${dep.oldVersion} -> ${dep.newVersion}`
116
116
  };
117
117
  }
118
118
  } catch (error) {
@@ -328,9 +328,10 @@ const collectAllDevDependencies = async (versionDir) => {
328
328
  * @param {string} workingDir - Working directory (optional)
329
329
  * @param {string} namespace - Optional namespace to filter second-level dependencies (e.g., @holepunch)
330
330
  * @param {boolean} ignoreDevDependencies - Whether to ignore devDependencies (optional)
331
+ * @param {boolean} debugTree - Whether to output debug information about dependency tree (optional)
331
332
  * @returns {Promise<Object>} - Analysis report
332
333
  */
333
- export const analyzeDependencyChanges = async (repoUrl, olderVersion, newerVersion, workingDir = process.cwd(), namespace = null, ignoreDevDependencies = false) => {
334
+ export const analyzeDependencyChanges = async (repoUrl, olderVersion, newerVersion, workingDir = process.cwd(), namespace = null, ignoreDevDependencies = false, debugTree = false) => {
334
335
  // Setup signal handlers for graceful shutdown
335
336
  setupSignalHandlers();
336
337
 
@@ -439,23 +440,117 @@ export const analyzeDependencyChanges = async (repoUrl, olderVersion, newerVersi
439
440
  console.log(` ${ignoredDevDependencies.slice(0, 10).join(', ')}${ignoredDevDependencies.length > 10 ? ` ... and ${ignoredDevDependencies.length - 10} more` : ''}`);
440
441
  }
441
442
 
443
+ // Helper function to recursively filter out dev dependencies from nested structures
444
+ const recursivelyFilterDevDeps = (deps, devDeps) => {
445
+ const filtered = {};
446
+ for (const [name, info] of Object.entries(deps)) {
447
+ // Skip if this is a dev dependency
448
+ if (devDeps.has(name)) {
449
+ continue;
450
+ }
451
+
452
+ // Copy the dependency info
453
+ filtered[name] = { ...info };
454
+
455
+ // Recursively filter nested dependencies if they exist
456
+ if (info.dependencies) {
457
+ const filteredNested = recursivelyFilterDevDeps(info.dependencies, devDeps);
458
+ if (Object.keys(filteredNested).length > 0) {
459
+ filtered[name].dependencies = filteredNested;
460
+ } else {
461
+ delete filtered[name].dependencies;
462
+ }
463
+ }
464
+ }
465
+ return filtered;
466
+ };
467
+
442
468
  // Get dependencies for both versions, with namespace filtering for second-level dependencies if specified
443
469
  const olderDeps = await getDependencies(olderVersionDir, namespace);
444
470
  const newerDeps = await getDependencies(newerVersionDir, namespace);
445
471
 
446
- // Filter out dev dependencies if requested
472
+ // Debug output: show dependency tree before filtering
473
+ if (debugTree) {
474
+ console.log('\n=== DEBUG: Dependency Tree BEFORE Filtering ===\n');
475
+ console.log(`Total dependencies in newer version: ${Object.keys(newerDeps).length}`);
476
+
477
+ // Show jest-related dependencies
478
+ const jestRelated = Object.keys(newerDeps).filter(name =>
479
+ name.includes('jest') || name.includes('@jest')
480
+ );
481
+ if (jestRelated.length > 0) {
482
+ console.log(`\nJest-related top-level dependencies (${jestRelated.length}):`);
483
+ jestRelated.forEach(name => {
484
+ const info = newerDeps[name];
485
+ console.log(` - ${name}@${info.version}`);
486
+ if (info.dependencies) {
487
+ const nestedJest = Object.keys(info.dependencies).filter(n =>
488
+ n.includes('jest') || n.includes('@jest')
489
+ );
490
+ if (nestedJest.length > 0) {
491
+ console.log(` Nested jest deps: ${nestedJest.join(', ')}`);
492
+ }
493
+ }
494
+ });
495
+ }
496
+
497
+ // Show all dev dependencies that will be filtered
498
+ if (ignoreDevDependencies && allDevDeps.size > 0) {
499
+ console.log(`\nDev dependencies to filter (${allDevDeps.size}):`);
500
+ const devDepsArray = Array.from(allDevDeps).sort();
501
+ const jestDevDeps = devDepsArray.filter(name =>
502
+ name.includes('jest') || name.includes('@jest')
503
+ );
504
+ if (jestDevDeps.length > 0) {
505
+ console.log(` Jest-related (${jestDevDeps.length}): ${jestDevDeps.slice(0, 20).join(', ')}${jestDevDeps.length > 20 ? ` ... and ${jestDevDeps.length - 20} more` : ''}`);
506
+ }
507
+ console.log(` Total: ${devDepsArray.slice(0, 10).join(', ')}${devDepsArray.length > 10 ? ` ... and ${devDepsArray.length - 10} more` : ''}`);
508
+ }
509
+ }
510
+
511
+ // Filter out dev dependencies if requested (recursively to handle nested deps)
447
512
  const filteredOlderDeps = ignoreDevDependencies ?
448
- Object.fromEntries(Object.entries(olderDeps).filter(([name]) => !allDevDeps.has(name))) :
513
+ recursivelyFilterDevDeps(olderDeps, allDevDeps) :
449
514
  olderDeps;
450
515
  const filteredNewerDeps = ignoreDevDependencies ?
451
- Object.fromEntries(Object.entries(newerDeps).filter(([name]) => !allDevDeps.has(name))) :
516
+ recursivelyFilterDevDeps(newerDeps, allDevDeps) :
452
517
  newerDeps;
453
518
 
519
+ // Debug output: show dependency tree after filtering
520
+ if (debugTree) {
521
+ console.log('\n=== DEBUG: Dependency Tree AFTER Filtering ===\n');
522
+ console.log(`Total dependencies in newer version: ${Object.keys(filteredNewerDeps).length}`);
523
+
524
+ // Show any remaining jest-related dependencies
525
+ const remainingJest = Object.keys(filteredNewerDeps).filter(name =>
526
+ name.includes('jest') || name.includes('@jest')
527
+ );
528
+ if (remainingJest.length > 0) {
529
+ console.log(`\n⚠️ WARNING: Jest-related dependencies still present (${remainingJest.length}):`);
530
+ remainingJest.forEach(name => {
531
+ const info = filteredNewerDeps[name];
532
+ console.log(` - ${name}@${info.version}`);
533
+ if (info.dependencies) {
534
+ const nestedJest = Object.keys(info.dependencies).filter(n =>
535
+ n.includes('jest') || n.includes('@jest')
536
+ );
537
+ if (nestedJest.length > 0) {
538
+ console.log(` Nested jest deps: ${nestedJest.join(', ')}`);
539
+ }
540
+ }
541
+ });
542
+ } else {
543
+ console.log('\n✅ No jest-related dependencies remaining after filtering');
544
+ }
545
+
546
+ console.log('\n=== END DEBUG ===\n');
547
+ }
548
+
454
549
  // Get package changes from package-lock.json if available
455
550
  const { changedPackages: lockFileChanges, packageVersions } = await getPackageLockChanges(olderVersionDir, newerVersionDir);
456
551
 
457
- // Compare dependencies, passing packageVersions for devDep information
458
- const comparison = compareDependencies(filteredOlderDeps, filteredNewerDeps, packageVersions);
552
+ // Compare dependencies, passing packageVersions for devDep information and allDevDeps for filtering
553
+ const comparison = compareDependencies(filteredOlderDeps, filteredNewerDeps, packageVersions, allDevDeps);
459
554
 
460
555
  // Create a combined list of packages to get changelogs for
461
556
  let allChangedPackages = [...comparison.upgraded];
@@ -467,6 +562,11 @@ export const analyzeDependencyChanges = async (repoUrl, olderVersion, newerVersi
467
562
 
468
563
  // Add any packages from lock file that aren't already in our list
469
564
  for (const packageName of lockFileChanges) {
565
+ // Skip if this is a nested dependency path (contains /node_modules/)
566
+ if (packageName.includes('/node_modules/')) {
567
+ continue;
568
+ }
569
+
470
570
  if (!comparison.upgraded.some(dep => dep.name === packageName)) {
471
571
  const versionInfo = packageVersions[packageName];
472
572
 
@@ -5,9 +5,10 @@ import semver from 'semver';
5
5
  * @param {Object} oldDeps - Old dependencies
6
6
  * @param {Object} newDeps - New dependencies
7
7
  * @param {Object} packageVersions - Package version info with devDep data from package-lock parser
8
+ * @param {Set} allDevDeps - Set of all dev dependencies (including nested) to filter out
8
9
  * @returns {Object} - Comparison result
9
10
  */
10
- export const compareDependencies = (oldDeps, newDeps, packageVersions = {}) => {
11
+ export const compareDependencies = (oldDeps, newDeps, packageVersions = {}, allDevDeps = new Set()) => {
11
12
  const added = [];
12
13
  const removed = [];
13
14
  const upgraded = [];
@@ -184,11 +185,39 @@ export const compareDependencies = (oldDeps, newDeps, packageVersions = {}) => {
184
185
 
185
186
  // Process nested dependencies for each top-level dependency
186
187
  for (const [name, info] of Object.entries(newDeps)) {
188
+ // Skip processing nested deps if the parent is a dev dependency
189
+ if (allDevDeps.size > 0 && allDevDeps.has(name)) {
190
+ continue;
191
+ }
192
+
187
193
  if (oldDeps[name] && info.dependencies) {
188
194
  processNestedDependencies(oldDeps[name], info, name);
189
195
  }
190
196
  }
191
197
 
198
+ // Filter out any nested dependencies that are themselves dev dependencies
199
+ if (allDevDeps.size > 0) {
200
+ const filterDevDeps = (deps) => deps.filter(dep => !allDevDeps.has(dep.name));
201
+
202
+ const filteredNestedAdded = filterDevDeps(nestedAdded);
203
+ const filteredNestedRemoved = filterDevDeps(nestedRemoved);
204
+ const filteredNestedUpgraded = filterDevDeps(nestedUpgraded);
205
+ const filteredNestedModified = filterDevDeps(nestedModified);
206
+
207
+ return {
208
+ added,
209
+ removed,
210
+ upgraded,
211
+ modified,
212
+ nested: {
213
+ added: filteredNestedAdded,
214
+ removed: filteredNestedRemoved,
215
+ upgraded: filteredNestedUpgraded,
216
+ modified: filteredNestedModified
217
+ }
218
+ };
219
+ }
220
+
192
221
  return {
193
222
  added,
194
223
  removed,
@@ -126,6 +126,7 @@ export const getCommitHistory = async (repoUrl, oldVersion, newVersion, reposDir
126
126
  errorMessage = 'Operation timed out - repository may be too large or network is slow';
127
127
  }
128
128
 
129
+ console.log(`Final clone failure for ${packageName}: ${errorMessage}`);
129
130
  throw new Error(`${errorMessage} (category: ${errorCategory})`);
130
131
  }
131
132
  };
@@ -303,6 +304,7 @@ export const getCommitHistory = async (repoUrl, oldVersion, newVersion, reposDir
303
304
 
304
305
  // Last resort: if we can't find specific versions, use default branch for newer and first commit for older
305
306
  if (!resolvedOldRef && !resolvedNewRef) {
307
+ console.log(`Could not find version references for ${packageName} (${oldVersion} -> ${newVersion}), using commit range fallback`);
306
308
  try {
307
309
  // Get the first commit
308
310
  const firstCommit = await executeCommand('git', ['rev-list', '--max-parents=0', 'HEAD'], tempDir, time_1min, `git rev-list first commit for ${packageName}`, false);
@@ -312,22 +314,27 @@ export const getCommitHistory = async (repoUrl, oldVersion, newVersion, reposDir
312
314
  const latestCommit = await executeCommand('git', ['rev-parse', 'HEAD'], tempDir, time_1min, `git rev-parse HEAD for ${packageName}`, false);
313
315
  resolvedNewRef = { ref: 'latest-commit', hash: latestCommit.trim() };
314
316
  } catch (error) {
317
+ console.log(`Failed to get fallback commits for ${packageName}: ${error.message}`);
315
318
  return [];
316
319
  }
317
320
  } else if (!resolvedOldRef) {
321
+ console.log(`Could not find old version reference for ${packageName} (${oldVersion}), using first commit`);
318
322
  try {
319
323
  // Get the first commit
320
324
  const firstCommit = await executeCommand('git', ['rev-list', '--max-parents=0', 'HEAD'], tempDir, time_1min, `git rev-list first commit for ${packageName}`, false);
321
325
  resolvedOldRef = { ref: 'first-commit', hash: firstCommit.trim() };
322
326
  } catch (error) {
327
+ console.log(`Failed to get first commit for ${packageName}: ${error.message}`);
323
328
  return [];
324
329
  }
325
330
  } else if (!resolvedNewRef) {
331
+ console.log(`Could not find new version reference for ${packageName} (${newVersion}), using latest commit`);
326
332
  try {
327
333
  // Get the latest commit on default branch
328
334
  const latestCommit = await executeCommand('git', ['rev-parse', 'HEAD'], tempDir, time_1min, `git rev-parse HEAD for ${packageName}`, false);
329
335
  resolvedNewRef = { ref: 'latest-commit', hash: latestCommit.trim() };
330
336
  } catch (error) {
337
+ console.log(`Failed to get latest commit for ${packageName}: ${error.message}`);
331
338
  return [];
332
339
  }
333
340
  }
@@ -360,6 +367,7 @@ export const getCommitHistory = async (repoUrl, oldVersion, newVersion, reposDir
360
367
 
361
368
  let output;
362
369
  try {
370
+ console.log(`Getting commit history for ${packageName}: ${resolvedOldRef.ref}(${resolvedOldRef.hash.substring(0, 7)}) -> ${resolvedNewRef.ref}(${resolvedNewRef.hash.substring(0, 7)})`);
363
371
  output = await executeCommand(
364
372
  'git',
365
373
  ['log', `${resolvedOldRef.hash}..${resolvedNewRef.hash}`, '--pretty=format:%H,%an,%ad,%s'],
@@ -369,17 +377,20 @@ export const getCommitHistory = async (repoUrl, oldVersion, newVersion, reposDir
369
377
  false
370
378
  );
371
379
  } catch (error) {
380
+ console.log(`Git log range failed for ${packageName}: ${error.message}`);
372
381
  // Try with a different approach - get all commits and filter
373
382
  try {
374
383
  output = await executeCommand(
375
384
  'git',
376
- ['log', '--pretty=format:%H,%an,%ad,%s'],
385
+ ['log', '--pretty=format:%H,%an,%ad,%s', '--max-count=50'],
377
386
  tempDir,
378
387
  time_1min,
379
- `git log all for ${packageName}`,
388
+ `git log recent for ${packageName}`,
380
389
  false
381
390
  );
391
+ console.log(`Using recent commits fallback for ${packageName}`);
382
392
  } catch (e) {
393
+ console.log(`All git log attempts failed for ${packageName}: ${e.message}`);
383
394
  return [];
384
395
  }
385
396
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dependency-change-report",
3
- "version": "1.4.9",
3
+ "version": "1.4.11",
4
4
  "type": "module",
5
5
  "description": "Generate dependency change reports between git references",
6
6
  "main": "lib/index.mjs",