Initial ecommerce standalone package
This commit is contained in:
@@ -0,0 +1,301 @@
|
||||
/**
|
||||
* 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('═══════════════════════════════════════════════');
|
||||
Reference in New Issue
Block a user