UNPKG

gather-ts

Version:

A powerful code analysis and packaging tool designed for creating AI-friendly code representations for javascript and typescript projects.

394 lines 17.3 kB
"use strict"; // src/core/dependency/DependencyAnalyzer.ts var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.DependencyAnalyzer = void 0; const madge_1 = __importDefault(require("madge")); const events_1 = require("events"); const errors_1 = require("@/errors"); class DependencyAnalyzer extends events_1.EventEmitter { constructor(deps, options = {}) { super(); this.deps = deps; this.isInitialized = false; this.debug = options.debug || false; this.webpackConfigPath = options.webpackConfigPath; this.tsConfigPath = options.tsConfigPath || "tsconfig.json"; this.fileExtensions = options.fileExtensions || ["ts", "tsx"]; this.maxConcurrency = options.maxConcurrency || 4; this.originalCwd = process.cwd(); } logDebug(message) { if (this.debug) { this.deps.logger.debug(`[DependencyAnalyzer] ${message}`); } } checkInitialized() { if (!this.isInitialized) { throw new Error("DependencyAnalyzer not initialized"); } } async initialize() { this.logDebug("Initializing DependencyAnalyzer"); try { if (this.isInitialized) { this.logDebug("DependencyAnalyzer already initialized"); return; } // Verify configuration files exist if (this.webpackConfigPath && !this.deps.fileSystem.exists(this.webpackConfigPath)) { this.deps.logger.warn(`Webpack config not found at: ${this.webpackConfigPath}`); } if (!this.deps.fileSystem.exists(this.tsConfigPath)) { this.deps.logger.warn(`TypeScript config not found at: ${this.tsConfigPath}`); } // Store original working directory this.originalCwd = process.cwd(); this.isInitialized = true; this.logDebug("DependencyAnalyzer initialization complete"); } catch (error) { this.handleError("initialize", error); } } cleanup() { this.logDebug("Cleaning up DependencyAnalyzer"); this.restoreWorkingDirectory(); this.removeAllListeners(); this.isInitialized = false; } handleError(operation, error, context) { const message = error instanceof Error ? error.message : String(error); const analysisError = new errors_1.DependencyAnalysisError(`Dependency analysis ${operation} failed: ${message}`, context?.entryPoint, context?.dependencies); this.deps.logger.error(analysisError.message); this.emit("analysis:error", { error: analysisError, phase: operation, timestamp: Date.now(), }); throw analysisError; } changeWorkingDirectory(dir) { this.originalCwd = process.cwd(); process.chdir(dir); this.logDebug(`Changed working directory to: ${dir}`); } restoreWorkingDirectory() { if (process.cwd() !== this.originalCwd) { process.chdir(this.originalCwd); this.logDebug(`Restored working directory to: ${this.originalCwd}`); } } emitProgress(phase, completed, total, currentFile) { this.emit("progress", { phase, completed, total, currentFile, }); } async validateEntryFiles(entryFiles, rootDir, options = {}) { if (!this.isInitialized) { throw new errors_1.ValidationError("DependencyAnalyzer not initialized"); } this.logDebug(`Validating ${entryFiles.length} entry files in ${rootDir}`); const validationResult = { isValid: true, validFiles: [], invalidFiles: [], errors: [], }; let processed = 0; const total = entryFiles.length; for (const file of entryFiles) { try { const resolvedPath = this.deps.fileSystem.resolvePath(rootDir, file.trim()); if (!this.deps.fileSystem.exists(resolvedPath)) { throw new errors_1.ValidationError("Entry file not found", { file: resolvedPath, }); } if (!options.includeNodeModules && this.deps.ignoreHandler.shouldIgnore(resolvedPath)) { throw new errors_1.ValidationError("Entry file is ignored", { file: resolvedPath, ignorePatterns: this.deps.ignoreHandler.getPatterns(), }); } validationResult.validFiles.push(resolvedPath); this.logDebug(`Validated entry file: ${resolvedPath}`); processed++; this.emitProgress("validation", processed, total, file); } catch (error) { validationResult.isValid = false; validationResult.invalidFiles.push(file); validationResult.errors.push({ file, error: error instanceof Error ? error.message : String(error), }); this.emit("warning", { message: `Failed to validate file: ${file}`, file, timestamp: Date.now(), }); } } if (!validationResult.isValid) { this.handleError("validate", new errors_1.ValidationError("Entry file validation failed", { errors: validationResult.errors, invalidFiles: validationResult.invalidFiles, })); } return validationResult.validFiles; } async analyzeDependencies(entryFiles, projectRoot, options = {}) { if (!this.isInitialized) { throw new errors_1.ValidationError("DependencyAnalyzer not initialized"); } const startTime = Date.now(); const files = Array.isArray(entryFiles) ? entryFiles : [entryFiles]; this.logDebug(`Project root: ${projectRoot}`); this.logDebug(`Entry files: ${files.join(", ")}`); if (options.useWorkingDir) { this.changeWorkingDirectory(projectRoot); } try { this.emit("analysis:start", { entryFiles: files, timestamp: startTime, }); // Convert entry files to absolute paths if they aren't already const absoluteFiles = files.map((file) => this.deps.fileSystem.isAbsolute(file) ? file : this.deps.fileSystem.resolvePath(projectRoot, file)); // Get relative paths for madge const relativeFiles = absoluteFiles.map((file) => this.deps.fileSystem.getRelativePath(projectRoot, file)); this.logDebug(`Analyzing dependencies for: ${relativeFiles.join(", ")}`); // Add the debug statements here this.logDebug(`Project root: ${projectRoot}`); this.logDebug(`Analyzing dependencies for: ${relativeFiles.join(", ")}`); this.logDebug(`Using TSConfig: ${this.deps.fileSystem.joinPath(projectRoot, this.tsConfigPath)}`); this.logDebug(`File extensions: ${this.fileExtensions.join(", ")}`); // Check cache if enabled if (!options.skipCache && this.deps.cache) { const cacheKey = this.getCacheKey(relativeFiles, projectRoot); const cachedDeps = this.deps.cache.get(cacheKey); if (cachedDeps) { const result = { entryFiles: absoluteFiles, dependencies: cachedDeps, circularDependencies: await this.getCircularDependencies(cachedDeps), totalFiles: Object.keys(cachedDeps).length, analysisTime: Date.now() - startTime, }; this.logDebug("Returned cached dependency analysis"); return result; } } const ignorePatterns = this.deps.ignoreHandler .getPatterns() .map((pattern) => new RegExp(pattern .replace(/\./g, "\\.") .replace(/\*\*/g, ".*") .replace(/\*/g, "[^/]*") .replace(/\?/g, "."))); const madgeConfig = { baseDir: projectRoot, tsConfig: this.deps.fileSystem.joinPath(projectRoot, this.tsConfigPath), fileExtensions: this.fileExtensions, excludeRegExp: options.includeNodeModules ? [] : [/node_modules/, ...ignorePatterns], detectiveOptions: { ts: { mixedImports: true }, tsx: { mixedImports: true }, }, followSymlinks: options.followSymlinks || false, cwd: projectRoot, // Add this line ...options.madgeConfig, }; // Before calling madge, ensure we're in the right directory this.changeWorkingDirectory(projectRoot); // Process each entry file let processedFiles = 0; const dependencyMaps = await Promise.all(relativeFiles.map(async (entryFile) => { this.logDebug(`Processing dependencies for: ${entryFile}`); this.emitProgress("analysis", ++processedFiles, relativeFiles.length, entryFile); try { const madgeResult = await (0, madge_1.default)(entryFile, madgeConfig); return await madgeResult.obj(); } catch (error) { this.deps.logger.error(`Failed to analyze dependencies for ${entryFile}: ${error}`); throw error; } })); const dependencies = {}; let totalFiles = 0; dependencyMaps.forEach((deps) => { Object.entries(deps).forEach(([file, fileDeps]) => { const absFile = this.deps.fileSystem.resolvePath(projectRoot, file); if (options.includeNodeModules || !this.deps.ignoreHandler.shouldIgnore(absFile)) { const validDeps = fileDeps .map((dep) => this.deps.fileSystem.resolvePath(projectRoot, dep)) .filter((dep) => options.includeNodeModules || !this.deps.ignoreHandler.shouldIgnore(dep)); if (validDeps.length > 0) { dependencies[absFile] = Array.from(new Set([...(dependencies[absFile] || []), ...validDeps])); totalFiles++; } } }); }); // Cache the result if enabled if (!options.skipCache && this.deps.cache) { const cacheKey = this.getCacheKey(relativeFiles, projectRoot); this.deps.cache.set(cacheKey, dependencies); } const circularDependencies = await this.getCircularDependencies(dependencies); const result = { entryFiles: absoluteFiles, dependencies, circularDependencies, totalFiles, analysisTime: Date.now() - startTime, warnings: circularDependencies.length > 0 ? [`Found ${circularDependencies.length} circular dependencies`] : undefined, }; this.emit("analysis:complete", { result, timestamp: Date.now(), }); this.logDebug(`Analysis complete: ${totalFiles} files, ` + `${circularDependencies.length} circular dependencies, ` + `${result.analysisTime}ms`); return result; } catch (error) { this.handleError("analyze", error, { entryPoint: Array.isArray(entryFiles) ? entryFiles[0] : entryFiles, dependencies: Array.isArray(entryFiles) ? entryFiles : [entryFiles], }); } finally { if (options.useWorkingDir) { this.restoreWorkingDirectory(); } } // This return will never be reached because handleError always throws, // but TypeScript needs it to understand all paths return something return Promise.reject(new Error("Unreachable")); } async gatherDependencies(adjacencyList, entryFiles, maxDepth) { if (!this.isInitialized) { throw new errors_1.ValidationError("DependencyAnalyzer not initialized"); } this.logDebug(`Gathering dependencies with max depth: ${maxDepth || "unlimited"}`); const queue = []; const visited = new Set(); let processed = 0; const total = Object.keys(adjacencyList).length; for (const file of entryFiles) { if (!this.deps.ignoreHandler.shouldIgnore(file)) { queue.push({ file, depth: 0 }); visited.add(file); this.logDebug(`Added entry file to queue: ${file}`); } } while (queue.length > 0) { const { file, depth } = queue.shift(); if (maxDepth !== undefined && depth >= maxDepth) { this.logDebug(`Skipping ${file} - max depth (${maxDepth}) reached`); continue; } const children = adjacencyList[file] || []; for (const child of children) { if (!this.deps.ignoreHandler.shouldIgnore(child) && !visited.has(child)) { visited.add(child); queue.push({ file: child, depth: depth + 1 }); this.logDebug(`Added dependency: ${child} (depth: ${depth + 1})`); } } processed++; this.emitProgress("gathering", processed, total, file); } const result = Array.from(visited); this.logDebug(`Gathered ${result.length} total dependencies`); // Filter out empty files const nonEmptyFiles = await Promise.all(result.map(async (filePath) => { try { const content = await this.deps.fileSystem.readFile(filePath); return content.trim() !== "" ? filePath : null; } catch (error) { this.deps.logger.warn(`Error reading file ${filePath}: ${error}`); return null; } })); return nonEmptyFiles.filter((file) => file !== null); } async getCircularDependencies(dependencyMap) { this.logDebug("Analyzing circular dependencies"); const visited = new Set(); const recursionStack = new Set(); const cycles = []; const dfs = (node, path = []) => { visited.add(node); recursionStack.add(node); path.push(node); const dependencies = dependencyMap[node] || []; for (const dependency of dependencies) { if (!visited.has(dependency)) { dfs(dependency, [...path]); } else if (recursionStack.has(dependency)) { const cycleStart = path.indexOf(dependency); const cycle = path.slice(cycleStart); if (!cycles.some((existing) => existing.length === cycle.length && existing.every((value, index) => value === cycle[index]))) { cycles.push(cycle); this.emit("warning", { message: `Found circular dependency: ${cycle.join(" -> ")}`, timestamp: Date.now(), }); } } } recursionStack.delete(node); path.pop(); }; for (const node of Object.keys(dependencyMap)) { if (!visited.has(node)) { dfs(node); } } if (cycles.length > 0) { this.logDebug(`Found ${cycles.length} circular dependencies`); cycles.forEach((cycle, index) => { this.logDebug(`Cycle ${index + 1}: ${cycle.join(" -> ")}`); }); } return cycles; } getCacheKey(relativeFiles, projectRoot) { const content = JSON.stringify({ files: relativeFiles.sort(), // Sort for consistent keys root: projectRoot, extensions: this.fileExtensions, config: { tsConfig: this.tsConfigPath, webpackConfig: this.webpackConfigPath, }, }); return require("crypto").createHash("md5").update(content).digest("hex"); } } exports.DependencyAnalyzer = DependencyAnalyzer; //# sourceMappingURL=DependencyAnalyzer.js.map