From 12bab6484905ab5f9143ea806126107b3881b5f1 Mon Sep 17 00:00:00 2001 From: bipproduction Date: Sun, 23 Nov 2025 17:06:17 +0800 Subject: [PATCH] tambahan --- bin/g3n.ts | 12 ++- bin/src/compose-log.ts | 67 +++++++++++++ bin/src/compose.ts | 116 ++++++++++++--------- bin/src/route.ts | 221 +++++++++++++++++++++++++++++++---------- bun.lock | 9 ++ package.json | 7 +- 6 files changed, 330 insertions(+), 102 deletions(-) create mode 100644 bin/src/compose-log.ts diff --git a/bin/g3n.ts b/bin/g3n.ts index bbc114a..b000b1c 100755 --- a/bin/g3n.ts +++ b/bin/g3n.ts @@ -12,6 +12,7 @@ import route from "./src/route"; import { version } from '../package.json' assert { type: 'json' }; import appCreate from "./src/app-create"; +import applyLogRotateCompose from "./src/compose-log"; interface CheckPortResult { @@ -36,6 +37,7 @@ Commands: env Generate env.d.ts from .env file scan-port Scan port range (default 3000-4000) route Generate routes.ts from AppRoutes.tsx + compose-log Apply log rotate to compose.yml compose Generate compose.yml from name docker-file Generate Dockerfile frp Show frp proxy list @@ -52,6 +54,7 @@ Examples: g3n env --env .env.local --out src/types/env.d.ts g3n scan-port --start 7700 --end 7800 --host 127.0.0.1 g3n route + g3n compose-log g3n compose g3n docker-file g3n frp @@ -103,6 +106,9 @@ async function main(): Promise { case "compose": handleCompose(name); break; + case "compose-log": + applyLogRotateCompose("compose.yml"); + break; case "docker-file": generateDockerfile(); @@ -175,12 +181,12 @@ function handleCompose(name?: string): void { } if (!args.env) { - console.error("❌ Compose env (staging/prod) is required"); + console.error("❌ Compose env (stg/prod) is required"); return; } - if (args.env !== "staging" && args.env !== "prod") { - console.error("❌ Compose env (staging/prod) is required"); + if (args.env !== "stg" && args.env !== "prod") { + console.error("❌ Compose env (stg/prod) is required"); return; } diff --git a/bin/src/compose-log.ts b/bin/src/compose-log.ts new file mode 100644 index 0000000..fcbb3a5 --- /dev/null +++ b/bin/src/compose-log.ts @@ -0,0 +1,67 @@ +import fs from "fs"; +import { parse, stringify } from "yaml"; + +export interface LogRotateOptions { + maxSize?: string; + maxFile?: string; +} + +/** + * Tambahkan log rotate (logging.driver json-file) ke semua service + * yang belum memiliki konfigurasi logging di docker-compose.yml. + */ +export async function applyLogRotateCompose( + filePath: string, + options: LogRotateOptions = {} +) { + const { maxSize = "10m", maxFile = "3" } = options; + + // Pastikan file ada + if (!fs.existsSync(filePath)) { + throw new Error(`❌ File not found: ${filePath}`); + } + + const raw = fs.readFileSync(filePath, "utf8"); + const compose = parse(raw); // ✅ Pakai yaml.parse() + + if (!compose.services) { + throw new Error("❌ Tidak ditemukan 'services:' di docker-compose.yml"); + } + + let modified = false; + + for (const [name, service] of Object.entries(compose.services)) { + if (!service.logging) { + service.logging = { + driver: "json-file", + options: { + "max-size": maxSize, + "max-file": maxFile, + }, + }; + console.log(`✅ Log rotate ditambahkan ke: ${name}`); + modified = true; + } else { + console.log(`⚠️ Lewati (sudah ada logging): ${name}`); + } + } + + if (!modified) { + console.log("👌 Semua service sudah punya log-rotate, tidak ada perubahan."); + return; + } + + // Backup file lama + const backupPath = `${filePath}.backup-${Date.now()}`; + fs.writeFileSync(backupPath, raw, "utf8"); + + // Simpan file baru + const updated = stringify(compose); // ✅ Pakai yaml.stringify() + fs.writeFileSync(filePath, updated, "utf8"); + + console.log(`✅ Selesai update file: ${filePath}`); + console.log(`📦 Backup dibuat: ${backupPath}`); +} + +export default applyLogRotateCompose; + diff --git a/bin/src/compose.ts b/bin/src/compose.ts index abd46b9..e7991ea 100644 --- a/bin/src/compose.ts +++ b/bin/src/compose.ts @@ -1,11 +1,11 @@ import fs from "fs/promises"; -const text = (name: string) => { - return ` +const text = (name: string, env: "stg" | "prod") => { + return ` services: - ${name}-docker-proxy: + ${name}-${env}-docker-proxy: image: tecnativa/docker-socket-proxy - container_name: ${name}-docker-proxy + container_name: ${name}-${env}-docker-proxy restart: unless-stopped volumes: - /var/run/docker.sock:/var/run/docker.sock @@ -13,42 +13,60 @@ services: CONTAINERS: 1 POST: 1 PING: 1 + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" networks: - - ${name} - ${name}-dev: + - ${name}-${env} + + ${name}-${env}-dev: image: bip/dev:latest build: dockerfile: Dockerfile context: . target: dev - container_name: ${name}-dev + container_name: ${name}-${env}-dev restart: unless-stopped volumes: - ./data/app:/app - ./data/ssh/authorized_keys:/home/bip/.ssh/authorized_keys:ro + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" networks: - - ${name} + - ${name}-${env} depends_on: - ${name}-postgres: + ${name}-${env}-postgres: condition: service_healthy - ${name}-prod: + + ${name}-${env}-prod: build: dockerfile: Dockerfile context: . target: prod image: bip/prod:latest - container_name: ${name}-prod + container_name: ${name}-${env}-prod restart: unless-stopped volumes: - ./data/app:/app + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" networks: - - ${name} + - ${name}-${env} depends_on: - ${name}-postgres: + ${name}-${env}-postgres: condition: service_healthy - ${name}-postgres: + + ${name}-${env}-postgres: image: postgres:16 - container_name: ${name}-postgres + container_name: ${name}-${env}-postgres restart: unless-stopped environment: - POSTGRES_USER=bip @@ -56,30 +74,41 @@ services: - POSTGRES_DB=${name} volumes: - ./data/postgres:/var/lib/postgresql/data + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" networks: - - ${name} + - ${name}-${env} healthcheck: test: ['CMD-SHELL', 'pg_isready -U bip -d ${name}'] interval: 5s timeout: 5s retries: 5 - ${name}-frpc: + + ${name}-${env}-frpc: image: snowdreamtech/frpc:latest - container_name: ${name}-frpc + container_name: ${name}-${env}-frpc restart: always volumes: - ./data/frpc/frpc.toml:/etc/frp/frpc.toml:ro + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" networks: - - ${name} + - ${name}-${env} + networks: - ${name}: + ${name}-${env}: driver: bridge +`; +}; - ` -} - -const generate = (name: string, env: "staging" | "prod", port: string) => { - return ` +const generate = (name: string, env: "stg" | "prod", port: string) => { + return ` #!/bin/bash echo "Generating directory..." @@ -91,9 +120,6 @@ ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDfXPd7ab21qdKtKKdv2bLxIa9hEqq2oLLj7c3i/rN2f EOF echo "Generating frpc.toml..." -touch data/frpc/frpc.toml - -echo "Generating frpc.toml content..." cat > data/frpc/frpc.toml < { + 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 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 + ? `} />\n` + : ""; + + jsx += ` + }> + ${indexRoute} + ${nestedRoutes} + + `; + } 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 += `} />\n`; + } + } + return jsx; +} + +// 🧾 Generate import otomatis +function generateImports(routes: any[]): string { + const imports = new Set(); + + 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 ( + + + ${jsxRoutes} + + + ); +} +`; + + 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"); @@ -15,73 +164,48 @@ interface RouteNode { function getAttributePath(attrs: (t.JSXAttribute | t.JSXSpreadAttribute)[]) { const pathAttr = attrs.find( - (attr) => - t.isJSXAttribute(attr) && - t.isJSXIdentifier(attr.name, { name: "path" }) + (attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name, { name: "path" }) ) as t.JSXAttribute | undefined; - if (pathAttr && t.isStringLiteral(pathAttr.value)) { - return pathAttr.value.value; - } + if (pathAttr && t.isStringLiteral(pathAttr.value)) return pathAttr.value.value; return ""; } -/** - * Rekursif baca node beserta anak-anaknya - */ function extractRouteNodes(node: t.JSXElement): RouteNode | null { const opening = node.openingElement; - - if (!t.isJSXIdentifier(opening.name) || opening.name.name !== "Route") { - return null; - } + if (!t.isJSXIdentifier(opening.name) || opening.name.name !== "Route") return null; const currentPath = getAttributePath(opening.attributes); - - // cari anak-anak 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 }; } -/** - * Flatten hasil rekursif jadi list path full - */ -function flattenRoutes( - node: RouteNode, - parentPath = "" -): Record { +function flattenRoutes(node: RouteNode, parentPath = ""): Record { const record: Record = {}; - - // gabung path parent + child let fullPath = node.path; + if (fullPath) { - if (parentPath) { - if (fullPath === "/") { - fullPath = parentPath; - } else { - fullPath = `${parentPath.replace(/\/$/, "")}/${fullPath.replace( - /^\//, - "" - )}`; - } - } if (!fullPath.startsWith("/")) { - fullPath = `/${fullPath}`; + 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; } @@ -92,18 +216,14 @@ function extractRoutes(code: string): Record { }); const routes: Record = {}; - traverse(ast, { JSXElement(path) { - const node = path.node; - const opening = node.openingElement; + const opening = path.node.openingElement; if (t.isJSXIdentifier(opening.name) && opening.name.name === "Routes") { - for (const child of node.children) { + for (const child of path.node.children) { if (t.isJSXElement(child)) { - const routeNode = extractRouteNodes(child); - if (routeNode) { - Object.assign(routes, flattenRoutes(routeNode)); - } + const node = extractRouteNodes(child); + if (node) Object.assign(routes, flattenRoutes(node)); } } } @@ -114,6 +234,8 @@ function extractRoutes(code: string): Record { } export default function route() { + generateRoutes(); + if (!fs.existsSync(APP_ROUTES_FILE)) { console.error("❌ AppRoutes.tsx not found in src/"); process.exit(1); @@ -128,11 +250,8 @@ export default function route() { 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;` + `// AUTO-GENERATED FILE\nconst clientRoutes = ${JSON.stringify(routes, null, 2)} as const;\n\nexport default clientRoutes;` ); - console.log(`📄 clientRoutes.ts generated at ${outPath}`); + + console.log(`📄 clientRoutes.ts saved → ${outPath}`); } diff --git a/bun.lock b/bun.lock index 648033c..d2eeff4 100644 --- a/bun.lock +++ b/bun.lock @@ -8,11 +8,14 @@ "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.2", "@types/babel__traverse": "^7.28.0", + "@types/lodash": "^4.17.20", "@types/minimist": "^1.2.5", "dedent": "^1.7.0", "dotenv": "^17.2.1", + "lodash": "^4.17.21", "minimist": "^1.2.8", "ora": "^9.0.0", + "yaml": "^2.8.1", }, "peerDependencies": { "typescript": "^5", @@ -48,6 +51,8 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + "@types/lodash": ["@types/lodash@4.17.20", "", {}, "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA=="], + "@types/minimist": ["@types/minimist@1.2.5", "", {}, "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag=="], "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], @@ -74,6 +79,8 @@ "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "log-symbols": ["log-symbols@7.0.1", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="], "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], @@ -100,6 +107,8 @@ "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], + "yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="], + "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], } } diff --git a/package.json b/package.json index 69c2d55..c74a87b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "g3n", - "version": "1.0.20", + "version": "1.0.30", "type": "module", "bin": { "g3n": "./bin/g3n.ts" @@ -13,10 +13,13 @@ "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.2", "@types/babel__traverse": "^7.28.0", + "@types/lodash": "^4.17.20", "@types/minimist": "^1.2.5", "dedent": "^1.7.0", "dotenv": "^17.2.1", + "lodash": "^4.17.21", "minimist": "^1.2.8", - "ora": "^9.0.0" + "ora": "^9.0.0", + "yaml": "^2.8.1" } }