feat(format): add check and fix workflows

This commit is contained in:
2026-05-14 13:18:49 +00:00
parent 6f0928e7c7
commit 278df40ba7
11 changed files with 635 additions and 250 deletions
+43 -15
View File
@@ -1,5 +1,5 @@
import { BaseFormatter } from '../classes.baseformatter.js';
import type { IPlannedChange } from '../interfaces.format.js';
import type { IFormatWarning, IPlannedChange } from '../interfaces.format.js';
import * as plugins from '../mod.plugins.js';
import * as paths from '../../paths.js';
import { logger } from '../../gitzone.logging.js';
@@ -11,6 +11,10 @@ export class LicenseFormatter extends BaseFormatter {
return 'license';
}
get runsWithoutChanges(): boolean {
return true;
}
async analyze(): Promise<IPlannedChange[]> {
// License formatter only checks for incompatible licenses
// It does not modify any files, so return empty array
@@ -18,29 +22,34 @@ export class LicenseFormatter extends BaseFormatter {
return [];
}
async validate(): Promise<IFormatWarning[]> {
const result = await this.checkLicenses();
if (!result || result.failingModules.length === 0) {
return [];
}
return [
{
level: 'error',
module: this.name,
message: `License check failed for ${result.failingModules.length} module(s): ${result.failingModules
.map((failedModule) => `${failedModule.name} (${failedModule.license})`)
.join(', ')}`,
},
];
}
async execute(changes: IPlannedChange[]): Promise<void> {
const startTime = this.stats.moduleStartTime(this.name);
this.stats.startModule(this.name);
try {
// Check if node_modules exists
const nodeModulesPath = plugins.path.join(paths.cwd, 'node_modules');
const nodeModulesExists = await plugins.smartfs
.directory(nodeModulesPath)
.exists();
if (!nodeModulesExists) {
const licenseCheckResult = await this.checkLicenses();
if (!licenseCheckResult) {
logger.log('warn', 'No node_modules found. Skipping license check');
return;
}
// Run license check
const licenseChecker = await plugins.smartlegal.createLicenseChecker();
const licenseCheckResult = await licenseChecker.excludeLicenseWithinPath(
paths.cwd,
INCOMPATIBLE_LICENSES,
);
if (licenseCheckResult.failingModules.length === 0) {
logger.log('info', 'License check passed - no incompatible licenses found');
} else {
@@ -59,4 +68,23 @@ export class LicenseFormatter extends BaseFormatter {
async applyChange(change: IPlannedChange): Promise<void> {
// No file changes for license formatter
}
private async checkLicenses(): Promise<{
failingModules: Array<{ name: string; license: string }>;
} | undefined> {
const nodeModulesPath = plugins.path.join(paths.cwd, 'node_modules');
const nodeModulesExists = await plugins.smartfs
.directory(nodeModulesPath)
.exists();
if (!nodeModulesExists) {
return undefined;
}
const licenseChecker = await plugins.smartlegal.createLicenseChecker();
return await licenseChecker.excludeLicenseWithinPath(
paths.cwd,
INCOMPATIBLE_LICENSES,
);
}
}
+54 -66
View File
@@ -56,7 +56,8 @@ export class PrettierFormatter extends BaseFormatter {
);
allFiles.push(...filteredFiles);
} catch (error) {
logVerbose(`Skipping directory ${dir}: ${error.message}`);
const errorMessage = error instanceof Error ? error.message : String(error);
logVerbose(`Skipping directory ${dir}: ${errorMessage}`);
}
}
@@ -72,7 +73,8 @@ export class PrettierFormatter extends BaseFormatter {
const rootLevelFiles = rootFiles.filter((f) => !f.includes('/'));
allFiles.push(...rootLevelFiles);
} catch (error) {
logVerbose(`Skipping pattern ${pattern}: ${error.message}`);
const errorMessage = error instanceof Error ? error.message : String(error);
logVerbose(`Skipping pattern ${pattern}: ${errorMessage}`);
}
}
@@ -89,20 +91,46 @@ export class PrettierFormatter extends BaseFormatter {
}
} catch (error) {
// Skip files that can't be accessed
logVerbose(`Skipping ${file} - cannot access: ${error.message}`);
const errorMessage = error instanceof Error ? error.message : String(error);
logVerbose(`Skipping ${file} - cannot access: ${errorMessage}`);
}
}
const prettier = await import('prettier');
const prettierConfig = await this.getPrettierConfig();
for (const file of validFiles) {
changes.push({
type: 'modify',
path: file,
module: this.name,
description: 'Format with Prettier',
});
try {
const fileExt = plugins.path.extname(file).toLowerCase();
if (!fileExt) {
continue;
}
const content = (await plugins.smartfs
.file(file)
.encoding('utf8')
.read()) as string;
const formatted = await prettier.format(content, {
filepath: file,
...prettierConfig,
});
if (formatted !== content) {
changes.push({
type: 'modify',
path: file,
module: this.name,
description: 'Format with Prettier',
content: formatted,
});
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logVerbose(`Skipping Prettier analysis for ${file}: ${errorMessage}`);
}
}
logger.log('info', `Found ${changes.length} files to format with Prettier`);
logger.log('info', `Found ${changes.length} files needing Prettier`);
return changes;
}
@@ -127,9 +155,10 @@ export class PrettierFormatter extends BaseFormatter {
this.stats.recordFileOperation(this.name, change.type, true);
} catch (error) {
this.stats.recordFileOperation(this.name, change.type, false);
const errorMessage = error instanceof Error ? error.message : String(error);
logger.log(
'error',
`Failed to format ${change.path}: ${error.message}`,
`Failed to format ${change.path}: ${errorMessage}`,
);
// Don't throw - continue with other files
}
@@ -192,28 +221,32 @@ export class PrettierFormatter extends BaseFormatter {
logVerbose(`No formatting changes for ${change.path}`);
}
} catch (prettierError) {
const prettierErrorMessage = prettierError instanceof Error
? prettierError.message
: String(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}`);
if (prettierErrorMessage.includes('No parser could be inferred')) {
logVerbose(`Skipping ${change.path} - ${prettierErrorMessage}`);
return; // Skip this file silently
}
throw prettierError;
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const errorStack = error instanceof Error ? error.stack : undefined;
// Log the full error stack for debugging mkdir issues
if (error.message && error.message.includes('mkdir')) {
if (errorMessage.includes('mkdir')) {
logger.log(
'error',
`Failed to format ${change.path}: ${error.message}`,
`Failed to format ${change.path}: ${errorMessage}`,
);
logger.log('error', `Error stack: ${error.stack}`);
if (errorStack) {
logger.log('error', `Error stack: ${errorStack}`);
}
} else {
logger.log(
'error',
`Failed to format ${change.path}: ${error.message}`,
`Failed to format ${change.path}: ${errorMessage}`,
);
}
throw error;
@@ -234,52 +267,7 @@ export class PrettierFormatter extends BaseFormatter {
});
}
/**
* Override check() to compute diffs on-the-fly by running prettier
*/
async check(): Promise<ICheckResult> {
const changes = await this.analyze();
const diffs: ICheckResult['diffs'] = [];
for (const change of changes) {
if (change.type !== 'modify') continue;
try {
// Read current content
const currentContent = (await plugins.smartfs
.file(change.path)
.encoding('utf8')
.read()) as string;
// Skip files without extension (prettier can't infer parser)
const fileExt = plugins.path.extname(change.path).toLowerCase();
if (!fileExt) continue;
// Format with prettier to get what it would produce
const prettier = await import('prettier');
const formatted = await prettier.format(currentContent, {
filepath: change.path,
...(await this.getPrettierConfig()),
});
// Only add to diffs if content differs
if (formatted !== currentContent) {
diffs.push({
path: change.path,
type: 'modify',
before: currentContent,
after: formatted,
});
}
} catch (error) {
// Skip files that can't be processed
logVerbose(`Skipping diff for ${change.path}: ${error.message}`);
}
}
return {
hasDiff: diffs.length > 0,
diffs,
};
return await super.check();
}
}