ts-comment-remover
Version:
TypeScript file compression tool that removes comments and unnecessary whitespace using AST
240 lines ⢠10.4 kB
JavaScript
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