@razen-core/zenweb
Version:
A minimalist TypeScript framework for building reactive web applications with no virtual DOM
215 lines (210 loc) • 6.9 kB
JavaScript
/**
* Route Scanner
* Scans file system for routes (Next.js style file-based routing)
*/
import * as fs from 'fs';
import * as path from 'path';
import { logger } from './logger.js';
/**
* Scan pages directory for routes
*/
export function scanRoutes(pagesDir) {
const routes = [];
const layouts = new Map();
const middleware = new Map();
if (!fs.existsSync(pagesDir)) {
logger.warning(`Pages directory not found: ${pagesDir}`);
return { routes, layouts, middleware };
}
// Scan directory recursively
scanDirectory(pagesDir, '', routes, layouts, middleware);
// Sort routes by specificity (static routes first, then dynamic, then catch-all)
routes.sort((a, b) => {
const aScore = getRouteSpecificity(a.path);
const bScore = getRouteSpecificity(b.path);
return bScore - aScore;
});
logger.info(`Found ${routes.length} routes`);
return { routes, layouts, middleware };
}
/**
* Scan directory recursively
*/
function scanDirectory(dir, routePath, routes, layouts, middleware) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
// Handle directory-based routes
let segmentPath = entry.name;
// Dynamic route: [id] -> :id
if (segmentPath.startsWith('[') && segmentPath.endsWith(']')) {
segmentPath = ':' + segmentPath.slice(1, -1);
}
// Catch-all route: [...slug] -> *
if (segmentPath.startsWith('[...') && segmentPath.endsWith(']')) {
segmentPath = '*';
}
// Optional catch-all: [[...slug]] -> :slug?
if (segmentPath.startsWith('[[...') && segmentPath.endsWith(']]')) {
const param = segmentPath.slice(5, -2);
segmentPath = `:${param}?`;
}
const newRoutePath = routePath + '/' + segmentPath;
scanDirectory(fullPath, newRoutePath, routes, layouts, middleware);
}
else if (entry.isFile()) {
// Handle file-based routes
const ext = path.extname(entry.name);
const baseName = path.basename(entry.name, ext);
// Only process TypeScript/JavaScript files
if (!['.ts', '.tsx', '.js', '.jsx'].includes(ext)) {
continue;
}
// Special files
if (baseName === 'layout') {
layouts.set(routePath || '/', fullPath);
continue;
}
if (baseName === 'middleware') {
middleware.set(routePath || '/', fullPath);
continue;
}
// Page files
if (baseName === 'page' || baseName === 'index') {
const route = createRoute(routePath || '/', fullPath);
routes.push(route);
// Check for special files in the same directory
checkSpecialFiles(dir, route);
}
}
}
}
/**
* Create route object
*/
function createRoute(routePath, filePath) {
const params = [];
let dynamic = false;
// Extract dynamic params
const segments = routePath.split('/').filter(Boolean);
for (const segment of segments) {
if (segment.startsWith(':')) {
dynamic = true;
const param = segment.slice(1).replace('?', '');
params.push(param);
}
else if (segment === '*') {
dynamic = true;
params.push('slug');
}
}
return {
path: routePath || '/',
filePath,
dynamic,
params
};
}
/**
* Check for special files (loading, error, not-found)
*/
function checkSpecialFiles(dir, route) {
const loadingPath = path.join(dir, 'loading.ts');
if (fs.existsSync(loadingPath)) {
route.loading = loadingPath;
}
const errorPath = path.join(dir, 'error.ts');
if (fs.existsSync(errorPath)) {
route.error = errorPath;
}
const notFoundPath = path.join(dir, 'not-found.ts');
if (fs.existsSync(notFoundPath)) {
route.notFound = notFoundPath;
}
}
/**
* Calculate route specificity for sorting
*/
function getRouteSpecificity(routePath) {
let score = 0;
const segments = routePath.split('/').filter(Boolean);
for (const segment of segments) {
if (segment === '*') {
score += 1; // Catch-all has lowest priority
}
else if (segment.startsWith(':')) {
score += 10; // Dynamic segments
}
else {
score += 100; // Static segments have highest priority
}
}
return score;
}
/**
* Generate route manifest file
*/
export function generateRouteManifest(manifest, outputPath) {
const code = `/**
* Auto-generated route manifest
* Do not edit manually
*/
export const routes = ${JSON.stringify(manifest.routes, null, 2)};
export const layouts = new Map(${JSON.stringify(Array.from(manifest.layouts.entries()))});
export const middleware = new Map(${JSON.stringify(Array.from(manifest.middleware.entries()))});
`;
fs.writeFileSync(outputPath, code, 'utf8');
logger.success(`Generated route manifest: ${outputPath}`);
}
/**
* Generate router configuration from manifest
*/
export function generateRouterConfig(manifest, outputPath) {
const imports = [];
const routeConfigs = [];
manifest.routes.forEach((route, index) => {
const importName = `Page${index}`;
const relativePath = route.filePath.replace(/\\/g, '/');
imports.push(`import ${importName} from '${relativePath}';`);
const config = `{
path: '${route.path}',
component: ${importName},
meta: {
dynamic: ${route.dynamic},
params: ${JSON.stringify(route.params)}
}
}`;
routeConfigs.push(config);
});
const code = `/**
* Auto-generated router configuration
* Do not edit manually
*/
${imports.join('\n')}
export const routerConfig = [
${routeConfigs.join(',\n ')}
];
`;
fs.writeFileSync(outputPath, code, 'utf8');
logger.success(`Generated router config: ${outputPath}`);
}
/**
* Watch pages directory for changes
*/
export function watchRoutes(pagesDir, onChange) {
const chokidar = require('chokidar');
const watcher = chokidar.watch(pagesDir, {
ignored: /(^|[\/\\])\../,
persistent: true
});
watcher.on('all', (event, filePath) => {
if (['add', 'unlink', 'addDir', 'unlinkDir'].includes(event)) {
logger.info(`Route change detected: ${event} ${filePath}`);
const manifest = scanRoutes(pagesDir);
onChange(manifest);
}
});
logger.info('Watching for route changes...');
}
//# sourceMappingURL=route-scanner.js.map