UNPKG

rasengan

Version:

The modern React Framework

326 lines (325 loc) 11.5 kB
import { DefaultLayout } from '../components/template.js'; import { RouterComponent } from '../interfaces.js'; const basePath = '/src/app/_routes/'; /** * Normalize a segment * @param segment Segment to normalize * @returns Normalized segment */ function normalizeSegment(segment) { // Handle index if (segment === 'index') return '.'; // eg. index => . // Handle dynamic segments if (segment.startsWith('[') && segment.endsWith(']')) { const param = segment.slice(1, -1); // eg. [locale] => locale if (param.at(0) === '_') return ':' + param.slice(1) + '?'; // eg. _locale => :locale? return ':' + param; } // Handling optional static segment if (segment.length > 1 && segment.at(0) === '_') return segment.slice(1) + '?'; // eg. _edit => edit? return segment; } /** * Get path segments from file path * @param filePath File path * @param foldersOnly Whether to return only folders * @returns Path segments */ export function getPathSegments(filePath, foldersOnly = false) { const relative = filePath.replace(basePath, ''); // eg. /src/app/_routes/docs/layout.tsx => docs/layout.tsx if (!foldersOnly) { let withoutExtension = ''; if (relative.includes('layout.')) { withoutExtension = relative.replace(/(layout)\.(js|ts|jsx|tsx)$/, '_'); // eg. docs/layout.tsx => docs/_ } else { withoutExtension = relative.replace(/\.(page)\.(js|ts|jsx|tsx|mdx|md)$/, ''); // eg. docs/index.page.tsx => docs/index } return withoutExtension.split('/').map(normalizeSegment).filter(Boolean); } return relative .split('/') .filter((segment) => !segment.includes('.')) // ignore last segment (file name) .map(normalizeSegment) .filter(Boolean); } /** * Generate the skeleton tree from modules * @param tree Tree to generate * @param modules Modules to generate tree from */ function generateSkeletonTree(tree, modules) { let currentLevel = tree; const root = { path: '/', fullPath: '/', segment: '_', isLayout: true, children: [], source: '', }; currentLevel.push(root); currentLevel = root.children ?? []; // change level for (const [, { segments }] of modules) { let tmpLevel = currentLevel; let fullPath = ''; for (const segment of segments) { if (!(segment.startsWith('(') && segment.endsWith(')'))) { fullPath += '/' + segment; } else { if (fullPath === '') { fullPath = '/'; } } const existing = tmpLevel.find((n) => n.segment === segment); if (existing) { tmpLevel = existing.children; // change level continue; } const node = { path: fullPath, fullPath, segment, isLayout: false, children: [], source: '', }; tmpLevel.push(node); tmpLevel = node.children ?? []; // change level } } } function insertNodeToTree(tree, segments, routeInfo) { let currentNode = tree[0]; let currentLevel = tree[0].children; let currentLayout = currentNode; // Handle the root layout if (segments.length === 1 && segments[0] === '_') { currentNode.isLayout = true; currentNode.component = routeInfo.component; // Has to be considered in the case where the developer doesn't provide a layout currentNode.metadata = routeInfo.metadata; currentNode.module = routeInfo.module; currentNode.source = routeInfo.source; return; } let fullPath = ''; for (let i = 0; i < segments.length; i++) { const segment = segments[i]; // We reached the end of the path if (segment === '.') break; const node = currentLevel.find((n) => n.segment === segment); if (node) { // Go to the next node currentNode = node; currentLevel = node.children; fullPath = node.fullPath; if (node.isLayout) { currentLayout = node; } } } if (routeInfo.isLayout) { currentNode.isLayout = true; currentNode.component = routeInfo.component; currentNode.metadata = routeInfo.metadata; currentNode.module = routeInfo.module; currentNode.source = routeInfo.source; } else { let path = ''; // The case where we create an index page directly at the root of _routes folder if (segments.length === 1 && segments[0] === '.') { path = '/'; } else { const segment = segments.at(-1); if (segment === '.') { path = fullPath; } else { path = fullPath + '/' + segment; } } if (currentLayout.path !== '/') { const position = path.indexOf(currentLayout.path); if (position !== -1) { path = path.slice(position + currentLayout.path.length); if (path === '') { path = '/'; } } } const lastSegment = segments.at(-1); const node = { path, fullPath: fullPath + '/' + (lastSegment === '.' ? '' : lastSegment), segment: lastSegment, isLayout: false, module: routeInfo.module, source: routeInfo.source, }; currentLevel.push(node); } } /** * This function receives a tree of routes and generate a router component * @param tree Tree of routes * @returns Router component */ async function generateRouter(tree) { const root = tree[0]; // Generate the base router const router = new RouterComponent(); // use default layout if not defined let layout; // Get layout if defined if (root.isLayout) { if (root.source) { layout = root; } else { layout = root.component; layout.path = root.path; layout.metadata = root.metadata; } } // Get pages const { routes: pages, routers } = await generateRoutes(root.children); // Add pages to the router router.pages = pages; router.routers = routers; // Add layout to the router router.layout = layout; router.useParentLayout = true; return router; } /** * This function receives a tree of routes and generate a list of pages and routers * @param tree Tree of routes * @returns List of pages and routers */ async function generateRoutes(tree) { try { const routes = []; const routers = []; for (const node of tree) { // Handle page if (!node.isLayout && node.module) { routes.push(node); continue; } // Handle layout if (node.isLayout) { let layout; if (node.source) { layout = node; } else { layout = node.component; if (!layout) { console.warn(`Layout component is not defined for route: ${node.path}`); continue; } layout.path = node.path; layout.metadata = node.metadata; layout.source = node.source; } if (node.children) { // Loop through children const { routes: subRoutes, routers: subRouters } = await generateRoutes(node.children); // Create a new router const router = new RouterComponent(); router.layout = layout; router.routers = subRouters; router.pages = subRoutes; router.useParentLayout = true; routers.push(router); } continue; } if (node.children) { // Handle intermediate node (folders) const { routes: subRoutes, routers: subRouters } = await generateRoutes(node.children); // Add sub routes and sub routers routes.push(...subRoutes); routers.push(...subRouters); } } return { routes, routers, }; } catch (error) { console.error(error); throw error; } } /** * This function receives a function that returns a record of modules and generate a router component * @param fn Function that returns a record of modules * @returns Router component */ export async function flatRoutes(fn) { try { let modules = fn(); const tree = []; const foldersMap = new Map(); const modulesMap = new Map(); // Map the modules and extract segments for folders and modules (layouts and pages) for (const [filePath, mod] of Object.entries(modules)) { const foldersSegments = getPathSegments(filePath, true); const modulesSegments = getPathSegments(filePath); foldersMap.set(filePath, { segments: foldersSegments, mod }); modulesMap.set(filePath, { segments: modulesSegments, mod }); } // Generate the skeleton tree containing just folders as nodes generateSkeletonTree(tree, foldersMap); // Filter out layouts const layoutModulesMap = new Map([...modulesMap.entries()].filter(([filePath]) => filePath.includes('layout.'))); // Filter out pages const pageModulesMap = new Map([...modulesMap.entries()].filter(([filePath]) => filePath.includes('.page.'))); const isRootLayoutExists = Object.keys(layoutModulesMap).find((path) => [ `${basePath}layout.ts`, `${basePath}layout.tsx`, `${basePath}layout.jsx`, `${basePath}layout.js`, ].includes(path)); // Handle the case where modules are empty if (!isRootLayoutExists) { insertNodeToTree(tree, ['_'], { component: DefaultLayout, metadata: {}, isLayout: true, }); } // Insert every layout into the tree for (const [filePath, { segments, mod }] of layoutModulesMap) { insertNodeToTree(tree, segments, { module: mod, // Only present for lazy routes, instead prefer component attribute isLayout: true, source: filePath, }); } // Insert every pages into the tree for (const [filePath, { segments, mod }] of pageModulesMap) { insertNodeToTree(tree, segments, { module: mod, // Only present for lazy routes, instead prefer component attribute isLayout: false, source: filePath, }); } // Convert the tree into a router component instance const router = await generateRouter(tree); return router; } catch (error) { console.error(error); throw error; } }