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'; import { FormatStats } from './classes.formatstats.js'; export abstract class BaseFormatter { protected context: FormatContext; protected project: Project; protected stats: FormatStats; 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(); } 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 { if (!filepath || filepath.trim() === '') { throw new Error(`Invalid empty filepath in modifyFile`); } 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 { let normalizedPath = filepath; if (!plugins.path.parse(filepath).dir) { normalizedPath = './' + filepath; } // Ensure parent directory exists const dir = plugins.path.dirname(normalizedPath); if (dir && dir !== '.') { await plugins.smartfs.directory(dir).recursive().create(); } await plugins.smartfs.file(normalizedPath).encoding('utf8').write(content); } protected async deleteFile(filepath: string): Promise { await plugins.smartfs.file(filepath).delete(); } /** * Check for diffs without applying changes */ async check(): Promise { const changes = await this.analyze(); const diffs: ICheckResult['diffs'] = []; for (const change of changes) { if (change.path === '') { continue; } if (change.type === 'modify' || change.type === 'create') { let currentContent: string | undefined; try { currentContent = await plugins.smartfs.file(change.path).encoding('utf8').read() as string; } catch { currentContent = undefined; } const newContent = change.content; if (currentContent !== newContent && newContent !== undefined) { diffs.push({ path: change.path, type: change.type, before: currentContent, after: newContent, }); } } else if (change.type === 'delete') { 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, }; } 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)'); 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)'); } } displayAllDiffs(result: ICheckResult): void { if (!result.hasDiff) { console.log(' No changes detected'); return; } for (const diff of result.diffs) { this.displayDiff(diff); } } }