UNPKG

sicua

Version:

A tool for analyzing project structure and dependencies

481 lines (474 loc) 18.6 kB
#!/usr/bin/env node "use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const commander_1 = require("commander"); const index_1 = __importDefault(require("./index")); const path_1 = __importDefault(require("path")); const promises_1 = __importDefault(require("fs/promises")); const picocolors_1 = __importDefault(require("picocolors")); // Version from package.json const packageJson = require("../package.json"); const program = new commander_1.Command(); /** * Format an output message with color and icons */ function formatMessage(message, type = "info") { const icons = { info: "ℹ", success: "✓", error: "✖", warning: "⚠", }; const colors = { info: picocolors_1.default.blue, success: picocolors_1.default.green, error: picocolors_1.default.red, warning: picocolors_1.default.yellow, }; return `${colors[type](icons[type])} ${message}`; } /** * Enhanced project validation with detailed structure detection */ async function validateProject(projectPath) { const result = { isValid: false, projectType: "unknown", hasSourceDirectory: false, sourceDirectory: projectPath, availableDirectories: [], issues: [], suggestions: [], }; try { // Check if directory exists const stats = await promises_1.default.stat(projectPath); if (!stats.isDirectory()) { result.issues.push("Path is not a directory"); return result; } // Check for package.json const packageJsonPath = path_1.default.join(projectPath, "package.json"); try { await promises_1.default.access(packageJsonPath); // Parse package.json to determine project type const packageContent = await promises_1.default.readFile(packageJsonPath, "utf-8"); const packageJson = JSON.parse(packageContent); const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies, }; // Check for React const hasReact = dependencies.react; if (!hasReact) { result.issues.push("No React dependency found in package.json"); result.suggestions.push("This tool is designed for React/Next.js projects"); return result; } // Determine project type const nextVersion = dependencies.next; if (nextVersion) { result.projectType = "nextjs"; result.nextjsVersion = nextVersion; // Determine router type const versionMatch = nextVersion.match(/(\d+)\.(\d+)/); if (versionMatch) { const major = parseInt(versionMatch[1]); const minor = parseInt(versionMatch[2]); if (major > 13 || (major === 13 && minor >= 4)) { result.routerType = "app"; } else { result.routerType = "pages"; } } } else { result.projectType = "react"; } } catch (error) { result.issues.push("package.json not found or invalid"); result.suggestions.push("Make sure you're in the root directory of a React/Next.js project"); return result; } // Check for common project directories const possibleDirectories = [ "src", "app", "pages", "components", "lib", "utils", "hooks", "context", "store", "styles", "public", ]; const availableDirectories = []; for (const dir of possibleDirectories) { const dirPath = path_1.default.join(projectPath, dir); try { const stat = await promises_1.default.stat(dirPath); if (stat.isDirectory()) { availableDirectories.push(dir); } } catch { // Directory doesn't exist, continue } } result.availableDirectories = availableDirectories; // Determine source directory if (availableDirectories.includes("src")) { result.hasSourceDirectory = true; result.sourceDirectory = path_1.default.join(projectPath, "src"); } else { result.sourceDirectory = projectPath; } // Check for source files const hasSourceFiles = await checkForSourceFiles(result.sourceDirectory); if (!hasSourceFiles) { result.issues.push("No React/TypeScript source files found"); result.suggestions.push("Make sure your project contains .tsx, .jsx, .ts, or .js files"); } // Project type specific validations if (result.projectType === "nextjs") { await validateNextJsProject(projectPath, result); } else { await validateReactProject(projectPath, result); } // Determine if project is valid result.isValid = result.issues.length === 0; return result; } catch (error) { result.issues.push(`Failed to validate project: ${error instanceof Error ? error.message : String(error)}`); return result; } } /** * Validate Next.js specific requirements */ async function validateNextJsProject(projectPath, result) { const { routerType, availableDirectories } = result; if (routerType === "app") { // App router validation if (!availableDirectories.includes("app") && !result.hasSourceDirectory) { result.issues.push("Next.js App Router project should have an 'app' directory"); result.suggestions.push("Create an 'app' directory or use 'src/app' structure"); } // Check for app router files const appDir = result.hasSourceDirectory ? path_1.default.join(projectPath, "src", "app") : path_1.default.join(projectPath, "app"); try { await promises_1.default.access(appDir); const appFiles = await promises_1.default.readdir(appDir); const hasLayout = appFiles.some((file) => file.startsWith("layout.")); if (!hasLayout) { result.suggestions.push("Consider adding a root layout.tsx file in your app directory"); } } catch { // App directory might not exist yet, which is okay for validation } } else if (routerType === "pages") { // Pages router validation if (!availableDirectories.includes("pages") && !result.hasSourceDirectory) { result.issues.push("Next.js Pages Router project should have a 'pages' directory"); result.suggestions.push("Create a 'pages' directory or use 'src/pages' structure"); } } // Check for Next.js config const configFiles = ["next.config.js", "next.config.ts", "next.config.mjs"]; let hasConfig = false; for (const configFile of configFiles) { try { await promises_1.default.access(path_1.default.join(projectPath, configFile)); hasConfig = true; break; } catch { // Continue checking } } if (!hasConfig) { result.suggestions.push("Consider adding a next.config.js file for better configuration"); } } /** * Validate React project requirements */ async function validateReactProject(projectPath, result) { // Check for common React project structure if (!result.hasSourceDirectory && !result.availableDirectories.includes("components")) { result.suggestions.push("Consider organizing your code in a 'src' or 'components' directory"); } // Check for common React files const commonFiles = ["index.html", "public/index.html"]; let hasEntryPoint = false; for (const file of commonFiles) { try { await promises_1.default.access(path_1.default.join(projectPath, file)); hasEntryPoint = true; break; } catch { // Continue checking } } if (!hasEntryPoint) { result.suggestions.push("Make sure your project has an entry point (index.html)"); } } /** * Check if directory contains React/TypeScript source files */ async function checkForSourceFiles(directory) { try { const files = await promises_1.default.readdir(directory, { recursive: true }); const sourceExtensions = [".tsx", ".jsx", ".ts", ".js"]; return files.some((file) => typeof file === "string" && sourceExtensions.some((ext) => file.endsWith(ext)) && !file.includes("node_modules") && !file.includes(".d.ts")); } catch (error) { return false; } } /** * Display project validation results */ function displayValidationResults(result, projectPath) { console.log(`\n🔍 Project Validation Results for: ${picocolors_1.default.cyan(projectPath)}`); // Project type and basic info if (result.projectType !== "unknown") { console.log(formatMessage(`Project type: ${result.projectType.toUpperCase()}`, "info")); if (result.projectType === "nextjs") { console.log(formatMessage(`Next.js version: ${result.nextjsVersion}`, "info")); console.log(formatMessage(`Router type: ${result.routerType?.toUpperCase()}`, "info")); } } // Source directory info console.log(formatMessage(`Source directory: ${result.hasSourceDirectory ? "src/" : "project root"}`, "info")); // Available directories if (result.availableDirectories.length > 0) { console.log(formatMessage(`Found directories: ${result.availableDirectories.join(", ")}`, "info")); } // Issues if (result.issues.length > 0) { console.log("\n❌ Issues found:"); result.issues.forEach((issue) => { console.log(` ${formatMessage(issue, "error")}`); }); } // Suggestions if (result.suggestions.length > 0) { console.log("\n💡 Suggestions:"); result.suggestions.forEach((suggestion) => { console.log(` ${formatMessage(suggestion, "warning")}`); }); } // Final result if (result.isValid) { console.log(formatMessage("\nProject validation passed! ✨", "success")); } else { console.log(formatMessage("\nProject validation failed", "error")); console.log(formatMessage("You can still try running the analysis, but results may be limited", "warning")); } } program .name("sicua") .description("A tool for analyzing React project structure and dependencies") .version(packageJson.version) .option("-p, --path <path>", "Path to the project", process.cwd()) .option("-o, --output <path>", "Output file path") .option("--src <dir>", "Source directory to analyze") .option("--root-components <names>", "Root component names (comma-separated)") .option("--extensions <exts>", "File extensions to process (comma-separated)") .option("--verbose", "Enable verbose output", false) .option("--init", "Initialize a config file", false) .option("--silent", "Disable progress output", true) .option("--force", "Force analysis even if validation fails", false) .action(async (options) => { const projectPath = path_1.default.resolve(options.path); if (options.init) { await initConfigFile(projectPath); return; } // Enhanced project validation const validationResult = await validateProject(projectPath); if (!options.silent) { displayValidationResults(validationResult, projectPath); } // Decide whether to proceed based on validation if (!validationResult.isValid && !options.force) { console.error(formatMessage("Project validation failed. Use --force to proceed anyway or fix the issues above.", "error")); process.exit(1); } if (!validationResult.isValid && options.force) { console.log(formatMessage("Forcing analysis despite validation issues...", "warning")); } try { // Prepare options to pass to analyzer const analyzerOptions = { projectPath, silent: options.silent, }; // Add optional parameters if specified if (options.output) analyzerOptions.outputFileName = options.output; if (options.src) analyzerOptions.srcDir = options.src; if (options.verbose) analyzerOptions.verbose = options.verbose; // Process array options if (options.rootComponents) { analyzerOptions.rootComponentNames = options.rootComponents .split(",") .map((c) => c.trim()); } if (options.extensions) { analyzerOptions.fileExtensions = options.extensions .split(",") .map((e) => e.trim()); } await (0, index_1.default)(analyzerOptions); } catch (error) { console.error(formatMessage(`Error during analysis: ${error instanceof Error ? error.message : String(error)}`, "error")); if (options.verbose && error instanceof Error && error.stack) { console.error("\nStack trace:"); console.error(error.stack); } // Provide helpful suggestions based on validation results if (!validationResult.isValid) { console.error("\n💡 The analysis failed and validation issues were detected."); console.error("Consider fixing the validation issues above and trying again."); } process.exit(1); } }); // Enhanced validate command program .command("validate") .description("Validate the project structure without running analysis") .option("-p, --path <path>", "Project path", process.cwd()) .option("--detailed", "Show detailed validation information", false) .action(async (options) => { const projectPath = path_1.default.resolve(options.path); const validationResult = await validateProject(projectPath); if (options.detailed) { displayValidationResults(validationResult, projectPath); } else { if (validationResult.isValid) { console.log(formatMessage(`${projectPath} is a valid ${validationResult.projectType.toUpperCase()} project.`, "success")); } else { console.error(formatMessage(`${projectPath} has validation issues.`, "error")); console.log("Run with --detailed for more information."); } } process.exit(validationResult.isValid ? 0 : 1); }); // Enhanced init command program .command("init") .description("Initialize a config file") .option("-p, --path <path>", "Project path", process.cwd()) .action(async (options) => { await initConfigFile(path_1.default.resolve(options.path), options.template); }); // New info command program .command("info") .description("Show project information and structure") .option("-p, --path <path>", "Project path", process.cwd()) .action(async (options) => { const projectPath = path_1.default.resolve(options.path); const validationResult = await validateProject(projectPath); displayValidationResults(validationResult, projectPath); }); async function initConfigFile(projectPath, template = "basic") { const configPath = path_1.default.join(projectPath, "sicua.config.js"); // Check if config already exists try { await promises_1.default.access(configPath); console.log(formatMessage(`Config file already exists at ${configPath}`, "warning")); console.log("To overwrite it, delete the file and run this command again."); return; } catch (error) { // File doesn't exist, continue } // Validate project first to generate appropriate config const validation = await validateProject(projectPath); // Generate config based on project type and template let configContent = ""; if (template === "nextjs" || validation.projectType === "nextjs") { configContent = generateNextJsConfig(validation); } else if (template === "react" || validation.projectType === "react") { configContent = generateReactConfig(validation); } else { configContent = generateBasicConfig(); } // Write config file await promises_1.default.writeFile(configPath, configContent); console.log(formatMessage(`Config file created at ${configPath}`, "success")); console.log(`Template used: ${validation.projectType || template}`); console.log("You can now run 'sicua' to start the analysis."); } function generateNextJsConfig(validation) { const srcDir = validation.hasSourceDirectory ? "src" : "."; const routerSpecificComponents = validation.routerType === "app" ? ["layout", "page", "loading", "error", "not-found", "template"] : ["_app", "_document", "index"]; return `module.exports = { // File extensions to analyze fileExtensions: [".ts", ".tsx", ".js", ".jsx"], // Root component names (Next.js ${validation.routerType} router specific) rootComponentNames: [${routerSpecificComponents .map((c) => `"${c}"`) .join(", ")}, "App", "Root", "Main"], // Source directory srcDir: "${srcDir}", // Output file outputFileName: "analysis-results.json", };`; } function generateReactConfig(validation) { const srcDir = validation.hasSourceDirectory ? "src" : "."; return `module.exports = { // File extensions to analyze fileExtensions: [".ts", ".tsx", ".js", ".jsx"], // Root component names (React project) rootComponentNames: ["App", "Root", "Main", "Index"], // Source directory srcDir: "${srcDir}", // Output file outputFileName: "analysis-results.json", };`; } function generateBasicConfig() { return `module.exports = { fileExtensions: [".ts", ".tsx", ".js", ".jsx"], rootComponentNames: ["App", "Root", "Main"], srcDir: "src", outputFileName: "analysis-results.json", };`; } program.parse(process.argv);