impact-analysis 1.0.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.
Files changed (3) hide show
  1. package/README.md +21 -0
  2. package/index.mjs +457 -0
  3. package/package.json +23 -0
package/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # Impact Analysis Tool
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
+ ---
6
+
7
+ ## Features
8
+
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
+ ---
15
+
16
+ ## Installation
17
+
18
+ Install globally using npm:
19
+
20
+ ```bash
21
+ npm install -g impact-analysis
package/index.mjs ADDED
@@ -0,0 +1,457 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import simpleGit from 'simple-git';
5
+
6
+ const git = simpleGit();
7
+
8
+ function readFile(filePath) {
9
+ return fs.readFileSync(filePath, 'utf8');
10
+ }
11
+
12
+ function extractDependencies(content, filePath, baseDir) {
13
+ const dependencies = [];
14
+ const importRegex = /import\s+.*\s+from\s+['"]([^'"]+)['"]/g;
15
+
16
+ let match;
17
+ while ((match = importRegex.exec(content))) {
18
+ const importPath = match[1];
19
+ const resolvedPath = resolveImportPath(importPath, filePath, baseDir);
20
+ if (resolvedPath) {
21
+ dependencies.push(resolvedPath);
22
+ }
23
+ }
24
+
25
+ return dependencies;
26
+ }
27
+
28
+ function resolveImportPath(importPath, filePath, baseDir) {
29
+ if (importPath.startsWith('@/')) {
30
+ return path.resolve(baseDir, importPath.substring(2));
31
+ }
32
+ if (importPath.startsWith('./') || importPath.startsWith('../')) {
33
+ return path.resolve(path.dirname(filePath), importPath);
34
+ }
35
+ const absolutePath = path.resolve(baseDir, importPath);
36
+ if (fs.existsSync(absolutePath)) {
37
+ return absolutePath;
38
+ }
39
+ return null;
40
+ }
41
+
42
+ function getAllFiles(dirPath, arrayOfFiles = []) {
43
+ const files = fs.readdirSync(dirPath);
44
+
45
+ files.forEach(file => {
46
+ const fullPath = path.join(dirPath, file);
47
+ if (fs.statSync(fullPath).isDirectory()) {
48
+ getAllFiles(fullPath, arrayOfFiles);
49
+ } else if (/\.(js|ts|vue)$/.test(file)) {
50
+ arrayOfFiles.push(fullPath);
51
+ }
52
+ });
53
+
54
+ return arrayOfFiles;
55
+ }
56
+
57
+ async function buildDependencyMap(baseDir) {
58
+ const dependencyMap = {};
59
+
60
+ try {
61
+ const allFiles = getAllFiles(baseDir);
62
+
63
+ for (const file of allFiles) {
64
+ const code = readFile(file);
65
+ const dependencies = extractDependencies(code, file, baseDir);
66
+ dependencyMap[file] = dependencies;
67
+ }
68
+
69
+ return dependencyMap;
70
+ } catch (error) {
71
+ console.error('Error building dependency map:', error);
72
+ throw error;
73
+ }
74
+ }
75
+
76
+ async function generateImpactList(baseDir) {
77
+ const impactMap = {};
78
+
79
+ try {
80
+ const dependencyMap = await buildDependencyMap(baseDir);
81
+
82
+ const status = await git.status();
83
+ const changedFiles = status.modified
84
+ .map(file => path.resolve(baseDir, file))
85
+ .filter(file => /\.(js|ts|vue)$/.test(file));
86
+
87
+ const reverseDependencyMap = {};
88
+ Object.keys(dependencyMap).forEach(file => {
89
+ dependencyMap[file].forEach(importPath => {
90
+ if (!reverseDependencyMap[importPath]) {
91
+ reverseDependencyMap[importPath] = new Set();
92
+ }
93
+ reverseDependencyMap[importPath].add(file);
94
+ });
95
+ });
96
+ changedFiles.forEach(changedFile => {
97
+ impactMap[changedFile] = [];
98
+ for (const [file, dependencies] of Object.entries(dependencyMap)) {
99
+ if (dependencies.includes(changedFile)) {
100
+ impactMap[changedFile].push(file);
101
+ }
102
+ }
103
+ });
104
+ const createImpactJson = (changedFiles, dependencyMap) => {
105
+ const result = {};
106
+
107
+ const getLastPart = (path) => path.split('\\').pop().split('/').pop().replace(/\.[^/.]+$/, "");
108
+
109
+ changedFiles.forEach(changedFile => {
110
+ const changedFileName = getLastPart(changedFile);
111
+ result[changedFile] = [];
112
+
113
+ for (const [file, dependencies] of Object.entries(dependencyMap)) {
114
+ if (dependencies.some(dep => getLastPart(dep) === changedFileName)) {
115
+ if (!result[changedFile].includes(file)) {
116
+ result[changedFile].push(file);
117
+ }
118
+ }
119
+ }
120
+ });
121
+
122
+ return Object.entries(result).map(([changedFile, impactsTo]) => ({
123
+ changedFile,
124
+ impactsTo
125
+ }));
126
+ };
127
+
128
+ const impactJson = createImpactJson(changedFiles, dependencyMap);
129
+ return impactJson;
130
+ } catch (error) {
131
+ console.error('Error generating impact list:', error);
132
+ }
133
+ }
134
+
135
+ const baseDir = process.cwd();
136
+ const dependencyJson = await generateImpactList(baseDir);
137
+ let results = dependencyJson;
138
+ function generateGraphData(dependencyData) {
139
+ const nodes = new Map();
140
+ const links = [];
141
+
142
+ dependencyData.forEach(({ changedFile, impactsTo }) => {
143
+ const changedFileName = path.basename(changedFile);
144
+ if (!nodes.has(changedFile)) {
145
+ nodes.set(changedFile, { id: changedFile, name: changedFileName, isChangedFile: true });
146
+ }
147
+ impactsTo.forEach(impactedFile => {
148
+ const impactedFileName = path.basename(impactedFile);
149
+ if (!nodes.has(impactedFile)) {
150
+ nodes.set(impactedFile, { id: impactedFileName, path: impactedFile, isChangedFile: false });
151
+ }
152
+ links.push({ source: changedFile, target: impactedFileName });
153
+ });
154
+ });
155
+
156
+ return {
157
+ nodes: Array.from(nodes.values()),
158
+ links
159
+ };
160
+ }
161
+
162
+ const graphData = generateGraphData(results);
163
+
164
+ const htmlContent = `
165
+ <!DOCTYPE html>
166
+ <html lang="en">
167
+ <head>
168
+ <meta charset="UTF-8">
169
+ <title>Impact Analysis</title>
170
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
171
+ <link rel="preconnect" href="https://fonts.googleapis.com">
172
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
173
+ <link href="https://fonts.googleapis.com/css2?family=Exo+2:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
174
+ <style>
175
+ body { font-family: "Exo 2", serif; background-color: #282828; color: #fff; overflow-x: hidden;
176
+ font-optical-sizing: auto; margin: 0; padding: 0; text-align: center; font-weight: 400;}
177
+ svg { width: 100%; height: 100vh; }
178
+ circle { cursor: pointer; }
179
+ text { font-size: 12px; fill: #fff; }
180
+ line { stroke: #fefefe; stroke-opacity: 0.6; }
181
+ #tooltip { position: absolute; background: #fff;color: #000; padding: 8px; border: 1px solid #ddd; display: none; }
182
+ .svg-wrapper{ padding: 10px; }
183
+ .impact-file-list {
184
+ display: list-item;
185
+ list-style: number;
186
+ text-align: left;
187
+ padding-left: 7px;
188
+ }
189
+ .impact-analysis-table {
190
+ width: 100%; border-collapse: collapse;
191
+ }
192
+ .impact-analysis-table td {
193
+ padding: 10px 20px;
194
+ }
195
+ th {
196
+ padding: 10px;
197
+ }
198
+ td:first-child {
199
+ background: #91fd9117;
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;
217
+ }
218
+
219
+ .btn-active {
220
+ background: #1f4afe;
221
+ background: linear-gradient(90deg,#1f4afe 35%,#167cfc);
222
+ color: #fff
223
+ }
224
+
225
+ .btn-active:hover svg path {
226
+ fill: #1f4afe;
227
+ }
228
+
229
+ .btn-theme:hover {
230
+ background: #fff;
231
+ color: #1f4afe;
232
+ border: 1px solid #1F4AFE
233
+ }
234
+
235
+ .btn-svg {
236
+ width: 20px;
237
+ height: 20px;
238
+ margin-right: 10px;
239
+ }
240
+
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
+ }
254
+
255
+ </style>
256
+ </head>
257
+ <body>
258
+ <h1>Impact Analysis</h1>
259
+ <div id="tooltip"></div>
260
+ <div style="text-align: center; margin-bottom: 10px;">
261
+ <button id="graphViewBtn" class="btn-theme btn-active"><svg class="btn-svg" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 256 256" enable-background="new 0 0 256 256" xml:space="preserve">
262
+ <g><g><path fill="#000000" d="M224.5,7.9c-11.8,0-21.5,9.6-21.5,21.5c0,7.4,3.8,14,9.6,17.9l-40.5,106.9c-1.1-0.2-2.3-0.3-3.4-0.3c-5.2,0-9.8,1.9-13.5,4.9l-40.6-33.5c1.6-3,2.6-6.4,2.6-10c0-11.8-9.6-21.5-21.5-21.5c-11.8,0-21.5,9.6-21.5,21.5c0,6.4,2.9,12.1,7.3,16l-43.3,75.3c-2.2-0.7-4.5-1.3-6.9-1.3c-11.8,0-21.5,9.6-21.5,21.5c0,11.8,9.6,21.5,21.5,21.5c11.8,0,21.5-9.6,21.5-21.5c0-6.3-2.8-11.9-7.1-15.8l43.3-75.4c2.1,0.7,4.3,1.2,6.7,1.2c5.1,0,9.7-1.9,13.4-4.8l40.6,33.6c-1.6,3-2.5,6.3-2.5,9.9c0,11.8,9.6,21.5,21.5,21.5c11.8,0,21.5-9.6,21.5-21.5c0-7.6-4-14.3-10-18.1l40.5-106.7c1.3,0.2,2.6,0.4,3.9,0.4c11.8,0,21.5-9.6,21.5-21.5C246,17.5,236.4,7.9,224.5,7.9L224.5,7.9z M31.5,239.6c-7.1,0-12.9-5.8-12.9-12.9c0-7.1,5.8-12.9,12.9-12.9c7.1,0,12.9,5.8,12.9,12.9C44.3,233.8,38.6,239.6,31.5,239.6z M95.7,128c-7.1,0-12.9-5.8-12.9-12.9c0-7.1,5.8-12.9,12.9-12.9c7.1,0,12.9,5.8,12.9,12.9C108.6,122.3,102.8,128,95.7,128z M168.7,188.1c-7.1,0-12.9-5.8-12.9-12.9c0-7.1,5.8-12.9,12.9-12.9c7.1,0,12.9,5.8,12.9,12.9C181.6,182.3,175.8,188.1,168.7,188.1L168.7,188.1z M224.5,42.2c-7.1,0-12.9-5.8-12.9-12.9c0-7.1,5.8-12.9,12.9-12.9c7.1,0,12.9,5.8,12.9,12.9C237.4,36.5,231.6,42.2,224.5,42.2L224.5,42.2z"/></g></g>
263
+ </svg> Graph View</button>
264
+ <button id="tableViewBtn" class="btn-theme"><svg class="btn-svg" width="12px" height="12px" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
265
+ </defs>
266
+ <g id="64px-Line" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
267
+ <g id="db-table">
268
+
269
+ </g>
270
+ <path d="M56,12 C56,9.794 54.206,8 52,8 L10,8 C7.794,8 6,9.794 6,12 L6,54 C6,56.206 7.794,58 10,58 L52,58 C54.206,58 56,56.206 56,54 L56,12 L56,12 Z M30,22 L30,32 L20,32 L20,22 L30,22 L30,22 Z M42,22 L42,32 L32,32 L32,22 L42,22 L42,22 Z M54,22 L54,32 L44,32 L44,22 L54,22 L54,22 Z M18,32 L8,32 L8,22 L18,22 L18,32 L18,32 Z M8,34 L18,34 L18,44 L8,44 L8,34 L8,34 Z M20,34 L30,34 L30,44 L20,44 L20,34 L20,34 Z M30,46 L30,56 L20,56 L20,46 L30,46 L30,46 Z M32,46 L42,46 L42,56 L32,56 L32,46 L32,46 Z M32,44 L32,34 L42,34 L42,44 L32,44 L32,44 Z M44,34 L54,34 L54,44 L44,44 L44,34 L44,34 Z M10,10 L52,10 C53.103,10 54,10.897 54,12 L54,20 L8,20 L8,12 C8,10.897 8.897,10 10,10 L10,10 Z M8,54 L8,46 L18,46 L18,56 L10,56 C8.897,56 8,55.103 8,54 L8,54 Z M52,56 L44,56 L44,46 L54,46 L54,54 C54,55.103 53.103,56 52,56 L52,56 Z" id="Shape" fill="#000000">
271
+
272
+ </path>
273
+ </g>
274
+ </svg> Table View</button>
275
+ </div>
276
+
277
+ <div id="graphView" style="display: block;">
278
+ <svg id="svgGraph" width="1000" height="600"></svg>
279
+ </div>
280
+
281
+ <div id="tableView" style="display: none;padding: 20px;">
282
+ <table class="impact-analysis-table" border="1">
283
+ <thead>
284
+ <tr>
285
+ <th>Changed File</th>
286
+ <th>Impacted Files</th>
287
+ </tr>
288
+ </thead>
289
+ <tbody id="tableBody"></tbody>
290
+ </table>
291
+ </div>
292
+
293
+
294
+ <script>
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
+ });
353
+
354
+ // Helper function to keep nodes within boundaries
355
+ function clamp(value, min, max) {
356
+ return Math.max(min, Math.min(max, value));
357
+ }
358
+
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
+ }
365
+
366
+ function dragged(event, d) {
367
+ d.fx = clamp(event.x, 10, width - 10);
368
+ d.fy = clamp(event.y, 10, height - 10);
369
+ }
370
+
371
+ function dragended(event, d) {
372
+ if (!event.active) simulation.alphaTarget(0);
373
+ d.fx = null;
374
+ d.fy = null;
375
+ }
376
+
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
+ }
397
+
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
+ }
418
+
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
424
+
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
+ }
430
+
431
+ // Populate table
432
+ tableData.forEach(function (result) {
433
+ var row = document.createElement("tr");
434
+
435
+ var changedFileCell = document.createElement("td");
436
+ changedFileCell.textContent = result.changedFile;
437
+
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("");
442
+
443
+ row.appendChild(changedFileCell);
444
+ row.appendChild(impactsToCell);
445
+ tableBody.appendChild(row);
446
+ });
447
+ }
448
+
449
+
450
+ </script>
451
+ </body>
452
+ </html>
453
+ `;
454
+
455
+ // Write HTML file
456
+ fs.writeFileSync('impact-analysis.html', htmlContent);
457
+ console.log('✅ Dependency graph generated: impact-analysis.html');
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "impact-analysis",
3
+ "version": "1.0.0",
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
+ },
9
+ "bin": {
10
+ "impact-analysis": "./index.mjs"
11
+ },
12
+ "keywords": [
13
+ "impact",
14
+ "analysis",
15
+ "dependency",
16
+ "npm"
17
+ ],
18
+ "author": "Sushant Keshav Thorat",
19
+ "license": "ISC",
20
+ "dependencies": {
21
+ "simple-git": "^3.27.0"
22
+ }
23
+ }