impact-analysis 1.0.6 → 2.0.1

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
@@ -1,29 +1,126 @@
1
- # Impact Analysis Tool
1
+ # Impact Analysis
2
2
 
3
- **Impact Analysis** is a tool that analyzes file dependencies in a code repository. It identifies the impact of code changes by generating a dependency tree and presenting the results in both tabular and graphical formats.
4
-
5
- ---
3
+ Analyze the real impact of code changes in JavaScript, TypeScript, React, Vue, and Angular projects.
6
4
 
7
5
  ## Features
8
6
 
9
- - **Impact Analysis:** Identifies dependent files for each changed file.
10
- - **Interactive Graph View:** Visualizes dependencies with an interactive graph.
11
- - **Table View:** Displays results in a clear tabular format.
12
- - **User-Friendly UI:** Toggle between Graph View and Table View.
13
-
14
- ---
7
+ - **Smart Dependency Analysis**
8
+ - **Visual Reports** - Beautiful HTML reports with interactive dependency graphs
9
+ - **Fast with Caching** - Caches dependency graphs for quick re-analysis
10
+ - **AI Explanations** (Optional) - Get AI-powered insights using Google Gemini
11
+ - **React Support** - Full support for .js, .jsx, .ts, .tsx files
12
+ - **Angular Support** - Supports TypeScript components
13
+ - **Vue Support** - Full support for Vue SFCs including `<script>` and `<script setup>`
14
+ - **Risk Scoring** - Automatic LOW/MEDIUM/HIGH risk classification
15
15
 
16
16
  ## Installation
17
17
 
18
- Install globally using npm:
19
-
20
18
  ```bash
21
19
  npm install -g impact-analysis
22
20
  ```
23
- ---
24
21
 
25
- ## Run
22
+ Or use locally in a project:
23
+ ```bash
24
+ npm install --save-dev impact-analysis
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ### Basic Analysis
26
30
 
27
31
  ```bash
32
+ # Analyze changes compared to main branch
28
33
  impact-analysis
29
- ```
34
+
35
+ # Compare against a different branch
36
+ impact-analysis develop
37
+
38
+ # Open HTML report in browser
39
+ impact-analysis --html
40
+
41
+ # Clear cache and rebuild dependency graph
42
+ impact-analysis --clear-cache
43
+ ```
44
+
45
+ ### AI Explanations (If you really want to get an AI powered analysis)
46
+
47
+ To enable AI-powered impact explanations:
48
+
49
+ 1. Get a free API key from [Google AI Studio](https://aistudio.google.com/app/apikey)
50
+ 2. Create a `.env` file in your project root:
51
+ ```bash
52
+ GEMINI_API_KEY=your-api-key-here
53
+ ```
54
+ 3. Run with `--ai` flag:
55
+ ```bash
56
+ impact-analysis --ai
57
+ ```
58
+
59
+ Note: AI features are completely optional. The tool works perfectly without an API key.
60
+
61
+ ## How It Works
62
+
63
+ 1. **Scans** your repository for all `.js`, `.ts`, and `.vue` files
64
+ 2. **Builds** a dependency graph using AST parsing
65
+ 3. **Detects** changed files using Git diff
66
+ 4. **Analyzes** which files import the changed files
67
+ 5. **Generates** an HTML report with interactive visualization
68
+
69
+ ## Supported Import Formats
70
+
71
+ - ES6 imports: `import x from './module'`
72
+ - Dynamic imports: `import('./module')`
73
+ - CommonJS: `require('./module')`
74
+ - Export from: `export { x } from './module'`
75
+ - Export all: `export * from './module'`
76
+ - Path aliases: `@/`, `~/`, `#`, tsconfig paths
77
+
78
+ ## Path Alias Support
79
+
80
+ The tool automatically resolves:
81
+ - `@/components/Header` → `components/Header`
82
+ - `~/utils/helper` → `utils/helper`
83
+ - `#components/Footer` → `components/Footer`
84
+ - Custom tsconfig paths
85
+
86
+ ## Output
87
+
88
+ ### Console Output
89
+ ```
90
+ Scanning repository...
91
+ Building dependency graph from 850 files...
92
+ Dependency graph built: 543 files with dependencies
93
+ Getting changed files against main...
94
+ Found 2 changed files. Analyzing impact...
95
+
96
+ --- Impact Analysis Results ---
97
+
98
+ 1. GlobalNavNuxt.vue
99
+ Risk: HIGH
100
+ Impacted files: 12
101
+ - Header.vue
102
+ - Layout.vue
103
+ - NavBar.vue
104
+ ... and 9 more
105
+
106
+ Generating HTML report...
107
+ Report saved: impact-analysis.html
108
+ ```
109
+
110
+ ### HTML Report
111
+
112
+ Interactive visualization with:
113
+ - **Graph View**: Visual dependency network
114
+ - **Table View**: Detailed file-by-file breakdown
115
+ - Clickable nodes and expandable lists
116
+
117
+ ## Requirements
118
+
119
+ - Node.js 18+
120
+ - Git (for change detection)
121
+ - A Git repository
122
+
123
+ ## License
124
+
125
+ MITbash
126
+ npm install -g impact-analysis
package/index.mjs CHANGED
@@ -149,7 +149,7 @@ function generateGraphData(dependencyData) {
149
149
  if (!nodes.has(impactedFile)) {
150
150
  nodes.set(impactedFile, { id: impactedFileName, path: impactedFile, isChangedFile: false });
151
151
  }
152
- links.push({ id: changedFile, source: changedFile, target: impactedFileName });
152
+ links.push({ source: changedFile, target: impactedFileName });
153
153
  });
154
154
  });
155
155
 
@@ -160,6 +160,7 @@ function generateGraphData(dependencyData) {
160
160
  }
161
161
 
162
162
  const graphData = generateGraphData(results);
163
+
163
164
  const htmlContent = `
164
165
  <!DOCTYPE html>
165
166
  <html lang="en">
@@ -196,23 +197,23 @@ const htmlContent = `
196
197
  }
197
198
  td:first-child {
198
199
  background: #91fd9117;
199
- }
200
- .btn-theme {
201
- min-width: 142px;
202
- padding: 12px 16px;
203
- font-size: 14px;
204
- border-radius: 100px;
205
- border: 1px solid #565454;
206
- outline: none;
207
- box-shadow: none;
208
- cursor: pointer;
209
- transition: all .3s ease-in-out;
210
- text-decoration: none;
211
- display: inline-flex;
212
- align-items: center;
213
- font-family: "Exo 2", serif;
214
- background: #222;
215
- color: #fff;
200
+ }
201
+ .btn-theme {
202
+ min-width: 142px;
203
+ padding: 12px 16px;
204
+ font-size: 14px;
205
+ border-radius: 100px;
206
+ border: 1px solid #565454;
207
+ outline: none;
208
+ box-shadow: none;
209
+ cursor: pointer;
210
+ transition: all .3s ease-in-out;
211
+ text-decoration: none;
212
+ display: inline-flex;
213
+ align-items: center;
214
+ font-family: "Exo 2", serif;
215
+ background: #222;
216
+ color: #fff;
216
217
  }
217
218
 
218
219
  .btn-active {
@@ -221,40 +222,36 @@ const htmlContent = `
221
222
  color: #fff
222
223
  }
223
224
 
224
- .btn-svg path {
225
- fill: #fefefe;
226
- }
227
-
228
225
  .btn-active:hover svg path {
229
226
  fill: #1f4afe;
230
227
  }
231
228
 
232
- .btn-theme:hover svg path {
233
- fill: #1f4afe;
234
- }
235
-
236
229
  .btn-theme:hover {
237
230
  background: #fff;
238
231
  color: #1f4afe;
239
232
  border: 1px solid #1F4AFE
240
- }
233
+ }
241
234
 
242
- .btn-svg {
243
- width: 20px;
244
- height: 20px;
245
- margin-right: 10px;
246
- }
235
+ .btn-svg {
236
+ width: 20px;
237
+ height: 20px;
238
+ margin-right: 10px;
239
+ }
247
240
 
248
- th {
249
- background: #1f4afe;
250
- background: linear-gradient(90deg,#1f4afe 35%,#167cfc);
251
- width: 50%;
252
- }
241
+ .btn-svg path {
242
+ fill: #fefefe;
243
+ }
244
+
245
+ th {
246
+ background: #1f4afe;
247
+ background: linear-gradient(90deg,#1f4afe 35%,#167cfc);
248
+ }
249
+
250
+ th:last-child {
251
+ background: #1f4afe;
252
+ background: linear-gradient(-90deg,#1f4afe 35%,#167cfc);
253
+ }
253
254
 
254
- th:last-child {
255
- background: #1f4afe;
256
- background: linear-gradient(-90deg,#1f4afe 35%,#167cfc);
257
- }
258
255
  </style>
259
256
  </head>
260
257
  <body>
@@ -295,161 +292,159 @@ const htmlContent = `
295
292
 
296
293
 
297
294
  <script>
298
- const impactData = ${JSON.stringify(dependencyJson)};
299
- function getFileName(path) {
300
- return path.split(/[/\\\\\\\\]/).pop();
301
- }
302
-
303
- const nodesMap = new Map();
304
- impactData.forEach(d => {
305
- nodesMap.set(d.changedFile, { id: d.changedFile, name: getFileName(d.changedFile), isChangedFile: true });
306
- d.impactsTo.forEach(target => {
307
- nodesMap.set(target, { id: target, name: getFileName(target), isChangedFile: false });
308
- });
309
- });
310
- const nodes = Array.from(nodesMap.values());
311
- const links = [];
312
- const impactMap = new Map();
313
-
314
- impactData.forEach(d => {
315
- d.impactsTo.forEach(target => {
316
- links.push({ source: d.changedFile, target });
317
- if (!impactMap.has(target)) impactMap.set(target, new Set());
318
- impactMap.get(target).add(d.changedFile);
319
- });
320
- });
321
-
322
- impactMap.forEach((sources, target) => {
323
- sources.forEach(source1 => {
324
- sources.forEach(source2 => {
325
- if (source1 !== source2) {
326
- links.push({ source: source1, target: source2 });
327
- }
328
- });
329
- });
330
- });
295
+ const graph = ${JSON.stringify(graphData)};
296
+
297
+ const width = window.innerWidth;
298
+ const height = window.innerHeight - 100;
299
+
300
+ const svg = d3.select("#svgGraph")
301
+ .attr("width", width)
302
+ .attr("height", height);
303
+
304
+ const simulation = d3.forceSimulation(graph.nodes)
305
+ .force("link", d3.forceLink(graph.links).id(d => d.id).distance(150)) // Increased distance for better spacing
306
+ .force("charge", d3.forceManyBody().strength(-200)) // Stronger repulsion to avoid overlap
307
+ .force("center", d3.forceCenter(width / 2, height / 2)) // Centering the graph
308
+ .force("collision", d3.forceCollide().radius(20)) // Prevents overlap
309
+ .on("tick", ticked);
310
+
311
+ const link = svg.selectAll("line")
312
+ .data(graph.links)
313
+ .enter().append("line");
314
+
315
+ const node = svg.selectAll("circle")
316
+ .data(graph.nodes)
317
+ .enter().append("circle")
318
+ .attr("r", d => d.isChangedFile ? 18 : 12) // Slightly larger nodes
319
+ .attr("fill", d => d.isChangedFile ? "#ff4500" : "#007bff") // Bright orange for changedFile, blue for others
320
+ .style("stroke", "#fff") // White border for clarity
321
+ .style("stroke-width", 2)
322
+ .call(drag(simulation));
323
+
324
+ const tooltip = d3.select("#tooltip");
325
+
326
+ node.on("mouseover", (event, d) => {
327
+ tooltip.style("display", "block")
328
+ .html(\`<strong>\${d.isChangedFile ? d.id : d.path}</strong>\`)
329
+ .style("left", (event.pageX + 10) + "px")
330
+ .style("top", (event.pageY - 10) + "px");
331
+ }).on("mouseout", () => tooltip.style("display", "none"));
332
+
333
+ svg.selectAll("text")
334
+ .data(graph.nodes)
335
+ .enter().append("text")
336
+ .attr("dy", 4)
337
+ .attr("x", 12)
338
+ .text(d => d.isChangedFile ? d.name : d.id);
339
+
340
+ simulation.on("tick", () => {
341
+ link.attr("x1", d => clamp(d.source.x, 0, width))
342
+ .attr("y1", d => clamp(d.source.y, 0, height))
343
+ .attr("x2", d => clamp(d.target.x, 0, width))
344
+ .attr("y2", d => clamp(d.target.y, 0, height));
345
+
346
+ node.attr("cx", d => clamp(d.x, 10, width - 10))
347
+ .attr("cy", d => clamp(d.y, 10, height - 10));
348
+
349
+ svg.selectAll("text")
350
+ .attr("x", d => clamp(d.x + 20, 10, width - 10))
351
+ .attr("y", d => clamp(d.y + 6, 10, height - 10));
352
+ });
331
353
 
332
- const graph = { nodes, links };
333
-
334
- const width = window.innerWidth;
335
- const height = window.innerHeight - 100;
336
-
337
- const svg = d3.select("#svgGraph")
338
- .attr("width", width)
339
- .attr("height", height);
340
-
341
- const simulation = d3.forceSimulation(graph.nodes)
342
- .force("link", d3.forceLink(graph.links)
343
- .id(d => d.id)
344
- .distance(80)
345
- )
346
- .force("charge", d3.forceManyBody()
347
- .strength(-50)
348
- )
349
- .force("center", d3.forceCenter(width / 2, height / 2))
350
- .force("collision", d3.forceCollide()
351
- .radius(35)
352
- )
353
- .on("tick", ticked);
354
-
355
- const link = svg.append("g")
356
- .selectAll("line")
357
- .data(graph.links)
358
- .enter().append("line")
359
- .attr("stroke", "#aaa");
360
-
361
- const nodeGroup = svg.append("g")
362
- .selectAll("g")
363
- .data(graph.nodes)
364
- .enter().append("g")
365
- .call(d3.drag()
366
- .on("start", dragStarted)
367
- .on("drag", dragged)
368
- .on("end", dragEnded));
369
-
370
- const node = nodeGroup.append("circle")
371
- .attr("r", 10)
372
- .attr("fill", d => d.isChangedFile ? "red" : "steelblue");
373
-
374
- const labels = nodeGroup.append("text")
375
- .attr("x", 12)
376
- .attr("y", 5)
377
- .text(d => d.name);
378
-
379
- simulation.on("tick", () => {
380
- link.attr("x1", d => d.source.x)
381
- .attr("y1", d => d.source.y)
382
- .attr("x2", d => d.target.x)
383
- .attr("y2", d => d.target.y);
384
-
385
- nodeGroup.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
386
- });
387
-
388
- function dragStarted(event, d) {
389
- if (!event.active) simulation.alphaTarget(0.3).restart();
390
- d.fx = d.x;
391
- d.fy = d.y;
392
- }
354
+ // Helper function to keep nodes within boundaries
355
+ function clamp(value, min, max) {
356
+ return Math.max(min, Math.min(max, value));
357
+ }
393
358
 
394
- function dragged(event, d) {
395
- d.fx = event.x;
396
- d.fy = event.y;
397
- }
359
+ function drag(simulation) {
360
+ function dragstarted(event, d) {
361
+ if (!event.active) simulation.alphaTarget(0.3).restart();
362
+ d.fx = d.x;
363
+ d.fy = d.y;
364
+ }
398
365
 
399
- function dragEnded(event, d) {
400
- if (!event.active) simulation.alphaTarget(0);
401
- d.fx = null;
402
- d.fy = null;
403
- }
366
+ function dragged(event, d) {
367
+ d.fx = clamp(event.x, 10, width - 10);
368
+ d.fy = clamp(event.y, 10, height - 10);
369
+ }
404
370
 
405
- document.getElementById("graphViewBtn").addEventListener("click", function () {
406
- document.getElementById("graphView").style.display = "block";
407
- document.getElementById("tableView").style.display = "none";
408
- setActiveButton(this);
409
- });
371
+ function dragended(event, d) {
372
+ if (!event.active) simulation.alphaTarget(0);
373
+ d.fx = null;
374
+ d.fy = null;
375
+ }
410
376
 
411
- document.getElementById("tableViewBtn").addEventListener("click", function () {
412
- document.getElementById("graphView").style.display = "none";
413
- document.getElementById("tableView").style.display = "block";
414
- setActiveButton(this);
415
- generateTableView(impactData);
416
- });
377
+ return d3.drag()
378
+ .on("start", dragstarted)
379
+ .on("drag", dragged)
380
+ .on("end", dragended);
381
+ }
382
+ function ticked() {
383
+ link
384
+ .attr("x1", d => d.source.x)
385
+ .attr("y1", d => d.source.y)
386
+ .attr("x2", d => d.target.x)
387
+ .attr("y2", d => d.target.y);
388
+
389
+ node
390
+ .attr("cx", d => d.x)
391
+ .attr("cy", d => d.y);
392
+
393
+ label
394
+ .attr("x", d => d.x + 10)
395
+ .attr("y", d => d.y + 5);
396
+ }
417
397
 
418
- function setActiveButton(activeBtn) {
419
- document.querySelectorAll(".btn-theme").forEach(button => {
420
- button.classList.remove("btn-active");
421
- });
422
- activeBtn.classList.add("btn-active");
423
- }
398
+ document.getElementById("graphViewBtn").addEventListener("click", function () {
399
+ document.getElementById("graphView").style.display = "block";
400
+ document.getElementById("tableView").style.display = "none";
401
+ setActiveButton(this);
402
+ });
403
+
404
+ document.getElementById("tableViewBtn").addEventListener("click", function () {
405
+ document.getElementById("graphView").style.display = "none";
406
+ document.getElementById("tableView").style.display = "block";
407
+ setActiveButton(this);
408
+ generateTableView(${JSON.stringify(dependencyJson)});
409
+ });
410
+
411
+ // Function to highlight the active button
412
+ function setActiveButton(activeBtn) {
413
+ document.querySelectorAll(".btn-theme").forEach(button => {
414
+ button.classList.remove("btn-active");
415
+ });
416
+ activeBtn.classList.add("btn-active");
417
+ }
424
418
 
425
- function generateTableView(data) {
426
- const tableBody = document.getElementById("tableBody");
427
- tableBody.innerHTML = "";
419
+ // Function to generate the table
420
+ function generateTableView(data) {
421
+ const tableData = data;
422
+ var tableBody = document.getElementById("tableBody");
423
+ tableBody.innerHTML = ""; // Clear previous content
428
424
 
429
- data.forEach(row => {
430
- const tr = document.createElement("tr");
425
+ // Ensure tableData is an array
426
+ if (!Array.isArray(tableData) || tableData.length === 0) {
427
+ tableBody.innerHTML = '<tr><td colspan="2">No data available</td></tr>';
428
+ return;
429
+ }
431
430
 
432
- const tdFile = document.createElement("td");
433
- tdFile.textContent = row.changedFile;
434
- tr.appendChild(tdFile);
431
+ // Populate table
432
+ tableData.forEach(function (result) {
433
+ var row = document.createElement("tr");
435
434
 
436
- const tdImpact = document.createElement("td");
437
- tdImpact.textContent = row.impactsTo.length ? row.impactsTo.join(", ") : "No Impact";
438
- tr.appendChild(tdImpact);
435
+ var changedFileCell = document.createElement("td");
436
+ changedFileCell.textContent = result.changedFile;
439
437
 
440
- tableBody.appendChild(tr);
441
- });
442
- }
443
- function ticked() {
444
- node.attr("cx", d => d.x = Math.max(20, Math.min(width - 20, d.x)))
445
- .attr("cy", d => d.y = Math.max(20, Math.min(height - 20, d.y)));
446
-
447
- link.attr("x1", d => d.source.x)
448
- .attr("y1", d => d.source.y)
449
- .attr("x2", d => d.target.x)
450
- .attr("y2", d => d.target.y);
451
- }
438
+ var impactsToCell = document.createElement("td");
439
+ impactsToCell.innerHTML = result.impactsTo.map(function (file) {
440
+ return '<div class="impact-file-list">' + file + '</div>';
441
+ }).join("");
452
442
 
443
+ row.appendChild(changedFileCell);
444
+ row.appendChild(impactsToCell);
445
+ tableBody.appendChild(row);
446
+ });
447
+ }
453
448
 
454
449
 
455
450
  </script>
@@ -457,5 +452,6 @@ const htmlContent = `
457
452
  </html>
458
453
  `;
459
454
 
455
+ // Write HTML file
460
456
  fs.writeFileSync('impact-analysis.html', htmlContent);
461
457
  console.log('✅ Dependency graph generated: impact-analysis.html');
package/package.json CHANGED
@@ -1,23 +1,31 @@
1
1
  {
2
2
  "name": "impact-analysis",
3
- "version": "1.0.6",
4
- "description": "Tool to analyze code impact based on changed files and dependencies.",
5
- "main": "index.js",
6
- "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
8
- },
3
+ "version": "2.0.1",
4
+ "description": "Analyze real impact of code changes using dependency graphs",
9
5
  "bin": {
10
- "impact-analysis": "./index.mjs"
6
+ "impact-analysis": "dist/cli.js"
7
+ },
8
+ "main": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "ts-node src/cli.ts"
11
13
  },
12
- "keywords": [
13
- "impact",
14
- "analysis",
15
- "dependency",
16
- "npm"
17
- ],
18
- "author": "Sushant Keshav Thorat",
19
14
  "license": "MIT",
20
15
  "dependencies": {
21
- "simple-git": "^3.27.0"
16
+ "@babel/parser": "^7.24.0",
17
+ "@babel/traverse": "^7.24.0",
18
+ "@google/generative-ai": "^0.24.1",
19
+ "@vue/compiler-sfc": "^3.4.0",
20
+ "commander": "^11.0.0",
21
+ "dotenv": "^17.2.3",
22
+ "fast-glob": "^3.3.2",
23
+ "simple-git": "^3.22.0"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^20.11.0",
27
+ "parse-json": "^8.3.0",
28
+ "ts-node": "^10.9.2",
29
+ "typescript": "^5.4.0"
22
30
  }
23
31
  }
@@ -0,0 +1,29 @@
1
+ import { GoogleGenerativeAI } from '@google/generative-ai';
2
+
3
+ export async function explainImpact(results: any[]) {
4
+ if (!process.env.GEMINI_API_KEY) {
5
+ console.log('No GEMINI_API_KEY found. Skipping AI explanation.');
6
+ return null;
7
+ }
8
+
9
+ try {
10
+ const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);
11
+ const model = genAI.getGenerativeModel({ model: 'gemini-3-flash-preview' });
12
+
13
+ const prompt = `Analyze these code changes and explain their impact:
14
+
15
+ ${JSON.stringify(results.map(r => ({
16
+ file: r.changedFile,
17
+ impactedFiles: r.impactedFiles.length,
18
+ risk: r.risk
19
+ })), null, 2)}
20
+
21
+ Provide a brief summary of the overall risk and key concerns.`;
22
+
23
+ const result = await model.generateContent(prompt);
24
+ const response = await result.response;
25
+ return response.text();
26
+ } catch (error) {
27
+ throw new Error(`AI explanation failed: ${error instanceof Error ? error.message : String(error)}`);
28
+ }
29
+ }