kawkab-frontend
Version:
Kawkab frontend is a frontend library for the Kawkab framework
169 lines (168 loc) • 6.53 kB
JavaScript
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);
}