restifyx.js
Version:
Advanced API endpoint handler system with automatic documentation
286 lines • 11.9 kB
JavaScript
"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