UNPKG

ctx-gen

Version:

AI-Enhanced Documentation Generator for Code Understanding

233 lines 8.95 kB
import fs from 'fs-extra'; import { glob } from 'glob'; import { isDirectory } from 'path-type'; import chalk from 'chalk'; import path from 'path'; import { generateMetadata } from '../generators/metadataGenerator.js'; import { generateDiagrams } from '../generators/diagramGenerator.js'; import { generateBatchDocumentation } from '../generators/fileDocGenerator.js'; import { createIndex } from '../generators/indexGenerator.js'; import { generateMachineReadable } from '../generators/machineReadableGenerator.js'; import { updateGitIgnore, readGitignorePatterns, shouldExcludeFile } from '../utils/fileUtils.js'; /** * Generate full documentation for a codebase * @param options - Configuration options for doc generation */ export async function generateFullDocs(options) { // Create output directory if it doesn't exist await fs.ensureDir(options.docsDir); // Initialize empty cache const cache = {}; // Process files const files = await getFilesToProcess(options); // Generate file documentation await generateDocumentation(files, options, cache); // Generate metadata await generateMetadata(options); // Generate diagrams if requested if (options.diagrams && options.diagrams.length > 0) { await generateDiagrams(options); } // Generate machine-readable formats if requested if (options.machineFormats) { await generateMachineReadable(options); } // Create index file await createIndex(options); // Update cache file await fs.writeJSON(options.cacheFile, cache, { spaces: 2 }); // Add docs directory to .gitignore if requested if (options.autoIgnore) { await updateGitIgnore(options.docsDir); } } /** * Update existing documentation * @param options - Configuration options for doc update */ export async function updateDocs(options) { // Check if cache file exists let cache = {}; if (await fs.pathExists(options.cacheFile)) { cache = await fs.readJSON(options.cacheFile); } // Get files to process const files = await getFilesToProcess(options); // Generate documentation for modified files await generateDocumentation(files, options, cache); // Regenerate metadata await generateMetadata(options); // Regenerate diagrams if requested if (options.diagrams && options.diagrams.length > 0) { await generateDiagrams(options); } // Regenerate machine-readable formats if requested if (options.machineFormats) { await generateMachineReadable(options); } // Update index file await createIndex(options); // Update cache file await fs.writeJSON(options.cacheFile, cache, { spaces: 2 }); } /** * Get the list of files to process based on options * @param options - Configuration options * @returns Array of file paths to process */ async function getFilesToProcess(options) { console.log(chalk.blue('Finding files to process...')); const excludePatterns = options.exclude.split(','); const languages = options.languages.split(','); console.log(`Languages to analyze: ${chalk.cyan(languages.join(', '))}`); // Get gitignore patterns if requested const gitignorePatterns = options.respectGitignore ? await readGitignorePatterns() : []; if (options.respectGitignore && gitignorePatterns.length > 0) { console.log(`Found ${chalk.yellow(gitignorePatterns.length)} patterns in .gitignore`); } // Log excluded patterns console.log(`Excluded patterns: ${chalk.yellow(excludePatterns.join(', '))}`); // Map language names to file extensions const extensionMap = { typescript: ['ts', 'tsx'], javascript: ['js', 'jsx', 'mjs', 'cjs'], python: ['py'], java: ['java'], csharp: ['cs'], cpp: ['cpp', 'cc', 'hpp', 'h'], c: ['c', 'h'], ruby: ['rb'], php: ['php'], go: ['go'], rust: ['rs'], swift: ['swift'], kotlin: ['kt'], scala: ['scala'], }; // Create glob patterns for each language, handling both direct extensions and mapped languages const patterns = []; for (const lang of languages) { const extensions = extensionMap[lang.trim().toLowerCase()]; if (extensions) { // If we have a mapping for this language, use all its extensions extensions.forEach(ext => patterns.push(`**/*.${ext}`)); } else { // Otherwise assume the language name is the extension patterns.push(`**/*.${lang}`); } } console.log(`Looking for file patterns: ${chalk.cyan(patterns.join(', '))}`); // First, get all potential files without ignoring anything const potentialFiles = await glob(patterns, { cwd: process.cwd(), absolute: true, }); console.log(`Found ${chalk.green(potentialFiles.length)} potential files before applying exclusions`); // Then manually filter out the excluded files const files = []; const ignoredFiles = []; const ignoredFolders = new Set(); for (const file of potentialFiles) { // Skip directories if (await isDirectory(file)) { continue; } // Check if file should be excluded const relativePath = path.relative(process.cwd(), file); // First check explicit exclude patterns let excluded = false; let _excludeReason = ''; for (const pattern of excludePatterns) { if (relativePath.includes(pattern)) { excluded = true; _excludeReason = `matched exclude pattern: ${pattern}`; // Track excluded folder const parts = relativePath.split('/'); for (let i = 0; i < parts.length; i++) { const folderPath = parts.slice(0, i + 1).join('/'); if (folderPath.includes(pattern)) { ignoredFolders.add(folderPath); break; } } break; } } // Then check gitignore patterns if (!excluded && options.respectGitignore) { for (const pattern of gitignorePatterns) { if (shouldExcludeFile(file, [], [pattern])) { excluded = true; _excludeReason = `matched gitignore pattern: ${pattern}`; break; } } } if (excluded) { ignoredFiles.push(relativePath); } else { files.push(file); } } // Summarize ignored information if (ignoredFiles.length > 0) { console.log(`${chalk.yellow('Ignored')} ${chalk.bold(ignoredFiles.length)} files based on exclusion patterns`); // Log first few ignored files as examples const exampleCount = Math.min(5, ignoredFiles.length); console.log(chalk.gray(`Examples of ignored files (showing ${exampleCount} of ${ignoredFiles.length}):`)); for (let i = 0; i < exampleCount; i++) { console.log(chalk.gray(` - ${ignoredFiles[i]}`)); } // Log ignored folders if (ignoredFolders.size > 0) { console.log(chalk.gray(`Ignored folders (${ignoredFolders.size}):`)); Array.from(ignoredFolders) .slice(0, 5) .forEach(folder => { console.log(chalk.gray(` - ${folder}/`)); }); if (ignoredFolders.size > 5) { console.log(chalk.gray(` ... and ${ignoredFolders.size - 5} more`)); } } } console.log(`${chalk.green('Found')} ${chalk.bold(files.length)} files to process`); return files; } /** * Generate documentation for a list of files * @param files - Array of file paths * @param options - Configuration options * @param cache - File cache object */ async function generateDocumentation(files, options, cache) { // Filter files that need to be processed (not in cache or modified) const filesToProcess = []; for (const file of files) { const stats = await fs.stat(file); const modTime = stats.mtime.getTime(); // Skip if file hasn't been modified since last run if (cache[file] && cache[file].modTime === modTime) { continue; } filesToProcess.push(file); } if (filesToProcess.length === 0) { console.log('No files need updating.'); return; } // Generate documentation for files in batches await generateBatchDocumentation(filesToProcess, options); // Update cache for processed files for (const file of filesToProcess) { const stats = await fs.stat(file); const modTime = stats.mtime.getTime(); cache[file] = { modTime, path: file, }; } } //# sourceMappingURL=ctxGen.js.map