import * as plugins from './mod.plugins.js'; import { FormatContext } from './classes.formatcontext.js'; import type { IPlannedChange, ICheckResult } from './interfaces.format.js'; import { Project } from '../classes.project.js'; export abstract class BaseFormatter { protected context: FormatContext; protected project: Project; protected stats: any; // Will be FormatStats from context constructor(context: FormatContext, project: Project) { this.context = context; this.project = project; this.stats = context.getFormatStats(); } abstract get name(): string; abstract analyze(): Promise; abstract applyChange(change: IPlannedChange): Promise; async execute(changes: IPlannedChange[]): Promise { const startTime = this.stats.moduleStartTime(this.name); this.stats.startModule(this.name); try { await this.preExecute(); for (const change of changes) { try { await this.applyChange(change); this.stats.recordFileOperation(this.name, change.type, true); } catch (error) { this.stats.recordFileOperation(this.name, change.type, false); throw error; } } await this.postExecute(); } catch (error) { // Don't rollback here - let the FormatPlanner handle it throw error; } finally { this.stats.endModule(this.name, startTime); } } protected async preExecute(): Promise { // Override in subclasses if needed } protected async postExecute(): Promise { // Override in subclasses if needed } protected async modifyFile(filepath: string, content: string): Promise { // Validate filepath before writing if (!filepath || filepath.trim() === '') { throw new Error(`Invalid empty filepath in modifyFile`); } // Ensure we have a proper path with directory component // If the path has no directory component (e.g., "package.json"), prepend "./" let normalizedPath = filepath; if (!plugins.path.parse(filepath).dir) { normalizedPath = './' + filepath; } await plugins.smartfs.file(normalizedPath).encoding('utf8').write(content); } protected async createFile(filepath: string, content: string): Promise { await plugins.smartfs.file(filepath).encoding('utf8').write(content); } protected async deleteFile(filepath: string): Promise { await plugins.smartfs.file(filepath).delete(); } protected async shouldProcessFile(filepath: string): Promise { return true; } /** * Check for diffs without applying changes * Returns information about what would change */ async check(): Promise { const changes = await this.analyze(); const diffs: ICheckResult['diffs'] = []; for (const change of changes) { // Skip generic changes that don't have actual content if (change.path === '') { continue; } if (change.type === 'modify' || change.type === 'create') { // Read current content if file exists let currentContent: string | undefined; try { currentContent = await plugins.smartfs.file(change.path).encoding('utf8').read() as string; } catch { // File doesn't exist yet currentContent = undefined; } const newContent = change.content; // Check if there's an actual diff if (currentContent !== newContent && newContent !== undefined) { diffs.push({ path: change.path, type: change.type, before: currentContent, after: newContent, }); } } else if (change.type === 'delete') { // Check if file exists before marking for deletion try { const currentContent = await plugins.smartfs.file(change.path).encoding('utf8').read() as string; diffs.push({ path: change.path, type: 'delete', before: currentContent, after: undefined, }); } catch { // File doesn't exist, nothing to delete } } } return { hasDiff: diffs.length > 0, diffs, }; } /** * Display a single diff using smartdiff */ displayDiff(diff: ICheckResult['diffs'][0]): void { console.log(`\n--- ${diff.path}`); if (diff.before && diff.after) { console.log(plugins.smartdiff.formatUnifiedDiffForConsole(diff.before, diff.after, { originalFileName: diff.path, revisedFileName: diff.path, context: 3, })); } else if (diff.after && !diff.before) { console.log(' (new file)'); // Show first few lines of new content const lines = diff.after.split('\n').slice(0, 10); lines.forEach(line => console.log(` + ${line}`)); if (diff.after.split('\n').length > 10) { console.log(' ... (truncated)'); } } else if (diff.before && !diff.after) { console.log(' (file will be deleted)'); } } /** * Display all diffs from a check result */ displayAllDiffs(result: ICheckResult): void { if (!result.hasDiff) { console.log(' No changes detected'); return; } for (const diff of result.diffs) { this.displayDiff(diff); } } }