import { BaseFormatter } from '../classes.baseformatter.js'; import type { IPlannedChange } from '../interfaces.format.js'; import * as plugins from '../mod.plugins.js'; import { logger, logVerbose } from '../../gitzone.logging.js'; export class PrettierFormatter extends BaseFormatter { get name(): string { return 'prettier'; } async analyze(): Promise { const changes: IPlannedChange[] = []; const globPattern = '**/*.{ts,tsx,js,jsx,json,md,css,scss,html,xml,yaml,yml}'; // Directories to exclude from formatting const excludePatterns = [ 'node_modules/**', '.git/**', 'dist/**', 'dist_*/**', '.nogit/**', 'coverage/**', '.nyc_output/**', 'vendor/**', 'bower_components/**', 'jspm_packages/**', '*.min.js', '*.min.css' ]; // Get all files that match the pattern const files = await plugins.smartfile.fs.listFileTree('.', globPattern); // Filter out excluded directories and ensure we only process files const validFiles: string[] = []; for (const file of files) { // Check if file matches any exclude pattern let shouldExclude = false; for (const pattern of excludePatterns) { // Simple pattern matching for common cases const patternBase = pattern.replace('/**', '').replace('**/', '').replace('*', ''); if (pattern.endsWith('/**') && file.startsWith(patternBase + '/')) { shouldExclude = true; logVerbose(`Excluding ${file} - matches exclude pattern ${pattern}`); break; } else if (pattern.startsWith('*.') && file.endsWith(patternBase)) { shouldExclude = true; logVerbose(`Excluding ${file} - matches exclude pattern ${pattern}`); break; } } if (shouldExclude) { continue; } try { const stats = await plugins.smartfile.fs.stat(file); if (!stats.isDirectory()) { validFiles.push(file); } } catch (error) { // Skip files that can't be accessed logVerbose(`Skipping ${file} - cannot access: ${error.message}`); } } // Check which files need formatting for (const file of validFiles) { // Skip files that haven't changed if (!await this.shouldProcessFile(file)) { logVerbose(`Skipping ${file} - no changes detected`); continue; } changes.push({ type: 'modify', path: file, module: this.name, description: 'Format with Prettier' }); } logger.log('info', `Found ${changes.length} files to format with Prettier`); return changes; } async execute(changes: IPlannedChange[]): Promise { const startTime = this.stats.moduleStartTime(this.name); this.stats.startModule(this.name); try { await this.preExecute(); // Batch process files const batchSize = 10; // Process 10 files at a time const batches: IPlannedChange[][] = []; for (let i = 0; i < changes.length; i += batchSize) { batches.push(changes.slice(i, i + batchSize)); } logVerbose(`Processing ${changes.length} files in ${batches.length} batches`); for (let i = 0; i < batches.length; i++) { const batch = batches[i]; logVerbose(`Processing batch ${i + 1}/${batches.length} (${batch.length} files)`); // Process batch in parallel const promises = batch.map(async (change) => { try { await this.applyChange(change); this.stats.recordFileOperation(this.name, change.type, true); } catch (error) { this.stats.recordFileOperation(this.name, change.type, false); logger.log('error', `Failed to format ${change.path}: ${error.message}`); // Don't throw - continue with other files } }); await Promise.all(promises); } await this.postExecute(); } catch (error) { await this.context.rollbackOperation(); throw error; } finally { this.stats.endModule(this.name, startTime); } } async applyChange(change: IPlannedChange): Promise { if (change.type !== 'modify') return; try { // Read current content const content = plugins.smartfile.fs.toStringSync(change.path); // Format with prettier const prettier = await import('prettier'); const formatted = await prettier.format(content, { filepath: change.path, ...(await this.getPrettierConfig()) }); // Only write if content actually changed if (formatted !== content) { await this.modifyFile(change.path, formatted); logVerbose(`Formatted ${change.path}`); } else { // Still update cache even if content didn't change await this.cache.updateFileCache(change.path); logVerbose(`No formatting changes for ${change.path}`); } } catch (error) { logger.log('error', `Failed to format ${change.path}: ${error.message}`); throw error; } } private async getPrettierConfig(): Promise { // Try to load prettier config from the project const prettierConfig = new plugins.npmextra.Npmextra(); return prettierConfig.dataFor('prettier', { // Default prettier config singleQuote: true, trailingComma: 'all', printWidth: 80, tabWidth: 2, semi: true, arrowParens: 'always' }); } }