204 lines
6.1 KiB
TypeScript
204 lines
6.1 KiB
TypeScript
import * as plugins from './mod.plugins.js';
|
|
import { FormatContext } from './classes.formatcontext.js';
|
|
import { BaseFormatter } from './classes.baseformatter.js';
|
|
import type {
|
|
IFormatPlan,
|
|
IPlannedChange,
|
|
IFormatWarning,
|
|
} from './interfaces.format.js';
|
|
import { getModuleIcon } from './interfaces.format.js';
|
|
import { logger } from '../gitzone.logging.js';
|
|
import { DiffReporter } from './classes.diffreporter.js';
|
|
|
|
export class FormatPlanner {
|
|
private plannedChanges: Map<string, IPlannedChange[]> = new Map();
|
|
private diffReporter = new DiffReporter();
|
|
|
|
async planFormat(modules: BaseFormatter[]): Promise<IFormatPlan> {
|
|
const plan: IFormatPlan = {
|
|
summary: {
|
|
totalFiles: 0,
|
|
filesAdded: 0,
|
|
filesModified: 0,
|
|
filesRemoved: 0,
|
|
},
|
|
changes: [],
|
|
warnings: [],
|
|
};
|
|
|
|
for (const module of modules) {
|
|
try {
|
|
const changes = await module.analyze();
|
|
this.plannedChanges.set(module.name, changes);
|
|
|
|
for (const change of changes) {
|
|
plan.changes.push(change);
|
|
|
|
switch (change.type) {
|
|
case 'create':
|
|
plan.summary.filesAdded++;
|
|
break;
|
|
case 'modify':
|
|
plan.summary.filesModified++;
|
|
break;
|
|
case 'delete':
|
|
plan.summary.filesRemoved++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
const warnings = await module.validate();
|
|
plan.warnings.push(...warnings);
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
plan.warnings.push({
|
|
level: 'error',
|
|
message: `Failed to analyze module ${module.name}: ${errorMessage}`,
|
|
module: module.name,
|
|
});
|
|
}
|
|
}
|
|
|
|
plan.warnings.push(...this.detectConflictingChanges(plan.changes));
|
|
|
|
plan.summary.totalFiles =
|
|
plan.summary.filesAdded +
|
|
plan.summary.filesModified +
|
|
plan.summary.filesRemoved;
|
|
|
|
return plan;
|
|
}
|
|
|
|
async executePlan(
|
|
plan: IFormatPlan,
|
|
modules: BaseFormatter[],
|
|
context: FormatContext,
|
|
): Promise<void> {
|
|
const startTime = Date.now();
|
|
const changesByModule = this.groupChangesByModule(plan.changes);
|
|
|
|
for (const module of modules) {
|
|
const changes = changesByModule.get(module.name) || [];
|
|
|
|
if (changes.length > 0 || module.runsWithoutChanges) {
|
|
logger.log('info', `Executing ${module.name} formatter...`);
|
|
await module.execute(changes);
|
|
}
|
|
}
|
|
|
|
const duration = Date.now() - startTime;
|
|
logger.log('info', `Format operations completed in ${duration}ms`);
|
|
}
|
|
|
|
async displayPlan(
|
|
plan: IFormatPlan,
|
|
detailed: boolean = false,
|
|
): Promise<void> {
|
|
console.log('\nFormat Plan:');
|
|
console.log('━'.repeat(50));
|
|
console.log(`Summary: ${plan.summary.totalFiles} files will be changed`);
|
|
console.log(` • ${plan.summary.filesAdded} new files`);
|
|
console.log(` • ${plan.summary.filesModified} modified files`);
|
|
console.log(` • ${plan.summary.filesRemoved} deleted files`);
|
|
console.log('');
|
|
console.log('Changes by module:');
|
|
|
|
const changesByModule = new Map<string, IPlannedChange[]>();
|
|
for (const change of plan.changes) {
|
|
const moduleChanges = changesByModule.get(change.module) || [];
|
|
moduleChanges.push(change);
|
|
changesByModule.set(change.module, moduleChanges);
|
|
}
|
|
|
|
for (const [module, changes] of changesByModule) {
|
|
console.log(
|
|
`\n${getModuleIcon(module)} ${module} (${changes.length} ${changes.length === 1 ? 'file' : 'files'})`,
|
|
);
|
|
|
|
for (const change of changes) {
|
|
const icon = this.getChangeIcon(change.type);
|
|
console.log(` ${icon} ${change.path} - ${change.description}`);
|
|
|
|
if (detailed && change.type === 'modify') {
|
|
const diff = await this.diffReporter.generateDiffForChange(change);
|
|
if (diff) {
|
|
this.diffReporter.displayDiff(change.path, diff);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (plan.warnings.length > 0) {
|
|
console.log('\nWarnings:');
|
|
for (const warning of plan.warnings) {
|
|
const icon = warning.level === 'error' ? '❌' : '⚠️';
|
|
console.log(` ${icon} ${warning.message}`);
|
|
}
|
|
}
|
|
|
|
console.log('\n' + '━'.repeat(50));
|
|
}
|
|
|
|
private getChangeIcon(type: 'create' | 'modify' | 'delete'): string {
|
|
switch (type) {
|
|
case 'create':
|
|
return '✅';
|
|
case 'modify':
|
|
return '✏️';
|
|
case 'delete':
|
|
return '❌';
|
|
}
|
|
}
|
|
|
|
private groupChangesByModule(
|
|
changes: IPlannedChange[],
|
|
): Map<string, IPlannedChange[]> {
|
|
const changesByModule = new Map<string, IPlannedChange[]>();
|
|
for (const change of changes) {
|
|
const moduleChanges = changesByModule.get(change.module) || [];
|
|
moduleChanges.push(change);
|
|
changesByModule.set(change.module, moduleChanges);
|
|
}
|
|
return changesByModule;
|
|
}
|
|
|
|
private detectConflictingChanges(
|
|
changes: IPlannedChange[],
|
|
): IFormatWarning[] {
|
|
const warnings: IFormatWarning[] = [];
|
|
const changesByPath = new Map<string, IPlannedChange[]>();
|
|
|
|
for (const change of changes) {
|
|
if (!change.path || change.path === '<various files>') {
|
|
continue;
|
|
}
|
|
|
|
const pathChanges = changesByPath.get(change.path) || [];
|
|
pathChanges.push(change);
|
|
changesByPath.set(change.path, pathChanges);
|
|
}
|
|
|
|
for (const [path, pathChanges] of changesByPath) {
|
|
const modules = [...new Set(pathChanges.map((change) => change.module))];
|
|
if (modules.length < 2) {
|
|
continue;
|
|
}
|
|
|
|
const hasDelete = pathChanges.some((change) => change.type === 'delete');
|
|
const plannedContents = pathChanges
|
|
.map((change) => change.content)
|
|
.filter((content): content is string => content !== undefined);
|
|
const uniqueContents = new Set(plannedContents);
|
|
const level = hasDelete || uniqueContents.size > 1 ? 'warning' : 'info';
|
|
|
|
warnings.push({
|
|
level,
|
|
module: 'planner',
|
|
message: `Multiple formatters plan changes for ${path}: ${modules.join(', ')}. They will run in formatter order.`,
|
|
});
|
|
}
|
|
|
|
return warnings;
|
|
}
|
|
}
|