mx-file-router
Version:
A simple file-based router for NodeJS
196 lines (175 loc) • 8.16 kB
JavaScript
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)
}
}