Skip to main content
Glama
importResolver.ts15.5 kB
/** * Import resolver utility for the Code-Map Generator tool. * This file contains utilities for resolving import paths. */ import resolve from 'resolve'; import * as path from 'path'; import * as fs from 'fs'; import logger from '../../../logger.js'; /** * LRU Cache implementation for resolved imports. * This helps limit memory usage while still providing caching benefits. */ class LRUCache { private cache: Map<string, string>; private maxSize: number; constructor(maxSize: number) { this.cache = new Map<string, string>(); this.maxSize = maxSize; } /** * Gets a value from the cache. * * @param key The cache key. * @returns The cached value, or undefined if not found. */ get(key: string): string | undefined { // If the key exists, delete it and re-add it to make it the most recently used const value = this.cache.get(key); if (value !== undefined) { this.cache.delete(key); this.cache.set(key, value); } return value; } /** * Sets a value in the cache. * * @param key The cache key. * @param value The value to cache. */ set(key: string, value: string): void { // If the key already exists, delete it first if (this.cache.has(key)) { this.cache.delete(key); } // If the cache is full, delete the least recently used entry if (this.cache.size >= this.maxSize) { const firstKey = this.cache.keys().next().value; if (firstKey !== undefined) { this.cache.delete(firstKey); } } // Add the new entry this.cache.set(key, value); } /** * Checks if the cache has a key. * * @param key The cache key. * @returns Whether the cache has the key. */ has(key: string): boolean { return this.cache.has(key); } /** * Clears the cache. */ clear(): void { this.cache.clear(); } /** * Gets the size of the cache. * * @returns The number of entries in the cache. */ get size(): number { return this.cache.size; } /** * Gets the keys in the cache. * * @returns An iterator over the keys in the cache. */ keys(): IterableIterator<string> { return this.cache.keys(); } /** * Deletes a key from the cache. * * @param key The key to delete. * @returns Whether the key was deleted. */ delete(key: string): boolean { return this.cache.delete(key); } } /** * Cache for resolved imports to improve performance. * Using a smaller cache size to reduce memory usage. */ const importCache = new LRUCache(1000); /** * Maximum size of the import cache. */ const CACHE_MAX_SIZE = 1000; /** * Options for resolving imports. */ export interface ImportResolverOptions { /** * The project root directory. */ projectRoot: string; /** * The file path of the file containing the import. */ fromFile: string; /** * The language of the file containing the import. */ language: string; /** * Whether to use the cache. */ useCache?: boolean; /** * Extensions to try when resolving imports. */ extensions?: string[]; /** * Whether to temporarily expand the security boundary for import resolution. * This only affects path resolution, not file content access. */ expandSecurityBoundary?: boolean; } /** * Resolves an import path to an absolute path. * * @param importPath The import path to resolve. * @param options Options for resolving the import. * @returns The resolved import path, or the original path if it couldn't be resolved. */ export function resolveImport( importPath: string, options: ImportResolverOptions ): string { // Skip resolution for built-in modules if (isBuiltinModule(importPath)) { return importPath; } // Generate a cache key const cacheKey = `${options.fromFile}:${importPath}:${options.language}`; // Check cache first if enabled if (options.useCache !== false && importCache.has(cacheKey)) { return importCache.get(cacheKey)!; } try { // Handle external packages differently if (isExternalPackage(importPath)) { // For external packages, we can't resolve them to absolute paths // But we can extract the package name for better display const packageName = getPackageName(importPath); // Try to resolve the package if it's installed try { // Get the directory containing the file const basedir = path.dirname(options.fromFile); // Try to resolve just the package name to see if it's installed const resolvedPackage = resolve.sync(packageName, { basedir, preserveSymlinks: false }); // If we get here, the package is installed // We can't resolve the exact import, but we can show that the package exists logger.debug({ packageName, resolvedPackage }, 'Package exists but specific import cannot be resolved'); // Return the original import path since we can't resolve it exactly return importPath; } catch (packageError) { // Package not found, return the original import path logger.debug({ err: packageError, packageName }, 'Package not found'); return importPath; } } // Get the directory containing the file const basedir = path.dirname(options.fromFile); // Default extensions based on language const extensions = options.extensions || getDefaultExtensions(options.language); // Try to resolve with expanded boundary only if the option is enabled if (options.expandSecurityBoundary === true) { const expandedResolvedPath = resolveImportWithExpandedBoundary(importPath, basedir, extensions); if (expandedResolvedPath) { // If resolved, process the path but mark it as external let finalPath = expandedResolvedPath; // Make relative to project root if possible if (options.projectRoot && typeof options.projectRoot === 'string' && expandedResolvedPath.startsWith(options.projectRoot)) { try { finalPath = path.relative(options.projectRoot, expandedResolvedPath); finalPath = finalPath.replace(/\\/g, '/'); if (!finalPath.startsWith('./') && !finalPath.startsWith('../')) { finalPath = `./${finalPath}`; } } catch (pathError) { logger.warn({ err: pathError, projectRoot: options.projectRoot, resolvedPath: expandedResolvedPath }, 'Error making path relative to project root'); // Keep the absolute path if we can't make it relative finalPath = expandedResolvedPath; } } logger.debug({ originalPath: importPath, resolvedPath: expandedResolvedPath, finalPath, projectRoot: options.projectRoot, securityExpanded: true }, 'Resolved import path with expanded security boundary'); // Cache the result if (options.useCache !== false) { importCache.set(cacheKey, finalPath); } return finalPath; } else { logger.debug({ importPath, basedir, securityExpanded: true }, 'Failed to resolve import with expanded boundary, falling back to standard resolution'); } } // Fall back to standard resolution with security checks // Try to resolve the import let standardResolvedPath: string; try { standardResolvedPath = resolve.sync(importPath, { basedir, extensions, preserveSymlinks: false }); } catch (error) { // If standard resolution fails, return the original import path logger.debug({ err: error, importPath, basedir }, 'Error resolving import with standard resolution'); return importPath; } // If we have a project root, make the path relative to it let finalPath = standardResolvedPath; if (options.projectRoot) { // Check if the resolved path is within the project root if (standardResolvedPath.startsWith(options.projectRoot)) { // Make the path relative to the project root finalPath = path.relative(options.projectRoot, standardResolvedPath); // Ensure consistent path format (use forward slashes) finalPath = finalPath.replace(/\\/g, '/'); // Add a ./ prefix if it doesn't start with one if (!finalPath.startsWith('./') && !finalPath.startsWith('../')) { finalPath = `./${finalPath}`; } logger.debug({ originalPath: importPath, resolvedPath: standardResolvedPath, finalPath, projectRoot: options.projectRoot }, 'Resolved import path relative to project root'); } else { // The resolved path is outside the project root logger.debug({ originalPath: importPath, resolvedPath: standardResolvedPath, projectRoot: options.projectRoot }, 'Resolved import path is outside project root'); } } // Cache the result if caching is enabled if (options.useCache !== false) { // Limit cache size if (importCache.size >= CACHE_MAX_SIZE) { // Remove the oldest entry const firstKey = importCache.keys().next().value; if (firstKey !== undefined) { importCache.delete(firstKey); } } importCache.set(cacheKey, finalPath); } return finalPath; } catch (error) { // Log the error logger.debug({ err: error, importPath, fromFile: options.fromFile }, 'Error resolving import'); // Return the original import path return importPath; } } /** * Clears the import cache. */ export function clearImportCache(): void { importCache.clear(); } /** * Gets the size of the import cache. * * @returns The number of entries in the import cache. */ export function getImportCacheSize(): number { return importCache.size; } /** * Checks if a module is a built-in Node.js module. * * @param moduleName The name of the module to check. * @returns Whether the module is a built-in Node.js module. */ function isBuiltinModule(moduleName: string): boolean { // List of built-in Node.js modules const builtinModules = [ 'assert', 'buffer', 'child_process', 'cluster', 'console', 'constants', 'crypto', 'dgram', 'dns', 'domain', 'events', 'fs', 'http', 'https', 'module', 'net', 'os', 'path', 'perf_hooks', 'process', 'punycode', 'querystring', 'readline', 'repl', 'stream', 'string_decoder', 'timers', 'tls', 'tty', 'url', 'util', 'v8', 'vm', 'wasi', 'worker_threads', 'zlib' ]; return builtinModules.includes(moduleName); } /** * Checks if a module is an external package. * * @param moduleName The name of the module to check. * @returns Whether the module is an external package. */ function isExternalPackage(moduleName: string): boolean { // Check if the module name starts with a package name // This is a simple heuristic that works for most cases return !moduleName.startsWith('.') && !moduleName.startsWith('/') && !isBuiltinModule(moduleName); } /** * Gets the package name from a module name. * * @param moduleName The name of the module to get the package name from. * @returns The package name. */ function getPackageName(moduleName: string): string { // Handle scoped packages if (moduleName.startsWith('@')) { const parts = moduleName.split('/'); if (parts.length >= 2) { return `${parts[0]}/${parts[1]}`; } } // Handle regular packages const parts = moduleName.split('/'); return parts[0]; } /** * Resolves an import path with temporarily expanded security boundary. * This function bypasses security checks ONLY for path resolution, not for file content access. * * @param importPath The import path to resolve. * @param basedir The directory containing the file with the import. * @param extensions Extensions to try when resolving the import. * @returns The resolved import path, or null if it couldn't be resolved. */ function resolveImportWithExpandedBoundary( importPath: string, basedir: string, extensions: string[] ): string | null { // Validate inputs to prevent potential security issues if (!importPath || typeof importPath !== 'string') { logger.warn({ importPath }, 'Invalid import path provided to resolveImportWithExpandedBoundary'); return null; } if (!basedir || typeof basedir !== 'string') { logger.warn({ basedir }, 'Invalid base directory provided to resolveImportWithExpandedBoundary'); return null; } // Log that we're using expanded boundary for this import logger.debug({ importPath, basedir, securityExpanded: true }, 'Attempting to resolve import with expanded security boundary'); try { // Use resolve.sync directly without security validation const resolvedPath = resolve.sync(importPath, { basedir, extensions, preserveSymlinks: false }); // Verify that the resolved path is a string if (!resolvedPath || typeof resolvedPath !== 'string') { logger.warn({ importPath, resolvedPath }, 'Resolve returned an invalid path'); return null; } // Log successful resolution with expanded boundary logger.debug({ importPath, resolvedPath, securityExpanded: true }, 'Successfully resolved import with expanded security boundary'); return resolvedPath; } catch (error) { logger.debug({ err: error, importPath, basedir, securityExpanded: true }, 'Error resolving import with expanded boundary'); // Try a more direct approach if the standard resolve fails try { // Try to resolve relative to the base directory const potentialPath = path.resolve(basedir, importPath); // Check if the file exists with any of the extensions for (const ext of extensions) { const fullPath = `${potentialPath}${ext}`; if (fs.existsSync(fullPath)) { logger.debug({ importPath, resolvedPath: fullPath, method: 'direct-fs' }, 'Resolved import path with direct filesystem check'); return fullPath; } } // If we get here, we couldn't find the file return null; } catch (directError) { logger.debug({ err: directError, importPath, basedir }, 'Error resolving import with direct filesystem check'); return null; } } } /** * Gets the default extensions for a language. * * @param language The language to get extensions for. * @returns An array of file extensions for the language. */ function getDefaultExtensions(language: string): string[] { switch (language) { case 'javascript': return ['.js', '.json', '.node', '.mjs', '.cjs']; case 'typescript': return ['.ts', '.tsx', '.js', '.jsx', '.json', '.node']; case 'python': return ['.py', '.pyw', '.pyc', '.pyo', '.pyd']; case 'java': return ['.java', '.class', '.jar']; case 'csharp': return ['.cs', '.dll']; case 'go': return ['.go']; case 'ruby': return ['.rb', '.rake', '.gemspec']; case 'rust': return ['.rs']; case 'php': return ['.php']; default: return ['.js', '.json', '.node']; // Default to JavaScript } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/freshtechbro/vibe-coder-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server