UNPKG

kawkab-frontend

Version:

Kawkab frontend is a frontend library for the Kawkab framework

169 lines (168 loc) 6.53 kB
#!/usr/bin/env node import chokidar from "chokidar"; import { writeFileSync, readdirSync, existsSync } from "fs"; import path from "path"; // ANSI Colors const reset = "\x1b[0m", bold = "\x1b[1m"; const gray = "\x1b[90m", green = "\x1b[32m", red = "\x1b[31m"; const yellow = "\x1b[33m", cyan = "\x1b[36m", magenta = "\x1b[35m", white = "\x1b[37m"; // Constants const DEBOUNCE_DELAY = 1000; const appDir = path.resolve(process.cwd(), "app"); const outputFile = path.resolve(appDir, "routes.ts"); let debounceTimer = null; function normalizePath(filePath) { return filePath.replace(/\\/g, "/"); } function isValidRouteFile(filePath) { const normalized = normalizePath(filePath); return normalized.endsWith(".tsx") && normalized.includes("/pages/"); } function toRoutePath(segment) { if ((segment.startsWith("(") && segment.endsWith(")")) || segment.startsWith("_")) return null; const name = segment.replace(/\.tsx?$/, ""); if (["page", "layout", "not-found"].includes(name)) return null; if (name === "index") return ""; if (name.startsWith("[[...") && name.endsWith("]]")) return "*"; if (name.startsWith("[...") && name.endsWith("]")) return "*"; if (name.startsWith("[[") && name.endsWith("]]")) return `:${name.slice(2, -2)}?`; if (name.startsWith("[") && name.endsWith("]")) return `:${name.slice(1, -1)}`; return name; } function toImportPath(filePath) { const relPath = path.relative(appDir, filePath).replace(/\\/g, "/"); return `./${relPath}`; } function buildRoutesFromDirectory(dir, isRoot = false) { if (!existsSync(dir)) return []; const entries = readdirSync(dir, { withFileTypes: true }); let children = []; for (const entry of entries) { if (entry.isDirectory()) { children.push(...buildRoutesFromDirectory(path.join(dir, entry.name), false)); } } const layoutFile = entries.find(e => e.name === 'layout.tsx'); const pageFile = entries.find(e => e.name === 'page.tsx'); const notFoundFile = entries.find(e => e.name === 'not-found.tsx'); for (const entry of entries) { if (entry.isFile() && entry.name.endsWith('.tsx') && !['layout.tsx', 'page.tsx', 'not-found.tsx'].includes(entry.name)) { const routePath = toRoutePath(entry.name); if (routePath !== null) { children.push(`route("${routePath}", "${toImportPath(path.join(dir, entry.name))}")`); } } } if (pageFile) { children.unshift(`index("${toImportPath(path.join(dir, pageFile.name))}")`); } if (notFoundFile) { children.push(`route("*", "${toImportPath(path.join(dir, notFoundFile.name))}")`); } children = children.filter(Boolean); if (children.length === 0) return []; const currentSegment = toRoutePath(path.basename(dir)); if (isRoot) { if (layoutFile) { return [`layout("${toImportPath(path.join(dir, layoutFile.name))}", [\n ${children.join(',\n ')}\n])`]; } return children; } if (layoutFile) { return [`layout("${toImportPath(path.join(dir, layoutFile.name))}", [\n ${children.join(',\n ')}\n])`]; } if (currentSegment !== null && currentSegment !== '') { return [`...prefix("${currentSegment}", [\n ${children.join(',\n ')}\n])`]; } return children; } function generateRoutes() { console.log(`${yellow}${bold}⚙️ [ROUTES] Generating routes...${reset}`); const moduleDirs = readdirSync(appDir, { withFileTypes: true }) .filter((dirent) => dirent.isDirectory()) .map((dirent) => dirent.name); const pagesRoots = moduleDirs .map((mod) => path.join(appDir, mod, "pages")) .filter((pagesDir) => existsSync(pagesDir)); let allRoutes = []; for (const root of pagesRoots) { allRoutes.push(...buildRoutesFromDirectory(root, true)); } const globalNotFound = path.resolve(appDir, 'pages', 'not-found.tsx'); if (existsSync(globalNotFound)) { allRoutes.push(`route("*", "${toImportPath(globalNotFound)}")`); } const imports = `import { route, index, layout, prefix } from "@react-router/dev/routes";\n\n`; const exportStatement = `export default [\n ${allRoutes.filter(Boolean).join(',\n ')}\n];\n`; writeFileSync(outputFile, imports + exportStatement, 'utf-8'); } function scheduleGenerate() { if (debounceTimer) clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { try { generateRoutes(); } catch (err) { console.error(`${red}${bold}✖️ [ERROR] ${err.message}${reset}`); } debounceTimer = null; }, DEBOUNCE_DELAY); } // --- Main Execution Logic --- const args = process.argv.slice(2); const noWatchMode = args.includes('--no-watch'); // 1. Always generate routes once on startup. try { generateRoutes(); if (!noWatchMode) { console.log(`${green}✅ Initial routes generated successfully.${reset}`); } } catch (err) { console.error(`${red}${bold}✖️ [ERROR] Failed to generate routes: ${err.message}${reset}`); process.exit(1); } // 2. If --no-watch is NOT passed, enter watch mode. if (!noWatchMode) { // --- Watch Mode --- console.log(`${magenta}${bold}🚀 [WATCHER] Watching: app/**/pages/**/*.tsx${reset}`); console.log(`${gray}📁 Current working directory: ${process.cwd()}${reset}`); const watcher = chokidar.watch("app", { ignoreInitial: true, usePolling: true, interval: 100, awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 100 }, }); watcher.on("add", (filePath) => { if (isValidRouteFile(filePath)) { console.log(`${green}➕ Added:${reset} ${white}${normalizePath(filePath)}${reset}`); scheduleGenerate(); } }); watcher.on("unlink", (filePath) => { if (isValidRouteFile(filePath)) { console.log(`${red}🗑️ Removed:${reset} ${white}${normalizePath(filePath)}${reset}`); scheduleGenerate(); } }); watcher.on("change", (filePath) => { if (isValidRouteFile(filePath)) { console.log(`${cyan}… Changed (ignored):${reset} ${white}${normalizePath(filePath)}${reset}`); } }); } else { // --- Single Run Mode --- console.log(`${green}✅ Routes generated successfully (single run).${reset}`); process.exit(0); }