tambahan
This commit is contained in:
53
bin/env.generate.ts
Normal file
53
bin/env.generate.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as dotenv from "dotenv";
|
||||
|
||||
interface GenerateEnvTypesOptions {
|
||||
envFilePath?: string;
|
||||
outputDir?: string;
|
||||
outputFileName?: string;
|
||||
}
|
||||
|
||||
export function generateEnvTypes(options: GenerateEnvTypesOptions = {}) {
|
||||
const {
|
||||
envFilePath = path.resolve(process.cwd(), ".env"),
|
||||
outputDir = path.resolve(process.cwd(), "types"),
|
||||
outputFileName = "env.d.ts",
|
||||
} = options;
|
||||
|
||||
const outputFile = path.join(outputDir, outputFileName);
|
||||
|
||||
// 1. Baca .env
|
||||
if (!fs.existsSync(envFilePath)) {
|
||||
console.warn(`⚠️ .env file not found at: ${envFilePath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const envContent = fs.readFileSync(envFilePath, "utf-8");
|
||||
const parsed = dotenv.parse(envContent);
|
||||
|
||||
// 2. Generate TypeScript declare
|
||||
const lines = Object.keys(parsed).map((key) => ` ${key}?: string;`);
|
||||
|
||||
const fileContent = `declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
${lines.join("\n")}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// 3. Buat folder kalau belum ada
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 4. Tulis file
|
||||
fs.writeFileSync(outputFile, fileContent, "utf-8");
|
||||
|
||||
console.log(`✅ Env types generated at: ${outputFile}`);
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
generateEnvTypes();
|
||||
}
|
||||
|
||||
260
bin/route.generate.ts
Normal file
260
bin/route.generate.ts
Normal file
@@ -0,0 +1,260 @@
|
||||
#!/usr/bin/env bun
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import * as parser from "@babel/parser";
|
||||
import traverse from "@babel/traverse";
|
||||
import * as t from "@babel/types";
|
||||
|
||||
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 menjadi PascalCase
|
||||
* - Support: snake_case, kebab-case, camelCase, PascalCase
|
||||
*/
|
||||
const toComponentName = (fileName: string): string => {
|
||||
return fileName
|
||||
.replace(/\.[^/.]+$/, "") // hilangkan ekstensi file
|
||||
.replace(/[_-]+/g, " ") // snake_case & kebab-case → spasi
|
||||
.replace(/([a-z])([A-Z])/g, "$1 $2") // camelCase → spasi
|
||||
.replace(/\b\w/g, (c) => c.toUpperCase()) // kapital tiap kata
|
||||
.replace(/\s+/g, ""); // gabung semua → PascalCase
|
||||
};
|
||||
|
||||
/**
|
||||
* ✅ Normalisasi nama menjadi path route (kebab-case)
|
||||
*/
|
||||
function toRoutePath(name: string): string {
|
||||
name = name.replace(/\.[^/.]+$/, ""); // hapus ekstensi
|
||||
|
||||
if (name.toLowerCase() === "home") return "/";
|
||||
if (name.toLowerCase() === "login") return "/login";
|
||||
if (name.toLowerCase() === "notfound") return "/*";
|
||||
|
||||
// Hapus prefix/suffix umum
|
||||
name = name.replace(/_page$/i, "").replace(/^form_/i, "");
|
||||
|
||||
// ✅ Normalisasi ke kebab-case
|
||||
return _.kebabCase(name);
|
||||
}
|
||||
|
||||
// 🧭 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: _.kebabCase(item),
|
||||
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) {
|
||||
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}`);
|
||||
|
||||
const homeFile = route.children.find((r: any) =>
|
||||
r.name.toLowerCase().endsWith("_home")
|
||||
);
|
||||
const indexRoute = homeFile
|
||||
? `<Route index element={<${toComponentName(homeFile.name)} />} />\n`
|
||||
: "";
|
||||
|
||||
jsx += `
|
||||
<Route path="${parentPath}/${route.path}" element={<${LayoutComponent} />}>
|
||||
${indexRoute}
|
||||
${nestedRoutes}
|
||||
</Route>
|
||||
`;
|
||||
} else {
|
||||
jsx += generateJSX(route.children, `${parentPath}/${route.path}`);
|
||||
}
|
||||
} else {
|
||||
const Component = toComponentName(route.name);
|
||||
const routePath = toRoutePath(route.name);
|
||||
|
||||
const fullPath = routePath.startsWith("/")
|
||||
? routePath
|
||||
: `${parentPath}/${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");
|
||||
}
|
||||
|
||||
function generateRoutes() {
|
||||
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";
|
||||
${imports}
|
||||
|
||||
export default function AppRoutes() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
${jsxRoutes}
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
`;
|
||||
|
||||
writeFileSync(OUTPUT_FILE, finalCode);
|
||||
console.log(`✅ Routes generated → ${OUTPUT_FILE}`);
|
||||
Bun.spawnSync(["bunx", "prettier", "--write", "src/**/*.tsx"]);
|
||||
}
|
||||
|
||||
// --- Extract untuk clientRoutes.ts ---
|
||||
const SRC_DIR = path.resolve(process.cwd(), "src");
|
||||
const APP_ROUTES_FILE = path.join(SRC_DIR, "AppRoutes.tsx");
|
||||
|
||||
interface RouteNode {
|
||||
path: string;
|
||||
children: RouteNode[];
|
||||
}
|
||||
|
||||
function getAttributePath(attrs: (t.JSXAttribute | t.JSXSpreadAttribute)[]) {
|
||||
const pathAttr = attrs.find(
|
||||
(attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name, { name: "path" })
|
||||
) as t.JSXAttribute | undefined;
|
||||
|
||||
if (pathAttr && t.isStringLiteral(pathAttr.value)) return pathAttr.value.value;
|
||||
return "";
|
||||
}
|
||||
|
||||
function extractRouteNodes(node: t.JSXElement): RouteNode | null {
|
||||
const opening = node.openingElement;
|
||||
if (!t.isJSXIdentifier(opening.name) || opening.name.name !== "Route") return null;
|
||||
|
||||
const currentPath = getAttributePath(opening.attributes);
|
||||
const children: RouteNode[] = [];
|
||||
|
||||
for (const child of node.children) {
|
||||
if (t.isJSXElement(child)) {
|
||||
const childNode = extractRouteNodes(child);
|
||||
if (childNode) children.push(childNode);
|
||||
}
|
||||
}
|
||||
return { path: currentPath, children };
|
||||
}
|
||||
|
||||
function flattenRoutes(node: RouteNode, parentPath = ""): Record<string, string> {
|
||||
const record: Record<string, string> = {};
|
||||
let fullPath = node.path;
|
||||
|
||||
if (fullPath) {
|
||||
if (!fullPath.startsWith("/")) {
|
||||
if (parentPath) {
|
||||
if (fullPath === "/") fullPath = parentPath;
|
||||
else fullPath = `${parentPath.replace(/\/$/, "")}/${fullPath}`;
|
||||
}
|
||||
if (!fullPath.startsWith("/")) fullPath = `/${fullPath}`;
|
||||
}
|
||||
fullPath = fullPath.replace(/\/+/g, "/");
|
||||
record[fullPath] = fullPath;
|
||||
}
|
||||
|
||||
for (const child of node.children) {
|
||||
Object.assign(record, flattenRoutes(child, fullPath || parentPath));
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
function extractRoutes(code: string): Record<string, string> {
|
||||
const ast = parser.parse(code, {
|
||||
sourceType: "module",
|
||||
plugins: ["typescript", "jsx"],
|
||||
});
|
||||
|
||||
const routes: Record<string, string> = {};
|
||||
traverse(ast, {
|
||||
JSXElement(path) {
|
||||
const opening = path.node.openingElement;
|
||||
if (t.isJSXIdentifier(opening.name) && opening.name.name === "Routes") {
|
||||
for (const child of path.node.children) {
|
||||
if (t.isJSXElement(child)) {
|
||||
const node = extractRouteNodes(child);
|
||||
if (node) Object.assign(routes, flattenRoutes(node));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return routes;
|
||||
}
|
||||
|
||||
export default function route() {
|
||||
generateRoutes();
|
||||
|
||||
if (!fs.existsSync(APP_ROUTES_FILE)) {
|
||||
console.error("❌ AppRoutes.tsx not found in src/");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const code = fs.readFileSync(APP_ROUTES_FILE, "utf-8");
|
||||
const routes = extractRoutes(code);
|
||||
|
||||
console.log("✅ Generated Routes:");
|
||||
console.log(routes);
|
||||
|
||||
const outPath = path.join(SRC_DIR, "clientRoutes.ts");
|
||||
fs.writeFileSync(
|
||||
outPath,
|
||||
`// AUTO-GENERATED FILE\nconst clientRoutes = ${JSON.stringify(routes, null, 2)} as const;\n\nexport default clientRoutes;`
|
||||
);
|
||||
|
||||
console.log(`📄 clientRoutes.ts saved → ${outPath}`);
|
||||
}
|
||||
|
||||
route()
|
||||
|
||||
Reference in New Issue
Block a user