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
JavaScript
;
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