224 lines
6.7 KiB
TypeScript
224 lines
6.7 KiB
TypeScript
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[] = [];
|
|
|
|
// Define directories to format (TypeScript directories by default)
|
|
const includeDirs = ['ts', 'ts_*', 'test', 'tests'];
|
|
|
|
// File extensions to format
|
|
const extensions = '{ts,tsx,js,jsx,json,md,css,scss,html,xml,yaml,yml}';
|
|
|
|
// Also format root-level config files
|
|
const rootConfigFiles = [
|
|
'package.json',
|
|
'tsconfig.json',
|
|
'npmextra.json',
|
|
'.prettierrc',
|
|
'.prettierrc.json',
|
|
'.prettierrc.js',
|
|
'readme.md',
|
|
'README.md',
|
|
'changelog.md',
|
|
'CHANGELOG.md',
|
|
// Skip files without extensions as prettier can't infer parser
|
|
// 'license',
|
|
// 'LICENSE',
|
|
'*.md',
|
|
];
|
|
|
|
// Collect all files to format
|
|
const allFiles: string[] = [];
|
|
|
|
// Add files from TypeScript directories
|
|
for (const dir of includeDirs) {
|
|
const globPattern = `${dir}/**/*.${extensions}`;
|
|
const dirFiles = await plugins.smartfile.fs.listFileTree(
|
|
'.',
|
|
globPattern,
|
|
);
|
|
allFiles.push(...dirFiles);
|
|
}
|
|
|
|
// Add root config files
|
|
for (const pattern of rootConfigFiles) {
|
|
const rootFiles = await plugins.smartfile.fs.listFileTree('.', pattern);
|
|
// Only include files at root level (no slashes in path)
|
|
const rootLevelFiles = rootFiles.filter((f) => !f.includes('/'));
|
|
allFiles.push(...rootLevelFiles);
|
|
}
|
|
|
|
// Remove duplicates
|
|
const uniqueFiles = [...new Set(allFiles)];
|
|
|
|
// Get all files that match the pattern
|
|
const files = uniqueFiles;
|
|
|
|
// Ensure we only process actual files (not directories)
|
|
const validFiles: string[] = [];
|
|
for (const file of files) {
|
|
try {
|
|
const stats = await plugins.smartfile.fs.stat(file);
|
|
if (!stats.isDirectory()) {
|
|
validFiles.push(file);
|
|
}
|
|
} catch (error) {
|
|
// Skip files that can't be accessed
|
|
logVerbose(`Skipping ${file} - cannot access: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// Check which files need formatting
|
|
for (const file of validFiles) {
|
|
// 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();
|
|
|
|
logVerbose(`Processing ${changes.length} files sequentially`);
|
|
|
|
// Process files sequentially to avoid prettier cache/state issues
|
|
for (let i = 0; i < changes.length; i++) {
|
|
const change = changes[i];
|
|
logVerbose(
|
|
`Processing file ${i + 1}/${changes.length}: ${change.path}`,
|
|
);
|
|
|
|
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 this.postExecute();
|
|
} catch (error) {
|
|
// Rollback removed - no longer tracking operations
|
|
throw error;
|
|
} finally {
|
|
this.stats.endModule(this.name, startTime);
|
|
}
|
|
}
|
|
|
|
async applyChange(change: IPlannedChange): Promise<void> {
|
|
if (change.type !== 'modify') return;
|
|
|
|
try {
|
|
// Validate the path before processing
|
|
if (!change.path || change.path.trim() === '') {
|
|
logger.log(
|
|
'error',
|
|
`Invalid empty path in change: ${JSON.stringify(change)}`,
|
|
);
|
|
throw new Error('Invalid empty path');
|
|
}
|
|
|
|
// Read current content
|
|
const content = plugins.smartfile.fs.toStringSync(change.path);
|
|
|
|
// Format with prettier
|
|
const prettier = await import('prettier');
|
|
|
|
// Skip files that prettier can't parse without explicit parser
|
|
const fileExt = plugins.path.extname(change.path).toLowerCase();
|
|
if (!fileExt || fileExt === '') {
|
|
// Files without extensions need explicit parser
|
|
logVerbose(
|
|
`Skipping ${change.path} - no file extension for parser inference`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const formatted = await prettier.format(content, {
|
|
filepath: change.path,
|
|
...(await this.getPrettierConfig()),
|
|
});
|
|
|
|
// Only write if content actually changed
|
|
if (formatted !== content) {
|
|
// Debug: log the path being written
|
|
logVerbose(`Writing formatted content to: ${change.path}`);
|
|
await this.modifyFile(change.path, formatted);
|
|
logVerbose(`Formatted ${change.path}`);
|
|
} else {
|
|
logVerbose(`No formatting changes for ${change.path}`);
|
|
}
|
|
} catch (prettierError) {
|
|
// Check if it's a parser error
|
|
if (
|
|
prettierError.message &&
|
|
prettierError.message.includes('No parser could be inferred')
|
|
) {
|
|
logVerbose(`Skipping ${change.path} - ${prettierError.message}`);
|
|
return; // Skip this file silently
|
|
}
|
|
throw prettierError;
|
|
}
|
|
} catch (error) {
|
|
// Log the full error stack for debugging mkdir issues
|
|
if (error.message && error.message.includes('mkdir')) {
|
|
logger.log(
|
|
'error',
|
|
`Failed to format ${change.path}: ${error.message}`,
|
|
);
|
|
logger.log('error', `Error stack: ${error.stack}`);
|
|
} else {
|
|
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',
|
|
});
|
|
}
|
|
}
|