feat(format): Enhance format module with rollback, diff reporting, and improved parallel execution

This commit is contained in:
2025-05-19 13:34:23 +00:00
parent 7b2ae01112
commit 949f273317
32 changed files with 2265 additions and 40 deletions

View File

@@ -0,0 +1,39 @@
import { BaseFormatter } from '../classes.baseformatter.js';
import type { IPlannedChange } from '../interfaces.format.js';
import * as plugins from '../mod.plugins.js';
import * as cleanupFormatter from '../format.cleanup.js';
export class CleanupFormatter extends BaseFormatter {
get name(): string {
return 'cleanup';
}
async analyze(): Promise<IPlannedChange[]> {
const changes: IPlannedChange[] = [];
// List of files to remove
const filesToRemove = ['yarn.lock', 'package-lock.json', 'tslint.json', 'defaults.yml'];
for (const file of filesToRemove) {
const exists = await plugins.smartfile.fs.fileExists(file);
if (exists) {
changes.push({
type: 'delete',
path: file,
module: this.name,
description: `Remove obsolete file`
});
}
}
return changes;
}
async applyChange(change: IPlannedChange): Promise<void> {
switch (change.type) {
case 'delete':
await this.deleteFile(change.path);
break;
}
}
}

View File

@@ -0,0 +1,8 @@
import { LegacyFormatter } from './legacy.formatter.js';
import * as formatCopy from '../format.copy.js';
export class CopyFormatter extends LegacyFormatter {
constructor(context: any, project: any) {
super(context, project, 'copy', formatCopy);
}
}

View File

@@ -0,0 +1,8 @@
import { LegacyFormatter } from './legacy.formatter.js';
import * as formatGitignore from '../format.gitignore.js';
export class GitignoreFormatter extends LegacyFormatter {
constructor(context: any, project: any) {
super(context, project, 'gitignore', formatGitignore);
}
}

View File

@@ -0,0 +1,36 @@
import { BaseFormatter } from '../classes.baseformatter.js';
import type { IPlannedChange } from '../interfaces.format.js';
import { Project } from '../../classes.project.js';
import * as plugins from '../mod.plugins.js';
// This is a wrapper for existing format modules
export class LegacyFormatter extends BaseFormatter {
private moduleName: string;
private formatModule: any;
constructor(context: any, project: Project, moduleName: string, formatModule: any) {
super(context, project);
this.moduleName = moduleName;
this.formatModule = formatModule;
}
get name(): string {
return this.moduleName;
}
async analyze(): Promise<IPlannedChange[]> {
// For legacy modules, we can't easily predict changes
// So we'll return a generic change that indicates the module will run
return [{
type: 'modify',
path: '<various files>',
module: this.name,
description: `Run ${this.name} formatter`
}];
}
async applyChange(change: IPlannedChange): Promise<void> {
// Run the legacy format module
await this.formatModule.run(this.project);
}
}

View File

@@ -0,0 +1,8 @@
import { LegacyFormatter } from './legacy.formatter.js';
import * as formatLicense from '../format.license.js';
export class LicenseFormatter extends LegacyFormatter {
constructor(context: any, project: any) {
super(context, project, 'license', formatLicense);
}
}

View File

@@ -0,0 +1,8 @@
import { LegacyFormatter } from './legacy.formatter.js';
import * as formatNpmextra from '../format.npmextra.js';
export class NpmextraFormatter extends LegacyFormatter {
constructor(context: any, project: any) {
super(context, project, 'npmextra', formatNpmextra);
}
}

View File

@@ -0,0 +1,8 @@
import { LegacyFormatter } from './legacy.formatter.js';
import * as formatPackageJson from '../format.packagejson.js';
export class PackageJsonFormatter extends LegacyFormatter {
constructor(context: any, project: any) {
super(context, project, 'packagejson', formatPackageJson);
}
}

View File

@@ -0,0 +1,125 @@
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[] = [];
const globPattern = '**/*.{ts,tsx,js,jsx,json,md,css,scss,html,xml,yaml,yml}';
// Get all files that match the pattern
const files = await plugins.smartfile.fs.listFileTree('.', globPattern);
// Check which files need formatting
for (const file of files) {
// 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();
// Batch process files
const batchSize = 10; // Process 10 files at a time
const batches: IPlannedChange[][] = [];
for (let i = 0; i < changes.length; i += batchSize) {
batches.push(changes.slice(i, i + batchSize));
}
logVerbose(`Processing ${changes.length} files in ${batches.length} batches`);
for (let i = 0; i < batches.length; i++) {
const batch = batches[i];
logVerbose(`Processing batch ${i + 1}/${batches.length} (${batch.length} files)`);
// Process batch in parallel
const promises = batch.map(async (change) => {
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 Promise.all(promises);
}
await this.postExecute();
} catch (error) {
await this.context.rollbackOperation();
throw error;
} finally {
this.stats.endModule(this.name, startTime);
}
}
async applyChange(change: IPlannedChange): Promise<void> {
if (change.type !== 'modify') return;
try {
// Read current content
const content = await plugins.smartfile.fs.toStringSync(change.path);
// Format with prettier
const prettier = await import('prettier');
const formatted = await prettier.format(content, {
filepath: change.path,
...(await this.getPrettierConfig())
});
// Only write if content actually changed
if (formatted !== content) {
await this.modifyFile(change.path, formatted);
logVerbose(`Formatted ${change.path}`);
} else {
// Still update cache even if content didn't change
await this.cache.updateFileCache(change.path);
logVerbose(`No formatting changes for ${change.path}`);
}
} catch (error) {
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'
});
}
}

View File

@@ -0,0 +1,22 @@
import { BaseFormatter } from '../classes.baseformatter.js';
import type { IPlannedChange } from '../interfaces.format.js';
import * as formatReadme from '../format.readme.js';
export class ReadmeFormatter extends BaseFormatter {
get name(): string {
return 'readme';
}
async analyze(): Promise<IPlannedChange[]> {
return [{
type: 'modify',
path: 'readme.md',
module: this.name,
description: 'Ensure readme files exist'
}];
}
async applyChange(change: IPlannedChange): Promise<void> {
await formatReadme.run();
}
}

View File

@@ -0,0 +1,8 @@
import { LegacyFormatter } from './legacy.formatter.js';
import * as formatTemplates from '../format.templates.js';
export class TemplatesFormatter extends LegacyFormatter {
constructor(context: any, project: any) {
super(context, project, 'templates', formatTemplates);
}
}

View File

@@ -0,0 +1,8 @@
import { LegacyFormatter } from './legacy.formatter.js';
import * as formatTsconfig from '../format.tsconfig.js';
export class TsconfigFormatter extends LegacyFormatter {
constructor(context: any, project: any) {
super(context, project, 'tsconfig', formatTsconfig);
}
}