frontend-standards-checker
Version:
A comprehensive frontend standards validation tool with TypeScript support
235 lines • 9 kB
JavaScript
import fs from 'fs';
import path from 'path';
/**
* Rule engine for validating file content against defined rules
*/
export class RuleEngine {
logger;
rules;
config;
constructor(logger) {
this.logger = logger;
this.rules = [];
this.config = null;
}
/**
* Check if a file is a configuration file that should be excluded from validation
* @param filePath The file path to check
* @returns True if the file is a configuration file
*/
isConfigFile(filePath) {
const fileName = path.basename(filePath);
// Common configuration file patterns
const configPatterns = [
/\.config\.(js|ts|mjs|cjs|json)$/,
/^(jest|vite|webpack|tailwind|next|eslint|prettier|babel|rollup|tsconfig)\.config\./,
/^(vitest|nuxt|quasar)\.config\./,
/^tsconfig.*\.json$/,
/^\.eslintrc/,
/^\.prettierrc/,
/^babel\.config/,
/^postcss\.config/,
/^stylelint\.config/,
/^cypress\.config/,
/^playwright\.config/,
/^storybook\.config/,
/^metro\.config/,
/^expo\.config/,
];
return configPatterns.some((pattern) => pattern.test(fileName));
}
/**
* Initialize the rule engine with configuration
*/
initialize(config, _options) {
this.config = config;
// Handle different types of rules configuration
if (Array.isArray(config.rules)) {
this.rules = config.rules;
}
else {
this.rules = [];
}
this.logger.debug(`Initialized rule engine with ${this.rules.length} rules`);
}
/**
* Validate a file against all rules
*/
async validateFile(filePath) {
if (this.isConfigFile(filePath)) {
this.logger.debug(`Skipping configuration file: ${filePath}`);
return [];
}
try {
const content = fs.readFileSync(filePath, 'utf8');
const errors = await this.validateFileContent(content, filePath);
return this.deduplicateErrors(errors);
}
catch (error) {
return this.handleValidationError(error, filePath);
}
}
async validateFileContent(content, filePath) {
const errors = [];
const fileName = path.basename(filePath);
const isIndexFile = fileName === 'index.ts' || fileName === 'index.tsx';
await this.runBasicRules(content, filePath, errors);
if (!isIndexFile) {
await this.runAdditionalValidations(content, filePath, errors);
}
await this.runAlwaysApplicableValidations(filePath, errors);
return errors;
}
async runBasicRules(content, filePath, errors) {
for (const rule of this.rules) {
if (rule.name === 'No unused variables')
continue;
try {
await this.applyRule(rule, content, filePath, errors);
}
catch (error) {
this.logRuleError(rule.name, filePath, error);
}
}
}
async applyRule(rule, content, filePath, errors) {
const ruleResult = await rule.check(content, filePath);
if (Array.isArray(ruleResult)) {
if (ruleResult.length === 0)
return; // No violaciones, no agregar error
for (const line of ruleResult) {
const errorInfo = this.createErrorInfo(rule, filePath);
errorInfo.line = line;
errorInfo.message = `${rule.message} (line ${line})`;
errors.push(errorInfo);
}
return;
}
if (!ruleResult)
return;
// If boolean true, add a generic error without line number
const errorInfo = this.createErrorInfo(rule, filePath);
if (rule.name === 'No variable shadowing' && rule.shadowingDetails) {
this.addShadowingDetails(errorInfo, rule);
}
errors.push(errorInfo);
}
createErrorInfo(rule, filePath) {
return {
rule: rule.name,
message: rule.message,
filePath,
severity: rule.severity ?? 'error',
category: rule.category ?? 'content',
};
}
addShadowingDetails(errorInfo, rule) {
const { shadowingDetails } = rule;
errorInfo.line = shadowingDetails.line;
errorInfo.message = `Variable '${shadowingDetails.variable}' shadows a variable from an outer scope (line ${shadowingDetails.line}). ${rule.message}`;
}
async runAdditionalValidations(content, filePath, errors) {
const validators = await this.loadAdditionalValidators();
if (validators) {
this.runContentValidators(validators, content, filePath, errors);
}
}
async runAlwaysApplicableValidations(filePath, errors) {
const validators = await this.loadAdditionalValidators();
if (validators) {
this.runFileValidators(validators, filePath, errors);
}
}
logRuleError(ruleName, filePath, error) {
this.logger.warn(`Rule "${ruleName}" failed for ${filePath}:`, error instanceof Error ? error.message : String(error));
}
deduplicateErrors(errors) {
const seen = new Set();
return errors.filter((err) => {
const key = `${err.filePath}|${err.rule}|${err.line ?? 'no-line'}`;
if (seen.has(key))
return false;
seen.add(key);
return true;
});
}
handleValidationError(error, filePath) {
this.logger.error(`Failed to validate file ${filePath}:`, error instanceof Error ? error.message : String(error));
return [
{
rule: 'File validation error',
message: `Could not validate file: ${error instanceof Error ? error.message : String(error)}`,
filePath,
severity: 'error',
category: 'content',
},
];
}
/**
* Validate content with context (compatibility method)
*/
async validate(_content, filePath, _context) {
// For now, we use validateFile method which reads the file content
// In future, we could refactor to use the provided content directly
return this.validateFile(filePath);
}
/**
* Check if a file is a configuration file (public method)
*/
isConfigurationFile(filePath) {
return this.isConfigFile(filePath);
}
/**
* Safely load additional validators
*/
async loadAdditionalValidators() {
try {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - Temporary workaround for JS module import
return await import('./additional-validators.js');
}
catch (error) {
this.logger.debug('Additional validators not found:', error.message);
return null;
}
}
/**
* Run content validators for non-index files
*/
runContentValidators(additionalValidators, content, filePath, errors) {
try {
const { checkInlineStyles, checkCommentedCode, checkHardcodedData, checkFunctionComments, checkFunctionNaming, checkInterfaceNaming, checkStyleConventions, } = additionalValidators;
errors.push(...(checkInlineStyles(content, filePath) ?? []));
errors.push(...(checkCommentedCode(content, filePath) ?? []));
errors.push(...(checkHardcodedData(content, filePath) ?? []));
errors.push(...(checkFunctionComments(content, filePath) ?? []));
errors.push(...(checkFunctionNaming(content, filePath) ?? []));
errors.push(...(checkInterfaceNaming(content, filePath) ?? []));
errors.push(...(checkStyleConventions(content, filePath) ?? []));
}
catch (error) {
this.logger.warn('Failed to run content validators:', error instanceof Error ? error.message : String(error));
}
}
/**
* Run file validators that apply to all files
*/
runFileValidators(additionalValidators, filePath, errors) {
try {
const { checkEnumsOutsideTypes, checkHookFileExtension, checkAssetNaming, } = additionalValidators;
const enumError = checkEnumsOutsideTypes(filePath);
if (enumError)
errors.push(enumError);
const hookExtError = checkHookFileExtension(filePath);
if (hookExtError)
errors.push(hookExtError);
const assetError = checkAssetNaming(filePath);
if (assetError)
errors.push(assetError);
}
catch (error) {
this.logger.warn('Failed to run file validators:', error instanceof Error ? error.message : String(error));
}
}
}
//# sourceMappingURL=rule-engine.js.map