burger-api
Version:
<div align="center"> <a href="https://burger-api.com"> <img src="https://github.com/user-attachments/assets/0d9b376e-1d89-479a-aa7f-e7ee3c6b2342" alt="BurgerAPI"/> </a> </div>
166 lines (165 loc) • 7.52 kB
JavaScript
// Import stuff from node
import { readdirSync } from 'fs';
import * as path from 'path';
// Import utils
import { cleanPrefix, normalizePath, compareRoutes, ROUTE_CONSTANTS, } from '../utils/index';
import { filePathToPageRoutePath } from '../utils/pathConversion';
/**
* PageRouter class for handling file-based page routing.
* Loads pages from a directory structure and matches requests to the appropriate page handlers.
* Supports dynamic segments (e.g., [id]) and uses default exports as page handlers.
*/
export class PageRouter {
pagesDir;
prefix;
/** Array of loaded page definitions */
pages = [];
/**
* Constructor for the PageRouter class.
* @param pagesDir The directory path where page modules are located.
* @param prefix Optional prefix to prepend to all routes (e.g., "pages" becomes "/pages/...").
*/
constructor(pagesDir, prefix = '') {
this.pagesDir = pagesDir;
this.prefix = prefix;
if (!pagesDir) {
throw new Error('Pages directory path must be provided');
}
// Normalize the pagesDir path
this.pagesDir = path.normalize(pagesDir);
// Normalize the prefix if provided
if (prefix) {
this.prefix = cleanPrefix(prefix);
}
}
/**
* Loads page modules from the specified directory and adds them to the pages array.
* After loading, sorts the pages to prioritize static routes over dynamic ones based on specificity.
* @returns A promise that resolves when all page modules have been loaded and sorted.
*/
async loadPages() {
// Clear the pages array
this.pages = [];
try {
await this.scanDirectory(this.pagesDir);
// Sort pages to ensure static routes are matched before dynamic ones
this.pages.sort((a, b) => compareRoutes(a, b));
}
catch (error) {
console.error('Failed to load pages:', error);
throw new Error(`Failed to load pages: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Recursively scans the directory for page modules and adds them to the pages array.
* @param dir The current directory to scan.
* @param basePath The base path for constructing the route path.
*/
async scanDirectory(dir, basePath = '') {
// Track if a dynamic folder has been found at this directory level
let dynamicFolderFound = false;
try {
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const entryPath = path.join(dir, entry.name);
const relativePath = path.join(basePath, entry.name);
if (entry.isDirectory()) {
if (entry.name.startsWith(ROUTE_CONSTANTS.WILDCARD_START)) {
continue;
}
// Handle dynamic directories (e.g., [id])
if (entry.name.startsWith(ROUTE_CONSTANTS.DYNAMIC_FOLDER_START) &&
entry.name.endsWith(ROUTE_CONSTANTS.DYNAMIC_FOLDER_END)) {
if (dynamicFolderFound) {
throw new Error(`Multiple dynamic page folders found in the same directory: '${entry.name}' conflicts with another dynamic folder.`);
}
dynamicFolderFound = true;
}
await this.scanDirectory(entryPath, relativePath);
}
else if (entry.isFile() &&
ROUTE_CONSTANTS.SUPPORTED_PAGE_EXTENSIONS.some((ext) => entry.name.endsWith(ext))) {
// Convert file path to route path and load the module
const cleanedRoutePath = filePathToPageRoutePath(relativePath, this.prefix);
// Get the module path
const modulePath = path.resolve(entryPath);
try {
// Import the module
const pageModule = await import(modulePath);
// Get the default export as the page handler
if (entry.name.endsWith('.tsx') &&
typeof pageModule.default !== 'function') {
throw new Error(`Page at ${entryPath} must export a default function as its handler.`);
}
// Create page definition
const pageDefWithSlash = {
path: cleanedRoutePath + '/',
handler: pageModule.default,
middleware: pageModule.middleware,
};
// Create page definition
const pageDef = {
path: cleanedRoutePath,
handler: pageModule.default,
middleware: pageModule.middleware,
};
// Add the page definition to the pages array
this.pages.push(pageDefWithSlash, pageDef);
}
catch (importError) {
console.error(`Failed to import module at ${modulePath}:`, importError);
}
}
}
}
catch (error) {
console.error(`Error scanning directory ${dir}:`, error);
throw error;
}
}
/**
* Resolves the given request by finding a matching page and extracting dynamic parameters.
* @param request The request to resolve.
* @returns An object containing the matched page and parameters, or an empty params object if no match.
*/
resolve(request) {
const url = new URL(request.url);
const reqPath = normalizePath(url.pathname);
console.debug(`Resolving route for path: ${reqPath}`);
for (const page of this.pages) {
const match = this.matchRoute(reqPath, page.path);
if (match) {
console.debug(`Route matched: ${page.path} with params:`, match);
return { page, params: match };
}
}
console.debug(`No matching route found for path: ${reqPath}`);
return { params: {} };
}
/**
* Checks if the request path matches the page path, extracting dynamic parameters if matched.
* @param requestPath The request path to check.
* @param pagePath The page path to match against.
* @returns A record of dynamic parameters if matched, otherwise null.
*/
matchRoute(requestPath, pagePath) {
const reqSegments = requestPath.split('/').filter(Boolean);
const pageSegments = pagePath.split('/').filter(Boolean);
if (reqSegments.length !== pageSegments.length) {
return null;
}
const params = {};
for (let i = 0; i < reqSegments.length; i++) {
const pSegment = pageSegments[i];
const reqSegment = reqSegments[i];
if (pSegment.startsWith(ROUTE_CONSTANTS.DYNAMIC_SEGMENT_PREFIX)) {
const paramName = pSegment.slice(ROUTE_CONSTANTS.DYNAMIC_SEGMENT_PREFIX.length);
params[paramName] = reqSegment;
}
else if (pSegment !== reqSegment) {
return null;
}
}
return params;
}
}