import { FileInfo, ImportInfo, ComponentInfo } from '@/types/file-manifest'; /** * Parse a JavaScript/JSX file to extract imports, exports, and component info */ export function parseJavaScriptFile(content: string, filePath: string): Partial { const imports = extractImports(content); const exports = extractExports(content); const componentInfo = extractComponentInfo(content, filePath); const fileType = determineFileType(filePath, content); return { imports, exports, componentInfo, type: fileType, }; } /** * Extract import statements from file content */ function extractImports(content: string): ImportInfo[] { const imports: ImportInfo[] = []; // Match import statements const importRegex = /import\s+(?:(.+?)\s+from\s+)?['"](.+?)['"]/g; const matches = content.matchAll(importRegex); for (const match of matches) { const [, importClause, source] = match; const importInfo: ImportInfo = { source, imports: [], isLocal: source.startsWith('./') || source.startsWith('../') || source.startsWith('@/'), }; if (importClause) { // Handle default import const defaultMatch = importClause.match(/^(\w+)(?:,|$)/); if (defaultMatch) { importInfo.defaultImport = defaultMatch[1]; } // Handle named imports const namedMatch = importClause.match(/\{([^}]+)\}/); if (namedMatch) { importInfo.imports = namedMatch[1] .split(',') .map(imp => imp.trim()) .map(imp => imp.split(/\s+as\s+/)[0].trim()); } } imports.push(importInfo); } return imports; } /** * Extract export statements from file content */ function extractExports(content: string): string[] { const exports: string[] = []; // Match default export if (/export\s+default\s+/m.test(content)) { // Try to find the name of the default export const defaultExportMatch = content.match(/export\s+default\s+(?:function\s+)?(\w+)/); if (defaultExportMatch) { exports.push(`default:${defaultExportMatch[1]}`); } else { exports.push('default'); } } // Match named exports const namedExportRegex = /export\s+(?:const|let|var|function|class)\s+(\w+)/g; const namedMatches = content.matchAll(namedExportRegex); for (const match of namedMatches) { exports.push(match[1]); } // Match export { ... } statements const exportBlockRegex = /export\s+\{([^}]+)\}/g; const blockMatches = content.matchAll(exportBlockRegex); for (const match of blockMatches) { const names = match[1] .split(',') .map(exp => exp.trim()) .map(exp => exp.split(/\s+as\s+/)[0].trim()); exports.push(...names); } return exports; } /** * Extract React component information */ function extractComponentInfo(content: string, filePath: string): ComponentInfo | undefined { // Check if this is likely a React component const hasJSX = /<[A-Z]\w*|<[a-z]+\s+[^>]*\/?>/.test(content); if (!hasJSX && !content.includes('React')) return undefined; // Try to find component name let componentName = ''; // Check for function component const funcComponentMatch = content.match(/(?:export\s+)?(?:default\s+)?function\s+([A-Z]\w*)\s*\(/); if (funcComponentMatch) { componentName = funcComponentMatch[1]; } else { // Check for arrow function component const arrowComponentMatch = content.match(/(?:export\s+)?(?:default\s+)?(?:const|let)\s+([A-Z]\w*)\s*=\s*(?:\([^)]*\)|[^=])*=>/); if (arrowComponentMatch) { componentName = arrowComponentMatch[1]; } } // If no component name found, try to get from filename if (!componentName) { const fileName = filePath.split('/').pop()?.replace(/\.(jsx?|tsx?)$/, ''); if (fileName && /^[A-Z]/.test(fileName)) { componentName = fileName; } } if (!componentName) return undefined; // Extract hooks used const hooks: string[] = []; const hookRegex = /use[A-Z]\w*/g; const hookMatches = content.matchAll(hookRegex); for (const match of hookMatches) { if (!hooks.includes(match[0])) { hooks.push(match[0]); } } // Check if component has state const hasState = hooks.includes('useState') || hooks.includes('useReducer'); // Extract child components (rough approximation) const childComponents: string[] = []; const componentRegex = /<([A-Z]\w*)[^>]*(?:\/?>|>)/g; const componentMatches = content.matchAll(componentRegex); for (const match of componentMatches) { const comp = match[1]; if (!childComponents.includes(comp) && comp !== componentName) { childComponents.push(comp); } } return { name: componentName, hooks, hasState, childComponents, }; } /** * Determine file type based on path and content */ function determineFileType( filePath: string, content: string ): FileInfo['type'] { const fileName = filePath.split('/').pop()?.toLowerCase() || ''; const dirPath = filePath.toLowerCase(); // Style files if (fileName.endsWith('.css')) return 'style'; // Config files if (fileName.includes('config') || fileName === 'vite.config.js' || fileName === 'tailwind.config.js' || fileName === 'postcss.config.js') { return 'config'; } // Hook files if (dirPath.includes('/hooks/') || fileName.startsWith('use')) { return 'hook'; } // Context files if (dirPath.includes('/context/') || fileName.includes('context')) { return 'context'; } // Layout components if (fileName.includes('layout') || content.includes('children')) { return 'layout'; } // Page components (in pages directory or have routing) if (dirPath.includes('/pages/') || content.includes('useRouter') || content.includes('useParams')) { return 'page'; } // Utility files if (dirPath.includes('/utils/') || dirPath.includes('/lib/') || !content.includes('export default')) { return 'utility'; } // Default to component return 'component'; } /** * Build component dependency tree */ export function buildComponentTree(files: Record) { const tree: Record = {}; // First pass: collect all components for (const [path, fileInfo] of Object.entries(files)) { if (fileInfo.componentInfo) { const componentName = fileInfo.componentInfo.name; tree[componentName] = { file: path, imports: [], importedBy: [], type: fileInfo.type === 'page' ? 'page' : fileInfo.type === 'layout' ? 'layout' : 'component', }; } } // Second pass: build relationships for (const [path, fileInfo] of Object.entries(files)) { if (fileInfo.componentInfo && fileInfo.imports) { const componentName = fileInfo.componentInfo.name; // Find imported components for (const imp of fileInfo.imports) { if (imp.isLocal && imp.defaultImport) { // Check if this import is a component we know about if (tree[imp.defaultImport]) { tree[componentName].imports.push(imp.defaultImport); tree[imp.defaultImport].importedBy.push(componentName); } } } } } return tree; }