UNPKG

@pulzar/cli

Version:

Ultimate command-line interface for Pulzar framework - scaffolding, development server, building, testing, code generation, health diagnostics, security auditing, and deployment tools for modern Node.js applications

322 lines 12.3 kB
import { Command } from "commander"; import * as path from "path"; import { logger } from "../utils/logger"; import { createASTCompiler } from "@pulzar/core"; /** * Build DI container from source code */ export async function buildDI(options = {}) { const startTime = performance.now(); const config = { sourceDir: options.sourceDir || "src", outputFile: options.outputFile || "src/generated/di-container.ts", tsconfig: options.tsconfig || "tsconfig.json", watch: options.watch || false, validate: options.validate ?? true, }; logger.info("🔨 Building DI container", { sourceDir: config.sourceDir, outputFile: config.outputFile, watch: config.watch, }); try { // Create AST compiler const compiler = createASTCompiler(config.tsconfig, config.sourceDir); // Scan project for DI metadata await compiler.scanProject(); // Compile to container const result = await compiler.compile(); // Validate if enabled if (config.validate) { await validateCompilationResult(result); } // Save generated code await compiler.saveGeneratedCode(config.outputFile, result); const buildTime = performance.now() - startTime; logger.info("✅ DI container built successfully", { providers: result.providers.length, modules: result.modules.length, outputFile: config.outputFile, buildTime: `${buildTime.toFixed(2)}ms`, }); // Setup watch mode if requested if (config.watch) { await setupWatchMode(compiler, config); } } catch (error) { logger.error("❌ Failed to build DI container", { error }); process.exit(1); } } /** * Validate compilation result */ async function validateCompilationResult(result) { const errors = []; // Check for missing dependencies for (const provider of result.providers) { for (const dep of provider.dependencies) { const depExists = result.providers.some((p) => p.token === dep); if (!depExists) { errors.push(`Missing dependency '${dep}' for provider '${provider.token}'`); } } } // Check for circular dependencies const visited = new Set(); const visiting = new Set(); function checkCircular(token, path = []) { if (visiting.has(token)) { errors.push(`Circular dependency detected: ${path.concat(token).join(" -> ")}`); return true; } if (visited.has(token)) return false; visiting.add(token); const provider = result.providers.find((p) => p.token === token); if (provider) { for (const dep of provider.dependencies) { if (checkCircular(dep, path.concat(token))) { return true; } } } visiting.delete(token); visited.add(token); return false; } for (const provider of result.providers) { checkCircular(provider.token); } if (errors.length > 0) { logger.error("❌ DI validation failed", { errors }); throw new Error(`DI validation failed: ${errors.join(", ")}`); } logger.info("✅ DI validation passed"); } /** * Setup incremental watch mode for auto-rebuilding */ async function setupWatchMode(compiler, config) { const chokidar = await import("chokidar"); const fs = await import("fs/promises"); // Track file changes for incremental builds const fileHashes = new Map(); const lastBuildTime = new Map(); const watcher = chokidar.watch(path.join(config.sourceDir, "**/*.ts"), { ignored: [ "**/*.d.ts", "**/*.test.ts", "**/*.spec.ts", "**/node_modules/**", config.outputFile, ], persistent: true, ignoreInitial: false, // Process existing files }); logger.info("👀 Setting up incremental watch mode...", { pattern: path.join(config.sourceDir, "**/*.ts"), }); // Initialize file hashes await initializeFileHashes(); async function initializeFileHashes() { try { const files = await getSourceFiles(config.sourceDir); for (const filePath of files) { const hash = await getFileHash(filePath); fileHashes.set(filePath, hash); lastBuildTime.set(filePath, Date.now()); } logger.info("📄 Initialized file tracking", { files: files.length }); } catch (error) { logger.warn("Failed to initialize file hashes", { error }); } } async function getSourceFiles(dir) { const files = []; async function scanDir(currentDir) { try { const entries = await fs.readdir(currentDir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(currentDir, entry.name); if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") { await scanDir(fullPath); } else if (entry.isFile() && entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts")) { files.push(fullPath); } } } catch (error) { // Directory might not exist, skip } } await scanDir(dir); return files; } async function getFileHash(filePath) { try { const content = await fs.readFile(filePath, "utf-8"); const crypto = await import("crypto"); return crypto.createHash("md5").update(content).digest("hex"); } catch { return ""; } } async function hasFileChanged(filePath) { const currentHash = await getFileHash(filePath); const lastHash = fileHashes.get(filePath); if (currentHash !== lastHash) { fileHashes.set(filePath, currentHash); return true; } return false; } async function incrementalRebuild(changedFiles) { const startTime = performance.now(); try { // Only rescan changed files and their dependencies logger.info("🔄 Incremental rebuild started", { changedFiles: changedFiles.length, files: changedFiles.map((f) => path.relative(process.cwd(), f)), }); // Determine what needs to be rebuilt const affectedFiles = await findAffectedFiles(changedFiles); if (affectedFiles.length === 0) { logger.info("✨ No changes detected, skipping rebuild"); return; } // Incremental scan only affected files await compiler.incrementalScan(affectedFiles); const result = await compiler.compile(); await compiler.saveGeneratedCode(config.outputFile, result); const buildTime = performance.now() - startTime; logger.info("✅ Incremental rebuild completed", { providers: result.providers.length, modules: result.modules.length, affectedFiles: affectedFiles.length, buildTime: `${buildTime.toFixed(2)}ms`, }); // Update build times changedFiles.forEach((file) => { lastBuildTime.set(file, Date.now()); }); } catch (error) { logger.error("❌ Incremental rebuild failed", { error, changedFiles: changedFiles.length, }); } } async function findAffectedFiles(changedFiles) { const affected = new Set(changedFiles); // Find files that import the changed files const allFiles = await getSourceFiles(config.sourceDir); for (const file of allFiles) { try { const content = await fs.readFile(file, "utf-8"); // Simple import detection (could be improved with AST) for (const changedFile of changedFiles) { const relativePath = path .relative(path.dirname(file), changedFile) .replace(/\.ts$/, ""); const normalizedPath = relativePath.replace(/\\/g, "/"); if (content.includes(`from '${normalizedPath}'`) || content.includes(`from "./${normalizedPath}"`) || content.includes(`import('${normalizedPath}')`)) { affected.add(file); } } } catch { // Skip files that can't be read } } return Array.from(affected); } // Debounce rapid file changes let rebuildTimeout = null; const pendingChanges = new Set(); const debouncedRebuild = (filePath) => { pendingChanges.add(filePath); if (rebuildTimeout) { clearTimeout(rebuildTimeout); } rebuildTimeout = setTimeout(async () => { const changedFiles = Array.from(pendingChanges); pendingChanges.clear(); // Filter to only files that actually changed const actualChanges = []; for (const file of changedFiles) { if (await hasFileChanged(file)) { actualChanges.push(file); } } if (actualChanges.length > 0) { await incrementalRebuild(actualChanges); } rebuildTimeout = null; }, 150); // 150ms debounce }; watcher.on("change", debouncedRebuild); watcher.on("add", debouncedRebuild); watcher.on("unlink", (filePath) => { fileHashes.delete(filePath); lastBuildTime.delete(filePath); debouncedRebuild(filePath); }); // Graceful shutdown process.on("SIGINT", () => { logger.info("🛑 Stopping incremental watch mode..."); if (rebuildTimeout) { clearTimeout(rebuildTimeout); } watcher.close(); process.exit(0); }); logger.info("👀 Incremental watch mode active", { debounce: "150ms", tracking: fileHashes.size, }); } /** * Commander.js command setup */ export function createBuildDICommand() { return new Command("build-di") .description("Build dependency injection container from source code") .option("-s, --source-dir <dir>", "Source directory to scan", "src") .option("-o, --output-file <file>", "Output file for generated container", "src/generated/di-container.ts") .option("-t, --tsconfig <file>", "TypeScript config file", "tsconfig.json") .option("-w, --watch", "Watch for changes and rebuild automatically", false) .option("--no-validate", "Skip validation of the generated container", false) .action(async (options) => { await buildDI(options); }); } /** * Dedicated watch command for better CLI experience */ export function createWatchDICommand() { return new Command("di:watch") .alias("watch-di") .description("Watch for changes and incrementally rebuild DI container") .option("-s, --source-dir <dir>", "Source directory to scan", "src") .option("-o, --output-file <file>", "Output file for generated container", "src/generated/di-container.ts") .option("-t, --tsconfig <file>", "TypeScript config file", "tsconfig.json") .option("--no-validate", "Skip validation of the generated container", false) .option("--debounce <ms>", "Debounce time for file changes in milliseconds", "150") .action(async (options) => { // Force watch mode await buildDI({ ...options, watch: true }); }); } //# sourceMappingURL=build-di.js.map