UNPKG

mx-file-router

Version:

A simple file-based router for NodeJS

196 lines (175 loc) 8.16 kB
const fs = require('fs/promises') const path = require('path') const colors = require('./Colors') const SYSTEM_MESSAGE = (msg) => console.log(`⚙️ ${msg}${colors.reset}`) const SEARCH_MESSAGE = (msg) => console.log(`🔍 ${msg}${colors.reset}`) const SUCCESS_MESSAGE = (msg) => console.log(`✅ ${msg}${colors.reset}`) const WARN_MESSAGE = (msg) => console.log(`⚠️ ${msg}${colors.reset}`) const ERROR_MESSAGE = (msg) => console.log(`❌ ${msg}${colors.reset}`) const getRoot = async (currentDir) => { const rootMarkerFiles = ['package-lock.json'] const parentDir = path.dirname(currentDir) for (const markerFile of rootMarkerFiles) { const markerFilePath = path.join(currentDir, markerFile) try { await fs.access(markerFilePath) return currentDir } catch (err) { // File does not exist in the current directory } } if (currentDir === parentDir) { throw new Error('Root directory not found.') } return await getRoot(parentDir) } const getControllerDir = async (dir) => { try { await fs.access(dir) return dir } catch (err) { return null } } exports.start = async (app, options = { port: 3000, controllers: 'controllers', extraLogs: false }) => { try { require.resolve('express') } catch (error) { ERROR_MESSAGE(`${colors.red}Unable to find dependency: ${colors.black}(express)`) ERROR_MESSAGE(`${colors.red}${colors.bright}Server could not be started.`) return } const ROOT_DIR = await getRoot(__dirname) const _CONTROLLERS_DIR = path.join(ROOT_DIR, options.controllers) const CONTROLLERS_DIR = await getControllerDir(_CONTROLLERS_DIR) console.log() if (options.controllers != 'controllers' && options.extraLogs === true) { SYSTEM_MESSAGE(`${colors.cyan}${colors.bright}Scanning for controllers... ${colors.black}(${CONTROLLERS_DIR})`) } else { SYSTEM_MESSAGE(`${colors.cyan}${colors.bright}Scanning for controllers...`) } if (!CONTROLLERS_DIR) { ERROR_MESSAGE(`${colors.red}Unable to find directory: ${colors.black}(${_CONTROLLERS_DIR})`) ERROR_MESSAGE(`${colors.red}${colors.bright}Server could not be started.`) console.log() return } const readFiles = async (directory) => { const files = await fs.readdir(directory) for (let file of files) { let filePath = path.join(directory, file) const stat = await fs.stat(filePath) file = file.toLowerCase() filePath = filePath.toLowerCase() if (!stat.isDirectory()) { if (isController(file)) { if (options.extraLogs === true) { SEARCH_MESSAGE(`${colors.white}Found ${file} ${colors.black}(${filePath})`) } else { SEARCH_MESSAGE(`${colors.white}Found ${file}`) } } else { if (options.extraLogs === true) { WARN_MESSAGE(`${colors.red}Skipping ${file} ${colors.black}(${filePath}) ${colors.black}(incorrect naming pattern)`) } else { WARN_MESSAGE(`${colors.red}Skipping ${file} ${colors.black}(incorrect naming pattern)`) } } } } for (let file of files) { let filePath = path.join(directory, file) const stat = await fs.stat(filePath) file = file.toLowerCase() filePath = filePath.toLowerCase() if (stat.isDirectory()) { await readFiles(filePath) } else if (isController(file)) { console.log() if (options.extraLogs === true) { SYSTEM_MESSAGE(`${colors.white}Attempting to register controller: ${file} ${colors.black}(${filePath})`) } else { SYSTEM_MESSAGE(`${colors.white}Attempting to register controller: ${file}`) } await processController(file, filePath) } } } const isController = (file) => file.toLowerCase().match(/^(\w+)\.(get|head|post|put|delete|connect|options|trace|patch)\.(js|ts)$/i) const checkParams = async (func) => { const functionString = func.toString().replace(/\s/g, '') // Remove whitespace for accurate regex matching const parameterRegex = /(?:async)?(?:function)?\*?\s*(?:\w+)?\s*\((.*?)\)/ const match = functionString.match(parameterRegex) if (match && match[1]) { const parameters = match[1].split(',').map((param) => param.trim()) return parameters.length } return 0 } const processController = async (file, filePath) => { file = file.toLowerCase() filePath = filePath.toLowerCase() try { const module = require(filePath) const errors = [] const { endpoints, handler, middleware } = module if (typeof endpoints === 'undefined') { errors.push(` ${colors.red}Missing 'endpoints' export.`) } else { if (!Array.isArray(endpoints) || endpoints.length === 0) { errors.push(` ${colors.red}'endpoints' export must be a non-empty array.`) } } if (typeof handler === 'undefined') { errors.push(` ${colors.red}Missing 'handler' export.`) } else { if (typeof handler !== 'function') { errors.push(` ${colors.red}'handler' export must be a function.`) } } const routeType = getRouteType(filePath) if (handler) { if (await checkParams(handler) !== 2) { errors.push(` ${colors.red}'handler' export must have exactly 2 parameters.`) } } if (errors.length > 0) { if (options.extraLogs === true) { WARN_MESSAGE(`${colors.red}${colors.bright}Could not register controller due to ${errors.length} error${errors.length > 1 ? 's' : ''}:`) } else { WARN_MESSAGE(`${colors.red}${colors.bright}Could not register controller due to ${errors.length} error${errors.length > 1 ? 's' : ''}:`) } for (const error of errors) { console.error(error) } return } if (middleware) { app[routeType](endpoints, middleware, handler) SUCCESS_MESSAGE(`${colors.green}${colors.bright}Registered successfully! ${colors.magenta}(with middleware)${colors.white} ${colors.yellow}(${routeType.toUpperCase()}) ${colors.black}${JSON.stringify(endpoints).replaceAll('"', '\'')}`) } else { app[routeType](endpoints, handler) SUCCESS_MESSAGE(`${colors.green}${colors.bright}Registered successfully! ${colors.yellow}(${routeType.toUpperCase()}) ${colors.black}${JSON.stringify(endpoints).replaceAll('"', '\'')}`) } } catch (err) { console.error(`Error processing file: ${colors.black}(${filePath})\n`, err) } } const getRouteType = (filePath) => { const routeTypeMatch = filePath.match(/\.([^.]+)\.(js|ts)$/) return routeTypeMatch[1] } try { await readFiles(CONTROLLERS_DIR) console.log() app.listen(options.port, () => { SYSTEM_MESSAGE(`${colors.cyan}${colors.bright}Server is running at http://localhost:${options.port}`) }) console.log() } catch (err) { WARN_MESSAGE(`Error during file checking: ${colors.black}(${filePath})\n`, err) } }