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
JavaScript
;
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
);
}
}