UNPKG

ts-project-indexer-mcp

Version:

A powerful MCP (Model Context Protocol) server that indexes and analyzes TypeScript/JavaScript projects, providing intelligent code exploration, dependency tracking, method discovery, and project structure analysis for AI assistants and development tools.

253 lines 9.4 kB
import { promises as fs } from 'node:fs'; import { join, resolve, relative, dirname } from 'node:path'; export class PathResolver { projectRoot; tsConfig = null; packageJson = null; pathMappings = new Map(); constructor(projectRoot) { this.projectRoot = resolve(projectRoot); } async initialize() { await Promise.all([ this.loadTsConfig(), this.loadPackageJson(), ]); this.buildPathMappings(); } /** * Resolve an import path to absolute file path */ resolveImportPath(importPath, fromFile) { const fromDir = dirname(fromFile); // Handle different import types if (importPath.startsWith('#')) { // Package imports (#root/*) return this.resolvePackageImport(importPath); } else if (importPath.startsWith('./') || importPath.startsWith('../')) { // Relative imports return this.resolveRelativeImport(importPath, fromDir); } else if (importPath.startsWith('/')) { // Absolute imports (rare) return this.resolveAbsoluteImport(importPath); } else if (this.tsConfig?.compilerOptions?.paths) { // TypeScript path mapping return this.resolvePathMapping(importPath); } // Node modules or other external imports return null; } /** * Normalize a file path to the project-relative format */ normalizeFilePath(filePath) { const absolutePath = resolve(filePath); const relativePath = relative(this.projectRoot, absolutePath); return relativePath.replace(/\\/g, '/'); } /** * Get possible file extensions for resolution */ getPossibleExtensions() { return ['.ts', '.js', '.tsx', '.jsx', '.json']; } /** * Resolve package imports like #root/* */ resolvePackageImport(importPath) { if (!this.packageJson?.imports) return null; for (const [pattern, target] of Object.entries(this.packageJson.imports)) { if (this.matchesPattern(importPath, pattern)) { let resolvedPath = this.replacePattern(importPath, pattern, target); // Convert dist/* to src/* for development-time resolution if (resolvedPath.startsWith('./dist/')) { resolvedPath = resolvedPath.replace('./dist/', './src/'); } else if (resolvedPath.startsWith('dist/')) { resolvedPath = resolvedPath.replace('dist/', 'src/'); } const absolutePath = resolve(this.projectRoot, resolvedPath); return this.resolveToExistingFile(absolutePath); } } return null; } /** * Resolve relative imports like ../../config */ resolveRelativeImport(importPath, fromDir) { const basePath = resolve(fromDir, importPath); return this.resolveToExistingFile(basePath); } /** * Resolve absolute imports */ resolveAbsoluteImport(importPath) { return this.resolveToExistingFile(importPath); } /** * Resolve TypeScript path mappings */ resolvePathMapping(importPath) { if (!this.tsConfig?.compilerOptions?.paths) return null; const baseUrl = this.tsConfig.compilerOptions.baseUrl || '.'; const baseDir = resolve(this.projectRoot, baseUrl); for (const [pattern, targets] of Object.entries(this.tsConfig.compilerOptions.paths)) { if (this.matchesPattern(importPath, pattern)) { for (const target of targets) { let resolvedPath = this.replacePattern(importPath, pattern, target); // Handle relative paths in tsconfig.json targets if (resolvedPath.startsWith('./')) { resolvedPath = resolvedPath.substring(2); // Remove "./" } const fullPath = resolve(baseDir, resolvedPath); const existingFile = this.resolveToExistingFile(fullPath); if (existingFile) return existingFile; } } } return null; } /** * Try to find existing file with different extensions */ resolveToExistingFile(basePath) { // Remove existing extension if any const withoutExt = basePath.replace(/\.(js|ts|jsx|tsx|json)$/, ''); // For .js imports, try .ts first (most common in our TypeScript project) if (basePath.endsWith('.js')) { const tsPath = withoutExt + '.ts'; return tsPath; // Return .ts version for .js imports } // For imports without extension, prefer .ts over .js if (!basePath.match(/\.(js|ts|jsx|tsx|json)$/)) { // Check if this looks like a TypeScript project file (within src/ or project root) if (basePath.includes('/src/') || relative(this.projectRoot, basePath).startsWith('src/')) { return withoutExt + '.ts'; } // For other files, try .js return withoutExt + '.js'; } // Return original path if it already has an extension return basePath; } /** * Check if import path matches a pattern like "#root/*" */ matchesPattern(importPath, pattern) { if (!pattern.includes('*')) { return importPath === pattern; } const regexPattern = pattern .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // Escape regex chars .replace(/\\\*/g, '.*'); // Replace \* with .* return new RegExp(`^${regexPattern}$`).test(importPath); } /** * Replace pattern wildcards with actual values */ replacePattern(importPath, pattern, target) { if (!pattern.includes('*') || !target.includes('*')) { return target; } // Extract the wildcard part const patternPrefix = pattern.split('*')[0]; const patternSuffix = pattern.split('*')[1] || ''; if (!importPath.startsWith(patternPrefix)) return target; let wildcardValue = importPath.slice(patternPrefix.length); if (patternSuffix && wildcardValue.endsWith(patternSuffix)) { wildcardValue = wildcardValue.slice(0, -patternSuffix.length); } return target.replace('*', wildcardValue); } /** * Load TypeScript configuration */ async loadTsConfig() { this.tsConfig = await this.findFileInParentDirs('tsconfig.json'); } /** * Load package.json configuration */ async loadPackageJson() { this.packageJson = await this.findFileInParentDirs('package.json'); } /** * Search for a file in project directory and limited parent directories */ async findFileInParentDirs(filename) { let currentDir = this.projectRoot; const projectRootResolved = resolve(this.projectRoot); // First try in the project root itself const projectFilePath = join(currentDir, filename); try { const content = await fs.readFile(projectFilePath, 'utf8'); return JSON.parse(content); } catch { // Not found in project root } // Only check up to 2 levels above project root (for monorepos) let levelsUp = 0; const maxLevelsUp = 2; while (levelsUp < maxLevelsUp) { const parentDir = join(currentDir, '..'); const parentDirResolved = resolve(parentDir); // Stop if we've reached the root directory or gone too far up if (parentDirResolved === resolve(currentDir)) { break; } const filePath = join(parentDir, filename); try { const content = await fs.readFile(filePath, 'utf8'); return JSON.parse(content); } catch { // File not found, continue to next parent } currentDir = parentDir; levelsUp++; } return null; } /** * Build internal path mappings for quick lookup */ buildPathMappings() { this.pathMappings.clear(); // Add TypeScript path mappings if (this.tsConfig?.compilerOptions?.paths) { for (const [pattern, targets] of Object.entries(this.tsConfig.compilerOptions.paths)) { // For now, just use the first target if (targets.length > 0) { this.pathMappings.set(pattern, targets[0]); } } } // Add package.json imports if (this.packageJson?.imports) { for (const [pattern, target] of Object.entries(this.packageJson.imports)) { this.pathMappings.set(pattern, target); } } } /** * Debug method to inspect current configuration */ getDebugInfo() { return { projectRoot: this.projectRoot, tsConfig: this.tsConfig, packageJson: this.packageJson, pathMappings: Array.from(this.pathMappings.entries()), }; } } //# sourceMappingURL=path-resolver.js.map