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
JavaScript
"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