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 +140 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +24 -0
- package/dist/bundleAnalyzer.d.ts +25 -0
- package/dist/bundleAnalyzer.d.ts.map +1 -0
- package/dist/bundleAnalyzer.js +375 -0
- package/dist/cli.js +42 -0
- package/dist/customRules.d.ts +31 -0
- package/dist/customRules.d.ts.map +1 -0
- package/dist/customRules.js +289 -0
- package/dist/detectors/complexity.d.ts +0 -4
- package/dist/detectors/complexity.d.ts.map +1 -1
- package/dist/detectors/complexity.js +1 -1
- package/dist/detectors/deadCode.d.ts +0 -7
- package/dist/detectors/deadCode.d.ts.map +1 -1
- package/dist/detectors/deadCode.js +0 -24
- package/dist/detectors/index.d.ts +2 -2
- package/dist/detectors/index.d.ts.map +1 -1
- package/dist/detectors/index.js +2 -2
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +0 -7
- package/dist/graphGenerator.d.ts +34 -0
- package/dist/graphGenerator.d.ts.map +1 -0
- package/dist/graphGenerator.js +320 -0
- package/dist/reporter.js +2 -0
- package/dist/types/index.d.ts +6 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +8 -0
- package/package.json +1 -1
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
|
```
|
package/dist/analyzer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"
|
|
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
|
+
}
|