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 +112 -15
- package/index.mjs +180 -184
- package/package.json +23 -15
- package/src/ai/explain.ts +29 -0
- package/src/cache/graphCache.ts +49 -0
- package/src/cli.ts +110 -0
- package/src/core/analyzer.ts +28 -0
- package/src/git/getChangedFiles.ts +15 -0
- package/src/graph/buildGraph.ts +151 -0
- package/src/graph/types.ts +5 -0
- package/src/parser/parseJS.ts +69 -0
- package/src/parser/parseVue.ts +23 -0
- package/src/parser/parseVueTemplate.ts +16 -0
- package/src/report/html-generator.ts +343 -0
- package/src/scanner/scanRepo.ts +25 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
const HTML_TEMPLATE = `<!DOCTYPE html>
|
|
5
|
+
<html>
|
|
6
|
+
<head>
|
|
7
|
+
<meta charset="UTF-8" />
|
|
8
|
+
<title>Impact Analysis Report</title>
|
|
9
|
+
<meta
|
|
10
|
+
name="description"
|
|
11
|
+
content="Impact Analysis tool for visualizing code dependency changes and risk. Built by Sushant Thorat."/>
|
|
12
|
+
<meta name="author" content="Sushant Thorat" />
|
|
13
|
+
<script src="https://unpkg.com/cytoscape/dist/cytoscape.min.js"></script>
|
|
14
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
15
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
16
|
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
17
|
+
<style>
|
|
18
|
+
* { box-sizing: border-box; }
|
|
19
|
+
body {
|
|
20
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
21
|
+
display: flex;
|
|
22
|
+
height: 100vh;
|
|
23
|
+
margin: 0;
|
|
24
|
+
padding: 0;
|
|
25
|
+
background: #0d1117;
|
|
26
|
+
color: #c9d1d9;
|
|
27
|
+
}
|
|
28
|
+
#left {
|
|
29
|
+
width: 350px;
|
|
30
|
+
padding: 24px;
|
|
31
|
+
border-right: 1px solid #30363d;
|
|
32
|
+
overflow-y: auto;
|
|
33
|
+
background: #161b22;
|
|
34
|
+
}
|
|
35
|
+
#center {
|
|
36
|
+
flex: 1;
|
|
37
|
+
position: relative;
|
|
38
|
+
background: #0d1117;
|
|
39
|
+
}
|
|
40
|
+
.file-item {
|
|
41
|
+
padding: 16px;
|
|
42
|
+
margin-bottom: 12px;
|
|
43
|
+
background: #0d1117;
|
|
44
|
+
border: 1px solid #30363d;
|
|
45
|
+
border-radius: 8px;
|
|
46
|
+
cursor: pointer;
|
|
47
|
+
transition: all 0.2s ease;
|
|
48
|
+
}
|
|
49
|
+
.file-item:hover {
|
|
50
|
+
background: #161b22;
|
|
51
|
+
border-color: #58a6ff;
|
|
52
|
+
transform: translateX(4px);
|
|
53
|
+
}
|
|
54
|
+
.file-name {
|
|
55
|
+
font-family: 'JetBrains Mono', monospace;
|
|
56
|
+
font-weight: 600;
|
|
57
|
+
color: #58a6ff;
|
|
58
|
+
margin-bottom: 8px;
|
|
59
|
+
word-break: break-word;
|
|
60
|
+
font-size: 13px;
|
|
61
|
+
}
|
|
62
|
+
.risk-badge {
|
|
63
|
+
display: inline-block;
|
|
64
|
+
padding: 4px 10px;
|
|
65
|
+
border-radius: 4px;
|
|
66
|
+
font-size: 11px;
|
|
67
|
+
font-weight: 700;
|
|
68
|
+
letter-spacing: 0.5px;
|
|
69
|
+
text-transform: uppercase;
|
|
70
|
+
margin-bottom: 8px;
|
|
71
|
+
}
|
|
72
|
+
.risk-LOW {
|
|
73
|
+
background: rgba(46, 160, 67, 0.15);
|
|
74
|
+
color: #3fb950;
|
|
75
|
+
border: 1px solid #2ea043;
|
|
76
|
+
}
|
|
77
|
+
.risk-MEDIUM {
|
|
78
|
+
background: rgba(219, 109, 40, 0.15);
|
|
79
|
+
color: #db6d28;
|
|
80
|
+
border: 1px solid #db6d28;
|
|
81
|
+
}
|
|
82
|
+
.risk-HIGH {
|
|
83
|
+
background: rgba(248, 81, 73, 0.15);
|
|
84
|
+
color: #f85149;
|
|
85
|
+
border: 1px solid #f85149;
|
|
86
|
+
}
|
|
87
|
+
.impact-count {
|
|
88
|
+
color: #8b949e;
|
|
89
|
+
font-size: 13px;
|
|
90
|
+
margin-top: 6px;
|
|
91
|
+
display: flex;
|
|
92
|
+
align-items: center;
|
|
93
|
+
gap: 6px;
|
|
94
|
+
}
|
|
95
|
+
.impact-count::before {
|
|
96
|
+
content: "*";
|
|
97
|
+
color: #f85149;
|
|
98
|
+
font-weight: bold;
|
|
99
|
+
}
|
|
100
|
+
.impacted-files {
|
|
101
|
+
max-height: 0;
|
|
102
|
+
overflow: hidden;
|
|
103
|
+
transition: max-height 0.3s ease;
|
|
104
|
+
margin-top: 12px;
|
|
105
|
+
padding-top: 0;
|
|
106
|
+
border-top: 1px solid #30363d;
|
|
107
|
+
}
|
|
108
|
+
.impacted-files.expanded {
|
|
109
|
+
max-height: 500px;
|
|
110
|
+
padding-top: 12px;
|
|
111
|
+
overflow-y: auto;
|
|
112
|
+
}
|
|
113
|
+
.impacted-file {
|
|
114
|
+
font-family: 'JetBrains Mono', monospace;
|
|
115
|
+
font-size: 12px;
|
|
116
|
+
color: #8b949e;
|
|
117
|
+
padding: 6px 12px;
|
|
118
|
+
background: #161b22;
|
|
119
|
+
border-left: 2px solid #30363d;
|
|
120
|
+
margin-bottom: 4px;
|
|
121
|
+
border-radius: 0 4px 4px 0;
|
|
122
|
+
}
|
|
123
|
+
.impacted-file:hover {
|
|
124
|
+
background: #0d1117;
|
|
125
|
+
border-left-color: #58a6ff;
|
|
126
|
+
color: #c9d1d9;
|
|
127
|
+
}
|
|
128
|
+
h2 {
|
|
129
|
+
margin-top: 0;
|
|
130
|
+
color: #c9d1d9;
|
|
131
|
+
font-size: 20px;
|
|
132
|
+
font-weight: 700;
|
|
133
|
+
margin-bottom: 24px;
|
|
134
|
+
padding-bottom: 12px;
|
|
135
|
+
border-bottom: 2px solid #21262d;
|
|
136
|
+
}
|
|
137
|
+
h3 {
|
|
138
|
+
color: #8b949e;
|
|
139
|
+
margin-top: 24px;
|
|
140
|
+
font-size: 14px;
|
|
141
|
+
font-weight: 600;
|
|
142
|
+
text-transform: uppercase;
|
|
143
|
+
letter-spacing: 1px;
|
|
144
|
+
}
|
|
145
|
+
#ai {
|
|
146
|
+
background: #0d1117;
|
|
147
|
+
border: 1px solid #30363d;
|
|
148
|
+
padding: 16px;
|
|
149
|
+
border-radius: 8px;
|
|
150
|
+
white-space: pre-wrap;
|
|
151
|
+
word-wrap: break-word;
|
|
152
|
+
font-family: 'JetBrains Mono', monospace;
|
|
153
|
+
font-size: 12px;
|
|
154
|
+
line-height: 1.6;
|
|
155
|
+
color: #c9d1d9;
|
|
156
|
+
}
|
|
157
|
+
::-webkit-scrollbar { width: 8px; }
|
|
158
|
+
::-webkit-scrollbar-track { background: #0d1117; }
|
|
159
|
+
::-webkit-scrollbar-thumb { background: #30363d; border-radius: 4px; }
|
|
160
|
+
::-webkit-scrollbar-thumb:hover { background: #484f58; }
|
|
161
|
+
.expand-indicator {
|
|
162
|
+
font-size: 10px;
|
|
163
|
+
color: #58a6ff;
|
|
164
|
+
margin-left: 4px;
|
|
165
|
+
transition: transform 0.2s ease;
|
|
166
|
+
}
|
|
167
|
+
.file-item.expanded .expand-indicator {
|
|
168
|
+
transform: rotate(90deg);
|
|
169
|
+
}
|
|
170
|
+
</style>
|
|
171
|
+
</head>
|
|
172
|
+
|
|
173
|
+
<body>
|
|
174
|
+
<div id="left">
|
|
175
|
+
<h2>Impact Analysis Report</h2>
|
|
176
|
+
<div id="list"></div>
|
|
177
|
+
<h3>AI Explanation</h3>
|
|
178
|
+
<pre id="ai">No AI explanation available. Use --ai flag to enable.</pre>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<div id="center"></div>
|
|
182
|
+
|
|
183
|
+
<script>
|
|
184
|
+
const impactData = __IMPACT_DATA__;
|
|
185
|
+
|
|
186
|
+
const elements = [];
|
|
187
|
+
const nodes = new Set();
|
|
188
|
+
|
|
189
|
+
impactData.forEach(item => {
|
|
190
|
+
if (!nodes.has(item.changedFile)) {
|
|
191
|
+
elements.push({
|
|
192
|
+
data: {
|
|
193
|
+
id: item.changedFile,
|
|
194
|
+
label: item.changedFile.split(/[\\\\/]/).pop(),
|
|
195
|
+
type: 'changed'
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
nodes.add(item.changedFile);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
item.impactedFiles.forEach(f => {
|
|
202
|
+
if (!nodes.has(f)) {
|
|
203
|
+
elements.push({
|
|
204
|
+
data: {
|
|
205
|
+
id: f,
|
|
206
|
+
label: f.split(/[\\\\/]/).pop(),
|
|
207
|
+
type: 'impacted'
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
nodes.add(f);
|
|
211
|
+
}
|
|
212
|
+
elements.push({
|
|
213
|
+
data: {
|
|
214
|
+
source: item.changedFile,
|
|
215
|
+
target: f
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
if (elements.length > 0) {
|
|
222
|
+
cytoscape({
|
|
223
|
+
container: document.getElementById('center'),
|
|
224
|
+
elements,
|
|
225
|
+
style: [
|
|
226
|
+
{
|
|
227
|
+
selector: 'node[type="changed"]',
|
|
228
|
+
style: {
|
|
229
|
+
'label': 'data(label)',
|
|
230
|
+
'background-color': '#f85149',
|
|
231
|
+
'color': '#fff',
|
|
232
|
+
'text-valign': 'center',
|
|
233
|
+
'text-halign': 'center',
|
|
234
|
+
'font-family': 'JetBrains Mono, monospace',
|
|
235
|
+
'font-size': '10px',
|
|
236
|
+
'font-weight': 'bold',
|
|
237
|
+
'width': '80px',
|
|
238
|
+
'height': '80px',
|
|
239
|
+
'border-width': 2,
|
|
240
|
+
'border-color': '#ff7b72',
|
|
241
|
+
'text-wrap': 'wrap',
|
|
242
|
+
'text-max-width': '70px'
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
selector: 'node[type="impacted"]',
|
|
247
|
+
style: {
|
|
248
|
+
'label': 'data(label)',
|
|
249
|
+
'background-color': '#388bfd',
|
|
250
|
+
'color': '#fff',
|
|
251
|
+
'text-valign': 'center',
|
|
252
|
+
'text-halign': 'center',
|
|
253
|
+
'font-family': 'JetBrains Mono, monospace',
|
|
254
|
+
'font-size': '9px',
|
|
255
|
+
'font-weight': '500',
|
|
256
|
+
'width': '60px',
|
|
257
|
+
'height': '60px',
|
|
258
|
+
'border-width': 2,
|
|
259
|
+
'border-color': '#58a6ff',
|
|
260
|
+
'text-wrap': 'wrap',
|
|
261
|
+
'text-max-width': '55px'
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
selector: 'edge',
|
|
266
|
+
style: {
|
|
267
|
+
'width': 2,
|
|
268
|
+
'line-color': '#30363d',
|
|
269
|
+
'target-arrow-color': '#58a6ff',
|
|
270
|
+
'target-arrow-shape': 'triangle',
|
|
271
|
+
'curve-style': 'bezier',
|
|
272
|
+
'arrow-scale': 1.2
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
],
|
|
276
|
+
layout: {
|
|
277
|
+
name: 'breadthfirst',
|
|
278
|
+
directed: true,
|
|
279
|
+
spacingFactor: 1.5
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
} else {
|
|
283
|
+
document.getElementById('center').innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:#8b949e;font-size:18px;font-family:JetBrains Mono,monospace;">No dependencies found</div>';
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const list = document.getElementById('list');
|
|
287
|
+
impactData.forEach(item => {
|
|
288
|
+
const div = document.createElement('div');
|
|
289
|
+
div.className = 'file-item';
|
|
290
|
+
const fileName = item.changedFile.split(/[\\\\/]/).pop();
|
|
291
|
+
|
|
292
|
+
const impactedFilesHtml = item.impactedFiles.length > 0
|
|
293
|
+
? \`<div class="impacted-files">
|
|
294
|
+
\${item.impactedFiles.map(f =>
|
|
295
|
+
\`<div class="impacted-file">\${f.split(/[\\\\/]/).pop()}</div>\`
|
|
296
|
+
).join('')}
|
|
297
|
+
</div>\`
|
|
298
|
+
: '';
|
|
299
|
+
|
|
300
|
+
div.innerHTML = \`
|
|
301
|
+
<div class="file-name">\${fileName}</div>
|
|
302
|
+
<span class="risk-badge risk-\${item.risk}">Risk: \${item.risk}</span>
|
|
303
|
+
<div class="impact-count">
|
|
304
|
+
\${item.impactedFiles.length} file(s) impacted
|
|
305
|
+
\${item.impactedFiles.length > 0 ? '<span class="expand-indicator">▸</span>' : ''}
|
|
306
|
+
</div>
|
|
307
|
+
\${impactedFilesHtml}
|
|
308
|
+
\`;
|
|
309
|
+
|
|
310
|
+
if (item.impactedFiles.length > 0) {
|
|
311
|
+
div.addEventListener('click', () => {
|
|
312
|
+
const filesDiv = div.querySelector('.impacted-files');
|
|
313
|
+
filesDiv.classList.toggle('expanded');
|
|
314
|
+
div.classList.toggle('expanded');
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
list.appendChild(div);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
if (impactData.aiExplanation) {
|
|
322
|
+
document.getElementById('ai').innerText = impactData.aiExplanation;
|
|
323
|
+
}
|
|
324
|
+
</script>
|
|
325
|
+
</body>
|
|
326
|
+
</html>`;
|
|
327
|
+
|
|
328
|
+
export function generateHtmlReport(data: any) {
|
|
329
|
+
const outDir = path.resolve('.impact-analysis');
|
|
330
|
+
const jsonPath = path.join(outDir, 'impact.json');
|
|
331
|
+
const htmlPath = path.join(outDir, 'report.html');
|
|
332
|
+
|
|
333
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
334
|
+
fs.writeFileSync(jsonPath, JSON.stringify(data, null, 2));
|
|
335
|
+
|
|
336
|
+
const finalHtml = HTML_TEMPLATE.replace(
|
|
337
|
+
'__IMPACT_DATA__',
|
|
338
|
+
JSON.stringify(data)
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
fs.writeFileSync(htmlPath, finalHtml);
|
|
342
|
+
return htmlPath;
|
|
343
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import fg from "fast-glob";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
export async function scanRepo(): Promise<string[]> {
|
|
5
|
+
try {
|
|
6
|
+
const files = await fg("**/*.{js,jsx,ts,tsx,vue}", {
|
|
7
|
+
ignore: [
|
|
8
|
+
"node_modules/**",
|
|
9
|
+
"dist/**",
|
|
10
|
+
"build/**",
|
|
11
|
+
"*.min.js",
|
|
12
|
+
"**/*.min.js",
|
|
13
|
+
".next/**",
|
|
14
|
+
"coverage/**",
|
|
15
|
+
".cache/**",
|
|
16
|
+
"out/**",
|
|
17
|
+
".nuxt/**"
|
|
18
|
+
]
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
return files.map(file => path.resolve(process.cwd(), file));
|
|
22
|
+
} catch (error) {
|
|
23
|
+
throw new Error(`Failed to scan repository: ${error instanceof Error ? error.message : String(error)}`);
|
|
24
|
+
}
|
|
25
|
+
}
|