/** * Dynamic analysis without Playwright - uses Node.js to analyze module structure, * dependency graph, import patterns, and potential runtime costs. */ import { readFileSync, readdirSync, statSync } from 'fs'; import { join, relative, basename } from 'path'; const SRC = join(import.meta.dirname, '..', 'src'); const DIST = join(import.meta.dirname, '..', 'dist'); const results = []; function walk(dir) { for (const entry of readdirSync(dir, { withFileTypes: true })) { const full = join(dir, entry.name); if (entry.isDirectory() && entry.name !== 'node_modules') walk(full); else if (/\.(tsx?|jsx?)$/.test(entry.name)) { const content = readFileSync(full, 'utf-8'); results.push({ file: relative(join(SRC, '..'), full), content, lines: content.split('\n').length }); } } } walk(SRC); // ─── 1. Dependency Graph Analysis ─── console.log('═══════════════════════════════════════════════'); console.log(' 1. MODULE DEPENDENCY GRAPH ANALYSIS'); console.log('═══════════════════════════════════════════════'); const importMap = new Map(); // file -> [imports] const importedBy = new Map(); // file -> [importers] for (const r of results) { const imports = []; // Match import statements const importRe = /import\s+(?:.*?\s+from\s+)?['"]([^'"]+)['"]/g; let m; while ((m = importRe.exec(r.content)) !== null) { imports.push(m[1]); } // Match dynamic imports const dynRe = /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g; while ((m = dynRe.exec(r.content)) !== null) { imports.push(`[dynamic]${m[1]}`); } importMap.set(r.file, imports); } // Find circular dependencies console.log('\n--- Circular Dependency Detection ---'); function findCircular(file, visited = new Set(), path = []) { if (visited.has(file)) { if (path.includes(file)) { console.log(` [CIRCULAR] ${path.slice(path.indexOf(file)).join(' -> ')} -> ${file}`); } return; } visited.add(file); path.push(file); const deps = importMap.get(file) || []; for (const dep of deps) { if (dep.startsWith('.') || dep.startsWith('/')) { // Resolve relative path const dir = file.split('/').slice(0, -1).join('/'); const resolved = dep.replace(/^\.\//, dir + '/').replace(/^\.\.\//, ''); // Find matching file for (const r of results) { if (r.file.includes(resolved) || r.file.includes(basename(resolved))) { findCircular(r.file, new Set(visited), [...path]); } } } } } // Check high-fanin files (imported by many) const fanIn = new Map(); for (const [file, imports] of importMap) { for (const imp of imports) { const key = imp.replace(/\[dynamic\]/, ''); fanIn.set(key, (fanIn.get(key) || 0) + 1); } } console.log('\n--- High Fan-In Modules (most imported) ---'); const sortedFanIn = [...fanIn.entries()].sort((a, b) => b[1] - a[1]).slice(0, 15); for (const [mod, count] of sortedFanIn) { const bar = '█'.repeat(Math.min(30, count)); console.log(` ${mod.padEnd(50)} ${String(count).padStart(3)}x ${bar}`); } // Check high-fanout files (import many) console.log('\n--- High Fan-Out Modules (import most) ---'); const sortedFanOut = [...importMap.entries()] .map(([f, imps]) => [f, imps.length]) .sort((a, b) => b[1] - a[1]).slice(0, 15); for (const [file, count] of sortedFanOut) { const bar = '█'.repeat(Math.min(30, count)); console.log(` ${file.padEnd(50)} ${String(count).padStart(3)} deps ${bar}`); } // Dynamic imports analysis (lazy loading effectiveness) console.log('\n--- Lazy Loading (Dynamic Imports) ---'); let dynamicImports = 0, staticImports = 0; for (const [file, imports] of importMap) { for (const imp of imports) { if (imp.startsWith('[dynamic]')) dynamicImports++; else staticImports++; } } console.log(` Static imports: ${staticImports}`); console.log(` Dynamic imports: ${dynamicImports}`); console.log(` Lazy load ratio: ${((dynamicImports / (staticImports + dynamicImports)) * 100).toFixed(1)}%`); // Find files that should be lazy loaded but aren't const largePages = results.filter(r => r.lines > 500 && r.file.includes('Page')); for (const r of largePages) { const isLazyImported = [...importMap.values()].some(imps => imps.some(i => i.startsWith('[dynamic]') && i.includes(basename(r.file, '.tsx'))) ); if (!isLazyImported && !r.file.includes('App')) { // Check if it's referenced in App.tsx const appContent = results.find(x => x.file === 'src/App.tsx')?.content || ''; if (appContent.includes(basename(r.file, '.tsx'))) { console.log(` [INFO] ${r.file} (${r.lines} lines) - loaded via App.tsx`); } } } // ─── 2. React Rendering Cost Analysis ─── console.log('\n═══════════════════════════════════════════════'); console.log(' 2. REACT RENDERING COST ANALYSIS'); console.log('═══════════════════════════════════════════════'); // Count useState/useReducer per component (state update triggers re-render) console.log('\n--- State Hook Density ---'); for (const r of results) { if (!r.file.endsWith('.tsx')) continue; const stateHooks = (r.content.match(/useState\s*[<(]/g) || []).length; const reducers = (r.content.match(/useReducer\s*[<(]/g) || []).length; const effects = (r.content.match(/useEffect\s*\(/g) || []).length; const memos = (r.content.match(/useMemo\s*\(/g) || []).length; const callbacks = (r.content.match(/useCallback\s*[<(]/g) || []).length; const refs = (r.content.match(/useRef\s*[<(]/g) || []).length; const totalHooks = stateHooks + reducers + effects + memos + callbacks + refs; if (totalHooks > 15) { const risk = totalHooks > 30 ? '🔴 HIGH' : totalHooks > 20 ? '🟡 MEDIUM' : '🟢 LOW'; console.log(` ${risk} ${r.file}`); console.log(` useState:${stateHooks} useReducer:${reducers} useEffect:${effects} useMemo:${memos} useCallback:${callbacks} useRef:${refs} (total:${totalHooks})`); // Check if there are many state updates that could be batched const setters = r.content.match(/set\w+\(/g) || []; if (setters.length > 20) { console.log(` ⚠️ ${setters.length} state setter calls — potential for excessive re-renders`); } // Check for missing useMemo on expensive computations const expensiveInRender = (r.content.match(/\.map\(|\.filter\(|\.reduce\(|\.sort\(/g) || []).length; if (expensiveInRender > 5 && memos === 0) { console.log(` ⚠️ ${expensiveInRender} array operations in render body with 0 useMemo`); } } } // ─── 3. useEffect Dependency Analysis ─── console.log('\n═══════════════════════════════════════════════'); console.log(' 3. useEffect DEPENDENCY ANALYSIS'); console.log('═══════════════════════════════════════════════'); for (const r of results) { if (!r.file.endsWith('.tsx')) continue; // Find useEffect with no dependency array (runs every render) const noDeps = (r.content.match(/useEffect\s*\(\s*\(\)\s*=>\s*\{[\s\S]*?\}\s*\)/g) || []).length; // Find useEffect with empty deps const emptyDeps = (r.content.match(/useEffect\s*\(\s*\(\)\s*=>\s*\{[\s\S]*?\}\s*,\s*\[\s*\]\s*\)/g) || []).length; if (noDeps > 0) { console.log(` [RENDER-COST] ${r.file}: ${noDeps} useEffect(s) run EVERY render`); } } // ─── 4. Zustand Store Analysis ─── console.log('\n═══════════════════════════════════════════════'); console.log(' 4. ZUSTAND STORE ANALYSIS'); console.log('═══════════════════════════════════════════════'); const storeFiles = results.filter(r => r.file.includes('store') || r.file.includes('Store')); for (const r of storeFiles) { const stateFields = (r.content.match(/^\s+\w+:/gm) || []).length; const actions = (r.content.match(/^\s+\w+\s*:\s*(\(|function|\w+\s*=>)/gm) || []).length; const subscribers = (r.content.match(/subscribe\s*\(/g) || []).length; console.log(`\n ${r.file}`); console.log(` State fields: ~${stateFields}`); console.log(` Actions: ~${actions}`); console.log(` Subscribers: ${subscribers}`); // Check for selector usage patterns if (r.content.includes('set(') && !r.content.includes('useShallow')) { console.log(` ⚠️ Uses set() without useShallow — may cause unnecessary re-renders`); } } // ─── 5. Bundle Composition Analysis ─── console.log('\n═══════════════════════════════════════════════'); console.log(' 5. BUNDLE COMPOSITION ANALYSIS'); console.log('═══════════════════════════════════════════════'); if (readdirSync(DIST).includes('assets')) { const assets = readdirSync(join(DIST, 'assets')); const jsFiles = assets.filter(f => f.endsWith('.js') && !f.endsWith('.br')); const cssFiles = assets.filter(f => f.endsWith('.css') && !f.endsWith('.br')); let totalJs = 0, totalCss = 0; const chunks = []; for (const f of jsFiles) { const size = statSync(join(DIST, 'assets', f)).size; totalJs += size; chunks.push({ name: f, sizeKB: size / 1024, type: 'js' }); } for (const f of cssFiles) { const size = statSync(join(DIST, 'assets', f)).size; totalCss += size; chunks.push({ name: f, sizeKB: size / 1024, type: 'css' }); } chunks.sort((a, b) => b.sizeKB - a.sizeKB); console.log(`\n Total JS: ${(totalJs / 1024).toFixed(1)} KB (${(totalJs / 1024 / 1024).toFixed(2)} MB)`); console.log(` Total CSS: ${(totalCss / 1024).toFixed(1)} KB (${(totalCss / 1024 / 1024).toFixed(2)} MB)`); console.log(` Total: ${((totalJs + totalCss) / 1024).toFixed(1)} KB`); // CSS is suspiciously large const cssRatio = totalCss / totalJs; if (cssRatio > 0.5) { console.log(`\n ⚠️ CSS/JS ratio: ${(cssRatio * 100).toFixed(0)}% — CSS bundle may contain unused styles`); console.log(` Consider: PurgeCSS, CSS Modules, or extracting to separate loads`); } // Check initial load budget const initialChunks = chunks.filter(c => c.name.includes('index') || c.name.includes('vendor-react') || c.name.includes('vendor-antd') ); const initialLoad = initialChunks.reduce((s, c) => s + c.sizeKB, 0); console.log(`\n Critical path (initial load):`); for (const c of initialChunks) { console.log(` ${c.name.padEnd(45)} ${c.sizeKB.toFixed(1).padStart(8)} KB`); } console.log(` ${'TOTAL'.padEnd(45)} ${initialLoad.toFixed(1).padStart(8)} KB`); if (initialLoad > 300) { console.log(`\n ⚠️ Initial load > 300KB — consider code splitting or tree-shaking`); } } // ─── 6. API Client Efficiency ─── console.log('\n═══════════════════════════════════════════════'); console.log(' 6. API CLIENT EFFICIENCY'); console.log('═══════════════════════════════════════════════'); const apiFiles = results.filter(r => r.file.includes('src/api/')); for (const r of apiFiles) { const fetchCalls = (r.content.match(/fetch\s*\(/g) || []).length; const retries = (r.content.match(/retry|Retry|RETRY/g) || []).length; const abortSignals = (r.content.match(/AbortSignal|AbortController/g) || []).length; const timeouts = (r.content.match(/timeout|Timeout|setTimeout/g) || []).length; if (fetchCalls > 0) { console.log(`\n ${r.file}`); console.log(` fetch calls: ${fetchCalls}`); console.log(` retry logic: ${retries > 0 ? '✅' : '❌ none'}`); console.log(` abort signals: ${abortSignals > 0 ? '✅' : '❌ none'}`); console.log(` timeouts: ${timeouts}`); } } // ─── 7. TypeScript Compilation Metrics ─── console.log('\n═══════════════════════════════════════════════'); console.log(' 7. TYPE COMPLEXITY'); console.log('═══════════════════════════════════════════════'); let totalTypes = 0, totalInterfaces = 0, totalEnums = 0, totalGenerics = 0; for (const r of results) { totalTypes += (r.content.match(/^export\s+type\s+/gm) || []).length; totalTypes += (r.content.match(/^type\s+/gm) || []).length; totalInterfaces += (r.content.match(/^export\s+interface\s+/gm) || []).length; totalInterfaces += (r.content.match(/^interface\s+/gm) || []).length; totalEnums += (r.content.match(/enum\s+\w+/g) || []).length; totalGenerics += (r.content.match(/<\w+(?:\s*,\s*\w+)*>/g) || []).length; } console.log(` Type aliases: ${totalTypes}`); console.log(` Interfaces: ${totalInterfaces}`); console.log(` Enums: ${totalEnums}`); console.log(` Generic usages: ${totalGenerics}`); console.log(` Total TS files: ${results.length}`); // ─── Final Summary ─── console.log('\n═══════════════════════════════════════════════'); console.log(' ANALYSIS COMPLETE'); console.log('═══════════════════════════════════════════════');