typeref-mcp
Version:
TypeScript type inference and symbol navigation MCP server for Claude Code
164 lines • 7.14 kB
JavaScript
import * as fs from 'fs/promises';
import * as path from 'path';
export class DiskCache {
logger;
CACHE_VERSION = '1.0.0';
CACHE_DIR = '.typeref';
constructor(logger) {
this.logger = logger;
}
getCacheFilePath(projectPath) {
const projectHash = this.hashPath(projectPath);
return path.join(projectPath, this.CACHE_DIR, `${projectHash}.json`);
}
getMetadataFilePath(projectPath) {
const projectHash = this.hashPath(projectPath);
return path.join(projectPath, this.CACHE_DIR, `${projectHash}.meta.json`);
}
hashPath(projectPath) {
return Buffer.from(projectPath).toString('base64').replace(/[/+=]/g, '_').substring(0, 16);
}
async getFileHashes(projectPath) {
const fileHashes = new Map();
try {
const files = await this.findTypeScriptFiles(projectPath);
for (const filePath of files) {
try {
const stats = await fs.stat(filePath);
fileHashes.set(filePath, stats.mtime.toISOString());
}
catch (error) {
this.logger.debug(`Could not stat file ${filePath}: ${error}`);
}
}
}
catch (error) {
this.logger.warn(`Failed to get file hashes for ${projectPath}: ${error}`);
}
return fileHashes;
}
async findTypeScriptFiles(projectPath) {
const files = [];
const excludeDirs = ['node_modules', 'dist', 'build', '.git', 'coverage'];
const scanDir = async (dirPath) => {
try {
const entries = await fs.readdir(dirPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name);
if (entry.isDirectory()) {
if (!excludeDirs.includes(entry.name)) {
await scanDir(fullPath);
}
}
else if (entry.isFile() && /\.(ts|tsx)$/.test(entry.name)) {
files.push(fullPath);
}
}
}
catch (error) {
this.logger.debug(`Could not scan directory ${dirPath}: ${error}`);
}
};
await scanDir(projectPath);
return files;
}
async isCacheValid(projectPath) {
try {
const metadataPath = this.getMetadataFilePath(projectPath);
const cacheFilePath = this.getCacheFilePath(projectPath);
await fs.access(metadataPath);
await fs.access(cacheFilePath);
const metadataContent = await fs.readFile(metadataPath, 'utf8');
const metadata = JSON.parse(metadataContent);
if (metadata.version !== this.CACHE_VERSION) {
this.logger.info(`Cache version mismatch for ${projectPath}, invalidating`);
return false;
}
const currentFileHashes = await this.getFileHashes(projectPath);
const cachedFileHashes = new Map(Object.entries(metadata.fileHashes));
if (currentFileHashes.size !== cachedFileHashes.size) {
this.logger.info(`File count changed for ${projectPath} (${cachedFileHashes.size} → ${currentFileHashes.size}), invalidating cache`);
return false;
}
for (const [filePath, currentHash] of currentFileHashes) {
const cachedHash = cachedFileHashes.get(filePath);
if (!cachedHash || cachedHash !== currentHash) {
this.logger.info(`File modified: ${filePath}, invalidating cache`);
return false;
}
}
this.logger.info(`Cache is valid for ${projectPath} (${currentFileHashes.size} files)`);
return true;
}
catch (error) {
this.logger.debug(`Cache validation failed for ${projectPath}: ${error}`);
return false;
}
}
async loadProjectIndex(projectPath) {
try {
const cacheFilePath = this.getCacheFilePath(projectPath);
const content = await fs.readFile(cacheFilePath, 'utf8');
const data = JSON.parse(content);
const index = {
projectPath: data.projectPath,
symbols: new Map(Object.entries(data.symbols)),
types: new Map(Object.entries(data.types)),
modules: new Map(Object.entries(data.modules)),
dependencies: new Map(Object.entries(data.dependencies)),
lastIndexed: new Date(data.lastIndexed),
};
this.logger.info(`Loaded cached index for ${projectPath}: ${index.symbols.size} symbols, ${index.types.size} types`);
return index;
}
catch (error) {
this.logger.debug(`Failed to load cached index for ${projectPath}: ${error}`);
return null;
}
}
async saveProjectIndex(index) {
try {
const projectPath = index.projectPath;
const cacheDir = path.join(projectPath, this.CACHE_DIR);
await fs.mkdir(cacheDir, { recursive: true });
const cacheFilePath = this.getCacheFilePath(projectPath);
const metadataPath = this.getMetadataFilePath(projectPath);
const serializable = {
projectPath: index.projectPath,
symbols: Object.fromEntries(index.symbols),
types: Object.fromEntries(index.types),
modules: Object.fromEntries(index.modules),
dependencies: Object.fromEntries(index.dependencies),
lastIndexed: index.lastIndexed.toISOString(),
};
await fs.writeFile(cacheFilePath, JSON.stringify(serializable, null, 2));
const fileHashes = await this.getFileHashes(projectPath);
const metadata = {
projectPath,
lastIndexed: index.lastIndexed,
fileCount: fileHashes.size,
fileHashes,
version: this.CACHE_VERSION,
};
await fs.writeFile(metadataPath, JSON.stringify({
...metadata,
fileHashes: Object.fromEntries(metadata.fileHashes)
}, null, 2));
this.logger.info(`Saved index cache for ${projectPath}: ${index.symbols.size} symbols, ${index.types.size} types`);
}
catch (error) {
this.logger.error(`Failed to save index cache for ${index.projectPath}: ${error}`);
}
}
async clearProjectCache(projectPath) {
try {
const cacheDir = path.join(projectPath, this.CACHE_DIR);
await fs.rm(cacheDir, { recursive: true, force: true });
this.logger.info(`Cleared cache for ${projectPath}`);
}
catch (error) {
this.logger.debug(`Failed to clear cache for ${projectPath}: ${error}`);
}
}
}
//# sourceMappingURL=DiskCache.js.map