UNPKG

purify-objects

Version:

A powerful TypeScript library for cleaning objects by removing empty values, with support for YAML and CSV formats

205 lines (175 loc) 7.98 kB
#!/usr/bin/env node import fs from 'fs'; import path from 'path'; import chalk from 'chalk'; import { cleanObject } from './index'; import { parseYAML, parseCSV, stringifyYAML, stringifyCSV } from './parsers'; import { FileFormat, ParserOptions, AnyObject } from './types'; interface CliOptions { inputFile: string; outputFile: string | null; compareMode: boolean; safeMode: boolean; format: FileFormat; convertTo: FileFormat | null; delimiter: string; headers: boolean; removeZeroValues: boolean; noClean: boolean; } const VALID_FORMATS = ['json', 'yaml', 'csv'] as const; type FormatExtensions = Record<FileFormat, readonly string[]>; const FORMAT_EXTENSIONS: FormatExtensions = { json: ['json'], yaml: ['yaml', 'yml'], csv: ['csv'] } as const; const detectFileFormat = (filename: string): FileFormat => { const ext = path.extname(filename).toLowerCase().slice(1); for (const [format, extensions] of Object.entries(FORMAT_EXTENSIONS)) { if ((extensions as readonly string[]).includes(ext)) { return format as FileFormat; } } return 'json'; }; const getDefaultOutputFilename = (inputFile: string, format: FileFormat): string => { const parsedPath = path.parse(inputFile); const extension = format === 'yaml' ? '.yaml' : `.${format}`; return path.join(parsedPath.dir, `${parsedPath.name}${extension}`); }; const extractCliOptions = (args: string[]): CliOptions => { const inputFile = args[0]; const formatIndex = args.indexOf('--format'); const formatValue = formatIndex !== -1 ? args[formatIndex + 1] : detectFileFormat(inputFile); const convertToIndex = args.indexOf('--convert-to'); const convertToValue = convertToIndex !== -1 ? args[convertToIndex + 1] : null; if (formatValue && !VALID_FORMATS.includes(formatValue as FileFormat)) { console.error(chalk.red(`Error: Invalid format "${formatValue}". Valid formats are: ${VALID_FORMATS.join(', ')}`)); process.exit(1); } if (convertToValue && !VALID_FORMATS.includes(convertToValue as FileFormat)) { console.error(chalk.red(`Error: Invalid conversion format "${convertToValue}". Valid formats are: ${VALID_FORMATS.join(', ')}`)); process.exit(1); } const outputIndex = args.indexOf('--output'); let outputFile = outputIndex !== -1 ? args[outputIndex + 1] : null; if (convertToValue && !outputFile) { outputFile = getDefaultOutputFilename(inputFile, convertToValue as FileFormat); } return { inputFile, outputFile, compareMode: args.includes('--compare'), safeMode: args.includes('--safe'), format: formatValue as FileFormat, convertTo: convertToValue as FileFormat | null, delimiter: args.indexOf('--delimiter') !== -1 ? args[args.indexOf('--delimiter') + 1] : ',', headers: !args.includes('--no-headers'), removeZeroValues: args.includes('--remove-zero-values'), noClean: args.includes('--noclean') }; }; const parseContent = (content: string, format: FileFormat, options: ParserOptions): AnyObject | AnyObject[] => { try { switch (format) { case 'yaml': return parseYAML(content); case 'csv': return parseCSV(content, { delimiter: options.delimiter || ',', headers: options.headers !== false }); default: return JSON.parse(content); } } catch (error: any) { console.error(chalk.red(`Error parsing ${format.toUpperCase()} content: ${error.message}`)); process.exit(1); } }; const stringifyContent = (data: AnyObject | AnyObject[], format: FileFormat, options: ParserOptions): string => { try { switch (format) { case 'yaml': return stringifyYAML(data as AnyObject); case 'csv': return stringifyCSV(Array.isArray(data) ? data : [data], { delimiter: options.delimiter || ',', headers: options.headers !== false }); default: return JSON.stringify(data, null, 2); } } catch (error: any) { console.error(chalk.red(`Error converting to ${format.toUpperCase()}: ${error.message}`)); process.exit(1); } }; const generateFieldMap = (original: any, cleaned: any, prefix = ''): string[] => ( Object.entries(original).reduce((acc: string[], [key, value]) => { const currentPath = prefix ? `${prefix}.${key}` : key; if (!(key in cleaned)) { return [...acc, currentPath]; } if (typeof value === 'object' && value !== null && typeof cleaned[key] === 'object' && cleaned[key] !== null) { return [...acc, ...generateFieldMap(value, cleaned[key], currentPath)]; } return acc; }, []) ); const executeCliOperation = (options: CliOptions): void => { const { inputFile, outputFile, compareMode, safeMode, format, convertTo, delimiter, headers, removeZeroValues, noClean } = options; if (!inputFile) { console.error(chalk.red('Please provide an input file')); console.log(chalk.yellow( 'Usage: npx purify-objects input.file [--output cleaned.file] [--compare] [--safe] ' + `[--format ${VALID_FORMATS.join('|')}] [--convert-to ${VALID_FORMATS.join('|')}] ` + '[--delimiter ","] [--no-headers] [--remove-zero-values] [--noclean]' )); process.exit(1); } try { const inputPath = path.resolve(process.cwd(), inputFile); if (!fs.existsSync(inputPath)) { console.error(chalk.red(`Error: File not found: ${inputFile}`)); process.exit(1); } const content = fs.readFileSync(inputPath, 'utf8'); const sourceData = parseContent(content, format, { delimiter, headers }); const customCleaner = removeZeroValues ? (key: string, value: any) => value === 0 : undefined; const processedData = noClean ? sourceData : Array.isArray(sourceData) ? sourceData.map(item => cleanObject(item, customCleaner, [], { safe: safeMode })) : cleanObject(sourceData, customCleaner, [], { safe: safeMode }); if (compareMode) { safeMode && console.log(chalk.blue('\nSafe Mode: Original file will not be modified')); console.log(chalk.yellow('\nOriginal data:'), stringifyContent(sourceData, format, { delimiter, headers })); console.log(chalk.green('\nCleaned data (preview):'), stringifyContent(processedData, format, { delimiter, headers })); if (!Array.isArray(sourceData)) { const modifications = generateFieldMap(sourceData, processedData); if (modifications.length) { console.log(chalk.red('\nFields to be removed:')); modifications.forEach(f => console.log(chalk.red(`- ${f}`))); } } safeMode && console.log(chalk.blue('\nNo changes were made to the original file (Safe Mode)')); process.exit(0); } if (outputFile) { const outputPath = path.resolve(process.cwd(), outputFile); const outputFormat = convertTo || format; fs.writeFileSync(outputPath, stringifyContent(processedData, outputFormat, { delimiter, headers })); if (convertTo) { console.log(chalk.green(`\nFile successfully ${noClean ? 'converted' : 'converted and cleaned'}:`)); console.log(chalk.blue(`Input: ${inputFile} (${format.toUpperCase()})`)); console.log(chalk.blue(`Output: ${outputFile} (${convertTo.toUpperCase()})`)); } else { safeMode && console.log(chalk.blue('Safe Mode: Created new file without modifying original')); console.log(chalk.green(`${noClean ? 'Data' : 'Cleaned data'} saved to ${outputFile}`)); } return; } console.log(stringifyContent(processedData, format, { delimiter, headers })); } catch (error: any) { console.error(chalk.red('Error:'), error?.message || 'Unknown error occurred'); process.exit(1); } }; executeCliOperation(extractCliOptions(process.argv.slice(2)));