rr-next-routes
Version:
generate nextjs-style routes in your react-router-v7 application
374 lines (368 loc) • 13.2 kB
JavaScript
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
}) : x)(function(x) {
if (typeof require !== "undefined") return require.apply(this, arguments);
throw Error('Dynamic require of "' + x + '" is not supported');
});
var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
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/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((route3) => route3.path);
const layoutsLast = sortedRoutes.filter((route3) => !route3.path && route3.children);
pathsFirst.forEach((route3) => {
result.push({
routePath: route3.path,
routeFile: route3.file,
parentLayout: parentLayout ?? void 0
});
});
layoutsLast.forEach((layout3) => {
result.push({
routePath: "(layout)",
routeFile: layout3.file,
parentLayout: parentLayout ?? void 0
});
if (layout3.children) {
const layoutChildren = extractRoutesForTable(layout3.children, layout3.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((route3) => route3.path);
const layoutsLast = sortedRoutes.filter((route3) => !route3.path && route3.children);
pathsFirst.forEach((route3) => {
const routePath = `"${route3.path}"`;
console.log(`${indentation}\u251C\u2500\u2500 ${routePath} (${route3.file})`);
});
layoutsLast.forEach((route3) => {
console.log(`${indentation}\u251C\u2500\u2500 (layout) (${route3.file})`);
if (route3.children) {
printRouteTree(route3.children, indent2 + 1);
}
});
}
console.groupCollapsed("\u2705 Generated Route Tree (open to see generated routes)");
printRouteTree(routes, indent);
console.groupEnd();
}
var init_utils = __esm({
"src/common/utils.ts"() {
"use strict";
}
});
// src/common/next-routes-common.ts
import { readdirSync, statSync } from "node:fs";
import { join, parse, relative, resolve } from "node:path";
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);
}
var appRouterStyle, pageRouterStyle, defaultOptions;
var init_next_routes_common = __esm({
"src/common/next-routes-common.ts"() {
"use strict";
init_utils();
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
};
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
};
defaultOptions = appRouterStyle;
}
});
// src/react-router/index.ts
var react_router_exports = {};
__export(react_router_exports, {
appRouterStyle: () => appRouterStyle,
generateRouteConfig: () => generateRouteConfig,
nextRoutes: () => nextRoutes,
pageRouterStyle: () => pageRouterStyle
});
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
);
}
var init_react_router = __esm({
"src/react-router/index.ts"() {
"use strict";
init_next_routes_common();
}
});
// src/remix/index.ts
var remix_exports = {};
__export(remix_exports, {
appRouterStyle: () => appRouterStyle,
generateRouteConfig: () => generateRouteConfig2,
nextRoutes: () => nextRoutes2,
pageRouterStyle: () => pageRouterStyle
});
import { getAppDirectory as getAppDirectory2, index as index2, route as route2, layout as layout2 } from "@remix-run/route-config";
function generateRouteConfig2(options = appRouterStyle) {
return nextRoutes2(options);
}
function nextRoutes2(options = appRouterStyle) {
return generateNextRoutes(
options,
getAppDirectory2,
index2,
route2,
layout2
);
}
var init_remix = __esm({
"src/remix/index.ts"() {
"use strict";
init_next_routes_common();
}
});
// src/common/implementationResolver.ts
function resolveImplementation() {
try {
__require("@react-router/dev/routes");
return init_react_router(), __toCommonJS(react_router_exports);
} catch (e) {
try {
__require("@remix-run/route-config");
return init_remix(), __toCommonJS(remix_exports);
} catch (e2) {
throw new Error(
"Could not import from either @react-router/dev/routes or @remix-run/route-config. Please install one of these packages to use this library."
);
}
}
}
// src/next-routes.ts
init_next_routes_common();
var implementation = resolveImplementation();
var nextRoutes3 = implementation.nextRoutes;
var generateRouteConfig3 = implementation.generateRouteConfig;
export {
appRouterStyle,
generateRouteConfig3 as generateRouteConfig,
nextRoutes3 as nextRoutes,
pageRouterStyle
};
//# sourceMappingURL=next-routes.js.map