baru ni e

This commit is contained in:
bipproduction
2025-12-11 15:52:22 +08:00
commit eb5eee6ae9
52 changed files with 5774 additions and 0 deletions

53
bin/env.generate.ts Normal file
View 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();
}

416
bin/route.generate.ts Normal file
View File

@@ -0,0 +1,416 @@
#!/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");
/******************************
* Prefetch Helper Template
******************************/
const PREFETCH_HELPER = `
/**
* Prefetch lazy component:
* - Hover
* - Visible (viewport)
* - Browser idle
*/
export function attachPrefetch(el: HTMLElement | null, preload: () => void) {
if (!el) return;
let done = false;
const run = () => {
if (done) return;
done = true;
preload();
};
// 1) On hover
el.addEventListener("pointerenter", run, { once: true });
// 2) On visible (IntersectionObserver)
const io = new IntersectionObserver((entries) => {
if (entries && entries[0] && entries[0].isIntersecting) {
run();
io.disconnect();
}
});
io.observe(el);
// 3) On idle
if ("requestIdleCallback" in window) {
requestIdleCallback(() => run());
} else {
setTimeout(run, 200);
}
}
`;
/******************************
* Component Name Generator
******************************/
const toComponentName = (fileName: string): string =>
fileName
.replace(/\.[^/.]+$/, "")
.replace(/[_-]+/g, " ")
.replace(/([a-z])([A-Z])/g, "$1 $2")
.replace(/\b\w/g, (c) => c.toUpperCase())
.replace(/\s+/g, "");
/******************************
* Route Path Normalizer
******************************/
function toRoutePath(name: string): string {
name = name.replace(/\.[^/.]+$/, "");
if (name.toLowerCase() === "home") return "/";
if (name.toLowerCase() === "login") return "/login";
if (name.toLowerCase() === "notfound") return "/*";
if (name.startsWith("[") && name.endsWith("]"))
return `:${name.slice(1, -1)}`;
name = name.replace(/_page$/i, "").replace(/^form_/i, "");
return _.kebabCase(name);
}
/******************************
* Scan Folder + Validation + Dynamic Duplicate Check
******************************/
function scan(dir: string): any[] {
const items = readdirSync(dir);
const routes: any[] = [];
const dynamicParams = new Set<string>();
for (const item of items) {
const full = join(dir, item);
const stat = statSync(full);
if (stat.isDirectory()) {
if (!/^[a-zA-Z0-9_-]+$/.test(item)) {
console.warn(`⚠️ Invalid folder name: ${item}`);
}
routes.push({
name: item,
path: _.kebabCase(item),
children: scan(full),
});
} else if (extname(item) === ".tsx") {
const base = basename(item, ".tsx");
if (!/^[a-zA-Z0-9_[\]-]+$/.test(base)) {
console.warn(`⚠️ Invalid file name: ${item}`);
}
if (base.startsWith("[") && base.endsWith("]")) {
const p = base.slice(1, -1);
if (dynamicParams.has(p)) {
console.error(`❌ Duplicate dynamic param "${p}" in ${dir}`);
process.exit(1);
}
dynamicParams.add(p);
}
routes.push({
name: base,
filePath: relative(join(process.cwd(), "src"), full).replace(/\\/g, "/"),
});
}
}
return routes;
}
/******************************
* Index Detection
******************************/
function findIndexFile(folderName: string, children: any[]) {
const lower = folderName.toLowerCase();
return (
children.find((r: any) => r.name.toLowerCase().endsWith("_home")) ||
children.find((r: any) => r.name.toLowerCase() === "index") ||
children.find((r: any) => r.name.toLowerCase() === `${lower}_page`)
);
}
/******************************
* Generate JSX <Route> (Lazy + Prefetch)
******************************/
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 LayoutComp = toComponentName(
layout.name.replace("_layout", "Layout")
);
const nested = route.children.filter((x: any) => x !== layout);
const nestedRoutes = generateJSX(nested, `${parentPath}/${route.path}`);
const indexFile = findIndexFile(route.name, route.children);
const indexRoute = indexFile
? `<Route index element={<${toComponentName(
indexFile.name
)}.Component />} />`
: `<Route index element={<Navigate to="${(
parentPath +
"/" +
route.path +
"/" +
(nested[0]?.name ?? "")
).replace(/\/+/g, "/")}" replace />}/>`;
jsx += `
<Route path="${parentPath}/${route.path}" element={<${LayoutComp}.Component />}>
${indexRoute}
${nestedRoutes}
</Route>
`;
} else {
jsx += generateJSX(route.children, `${parentPath}/${route.path}`);
}
} else {
const Comp = toComponentName(route.name);
const routePath = toRoutePath(route.name);
const fullPath = routePath.startsWith("/")
? routePath
: `${parentPath}/${routePath}`.replace(/\/+/g, "/");
jsx += `
<Route
path="${fullPath}"
element={
<React.Suspense fallback={<SkeletonLoading />}>
<${Comp}.Component />
</React.Suspense>
}
/>
`;
}
}
return jsx;
}
/******************************
* Lazy Import + Prefetch Injection
******************************/
function generateImports(routes: any[]): string {
const list: string[] = [];
function walk(rs: any[]) {
for (const r of rs) {
if (r.children) walk(r.children);
else {
const C = toComponentName(r.name);
const file = r.filePath.replace(/\.tsx$/, "");
list.push(`
const ${C} = {
Component: React.lazy(() => import("./${file}")),
preload: () => import("./${file}")
};
`);
}
}
}
walk(routes);
return list.join("\n");
}
/******************************
* Generate AppRoutes.tsx
******************************/
function generateRoutes() {
const allRoutes = scan(PAGES_DIR);
const imports = generateImports(allRoutes);
const jsx = generateJSX(allRoutes);
let loadingSkeleton = `
const SkeletonLoading = () => {
return (
<div style={{ padding: "20px" }}>
{Array.from({ length: 5 }, (_, i) => (
<Skeleton key={i} height={70} radius="md" animate={true} mb="sm" />
))}
</div>
);
};
`
const final = `
// ⚡ AUTO-GENERATED — DO NOT EDIT
import React from "react";
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import { Skeleton } from "@mantine/core";
${loadingSkeleton}
${PREFETCH_HELPER}
${imports}
export default function AppRoutes() {
return (
<BrowserRouter>
<Routes>
${jsx}
</Routes>
</BrowserRouter>
);
}
`;
writeFileSync(OUTPUT_FILE, final);
console.log(`✅ Routes generated → ${OUTPUT_FILE}`);
Bun.spawnSync(["bunx", "prettier", "--write", "src/**/*.tsx"]);
}
/******************************
* Extract flat client routes
******************************/
const SRC_DIR = path.resolve("src");
const APP_ROUTES_FILE = join(SRC_DIR, "AppRoutes.tsx");
interface RouteNode {
path: string;
children: RouteNode[];
}
function getAttributePath(attrs: any[]) {
const attr = attrs.find(
(a) => t.isJSXAttribute(a) && a.name.name === "path"
) as any;
return attr?.value?.value ?? "";
}
function extractRouteNodes(node: t.JSXElement): RouteNode | null {
const op = node.openingElement;
if (!t.isJSXIdentifier(op.name) || op.name.name !== "Route") return null;
const cur = getAttributePath(op.attributes);
const children: RouteNode[] = [];
for (const c of node.children) {
if (t.isJSXElement(c)) {
const n = extractRouteNodes(c);
if (n) children.push(n);
}
}
return { path: cur, children };
}
function flattenRoutes(node: RouteNode, parent = ""): Record<string, string> {
const r: Record<string, string> = {};
let full = node.path;
if (full) {
if (!full.startsWith("/"))
full =
parent && full !== "/"
? `${parent.replace(/\/$/, "")}/${full}`
: "/" + full;
full = full.replace(/\/+/g, "/");
r[full] = full;
}
for (const c of node.children)
Object.assign(r, flattenRoutes(c, full || parent));
return r;
}
function extractRoutes(code: string) {
const ast = parser.parse(code, {
sourceType: "module",
plugins: ["jsx", "typescript"],
});
const routes: Record<string, string> = {};
traverse(ast, {
JSXElement(p) {
const op = p.node.openingElement;
if (t.isJSXIdentifier(op.name) && op.name.name === "Routes") {
for (const c of p.node.children) {
if (t.isJSXElement(c)) {
const root = extractRouteNodes(c);
if (root) Object.assign(routes, flattenRoutes(root));
}
}
}
},
});
return routes;
}
/******************************
* Type-Safe Route Builder
******************************/
function generateTypeSafe(routes: Record<string, string>) {
const keys = Object.keys(routes).filter((x) => !x.includes("*"));
const union = keys.map((x) => `"${x}"`).join(" | ");
const code = `
export type AppRoute = ${union};
export function route(path: AppRoute, params?: Record<string,string|number>) {
if (!params) return path;
let final = path;
for (const k of Object.keys(params)) {
final = final.replace(":" + k, params[k] + "") as AppRoute;
}
return final;
}
`;
fs.writeFileSync(join(SRC_DIR, "routeTypes.ts"), code);
console.log("📄 routeTypes.ts generated.");
}
/******************************
* MAIN
******************************/
export default function run() {
generateRoutes();
const code = fs.readFileSync(APP_ROUTES_FILE, "utf-8");
const routes = extractRoutes(code);
const out = join(SRC_DIR, "clientRoutes.ts");
fs.writeFileSync(
out,
`// AUTO-GENERATED\nconst clientRoutes = ${JSON.stringify(
routes,
null,
2
)} as const;\nexport default clientRoutes;`
);
console.log(`📄 clientRoutes.ts saved → ${out}`);
generateTypeSafe(routes);
}
run();