173 lines
5.1 KiB
TypeScript
173 lines
5.1 KiB
TypeScript
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<IPlannedChange[]>;
|
|
abstract applyChange(change: IPlannedChange): Promise<void>;
|
|
|
|
async execute(changes: IPlannedChange[]): Promise<void> {
|
|
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<void> {
|
|
// Override in subclasses if needed
|
|
}
|
|
|
|
protected async postExecute(): Promise<void> {
|
|
// Override in subclasses if needed
|
|
}
|
|
|
|
protected async modifyFile(filepath: string, content: string): Promise<void> {
|
|
// 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<void> {
|
|
await plugins.smartfs.file(filepath).encoding('utf8').write(content);
|
|
}
|
|
|
|
protected async deleteFile(filepath: string): Promise<void> {
|
|
await plugins.smartfs.file(filepath).delete();
|
|
}
|
|
|
|
protected async shouldProcessFile(filepath: string): Promise<boolean> {
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Check for diffs without applying changes
|
|
* Returns information about what would change
|
|
*/
|
|
async check(): Promise<ICheckResult> {
|
|
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 === '<various files>') {
|
|
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.formatLineDiffForConsole(diff.before, diff.after));
|
|
} 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);
|
|
}
|
|
}
|
|
}
|