← vibesdk
/**
* Path resolution utilities for import specifiers
* Extracted from working ImportExportAnalyzer to preserve exact functionality
*/
import { FileMap } from '../types';
import { isScriptFile } from './ast';
import { getFileContent } from './imports';
// ============================================================================
// PATH ALIAS RESOLUTION
// ============================================================================
/**
* Resolve path aliases like @/components/ui/button to src/components/ui/button
* Preserves exact logic from working implementation
*/
export function resolvePathAlias(importSpecifier: string): string {
if (importSpecifier.startsWith('@/')) {
// Convert @/components/ui/button to src/components/ui/button
return importSpecifier.replace('@/', 'src/');
}
return importSpecifier;
}
// ============================================================================
// RELATIVE PATH RESOLUTION
// ============================================================================
/**
* Resolve relative import paths to absolute paths within the project
* Preserves exact logic from working implementation
*/
export function resolveImportPath(
importSpecifier: string,
currentFilePath: string,
files: FileMap
): string {
if (importSpecifier.startsWith('./') || importSpecifier.startsWith('../')) {
// Relative import - resolve relative to current file directory
const currentDirParts = currentFilePath.split('/').slice(0, -1);
const importParts = importSpecifier.split('/');
// Combine current directory with import path parts
const combinedParts = [...currentDirParts, ...importParts];
const normalizedParts: string[] = [];
// Normalize path (handle ../ and ./)
for (const part of combinedParts) {
if (part === '..') {
normalizedParts.pop();
} else if (part !== '.' && part !== '') {
normalizedParts.push(part);
}
}
const resolvedPath = normalizedParts.join('/');
// Try common extensions
for (const ext of ['.ts', '.tsx', '.js', '.jsx']) {
if (!resolvedPath.endsWith(ext)) {
const withExt = resolvedPath + ext;
try {
const fileContent = getFileContent(withExt, files);
if (fileContent) return withExt;
} catch {
// File doesn't exist or can't be fetched, try next extension
}
}
}
return resolvedPath;
} else {
// Absolute import - return as is for now
return importSpecifier;
}
}
// ============================================================================
// MODULE FILE FINDING
// ============================================================================
/**
* Find a module file using fuzzy matching and file fetching
* Preserves exact logic from working ImportExportAnalyzer.findModuleFile
*/
export function findModuleFile(
importSpecifier: string,
currentFilePath: string,
files: FileMap
): string | null {
// Handle path aliases like @/components/ui/button
const resolvedSpecifier = resolvePathAlias(importSpecifier);
// Try exact match first (relative/absolute paths)
const exactMatch = resolveImportPath(resolvedSpecifier, currentFilePath, files);
if (exactMatch) {
// Check if file exists in files Map after potential fetching
const allFiles = Array.from(files.keys());
if (allFiles.some(file => file === exactMatch)) {
return exactMatch;
}
}
// Get current files list for fallback logic
const allFiles = Array.from(files.keys());
// Try direct path matching for aliases
for (const file of allFiles) {
if (file === resolvedSpecifier ||
file === resolvedSpecifier + '.ts' ||
file === resolvedSpecifier + '.tsx' ||
file === resolvedSpecifier + '.js' ||
file === resolvedSpecifier + '.jsx') {
return file;
}
}
// Try to fetch the file with common extensions
const extensionsToTry = ['.tsx', '.ts', '.jsx', '.js'];
for (const ext of extensionsToTry) {
const candidatePath = resolvedSpecifier + ext;
try {
// Try to get file content (this will trigger fetching if available)
const content = getFileContent(candidatePath, files);
if (content) {
return candidatePath;
}
} catch {
// Failed to fetch, continue to next extension
}
}
// Fuzzy matching: look for files with similar names
const searchTerm = resolvedSpecifier.replace(/^\.\/|^\.\.\/|^\//, '').replace(/\.(ts|tsx|js|jsx)$/, '');
for (const file of allFiles) {
const fileName = file.split('/').pop()?.replace(/\.(ts|tsx|js|jsx)$/, '') || '';
// Check if filename matches or contains the search term
if (fileName === searchTerm || fileName.includes(searchTerm) || searchTerm.includes(fileName)) {
return file;
}
}
return null;
}
// ============================================================================
// RELATIVE IMPORT CREATION
// ============================================================================
/**
* Create a relative import path from one file to another
* Preserves exact logic from working implementation
*/
export function makeRelativeImport(fromFile: string, toFile: string): string {
const fromParts = fromFile.split('/').slice(0, -1); // Remove filename
const toParts = toFile.split('/').slice(0, -1); // Remove filename
const toFileName = toFile.split('/').pop()?.replace(/\.(ts|tsx|js|jsx)$/, '') || '';
// Find common prefix
let commonLength = 0;
while (commonLength < fromParts.length &&
commonLength < toParts.length &&
fromParts[commonLength] === toParts[commonLength]) {
commonLength++;
}
// Build relative path
const upLevels = fromParts.length - commonLength;
const downPath = toParts.slice(commonLength);
let relativePath = '';
if (upLevels > 0) {
relativePath = '../'.repeat(upLevels);
} else {
relativePath = './';
}
if (downPath.length > 0) {
relativePath += downPath.join('/') + '/';
}
relativePath += toFileName;
return relativePath;
}
// ============================================================================
// IMPORT TO FILE PATH RESOLUTION
// ============================================================================
/**
* Resolve an import specifier to a target file path for stub creation
* Preserves exact logic from working implementation
*/
export function resolveImportToFilePath(importSpecifier: string, currentFilePath: string): string {
if (importSpecifier.startsWith('./') || importSpecifier.startsWith('../')) {
const currentDir = currentFilePath.split('/').slice(0, -1).join('/');
// Resolve the path manually
const pathParts = currentDir.split('/').concat(importSpecifier.split('/'));
const normalizedParts: string[] = [];
for (const part of pathParts) {
if (part === '..') {
normalizedParts.pop();
} else if (part !== '.' && part !== '') {
normalizedParts.push(part);
}
}
const resolvedPath = normalizedParts.join('/');
// Add appropriate extension if not present
if (!resolvedPath.match(/\.(ts|tsx|js|jsx)$/)) {
return resolvedPath + '.tsx'; // Default to .tsx for React components
}
return resolvedPath;
} else if (importSpecifier.startsWith('@/')) {
// Handle path aliases - convert @/ to src/
const withoutAlias = importSpecifier.replace('@/', 'src/');
if (!withoutAlias.match(/\.(ts|tsx|js|jsx)$/)) {
return withoutAlias + '.tsx';
}
return withoutAlias;
} else {
// For other absolute imports, create in src directory by default
const fileName = importSpecifier.split('/').pop() || 'index';
return `src/${fileName}.tsx`;
}
}
// ============================================================================
// PATH VALIDATION
// ============================================================================
/**
* Check if a path is a valid script file path
*/
export function isValidScriptPath(filePath: string): boolean {
return isScriptFile(filePath);
}
/**
* Normalize a file path by removing redundant parts
*/
export function normalizePath(filePath: string): string {
const parts = filePath.split('/').filter(part => part !== '');
const normalized: string[] = [];
for (const part of parts) {
if (part === '..') {
normalized.pop();
} else if (part !== '.') {
normalized.push(part);
}
}
return normalized.join('/');
}
/**
* Get the directory part of a file path
*/
export function getDirectory(filePath: string): string {
return filePath.split('/').slice(0, -1).join('/');
}
/**
* Get the filename part of a file path (without extension)
*/
export function getFilename(filePath: string): string {
return filePath.split('/').pop()?.replace(/\.(ts|tsx|js|jsx)$/, '') || '';
}
/**
* Get the file extension
*/
export function getExtension(filePath: string): string {
const match = filePath.match(/\.(ts|tsx|js|jsx)$/);
return match ? match[1] : '';
}