structure-validation
Version:
A Node.js CLI tool for validating codebase folder and file structure using a clean declarative configuration. Part of the guardz ecosystem for comprehensive TypeScript development.
210 lines • 9.31 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConfigService = void 0;
const ValidationRule_1 = require("../../domain/entities/ValidationRule");
const ConfigValidator_1 = require("./ConfigValidator");
const CacheService_1 = require("../../infrastructure/cache/CacheService");
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
/**
* Application service for loading and parsing configuration
*/
class ConfigService {
constructor(configPath = 'structure-validation.config.json') {
this.configPath = configPath;
this.configValidator = new ConfigValidator_1.ConfigValidator();
this.cacheService = new CacheService_1.CacheService();
}
/**
* Find configuration file in project root (where package.json is located)
*/
findConfigInProjectRoot() {
let currentDir = process.cwd();
const maxDepth = 10; // Prevent infinite loops
for (let depth = 0; depth < maxDepth; depth++) {
const packageJsonPath = path_1.default.join(currentDir, 'package.json');
const configPath = path_1.default.join(currentDir, this.configPath);
// Check if this directory has a package.json (indicating it's a project root)
if (fs_1.default.existsSync(packageJsonPath)) {
// Check if config file exists in this directory
if (fs_1.default.existsSync(configPath)) {
return configPath;
}
}
// Move up one directory
const parentDir = path_1.default.dirname(currentDir);
if (parentDir === currentDir) {
// We've reached the filesystem root
break;
}
currentDir = parentDir;
}
return null;
}
/**
* Load configuration from file
*/
async loadConfig() {
// First try to find the config file in the project root (where package.json is)
let configPath = this.findConfigInProjectRoot();
// If not found in project root, fall back to current directory
if (!configPath) {
configPath = path_1.default.resolve(this.configPath);
}
if (!fs_1.default.existsSync(configPath)) {
throw new Error(`Configuration file not found: ${this.configPath}\n\n💡 Create a configuration file in your project root:\n\n1. Create structure-validation.config.json\n2. Define your folder structure rules\n3. Run validation again\n\nExample configuration:\n\n{\n "root": "tests/fixtures",\n "structure": {\n "components": ["*.tsx"],\n "services": ["*.service.ts"],\n "utils": ["*.util.ts"],\n "**": ["*.test.ts", "*.md"]\n }\n}`);
}
// Check if config file has been modified
if (this.cacheService.isFileModified(configPath)) {
// Invalidate cache when config file changes
this.cacheService.invalidate(this.cacheService.getConfigKey(configPath));
}
// Try to get from cache first
const cacheKey = this.cacheService.getConfigKey(configPath);
const cachedConfig = this.cacheService.get(cacheKey);
if (cachedConfig) {
// Reconstruct ValidationRule instances from cached data
const rules = cachedConfig.rules.map(rule => new ValidationRule_1.ValidationRule(rule.folder, rule.patterns));
return cachedConfig.root ? { rules, root: cachedConfig.root } : { rules };
}
try {
// Read and parse JSON config file
const configContent = fs_1.default.readFileSync(configPath, 'utf8');
const config = JSON.parse(configContent);
// Validate configuration
const validation = this.configValidator.validateConfig(config);
if (!validation.isValid) {
const errorMessage = `Configuration validation failed:\n\n${validation.errors.map(error => `❌ ${error}`).join('\n')}`;
if (validation.warnings.length > 0) {
const warningMessage = `\n\n⚠️ Warnings:\n${validation.warnings.map(warning => `• ${warning}`).join('\n')}`;
throw new Error(errorMessage + warningMessage);
}
throw new Error(errorMessage);
}
// Show warnings if any
if (validation.warnings.length > 0) {
console.warn('⚠️ Configuration warnings:');
validation.warnings.forEach(warning => console.warn(` • ${warning}`));
console.warn('');
}
const parsedConfig = this.parseConfig(config);
// Cache the parsed configuration data (without ValidationRule instances)
const cacheData = {
rules: parsedConfig.rules.map(rule => ({
folder: rule.folder,
patterns: rule.patterns
})),
root: parsedConfig.root
};
this.cacheService.set(cacheKey, cacheData, 10 * 60 * 1000);
return parsedConfig;
}
catch (error) {
if (error instanceof Error && error.message.includes('Configuration validation failed')) {
throw error;
}
throw new Error(`Failed to load configuration: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Parse configuration object into ValidationConfig
*/
parseConfig(config) {
let structure;
let root;
// Handle new structure format
if (config.structure) {
structure = config.structure;
root = config.root;
}
else {
// Handle legacy format (direct structure)
structure = config;
root = undefined;
}
// If root is not defined, try to detect it based on project type
if (!root) {
root = this.detectProjectRoot();
}
const rules = [];
for (const [folder, patterns] of Object.entries(structure)) {
if (!Array.isArray(patterns)) {
throw new Error(`Invalid patterns for folder '${folder}': must be an array`);
}
if (patterns.length === 0) {
throw new Error(`No patterns specified for folder '${folder}'`);
}
rules.push(new ValidationRule_1.ValidationRule(folder, patterns));
}
if (rules.length === 0) {
throw new Error('No validation rules found in configuration');
}
return { rules, root };
}
/**
* Detect project root based on project type
*/
detectProjectRoot() {
const currentDir = process.cwd();
// Check for TypeScript project
if (fs_1.default.existsSync(path_1.default.join(currentDir, 'tsconfig.json'))) {
try {
const tsconfig = JSON.parse(fs_1.default.readFileSync(path_1.default.join(currentDir, 'tsconfig.json'), 'utf8'));
if (tsconfig.compilerOptions?.rootDir) {
return tsconfig.compilerOptions.rootDir;
}
if (tsconfig.include && tsconfig.include.length > 0) {
// Use the first include pattern as root
const firstInclude = tsconfig.include[0];
if (firstInclude.endsWith('/**/*')) {
return firstInclude.replace('/**/*', '');
}
if (firstInclude.endsWith('/*')) {
return firstInclude.replace('/*', '');
}
return firstInclude;
}
}
catch (error) {
console.warn('⚠️ Failed to parse tsconfig.json, using default root');
}
}
// Check for JavaScript project
if (fs_1.default.existsSync(path_1.default.join(currentDir, 'package.json'))) {
try {
const packageJson = JSON.parse(fs_1.default.readFileSync(path_1.default.join(currentDir, 'package.json'), 'utf8'));
if (packageJson.main) {
const mainDir = path_1.default.dirname(packageJson.main);
if (mainDir !== '.') {
return mainDir;
}
}
}
catch (error) {
console.warn('⚠️ Failed to parse package.json, using default root');
}
}
// Default to 'src' if it exists, otherwise use current directory
if (fs_1.default.existsSync(path_1.default.join(currentDir, 'src'))) {
return 'src';
}
return '.';
}
/**
* Get configuration schema for documentation
*/
getSchema() {
return this.configValidator.getSchema();
}
/**
* Clear configuration cache
*/
clearCache() {
this.cacheService.invalidate(this.cacheService.getConfigKey(this.configPath));
}
}
exports.ConfigService = ConfigService;
//# sourceMappingURL=ConfigService.js.map