224 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			224 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
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<IPlannedChange[]> {
 | 
						|
    const changes: IPlannedChange[] = [];
 | 
						|
 | 
						|
    // Define directories to format (TypeScript directories by default)
 | 
						|
    const includeDirs = ['ts', 'ts_*', 'test', 'tests'];
 | 
						|
 | 
						|
    // File extensions to format
 | 
						|
    const extensions = '{ts,tsx,js,jsx,json,md,css,scss,html,xml,yaml,yml}';
 | 
						|
 | 
						|
    // Also format root-level config files
 | 
						|
    const rootConfigFiles = [
 | 
						|
      'package.json',
 | 
						|
      'tsconfig.json',
 | 
						|
      'npmextra.json',
 | 
						|
      '.prettierrc',
 | 
						|
      '.prettierrc.json',
 | 
						|
      '.prettierrc.js',
 | 
						|
      'readme.md',
 | 
						|
      'README.md',
 | 
						|
      'changelog.md',
 | 
						|
      'CHANGELOG.md',
 | 
						|
      // Skip files without extensions as prettier can't infer parser
 | 
						|
      // 'license',
 | 
						|
      // 'LICENSE',
 | 
						|
      '*.md',
 | 
						|
    ];
 | 
						|
 | 
						|
    // Collect all files to format
 | 
						|
    const allFiles: string[] = [];
 | 
						|
 | 
						|
    // Add files from TypeScript directories
 | 
						|
    for (const dir of includeDirs) {
 | 
						|
      const globPattern = `${dir}/**/*.${extensions}`;
 | 
						|
      const dirFiles = await plugins.smartfile.fs.listFileTree(
 | 
						|
        '.',
 | 
						|
        globPattern,
 | 
						|
      );
 | 
						|
      allFiles.push(...dirFiles);
 | 
						|
    }
 | 
						|
 | 
						|
    // Add root config files
 | 
						|
    for (const pattern of rootConfigFiles) {
 | 
						|
      const rootFiles = await plugins.smartfile.fs.listFileTree('.', pattern);
 | 
						|
      // Only include files at root level (no slashes in path)
 | 
						|
      const rootLevelFiles = rootFiles.filter((f) => !f.includes('/'));
 | 
						|
      allFiles.push(...rootLevelFiles);
 | 
						|
    }
 | 
						|
 | 
						|
    // Remove duplicates
 | 
						|
    const uniqueFiles = [...new Set(allFiles)];
 | 
						|
 | 
						|
    // Get all files that match the pattern
 | 
						|
    const files = uniqueFiles;
 | 
						|
 | 
						|
    // Ensure we only process actual files (not directories)
 | 
						|
    const validFiles: string[] = [];
 | 
						|
    for (const file of files) {
 | 
						|
      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<void> {
 | 
						|
    const startTime = this.stats.moduleStartTime(this.name);
 | 
						|
    this.stats.startModule(this.name);
 | 
						|
 | 
						|
    try {
 | 
						|
      await this.preExecute();
 | 
						|
 | 
						|
      logVerbose(`Processing ${changes.length} files sequentially`);
 | 
						|
 | 
						|
      // Process files sequentially to avoid prettier cache/state issues
 | 
						|
      for (let i = 0; i < changes.length; i++) {
 | 
						|
        const change = changes[i];
 | 
						|
        logVerbose(
 | 
						|
          `Processing file ${i + 1}/${changes.length}: ${change.path}`,
 | 
						|
        );
 | 
						|
 | 
						|
        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 this.postExecute();
 | 
						|
    } catch (error) {
 | 
						|
      // Rollback removed - no longer tracking operations
 | 
						|
      throw error;
 | 
						|
    } finally {
 | 
						|
      this.stats.endModule(this.name, startTime);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  async applyChange(change: IPlannedChange): Promise<void> {
 | 
						|
    if (change.type !== 'modify') return;
 | 
						|
 | 
						|
    try {
 | 
						|
      // Validate the path before processing
 | 
						|
      if (!change.path || change.path.trim() === '') {
 | 
						|
        logger.log(
 | 
						|
          'error',
 | 
						|
          `Invalid empty path in change: ${JSON.stringify(change)}`,
 | 
						|
        );
 | 
						|
        throw new Error('Invalid empty path');
 | 
						|
      }
 | 
						|
 | 
						|
      // Read current content
 | 
						|
      const content = plugins.smartfile.fs.toStringSync(change.path);
 | 
						|
 | 
						|
      // Format with prettier
 | 
						|
      const prettier = await import('prettier');
 | 
						|
 | 
						|
      // Skip files that prettier can't parse without explicit parser
 | 
						|
      const fileExt = plugins.path.extname(change.path).toLowerCase();
 | 
						|
      if (!fileExt || fileExt === '') {
 | 
						|
        // Files without extensions need explicit parser
 | 
						|
        logVerbose(
 | 
						|
          `Skipping ${change.path} - no file extension for parser inference`,
 | 
						|
        );
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      try {
 | 
						|
        const formatted = await prettier.format(content, {
 | 
						|
          filepath: change.path,
 | 
						|
          ...(await this.getPrettierConfig()),
 | 
						|
        });
 | 
						|
 | 
						|
        // Only write if content actually changed
 | 
						|
        if (formatted !== content) {
 | 
						|
          // Debug: log the path being written
 | 
						|
          logVerbose(`Writing formatted content to: ${change.path}`);
 | 
						|
          await this.modifyFile(change.path, formatted);
 | 
						|
          logVerbose(`Formatted ${change.path}`);
 | 
						|
        } else {
 | 
						|
          logVerbose(`No formatting changes for ${change.path}`);
 | 
						|
        }
 | 
						|
      } catch (prettierError) {
 | 
						|
        // Check if it's a parser error
 | 
						|
        if (
 | 
						|
          prettierError.message &&
 | 
						|
          prettierError.message.includes('No parser could be inferred')
 | 
						|
        ) {
 | 
						|
          logVerbose(`Skipping ${change.path} - ${prettierError.message}`);
 | 
						|
          return; // Skip this file silently
 | 
						|
        }
 | 
						|
        throw prettierError;
 | 
						|
      }
 | 
						|
    } catch (error) {
 | 
						|
      // Log the full error stack for debugging mkdir issues
 | 
						|
      if (error.message && error.message.includes('mkdir')) {
 | 
						|
        logger.log(
 | 
						|
          'error',
 | 
						|
          `Failed to format ${change.path}: ${error.message}`,
 | 
						|
        );
 | 
						|
        logger.log('error', `Error stack: ${error.stack}`);
 | 
						|
      } else {
 | 
						|
        logger.log(
 | 
						|
          'error',
 | 
						|
          `Failed to format ${change.path}: ${error.message}`,
 | 
						|
        );
 | 
						|
      }
 | 
						|
      throw error;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  private async getPrettierConfig(): Promise<any> {
 | 
						|
    // 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',
 | 
						|
    });
 | 
						|
  }
 | 
						|
}
 |