128 lines
3.6 KiB
TypeScript
128 lines
3.6 KiB
TypeScript
import { readdirSync, statSync, writeFileSync } from "fs";
|
|
import _ from "lodash";
|
|
import { basename, extname, join, relative } from "path";
|
|
|
|
const PAGES_DIR = join(process.cwd(), "src/pages");
|
|
const OUTPUT_FILE = join(process.cwd(), "src/AppRoutes.tsx");
|
|
|
|
// 🧩 Ubah nama file ke nama komponen (PascalCase)
|
|
const toComponentName = (fileName: string) =>
|
|
fileName
|
|
.replace(/_/g, " ")
|
|
.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
.replace(/\s/g, "");
|
|
|
|
// 🧩 Ubah nama file ke path route
|
|
function toRoutePath(name: string): string {
|
|
if (name.toLowerCase() === "home") return "/";
|
|
if (name.toLowerCase() === "login") return "/login";
|
|
if (name.toLowerCase() === "notfound") return "/*";
|
|
if (name.endsWith("_page")) return name.replace("_page", "").toLowerCase();
|
|
if (name.startsWith("form_")) return name.replace("form_", "").toLowerCase();
|
|
return name.toLowerCase();
|
|
}
|
|
|
|
// 🧭 Scan folder pages secara rekursif
|
|
function scan(dir: string): any[] {
|
|
const items = readdirSync(dir);
|
|
const routes: any[] = [];
|
|
|
|
for (const item of items) {
|
|
const full = join(dir, item);
|
|
const stat = statSync(full);
|
|
|
|
if (stat.isDirectory()) {
|
|
routes.push({
|
|
name: item,
|
|
path: item.toLowerCase(),
|
|
children: scan(full),
|
|
});
|
|
} else if (extname(item) === ".tsx") {
|
|
routes.push({
|
|
name: basename(item, ".tsx"),
|
|
filePath: relative(join(process.cwd(), "src"), full).replace(/\\/g, "/"),
|
|
});
|
|
}
|
|
}
|
|
return routes;
|
|
}
|
|
|
|
// 🏗️ Generate <Route> JSX dari struktur folder
|
|
function generateJSX(routes: any[], parentPath = ""): string {
|
|
let jsx = "";
|
|
|
|
for (const route of routes) {
|
|
if (route.children) {
|
|
// cari layout di folder
|
|
const layout = route.children.find((r: any) => r.name.endsWith("_layout"));
|
|
if (layout) {
|
|
const LayoutComponent = toComponentName(layout.name.replace("_layout", "Layout"));
|
|
const nested = route.children.filter((r: any) => r !== layout);
|
|
const nestedRoutes = generateJSX(nested, `${parentPath}/${route.path}`);
|
|
jsx += `
|
|
<Route path="${parentPath}/${route.path}" element={<${LayoutComponent} />}>
|
|
${nestedRoutes}
|
|
</Route>
|
|
`;
|
|
} else {
|
|
jsx += generateJSX(route.children, `${parentPath}/${route.path}`);
|
|
}
|
|
} else {
|
|
const Component = toComponentName(route.name);
|
|
const routePath = toRoutePath(route.name);
|
|
|
|
// Hapus duplikasi segmen
|
|
const fullPath =
|
|
routePath.startsWith("/")
|
|
? routePath
|
|
: `${parentPath}/${_.kebabCase(routePath)}`.replace(/\/+/g, "/");
|
|
|
|
jsx += `<Route path="${fullPath}" element={<${Component} />} />\n`;
|
|
}
|
|
}
|
|
return jsx;
|
|
}
|
|
|
|
// 🧾 Generate import otomatis
|
|
function generateImports(routes: any[]): string {
|
|
const imports = new Set<string>();
|
|
|
|
function collect(rs: any[]) {
|
|
for (const r of rs) {
|
|
if (r.children) collect(r.children);
|
|
else {
|
|
const Comp = toComponentName(r.name);
|
|
imports.add(`import ${Comp} from "./${r.filePath.replace(/\.tsx$/, "")}";`);
|
|
}
|
|
}
|
|
}
|
|
collect(routes);
|
|
return Array.from(imports).join("\n");
|
|
}
|
|
|
|
// 🧠 Main generator
|
|
const allRoutes = scan(PAGES_DIR);
|
|
const imports = generateImports(allRoutes);
|
|
const jsxRoutes = generateJSX(allRoutes);
|
|
|
|
const finalCode = `
|
|
// ⚡ Auto-generated by generateRoutes.ts — DO NOT EDIT MANUALLY
|
|
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
|
import ProtectedRoute from "./components/ProtectedRoute";
|
|
${imports}
|
|
|
|
export default function AppRoutes() {
|
|
return (
|
|
<BrowserRouter>
|
|
<Routes>
|
|
${jsxRoutes}
|
|
</Routes>
|
|
</BrowserRouter>
|
|
);
|
|
}
|
|
`;
|
|
|
|
writeFileSync(OUTPUT_FILE, finalCode);
|
|
console.log("✅ Routes generated successfully → src/AppRoutes.generated.tsx");
|
|
Bun.spawnSync(["bunx", "prettier", "--write", "src/**/*.tsx"])
|