UNPKG

code-graph-generator

Version:

Generate Json Object of code that can be used to generate code-graphs for JavaScript/TypeScript/Range projects

270 lines 12.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __exportStar = (this && this.__exportStar) || function(m, exports) { for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.createCodeGraph = createCodeGraph; // src/index.ts const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const glob_1 = require("glob"); const js_parser_1 = require("./parser/js-parser"); const react_parser_1 = require("./parser/react-parser"); const ts_parser_1 = require("./parser/ts-parser"); const graph_builder_1 = require("./analyzer/graph-builder"); const file_utils_1 = require("./utils/file-utils"); const queue_1 = require("./utils/queue"); const relationship_analyzer_1 = require("./analyzer/relationship-analyzer"); const ast_utils_1 = require("./utils/ast-utils"); const DEFAULT_INCLUDE = ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx']; const DEFAULT_EXCLUDE = [ '**/node_modules/**', '**/dist/**', '**/build/**', '**/*.test.*', '**/*.spec.*', '**/*.min.*', ]; async function createCodeGraph(options) { const { projectName, rootDir, include = DEFAULT_INCLUDE, exclude = DEFAULT_EXCLUDE, concurrency = 10, includeDeclarations = false, includeNodeModules = false, customParsers = {}, debug = false, outputPath, streamOutput = false, } = options; file_utils_1.logger.setDebug(debug); file_utils_1.logger.info(`Starting code graph generation for ${projectName}`); file_utils_1.logger.info(`Root directory: ${rootDir}`); // Set up exclusion patterns let finalExclude = [...exclude]; if (!includeDeclarations) { finalExclude.push('**/*.d.ts'); } if (!includeNodeModules && !finalExclude.includes('**/node_modules/**')) { finalExclude.push('**/node_modules/**'); } // Configure parsers const parsers = { '.js': customParsers['.js'] || (0, js_parser_1.createJsParser)(), '.jsx': customParsers['.jsx'] || (0, react_parser_1.createReactParser)(), '.ts': customParsers['.ts'] || (0, ts_parser_1.createTsParser)(), '.tsx': customParsers['.tsx'] || (0, react_parser_1.createReactParser)(), ...customParsers, }; // Create graph builder const graphBuilder = new graph_builder_1.IncrementalGraphBuilder(projectName, rootDir); // Find files to analyze with improved glob pattern file_utils_1.logger.info('Finding files to analyze...'); const files = await (0, glob_1.glob)(include, { cwd: rootDir, ignore: finalExclude, absolute: false, dot: true, // Include dot files like .jsx nocase: false, // Preserve case }); // Sort files to ensure consistent processing order files.sort((a, b) => a.localeCompare(b)); file_utils_1.logger.info(`Found ${files.length} files to analyze`); // Check for index.jsx specifically at the root (it's often missed) if (!files.some(f => f.toLowerCase() === 'index.jsx')) { const indexExists = await (0, file_utils_1.fileExists)(path_1.default.join(rootDir, 'index.jsx')); if (indexExists) { files.push('index.jsx'); file_utils_1.logger.info('Added index.jsx file that was missed by glob'); } } const queue = new queue_1.TaskQueue(concurrency); let completedFiles = 0; const errors = []; const batchSize = 100; for (let i = 0; i < files.length; i += batchSize) { const batch = files.slice(i, i + batchSize); const batchPromises = batch.map(filePath => queue.push(async () => { try { const fullPath = path_1.default.join(rootDir, filePath); const content = await (0, file_utils_1.readFile)(fullPath); // Get file extension (case insensitive) const ext = path_1.default.extname(filePath).toLowerCase(); const parser = parsers[ext]; if (!parser) { file_utils_1.logger.warn(`No parser found for ${ext}, skipping ${filePath}`); return; } file_utils_1.logger.debug(`Parsing ${filePath}`); // Keep original case in filePath const fileGraph = await parser.parse(filePath, content); graphBuilder.addFile(fileGraph); completedFiles++; if (debug && completedFiles % 50 === 0 || completedFiles === files.length) { file_utils_1.logger.debug(`Progress: ${completedFiles}/${files.length} files processed (${Math.round(completedFiles / files.length * 100)}%)`); } } catch (error) { file_utils_1.logger.error(`Error processing ${filePath}:`, error); errors.push(error); } })); await Promise.all(batchPromises); } await queue.waitForAll(); if (errors.length > 0) { file_utils_1.logger.warn(`Completed with ${errors.length} errors. Some files may not be included in the graph.`); } file_utils_1.logger.info('All files processed'); file_utils_1.logger.info('Generating initial code graph'); const codeGraph = graphBuilder.getGraph(); // Ensure root package is properly represented ensurePackageStructure(codeGraph); file_utils_1.logger.info('Analyzing relationships between code elements...'); const codeGraphWithRelationships = (0, relationship_analyzer_1.analyzeRelationships)(codeGraph); if (streamOutput && outputPath) { file_utils_1.logger.info(`Streaming results to ${outputPath}`); await (0, file_utils_1.ensureDir)(path_1.default.dirname(outputPath)); const writeStream = fs_1.default.createWriteStream(outputPath); await graphBuilder.writeToStream(writeStream); return codeGraphWithRelationships; // Return enhanced graph even when streaming } if (outputPath) { file_utils_1.logger.info(`Writing results to ${outputPath}`); await (0, file_utils_1.ensureDir)(path_1.default.dirname(outputPath)); await fs_1.default.promises.writeFile(outputPath, JSON.stringify(codeGraphWithRelationships, null, 2), 'utf-8'); } file_utils_1.logger.info('Code graph generation complete'); return codeGraphWithRelationships; } /** * Ensures the package structure has both root and src packages if needed */ /** * Ensures the package structure has both root and src packages with correct dependencies */ function ensurePackageStructure(codeGraph) { // Find if we have root and src packages let rootPackage = codeGraph.packages.find(p => p.name === '.' || p.name === ''); const srcPackage = codeGraph.packages.find(p => p.name === 'src'); // Check for files at root level const rootFiles = []; const srcFiles = []; // Organize files into correct packages for (const pkg of codeGraph.packages) { for (const file of pkg.files) { if (file.path.startsWith('src/')) { srcFiles.push(file); } else if (!file.path.includes('/')) { rootFiles.push(file); } } } // Create or update src package if (srcFiles.length > 0) { if (!srcPackage) { codeGraph.packages.push({ name: 'src', files: srcFiles, dependencies: [], exports: srcFiles.flatMap(f => f.exports || []) }); } else { // Update src package files srcPackage.files = srcFiles; srcPackage.exports = srcFiles.flatMap(f => f.exports || []); } } // Create or update root package if (rootFiles.length > 0) { if (!rootPackage) { rootPackage = { name: '.', files: rootFiles, dependencies: [], exports: rootFiles.flatMap(f => f.exports || []) }; codeGraph.packages.push(rootPackage); } else { // Update root package files rootPackage.files = rootFiles; rootPackage.exports = rootFiles.flatMap(f => f.exports || []); } } // Analyze dependencies between packages for (const pkg of codeGraph.packages) { const packageDeps = new Set(); for (const file of pkg.files) { // Check each file dependency for (const dep of file.dependencies || []) { // Skip external/absolute dependencies if (!dep.startsWith('.')) continue; // Follow the relative import const fileDir = path_1.default.dirname(file.path); let resolvedPath = (0, ast_utils_1.normalizePath)(path_1.default.join(fileDir, dep)); // Find the target package let targetPkg; // Check if this path points to files in another package for (const otherPkg of codeGraph.packages) { if (otherPkg === pkg) continue; // Skip self // Try to match by exact path or directory const matchesFile = otherPkg.files.some(f => { // Try with extensions if (path_1.default.extname(resolvedPath) === '') { const extensions = ['.js', '.jsx', '.ts', '.tsx']; for (const ext of extensions) { if (f.path === `${resolvedPath}${ext}`) { return true; } } return false; } else { return f.path === resolvedPath; } }); // Try directory match (for index files) const matchesDir = otherPkg.files.some(f => f.path.startsWith(`${resolvedPath}/`) || f.path === `${resolvedPath}/index.js` || f.path === `${resolvedPath}/index.jsx` || f.path === `${resolvedPath}/index.ts` || f.path === `${resolvedPath}/index.tsx`); if (matchesFile || matchesDir) { targetPkg = otherPkg.name; break; } } if (targetPkg) { packageDeps.add(targetPkg); } } } // Update package dependencies pkg.dependencies = Array.from(packageDeps); } // Special case: if rootPackage imports from src and not vice versa, add the dependency if (rootPackage && srcPackage) { // Check file dependencies const hasSrcDep = rootPackage.files.some(file => file.dependencies?.some(dep => dep === './src' || dep.startsWith('./src/'))); // Check detailed imports for the App case const hasAppDep = rootPackage.files.some(file => file.dependencies?.some(dep => dep === './App') || (file.detailedDependencies?.some(dep => dep.module === './App' || dep.module === './src/App'))); if ((hasSrcDep || hasAppDep) && !rootPackage.dependencies.includes('src')) { rootPackage.dependencies.push('src'); } } } __exportStar(require("./types/interfaces"), exports); exports.default = { createCodeGraph }; //# sourceMappingURL=index.js.map