UNPKG

@git.zone/cli

Version:

A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.

224 lines (195 loc) 6.83 kB
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', }); } }