agentsqripts
Version:
Comprehensive static code analysis toolkit for identifying technical debt, security vulnerabilities, performance issues, and code quality problems
460 lines (388 loc) ⢠13.4 kB
JavaScript
#!/usr/bin/env node
/**
* @file Utility function promotion script
* @description Automatically promotes utility functions to exports and updates index files based on the refactoring analysis
*/
const fs = require('fs');
const path = require('path');
const { getProcessArgs } = require('../lib/utils/processHelpers');
const { analyzeProjectRefactoring } = require('../lib/export-promotion/projectRefactoringAnalyzer');
const { analyzeFileRefactoring } = require('../lib/export-promotion/fileRefactoringAnalyzer');
const { DEFAULT_CONFIG, PATTERNS } = require('../config/localVars');
const { parseArgs: sharedParseArgs } = require('./lib/argumentParser');
/**
* Show help information
*/
function showHelp() {
console.log(`Usage: node promote-exports.js [OPTIONS] <path>
Automatically promotes utility functions to exports and updates index files for better code organization.
OPTIONS:
--dry-run Show what would be changed without making modifications
--target-dirs <dirs> Comma-separated list of target utility directories
--extensions <exts> Comma-separated list of file extensions (default: .js,.ts,.jsx,.tsx)
--index-files <files> Comma-separated list of index file names (default: index.ts,index.js)
--skip-private Skip functions starting with underscore (default: true)
--help Show this help message
EXAMPLES:
node promote-exports.js .
node promote-exports.js --dry-run --target-dirs lib,src/utils .
node promote-exports.js --extensions .js,.ts --skip-private src/
WHAT IT DOES:
1. Analyzes project for unexported utility functions
2. Adds 'export' keyword to eligible functions and classes
3. Updates or creates index files with appropriate re-exports
4. Provides detailed reporting of all changes made
SAFETY:
- Skips functions starting with underscore (private/internal)
- Only modifies files in specified target directories
- Uses --dry-run to preview changes before applying them`);
}
/**
* Promote exports in a single file
*/
async function promoteExportsInFile(filePath, options = {}) {
const { skipPrivate = true } = options;
try {
const content = await fs.promises.readFile(filePath, 'utf8');
const lines = content.split('\n');
let changed = false;
const promoted = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Skip already exported items
if (PATTERNS.export.test(line)) {
continue;
}
// Check for function declarations
const functionMatch = line.match(PATTERNS.functionDeclaration);
if (functionMatch) {
const name = functionMatch[1];
// Skip private functions if option is set
if (skipPrivate && name.startsWith('_')) {
continue;
}
// Skip if name contains 'internal' (case insensitive)
if (name.toLowerCase().includes('internal')) {
continue;
}
lines[i] = `export ${line}`;
promoted.push({
type: 'function',
name,
line: i + 1,
declarationType: 'function',
content: line.trim()
});
changed = true;
}
// Check for arrow functions
const arrowMatch = line.match(PATTERNS.arrowFunction);
if (arrowMatch) {
const name = arrowMatch[1];
// Skip private functions if option is set
if (skipPrivate && name.startsWith('_')) {
continue;
}
// Skip if name contains 'internal' (case insensitive)
if (name.toLowerCase().includes('internal')) {
continue;
}
lines[i] = `export ${line}`;
promoted.push({
type: 'function',
name,
line: i + 1,
declarationType: 'const',
content: line.trim()
});
changed = true;
}
// Check for class declarations
const classMatch = line.match(PATTERNS.classDeclaration);
if (classMatch) {
const className = classMatch[1];
// Skip private classes
if (skipPrivate && className.startsWith('_')) {
continue;
}
lines[i] = `export ${line}`;
promoted.push({
type: 'class',
name: className,
line: i + 1,
declarationType: 'class',
content: line.trim()
});
changed = true;
}
}
return {
filePath,
promoted,
changed,
newContent: changed ? lines.join('\n') : content
};
} catch (error) {
return {
filePath,
error: error.message
};
}
}
/**
* Get or create index file for a directory
*/
async function getIndexFile(directory, options = {}) {
const { indexFiles = DEFAULT_CONFIG.indexFiles } = options;
// Check if any index file already exists
for (const fileName of indexFiles) {
const indexPath = path.join(directory, fileName);
try {
await fs.promises.access(indexPath);
return indexPath;
} catch (error) {
// File doesn't exist, continue to next
}
}
// Create new index file (prefer .ts if no existing file found)
const newIndexPath = path.join(directory, indexFiles[0] || 'index.ts');
return newIndexPath;
}
/**
* Update index file with new exports
*/
async function updateIndexFile(indexPath, promotedItems, sourceFile, options = {}) {
const { dryRun = false } = options;
const relativePath = './' + path.basename(sourceFile, path.extname(sourceFile));
const updates = [];
let existingContent = '';
let isNewFile = true;
try {
existingContent = await fs.promises.readFile(indexPath, 'utf8');
isNewFile = false;
} catch (error) {
// File doesn't exist, will be created
}
for (const { name } of promotedItems) {
const exportLine = `export { ${name} } from '${relativePath}';`;
if (!existingContent.includes(exportLine)) {
updates.push(exportLine);
if (!dryRun) {
await fs.promises.appendFile(indexPath, exportLine + '\n');
}
}
}
return {
indexPath,
updates,
isNewFile
};
}
/**
* Promote exports across the project
*/
async function promoteProjectExports(projectPath, options = {}) {
const {
targetDirs = DEFAULT_CONFIG.targetDirs,
dryRun = false,
skipPrivate = true
} = options;
console.log(`š§ ${dryRun ? 'Analyzing' : 'Promoting'} exports in: ${projectPath}`);
console.log(`š Target directories: ${targetDirs.join(', ')}`);
console.log(`š Extensions: ${options.extensions?.join(', ') || DEFAULT_CONFIG.validExtensions.join(', ')}`);
console.log(`${dryRun ? 'š DRY RUN MODE - No changes will be made' : 'āļø APPLYING CHANGES'}`);
console.log('');
// First, get refactoring analysis
const analysis = await analyzeProjectRefactoring(projectPath, {
targetDirs,
includeNonUtility: false,
includeBarrelFiles: true
});
const results = {
totalFiles: 0,
filesChanged: 0,
totalPromotions: 0,
indexFilesUpdated: 0,
fileResults: [],
indexResults: [],
errors: []
};
// Process files with promotion opportunities
for (const fileAnalysis of analysis.files) {
if (fileAnalysis.promotionOpportunities.length === 0) continue;
results.totalFiles++;
// Promote exports in this file
const promotionResult = await promoteExportsInFile(fileAnalysis.file, { skipPrivate });
if (promotionResult.error) {
results.errors.push(promotionResult);
continue;
}
if (promotionResult.changed) {
results.filesChanged++;
results.totalPromotions += promotionResult.promoted.length;
// Write changes if not dry run
if (!dryRun) {
await fs.promises.writeFile(promotionResult.filePath, promotionResult.newContent);
}
// Update index file
const directory = path.dirname(promotionResult.filePath);
let indexPath, indexResult;
try {
indexPath = await getIndexFile(directory, options);
indexResult = await updateIndexFile(indexPath, promotionResult.promoted, promotionResult.filePath, { dryRun });
} catch (error) {
console.error(`Error updating index file for ${directory}:`, error.message);
results.errors.push({ directory, error: error.message });
continue;
}
if (indexResult.updates.length > 0) {
results.indexFilesUpdated++;
results.indexResults.push(indexResult);
}
results.fileResults.push(promotionResult);
}
}
return results;
}
/**
* Format and display results
*/
function displayResults(results, dryRun = false) {
const action = dryRun ? 'WOULD PROMOTE' : 'PROMOTED';
if (results.totalPromotions === 0) {
console.log('ā
No utility functions found that need export promotion.');
return;
}
console.log(`ā
${action} ${results.totalPromotions} functions to exports:\n`);
// Show file changes
results.fileResults.forEach(fileResult => {
console.log(`š ${fileResult.filePath}`);
fileResult.promoted.forEach(item => {
const typeIcon = item.type === 'function' ? 'ā”' :
item.type === 'class' ? 'šļø' : 'š¦';
console.log(` ${typeIcon} export ${item.declarationType} ${item.name} (line ${item.line})`);
});
console.log('');
});
// Show index file updates
if (results.indexResults.length > 0) {
console.log(`š ${dryRun ? 'WOULD UPDATE' : 'UPDATED'} ${results.indexFilesUpdated} index files:\n`);
results.indexResults.forEach(indexResult => {
const fileIcon = indexResult.isNewFile ? 'š' : 'š';
console.log(`${fileIcon} ${indexResult.indexPath}`);
indexResult.updates.forEach(update => {
console.log(` ā ${update}`);
});
console.log('');
});
}
// Show summary
console.log(`š SUMMARY:`);
console.log(` Files processed: ${results.totalFiles}`);
console.log(` Files ${dryRun ? 'to be changed' : 'changed'}: ${results.filesChanged}`);
console.log(` Functions ${dryRun ? 'to be promoted' : 'promoted'}: ${results.totalPromotions}`);
console.log(` Index files ${dryRun ? 'to be updated' : 'updated'}: ${results.indexFilesUpdated}`);
if (results.errors.length > 0) {
console.log(` ā Errors: ${results.errors.length}`);
results.errors.forEach(error => {
console.log(` ${error.filePath}: ${error.error}`);
});
}
}
/**
* Parse command line arguments
*/
function parseArgs() {
const toolOptions = {
dryRun: {
flags: ['--dry-run'],
boolean: true,
default: false,
description: 'Show what would be changed without making modifications'
},
targetDirs: {
flags: ['--target-dirs'],
parser: (value) => value.split(',').map(dir => dir.trim()),
default: DEFAULT_CONFIG.targetDirs,
description: 'Comma-separated list of target utility directories'
},
indexFiles: {
flags: ['--index-files'],
parser: (value) => value.split(',').map(file => file.trim()),
default: DEFAULT_CONFIG.indexFiles,
description: 'Comma-separated list of index file names'
},
skipPrivate: {
flags: ['--skip-private'],
boolean: true,
default: true,
description: 'Skip functions starting with underscore'
},
includePrivate: {
flags: ['--include-private'],
boolean: true,
description: 'Include private functions'
}
};
const { options: parsedOptions, targetPath, error } = sharedParseArgs(getProcessArgs(), {
defaults: {
extensions: DEFAULT_CONFIG.validExtensions,
dryRun: false,
targetDirs: DEFAULT_CONFIG.targetDirs,
indexFiles: DEFAULT_CONFIG.indexFiles,
skipPrivate: true
},
flags: {
extensions: { type: 'list' },
dryRun: { type: 'boolean' },
targetDirs: { type: 'list' },
indexFiles: { type: 'list' },
skipPrivate: { type: 'boolean' },
includePrivate: { type: 'boolean' }
}
});
if (error) {
console.error(`ā Error: ${error}`);
process.exit(1);
}
const options = parsedOptions;
// Handle include-private flag
if (options.includePrivate) options.skipPrivate = false;
if (options.help) {
showHelp();
process.exit(0);
}
return { options, targetPath };
}
/**
* Main execution function
*/
async function main() {
try {
const { options, targetPath } = parseArgs();
try {
await fs.promises.access(targetPath);
} catch (error) {
console.error(`ā Error: Path '${targetPath}' does not exist`);
process.exit(1);
}
const results = await promoteProjectExports(targetPath, options);
displayResults(results, options.dryRun);
if (options.dryRun && results.totalPromotions > 0) {
console.log('\nš” To apply these changes, run the command again without --dry-run');
}
} catch (error) {
console.error(`ā Error: ${error.message}`);
process.exit(1);
}
}
// Run the CLI
if (require.main === module) {
main().catch(error => { console.error("Fatal error:", error); process.exit(1); });
}
module.exports = {
main,
promoteProjectExports,
promoteExportsInFile,
updateIndexFile
};