Spaces:
Build error
Build error
| 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<FileInfo> { | |
| 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<string, FileInfo>) { | |
| const tree: Record<string, { | |
| file: string; | |
| imports: string[]; | |
| importedBy: string[]; | |
| type: 'page' | 'layout' | 'component'; | |
| }> = {}; | |
| // 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; | |
| } |