UNPKG

intellify

Version:

Detect JavaScript & TypeScript errors and make codes more optimized.

583 lines (455 loc) • 18.8 kB
console.clear(); import fs from 'fs'; import path from 'path'; import { Command } from 'commander'; import chalk from 'chalk'; import * as acorn from 'acorn'; import * as walk from 'acorn-walk'; import { globSync } from 'glob'; import readline from 'readline'; import boxen from 'boxen'; const program = new Command(); program .name('intellify') .description('Optimize your JavaScript projects by detecting errors and unused code') .version('1.0.0') .action(analyze); try { program.parse(); } catch (error) { console.error(chalk.red.bold('\nšŸ’„ Top-level application error:'), error); process.exit(1); } async function analyze() { try { console.clear(); const headerText = `${chalk.bold.cyan('🧠 INTELLIFY')}\n${chalk.cyan('Modern Code Intelligence Tool')}`; console.log(boxen(headerText, {padding: 1, margin: 1, borderStyle: 'double', borderColor: 'cyan', textAlignment: 'center'})); const projectPath = process.cwd(); const projectName = path.basename(projectPath); console.log(chalk.blue(`šŸ“‚ Project: `) + chalk.blue.bold(`${projectName}`) + chalk.blue(` (${projectPath})`)); const packageJsonPath = path.join(projectPath, 'package.json'); if (fs.existsSync(packageJsonPath)) { try { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); const projectInfo = { name: packageJson.name || projectName, version: packageJson.version || 'N/A', description: packageJson.description || 'No description', dependencies: Object.keys(packageJson.dependencies || {}).length, devDependencies: Object.keys(packageJson.devDependencies || {}).length }; const projectInfoText = `${chalk.white.bold(`${projectInfo.name}@${projectInfo.version}`)}\n` + `${chalk.white(projectInfo.description.substring(0,45))}${projectInfo.description.length > 45 ? '...' : ''}\n` + `${chalk.white(`Total Dependencies: ${projectInfo.dependencies + projectInfo.devDependencies}`)}`; console.log(boxen(projectInfoText, {title: 'Project Info', titleAlignment: 'center', padding: 1, margin: {top:1, bottom:1}, borderStyle: 'round', borderColor: 'blue'})); } catch (error) { console.log(chalk.yellow(boxen('Could not parse package.json', {padding:1, borderColor:'yellow'}))); } } const results = { syntaxErrors: [], unusedPackages: [], unusedVariables: [], commentLines: [], analyzedFiles: [] }; const stats = { filesAnalyzed: 0, linesOfCode: 0, fileTypes: {}, totalSize: 0 }; console.log(chalk.yellow('šŸ” Scanning files...')); const spinnerFrames = ['ā ‹', 'ā ™', 'ā ¹', 'ā ø', 'ā ¼', 'ā “', 'ā ¦', 'ā §', 'ā ‡', 'ā ']; let spinnerInterval; let spinnerFrame = 0; spinnerInterval = setInterval(() => { process.stdout.write(`\r${chalk.yellow(spinnerFrames[spinnerFrame])} Analyzing...`); spinnerFrame = (spinnerFrame + 1) % spinnerFrames.length; }, 80); await analyzeProject(projectPath, results, stats); clearInterval(spinnerInterval); process.stdout.write('\r' + ' '.repeat(30) + '\r'); displayResults(results, stats); await promptFix(results, projectPath); } catch (error) { console.error(chalk.red.bold('\nšŸ’„ Error during analysis:'), error); if (typeof spinnerInterval !== 'undefined' && spinnerInterval) { clearInterval(spinnerInterval); process.stdout.write('\r' + ' '.repeat(30) + '\r'); } process.exit(1); } } async function analyzeProject(projectPath, results, stats) { try { results.comments = []; const patterns = [ '**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx', '**/*.vue', '**/*.svelte' ]; const ignorePatterns = [ '**/node_modules/**', '**/dist/**', '**/build/**', '**/.git/**', '**/coverage/**', '**/.next/**', '**/out/**', '**/.cache/**', '**/public/build/**' ]; const files = globSync(patterns, { ignore: ignorePatterns, cwd: projectPath, absolute: true, }); const packageJsonPath = path.join(projectPath, 'package.json'); let packageDependencies = []; let packageDevDependencies = []; if (fs.existsSync(packageJsonPath)) { const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf-8'); const packageJson = JSON.parse(packageJsonContent); packageDependencies = Object.keys(packageJson.dependencies || {}); packageDevDependencies = Object.keys(packageJson.devDependencies || {}); } const importedPackages = new Set(); for (const file of files) { try { stats.filesAnalyzed++; const ext = path.extname(file).slice(1).toLowerCase(); stats.fileTypes[ext] = (stats.fileTypes[ext] || 0) + 1; const fileStat = fs.statSync(file); stats.totalSize += fileStat.size; const code = fs.readFileSync(file, 'utf-8'); const lineCount = code.split('\n').length; stats.linesOfCode += lineCount; const relativePath = path.relative(projectPath, file); const fileInfo = { name: path.basename(file), path: relativePath, size: fileStat.size, lines: lineCount, type: ext }; results.analyzedFiles.push(fileInfo); let ast; try { ast = acorn.parse(code, { ecmaVersion: 'latest', sourceType: 'module', locations: true, allowHashBang: true, allowAwaitOutsideFunction: true, onComment: (isBlock, text, start, end, locStart, locEnd) => { results.comments.push({ file, isBlock, text, start, end, locStart, locEnd }); } }); } catch (parsingError) { results.syntaxErrors.push({ file, line: parsingError.loc ? parsingError.loc.line : 'unknown', message: `Acorn parsing error: ${parsingError.message}`, }); console.warn(chalk.yellow(`Skipping analysis of ${file} due to parsing error: ${parsingError.message}`)); continue; } if (ast) { findUnusedVariables(file, ast, code, results); findImportedPackages(ast, importedPackages); } } catch (fileProcessingError) { console.error(chalk.red(`\nšŸ’„ Error processing file ${file} (outside parsing):`), fileProcessingError); results.syntaxErrors.push({ file, line: 'unknown', message: `File processing error: ${fileProcessingError.message}` }); } } findUnusedPackages( packageDependencies, packageDevDependencies, importedPackages, results ); } catch (projectAnalysisError) { console.error(chalk.red.bold('\nšŸ’„ Error during project analysis setup:'), projectAnalysisError); throw projectAnalysisError; } } function findComments(file, code, results) { } function findUnusedVariables(file, ast, code, results) { const declarations = new Map(); const usages = new Set(); walk.simple(ast, { VariableDeclarator(node) { if (node.id.type === 'Identifier') { declarations.set(node.id.name, { node, file, line: getLineFromLoc(node.loc, code), }); } }, FunctionDeclaration(node) { if (node.id && node.id.type === 'Identifier') { declarations.set(node.id.name, { node, file, line: getLineFromLoc(node.loc, code), }); } }, ClassDeclaration(node) { if (node.id && node.id.type === 'Identifier') { declarations.set(node.id.name, { node, file, line: getLineFromLoc(node.loc, code), }); } }, Identifier(node) { usages.add(node.name); }, }); for (const [name, info] of declarations.entries()) { if (!usages.has(name) || usages.has(name) && declarations.has(name)) { const parent = info.node.parent; if (parent && parent.type !== 'ExportNamedDeclaration' && parent.type !== 'ExportDefaultDeclaration') { results.unusedVariables.push({ file: info.file, name, line: info.line, }); } } } } function findImportedPackages(ast, importedPackages) { walk.simple(ast, { ImportDeclaration(node) { const source = node.source.value; if (!source.startsWith('.') && !source.startsWith('/')) { const packageName = source.split('/')[0]; importedPackages.add(packageName); } }, CallExpression(node) { if ( node.callee.type === 'Identifier' && node.callee.name === 'require' && node.arguments.length && node.arguments[0].type === 'Literal' ) { const source = node.arguments[0].value; if (typeof source === 'string' && !source.startsWith('.') && !source.startsWith('/')) { const packageName = source.split('/')[0]; importedPackages.add(packageName); } } }, }); } async function fixComments(allComments, projectPath) { console.log(chalk.blue('\nšŸ’¬ Attempting to remove comments...')); let commentsRemovedCount = 0; let commentsSkippedCount = 0; const commentsByFile = allComments.reduce((acc, comment) => { if (!acc[comment.file]) { acc[comment.file] = []; } acc[comment.file].push(comment); return acc; }, {}); for (const file in commentsByFile) { let fileContent = fs.readFileSync(file, 'utf-8'); const originalFileContent = fileContent; let fileModified = false; const sortedComments = commentsByFile[file].sort((a, b) => b.start - a.start); for (const comment of sortedComments) { let tempFileContent = fileContent.substring(0, comment.start) + fileContent.substring(comment.end); let lineOfChangeStart = comment.locStart.line; let checkContent; let tempLines = tempFileContent.split('\n'); let lineBeforeComment = originalFileContent.substring(0, comment.start).split('\n').pop(); let lineAfterComment = originalFileContent.substring(comment.end).split('\n').shift(); let isSafeToRemove = false; if (lineBeforeComment.trim() === '' && lineAfterComment.trim() === '') { isSafeToRemove = true; } else { checkContent = tempLines[lineOfChangeStart -1]; if (checkContent !== undefined && checkContent.trim() !== '') { try { acorn.parse(checkContent, { ecmaVersion: 'latest', sourceType: 'module', allowReturnOutsideFunction: true, allowAwaitOutsideFunction: true, allowHashBang: true }); isSafeToRemove = true; } catch (e) { isSafeToRemove = false; } } else if (checkContent !== undefined && checkContent.trim() === '') { isSafeToRemove = true; } } if (isSafeToRemove) { fileContent = tempFileContent; commentsRemovedCount++; fileModified = true; } else { commentsSkippedCount++; console.log(chalk.yellow(` Skipped comment in ${path.basename(file)} at line ${comment.locStart.line} (potential syntax issue)`)); } } if (fileModified) { try { acorn.parse(fileContent, { ecmaVersion: 'latest', sourceType: 'module', allowHashBang: true, allowAwaitOutsideFunction: true }); fs.writeFileSync(file, fileContent, 'utf-8'); console.log(chalk.green(` Updated ${path.basename(file)} with comment removals.`)); } catch (e) { fs.writeFileSync(file, originalFileContent, 'utf-8'); console.log(chalk.red(` Failed to update ${path.basename(file)} due to syntax errors after attempting comment removals. File reverted.`)); commentsSkippedCount += sortedComments.length; commentsRemovedCount -= sortedComments.length; if(commentsRemovedCount < 0) commentsRemovedCount = 0; } } } if (commentsRemovedCount > 0) { console.log(chalk.green.bold(`āœ… Successfully removed ${commentsRemovedCount} comments.`)); } if (commentsSkippedCount > 0) { console.log(chalk.yellow.bold(`āš ļø Skipped ${commentsSkippedCount} comments due to potential syntax issues or batch failure.`)); } if (commentsRemovedCount === 0 && commentsSkippedCount === 0 && allComments.length > 0) { console.log(chalk.blue('No comments were removed or skipped (perhaps all were safe but no changes made, or no comments to remove).')); } } function findUnusedPackages(dependencies, devDependencies, importedPackages, results) { for (const dep of dependencies) { if (!importedPackages.has(dep)) { results.unusedPackages.push({ name: dep, type: 'dependency', }); } } for (const dep of devDependencies) { if (!importedPackages.has(dep)) { results.unusedPackages.push({ name: dep, type: 'devDependency', }); } } } function getLineFromLoc(loc, code) { if (!loc) return 'unknown'; return loc.start.line; } function displayResults(results, stats) { console.log('\n✨ Analysis Complete ✨'); console.log(); } async function promptFix(results, projectPath) { if (results.syntaxErrors.length === 0 && results.unusedPackages.length === 0 && results.unusedVariables.length === 0 && (!results.comments || results.comments.length === 0)) { console.log(chalk.green.bold('✨ Your project looks clean! No issues found.')); return; } const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); let optimizationOptionsText = ''; if (results.unusedPackages.length > 0) { optimizationOptionsText += `${chalk.yellow.bold('šŸ“¦ Remove unused packages')} ${chalk.yellow(`(${results.unusedPackages.length})`)}\n`; } if (results.unusedVariables.length > 0) { optimizationOptionsText += `${chalk.magenta.bold('šŸ” Remove unused variables')} ${chalk.magenta(`(${results.unusedVariables.length})`)}\n`; } if (results.comments && results.comments.length > 0) { optimizationOptionsText += `${chalk.blue.bold('šŸ’¬ Remove comments')} ${chalk.blue(`(${results.comments.length})`)}\n`; } if (optimizationOptionsText.length > 0) { console.log(boxen(optimizationOptionsText.trim(), {title: 'Optimization Options', titleAlignment: 'center', padding: 1, margin: {top:1, bottom:1}, borderStyle: 'round', borderColor: 'cyan'})); } else { } console.log(chalk.cyan.bold('Would you like to optimize your codebase?')); const answer = 'yes'; console.log(chalk.cyan('šŸ’¬ (yes/no): yes')); if (answer.toLowerCase() === 'yes' || answer.toLowerCase() === 'y') { console.log(chalk.green.bold('\nšŸ”§ Starting optimization process...')); if (results.unusedPackages.length > 0) { console.log(chalk.yellow.bold('\nšŸ“¦ Would you like to remove unused packages?')); const removePackages = 'yes'; console.log(chalk.yellow('šŸ’¬ (yes/no): yes')); if (removePackages.toLowerCase() === 'yes' || removePackages.toLowerCase() === 'y') { process.stdout.write(chalk.yellow('\nā–¬ Removing unused packages... ')); const progressChars = ['ā–‚', 'ā–ƒ', 'ā–…', 'ā–†', 'ā–‡', 'ā–ˆ']; let progressIndex = 0; const progressInterval = setInterval(() => { process.stdout.write(`\r${chalk.yellow('ā–¬ Removing unused packages... ')}${chalk.yellow(progressChars[progressIndex])}`); progressIndex = (progressIndex + 1) % progressChars.length; }, 100); await fixUnusedPackages(results.unusedPackages, projectPath); clearInterval(progressInterval); process.stdout.write(`\r${chalk.yellow('ā–¬ Removing unused packages... ')}${chalk.green('āœ” Done!')}\n`); } } if (results.unusedVariables.length > 0) { console.log(chalk.magenta.bold('\nšŸ” Would you like to remove unused variables?')); const removeVariables = 'yes'; console.log(chalk.magenta('šŸ’¬ (yes/no): yes')); if (removeVariables.toLowerCase() === 'yes' || removeVariables.toLowerCase() === 'y') { await fixUnusedVariables(results.unusedVariables, projectPath); } } if (results.comments && results.comments.length > 0) { console.log(chalk.blue.bold('\nšŸ’¬ Would you like to remove comments?')); const removeComments = 'yes'; console.log(chalk.blue('šŸ’¬ (yes/no): yes')); if (removeComments.toLowerCase() === 'yes' || removeComments.toLowerCase() === 'y') { await fixComments(results.comments, projectPath); } } console.log(chalk.green.bold('\nāœ… All selected optimizations have been applied!')); console.log(chalk.green('šŸš€ Your codebase is now faster and cleaner.')); } else { console.log(chalk.blue('\nšŸ‘‹ No problem! You can run intellify again anytime to optimize your code.')); } rl.close(); } async function fixUnusedPackages(unusedPackages, projectPath) { console.log(chalk.yellow('Removing unused packages from package.json...')); const packageJsonPath = path.join(projectPath, 'package.json'); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); let packagesRemoved = 0; for (const pkg of unusedPackages) { if (pkg.type === 'dependency' && packageJson.dependencies && packageJson.dependencies[pkg.name]) { delete packageJson.dependencies[pkg.name]; packagesRemoved++; } else if (pkg.type === 'devDependency' && packageJson.devDependencies && packageJson.devDependencies[pkg.name]) { delete packageJson.devDependencies[pkg.name]; packagesRemoved++; } } fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); console.log(chalk.green(`āœ… Successfully removed ${packagesRemoved} unused packages from package.json!`)); } async function fixUnusedVariables(unusedVariables, projectPath) { console.log(chalk.magenta('This would remove unused variables from your code files.')); console.log(chalk.yellow('Note: For safety reasons, this operation is not implemented in this version.')); console.log(chalk.yellow('Please remove the following unused variables manually:')); unusedVariables.forEach(variable => { console.log(chalk.yellow(` • ${variable.name} in ${path.basename(variable.file)} (line ${variable.line})`)); }); }