const path = require('path')
const { readdirSync, statSync } = require('fs')
/**
* Universal module loader that handles both CommonJS and ESM modules
* Tries CommonJS first, falls back to dynamic import for ESM
*
* @param {string} modulePath - Path to the module to load
* @returns {Promise<any>|any} - The loaded module
*/
// function loadModule(modulePath) {
// try {
// // Try CommonJS first (synchronous)
// return require(modulePath)
// } catch (requireError) {
// // If require fails, try dynamic import (asynchronous)
// try {
// // Use Function constructor to avoid static analysis issues
// const dynamicImport = new Function('modulePath', 'return import(modulePath)')
// return dynamicImport(modulePath).then(module => module.default || module)
// } catch (importError) {
// // If both fail, throw the original require error
// throw requireError
// }
// }
// }
function loadModule(modulePath) {
// Check if we're in a CommonJS environment (require is available)
if (typeof require !== 'undefined') {
try {
// Try CommonJS first (synchronous)
return require(modulePath)
} catch (requireError) {
// If require fails (e.g., ESM module), fall back to dynamic import
try {
const dynamicImport = new Function(
'modulePath',
'return import(modulePath)'
)
return dynamicImport(modulePath).then((module) => {
// Handle both default and named exports properly
if (module.default !== undefined) {
return module.default
}
return module
})
} catch (importError) {
console.warn('Dynamic import failed:', importError)
requireError.cause = importError
throw requireError
}
}
} else {
// We're in an ESM environment, use dynamic import directly
try {
const dynamicImport = new Function(
'modulePath',
'return import(modulePath)'
)
return dynamicImport(modulePath).then((module) => {
// Handle both default and named exports properly
if (module.default !== undefined) {
return module.default
}
return module
})
} catch (importError) {
console.warn('Dynamic import failed:', importError)
throw importError
}
}
}
const PLACEHOLDER_REGEX = /\[(.+?)\]/
const PLACEHOLDER_GLOBAL_REGEX = /\[(.+?)\]/g
/**
* Checks if a directory entry is a middleware file
*
* @param {Object} entry - The directory entry to check (fs.Dirent object)
* @returns {boolean} - True if the entry is a file named '_middleware.js'
*
* @example
* // With a file entry for '_middleware.js'
* const middlewareEntry = { isFile: () => true, name: '_middleware.js' };
* isMiddlewareFile(middlewareEntry); // Returns: true
*
* @example
* // With a directory entry
* const dirEntry = { isFile: () => false, name: '_middleware.js' };
* isMiddlewareFile(dirEntry); // Returns: false
*
* @example
* // With a different file
* const otherFileEntry = { isFile: () => true, name: 'index.js' };
* isMiddlewareFile(otherFileEntry); // Returns: false
*/
function isMiddlewareFile(entry) {
return entry.isFile() && entry.name === '_middleware.js'
}
/**
* Ensures a value is always an array by wrapping non-array values
*
* @param {*} ary - The value to convert to an array
* @returns {Array} - Wraps the value in an array, or if the input was an array already it will return it as is.
*
* @example
* // With a non-array value
* autoBox(5); // Returns: [5]
*
* @example
* // With an array value
* autoBox([1, 2, 3]); // Returns: [1, 2, 3]
*
* @example
* // With null or undefined
* autoBox(null); // Returns: [null]
* autoBox(undefined); // Returns: [undefined]
*
* @example
* // With an object
* autoBox({ key: 'value' }); // Returns: [{ key: 'value' }]
*/
function autoBox(ary) {
return Array.isArray(ary) ? ary : [ary]
}
/**
* Converts URL placeholder syntax [param] to Express parameter syntax :param
*
* @param {string} urlPath - The URL path containing placeholders
* @returns {string} - The URL path with Express-style parameters
*
* @example
* // With single placeholder
* replaceUrlPlaceholders('/users/[id]'); // Returns: '/users/:id'
*
* @example
* // With multiple placeholders
* replaceUrlPlaceholders('/users/[id]/posts/[postId]'); // Returns: '/users/:id/posts/:postId'
*
* @example
* // With no placeholders
* replaceUrlPlaceholders('/users/list'); // Returns: '/users/list'
*
* @example
* // With nested/complex placeholders
* replaceUrlPlaceholders('/products/[category]/[id]/reviews/[reviewId]');
* // Returns: '/products/:category/:id/reviews/:reviewId'
*/
function replaceUrlPlaceholders(urlPath) {
return urlPath.replace(
PLACEHOLDER_GLOBAL_REGEX,
(match, variable) => `:${variable}`
)
}
/**
* Checks if a URL path contains a placeholder
*
* @param {string} urlPath - The URL path to check
* @returns {boolean} - True if the path contains a placeholder
*
* @example
* // With placeholder
* isPlaceholder('/users/[id]'); // Returns: true
*
* @example
* // With multiple placeholders
* isPlaceholder('/users/[id]/posts/[postId]'); // Returns: true
*
* @example
* // Without placeholder
* isPlaceholder('/users/list'); // Returns: false
*
* @example
* // With square brackets in a different context (not a placeholder)
* isPlaceholder('/users/list[all]'); // Returns: true (matches the regex pattern)
*/
function isPlaceholder(urlPath) {
return PLACEHOLDER_REGEX.test(urlPath)
}
/**
* Validates if a path is a non-empty string
*
* @param {string} path - The path to validate
* @throws {Error} If path is not a string or is empty
*
* @example
* // With valid path
* validatePath('/api/users'); // No error thrown
*
* @example
* // With empty string
* try {
* validatePath('');
* } catch (error) {
* console.error(error.message); // Outputs: 'Invalid path provided'
* }
*
* @example
* // With null value
* try {
* validatePath(null);
* } catch (error) {
* console.error(error.message); // Outputs: 'Invalid path provided'
* }
*
* @example
* // With non-string value
* try {
* validatePath(123);
* } catch (error) {
* console.error(error.message); // Outputs: 'Invalid path provided'
* }
*/
function validatePath(path) {
if (typeof path !== 'string' || !path) {
throw new Error('Invalid path provided')
}
}
/**
* Safely joins URL paths without creating double slashes
* Removes trailing slash from base and ensures segment starts with slash
*
* @param {string} base - The base URL path
* @param {string} segment - The path segment to append
* @returns {string} - The joined path without double slashes
*
* @example
* // With base having trailing slash
* joinUrlPaths('/api/', 'users')
* // Returns: '/api/users'
*
* @example
* // With base not having trailing slash
* joinUrlPaths('/api', 'users')
* // Returns: '/api/users'
*
* @example
* // With segment having leading slash
* joinUrlPaths('/api', '/users')
* // Returns: '/api/users'
*
* @example
* // Preventing double slashes
* joinUrlPaths('/api/', '/users')
* // Returns: '/api/users'
*
* @example
* // With empty base (edge case)
* joinUrlPaths('', 'users')
* // Returns: '/users'
*
* @example
* // With empty segment (edge case)
* joinUrlPaths('/api', '')
* // Returns: '/api/'
*
* @example
* // With both empty (edge case)
* joinUrlPaths('', '')
* // Returns: '/'
*/
function joinUrlPaths(base, segment) {
// Remove trailing slash from base if it exists
const cleanBase = base.endsWith('/') ? base.slice(0, -1) : base
// Ensure segment starts with /
const cleanSegment = segment.startsWith('/') ? segment : '/' + segment
return cleanBase + cleanSegment
}
/**
* Parses directory name for priority prefix, extracts route name, and detects route type
*
* @param {string} dirName - Directory name (e.g., "10-users", "users", "05-[id]", "[sessionId]")
* @returns {Object} - { priority: number, name: string, hasPrefix: boolean, isDynamic: boolean }
*
* @example
* // With priority prefix and static route
* parseDirectoryPriority("10-users")
* // Returns: { priority: 10, name: "users", hasPrefix: true, isDynamic: false }
*
* @example
* // With priority prefix and dynamic route
* parseDirectoryPriority("05-[userId]")
* // Returns: { priority: 5, name: "[userId]", hasPrefix: true, isDynamic: true }
*
* @example
* // Without priority prefix (static route)
* parseDirectoryPriority("users")
* // Returns: { priority: 50, name: "users", hasPrefix: false, isDynamic: false }
*
* @example
* // Without priority prefix (dynamic route)
* parseDirectoryPriority("[sessionId]")
* // Returns: { priority: 50, name: "[sessionId]", hasPrefix: false, isDynamic: true }
*
* @example
* // Invalid priority range (falls back to default)
* parseDirectoryPriority("150-invalid")
* // Logs: "Invalid priority prefix detected in directory "150-invalid", using default priority 50"
* // Returns: { priority: 50, name: "150-invalid", hasPrefix: false, isDynamic: false }
*
* @example
* // Invalid priority format (falls back to default)
* parseDirectoryPriority("x5-invalid")
* // Returns: { priority: 50, name: "x5-invalid", hasPrefix: false, isDynamic: false }
*
* @note Logs warning message to console.info when invalid priority prefix is detected (out of 00-99 range)
* @note Valid priority range is 00-99; invalid ranges default to priority 50 with hasPrefix: false
*/
function parseDirectoryPriority(dirName) {
const match = dirName.match(/^(\d{2})-(.+)$/)
if (match) {
const priority = parseInt(match[1], 10)
const name = match[2]
if (priority >= 0 && priority <= 99) {
return {
priority,
name,
hasPrefix: true,
isDynamic: isPlaceholder(name)
}
}
console.info(
`Invalid priority prefix detected in directory "${dirName}", using default priority 50`
)
}
return {
priority: 50, // Default middle priority for non-prefixed directories
name: dirName,
hasPrefix: false,
isDynamic: isPlaceholder(dirName)
}
}
/**
* Normalizes middleware to priority objects with consistent structure
*
* @param {Function|Object|Array} middleware - Middleware function(s) or priority objects
* @param {number} sourceIndex - Original array position for tracking
* @param {string} sourcePath - Source path for specificity tracking
* @returns {Array} Array of {fn, priority, sourceIndex, sourcePath} objects
*
* @example
* // With plain function
* normalizeMiddlewarePriority(corsMiddleware, 0, '/api/')
* // Returns: [{ fn: corsMiddleware, priority: 50, sourceIndex: 0, sourcePath: '/api/' }]
*
* @example
* // With priority object
* normalizeMiddlewarePriority({ fn: authMiddleware, priority: 10 }, 1, '/api/')
* // Returns: [{ fn: authMiddleware, priority: 10, sourceIndex: 1, sourcePath: '/api/' }]
*
* @example
* // With array of mixed types
* normalizeMiddlewarePriority([corsMiddleware, { fn: authMiddleware, priority: 20 }], 0, '/api/')
* // Returns: [
* // { fn: corsMiddleware, priority: 50, sourceIndex: 0, sourcePath: '/api/' },
* // { fn: authMiddleware, priority: 20, sourceIndex: 1, sourcePath: '/api/' }
* // ]
*/
function normalizeMiddlewarePriority(
middleware,
sourceIndex = 0,
sourcePath = ''
) {
const items = Array.isArray(middleware) ? middleware : [middleware]
return items.map((item, index) => {
if (typeof item === 'function') {
return {
fn: item,
priority: 50,
sourceIndex: sourceIndex + index,
sourcePath
}
}
if (item && typeof item.fn === 'function') {
return {
fn: item.fn,
priority: item.priority || 50,
sourceIndex: sourceIndex + index,
sourcePath
}
}
throw new Error(
'Invalid middleware: must be function or {fn, priority} object'
)
})
}
/**
* Sorts and flattens middleware functions by four-level priority system
*
* @param {Array} middlewareArray - Array of {fn, priority, sourceIndex, sourcePath} objects
* @returns {Array} Array of middleware functions sorted by priority
*
* @example
* // With mixed priority middleware
* const middleware = [
* { fn: authMiddleware, priority: 20, sourceIndex: 0, sourcePath: '/api/' },
* { fn: corsMiddleware, priority: 5, sourceIndex: 1, sourcePath: '/api/' },
* { fn: loggingMiddleware, priority: 90, sourceIndex: 0, sourcePath: '/api/users/' }
* ]
* sortMiddlewareFunctions(middleware)
* // Returns: [corsMiddleware, authMiddleware, loggingMiddleware]
*/
function sortMiddlewareFunctions(middlewareArray) {
return middlewareArray
.sort((a, b) => {
// Level 1: Priority (00-99)
if (a.priority !== b.priority) {
return a.priority - b.priority
}
// Level 2: Function name alphabetically (arrow functions get inferred names)
const aName = a.fn.name || ''
const bName = b.fn.name || ''
if (aName !== bName) {
// Anonymous functions ("") come after named functions
if (aName === '' && bName !== '') return 1
if (aName !== '' && bName === '') return -1
return aName.localeCompare(bName)
}
// Level 3: For anonymous functions, sort by original array position
if (aName === '' && bName === '') {
if (a.sourceIndex !== b.sourceIndex) {
return a.sourceIndex - b.sourceIndex
}
}
// Level 4: Source path specificity (longer paths = more specific)
return a.sourcePath.length - b.sourcePath.length
})
.map((item) => item.fn)
}
/**
* Retrieves and sorts middleware functions that match a given path
* Finds all entries in the dictionary where the given path starts with the dictionary key,
* sorts them by key length (shortest first), and returns the flattened array of middleware functions
*
* Supports both legacy middleware format (plain functions) and priority object format ({ fn, priority })
* with backward compatibility. Priority objects are sorted using the four-level priority system.
*
* @param {Object<string, Function|Array<Function>|Array<Object>>} dictionary - Dictionary of paths to middleware functions or priority objects
* @param {string} path - The path to match
* @returns {Array<Function>} - Array of middleware functions that apply to the path, ordered by priority and path specificity
*
* @example
* // With matching paths (legacy format)
* const dict = {
* '/api/': [authMiddleware],
* '/api/users/': [userMiddleware]
* };
* dictionaryKeyStartsWithPath(dict, '/api/users/profile');
* // Returns: [authMiddleware, userMiddleware] (in order from least to most specific)
*
* @example
* // With priority objects (new format)
* const dict = {
* '/api/': [
* { fn: corsMiddleware, priority: 5 },
* { fn: authMiddleware, priority: 20 }
* ],
* '/api/users/': [
* { fn: userValidationMiddleware, priority: 15 }
* ]
* };
* dictionaryKeyStartsWithPath(dict, '/api/users/profile');
* // Returns: [corsMiddleware, userValidationMiddleware, authMiddleware] (sorted by priority)
*
* @example
* // With mixed legacy and priority format (backward compatible)
* const dict = {
* '/api/': [legacyMiddleware, { fn: priorityMiddleware, priority: 10 }],
* '/api/users/': userMiddleware // Single function
* };
* dictionaryKeyStartsWithPath(dict, '/api/users/');
* // Returns: [priorityMiddleware, legacyMiddleware, userMiddleware] (priority objects sorted first)
*
* @example
* // With no matching paths
* const dict = {
* '/api/': [authMiddleware],
* '/api/users/': [userMiddleware]
* };
* dictionaryKeyStartsWithPath(dict, '/admin/');
* // Returns: []
*
* @example
* // With null or undefined values in the dictionary (they are filtered out)
* const dict = {
* '/api/': [authMiddleware, null],
* '/api/users/': undefined
* };
* dictionaryKeyStartsWithPath(dict, '/api/users/');
* // Returns: [authMiddleware]
*
* @note Automatically converts legacy middleware functions to priority objects with default priority 50
* @note Uses four-level sorting: priority → function name → source index → path specificity
*/
function dictionaryKeyStartsWithPath(dictionary, path) {
if (!dictionary || typeof dictionary !== 'object') {
throw new Error('Dictionary must be an object')
}
const allMiddleware = Object.entries(dictionary)
.filter(([key]) => path.startsWith(key))
.sort(([aKey], [bKey]) => aKey.length - bKey.length)
.flatMap(([, value]) => {
const middlewareArray = Array.isArray(value) ? value : [value]
// Check if we have priority objects or plain functions for backward compatibility
return middlewareArray
.map((item, index) => {
if (typeof item === 'function') {
// Legacy format - convert to priority object
return {
fn: item,
priority: 50,
sourceIndex: index,
sourcePath: ''
}
} else if (item && typeof item.fn === 'function') {
// New format - already a priority object
return item
}
return null
})
.filter(Boolean)
})
.filter(Boolean)
return sortMiddlewareFunctions(allMiddleware)
}
/**
* Creates a curried router object with pre-configured URL path and middleware
* Returns a proxy to the original router that applies the given URL path and middleware functions
* to all HTTP method calls (get, post, put, etc.) automatically
*
* @param {Object} router - Express router instance
* @param {string} urlPath - The URL path to be curried
* @param {...Function} initialMiddleWareFunctions - Initial middleware functions to be applied (rest parameter, accepts multiple functions)
* @returns {Object} - Curried router proxy with pre-configured path and middleware
*
* @example
* // Basic usage with a single middleware function
* const router = express.Router();
* const curriedRouter = curryObjectMethods(router, '/users', authMiddleware);
* curriedRouter.get((req, res) => res.json({}));
* // Equivalent to: router.get('/users', authMiddleware, (req, res) => res.json({}));
*
* @example
* // With multiple middleware functions
* const curriedRouter = curryObjectMethods(router, '/posts', authMiddleware, logMiddleware);
* curriedRouter.post((req, res) => res.status(201).json({}));
* // Equivalent to: router.post('/posts', authMiddleware, logMiddleware, (req, res) => res.status(201).json({}));
*
* @example
* // With no middleware
* const curriedRouter = curryObjectMethods(router, '/public');
* curriedRouter.get((req, res) => res.send('Hello'));
* // Equivalent to: router.get('/public', (req, res) => res.send('Hello'));
*
* @example
* // Accessing the original router object
* const curriedRouter = curryObjectMethods(router, '/api');
* const originalRouter = curriedRouter._getOriginalObject();
* // originalRouter is the router instance passed in the first parameter
*/
function curryObjectMethods(router, urlPath, ...initialMiddleWareFunctions) {
const originalRouter = router
const httpMethods = [
'get',
'post',
'put',
'delete',
'patch',
'options',
'head',
'all'
]
const handler = {
get(target, prop) {
const originalHttpMethod = target[prop]
if (
typeof originalHttpMethod === 'function' &&
httpMethods.includes(prop)
) {
return (...remainingFns) => {
const allFns = [...initialMiddleWareFunctions, ...remainingFns]
return originalHttpMethod.call(target, urlPath, ...allFns)
}
}
return originalHttpMethod
}
}
const curriedRouterObject = new Proxy(router, handler)
curriedRouterObject._getOriginalObject = () => originalRouter
return curriedRouterObject
}
/**
* Builds a dictionary of middleware functions from a directory structure
* Recursively scans the given directory for '_middleware.js' files and builds a dictionary
* mapping URL paths to their corresponding middleware functions
*
* @param {string} basePath - Base filesystem path to start scanning
* @param {string} baseURL - Base URL path for the routes
* @param {Object} [options=undefined] - Options that can be passed to all controllers when they are executed.
* @returns {Object<string, Array<Function>>} Dictionary where keys are URL paths and values are arrays of middleware functions
*
* @example
* // Basic directory structure with middleware
* // ./src/routes/_middleware.js -> exports a global middleware
* // ./src/routes/users/_middleware.js -> exports a users-specific middleware
* const middlewares = buildMiddlewareDictionary('./src/routes', '/api');
* // Returns: {
* // '/api/': [globalMiddleware],
* // '/api/users/': [usersMiddleware]
* // }
*
* @example
* // With dynamic route parameters
* // ./src/routes/users/[id]/_middleware.js -> exports a user-specific middleware
* const middlewares = buildMiddlewareDictionary('./src/routes', '/api');
* // Returns: {
* // '/api/': [globalMiddleware],
* // '/api/users/': [usersMiddleware],
* // '/api/users/:id/': [userSpecificMiddleware]
* // }
*
* @example
* // With middleware exporting multiple functions
* // ./src/routes/_middleware.js -> exports [authMiddleware, logMiddleware]
* const middlewares = buildMiddlewareDictionary('./src/routes', '/api');
* // Returns: {
* // '/api/': [authMiddleware, logMiddleware]
* // }
*
* @example
* // With middleware exporting a single function
* // ./src/routes/_middleware.js -> exports singleMiddleware (not in an array)
* const middlewares = buildMiddlewareDictionary('./src/routes', '/api');
* // Returns: {
* // '/api/': [singleMiddleware]
* // }
*/
async function buildMiddlewareDictionary(basePath, baseURL, options) {
if (!statSync(basePath).isDirectory()) {
throw new Error(`Base path "${basePath}" is not a directory`)
}
const dictionary = {}
const traverseDirectory = async (currentPath, currentURL) => {
const dirEntries = readdirSync(currentPath, { withFileTypes: true })
for (const entry of dirEntries) {
const entryPath = path.resolve(currentPath, entry.name)
let entryURL = joinUrlPaths(currentURL, entry.name)
if (entry.isDirectory()) {
const dirInfo = parseDirectoryPriority(entry.name)
const routeName = dirInfo.name
if (isPlaceholder(routeName)) {
entryURL = joinUrlPaths(currentURL, replaceUrlPlaceholders(routeName))
} else {
entryURL = joinUrlPaths(currentURL, routeName)
}
await traverseDirectory(entryPath, entryURL)
} else if (isMiddlewareFile(entry)) {
try {
const middlewareModule = await loadModule(entryPath)
const middleware = middlewareModule(options)
if (
!middleware ||
(typeof middleware !== 'function' && !Array.isArray(middleware))
) {
throw new Error(
`Middleware at ${entryPath} must export a function or array of functions`
)
}
const middlewareURL = entryURL.replace('_middleware.js', '')
const normalizedMiddleware = normalizeMiddlewarePriority(
middleware,
0,
middlewareURL
)
dictionary[middlewareURL] = normalizedMiddleware
} catch (e) {
throw new Error(
`Failed to load middleware at ${entryPath}: ${e.message}`
)
}
}
}
}
await traverseDirectory(basePath, baseURL)
return dictionary
}
/**
* Builds an array of route mappings from a directory structure
* Recursively scans the given directory for 'index.js' files and builds an array of
* URL paths and their corresponding file paths, converting directory placeholders to Express params
*
* @param {string} basePath - Base filesystem path to start scanning
* @param {string} baseURL - Base URL path for the routes
* @returns {Array<Array<string>>} Array of tuples where first element is URL path and second is file path
*
* @example
* // Basic directory structure
* // ./src/routes/users/index.js
* // ./src/routes/posts/index.js
* const routes = buildRoutes('./src/routes', '/api');
* // Returns: [
* // ['/api/users/', './src/routes/users/index.js'],
* // ['/api/posts/', './src/routes/posts/index.js']
* // ]
*
* @example
* // With dynamic route parameters
* // ./src/routes/users/[id]/index.js
* const routes = buildRoutes('./src/routes', '/api');
* // Returns: [
* // ['/api/users/:id/', './src/routes/users/[id]/index.js']
* // ]
*
* @example
* // With nested dynamic routes
* // ./src/routes/users/[userId]/posts/[postId]/index.js
* const routes = buildRoutes('./src/routes', '/api');
* // Returns: [
* // ['/api/users/:userId/posts/:postId/', './src/routes/users/[userId]/posts/[postId]/index.js']
* // ]
*
* @example
* // With root route
* // ./src/routes/index.js
* const routes = buildRoutes('./src/routes', '/api');
* // Returns: [
* // ['/api/', './src/routes/index.js']
* // ]
*/
function buildRoutes(basePath, baseURL) {
if (!statSync(basePath).isDirectory()) {
throw new Error(`Base path "${basePath}" is not a directory`)
}
const result = []
const queue = [[basePath, baseURL.endsWith('/') ? baseURL : baseURL + '/']]
while (queue.length > 0) {
const [currentPath, currentURL] = queue.shift()
const files = readdirSync(currentPath)
const indexFile = files.find((file) => file === 'index.js')
if (indexFile) {
const indexFilePath = path.resolve(currentPath, indexFile)
result.push([currentURL, indexFilePath])
}
const directories = files
.filter((file) => statSync(path.resolve(currentPath, file)).isDirectory())
.map((dir) => path.join(currentPath, dir))
.sort((a, b) => {
const aParsed = parseDirectoryPriority(path.basename(a))
const bParsed = parseDirectoryPriority(path.basename(b))
// Primary sort: by priority (00-99)
if (aParsed.priority !== bParsed.priority) {
return aParsed.priority - bParsed.priority
}
// Secondary sort: static routes before dynamic routes
if (aParsed.isDynamic !== bParsed.isDynamic) {
return aParsed.isDynamic ? 1 : -1
}
// Tertiary sort: alphabetical by name
return aParsed.name.localeCompare(bParsed.name)
})
directories.forEach((dir) => {
const dirInfo = parseDirectoryPriority(path.basename(dir))
const routeName = dirInfo.name // Use name without priority prefix
let entryURL = joinUrlPaths(currentURL, routeName) + '/'
if (isPlaceholder(routeName)) {
entryURL =
joinUrlPaths(currentURL, replaceUrlPlaceholders(routeName)) + '/'
}
queue.push([dir, entryURL])
})
}
return result
}
/**
* Composes Express routes from a directory structure with middleware support.
* This is the main function that processes route mappings, builds middleware dictionaries,
* and configures an Express router with all discovered routes and middleware.
*
* @param {Object} express - The Express module instance
* @param {Array<Object>} routeMappings - Array of route mapping configurations
* @param {string} routeMappings[].basePath - Base filesystem path to start scanning
* @param {string} routeMappings[].baseURL - Base URL path for the routes
* @param {Object} [options] - Configuration options
* @param {Object} [options.routerOptions] - Options for the Express router (default: `{ strict: true }` stay with this for best results but be advised it makes paths require to be terminated with `/` )
* @param {Object} [options.middlewareOptions=undefined] - Options passed to every middleware.
* @param {Object} [options.controllerOptions=undefined] - Options passed to every controller.
* @returns {Object} Configured Express router with applied routes
*
* @example
* // Basic usage with a single route mapping
* const express = require('express');
* const app = express();
*
* const router = composeRoutes(express, [
* {
* basePath: './src/routes',
* baseURL: '/api'
* }
* ]);
*
* app.use(router);
* // This will set up all routes found in './src/routes' with their middleware
*
* @example
* // With multiple route mappings
* const router = composeRoutes(express, [
* {
* basePath: './src/api/routes',
* baseURL: '/api'
* },
* {
* basePath: './src/admin/routes',
* baseURL: '/admin'
* }
* ]);
*
* @example
* // With custom router options
* const router = composeRoutes(express, [
* {
* basePath: './src/routes',
* baseURL: '/api'
* }
* ], {
* routerOptions: {
* strict: true,
* }
* });
*
* @example
* // With an existing router instance
* const existingRouter = express.Router();
* const router = composeRoutes(express, [
* {
* basePath: './src/routes',
* baseURL: '/api'
* }
* ], {
* router: existingRouter
* });
*/
async function composeRoutes(
express,
routeMappings,
options = {
routerOptions: { strict: true },
middlewareOptions: undefined,
controllerOptions: undefined
}
) {
if (!Array.isArray(routeMappings)) {
routeMappings = [routeMappings]
}
const routerOptions = options.routerOptions || { strict: true }
const middlewareOptions = options.middlewareOptions || undefined
const controllerOptions = options.controllerOptions || undefined
const router = express.Router(routerOptions)
for (const { basePath, baseURL } of routeMappings) {
validatePath(basePath)
validatePath(baseURL)
const middlewareFunctionDictionary = await buildMiddlewareDictionary(
basePath,
baseURL,
middlewareOptions
)
const routes = buildRoutes(basePath, baseURL)
for (const [url, filepath] of routes) {
// curry the Router, so that the URL is set to the route, and the Middleware is loaded.
let curriedRouter = curryObjectMethods(
router,
url,
...dictionaryKeyStartsWithPath(middlewareFunctionDictionary, url)
)
const controllerModule = await loadModule(filepath)
const controllers = controllerModule
if (typeof controllers !== 'function') {
throw new Error(`Controller at ${filepath} must export a function`)
}
curriedRouter = controllers(curriedRouter, controllerOptions)
if (!curriedRouter?._getOriginalObject) {
throw new Error(
`Controller at ${filepath} did not return a valid router (returned: ${curriedRouter})`
)
}
}
}
return router
}
// Define exports object
const moduleExports = {
loadModule,
isMiddlewareFile,
autoBox,
replaceUrlPlaceholders,
isPlaceholder,
validatePath,
joinUrlPaths,
parseDirectoryPriority,
normalizeMiddlewarePriority,
sortMiddlewareFunctions,
dictionaryKeyStartsWithPath,
curryObjectMethods,
buildMiddlewareDictionary,
buildRoutes,
composeRoutes,
default: composeRoutes // For ESM default import compatibility and unfortuntely, rollup does not allow any way of exporting otherwise.
}
// Export for CommonJS
module.exports = moduleExports
//
// // CommonJS exports with ESM compatibility
// const moduleExports = {
// loadModule,
// isMiddlewareFile,
// autoBox,
// replaceUrlPlaceholders,
// isPlaceholder,
// validatePath,
// joinUrlPaths,
// parseDirectoryPriority,
// normalizeMiddlewarePriority,
// sortMiddlewareFunctions,
// dictionaryKeyStartsWithPath,
// curryObjectMethods,
// buildMiddlewareDictionary,
// buildRoutes,
// composeRoutes,
// default: composeRoutes // Main export for ESM compatibility
// }
//
// // Export all named functions
// Object.assign(module.exports, moduleExports)
// // Also export as default for ESM compatibility
// module.exports.default = composeRoutes