vjsrouter
Version:
A modern, file-system based router for vanilla JavaScript with SSR support.
143 lines (118 loc) • 5.05 kB
JavaScript
// File: cli/commands/build.cjs
const fs = require('fs');
const path = require('path');
/**
* A robust recursive function to find all page files and layouts.
* It correctly maps layouts to their specific directories.
* @param {string} baseDir - The root 'pages' directory.
* @param {string} currentDir - The directory currently being scanned.
* @returns {{pages: string[], layouts: Map<string, string>}} An object containing page files and a map of layouts.
*/
function _recursivelyFindFiles(baseDir, currentDir) {
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
let pages = [];
const layouts = new Map();
const layoutFile = entries.find(entry => entry.isFile() && entry.name === '_layout.js');
if (layoutFile) {
const layoutPath = path.join(currentDir, layoutFile.name);
// Use '.' for the root directory to handle it correctly.
const relativeDir = path.relative(baseDir, currentDir) || '.';
layouts.set(relativeDir, path.relative(baseDir, layoutPath));
}
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
const nested = _recursivelyFindFiles(baseDir, fullPath);
pages = pages.concat(nested.pages);
nested.layouts.forEach((value, key) => layouts.set(key, value));
} else if (entry.isFile() && entry.name !== '_layout.js' && (entry.name.endsWith('.js') || entry.name.endsWith('.mjs'))) {
pages.push(path.relative(baseDir, fullPath));
}
}
return { pages, layouts };
}
/**
* Creates a clean, URL-friendly route path from a file path and handles dynamic segments.
* This version includes robust checks to prevent invalid path generation.
* @param {string} relativeFilePath - The file path relative to the pages directory.
* @returns {string} A clean, URL-friendly route path.
*/
function _createRoutePathFromFile(relativeFilePath) {
// Special case for the root index file to prevent any ambiguity.
if (relativeFilePath === 'index.js' || relativeFilePath === 'index.mjs') {
return '/';
}
const urlPath = relativeFilePath.split(path.sep).join('/');
let route = urlPath.replace(/\.m?js$/, '');
// More specific replacement for '/index' to avoid bugs with other paths.
if (route.endsWith('/index')) {
route = route.slice(0, -'/index'.length);
}
route = route.replace(/\[([^\]]+)\]/g, ':$1');
if (!route.startsWith('/')) {
route = '/' + route;
}
// This robust cleanup prevents invalid paths like `/page/` that could lead to errors.
if (route.length > 1 && route.endsWith('/')) {
route = route.slice(0, -1);
}
// If after all this, the route is empty, it must be the root.
if (route === '') {
return '/';
}
return route;
}
/**
* The main build function that scans the file system and generates the routes.json manifest.
* @param {Object} options - Configuration options for the build.
*/
function buildRoutesManifest(options = {}) {
const pagesDir = options.pagesDir || path.join(process.cwd(), 'pages');
const outputFile = options.outputFile || path.join(process.cwd(), 'routes.json');
console.log(`[vjsrouter-cli] Starting build...`);
console.log(`[vjsrouter-cli] > Scanning for pages and layouts in: ${pagesDir}`);
if (!fs.existsSync(pagesDir)) {
console.error(`\x1b[31m[vjsrouter-cli] ERROR: The specified pages directory does not exist: ${pagesDir}\x1b[0m`);
process.exit(1);
}
const { pages, layouts } = _recursivelyFindFiles(pagesDir, pagesDir);
if (pages.length === 0) {
console.warn(`\x1b[33m[vjsrouter-cli] WARNING: No page files were found.\x1b[0m`);
} else {
console.log(`[vjsrouter-cli] > Found ${pages.length} page files and ${layouts.size} layout files.`);
}
const routes = pages.map(pageFile => {
const routePath = _createRoutePathFromFile(pageFile);
const browserSafePagePath = '/pages/' + pageFile.split(path.sep).join('/');
const pageDir = path.dirname(pageFile);
const layoutHierarchy = [];
let currentDir = pageDir;
// This loop correctly walks up the directory tree to find all applicable layouts.
while (true) {
if (layouts.has(currentDir)) {
layoutHierarchy.unshift('/pages/' + layouts.get(currentDir).split(path.sep).join('/'));
}
if (currentDir === '.') break;
currentDir = path.dirname(currentDir);
}
return {
path: routePath,
file: browserSafePagePath,
layouts: layoutHierarchy,
};
});
routes.sort((a, b) => a.path.localeCompare(b.path));
const manifest = {
createdAt: new Date().toISOString(),
routeCount: routes.length,
routes: routes,
};
try {
fs.writeFileSync(outputFile, JSON.stringify(manifest, null, 2));
console.log(`\x1b[32m[vjsrouter-cli] SUCCESS: Route manifest created at ${outputFile}\x1b[0m`);
} catch (error) {
console.error(`\x1b[31m[vjsrouter-cli] ERROR: Failed to write manifest file to ${outputFile}\x1b[0m`, error);
process.exit(1);
}
}
module.exports = { buildRoutesManifest };