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
JavaScript
;
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