feat(format): Enhance format module with rollback, diff reporting, and improved parallel execution
This commit is contained in:
39
ts/mod_format/formatters/cleanup.formatter.ts
Normal file
39
ts/mod_format/formatters/cleanup.formatter.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
8
ts/mod_format/formatters/copy.formatter.ts
Normal file
8
ts/mod_format/formatters/copy.formatter.ts
Normal 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);
|
||||
}
|
||||
}
|
8
ts/mod_format/formatters/gitignore.formatter.ts
Normal file
8
ts/mod_format/formatters/gitignore.formatter.ts
Normal 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);
|
||||
}
|
||||
}
|
36
ts/mod_format/formatters/legacy.formatter.ts
Normal file
36
ts/mod_format/formatters/legacy.formatter.ts
Normal 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);
|
||||
}
|
||||
}
|
8
ts/mod_format/formatters/license.formatter.ts
Normal file
8
ts/mod_format/formatters/license.formatter.ts
Normal 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);
|
||||
}
|
||||
}
|
8
ts/mod_format/formatters/npmextra.formatter.ts
Normal file
8
ts/mod_format/formatters/npmextra.formatter.ts
Normal 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);
|
||||
}
|
||||
}
|
8
ts/mod_format/formatters/packagejson.formatter.ts
Normal file
8
ts/mod_format/formatters/packagejson.formatter.ts
Normal 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);
|
||||
}
|
||||
}
|
125
ts/mod_format/formatters/prettier.formatter.ts
Normal file
125
ts/mod_format/formatters/prettier.formatter.ts
Normal 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'
|
||||
});
|
||||
}
|
||||
}
|
22
ts/mod_format/formatters/readme.formatter.ts
Normal file
22
ts/mod_format/formatters/readme.formatter.ts
Normal 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();
|
||||
}
|
||||
}
|
8
ts/mod_format/formatters/templates.formatter.ts
Normal file
8
ts/mod_format/formatters/templates.formatter.ts
Normal 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);
|
||||
}
|
||||
}
|
8
ts/mod_format/formatters/tsconfig.formatter.ts
Normal file
8
ts/mod_format/formatters/tsconfig.formatter.ts
Normal 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);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user