react-code-smell-detector 1.4.1 → 1.4.2

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
@@ -21,6 +21,9 @@ A CLI tool that analyzes React projects and detects common code smells, providin
21
21
  - 🗑️ **Unused Code Detection**: Find unused exports and dead imports
22
22
  - 📈 **Baseline Tracking**: Track code smell trends over time with git commit history
23
23
  - 💬 **Chat Notifications**: Send analysis results to Slack, Discord, or custom webhooks
24
+ - 🔗 **Dependency Graph Visualization**: Visual SVG/HTML of component and import relationships
25
+ - 📦 **Bundle Size Impact**: Per-component bundle size estimates and optimization suggestions
26
+ - ⚙️ **Custom Rules Engine**: Define project-specific code quality rules in configuration
24
27
 
25
28
  ### Detected Code Smells
26
29
 
@@ -38,6 +41,7 @@ A CLI tool that analyzes React projects and detects common code smells, providin
38
41
  | **Code Complexity** | Cyclomatic complexity, cognitive complexity, deep nesting |
39
42
  | **Import Issues** | Circular dependencies, barrel file imports, excessive imports |
40
43
  | **Unused Code** | Unused exports, dead imports |
44
+ | **Custom Rules** | User-defined code quality rules |
41
45
  | **Framework-Specific** | Next.js, React Native, Node.js, TypeScript issues |
42
46
 
43
47
  ## Installation
@@ -124,6 +128,10 @@ Or create manually:
124
128
  | `--discord <url>` | Discord webhook URL for notifications | - |
125
129
  | `--webhook <url>` | Generic webhook URL for notifications | - |
126
130
  | `--webhook-threshold <number>` | Only notify if smells exceed threshold | `10` |
131
+ | `--graph` | Generate dependency graph visualization | `false` |
132
+ | `--graph-format <format>` | Graph output format: svg, html | `html` |
133
+ | `--bundle` | Analyze bundle size impact per component | `false` |
134
+ | `--rules <file>` | Custom rules configuration file | - |
127
135
 
128
136
  ### Auto-Fix
129
137
 
@@ -252,6 +260,138 @@ react-smell ./src \
252
260
  --ci
253
261
  ```
254
262
 
263
+ ### Dependency Graph Visualization
264
+
265
+ Generate interactive dependency graphs showing component and file relationships:
266
+
267
+ ```bash
268
+ # Generate dependency graph
269
+ react-smell ./src --graph
270
+
271
+ # Auto-generates: dependency-graph.html
272
+ ```
273
+
274
+ **Features:**
275
+ - Visual representation of file imports and dependencies
276
+ - Circular dependency detection and highlighting
277
+ - Force-directed layout for clarity
278
+ - Exportable SVG or HTML format
279
+ - Legend showing node types and relationships
280
+
281
+ **Example Usage:**
282
+ ```bash
283
+ # Generate and analyze all at once
284
+ react-smell ./src --graph --bundle --baseline --ci
285
+ ```
286
+
287
+ ### Bundle Size Impact Analysis
288
+
289
+ Estimate per-component bundle size impact:
290
+
291
+ ```bash
292
+ # Analyze bundle impact
293
+ react-smell ./src --bundle
294
+
295
+ # Auto-generates: bundle-analysis.html
296
+ ```
297
+
298
+ **Features:**
299
+ - Estimated size per component (in bytes)
300
+ - Line of code (LOC) analysis
301
+ - Dependency complexity scoring
302
+ - Impact level classification (low/medium/high/critical)
303
+ - Recommendations for optimization
304
+ - Breakdown of largest components
305
+
306
+ **Impact Levels:**
307
+ - 🟢 **Low**: <2KB, <150 LOC, low complexity
308
+ - 🟡 **Medium**: 2-5KB, 150-300 LOC, medium complexity
309
+ - 🟠 **High**: 5-10KB, 300-500 LOC, high complexity
310
+ - 🔴 **Critical**: >10KB, >500 LOC, very complex
311
+
312
+ ### Custom Rules Engine
313
+
314
+ Define project-specific code quality rules:
315
+
316
+ **Configuration File (.smellrc-rules.json):**
317
+ ```json
318
+ {
319
+ "rules": [
320
+ {
321
+ "name": "no-hardcoded-strings",
322
+ "description": "Prevent hardcoded strings (use i18n)",
323
+ "severity": "warning",
324
+ "pattern": "\"(hello|world|test)\"",
325
+ "patternType": "regex",
326
+ "enabled": true
327
+ },
328
+ {
329
+ "name": "require-display-name",
330
+ "description": "All components must have displayName",
331
+ "severity": "info",
332
+ "pattern": "displayName",
333
+ "patternType": "text",
334
+ "enabled": true
335
+ }
336
+ ]
337
+ }
338
+ ```
339
+
340
+ **CLI Usage:**
341
+ ```bash
342
+ # Use custom rules
343
+ react-smell ./src --rules .smellrc-rules.json
344
+
345
+ # Combined with other features
346
+ react-smell ./src --rules .smellrc-rules.json --format json --ci
347
+ ```
348
+
349
+ **Rule Properties:**
350
+
351
+ | Property | Type | Description |
352
+ |----------|------|-------------|
353
+ | `name` | string | Unique rule identifier |
354
+ | `description` | string | Human-readable explanation |
355
+ | `severity` | string | error, warning, or info |
356
+ | `pattern` | string | Regex or text pattern |
357
+ | `patternType` | string | regex, text, or ast |
358
+ | `enabled` | boolean | Enable/disable rule |
359
+
360
+ **Pattern Types:**
361
+
362
+ - **regex**: Regular expression matching (e.g., `"hardcoded.*string"`)
363
+ - **text**: Simple string matching
364
+ - **ast**: Babel AST node type matching (e.g., `FunctionExpression`)
365
+
366
+ **Real-World Examples:**
367
+
368
+ ```json
369
+ {
370
+ "rules": [
371
+ {
372
+ "name": "no-console-in-production",
373
+ "pattern": "console\\.(log|warn|info)",
374
+ "patternType": "regex",
375
+ "severity": "error"
376
+ },
377
+ {
378
+ "name": "enforce-prop-types",
379
+ "pattern": "PropTypes",
380
+ "patternType": "text",
381
+ "severity": "warning",
382
+ "message": "Consider using TypeScript instead of PropTypes"
383
+ },
384
+ {
385
+ "name": "limit-nesting",
386
+ "description": "Flag deeply nested JSX",
387
+ "pattern": "<.*>.*<.*>.*<.*>.*<.*>.*<",
388
+ "patternType": "regex",
389
+ "severity": "info"
390
+ }
391
+ ]
392
+ }
393
+ ```
394
+
255
395
  ## Example Output
256
396
 
257
397
  ```
@@ -1 +1 @@
1
- {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AA6BA,OAAO,EACL,cAAc,EAMd,cAAc,EAIf,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC;CAClC;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CA0CtF;AAkQD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AA+BA,OAAO,EACL,cAAc,EAMd,cAAc,EAIf,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC;CAClC;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CA8DtF;AA0QD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC"}
package/dist/analyzer.js CHANGED
@@ -2,6 +2,9 @@ import fg from 'fast-glob';
2
2
  import path from 'path';
3
3
  import { parseFile } from './parser/index.js';
4
4
  import { detectUseEffectOveruse, detectPropDrilling, analyzePropDrillingDepth, detectLargeComponent, detectUnmemoizedCalculations, detectMissingKeys, detectHooksRulesViolations, detectDependencyArrayIssues, detectNestedTernaries, detectDeadCode, detectMagicValues, detectNextjsIssues, detectReactNativeIssues, detectNodejsIssues, detectJavascriptIssues, detectTypescriptIssues, detectDebugStatements, detectSecurityIssues, detectAccessibilityIssues, detectComplexity, detectMemoryLeaks, detectImportIssues, detectUnusedCode, } from './detectors/index.js';
5
+ import { parseCustomRules, detectCustomRuleViolations } from './customRules.js';
6
+ import { buildDependencyGraph } from './graphGenerator.js';
7
+ import { analyzeBundleImpact } from './bundleAnalyzer.js';
5
8
  import { DEFAULT_CONFIG, } from './types/index.js';
6
9
  export async function analyzeProject(options) {
7
10
  const { rootDir, include = ['**/*.tsx', '**/*.jsx'], exclude = ['**/node_modules/**', '**/dist/**', '**/build/**', '**/*.test.*', '**/*.spec.*'], config: userConfig = {}, } = options;
@@ -30,6 +33,20 @@ export async function analyzeProject(options) {
30
33
  // Calculate summary and score
31
34
  const summary = calculateSummary(fileAnalyses);
32
35
  const debtScore = calculateTechnicalDebtScore(fileAnalyses, summary);
36
+ // Generate dependency graph if requested
37
+ if (config.generateDependencyGraph) {
38
+ const importsData = fileAnalyses.map(f => ({ file: f.file, imports: f.imports }));
39
+ const graph = buildDependencyGraph(importsData, rootDir);
40
+ // Graph is available via extension (not in standard result yet)
41
+ global._dependencyGraph = graph;
42
+ }
43
+ // Analyze bundle size if requested
44
+ if (config.analyzeBundleSize) {
45
+ const allComponents = fileAnalyses.flatMap(f => f.components);
46
+ const bundleAnalysis = analyzeBundleImpact(fileAnalyses.map(f => ({ components: f.components, file: f.file })), fileAnalyses, '');
47
+ // Bundle analysis is available via extension
48
+ global._bundleAnalysis = bundleAnalysis;
49
+ }
33
50
  return {
34
51
  files: fileAnalyses,
35
52
  summary,
@@ -83,6 +100,11 @@ function analyzeFile(parseResult, filePath, config) {
83
100
  smells.push(...detectMemoryLeaks(component, filePath, sourceCode, config));
84
101
  smells.push(...detectImportIssues(component, filePath, sourceCode, config));
85
102
  smells.push(...detectUnusedCode(component, filePath, sourceCode, config));
103
+ // Custom rules
104
+ const customRules = parseCustomRules(config);
105
+ if (customRules.length > 0) {
106
+ smells.push(...detectCustomRuleViolations(component, filePath, sourceCode, customRules));
107
+ }
86
108
  });
87
109
  // Run cross-component analysis
88
110
  smells.push(...analyzePropDrillingDepth(components, filePath, sourceCode, config));
@@ -191,6 +213,8 @@ function calculateSummary(files) {
191
213
  // Unused code
192
214
  'unused-export': 0,
193
215
  'dead-import': 0,
216
+ // Custom rules
217
+ 'custom-rule': 0,
194
218
  };
195
219
  const smellsBySeverity = {
196
220
  error: 0,
@@ -0,0 +1,25 @@
1
+ export interface ComponentBundleInfo {
2
+ name: string;
3
+ file: string;
4
+ estimatedSize: number;
5
+ dependencies: number;
6
+ exports: number;
7
+ complexity: number;
8
+ impact: 'low' | 'medium' | 'high' | 'critical';
9
+ }
10
+ export interface BundleAnalysisResult {
11
+ components: ComponentBundleInfo[];
12
+ totalEstimatedSize: number;
13
+ averageComponentSize: number;
14
+ largestComponents: ComponentBundleInfo[];
15
+ recommendations: string[];
16
+ }
17
+ /**
18
+ * Estimate bundle size impact per component
19
+ */
20
+ export declare function analyzeBundleImpact(components: any[], files: any[], sourceCode: string): BundleAnalysisResult;
21
+ /**
22
+ * Generate HTML bundle analysis report
23
+ */
24
+ export declare function generateBundleReport(analysis: BundleAnalysisResult, projectName: string): string;
25
+ //# sourceMappingURL=bundleAnalyzer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundleAnalyzer.d.ts","sourceRoot":"","sources":["../src/bundleAnalyzer.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAC;CAChD;AAED,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,mBAAmB,EAAE,CAAC;IAClC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,iBAAiB,EAAE,mBAAmB,EAAE,CAAC;IACzC,eAAe,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,GAAG,EAAE,EACjB,KAAK,EAAE,GAAG,EAAE,EACZ,UAAU,EAAE,MAAM,GACjB,oBAAoB,CAuBtB;AAuJD;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,oBAAoB,EAC9B,WAAW,EAAE,MAAM,GAClB,MAAM,CAmOR"}
@@ -0,0 +1,375 @@
1
+ import _traverse from '@babel/traverse';
2
+ const traverse = typeof _traverse === 'function' ? _traverse : _traverse.default;
3
+ /**
4
+ * Estimate bundle size impact per component
5
+ */
6
+ export function analyzeBundleImpact(components, files, sourceCode) {
7
+ const componentInfo = [];
8
+ for (const component of components) {
9
+ const info = estimateComponentSize(component, sourceCode);
10
+ componentInfo.push(info);
11
+ }
12
+ // Sort by size
13
+ componentInfo.sort((a, b) => b.estimatedSize - a.estimatedSize);
14
+ const totalSize = componentInfo.reduce((sum, c) => sum + c.estimatedSize, 0);
15
+ const avgSize = totalSize / componentInfo.length || 0;
16
+ const recommendations = generateBundleRecommendations(componentInfo, totalSize);
17
+ return {
18
+ components: componentInfo,
19
+ totalEstimatedSize: totalSize,
20
+ averageComponentSize: avgSize,
21
+ largestComponents: componentInfo.slice(0, 5),
22
+ recommendations,
23
+ };
24
+ }
25
+ /**
26
+ * Estimate size of a single component in bytes
27
+ */
28
+ function estimateComponentSize(component, sourceCode) {
29
+ let size = 0;
30
+ let lineCount = 0;
31
+ let exportCount = 0;
32
+ let dependencyCount = 0;
33
+ let complexity = 0;
34
+ // Count lines
35
+ if (component.code) {
36
+ lineCount = component.code.split('\n').length;
37
+ size += lineCount * 4; // ~4 bytes per line average
38
+ }
39
+ // Count exports
40
+ component.path.traverse({
41
+ ExportNamedDeclaration() {
42
+ exportCount++;
43
+ size += 100; // export overhead
44
+ },
45
+ ExportDefaultDeclaration() {
46
+ exportCount++;
47
+ size += 100;
48
+ },
49
+ });
50
+ // Count dependencies
51
+ component.path.traverse({
52
+ ImportDeclaration() {
53
+ dependencyCount++;
54
+ size += 80;
55
+ },
56
+ CallExpression(path) {
57
+ if (path.node.callee.type === 'Import') {
58
+ dependencyCount++;
59
+ size += 100; // dynamic import is heavier
60
+ }
61
+ },
62
+ });
63
+ // Estimate complexity
64
+ component.path.traverse({
65
+ IfStatement() {
66
+ complexity++;
67
+ size += 30;
68
+ },
69
+ FunctionDeclaration() {
70
+ complexity++;
71
+ size += 50;
72
+ },
73
+ ArrowFunctionExpression() {
74
+ complexity++;
75
+ size += 40;
76
+ },
77
+ });
78
+ // Add base overhead
79
+ size += 200;
80
+ const impact = determineImpact(size, lineCount, complexity);
81
+ return {
82
+ name: component.name || 'Anonymous',
83
+ file: component.file || 'unknown',
84
+ estimatedSize: size,
85
+ dependencies: dependencyCount,
86
+ exports: exportCount,
87
+ complexity: lineCount,
88
+ impact,
89
+ };
90
+ }
91
+ /**
92
+ * Determine bundle impact level
93
+ */
94
+ function determineImpact(size, lines, complexity) {
95
+ let score = 0;
96
+ // Size factor
97
+ if (size > 10000)
98
+ score += 3; // 10KB+
99
+ else if (size > 5000)
100
+ score += 2; // 5KB+
101
+ else if (size > 2000)
102
+ score += 1; // 2KB+
103
+ // Lines factor
104
+ if (lines > 500)
105
+ score += 3;
106
+ else if (lines > 300)
107
+ score += 2;
108
+ else if (lines > 150)
109
+ score += 1;
110
+ // Complexity factor
111
+ if (complexity > 50)
112
+ score += 2;
113
+ else if (complexity > 30)
114
+ score += 1;
115
+ if (score >= 7)
116
+ return 'critical';
117
+ if (score >= 5)
118
+ return 'high';
119
+ if (score >= 2)
120
+ return 'medium';
121
+ return 'low';
122
+ }
123
+ /**
124
+ * Generate recommendations to reduce bundle size
125
+ */
126
+ function generateBundleRecommendations(components, totalSize) {
127
+ const recommendations = [];
128
+ // Check for large components
129
+ const largeComps = components.filter(c => c.impact === 'critical' || c.impact === 'high');
130
+ if (largeComps.length > 0) {
131
+ recommendations.push(`Consider splitting large components: ${largeComps.slice(0, 3).map(c => c.name).join(', ')}`);
132
+ }
133
+ // Check for high dependency count
134
+ const highDepComps = components.filter(c => c.dependencies > 10);
135
+ if (highDepComps.length > 0) {
136
+ recommendations.push(`These components have many dependencies (${highDepComps.length}). Consider lazy loading or code splitting.`);
137
+ }
138
+ // Check total size
139
+ if (totalSize > 100000) {
140
+ recommendations.push(`Total estimated bundle size is ${(totalSize / 1024).toFixed(1)}KB. Consider code-splitting or tree-shaking unused imports.`);
141
+ }
142
+ // Check for high complexity
143
+ const complexComps = components.filter(c => c.complexity > 300);
144
+ if (complexComps.length > 0) {
145
+ recommendations.push(`${complexComps.length} component(s) have high complexity (>300 LOC). Extract logic to custom hooks or utilities.`);
146
+ }
147
+ return recommendations.length > 0
148
+ ? recommendations
149
+ : ['✅ Bundle size is well-optimized. Keep monitoring as code grows.'];
150
+ }
151
+ /**
152
+ * Generate HTML bundle analysis report
153
+ */
154
+ export function generateBundleReport(analysis, projectName) {
155
+ const formatSize = (bytes) => {
156
+ if (bytes > 1024 * 1024)
157
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
158
+ if (bytes > 1024)
159
+ return `${(bytes / 1024).toFixed(1)}KB`;
160
+ return `${bytes}B`;
161
+ };
162
+ const impactColor = {
163
+ low: '#4caf50',
164
+ medium: '#ff9800',
165
+ high: '#f44336',
166
+ critical: '#b71c1c',
167
+ };
168
+ return `<!DOCTYPE html>
169
+ <html lang="en">
170
+ <head>
171
+ <meta charset="UTF-8">
172
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
173
+ <title>Bundle Analysis - ${projectName}</title>
174
+ <style>
175
+ body {
176
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
177
+ background: #f5f5f5;
178
+ margin: 0;
179
+ padding: 20px;
180
+ }
181
+ .container {
182
+ max-width: 1200px;
183
+ margin: 0 auto;
184
+ background: white;
185
+ border-radius: 8px;
186
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
187
+ padding: 30px;
188
+ }
189
+ h1 {
190
+ color: #1976d2;
191
+ margin: 0 0 10px 0;
192
+ }
193
+ .stats-grid {
194
+ display: grid;
195
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
196
+ gap: 20px;
197
+ margin: 30px 0;
198
+ }
199
+ .stat-card {
200
+ background: #f8f9fa;
201
+ border-left: 4px solid #1976d2;
202
+ padding: 20px;
203
+ border-radius: 4px;
204
+ }
205
+ .stat-label {
206
+ font-size: 12px;
207
+ color: #666;
208
+ text-transform: uppercase;
209
+ letter-spacing: 0.5px;
210
+ }
211
+ .stat-value {
212
+ font-size: 28px;
213
+ font-weight: bold;
214
+ color: #1976d2;
215
+ margin-top: 8px;
216
+ }
217
+ .recommendations {
218
+ background: #e3f2fd;
219
+ border-left: 4px solid #1976d2;
220
+ padding: 20px;
221
+ border-radius: 4px;
222
+ margin: 30px 0;
223
+ }
224
+ .recommendations h3 {
225
+ margin: 0 0 10px 0;
226
+ color: #1976d2;
227
+ }
228
+ .recommendations ul {
229
+ margin: 0;
230
+ padding-left: 20px;
231
+ }
232
+ .recommendations li {
233
+ margin: 8px 0;
234
+ color: #333;
235
+ }
236
+ table {
237
+ width: 100%;
238
+ border-collapse: collapse;
239
+ margin-top: 30px;
240
+ }
241
+ th {
242
+ background: #f5f5f5;
243
+ padding: 12px;
244
+ text-align: left;
245
+ font-weight: 600;
246
+ border-bottom: 2px solid #e0e0e0;
247
+ }
248
+ td {
249
+ padding: 12px;
250
+ border-bottom: 1px solid #e0e0e0;
251
+ }
252
+ tr:hover {
253
+ background: #fafafa;
254
+ }
255
+ .impact-badge {
256
+ display: inline-block;
257
+ padding: 4px 12px;
258
+ border-radius: 12px;
259
+ font-size: 12px;
260
+ font-weight: 600;
261
+ color: white;
262
+ }
263
+ .impact-low {
264
+ background: #4caf50;
265
+ }
266
+ .impact-medium {
267
+ background: #ff9800;
268
+ }
269
+ .impact-high {
270
+ background: #f44336;
271
+ }
272
+ .impact-critical {
273
+ background: #b71c1c;
274
+ }
275
+ .size-bar {
276
+ display: inline-block;
277
+ height: 8px;
278
+ background: #1976d2;
279
+ border-radius: 4px;
280
+ margin-right: 8px;
281
+ vertical-align: middle;
282
+ }
283
+ </style>
284
+ </head>
285
+ <body>
286
+ <div class="container">
287
+ <h1>📦 Bundle Size Analysis - ${projectName}</h1>
288
+ <p style="color: #666; margin-top: 5px;">Estimated size impact per component</p>
289
+
290
+ <div class="stats-grid">
291
+ <div class="stat-card">
292
+ <div class="stat-label">Total Size</div>
293
+ <div class="stat-value">${formatSize(analysis.totalEstimatedSize)}</div>
294
+ </div>
295
+ <div class="stat-card">
296
+ <div class="stat-label">Components</div>
297
+ <div class="stat-value">${analysis.components.length}</div>
298
+ </div>
299
+ <div class="stat-card">
300
+ <div class="stat-label">Avg Component</div>
301
+ <div class="stat-value">${formatSize(analysis.averageComponentSize)}</div>
302
+ </div>
303
+ </div>
304
+
305
+ ${analysis.recommendations.length > 0
306
+ ? `
307
+ <div class="recommendations">
308
+ <h3>💡 Recommendations</h3>
309
+ <ul>
310
+ ${analysis.recommendations.map(rec => `<li>${rec}</li>`).join('')}
311
+ </ul>
312
+ </div>
313
+ `
314
+ : ''}
315
+
316
+ <h2 style="margin-top: 40px; color: #333;">📊 Component Breakdown</h2>
317
+ <table>
318
+ <thead>
319
+ <tr>
320
+ <th>Component</th>
321
+ <th>Est. Size</th>
322
+ <th>LOC</th>
323
+ <th>Dependencies</th>
324
+ <th>Exports</th>
325
+ <th>Impact</th>
326
+ </tr>
327
+ </thead>
328
+ <tbody>
329
+ ${analysis.components
330
+ .map(c => `
331
+ <tr>
332
+ <td><strong>${c.name}</strong></td>
333
+ <td>${formatSize(c.estimatedSize)}</td>
334
+ <td>${c.complexity}</td>
335
+ <td>${c.dependencies}</td>
336
+ <td>${c.exports}</td>
337
+ <td>
338
+ <span class="impact-badge impact-${c.impact}">${c.impact.toUpperCase()}</span>
339
+ </td>
340
+ </tr>
341
+ `)
342
+ .join('')}
343
+ </tbody>
344
+ </table>
345
+
346
+ <h2 style="margin-top: 40px; color: #333;">🏆 Largest Components</h2>
347
+ <table>
348
+ <thead>
349
+ <tr>
350
+ <th>Rank</th>
351
+ <th>Component</th>
352
+ <th>Size</th>
353
+ <th>% of Total</th>
354
+ </tr>
355
+ </thead>
356
+ <tbody>
357
+ ${analysis.largestComponents
358
+ .map((c, i) => `
359
+ <tr>
360
+ <td><strong>#${i + 1}</strong></td>
361
+ <td>${c.name}</td>
362
+ <td>${formatSize(c.estimatedSize)}</td>
363
+ <td>
364
+ ${((c.estimatedSize / analysis.totalEstimatedSize) * 100).toFixed(1)}%
365
+ <div class="size-bar" style="width: ${((c.estimatedSize / analysis.totalEstimatedSize) * 200).toFixed(0)}px;"></div>
366
+ </td>
367
+ </tr>
368
+ `)
369
+ .join('')}
370
+ </tbody>
371
+ </table>
372
+ </div>
373
+ </body>
374
+ </html>`;
375
+ }