UNPKG

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
// 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; } }