rr-next-routes
Version:
generate nextjs-style routes in your react-router-v7 application
273 lines (270 loc) • 9.74 kB
JavaScript
// src/common/next-routes-common.ts
import { readdirSync, statSync } from "node:fs";
import { join, parse, relative, resolve } from "node:path";
// src/common/utils.ts
function transformRoutePath(path) {
let transformedPath = path.replace(/\[\[\s*([^\]]+)\s*]]/g, ":$1?").replace(/\[\.\.\.\s*([^\]]+)\s*]/g, "*").replace(/\[([^\]]+)]/g, ":$1").replace(/\/\([^)]*\)\//g, "/").replace(/\{([^}]+)\}/g, "$1").replace(/\/\([^)]*\)/g, "");
if (transformedPath === "") {
transformedPath = "/" + transformedPath;
}
return transformedPath;
}
function isHoistedFolder(name) {
return /^{[^{}]+}$/.test(name);
}
function parseDynamicRoute(name) {
const paramMatch = name.match(/\[(.+?)]/);
if (!paramMatch) return { routeName: name };
const paramName = paramMatch[1];
return {
paramName,
routeName: `:${paramName}`
};
}
function parseCatchAllParam(name) {
const paramMatch = name.match(/\[\.\.\.(.+?)]/);
if (!paramMatch) return { routeName: name };
const paramName = paramMatch[1];
return {
paramName,
routeName: "*"
// Placeholder to indicate "catch-all" route for now
};
}
function parseOptionalDynamicRoute(name) {
const paramMatch = name.match(/\[\[(.+?)]]/);
if (!paramMatch) return { routeName: name };
const paramName = paramMatch[1];
return {
paramName,
routeName: `:${paramName}?`
};
}
function parseParameter(name) {
if (name.startsWith("[[") && name.endsWith("]]")) {
return parseOptionalDynamicRoute(name);
} else if (name.startsWith("[...") && name.endsWith("]")) {
return parseCatchAllParam(name);
} else if (name.startsWith("[") && name.endsWith("]")) {
return parseDynamicRoute(name);
} else {
return { routeName: name };
}
}
function deepSortByPath(value) {
if (Array.isArray(value)) {
return value.map(deepSortByPath).sort((a, b) => compareByPath(a, b));
}
if (typeof value === "object" && value !== null) {
if ("path" in value) {
return {
...value,
children: value.children ? deepSortByPath(value.children) : void 0
};
}
return Object.keys(value).sort().reduce((acc, key) => {
acc[key] = deepSortByPath(value[key]);
return acc;
}, {});
}
return value;
}
function compareByPath(a, b) {
const pathA = a.path || "";
const pathB = b.path || "";
const aHoisted = a.file?.includes("/{");
const bHoisted = b.file?.includes("/{");
if (aHoisted && !bHoisted) return -1;
if (!aHoisted && bHoisted) return 1;
return pathA.localeCompare(pathB);
}
function printRoutesAsTable(routes) {
function extractRoutesForTable(routes2, parentLayout = null) {
const result = [];
const sortedRoutes = routes2.sort((a, b) => {
const pathA = a.path ?? "";
const pathB = b.path ?? "";
return pathA.localeCompare(pathB);
});
const pathsFirst = sortedRoutes.filter((route2) => route2.path);
const layoutsLast = sortedRoutes.filter((route2) => !route2.path && route2.children);
pathsFirst.forEach((route2) => {
result.push({
routePath: route2.path,
routeFile: route2.file,
parentLayout: parentLayout ?? void 0
});
});
layoutsLast.forEach((layout2) => {
result.push({
routePath: "(layout)",
routeFile: layout2.file,
parentLayout: parentLayout ?? void 0
});
if (layout2.children) {
const layoutChildren = extractRoutesForTable(layout2.children, layout2.file);
result.push(...layoutChildren);
}
});
return result;
}
console.groupCollapsed("\u2705 Generated Routes Table (open to see generated routes)");
console.table(extractRoutesForTable(routes));
console.groupEnd();
}
function printRoutesAsTree(routes, indent = 0) {
function printRouteTree(routes2, indent2 = 0) {
const indentation = " ".repeat(indent2);
const sortedRoutes = routes2.sort((a, b) => {
const pathA = a.path ?? "";
const pathB = b.path ?? "";
return pathA.localeCompare(pathB);
});
const pathsFirst = sortedRoutes.filter((route2) => route2.path);
const layoutsLast = sortedRoutes.filter((route2) => !route2.path && route2.children);
pathsFirst.forEach((route2) => {
const routePath = `"${route2.path}"`;
console.log(`${indentation}\u251C\u2500\u2500 ${routePath} (${route2.file})`);
});
layoutsLast.forEach((route2) => {
console.log(`${indentation}\u251C\u2500\u2500 (layout) (${route2.file})`);
if (route2.children) {
printRouteTree(route2.children, indent2 + 1);
}
});
}
console.groupCollapsed("\u2705 Generated Route Tree (open to see generated routes)");
printRouteTree(routes, indent);
console.groupEnd();
}
// src/common/next-routes-common.ts
var appRouterStyle = {
folderName: "",
print: "info",
layoutFileName: "layout",
routeFileNames: ["page", "route"],
// in nextjs this is the difference between a page (with components) and an api route (without components). in react-router an api route (resource route) just does not export a default component.
extensions: [".tsx", ".ts", ".jsx", ".js"],
routeFileNameOnly: true,
// all files with names different from routeFileNames get no routes
enableHoistedFolders: false
};
var pageRouterStyle = {
folderName: "pages",
print: "info",
layoutFileName: "_layout",
//layouts do no exist like that in nextjs pages router so we use a special file.
routeFileNames: ["index"],
extensions: [".tsx", ".ts", ".jsx", ".js"],
routeFileNameOnly: false,
// all files without a leading underscore get routes as long as the extension matches
enableHoistedFolders: false
};
var defaultOptions = appRouterStyle;
function createRouteConfig(name, parentPath, relativePath, folderName, routeFileNames, indexCreator, routeCreator) {
if (routeFileNames.includes(name) && folderName.startsWith("[") && folderName.endsWith("]")) {
const { routeName: routeName2 } = parseParameter(folderName);
const routePath2 = parentPath === "" ? routeName2 : `${parentPath.replace(folderName, "")}${routeName2}`;
return routeCreator(transformRoutePath(routePath2), relativePath);
}
if (routeFileNames.includes(name)) {
if (parentPath === "") {
return indexCreator(relativePath);
} else {
return routeCreator(transformRoutePath(parentPath), relativePath);
}
}
const { routeName } = parseParameter(name);
const routePath = parentPath === "" ? `/${routeName}` : `${parentPath}/${routeName}`;
return routeCreator(transformRoutePath(routePath), relativePath);
}
function generateNextRoutes(options = defaultOptions, getAppDir, indexCreator, routeCreator, layoutCreator) {
const {
folderName: baseFolder = defaultOptions.folderName,
print: printOption = defaultOptions.print,
extensions = defaultOptions.extensions,
layoutFileName = defaultOptions.layoutFileName,
routeFileNames = defaultOptions.routeFileNames,
routeFileNameOnly = defaultOptions.routeFileNameOnly,
enableHoistedFolders = defaultOptions.enableHoistedFolders
} = options;
let appDirectory = getAppDir();
const pagesDir = resolve(appDirectory, baseFolder);
function scanDir(dirPath) {
return {
folderName: parse(dirPath).base,
files: readdirSync(dirPath).sort((a, b) => {
const { ext: aExt, name: aName } = parse(a);
return routeFileNames.includes(aName) && extensions.includes(aExt) ? -1 : 1;
})
};
}
function scanDirectory(dir, parentPath = "") {
const routes = [];
const { files, folderName } = scanDir(dir);
const layoutFile = files.find((item) => {
const { ext, name } = parse(item);
return name === layoutFileName && extensions.includes(ext);
});
const currentLevelRoutes = [];
files.forEach((item) => {
if (item.startsWith("_")) return;
const fullPath = join(dir, item);
const stats = statSync(fullPath);
const { name, ext, base } = parse(item);
const relativePath = join(baseFolder, relative(pagesDir, fullPath));
if (layoutFileName && name === layoutFileName) return;
if (stats.isDirectory()) {
const nestedRoutes = scanDirectory(fullPath, `${parentPath}/${base}`);
(layoutFile && !(enableHoistedFolders && isHoistedFolder(name)) ? currentLevelRoutes : routes).push(...nestedRoutes);
} else if (extensions.includes(ext)) {
if (routeFileNameOnly && !routeFileNames.includes(name)) return;
const routeConfig = createRouteConfig(name, parentPath, relativePath, folderName, routeFileNames, indexCreator, routeCreator);
(layoutFile ? currentLevelRoutes : routes).push(routeConfig);
}
});
if (layoutFile) {
const layoutPath = join(baseFolder, relative(pagesDir, join(dir, layoutFile)));
routes.push(layoutCreator(layoutPath, currentLevelRoutes));
} else {
routes.push(...currentLevelRoutes);
}
return routes;
}
const results = scanDirectory(pagesDir);
switch (printOption) {
case "tree":
printRoutesAsTree(results);
break;
case "table":
printRoutesAsTable(results);
break;
case "info":
console.log("\u2705 Generated Routes");
break;
case "no":
break;
}
return deepSortByPath(results);
}
// src/react-router/index.ts
import { getAppDirectory, route, layout, index } from "@react-router/dev/routes";
function generateRouteConfig(options = appRouterStyle) {
return nextRoutes(options);
}
function nextRoutes(options = appRouterStyle) {
return generateNextRoutes(
options,
getAppDirectory,
index,
route,
layout
);
}
export {
appRouterStyle,
generateRouteConfig,
nextRoutes,
pageRouterStyle
};
//# sourceMappingURL=index.js.map