UNPKG

restifyx.js

Version:

Advanced API endpoint handler system with automatic documentation

286 lines 11.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.HotReloader = void 0; const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const logger_1 = require("./logger"); const glob_1 = require("glob"); var ItemType; (function (ItemType) { ItemType[ItemType["FILE"] = 0] = "FILE"; ItemType[ItemType["DIRECTORY"] = 1] = "DIRECTORY"; ItemType[ItemType["SYMLINK"] = 2] = "SYMLINK"; ItemType[ItemType["UNKNOWN"] = 3] = "UNKNOWN"; })(ItemType || (ItemType = {})); /** * HotReloader class for watching and reloading endpoint files * @class * @param {RestifyXApp} app - The RestifyX application instance * @param {ResolvedConfig} config - The resolved configuration object * @property {string} endpointsDir - The directory where endpoint files are located * @property {string} pattern - The pattern to match endpoint files */ class HotReloader { constructor(app, config) { this.watchers = new Map(); this.logger = (0, logger_1.getLogger)(); this.reloadDebounceTimeout = null; this.reloadQueue = new Set(); this.isReloading = false; this.watchedFiles = new Set(); this.watchedDirs = new Set(); this.initialScan = true; this.app = app; this.endpointsDir = path_1.default.resolve(process.cwd(), config.endpoints.directory); this.pattern = config.endpoints.pattern; this.config = config.hotReload; } /** * Start the hot reload watcher * @returns {Promise<void>} */ async start() { this.logger.info(`Starting hot reload watcher for ${this.endpointsDir}`); if (!fs_1.default.existsSync(this.endpointsDir)) { this.logger.info(`Creating endpoints directory: ${this.endpointsDir}`); fs_1.default.mkdirSync(this.endpointsDir, { recursive: true }); } await this.initialScanDirectories(); this.watchDirectory(this.endpointsDir); this.initialScan = false; this.logger.info("Hot reload watcher started"); if (this.config.verbose) { this.logger.info(`Watching ${this.watchedDirs.size} directories and ${this.watchedFiles.size} files`); this.logger.debug(`Watched directories: ${Array.from(this.watchedDirs).join(", ")}`); } } /** * Stop the hot reload watcher and clear all watchers * @returns {void} */ stop() { this.logger.info("Stopping hot reload watcher"); for (const watcher of this.watchers.values()) { watcher.close(); } this.watchers.clear(); this.watchedFiles.clear(); this.watchedDirs.clear(); if (this.reloadDebounceTimeout) { clearTimeout(this.reloadDebounceTimeout); this.reloadDebounceTimeout = null; } } async initialScanDirectories() { try { const pattern = path_1.default.join(this.endpointsDir, this.pattern).replace(/\\/g, "/"); this.logger.debug(`Scanning for files with pattern: ${pattern}`); const files = await (0, glob_1.glob)(pattern); if (files.length === 0) { this.logger.warn(`No endpoint files found matching pattern: ${pattern}`); } else { this.logger.info(`Found ${files.length} endpoint files`); for (const file of files) { this.watchedFiles.add(file); let dir = path_1.default.dirname(file); while (dir && dir !== "." && !this.watchedDirs.has(dir) && dir.startsWith(this.endpointsDir)) { this.watchedDirs.add(dir); dir = path_1.default.dirname(dir); } } } } catch (error) { this.logger.error(`Error during initial directory scan: ${error instanceof Error ? error.message : String(error)}`); } } watchDirectory(dir) { if (this.watchers.has(dir)) return; try { if (this.shouldIgnore(dir)) { if (this.config.verbose) { this.logger.debug(`Ignoring directory: ${dir}`); } return; } const watcher = fs_1.default.watch(dir, { recursive: false }, (eventType, filename) => { if (!filename) return; const fullPath = path_1.default.join(dir, filename); if (this.shouldIgnore(fullPath)) { return; } if (this.matchesPattern(fullPath) || this.isEndpointRelatedFile(fullPath)) { if (this.config.verbose) { this.logger.debug(`File change detected: ${fullPath} (${eventType})`); } this.queueReload(fullPath); } if (eventType === "rename") { try { if (fs_1.default.existsSync(fullPath)) { const stats = fs_1.default.statSync(fullPath); if (stats.isDirectory()) { this.watchedDirs.add(fullPath); this.watchDirectory(fullPath); if (this.config.verbose) { this.logger.debug(`New directory detected: ${fullPath}`); } } else if (stats.isFile() && this.matchesPattern(fullPath)) { this.watchedFiles.add(fullPath); if (this.config.verbose) { this.logger.debug(`New file detected: ${fullPath}`); } this.queueReload(fullPath); } } else { if (this.watchedDirs.has(fullPath)) { this.watchedDirs.delete(fullPath); if (this.watchers.has(fullPath)) { this.watchers.get(fullPath)?.close(); this.watchers.delete(fullPath); } if (this.config.verbose) { this.logger.debug(`Directory removed: ${fullPath}`); } } else if (this.watchedFiles.has(fullPath)) { this.watchedFiles.delete(fullPath); if (this.config.verbose) { this.logger.debug(`File removed: ${fullPath}`); } this.queueReload(fullPath); } } } catch (err) { } } }); this.watchers.set(dir, watcher); this.watchedDirs.add(dir); if (this.config.verbose && !this.initialScan) { this.logger.debug(`Watching directory: ${dir}`); } try { const entries = fs_1.default.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const entryPath = path_1.default.join(dir, entry.name); if (this.shouldIgnore(entryPath)) { continue; } if (entry.isDirectory()) { this.watchDirectory(entryPath); } else if (entry.isFile() && this.matchesPattern(entryPath)) { this.watchedFiles.add(entryPath); } } } catch (err) { this.logger.error(`Error reading directory ${dir}: ${err}`); } } catch (err) { this.logger.error(`Error setting up watcher for ${dir}: ${err}`); } } shouldIgnore(filePath) { const relativePath = path_1.default.relative(process.cwd(), filePath); for (const pattern of this.config.ignorePatterns) { if (relativePath.includes(pattern)) { return true; } } if (!this.config.watchNodeModules && relativePath.includes("node_modules")) { return true; } return false; } matchesPattern(filePath) { const relativePath = path_1.default.relative(process.cwd(), filePath); return new RegExp(this.pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*")).test(relativePath); } isEndpointRelatedFile(filePath) { const ext = path_1.default.extname(filePath); return this.config.watchExtensions.includes(ext); } queueReload(filePath) { this.reloadQueue.add(filePath); if (this.reloadDebounceTimeout) { clearTimeout(this.reloadDebounceTimeout); } this.reloadDebounceTimeout = setTimeout(() => { this.reloadEndpoints(); }, this.config.debounceMs); } async reloadEndpoints() { if (this.isReloading) return; this.isReloading = true; const filesToReload = [...this.reloadQueue]; this.reloadQueue.clear(); this.logger.info(`Reloading endpoints due to changes in ${filesToReload.length} files`); if (this.config.verbose) { this.logger.debug(`Changed files: ${filesToReload.join(", ")}`); } try { filesToReload.forEach((file) => { try { const resolvedPath = require.resolve(file); delete require.cache[resolvedPath]; if (this.config.verbose) { this.logger.debug(`Cleared module cache for: ${file}`); } } catch (err) { } }); const result = await this.app.registerEndpoints(); if (result.success) { this.logger.info(`Endpoints reloaded successfully: ${result.endpoints.length} endpoints registered`); } else { this.logger.error(`Error reloading endpoints: ${result.error}`); if (result.failedEndpoints && result.failedEndpoints.length > 0) { this.logger.error(`Failed endpoints: ${result.failedEndpoints.length}`); if (this.config.verbose) { result.failedEndpoints.forEach(({ path, error }) => { this.logger.error(`- ${path}: ${error}`); }); } } } } catch (err) { this.logger.error(`Error reloading endpoints: ${err}`); } finally { this.isReloading = false; } } getItemType(file) { try { const stats = fs_1.default.lstatSync(file); if (stats.isFile()) return ItemType.FILE; if (stats.isDirectory()) return ItemType.DIRECTORY; if (stats.isSymbolicLink()) return ItemType.SYMLINK; } catch (err) { this.logger.error(`Error getting item type for file: ${file}`); } return ItemType.UNKNOWN; } } exports.HotReloader = HotReloader; //# sourceMappingURL=hotReloader.js.map