#!/usr/bin/env tsx import glob from 'fast-glob' import fs from 'fs' import path from 'path' interface ImportInfo { source: string imports: string[] } interface DependencyGraph { nodes: Array<{ id: string label: string group: string size: number inCircularDep?: boolean circularChains?: string[][] }> links: Array<{ source: string target: string value: number isCircular?: boolean }> circularDependencies?: Array<{ chain: string[] edges: Array<{ source: string; target: string }> }> } // Extract imports from a TypeScript/Vue file function extractImports(filePath: string): ImportInfo { const content = fs.readFileSync(filePath, 'utf-8') const imports: string[] = [] // Match ES6 import statements const importRegex = /import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s+from\s+)?['"]([^'"]+)['"]/g let match while ((match = importRegex.exec(content)) !== null) { imports.push(match[1]) } // Also match dynamic imports const dynamicImportRegex = /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g while ((match = dynamicImportRegex.exec(content)) !== null) { imports.push(match[1]) } return { source: filePath, imports: [...new Set(imports)] // Remove duplicates } } // Categorize file by its path function getFileGroup(filePath: string): string { const relativePath = path.relative(process.cwd(), filePath) if (relativePath.includes('node_modules')) return 'external' if (relativePath.startsWith('src/components')) return 'components' if (relativePath.startsWith('src/stores')) return 'stores' if (relativePath.startsWith('src/services')) return 'services' if (relativePath.startsWith('src/views')) return 'views' if (relativePath.startsWith('src/composables')) return 'composables' if (relativePath.startsWith('src/utils')) return 'utils' if (relativePath.startsWith('src/types')) return 'types' if (relativePath.startsWith('src/extensions')) return 'extensions' if (relativePath.startsWith('src/lib')) return 'lib' if (relativePath.startsWith('src/scripts')) return 'scripts' if (relativePath.startsWith('tests')) return 'tests' if (relativePath.startsWith('browser_tests')) return 'browser_tests' return 'other' } // Resolve import path to actual file function resolveImportPath(importPath: string, sourceFile: string): string { // Handle aliases if (importPath.startsWith('@/')) { return path.join(process.cwd(), 'src', importPath.slice(2)) } // Handle relative paths if (importPath.startsWith('.')) { const sourceDir = path.dirname(sourceFile) return path.resolve(sourceDir, importPath) } // External module return importPath } // Detect circular dependencies using DFS function detectCircularDependencies( nodes: Map, links: Map ): Array<{ chain: string[] edges: Array<{ source: string; target: string }> }> { const adjacencyList = new Map>() const circularDeps: Array<{ chain: string[] edges: Array<{ source: string; target: string }> }> = [] // Build adjacency list for (const link of links.values()) { if (!adjacencyList.has(link.source)) { adjacencyList.set(link.source, new Set()) } adjacencyList.get(link.source)!.add(link.target) } // DFS to find cycles const visited = new Set() const recStack = new Set() const parent = new Map() function findCycle(node: string, path: string[] = []): void { visited.add(node) recStack.add(node) path.push(node) const neighbors = adjacencyList.get(node) || new Set() for (const neighbor of neighbors) { if (!visited.has(neighbor)) { parent.set(neighbor, node) findCycle(neighbor, [...path]) } else if (recStack.has(neighbor)) { // Found a cycle const cycleStartIndex = path.indexOf(neighbor) if (cycleStartIndex !== -1) { const chain = path.slice(cycleStartIndex) chain.push(neighbor) // Complete the cycle // Create edges for the circular dependency const edges: Array<{ source: string; target: string }> = [] for (let i = 0; i < chain.length - 1; i++) { edges.push({ source: chain[i], target: chain[i + 1] }) } // Check if this cycle is already recorded (avoid duplicates) const chainStr = [...chain].sort().join('->') const isNew = !circularDeps.some((dep) => { const existingChainStr = [...dep.chain].sort().join('->') return existingChainStr === chainStr }) if (isNew) { circularDeps.push({ chain, edges }) } } } } recStack.delete(node) } // Run DFS from each unvisited node for (const node of nodes.keys()) { if (!visited.has(node) && !node.startsWith('external:')) { findCycle(node) } } return circularDeps } // Generate dependency graph async function generateDependencyGraph(): Promise { const sourceFiles = await glob('src/**/*.{ts,tsx,vue,mts}', { ignore: [ '**/node_modules/**', '**/*.d.ts', '**/*.spec.ts', '**/*.test.ts', '**/*.stories.ts' ] }) const nodes = new Map< string, { id: string label: string group: string size: number inCircularDep?: boolean circularChains?: string[][] } >() const links = new Map< string, { source: string; target: string; value: number; isCircular?: boolean } >() // Process each file for (const file of sourceFiles) { const importInfo = extractImports(file) const sourceId = path.relative(process.cwd(), file) // Add source node if (!nodes.has(sourceId)) { nodes.set(sourceId, { id: sourceId, label: path.basename(file), group: getFileGroup(file), size: 1 }) } // Process imports for (const importPath of importInfo.imports) { const resolvedPath = resolveImportPath(importPath, file) let targetId: string // Check if it's an external module if (!resolvedPath.startsWith('/') && !resolvedPath.startsWith('.')) { targetId = `external:${importPath}` if (!nodes.has(targetId)) { nodes.set(targetId, { id: targetId, label: importPath, group: 'external', size: 1 }) } } else { // Try to find the actual file const possibleExtensions = [ '.ts', '.tsx', '.vue', '.mts', '.js', '.json', '/index.ts', '/index.js' ] let actualFile = resolvedPath for (const ext of possibleExtensions) { if (fs.existsSync(resolvedPath + ext)) { actualFile = resolvedPath + ext break } } if (fs.existsSync(actualFile)) { targetId = path.relative(process.cwd(), actualFile) if (!nodes.has(targetId)) { nodes.set(targetId, { id: targetId, label: path.basename(actualFile), group: getFileGroup(actualFile), size: 1 }) } } else { continue // Skip unresolved imports } } // Add link const linkKey = `${sourceId}->${targetId}` if (links.has(linkKey)) { links.get(linkKey)!.value++ } else { links.set(linkKey, { source: sourceId, target: targetId, value: 1 }) } // Increase target node size const targetNode = nodes.get(targetId) if (targetNode) { targetNode.size++ } } } // Detect circular dependencies const circularDeps = detectCircularDependencies(nodes, links) // Mark nodes and links involved in circular dependencies const nodesInCircularDeps = new Set() const circularLinkKeys = new Set() for (const dep of circularDeps) { // Mark all nodes in the chain for (const nodeId of dep.chain) { nodesInCircularDeps.add(nodeId) const node = nodes.get(nodeId) if (node) { node.inCircularDep = true if (!node.circularChains) { node.circularChains = [] } node.circularChains.push(dep.chain) } } // Mark all edges in the chain for (const edge of dep.edges) { const linkKey = `${edge.source}->${edge.target}` circularLinkKeys.add(linkKey) const link = links.get(linkKey) if (link) { link.isCircular = true } } } console.log(`Found ${circularDeps.length} circular dependencies:`) circularDeps.forEach((dep, index) => { console.log(` ${index + 1}. ${dep.chain.join(' → ')}`) }) return { nodes: Array.from(nodes.values()), links: Array.from(links.values()), circularDependencies: circularDeps } } // Generate HTML visualization function generateHTML(graph: DependencyGraph): string { return ` ComfyUI Frontend Import Map
` } // Main function async function main() { console.log('Generating import map...') try { const graph = await generateDependencyGraph() console.log( `Found ${graph.nodes.length} nodes and ${graph.links.length} dependencies` ) if (graph.circularDependencies && graph.circularDependencies.length > 0) { console.log( `\n⚠️ Warning: Found ${graph.circularDependencies.length} circular dependencies!` ) } // Save JSON data const jsonPath = path.join( process.cwd(), 'scripts', 'map', 'import-map.json' ) fs.mkdirSync(path.dirname(jsonPath), { recursive: true }) fs.writeFileSync(jsonPath, JSON.stringify(graph, null, 2)) console.log(`Saved JSON data to ${jsonPath}`) // Generate and save HTML visualization const html = generateHTML(graph) const htmlPath = path.join( process.cwd(), 'scripts', 'map', 'import-map.html' ) fs.writeFileSync(htmlPath, html) console.log(`Saved HTML visualization to ${htmlPath}`) console.log('✅ Import map generation complete!') console.log( 'Open scripts/map/import-map.html in a browser to view the visualization' ) } catch (error) { console.error('Error generating import map:', error) process.exit(1) } } void main()