UNPKG

component-dependency-collapser

Version:

šŸ“¦ Component Dependency Collapser is a CLI tool that helps you analyze, visualize, and trace the dependency structure of your frontend components

443 lines (442 loc) • 19.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.analyzeComponent = analyzeComponent; const ts_morph_1 = require("ts-morph"); const path_1 = __importDefault(require("path")); const fs_1 = __importDefault(require("fs")); const chalk_1 = __importDefault(require("chalk")); const ts = __importStar(require("typescript")); const seenFiles = new Set(); function formatBytes(bytes) { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`; return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; } function calculateDependencySize(sourceFile, visited = new Set(), project, tsConfigPaths = null, baseUrl = null) { const filePath = sourceFile.getFilePath(); if (visited.has(filePath)) return 0; visited.add(filePath); let totalSize = 0; try { const stat = fs_1.default.statSync(filePath); totalSize += stat.size; } catch { // ignore } const imports = sourceFile.getImportDeclarations(); for (const imp of imports) { const spec = imp.getModuleSpecifierValue(); let resolvedPath = null; if (spec.startsWith('.') || spec.startsWith('/')) { resolvedPath = imp.getModuleSpecifierSourceFile()?.getFilePath() ?? null; } else if (tsConfigPaths && baseUrl) { resolvedPath = resolveAliasImport(spec, tsConfigPaths, baseUrl); if (resolvedPath) { for (const ext of ['.ts', '.tsx', '.js', '.jsx']) { const fullPath = `${resolvedPath}${ext}`; if (fs_1.default.existsSync(fullPath)) { resolvedPath = fullPath; break; } } } } if (resolvedPath && fs_1.default.existsSync(resolvedPath)) { const importedFile = project.addSourceFileAtPathIfExists(resolvedPath); if (importedFile) { totalSize += calculateDependencySize(importedFile, visited, project, tsConfigPaths, baseUrl); } } } return totalSize; } // Load tsconfig.json paths and baseUrl function getTSConfigPaths(rootDir) { const configFile = ts.findConfigFile(rootDir, ts.sys.fileExists, 'tsconfig.json'); if (!configFile) return null; const configText = ts.sys.readFile(configFile); if (!configText) return null; const result = ts.parseConfigFileTextToJson(configFile, configText); if (!result.config) return null; const compilerOptions = result.config.compilerOptions || {}; const baseUrl = compilerOptions.baseUrl || '.'; const paths = compilerOptions.paths || {}; return { baseUrl: path_1.default.resolve(path_1.default.dirname(configFile), baseUrl), paths }; } // Resolve alias imports like '@components/Button' -> 'src/components/Button' function resolveAliasImport(specifier, tsConfigPaths, baseUrl) { for (const alias in tsConfigPaths) { const aliasPattern = alias.replace(/\*/g, '(.*)'); const regex = new RegExp(`^${aliasPattern}$`); const match = specifier.match(regex); if (match) { const replacements = tsConfigPaths[alias]; if (replacements && replacements.length > 0) { // Replace '*' with matched group or empty string const replacement = replacements[0].replace('*', match[1] || ''); const resolvedPath = path_1.default.resolve(baseUrl, replacement); return resolvedPath; } } } return null; } async function traceImportChains(project, sourceFile, target, pathStack = [], visited = new Set(), results = [], tsConfigPaths = null, baseUrl = null) { const filePath = sourceFile.getFilePath(); if (visited.has(filePath)) return; visited.add(filePath); pathStack.push(filePath); const imports = sourceFile.getImportDeclarations(); const matchesTarget = imports.some((imp) => { const spec = imp.getModuleSpecifierValue(); const baseName = path_1.default.basename(spec).replace(/\.(tsx?|jsx?)$/, ''); return (spec === target || spec.startsWith(`${target}/`) || baseName === target); }); if (matchesTarget) { results.push([...pathStack]); } else { for (const imp of imports) { const spec = imp.getModuleSpecifierValue(); if (spec.startsWith('.') || spec.startsWith('/')) { const importedFilePath = imp.getModuleSpecifierSourceFile()?.getFilePath(); if (importedFilePath) { const importedFile = project.addSourceFileAtPathIfExists(importedFilePath); if (importedFile) { await traceImportChains(project, importedFile, target, pathStack, visited, results, tsConfigPaths, baseUrl); } } } else if (tsConfigPaths && baseUrl) { // Try resolving alias imports here too const aliasResolved = resolveAliasImport(spec, tsConfigPaths, baseUrl); if (aliasResolved) { for (const ext of ['.ts', '.tsx', '.js', '.jsx', '.mjs']) { const testPath = `${aliasResolved}${ext}`; if (fs_1.default.existsSync(testPath)) { const importedFile = project.addSourceFileAtPathIfExists(testPath); if (importedFile) { await traceImportChains(project, importedFile, target, pathStack, visited, results, tsConfigPaths, baseUrl); } break; } } for (const ext of ['.ts', '.tsx', '.js', '.jsx', '.mjs']) { const testPath = path_1.default.join(aliasResolved, `index${ext}`); if (fs_1.default.existsSync(testPath)) { const importedFile = project.addSourceFileAtPathIfExists(testPath); if (importedFile) { await traceImportChains(project, importedFile, target, pathStack, visited, results, tsConfigPaths, baseUrl); } break; } } } } } } pathStack.pop(); visited.delete(filePath); return results; } function importContainsPackage(sourceFile, target) { const imports = sourceFile.getImportDeclarations(); return imports.some((imp) => { const spec = imp.getModuleSpecifierValue(); // Normalize for internal or external const isMatch = spec === target || spec.startsWith(`${target}/`) || path_1.default.basename(spec).replace(/\.(tsx?|jsx?)$/, '') === target; return isMatch; }); } function analyzeFileRecursive(sourceFile, depth = 0, options = {}, tsConfigPaths = null, baseUrl = null, visitedSize = new Map() // cache for file sizes ) { const indent = ' '.repeat(depth); const filePath = sourceFile.getFilePath(); if (seenFiles.has(filePath)) { console.log(`${indent}${chalk_1.default.gray('(already visited)')} ${path_1.default.basename(filePath)}`); return; } seenFiles.add(filePath); // Calculate own size for current file let sizeStr = ''; if (options.tree) { let size = visitedSize.get(filePath); if (size === undefined) { try { const stat = fs_1.default.statSync(filePath); size = stat.size; } catch { size = 0; } visitedSize.set(filePath, size); } sizeStr = ` (${formatBytes(size)})`; } // Print root file with šŸ“„ and size if depth=0 if (depth === 0 && options.tree) { console.log(`šŸ“„ ${chalk_1.default.cyan(path_1.default.basename(filePath))}${sizeStr}`); } const imports = sourceFile.getImportDeclarations(); // Filter imports for externalOnly flag const visibleImports = imports.filter((imp) => { const specifier = imp.getModuleSpecifierValue(); const isExternal = !specifier.startsWith('.') && !specifier.startsWith('/'); return !options.externalOnly || (options.externalOnly && isExternal); }); visibleImports.forEach((imp, index) => { const specifier = imp.getModuleSpecifierValue(); const isExternal = !specifier.startsWith('.') && !specifier.startsWith('/'); const label = isExternal ? chalk_1.default.yellow('šŸ“¦') : chalk_1.default.cyan('šŸ”—'); const isLast = index === visibleImports.length - 1; const branch = options.tree ? (isLast ? '└── ' : 'ā”œā”€ā”€ ') : ''; // Calculate size for imported file (only internal) let importSizeStr = ''; if (options.tree && !isExternal) { try { const resolvedPath = resolveImport(sourceFile, specifier, tsConfigPaths, baseUrl); if (resolvedPath && fs_1.default.existsSync(resolvedPath)) { let impSize = visitedSize.get(resolvedPath); if (impSize === undefined) { impSize = fs_1.default.statSync(resolvedPath).size; visitedSize.set(resolvedPath, impSize); } importSizeStr = ` (${formatBytes(impSize)})`; } } catch { importSizeStr = ''; } } console.log(`${indent}${branch}${label} ${specifier}${importSizeStr}`); // Recurse only for internal imports if (!isExternal) { try { const resolvedPath = resolveImport(sourceFile, specifier, tsConfigPaths, baseUrl); if (resolvedPath && fs_1.default.existsSync(resolvedPath)) { const childSource = sourceFile.getProject().addSourceFileAtPathIfExists(resolvedPath); if (childSource) { analyzeFileRecursive(childSource, depth + 1, options, tsConfigPaths, baseUrl, visitedSize); } } } catch (e) { console.warn(`${indent}${chalk_1.default.red('āš ļø Failed to resolve')}: ${specifier}`); } } }); } // now resolve @ based imports too function resolveImport(sourceFile, specifier, tsConfigPaths, baseUrl) { const baseDir = path_1.default.dirname(sourceFile.getFilePath()); if (specifier.startsWith('.') || specifier.startsWith('/')) { // Relative import - existing logic const fullPath = path_1.default.resolve(baseDir, specifier); for (const ext of ['.ts', '.tsx', '.js', '.jsx', '.mjs']) { const testPath = `${fullPath}${ext}`; if (fs_1.default.existsSync(testPath)) return testPath; } for (const ext of ['.ts', '.tsx', '.js', '.jsx', '.mjs']) { const testPath = path_1.default.join(fullPath, `index${ext}`); if (fs_1.default.existsSync(testPath)) return testPath; } return null; } // Non-relative, try alias resolution if available if (tsConfigPaths && baseUrl) { const aliasResolved = resolveAliasImport(specifier, tsConfigPaths, baseUrl); if (aliasResolved) { for (const ext of ['.ts', '.tsx', '.js', '.jsx', '.mjs']) { const testPath = `${aliasResolved}${ext}`; if (fs_1.default.existsSync(testPath)) return testPath; } for (const ext of ['.ts', '.tsx', '.js', '.jsx', '.mjs']) { const testPath = path_1.default.join(aliasResolved, `index${ext}`); if (fs_1.default.existsSync(testPath)) return testPath; } } } // Could be an external package or unresolved import return null; } //trace function added async function analyzeComponent(entryPath, options) { const project = new ts_morph_1.Project(); const allFiles = []; const tsConfig = getTSConfigPaths(process.cwd()); const tsConfigPaths = tsConfig?.paths || null; const baseUrl = tsConfig?.baseUrl || null; if (fs_1.default.lstatSync(entryPath).isDirectory()) { const fastGlob = await Promise.resolve().then(() => __importStar(require('fast-glob'))); const matches = await fastGlob.default(`${entryPath}/**/*.{ts,tsx,js,jsx}`); allFiles.push(...matches.map((m) => path_1.default.resolve(m))); } else { allFiles.push(path_1.default.resolve(entryPath)); } const targetPackage = options.find; const traceTarget = options.trace; const sizeTarget = options.size; const foundIn = []; if (sizeTarget) { const sizeResults = []; for (const filePath of allFiles) { const sourceFile = project.addSourceFileAtPathIfExists(filePath); if (!sourceFile) continue; const totalSizeBytes = calculateDependencySize(sourceFile, new Set(), project, tsConfigPaths, baseUrl); sizeResults.push({ file: path_1.default.relative(process.cwd(), filePath), size: totalSizeBytes }); } sizeResults.sort((a, b) => b.size - a.size); console.log(chalk_1.default.green(`\nšŸ“¦ Component Size Analysis:`)); console.log(); sizeResults.forEach((res, idx) => { const rankStyle = idx === 0 ? chalk_1.default.red.bold : idx === 1 ? chalk_1.default.yellow.bold : idx === 2 ? chalk_1.default.magenta : chalk_1.default.cyan; console.log(`${rankStyle(res.file)} → ${chalk_1.default.bold(formatBytes(res.size))}`); }); console.log(); // spacing return; } if (traceTarget) { let totalResults = []; for (const filePath of allFiles) { const sourceFile = project.addSourceFileAtPathIfExists(filePath); if (!sourceFile) continue; // const chains = await traceImportChains(project, sourceFile, traceTarget); const chains = await traceImportChains(project, sourceFile, traceTarget, [], new Set(), [], tsConfigPaths, baseUrl); if (chains && chains.length > 0) { totalResults = totalResults.concat(chains); } } if (totalResults.length === 0) { console.log(chalk_1.default.yellow(`āš ļø No import chains found to: ${traceTarget}`)); } else { console.log(chalk_1.default.green(`āœ” Found import chains to: ${traceTarget}\n`)); for (const chain of totalResults) { // Indented tree output // for (let i = 0; i < chain.length; i++) { // const indent = ' '.repeat(i); // console.log(`${indent}${path.relative(process.cwd(), chain[i])}`); // } for (let i = 0; i < chain.length; i++) { const indent = ' '.repeat(i); const fileRelative = path_1.default.relative(process.cwd(), chain[i]); if (i === 0) { // First item: print with šŸ“„ console.log(`šŸ“„ ${fileRelative}`); } else { // Subsequent items: print with arrow console.log(`${indent}↳ ${fileRelative}`); } } console.log(); // Compact arrow-chain output const compact = chain .map((f) => path_1.default.basename(f).replace(/\.(tsx?|jsx?)$/, '')) .join(' → '); console.log(`šŸ”— Chain: ${compact}\n`); } } return; } // Existing find mode if (targetPackage) { for (const filePath of allFiles) { const sourceFile = project.addSourceFileAtPathIfExists(filePath); if (!sourceFile) continue; if (importContainsPackage(sourceFile, targetPackage)) { foundIn.push(filePath); } } if (foundIn.length === 0) { console.log(chalk_1.default.yellow(`āš ļø No files found importing: ${targetPackage}`)); } else { console.log(chalk_1.default.green(`\nāœ” Found in:`)); foundIn.forEach((file) => { console.log(`- ${path_1.default.relative(process.cwd(), file)}`); }); } return; } // Normal or tree mode const htmlTrees = []; // Normal or tree mode for (const filePath of allFiles) { const sourceFile = project.addSourceFileAtPathIfExists(filePath); if (!sourceFile) continue; seenFiles.clear(); console.log(chalk_1.default.green(`\nšŸ“ Component: ${path_1.default.relative(process.cwd(), filePath)}\n`)); // analyzeFileRecursive(sourceFile, 0, options); // analyzeFileRecursive(sourceFile, 0, options, tsConfigPaths, baseUrl); analyzeFileRecursive(sourceFile, 0, options, tsConfigPaths, baseUrl, new Map() // track visited file sizes ); } }