feat(format): add check and fix workflows
This commit is contained in:
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user