UNPKG

fs-routes-next

Version:

Enhanced file-based routing for React Router 7+ with nested directories, automatic layout inheritance, and layout overrides

288 lines (282 loc) 10.7 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { flatRoutes: () => enhancedFlatRoutes }); module.exports = __toCommonJS(index_exports); var import_fs_routes = require("@react-router/fs-routes"); var import_path2 = require("path"); // src/layouts/resolver.ts var import_routes = require("@react-router/dev/routes"); // src/utils/index.ts var generateRouteId = (filePath) => { const cleanPath = filePath.replace("./routes/", "").replace(/\.(tsx?|jsx?)$/, ""); return `routes.${cleanPath}`.replace(/\//g, "."); }; var getLayoutName = (fileName) => { if (!fileName.startsWith("_")) return null; return fileName.slice(1); }; var shouldLayoutApplyToRoute = (routePath, layoutInfo) => { const layoutFileName = layoutInfo.file.split("/").pop()?.replace(/\.tsx?$/, "") || ""; const layoutName = getLayoutName(layoutFileName); if (!layoutName) return false; const routeSegments = routePath.split("/").filter(Boolean); if (layoutInfo.path === "") { const firstSegment = routeSegments[0] || ""; return firstSegment === layoutName || routePath.startsWith(`${layoutName}/`); } const layoutSegments = layoutInfo.path.split("/").filter(Boolean); const isDeeper = routeSegments.length > layoutSegments.length; return isDeeper && routePath.startsWith(`${layoutInfo.path}/`); }; var findLayoutForRoute = (routePath, directoryPrefix, layoutsByPath) => { let mostSpecificLayout = null; for (const [layoutPath, layoutInfo] of layoutsByPath.entries()) { if (!layoutPath.startsWith(`${directoryPrefix}/`)) continue; if (shouldLayoutApplyToRoute(routePath, layoutInfo)) { if (!mostSpecificLayout || layoutInfo.path.split("/").length > mostSpecificLayout.path.split("/").length) { mostSpecificLayout = layoutInfo; } } } return mostSpecificLayout; }; // src/layouts/resolver.ts var buildLayoutHierarchy = (routes, existingRoutes, layoutsByPath) => { const layoutRoutes = /* @__PURE__ */ new Map(); const existingPaths = /* @__PURE__ */ new Set(); const addExistingPaths = (routeList) => { for (const route2 of routeList) { if (route2.path) { existingPaths.add(route2.path); } if (route2.children) { addExistingPaths(route2.children); } } }; addExistingPaths(existingRoutes); for (const [layoutPath, layoutInfo] of layoutsByPath.entries()) { const layoutPattern = layoutInfo.directoryPrefix.replace(/^_app\./, "").replace(/\./g, "/"); const fullLayoutPattern = layoutInfo.path ? `${layoutPattern}/${layoutInfo.path}` : layoutPattern; const isRootLayoutInDirectory = layoutInfo.path === ""; if (!isRootLayoutInDirectory && existingPaths.has(fullLayoutPattern)) { continue; } const layoutId = `routes/${layoutPath.replace(/\//g, ".")}`.replace( /\.tsx$/, "" ); let layoutRoute; if (isRootLayoutInDirectory) { layoutRoute = { id: layoutId, file: `./routes/${layoutPath}`, children: [] }; } else { const routeConfig = (0, import_routes.route)(fullLayoutPattern, `./routes/${layoutPath}`); layoutRoute = { ...routeConfig, id: layoutId, children: [] }; } layoutRoutes.set(layoutPath, layoutRoute); } const rootRoutes = []; for (const routeConfig of routes) { const layout = findLayoutForRoute( routeConfig.originalPath || "", routeConfig.directoryPrefix || "", layoutsByPath ); if (layout) { const layoutPath = `${routeConfig.directoryPrefix}/${layout.file}`; const layoutRoute = layoutRoutes.get(layoutPath); if (layoutRoute) { if (!routeConfig.id) { routeConfig.id = generateRouteId(routeConfig.file); } const layoutBasePath = layout.path; const routeOriginalPath = routeConfig.originalPath || ""; if (layoutBasePath && routeOriginalPath.startsWith(`${layoutBasePath}/`)) { routeConfig.path = routeOriginalPath.slice(layoutBasePath.length + 1); } else if (layoutBasePath && routeOriginalPath === layoutBasePath) { routeConfig.path = void 0; routeConfig.index = true; } else if (layoutBasePath) { routeConfig.path = routeOriginalPath.startsWith(`${layoutBasePath}/`) ? routeOriginalPath.slice(layoutBasePath.length + 1) : routeOriginalPath; } if (!layoutRoute.children) layoutRoute.children = []; layoutRoute.children.push(routeConfig); } else { if (!routeConfig.id) { routeConfig.id = generateRouteId(routeConfig.file); } rootRoutes.push(routeConfig); } } else { if (!routeConfig.id) { routeConfig.id = generateRouteId(routeConfig.file); } rootRoutes.push(routeConfig); } } for (const [, layoutRoute] of layoutRoutes.entries()) { if (layoutRoute.children && layoutRoute.children.length > 0) { rootRoutes.push(layoutRoute); } } return rootRoutes; }; // src/routes/scanner.ts var import_fs = require("fs"); var import_path = require("path"); var scanDirectory = (dirPath, relativePath = "") => { const routes = []; try { const items = (0, import_fs.readdirSync)(dirPath); for (const item of items) { const itemPath = (0, import_path.join)(dirPath, item); const stat = (0, import_fs.statSync)(itemPath); if (stat.isFile() && (item.endsWith(".tsx") || item.endsWith(".ts"))) { const routeName = item.replace(/\.(tsx?|jsx?)$/, ""); const isLayout = routeName.startsWith("_"); if (routeName === "index" && !relativePath) continue; if (isLayout) { routes.push({ path: relativePath, file: (0, import_path.join)(relativePath, item).replace(/\\/g, "/"), isLayout: true }); } else if (routeName === "index") { routes.push({ path: relativePath, file: (0, import_path.join)(relativePath, item).replace(/\\/g, "/"), isLayout: false }); } else { let routePath; if (routeName.startsWith("$")) { const paramName = routeName.slice(1); const paramRoute = `:${paramName}`; routePath = relativePath ? `${relativePath}/${paramRoute}` : paramRoute; } else { routePath = relativePath ? `${relativePath}/${routeName}` : routeName; } routes.push({ path: routePath, file: (0, import_path.join)(relativePath, item).replace(/\\/g, "/"), isLayout: false }); } } else if (stat.isDirectory()) { const subPath = relativePath ? `${relativePath}/${item}` : item; routes.push(...scanDirectory(itemPath, subPath)); } } } catch (_error) { } return routes; }; var scanNestedRoutes = (routesDir) => { const nestedRoutesByLayout = /* @__PURE__ */ new Map(); const layoutsByPath = /* @__PURE__ */ new Map(); try { const items = (0, import_fs.readdirSync)(routesDir); for (const item of items) { const itemPath = (0, import_path.join)(routesDir, item); const stat = (0, import_fs.statSync)(itemPath); if (stat.isDirectory() && item.startsWith("_")) { const layoutPrefix = item.split(".")[0]; const routes = scanDirectory(itemPath); const layoutRoutes = routes.filter((r) => r.isLayout); const regularRoutes = routes.filter((r) => !r.isLayout); for (const layoutRoute of layoutRoutes) { const layoutPath = `${item}/${layoutRoute.file}`; layoutsByPath.set(layoutPath, { path: layoutRoute.path, file: layoutRoute.file, directoryPrefix: item }); } for (const routeInfo of regularRoutes) { const routePattern = item.replace(/^_app\./, "").replace(/\./g, "/"); const fullRoutePattern = `${routePattern}/${routeInfo.path}`; if (!nestedRoutesByLayout.has(layoutPrefix)) { nestedRoutesByLayout.set(layoutPrefix, []); } nestedRoutesByLayout.get(layoutPrefix).push({ path: fullRoutePattern, file: `./routes/${item}/${routeInfo.file}`, originalPath: routeInfo.path, directoryPrefix: item }); } } } } catch (error) { console.warn("Could not scan routes directory:", error); } return { nestedRoutesByLayout, layoutsByPath }; }; // src/index.ts var enhancedFlatRoutes = async () => { const standardRoutes = await (0, import_fs_routes.flatRoutes)(); const routesDir = (0, import_path2.join)(process.cwd(), "app/routes"); const { nestedRoutesByLayout, layoutsByPath } = scanNestedRoutes(routesDir); const enhanceRoutes = (routes) => { return routes.map((routeConfig) => { if (routeConfig.children) { routeConfig.children = enhanceRoutes(routeConfig.children); } if (routeConfig.file) { const layoutMatch = routeConfig.file.match(/^routes\/(_[^.]+)\.tsx?$/); if (layoutMatch) { const layoutPrefix = layoutMatch[1]; const additionalRoutes = nestedRoutesByLayout.get(layoutPrefix) || []; if (additionalRoutes.length > 0) { const hierarchicalRoutes = buildLayoutHierarchy( additionalRoutes, standardRoutes, layoutsByPath ); routeConfig.children = [ ...routeConfig.children || [], ...hierarchicalRoutes ]; nestedRoutesByLayout.delete(layoutPrefix); } } } return routeConfig; }); }; const enhancedRoutes = enhanceRoutes(standardRoutes); return enhancedRoutes; }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { flatRoutes }); //# sourceMappingURL=index.js.map