react-code-smell-detector 1.4.1 → 1.5.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 (56) hide show
  1. package/README.md +347 -22
  2. package/dist/__tests__/parser.test.d.ts +2 -0
  3. package/dist/__tests__/parser.test.d.ts.map +1 -0
  4. package/dist/__tests__/parser.test.js +56 -0
  5. package/dist/__tests__/performanceBudget.test.d.ts +2 -0
  6. package/dist/__tests__/performanceBudget.test.d.ts.map +1 -0
  7. package/dist/__tests__/performanceBudget.test.js +91 -0
  8. package/dist/__tests__/prComments.test.d.ts +2 -0
  9. package/dist/__tests__/prComments.test.d.ts.map +1 -0
  10. package/dist/__tests__/prComments.test.js +118 -0
  11. package/dist/analyzer.d.ts.map +1 -1
  12. package/dist/analyzer.js +34 -1
  13. package/dist/bundleAnalyzer.d.ts +25 -0
  14. package/dist/bundleAnalyzer.d.ts.map +1 -0
  15. package/dist/bundleAnalyzer.js +375 -0
  16. package/dist/cli.js +148 -1
  17. package/dist/customRules.d.ts +31 -0
  18. package/dist/customRules.d.ts.map +1 -0
  19. package/dist/customRules.js +289 -0
  20. package/dist/detectors/complexity.d.ts +0 -4
  21. package/dist/detectors/complexity.d.ts.map +1 -1
  22. package/dist/detectors/complexity.js +1 -1
  23. package/dist/detectors/deadCode.d.ts +0 -7
  24. package/dist/detectors/deadCode.d.ts.map +1 -1
  25. package/dist/detectors/deadCode.js +0 -24
  26. package/dist/detectors/index.d.ts +3 -2
  27. package/dist/detectors/index.d.ts.map +1 -1
  28. package/dist/detectors/index.js +4 -2
  29. package/dist/detectors/serverComponents.d.ts +11 -0
  30. package/dist/detectors/serverComponents.d.ts.map +1 -0
  31. package/dist/detectors/serverComponents.js +222 -0
  32. package/dist/docGenerator.d.ts +37 -0
  33. package/dist/docGenerator.d.ts.map +1 -0
  34. package/dist/docGenerator.js +306 -0
  35. package/dist/git.d.ts.map +1 -1
  36. package/dist/git.js +0 -7
  37. package/dist/graphGenerator.d.ts +34 -0
  38. package/dist/graphGenerator.d.ts.map +1 -0
  39. package/dist/graphGenerator.js +320 -0
  40. package/dist/index.d.ts +4 -0
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +4 -0
  43. package/dist/interactiveFixer.d.ts +20 -0
  44. package/dist/interactiveFixer.d.ts.map +1 -0
  45. package/dist/interactiveFixer.js +178 -0
  46. package/dist/performanceBudget.d.ts +54 -0
  47. package/dist/performanceBudget.d.ts.map +1 -0
  48. package/dist/performanceBudget.js +218 -0
  49. package/dist/prComments.d.ts +47 -0
  50. package/dist/prComments.d.ts.map +1 -0
  51. package/dist/prComments.js +233 -0
  52. package/dist/reporter.js +2 -0
  53. package/dist/types/index.d.ts +7 -1
  54. package/dist/types/index.d.ts.map +1 -1
  55. package/dist/types/index.js +10 -0
  56. package/package.json +10 -4
@@ -0,0 +1,222 @@
1
+ import * as t from '@babel/types';
2
+ // Hooks that are not allowed in Server Components
3
+ const CLIENT_ONLY_HOOKS = [
4
+ 'useState',
5
+ 'useReducer',
6
+ 'useEffect',
7
+ 'useLayoutEffect',
8
+ 'useInsertionEffect',
9
+ 'useRef',
10
+ 'useImperativeHandle',
11
+ 'useSyncExternalStore',
12
+ 'useTransition',
13
+ 'useDeferredValue',
14
+ 'useOptimistic',
15
+ 'useFormStatus',
16
+ 'useActionState',
17
+ ];
18
+ // Browser-only APIs
19
+ const BROWSER_APIS = [
20
+ 'window',
21
+ 'document',
22
+ 'localStorage',
23
+ 'sessionStorage',
24
+ 'navigator',
25
+ 'location',
26
+ 'history',
27
+ 'alert',
28
+ 'confirm',
29
+ 'prompt',
30
+ 'fetch', // Can be used in Server Components but behaves differently
31
+ ];
32
+ // Event handler props (indicate client interactivity)
33
+ const EVENT_HANDLER_PATTERN = /^on[A-Z]/;
34
+ /**
35
+ * Detect React 19 Server/Client component boundary issues
36
+ */
37
+ export function detectServerComponentIssues(component, filePath, sourceCode, config, imports = []) {
38
+ if (!config.checkServerComponents)
39
+ return [];
40
+ const smells = [];
41
+ const lines = sourceCode.split('\n');
42
+ // Check if file has 'use client' or 'use server' directive
43
+ const hasUseClient = lines.some(line => /^['"]use client['"];?\s*$/.test(line.trim()));
44
+ const hasUseServer = lines.some(line => /^['"]use server['"];?\s*$/.test(line.trim()));
45
+ // Detect if this is a Server Component (no 'use client' directive in app/ directory)
46
+ const isInAppDir = filePath.includes('/app/') || filePath.includes('\\app\\');
47
+ const isServerComponent = isInAppDir && !hasUseClient;
48
+ if (isServerComponent) {
49
+ // Check for client-only hooks in Server Components
50
+ const clientHooksUsed = [];
51
+ for (const hookName of CLIENT_ONLY_HOOKS) {
52
+ const hooks = component.hooks[hookName];
53
+ if (hooks && hooks.length > 0) {
54
+ clientHooksUsed.push(hookName);
55
+ }
56
+ }
57
+ // Also traverse for any hook calls
58
+ component.path.traverse({
59
+ CallExpression(path) {
60
+ const callee = path.node.callee;
61
+ if (t.isIdentifier(callee) && CLIENT_ONLY_HOOKS.includes(callee.name)) {
62
+ if (!clientHooksUsed.includes(callee.name)) {
63
+ clientHooksUsed.push(callee.name);
64
+ }
65
+ }
66
+ },
67
+ });
68
+ if (clientHooksUsed.length > 0) {
69
+ smells.push({
70
+ type: 'server-component-hooks',
71
+ severity: 'error',
72
+ message: `Server Component "${component.name}" uses client-only hooks: ${clientHooksUsed.join(', ')}`,
73
+ file: filePath,
74
+ line: component.startLine,
75
+ column: 0,
76
+ suggestion: `Add 'use client' directive at the top of the file, or move stateful logic to a Client Component.`,
77
+ });
78
+ }
79
+ // Check for event handlers in JSX (onClick, onChange, etc.)
80
+ const eventHandlers = [];
81
+ component.path.traverse({
82
+ JSXAttribute(path) {
83
+ if (t.isJSXIdentifier(path.node.name)) {
84
+ const name = path.node.name.name;
85
+ if (EVENT_HANDLER_PATTERN.test(name)) {
86
+ if (!eventHandlers.includes(name)) {
87
+ eventHandlers.push(name);
88
+ }
89
+ }
90
+ }
91
+ },
92
+ });
93
+ if (eventHandlers.length > 0) {
94
+ smells.push({
95
+ type: 'server-component-events',
96
+ severity: 'error',
97
+ message: `Server Component "${component.name}" uses event handlers: ${eventHandlers.join(', ')}`,
98
+ file: filePath,
99
+ line: component.startLine,
100
+ column: 0,
101
+ suggestion: `Add 'use client' directive or extract interactive elements to a Client Component.`,
102
+ });
103
+ }
104
+ // Check for browser APIs usage
105
+ const browserApisUsed = [];
106
+ component.path.traverse({
107
+ Identifier(path) {
108
+ const name = path.node.name;
109
+ if (BROWSER_APIS.includes(name)) {
110
+ // Make sure it's not a property access like obj.window
111
+ if (!t.isMemberExpression(path.parent) || !t.isIdentifier(path.parent.property) || path.parent.object === path.node) {
112
+ if (!browserApisUsed.includes(name)) {
113
+ browserApisUsed.push(name);
114
+ }
115
+ }
116
+ }
117
+ },
118
+ });
119
+ if (browserApisUsed.length > 0) {
120
+ smells.push({
121
+ type: 'server-component-browser-api',
122
+ severity: 'warning',
123
+ message: `Server Component "${component.name}" may use browser APIs: ${browserApisUsed.join(', ')}`,
124
+ file: filePath,
125
+ line: component.startLine,
126
+ column: 0,
127
+ suggestion: `Browser APIs are not available in Server Components. Move to a Client Component or use conditional checks.`,
128
+ });
129
+ }
130
+ }
131
+ // Check for 'use server' in client components (async actions)
132
+ if (hasUseClient && hasUseServer) {
133
+ smells.push({
134
+ type: 'mixed-directives',
135
+ severity: 'error',
136
+ message: `File has both 'use client' and 'use server' directives`,
137
+ file: filePath,
138
+ line: 1,
139
+ column: 0,
140
+ suggestion: `A file can only be either a Client Component or contain Server Actions, not both. Separate them into different files.`,
141
+ });
142
+ }
143
+ // Check for async component without 'use server' actions (they should be in Server Components)
144
+ if (!isServerComponent && !hasUseServer) {
145
+ let hasAsyncServerAction = false;
146
+ component.path.traverse({
147
+ FunctionDeclaration(path) {
148
+ if (path.node.async) {
149
+ // Check if body starts with 'use server'
150
+ const body = path.node.body;
151
+ if (body.directives?.some(d => d.value.value === 'use server')) {
152
+ hasAsyncServerAction = true;
153
+ }
154
+ }
155
+ },
156
+ ArrowFunctionExpression(path) {
157
+ if (path.node.async && t.isBlockStatement(path.node.body)) {
158
+ const directives = path.node.body.directives;
159
+ if (directives?.some(d => d.value.value === 'use server')) {
160
+ hasAsyncServerAction = true;
161
+ }
162
+ }
163
+ },
164
+ });
165
+ // This is fine - Server Actions can be defined inline with 'use server' inside
166
+ }
167
+ // Check for passing server actions incorrectly to client components
168
+ // (This is complex to detect statically, so we provide guidance)
169
+ // Check for importing Server Component into Client Component (potential issue)
170
+ if (hasUseClient) {
171
+ // Look for imports that might be Server Components
172
+ const serverComponentImports = imports.filter(imp => {
173
+ // Heuristic: imports from app/ directory without 'use client'
174
+ return imp.includes('/app/') && !imp.includes('.client');
175
+ });
176
+ // Can't definitively detect, but can warn about patterns
177
+ }
178
+ return smells;
179
+ }
180
+ /**
181
+ * Detect proper async component patterns in React 19
182
+ */
183
+ export function detectAsyncComponentIssues(component, filePath, sourceCode, config) {
184
+ if (!config.checkServerComponents)
185
+ return [];
186
+ const smells = [];
187
+ const lines = sourceCode.split('\n');
188
+ const hasUseClient = lines.some(line => /^['"]use client['"];?\s*$/.test(line.trim()));
189
+ // Check if component is async
190
+ let isAsync = false;
191
+ const node = component.path.node;
192
+ if (t.isFunctionDeclaration(node) || t.isFunctionExpression(node) || t.isArrowFunctionExpression(node)) {
193
+ isAsync = node.async || false;
194
+ }
195
+ // Async components in Client Components are not allowed
196
+ if (isAsync && hasUseClient) {
197
+ smells.push({
198
+ type: 'async-client-component',
199
+ severity: 'error',
200
+ message: `Client Component "${component.name}" is async, which is not supported`,
201
+ file: filePath,
202
+ line: component.startLine,
203
+ column: 0,
204
+ suggestion: `Remove async from the component. Use useEffect or React Query for data fetching in Client Components.`,
205
+ });
206
+ }
207
+ // Check for proper Suspense usage with async Server Components
208
+ if (isAsync && !hasUseClient) {
209
+ // Good pattern - async Server Component
210
+ // Could check if parent has Suspense boundary, but that requires cross-file analysis
211
+ }
212
+ return smells;
213
+ }
214
+ /**
215
+ * Get code snippet for context
216
+ */
217
+ function getCodeSnippet(sourceCode, line) {
218
+ const lines = sourceCode.split('\n');
219
+ const startLine = Math.max(0, line - 2);
220
+ const endLine = Math.min(lines.length, line + 1);
221
+ return lines.slice(startLine, endLine).join('\n');
222
+ }
@@ -0,0 +1,37 @@
1
+ import { AnalysisResult, CodeSmell } from './types/index.js';
2
+ export interface DocGeneratorOptions {
3
+ format: 'markdown' | 'html' | 'json';
4
+ outputDir?: string;
5
+ includeSmells?: boolean;
6
+ includeMetrics?: boolean;
7
+ groupByFolder?: boolean;
8
+ }
9
+ export interface ComponentDoc {
10
+ name: string;
11
+ file: string;
12
+ relativePath: string;
13
+ lineCount: number;
14
+ props: string[];
15
+ hooks: {
16
+ useState: number;
17
+ useEffect: number;
18
+ useMemo: number;
19
+ useCallback: number;
20
+ useRef: number;
21
+ custom: string[];
22
+ };
23
+ smells: CodeSmell[];
24
+ metrics: {
25
+ complexity: string;
26
+ maintainability: string;
27
+ };
28
+ }
29
+ /**
30
+ * Generate documentation from component analysis
31
+ */
32
+ export declare function generateComponentDocs(result: AnalysisResult, rootDir: string, options?: DocGeneratorOptions): Promise<string>;
33
+ /**
34
+ * Write documentation to file
35
+ */
36
+ export declare function writeComponentDocs(result: AnalysisResult, rootDir: string, options: DocGeneratorOptions): Promise<string>;
37
+ //# sourceMappingURL=docGenerator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"docGenerator.d.ts","sourceRoot":"","sources":["../src/docGenerator.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAA+B,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE1F,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,EAAE;QACL,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,EAAE,MAAM,EAAE,CAAC;KAClB,CAAC;IACF,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,OAAO,EAAE;QACP,UAAU,EAAE,MAAM,CAAC;QACnB,eAAe,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,mBAA4C,GACpD,OAAO,CAAC,MAAM,CAAC,CAYjB;AAqTD;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,MAAM,CAAC,CAYjB"}
@@ -0,0 +1,306 @@
1
+ import path from 'path';
2
+ import fs from 'fs/promises';
3
+ /**
4
+ * Generate documentation from component analysis
5
+ */
6
+ export async function generateComponentDocs(result, rootDir, options = { format: 'markdown' }) {
7
+ const docs = extractComponentDocs(result, rootDir);
8
+ switch (options.format) {
9
+ case 'html':
10
+ return generateHTMLDocs(docs, result, options);
11
+ case 'json':
12
+ return JSON.stringify(docs, null, 2);
13
+ case 'markdown':
14
+ default:
15
+ return generateMarkdownDocs(docs, result, options);
16
+ }
17
+ }
18
+ /**
19
+ * Extract documentation data from analysis
20
+ */
21
+ function extractComponentDocs(result, rootDir) {
22
+ const docs = [];
23
+ for (const file of result.files) {
24
+ for (const component of file.components) {
25
+ const relativePath = file.file.replace(rootDir, '').replace(/^\//, '');
26
+ const componentSmells = file.smells.filter(s => s.line >= component.startLine && s.line <= component.endLine);
27
+ // Determine complexity rating
28
+ const complexity = getComplexityRating(component);
29
+ const maintainability = getMaintainabilityRating(componentSmells.length, component.lineCount);
30
+ docs.push({
31
+ name: component.name,
32
+ file: file.file,
33
+ relativePath,
34
+ lineCount: component.lineCount,
35
+ props: [], // Would need parser enhancement to get prop names
36
+ hooks: {
37
+ useState: component.useStateCount,
38
+ useEffect: component.useEffectCount,
39
+ useMemo: component.useMemoCount,
40
+ useCallback: component.useCallbackCount,
41
+ useRef: 0, // Would need parser enhancement
42
+ custom: [],
43
+ },
44
+ smells: componentSmells,
45
+ metrics: {
46
+ complexity,
47
+ maintainability,
48
+ },
49
+ });
50
+ }
51
+ }
52
+ return docs.sort((a, b) => a.name.localeCompare(b.name));
53
+ }
54
+ /**
55
+ * Generate Markdown documentation
56
+ */
57
+ function generateMarkdownDocs(docs, result, options) {
58
+ let md = '# Component Documentation\n\n';
59
+ md += `*Generated by react-code-smell-detector*\n\n`;
60
+ md += `---\n\n`;
61
+ // Summary
62
+ md += '## Summary\n\n';
63
+ md += `| Metric | Value |\n`;
64
+ md += `|--------|-------|\n`;
65
+ md += `| Total Components | ${docs.length} |\n`;
66
+ md += `| Total Files | ${result.summary.totalFiles} |\n`;
67
+ md += `| Technical Debt Grade | ${result.debtScore.grade} |\n`;
68
+ md += `| Estimated Refactor Time | ${result.debtScore.estimatedRefactorTime} |\n\n`;
69
+ // Table of contents
70
+ md += '## Components\n\n';
71
+ if (options.groupByFolder) {
72
+ const grouped = groupByFolder(docs);
73
+ for (const [folder, folderDocs] of Object.entries(grouped)) {
74
+ md += `### 📁 ${folder || 'Root'}\n\n`;
75
+ for (const doc of folderDocs) {
76
+ md += formatComponentMarkdown(doc, options);
77
+ }
78
+ }
79
+ }
80
+ else {
81
+ for (const doc of docs) {
82
+ md += formatComponentMarkdown(doc, options);
83
+ }
84
+ }
85
+ // Index
86
+ md += '\n---\n\n';
87
+ md += '## Index\n\n';
88
+ md += '| Component | File | Lines | Hooks | Issues |\n';
89
+ md += '|-----------|------|-------|-------|--------|\n';
90
+ for (const doc of docs) {
91
+ const totalHooks = doc.hooks.useState + doc.hooks.useEffect + doc.hooks.useMemo + doc.hooks.useCallback;
92
+ md += `| [${doc.name}](#${doc.name.toLowerCase()}) | \`${doc.relativePath}\` | ${doc.lineCount} | ${totalHooks} | ${doc.smells.length} |\n`;
93
+ }
94
+ return md;
95
+ }
96
+ /**
97
+ * Format a single component as Markdown
98
+ */
99
+ function formatComponentMarkdown(doc, options) {
100
+ let md = `#### ${doc.name}\n\n`;
101
+ md += `📄 \`${doc.relativePath}\`\n\n`;
102
+ // Metrics table
103
+ if (options.includeMetrics !== false) {
104
+ md += `| Metric | Value |\n`;
105
+ md += `|--------|-------|\n`;
106
+ md += `| Lines | ${doc.lineCount} |\n`;
107
+ md += `| Complexity | ${doc.metrics.complexity} |\n`;
108
+ md += `| Maintainability | ${doc.metrics.maintainability} |\n`;
109
+ }
110
+ // Hooks
111
+ const hooks = [];
112
+ if (doc.hooks.useState > 0)
113
+ hooks.push(`useState (${doc.hooks.useState})`);
114
+ if (doc.hooks.useEffect > 0)
115
+ hooks.push(`useEffect (${doc.hooks.useEffect})`);
116
+ if (doc.hooks.useMemo > 0)
117
+ hooks.push(`useMemo (${doc.hooks.useMemo})`);
118
+ if (doc.hooks.useCallback > 0)
119
+ hooks.push(`useCallback (${doc.hooks.useCallback})`);
120
+ if (hooks.length > 0) {
121
+ md += `\n**Hooks:** ${hooks.join(', ')}\n`;
122
+ }
123
+ // Issues
124
+ if (options.includeSmells !== false && doc.smells.length > 0) {
125
+ md += `\n**Issues (${doc.smells.length}):**\n`;
126
+ for (const smell of doc.smells.slice(0, 5)) {
127
+ const emoji = smell.severity === 'error' ? '🔴' : smell.severity === 'warning' ? '🟡' : '🔵';
128
+ md += `- ${emoji} ${smell.type}: ${smell.message}\n`;
129
+ }
130
+ if (doc.smells.length > 5) {
131
+ md += `- *... and ${doc.smells.length - 5} more*\n`;
132
+ }
133
+ }
134
+ md += '\n---\n\n';
135
+ return md;
136
+ }
137
+ /**
138
+ * Generate HTML documentation
139
+ */
140
+ function generateHTMLDocs(docs, result, options) {
141
+ return `<!DOCTYPE html>
142
+ <html lang="en">
143
+ <head>
144
+ <meta charset="UTF-8">
145
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
146
+ <title>Component Documentation</title>
147
+ <style>
148
+ * { box-sizing: border-box; margin: 0; padding: 0; }
149
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; background: #f5f5f5; }
150
+ .container { max-width: 1200px; margin: 0 auto; padding: 20px; }
151
+ header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 40px 20px; margin-bottom: 30px; border-radius: 10px; }
152
+ h1 { font-size: 2.5rem; margin-bottom: 10px; }
153
+ .subtitle { opacity: 0.9; }
154
+ .summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px; }
155
+ .summary-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); text-align: center; }
156
+ .summary-value { font-size: 2rem; font-weight: bold; color: #667eea; }
157
+ .summary-label { color: #666; font-size: 0.9rem; }
158
+ .components { display: grid; gap: 20px; }
159
+ .component-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
160
+ .component-name { font-size: 1.3rem; color: #333; margin-bottom: 5px; }
161
+ .component-path { color: #666; font-size: 0.85rem; font-family: monospace; background: #f0f0f0; padding: 2px 8px; border-radius: 4px; }
162
+ .metrics { display: flex; gap: 15px; margin: 15px 0; flex-wrap: wrap; }
163
+ .metric { background: #f0f0f0; padding: 5px 12px; border-radius: 20px; font-size: 0.85rem; }
164
+ .hooks { display: flex; gap: 10px; flex-wrap: wrap; margin: 10px 0; }
165
+ .hook { background: #e3f2fd; color: #1976d2; padding: 4px 10px; border-radius: 4px; font-size: 0.8rem; }
166
+ .issues { margin-top: 15px; }
167
+ .issue { padding: 8px; margin: 5px 0; border-radius: 4px; font-size: 0.9rem; }
168
+ .issue.error { background: #ffebee; color: #c62828; }
169
+ .issue.warning { background: #fff3e0; color: #ef6c00; }
170
+ .issue.info { background: #e3f2fd; color: #1565c0; }
171
+ .search { margin-bottom: 20px; }
172
+ .search input { width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 1rem; }
173
+ .grade { font-size: 3rem; }
174
+ footer { text-align: center; padding: 20px; color: #666; font-size: 0.85rem; }
175
+ </style>
176
+ </head>
177
+ <body>
178
+ <div class="container">
179
+ <header>
180
+ <h1>📚 Component Documentation</h1>
181
+ <p class="subtitle">Generated by react-code-smell-detector</p>
182
+ </header>
183
+
184
+ <div class="summary">
185
+ <div class="summary-card">
186
+ <div class="summary-value">${docs.length}</div>
187
+ <div class="summary-label">Components</div>
188
+ </div>
189
+ <div class="summary-card">
190
+ <div class="summary-value">${result.summary.totalFiles}</div>
191
+ <div class="summary-label">Files</div>
192
+ </div>
193
+ <div class="summary-card">
194
+ <div class="summary-value grade">${result.debtScore.grade}</div>
195
+ <div class="summary-label">Debt Grade</div>
196
+ </div>
197
+ <div class="summary-card">
198
+ <div class="summary-value">${result.debtScore.score}</div>
199
+ <div class="summary-label">Score</div>
200
+ </div>
201
+ </div>
202
+
203
+ <div class="search">
204
+ <input type="text" placeholder="Search components..." id="search" onkeyup="filterComponents()">
205
+ </div>
206
+
207
+ <div class="components" id="components">
208
+ ${docs.map(doc => `
209
+ <div class="component-card" data-name="${doc.name.toLowerCase()}">
210
+ <div class="component-name">${doc.name}</div>
211
+ <span class="component-path">${doc.relativePath}</span>
212
+
213
+ <div class="metrics">
214
+ <span class="metric">📏 ${doc.lineCount} lines</span>
215
+ <span class="metric">⚡ ${doc.metrics.complexity}</span>
216
+ <span class="metric">🔧 ${doc.metrics.maintainability}</span>
217
+ </div>
218
+
219
+ <div class="hooks">
220
+ ${doc.hooks.useState > 0 ? `<span class="hook">useState ×${doc.hooks.useState}</span>` : ''}
221
+ ${doc.hooks.useEffect > 0 ? `<span class="hook">useEffect ×${doc.hooks.useEffect}</span>` : ''}
222
+ ${doc.hooks.useMemo > 0 ? `<span class="hook">useMemo ×${doc.hooks.useMemo}</span>` : ''}
223
+ ${doc.hooks.useCallback > 0 ? `<span class="hook">useCallback ×${doc.hooks.useCallback}</span>` : ''}
224
+ </div>
225
+
226
+ ${doc.smells.length > 0 ? `
227
+ <div class="issues">
228
+ ${doc.smells.slice(0, 3).map(s => `
229
+ <div class="issue ${s.severity}">${s.type}: ${s.message}</div>
230
+ `).join('')}
231
+ ${doc.smells.length > 3 ? `<div class="issue info">... and ${doc.smells.length - 3} more</div>` : ''}
232
+ </div>
233
+ ` : ''}
234
+ </div>
235
+ `).join('')}
236
+ </div>
237
+
238
+ <footer>
239
+ Generated by <a href="https://github.com/vsthakur101/react-code-smell-detector">react-code-smell-detector</a>
240
+ </footer>
241
+ </div>
242
+
243
+ <script>
244
+ function filterComponents() {
245
+ const query = document.getElementById('search').value.toLowerCase();
246
+ const cards = document.querySelectorAll('.component-card');
247
+ cards.forEach(card => {
248
+ const name = card.dataset.name;
249
+ card.style.display = name.includes(query) ? 'block' : 'none';
250
+ });
251
+ }
252
+ </script>
253
+ </body>
254
+ </html>`;
255
+ }
256
+ /**
257
+ * Group components by folder
258
+ */
259
+ function groupByFolder(docs) {
260
+ const grouped = {};
261
+ for (const doc of docs) {
262
+ const parts = doc.relativePath.split('/');
263
+ const folder = parts.length > 1 ? parts.slice(0, -1).join('/') : '';
264
+ if (!grouped[folder]) {
265
+ grouped[folder] = [];
266
+ }
267
+ grouped[folder].push(doc);
268
+ }
269
+ return grouped;
270
+ }
271
+ /**
272
+ * Get complexity rating based on component metrics
273
+ */
274
+ function getComplexityRating(component) {
275
+ const hookCount = component.useEffectCount + component.useStateCount +
276
+ component.useMemoCount + component.useCallbackCount;
277
+ if (component.lineCount > 300 || hookCount > 10)
278
+ return '🔴 High';
279
+ if (component.lineCount > 150 || hookCount > 5)
280
+ return '🟡 Medium';
281
+ return '🟢 Low';
282
+ }
283
+ /**
284
+ * Get maintainability rating based on smells and size
285
+ */
286
+ function getMaintainabilityRating(smellCount, lineCount) {
287
+ const ratio = smellCount / Math.max(lineCount / 50, 1);
288
+ if (ratio > 2 || smellCount > 5)
289
+ return '🔴 Poor';
290
+ if (ratio > 1 || smellCount > 2)
291
+ return '🟡 Fair';
292
+ return '🟢 Good';
293
+ }
294
+ /**
295
+ * Write documentation to file
296
+ */
297
+ export async function writeComponentDocs(result, rootDir, options) {
298
+ const content = await generateComponentDocs(result, rootDir, options);
299
+ const ext = options.format === 'html' ? 'html' : options.format === 'json' ? 'json' : 'md';
300
+ const filename = `COMPONENTS.${ext}`;
301
+ const outputPath = options.outputDir
302
+ ? path.join(options.outputDir, filename)
303
+ : path.join(rootDir, filename);
304
+ await fs.writeFile(outputPath, content, 'utf-8');
305
+ return outputPath;
306
+ }
package/dist/git.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,OAAO;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAiFnD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAcpE;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,GAAE,MAAU,GAAG,MAAM,EAAE,CAcpF;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAW7D;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAG1D"}
1
+ {"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,OAAO;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAyEnD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAcpE;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,GAAE,MAAU,GAAG,MAAM,EAAE,CAcpF;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAW7D;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAG1D"}
package/dist/git.js CHANGED
@@ -61,13 +61,6 @@ export function getGitInfo(rootDir) {
61
61
  stagedFiles,
62
62
  untrackedFiles,
63
63
  };
64
- return {
65
- isGitRepo: true,
66
- currentBranch,
67
- changedFiles,
68
- stagedFiles,
69
- untrackedFiles,
70
- };
71
64
  }
72
65
  catch {
73
66
  return {
@@ -0,0 +1,34 @@
1
+ export interface DependencyNode {
2
+ id: string;
3
+ file: string;
4
+ type: 'component' | 'file';
5
+ imports: string[];
6
+ importedBy: string[];
7
+ size?: number;
8
+ isCircular?: boolean;
9
+ }
10
+ export interface DependencyGraph {
11
+ nodes: Map<string, DependencyNode>;
12
+ edges: Array<{
13
+ from: string;
14
+ to: string;
15
+ circular: boolean;
16
+ }>;
17
+ circularDependencies: string[][];
18
+ }
19
+ /**
20
+ * Build dependency graph from parsed components
21
+ */
22
+ export declare function buildDependencyGraph(files: Array<{
23
+ file: string;
24
+ imports: string[];
25
+ }>, rootDir: string): DependencyGraph;
26
+ /**
27
+ * Generate SVG representation of dependency graph
28
+ */
29
+ export declare function generateDependencyGraph(graph: DependencyGraph, width?: number, height?: number): string;
30
+ /**
31
+ * Generate HTML report with dependency graph
32
+ */
33
+ export declare function generateDependencyGraphHTML(graph: DependencyGraph, projectName: string, circularCount?: number): string;
34
+ //# sourceMappingURL=graphGenerator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graphGenerator.d.ts","sourceRoot":"","sources":["../src/graphGenerator.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,WAAW,GAAG,MAAM,CAAC;IAC3B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IACnC,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAC9D,oBAAoB,EAAE,MAAM,EAAE,EAAE,CAAC;CAClC;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,EACjD,OAAO,EAAE,MAAM,GACd,eAAe,CA2CjB;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,eAAe,EAAE,KAAK,SAAO,EAAE,MAAM,SAAM,GAAG,MAAM,CA4DlG;AAED;;GAEG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,eAAe,EACtB,WAAW,EAAE,MAAM,EACnB,aAAa,SAAI,GAChB,MAAM,CA6HR"}