@context-sync/server
Version:
MCP server for AI context sync with persistent memory, workspace file access, and intelligent code operations
449 lines • 16.7 kB
JavaScript
import * as fs from 'fs';
import * as path from 'path';
import * as chokidar from 'chokidar';
export class DependencyAnalyzer {
workspacePath;
fileCache;
dependencyCache;
fileWatcher = null;
// File size limits to prevent OOM crashes
MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB - prevents OOM crashes
WARN_FILE_SIZE = 1 * 1024 * 1024; // 1MB - warn but still process
constructor(workspacePath) {
this.workspacePath = workspacePath;
this.fileCache = new Map();
this.dependencyCache = new Map();
this.setupFileWatcher();
}
/**
* Set up file watcher for cache invalidation
*/
setupFileWatcher() {
const watchPatterns = [
path.join(this.workspacePath, '**/*.{ts,tsx,js,jsx,mjs,cjs}'),
];
this.fileWatcher = chokidar.watch(watchPatterns, {
ignored: [
'**/node_modules/**',
'**/.git/**',
'**/dist/**',
'**/build/**',
'**/.next/**',
'**/out/**',
'**/coverage/**'
],
ignoreInitial: true,
persistent: true,
awaitWriteFinish: {
stabilityThreshold: 100,
pollInterval: 50
}
});
this.fileWatcher
.on('change', (filePath) => {
this.invalidateCache(filePath);
})
.on('add', (filePath) => {
this.invalidateCache(filePath);
})
.on('unlink', (filePath) => {
this.invalidateCache(filePath);
})
.on('error', (error) => {
console.error('Dependency analyzer file watcher error:', error);
});
}
/**
* Invalidate caches for a specific file
*/
invalidateCache(filePath) {
// Remove file from cache
this.fileCache.delete(filePath);
// Remove dependency graph from cache
this.dependencyCache.delete(filePath);
// Also invalidate any dependent files (files that import this file)
for (const [cachedFile, graph] of this.dependencyCache.entries()) {
if (graph.dependencies.includes(filePath) || graph.importers.includes(filePath)) {
this.dependencyCache.delete(cachedFile);
}
}
console.error(`🔄 Dependency cache invalidated: ${path.relative(this.workspacePath, filePath)}`);
}
/**
* Main method: Analyze all dependencies for a file
*/
analyzeDependencies(filePath) {
const absolutePath = this.resolveFilePath(filePath);
// Check cache first
if (this.dependencyCache.has(absolutePath)) {
return this.dependencyCache.get(absolutePath);
}
const imports = this.getImports(absolutePath);
const exports = this.getExports(absolutePath);
const importers = this.findImporters(absolutePath);
const dependencies = imports.map(imp => this.resolveImportPath(absolutePath, imp.source)).filter(Boolean);
const circularDeps = this.detectCircularDependencies(absolutePath);
const graph = {
filePath: absolutePath,
imports,
exports,
importers,
dependencies,
circularDeps
};
this.dependencyCache.set(absolutePath, graph);
return graph;
}
/**
* Get all imports from a file
*/
getImports(filePath) {
const content = this.readFile(filePath);
const imports = [];
// Regex patterns for different import styles
const patterns = [
// ES6 imports: import { x, y } from 'module'
/import\s+{([^}]+)}\s+from\s+['"]([^'"]+)['"]/g,
// Default import: import React from 'react'
/import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g,
// Namespace import: import * as name from 'module'
/import\s+\*\s+as\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g,
// Side-effect import: import 'module'
/import\s+['"]([^'"]+)['"]/g,
// require(): const x = require('module')
/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
];
const lines = content.split('\n');
lines.forEach((line, lineNumber) => {
// ES6 named imports
const namedMatch = /import\s+{([^}]+)}\s+from\s+['"]([^'"]+)['"]/.exec(line);
if (namedMatch) {
const importedNames = namedMatch[1].split(',').map(s => s.trim().split(/\s+as\s+/)[0]);
const source = namedMatch[2];
imports.push({
source,
importedNames,
isExternal: this.isExternalModule(source),
line: lineNumber + 1,
rawStatement: line.trim()
});
return;
}
// Default import
const defaultMatch = /import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/.exec(line);
if (defaultMatch && !line.includes('*')) {
imports.push({
source: defaultMatch[2],
importedNames: [],
defaultImport: defaultMatch[1],
isExternal: this.isExternalModule(defaultMatch[2]),
line: lineNumber + 1,
rawStatement: line.trim()
});
return;
}
// Namespace import
const namespaceMatch = /import\s+\*\s+as\s+(\w+)\s+from\s+['"]([^'"]+)['"]/.exec(line);
if (namespaceMatch) {
imports.push({
source: namespaceMatch[2],
importedNames: [],
namespaceImport: namespaceMatch[1],
isExternal: this.isExternalModule(namespaceMatch[2]),
line: lineNumber + 1,
rawStatement: line.trim()
});
return;
}
// Side-effect import
const sideEffectMatch = /^import\s+['"]([^'"]+)['"]/.exec(line.trim());
if (sideEffectMatch) {
imports.push({
source: sideEffectMatch[1],
importedNames: [],
isExternal: this.isExternalModule(sideEffectMatch[1]),
line: lineNumber + 1,
rawStatement: line.trim()
});
return;
}
// require()
const requireMatch = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/.exec(line);
if (requireMatch) {
imports.push({
source: requireMatch[1],
importedNames: [],
isExternal: this.isExternalModule(requireMatch[1]),
line: lineNumber + 1,
rawStatement: line.trim()
});
}
});
return imports;
}
/**
* Get all exports from a file
*/
getExports(filePath) {
const content = this.readFile(filePath);
const exports = [];
const lines = content.split('\n');
lines.forEach((line, lineNumber) => {
// Named exports: export { x, y }
const namedMatch = /export\s+{([^}]+)}/.exec(line);
if (namedMatch) {
const exportedNames = namedMatch[1].split(',').map(s => s.trim().split(/\s+as\s+/)[0]);
exports.push({
exportedNames,
hasDefaultExport: false,
line: lineNumber + 1,
rawStatement: line.trim()
});
return;
}
// Export declaration: export const x = ...
const declMatch = /export\s+(const|let|var|function|class|interface|type|enum)\s+(\w+)/.exec(line);
if (declMatch) {
exports.push({
exportedNames: [declMatch[2]],
hasDefaultExport: false,
line: lineNumber + 1,
rawStatement: line.trim()
});
return;
}
// Default export
if (line.includes('export default')) {
exports.push({
exportedNames: [],
hasDefaultExport: true,
line: lineNumber + 1,
rawStatement: line.trim()
});
}
});
return exports;
}
/**
* Find all files that import the given file
*/
findImporters(filePath, maxFiles = 1000) {
const absolutePath = this.resolveFilePath(filePath);
const importers = [];
const allFiles = this.getAllProjectFiles(maxFiles);
for (const file of allFiles) {
if (file === absolutePath)
continue;
const imports = this.getImports(file);
for (const imp of imports) {
const resolvedImport = this.resolveImportPath(file, imp.source);
if (resolvedImport === absolutePath) {
importers.push(file);
break;
}
}
}
return importers;
}
/**
* Detect circular dependencies
*/
detectCircularDependencies(filePath) {
const absolutePath = this.resolveFilePath(filePath);
const visited = new Set();
const recursionStack = new Set();
const cycles = [];
const dfs = (currentFile, path) => {
if (recursionStack.has(currentFile)) {
// Found a cycle
const cycleStart = path.indexOf(currentFile);
const cycle = path.slice(cycleStart).concat([currentFile]);
cycles.push({
cycle,
description: `Circular dependency: ${cycle.join(' → ')}`
});
return;
}
if (visited.has(currentFile)) {
return;
}
visited.add(currentFile);
recursionStack.add(currentFile);
const imports = this.getImports(currentFile);
for (const imp of imports) {
if (!imp.isExternal) {
const resolvedPath = this.resolveImportPath(currentFile, imp.source);
if (resolvedPath) {
dfs(resolvedPath, [...path, currentFile]);
}
}
}
recursionStack.delete(currentFile);
};
dfs(absolutePath, []);
return cycles;
}
/**
* Get dependency tree with depth
*/
getDependencyTree(filePath, maxDepth = 3) {
const absolutePath = this.resolveFilePath(filePath);
const visited = new Set();
const buildTree = (file, depth) => {
const imports = this.getImports(file);
const tree = {
file: this.getRelativePath(file),
depth,
imports: [],
isExternal: false,
isCyclic: visited.has(file)
};
if (depth >= maxDepth || visited.has(file)) {
return tree;
}
visited.add(file);
for (const imp of imports) {
if (imp.isExternal) {
tree.imports.push({
file: imp.source,
depth: depth + 1,
imports: [],
isExternal: true
});
}
else {
const resolvedPath = this.resolveImportPath(file, imp.source);
if (resolvedPath) {
tree.imports.push(buildTree(resolvedPath, depth + 1));
}
}
}
return tree;
};
return buildTree(absolutePath, 0);
}
// Helper methods
readFile(filePath) {
if (this.fileCache.has(filePath)) {
return this.fileCache.get(filePath);
}
try {
// Check file size first to prevent OOM crashes
const stats = fs.statSync(filePath);
if (stats.size > this.MAX_FILE_SIZE) {
console.error(`⚠️ File too large for dependency analysis (${(stats.size / 1024 / 1024).toFixed(1)}MB), skipping: ${path.relative(this.workspacePath, filePath)}`);
return '';
}
if (stats.size > this.WARN_FILE_SIZE) {
console.error(`⚠️ Large file in dependency analysis (${(stats.size / 1024 / 1024).toFixed(1)}MB): ${path.relative(this.workspacePath, filePath)}`);
}
const content = fs.readFileSync(filePath, 'utf-8');
this.fileCache.set(filePath, content);
return content;
}
catch (error) {
return '';
}
}
resolveFilePath(filePath) {
if (path.isAbsolute(filePath)) {
return filePath;
}
return path.resolve(this.workspacePath, filePath);
}
getRelativePath(filePath) {
return path.relative(this.workspacePath, filePath);
}
isExternalModule(source) {
// External if it doesn't start with . or /
return !source.startsWith('.') && !source.startsWith('/');
}
resolveImportPath(fromFile, importSource) {
if (this.isExternalModule(importSource)) {
return null; // Don't resolve external modules
}
const dir = path.dirname(fromFile);
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
// Try to resolve with extensions
for (const ext of extensions) {
const withExt = path.resolve(dir, importSource + ext);
if (fs.existsSync(withExt)) {
return withExt;
}
}
// Try index files
for (const ext of extensions) {
const indexFile = path.resolve(dir, importSource, 'index' + ext);
if (fs.existsSync(indexFile)) {
return indexFile;
}
}
// Try as-is
const asIs = path.resolve(dir, importSource);
if (fs.existsSync(asIs)) {
return asIs;
}
return null;
}
getAllProjectFiles(maxFiles = 1000) {
const files = [];
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
let fileCount = 0;
const walk = (dir) => {
// Stop if we've hit the file limit
if (fileCount >= maxFiles) {
return;
}
try {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
// Check limit again in the loop
if (fileCount >= maxFiles) {
console.warn(`⚠️ File limit reached (${maxFiles} files). Stopping scan to prevent hangs.`);
return;
}
const fullPath = path.join(dir, entry.name);
// Skip node_modules, dist, build, etc.
if (entry.isDirectory()) {
if (!['node_modules', 'dist', 'build', '.git', '.next', 'out', 'coverage'].includes(entry.name)) {
walk(fullPath);
}
}
else {
const ext = path.extname(entry.name);
if (extensions.includes(ext)) {
files.push(fullPath);
fileCount++;
}
}
}
}
catch (error) {
// Skip directories we can't read
}
};
walk(this.workspacePath);
if (fileCount >= maxFiles) {
console.warn(`📊 Scanned ${maxFiles} files (limit reached). Use smaller projects or increase limit for complete analysis.`);
}
return files;
}
/**
* Clear caches (useful for testing or when files change)
*/
clearCache() {
this.fileCache.clear();
this.dependencyCache.clear();
}
/**
* Dispose resources (cleanup file watcher)
*/
dispose() {
if (this.fileWatcher) {
this.fileWatcher.close();
this.fileWatcher = null;
console.error('📁 Dependency analyzer file watcher disposed');
}
}
}
//# sourceMappingURL=dependency-analyzer.js.map