UNPKG

ts-comment-remover

Version:

TypeScript file compression tool that removes comments and unnecessary whitespace using AST

240 lines • 10.4 kB
import { createInterface } from 'node:readline/promises'; import { stdin as input, stdout as output } from 'node:process'; import chalk from 'chalk'; import { readFile } from 'node:fs/promises'; import { compressTypeScriptFiles, defaultCompressionPipeline, } from './compressor.js'; import { getTypeScriptFiles, copyToClipboard, formatFileSize, getRelativePath, calculateStats, } from './utils.js'; const createMenuDisplay = (files, baseDir, selectedIndices, isMultiSelectMode) => { const header = [ chalk.cyan('========================================'), chalk.cyan.bold('TypeScript File Compression Tool'), chalk.cyan('========================================\n'), isMultiSelectMode ? chalk.yellow.bold('šŸ“Œ Multi-select mode (press m to exit)') : '', selectedIndices.size > 0 ? chalk.green(`āœ“ ${selectedIndices.size} file(s) selected`) : '', chalk.yellow('\nAvailable files:\n'), ] .filter(Boolean) .join('\n'); const fileList = files .map((file, index) => { const relativePath = getRelativePath(file, baseDir); const isSelected = selectedIndices.has(index); const prefix = isSelected ? chalk.green('āœ“') : ' '; const number = chalk.gray(`${index + 1}.`); const fileName = isSelected ? chalk.green(relativePath) : chalk.white(relativePath); return ` ${prefix} ${number} ${fileName}`; }) .join('\n'); const options = [ chalk.gray('\n========================================'), isMultiSelectMode ? chalk.yellow(' Select files by number (comma-separated)') : chalk.gray(' 1-9. Select single file'), !isMultiSelectMode && chalk.gray(' m. Multi-select mode'), selectedIndices.size > 0 && chalk.cyan(` c. Compress selected (${selectedIndices.size} files)`), selectedIndices.size > 0 && chalk.gray(' x. Clear selection'), chalk.gray(' a. Compress all files'), chalk.gray(' q. Quit\n'), ] .filter(Boolean) .join('\n'); return `${header}\n${fileList}${options}`; }; const parseChoice = (choice, fileCount, isMultiSelectMode, hasSelection) => { const lowerChoice = choice.toLowerCase().trim(); if (lowerChoice === 'q') return { type: 'quit' }; if (lowerChoice === 'a') return { type: 'all' }; if (lowerChoice === 'm' && !isMultiSelectMode) return { type: 'multi' }; if (lowerChoice === 'c' && hasSelection) return { type: 'compress-selected' }; if (lowerChoice === 'x' && hasSelection) return { type: 'clear' }; if (isMultiSelectMode) { const indices = choice .split(/[,\s]+/) .map(s => parseInt(s.trim()) - 1) .filter(n => !isNaN(n) && n >= 0 && n < fileCount); return indices.length > 0 ? indices : null; } const index = parseInt(choice) - 1; if (index >= 0 && index < fileCount) { return { type: 'file', index }; } return null; }; const toggleSelection = (currentSelection, indices) => { const newSelection = new Set(currentSelection); for (const index of indices) { if (newSelection.has(index)) { newSelection.delete(index); } else { newSelection.add(index); } } return newSelection; }; const createStatsDisplay = (title, stats, fileCount) => { const lines = [ chalk.cyan(`\n${title}`), ...(fileCount !== undefined ? [` Files: ${fileCount}`] : []), ` Original: ${formatFileSize(stats.originalSize)}`, ` Compressed: ${formatFileSize(stats.compressedSize)}`, ` Ratio: ${stats.ratio}%`, ]; return lines.join('\n'); }; const processSingleFile = (filePath, baseDir) => async () => { const originalContent = await readFile(filePath, 'utf-8'); const compressedContent = defaultCompressionPipeline(originalContent); const relativePath = getRelativePath(filePath, baseDir); const output = `/*${relativePath}*/${compressedContent}`; const stats = calculateStats(originalContent.length, output.length); return { content: output, stats }; }; const compressSelectedFiles = (files, selectedIndices, baseDir, preserveStructure = false) => async () => { const selectedFiles = Array.from(selectedIndices) .sort((a, b) => a - b) .map(i => files[i]) .filter(Boolean); if (selectedFiles.length === 0) { throw new Error('No files selected'); } const results = await Promise.all(selectedFiles.map(async (file) => { const content = await readFile(file, 'utf-8'); return { path: file, original: content, compressed: defaultCompressionPipeline(content), }; })); const output = results .map(r => { const relativePath = getRelativePath(r.path, baseDir); return preserveStructure ? `\n/*=== ${relativePath} ===*/\n${r.compressed}\n` : `/*${relativePath}*/${r.compressed}`; }) .join(preserveStructure ? '\n' : ''); const totalOriginal = results.reduce((sum, r) => sum + r.original.length, 0); const stats = calculateStats(totalOriginal, output.length); return { output, stats, fileCount: selectedFiles.length }; }; const compressAllFiles = (options) => async () => { const result = await compressTypeScriptFiles(options)(); return { output: result.output, stats: result.totalStats, fileCount: result.files.length, }; }; const createSuccessMessage = (action) => chalk.green(`\nāœ… ${action}`); const createErrorMessage = (error) => chalk.red('\nāŒ Error: ') + (error instanceof Error ? error.message : String(error)); const promptContinue = (rl) => async () => { await rl.question(chalk.gray('\nPress Enter to continue...')); }; const menuLoop = (files, options, rl, selectedIndices = new Set(), isMultiSelectMode = false) => async () => { console.clear(); console.log(createMenuDisplay(files, options.targetDir, selectedIndices, isMultiSelectMode)); const prompt = isMultiSelectMode ? chalk.yellow('Select files (comma-separated) or m to exit: ') : chalk.green('Select option: '); const choice = await rl.question(prompt); const parsed = parseChoice(choice, files.length, isMultiSelectMode, selectedIndices.size > 0); if (!parsed) { console.log(chalk.red('\nāŒ Invalid selection')); await promptContinue(rl)(); return menuLoop(files, options, rl, selectedIndices, isMultiSelectMode)(); } if (Array.isArray(parsed)) { const newSelection = toggleSelection(selectedIndices, parsed); console.log(chalk.green(`\nāœ“ Selection updated: ${newSelection.size} files selected`)); await promptContinue(rl)(); return menuLoop(files, options, rl, newSelection, isMultiSelectMode)(); } const menuChoice = parsed; switch (menuChoice.type) { case 'quit': console.log(chalk.gray('\nGoodbye!')); return; case 'multi': return menuLoop(files, options, rl, selectedIndices, true)(); case 'clear': console.log(chalk.gray('\nāœ“ Selection cleared')); await promptContinue(rl)(); return menuLoop(files, options, rl, new Set(), isMultiSelectMode)(); case 'compress-selected': { console.log(chalk.cyan(`\nCompressing ${selectedIndices.size} selected files...`)); try { const result = await compressSelectedFiles(files, selectedIndices, options.targetDir, options.preserveStructure)(); await copyToClipboard(result.output)(); console.log(createSuccessMessage('Selected files compressed and copied to clipboard')); console.log(createStatsDisplay('šŸ“Š Statistics:', result.stats, result.fileCount)); } catch (error) { console.error(createErrorMessage(error)); } await promptContinue(rl)(); return menuLoop(files, options, rl, new Set(), false)(); } case 'all': { console.log(chalk.cyan('\nCompressing all files...')); try { const result = await compressAllFiles(options)(); await copyToClipboard(result.output)(); console.log(createSuccessMessage('All files compressed and copied to clipboard')); console.log(createStatsDisplay('šŸ“Š Statistics:', result.stats, result.fileCount)); } catch (error) { console.error(createErrorMessage(error)); } await promptContinue(rl)(); return menuLoop(files, options, rl, selectedIndices, isMultiSelectMode)(); } case 'file': { if (isMultiSelectMode) { return menuLoop(files, options, rl, selectedIndices, isMultiSelectMode)(); } const filePath = files[menuChoice.index]; const relativePath = getRelativePath(filePath, options.targetDir); console.log(chalk.cyan(`\nCompressing ${relativePath}...`)); try { const result = await processSingleFile(filePath, options.targetDir)(); await copyToClipboard(result.content)(); console.log(createSuccessMessage(`${relativePath} compressed and copied to clipboard`)); console.log(createStatsDisplay('šŸ“Š Statistics:', result.stats)); } catch (error) { console.error(createErrorMessage(error)); } await promptContinue(rl)(); return menuLoop(files, options, rl, selectedIndices, isMultiSelectMode)(); } } }; export const interactiveMode = (options) => async () => { const rl = createInterface({ input, output }); try { const files = await getTypeScriptFiles(options.targetDir, options.includePatterns, options.excludePatterns)(); if (files.length === 0) { console.log(chalk.yellow('No TypeScript files found.')); return; } await menuLoop(files, options, rl)(); } finally { rl.close(); } }; //# sourceMappingURL=interactive.js.map