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.
401 lines ⢠21.2 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.AddFilePatternToConfigService = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
/**
* Dedicated service for adding file patterns to structure-validation.config.json
* This service is completely independent with no code reuse from other services
*/
class AddFilePatternToConfigService {
constructor(rl) {
this.rl = rl;
}
/**
* Execute the add file pattern to config action
* @param filePath The path of the file
* @param fileName The name of the file
* @returns Promise<void>
*/
async executeAddFilePatternToConfig(filePath, fileName) {
console.log(`š executeAddFilePatternToConfig called with:`);
console.log(` - filePath: "${filePath}"`);
console.log(` - fileName: "${fileName}"`);
const configPath = path.join(process.cwd(), 'structure-validation.config.json');
console.log(` - configPath: "${configPath}"`);
if (!fs.existsSync(configPath)) {
console.log('ā structure-validation.config.json not found in project root.');
return;
}
try {
// Generate a smart pattern based on the file name analysis
const suggestedPattern = this.generateSuggestedPattern(fileName);
// Read the current config
const configContent = fs.readFileSync(configPath, 'utf8');
// Parse the JSON config to get the actual structure
const config = JSON.parse(configContent);
const existingFolders = Object.keys(config.structure || config);
// Extract folder name from file path
const fileDir = path.dirname(filePath);
const projectRoot = process.cwd();
const relativePath = path.relative(projectRoot, fileDir);
const folderName = relativePath.split(path.sep).pop() || '';
console.log(` - Extracted folder name: "${folderName}"`);
// Get the root path from config
const configRoot = config.root || '.';
const absoluteConfigRoot = path.resolve(projectRoot, configRoot);
// Check if the file should be added to {root} folder in config
// This happens when the file is in the project root (as defined in config)
const isFileInProjectRoot = fileDir === absoluteConfigRoot;
console.log(` - File is in project root: ${isFileInProjectRoot}`);
console.log(` - File directory: ${fileDir}`);
console.log(` - Config root: ${absoluteConfigRoot}`);
// Check if the folder exists in config
const folderExists = existingFolders.includes(folderName);
console.log(` - Folder "${folderName}" exists in config: ${folderExists}`);
// Create the list of available folders
let availableFolders = [...existingFolders];
let newFolderAdded = false;
// Only add the folder as a new option if the file is in the project root
// and the folder doesn't exist in config
if (!folderExists && folderName && isFileInProjectRoot) {
// Add the folder to the list if it doesn't exist and file is in project root
availableFolders.push(folderName);
newFolderAdded = true;
console.log(` - Added "${folderName}" to available folders list`);
}
return new Promise((resolve) => {
this.rl.question(`\nSuggested pattern: "${suggestedPattern}"\nEnter the pattern you want to add (or press Enter to use the suggested pattern): `, async (patternAnswer) => {
const finalPattern = patternAnswer.trim() || suggestedPattern;
if (!finalPattern) {
console.log('ā Pattern cannot be empty.');
resolve();
return;
}
console.log(`\nUsing pattern: "${finalPattern}"`);
// Show available folders after pattern selection
console.log('\nAvailable folders in config:');
availableFolders.forEach((folder, index) => {
const marker = folder === folderName && newFolderAdded ? ' (NEW)' : '';
console.log(` ${index + 1} - ${folder}${marker}`);
});
console.log(` ${availableFolders.length + 1} - Create new folder`);
// Determine default choice based on file location
let defaultChoice = availableFolders.length + 1; // Default to "Create new folder"
let defaultMessage = `(1-${availableFolders.length + 1}, default: ${defaultChoice})`;
if (isFileInProjectRoot) {
// If file is in project root, default to {root} folder (if it exists) or create new
const rootIndex = availableFolders.findIndex(folder => folder === '{root}');
if (rootIndex !== -1) {
defaultChoice = rootIndex + 1;
defaultMessage = `(1-${availableFolders.length + 1}, default: ${defaultChoice})`;
}
else {
// {root} folder doesn't exist, default to creating new
defaultChoice = availableFolders.length + 1;
defaultMessage = `(1-${availableFolders.length + 1}, default: ${defaultChoice})`;
}
}
else {
// If file is not in project root (i.e., in a subfolder), default to {root} folder
const rootIndex = availableFolders.findIndex(folder => folder === '{root}');
if (rootIndex !== -1) {
defaultChoice = rootIndex + 1;
defaultMessage = `(1-${availableFolders.length + 1}, default: ${defaultChoice})`;
}
else {
// {root} folder doesn't exist, default to creating new
defaultChoice = availableFolders.length + 1;
defaultMessage = `(1-${availableFolders.length + 1}, default: ${defaultChoice})`;
}
}
this.rl.question(`Which folder would you like to add the pattern "${finalPattern}" to? ${defaultMessage}: `, async (answer) => {
let choice = parseInt(answer.trim());
// If no input provided, use default choice
if (isNaN(choice) || choice < 1 || choice > availableFolders.length + 1) {
if (answer.trim() === '') {
choice = defaultChoice;
console.log(`Using default choice: ${choice}`);
}
else {
console.log(`ā Invalid choice "${answer}". Please enter a number between 1 and ${availableFolders.length + 1}.`);
resolve();
return;
}
}
if (choice <= availableFolders.length) {
// Add to existing folder or newly added folder
const targetFolder = availableFolders[choice - 1];
if (!targetFolder) {
console.log('ā Invalid folder selection.');
resolve();
return;
}
// Check if this is a newly added folder that needs to be created
if (newFolderAdded && targetFolder === folderName) {
// Create the new folder with the pattern
await this.createNewFolderWithPattern(configContent, configPath, existingFolders, finalPattern, resolve, targetFolder);
}
else {
// Add to existing folder
await this.addPatternToExistingFolder(configContent, configPath, targetFolder, finalPattern);
resolve();
}
}
else {
// Create new folder
await this.createNewFolderWithPattern(configContent, configPath, existingFolders, finalPattern, resolve);
}
});
});
});
}
catch (error) {
console.error(`ā Failed to update config: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Generate a suggested pattern based on file name analysis
* @param fileName The file name to analyze
* @returns string The suggested pattern
*/
generateSuggestedPattern(fileName) {
const fileExt = path.extname(fileName);
// Generate a smart pattern based on the file name analysis
let suggestedPattern = `*${fileExt}`;
// Extract the base name without extension
const baseName = path.basename(fileName, fileExt);
// Check for patterns with dots (e.g., .guardz., .constant., etc.)
const dotParts = baseName.split('.');
if (dotParts.length > 1) {
// Get the last part before the extension (e.g., "guardz" from "isApiKey.guardz")
const lastPart = dotParts[dotParts.length - 1];
suggestedPattern = `*.${lastPart}${fileExt}`;
}
else if (baseName.startsWith('use')) {
// Hook pattern
suggestedPattern = `use*${fileExt}`;
}
else if (baseName.startsWith('is')) {
// Guard pattern
suggestedPattern = `is*${fileExt}`;
}
else if (baseName.endsWith('Enum')) {
// Enum pattern
suggestedPattern = `*Enum${fileExt}`;
}
return suggestedPattern;
}
/**
* Add pattern to existing folder
* @param configContent The current config content
* @param configPath The config file path
* @param targetFolder The target folder name
* @param suggestedPattern The pattern to add
* @returns Promise<void>
*/
async addPatternToExistingFolder(configContent, configPath, targetFolder, suggestedPattern) {
console.log(`š addPatternToExistingFolder called with:`);
console.log(` - targetFolder: "${targetFolder}"`);
console.log(` - suggestedPattern: "${suggestedPattern}"`);
console.log(` - configPath: "${configPath}"`);
if (!targetFolder) {
console.log('ā Invalid folder selection.');
return;
}
try {
// Parse the JSON content
const config = JSON.parse(configContent);
// Check if the target folder exists in the structure
if (!config.structure || !config.structure[targetFolder]) {
console.log(`ā Folder "${targetFolder}" not found in config structure.`);
return;
}
// Check if the pattern already exists
if (config.structure[targetFolder].includes(suggestedPattern)) {
console.log(`ā ļø Pattern "${suggestedPattern}" already exists in "${targetFolder}" folder.`);
return;
}
// Add the pattern to the existing folder
config.structure[targetFolder].push(suggestedPattern);
console.log(`š Added pattern "${suggestedPattern}" to existing "${targetFolder}" folder`);
// Write the updated config back to file
const updatedContent = JSON.stringify(config, null, 2);
console.log(`\nš Writing to config file: ${configPath}`);
console.log(`š Content length before: ${configContent.length}`);
console.log(`š Content length after: ${updatedContent.length}`);
fs.writeFileSync(configPath, updatedContent);
// Verify the file was written
const writtenContent = fs.readFileSync(configPath, 'utf8');
console.log(`š File written successfully. New content length: ${writtenContent.length}`);
// Display the message and continue
console.log('\nš Configuration updated! Continuing with new rules...');
console.log('ā ļø Files that match the new configuration will be automatically moved.');
}
catch (error) {
console.error(`ā Failed to update config: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
/**
* Create new folder with pattern
* @param configContent The current config content
* @param configPath The config file path
* @param existingFolders The list of existing folders
* @param suggestedPattern The pattern to add
* @param resolve The resolve function for the promise
* @param predefinedFolderName Optional predefined folder name (for auto-detected folders)
* @returns Promise<void>
*/
async createNewFolderWithPattern(configContent, configPath, existingFolders, suggestedPattern, resolve, predefinedFolderName) {
return new Promise((resolve2) => {
if (predefinedFolderName) {
// Use the predefined folder name directly
const cleanFolderName = this.sanitizeFolderName(predefinedFolderName);
if (!cleanFolderName) {
console.log('ā Invalid folder name.');
resolve2();
resolve();
return;
}
// Check if folder already exists
if (existingFolders.includes(cleanFolderName)) {
console.log(`ā Folder "${cleanFolderName}" already exists.`);
resolve2();
resolve();
return;
}
// Parse the JSON content and add the new folder
const config = JSON.parse(configContent);
// Add the new folder to the structure
if (!config.structure) {
config.structure = {};
}
config.structure[cleanFolderName] = [suggestedPattern];
console.log(`š Created new "${cleanFolderName}" folder with pattern "${suggestedPattern}"`);
// Write the updated config back to file
const updatedContent = JSON.stringify(config, null, 2);
// Write the updated config
try {
console.log(`\nš Writing to config file: ${configPath}`);
console.log(`š Content length before: ${configContent.length}`);
console.log(`š Content length after: ${updatedContent.length}`);
fs.writeFileSync(configPath, updatedContent);
// Verify the file was written
const writtenContent = fs.readFileSync(configPath, 'utf8');
console.log(`š File written successfully. New content length: ${writtenContent.length}`);
// Display the message and continue
console.log('\nš Configuration updated! Continuing with new rules...');
console.log('ā ļø Files that match the new configuration will be automatically moved.');
}
catch (error) {
console.error(`ā Failed to write config file: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
// Resolve the promises and continue
resolve2();
resolve();
}
else {
// Ask user for folder name
this.rl.question('Enter new folder name: ', async (folderName) => {
const cleanFolderName = this.sanitizeFolderName(folderName.trim());
if (!cleanFolderName) {
console.log('ā Invalid folder name.');
resolve2();
resolve();
return;
}
// Check if folder already exists
if (existingFolders.includes(cleanFolderName)) {
console.log(`ā Folder "${cleanFolderName}" already exists.`);
resolve2();
resolve();
return;
}
// Parse the JSON content and add the new folder
const config = JSON.parse(configContent);
// Add the new folder to the structure
if (!config.structure) {
config.structure = {};
}
config.structure[cleanFolderName] = [suggestedPattern];
console.log(`š Created new "${cleanFolderName}" folder with pattern "${suggestedPattern}"`);
// Write the updated config back to file
const updatedContent = JSON.stringify(config, null, 2);
// Write the updated config
try {
console.log(`\nš Writing to config file: ${configPath}`);
console.log(`š Content length before: ${configContent.length}`);
console.log(`š Content length after: ${updatedContent.length}`);
fs.writeFileSync(configPath, updatedContent);
// Verify the file was written
const writtenContent = fs.readFileSync(configPath, 'utf8');
console.log(`š File written successfully. New content length: ${writtenContent.length}`);
// Display the message and continue
console.log('\nš Configuration updated! Continuing with new rules...');
console.log('ā ļø Files that match the new configuration will be automatically moved.');
}
catch (error) {
console.error(`ā Failed to write config file: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
// Resolve the promises and continue
resolve2();
resolve();
});
}
});
}
/**
* Sanitize folder name for file system compatibility
* @param folderName The folder name to clean
* @returns string The cleaned folder name
*/
sanitizeFolderName(folderName) {
if (!folderName || folderName.trim().length === 0) {
return '';
}
const cleaned = folderName
.replace(/[<>:"/\\|?*]/g, '') // Remove invalid file system characters
.replace(/\s+/g, '-') // Replace spaces with hyphens
.toLowerCase() // Convert to lowercase
.trim(); // Remove leading/trailing whitespace
return cleaned;
}
}
exports.AddFilePatternToConfigService = AddFilePatternToConfigService;
//# sourceMappingURL=AddFilePatternToConfigService.js.map