vjsrouter
Version:
A modern, file-system based router for vanilla JavaScript with SSR support.
150 lines (130 loc) • 4.75 kB
JavaScript
// File: src/server/VJSServerRouter.js
// We use Node.js's built-in 'fs' and 'path' modules for file system operations.
const fs = require('fs');
const path = require('path');
/**
* A router class designed to run in a Node.js environment for Server-Side Rendering.
* It does not interact with the DOM or browser APIs. Its job is to resolve a URL path
* to the correct page component, layout hierarchy, and pre-fetched data.
*/
class VJSServerRouter {
#routes = [];
#notFoundRoute = null;
#manifestPath;
/**
* @constructor
* @param {Object} options - Configuration options for the server router.
* @param {string} options.manifestPath - The absolute path to the routes.json manifest file.
*/
constructor(options) {
if (!options || !options.manifestPath) {
throw new Error('VJSServerRouter requires a `manifestPath` option.');
}
this.#manifestPath = options.manifestPath;
this.#loadAndProcessRoutes();
}
/**
* Synchronously loads and processes the route manifest from the file system.
* @private
*/
#loadAndProcessRoutes() {
try {
const manifestContent = fs.readFileSync(this.#manifestPath, 'utf-8');
const manifest = JSON.parse(manifestContent);
if (!manifest || !Array.isArray(manifest.routes)) {
throw new Error('Invalid routes manifest format on server.');
}
const regularRoutes = manifest.routes.filter(route => {
if (route.path === '/404') {
this.#notFoundRoute = route;
return false;
}
return true;
});
this.#routes = regularRoutes.map(route => {
const paramNames = [];
const regexPath = route.path.replace(/:([^/]+)/g, (_, paramName) => {
paramNames.push(paramName);
return '([^/]+)';
});
return { ...route, paramNames, regex: new RegExp(`^${regexPath}$`) };
});
} catch (error) {
console.error('[VJSServerRouter] Failed to load or parse route manifest:', error);
throw new Error('Could not initialize server router.');
}
}
/**
* Finds a matching route for a given path and extracts URL parameters.
* @param {string} path - The URL path to match.
* @returns {{route: Object, params: Object}|null} The matched route and params, or null.
* @private
*/
#findRoute(path) {
for (const route of this.#routes) {
const match = path.match(route.regex);
if (match) {
const params = {};
for (let i = 0; i < route.paramNames.length; i++) {
params[route.paramNames[i]] = match[i + 1];
}
return { route, params };
}
}
return null;
}
/**
* Resolves a URL path to its corresponding route definition and parameters.
* This is the main public method for the server router.
* @param {string} path - The URL path from an incoming server request.
* @returns {Promise<{route: Object, params: Object, data: Object, error: Error|null}>} An object containing everything needed to render the page.
*/
async resolve(path) {
const match = this.#findRoute(path);
let routeToRender;
let params = {};
if (!match) {
// If no route matches, we fall back to the 404 route.
if (!this.#notFoundRoute) {
throw new Error(`No route found for path "${path}" and no 404 route is defined.`);
}
routeToRender = this.#notFoundRoute;
} else {
routeToRender = match.route;
params = match.params;
}
// We need to resolve the file path relative to the project root.
// This assumes the server is run from the project root.
const projectRoot = process.cwd();
const modulePath = path.join(projectRoot, routeToRender.file);
let loadedData = {};
let loadError = null;
try {
// Dynamically import the page module in the Node.js environment.
const pageModule = await import(modulePath);
// If the page has a 'load' function, execute it to pre-fetch data.
if (typeof pageModule.load === 'function') {
loadedData = await pageModule.load(params);
}
return {
route: routeToRender,
params: params,
data: loadedData,
error: null
};
} catch (error) {
console.error(`[VJSServerRouter] Error during server-side data loading for path "${path}":`, error);
loadError = error;
// Even if data loading fails, we still return the route information
// so we can attempt to render an error state.
return {
route: routeToRender,
params: params,
data: {},
error: loadError
};
}
}
}
// Export the class for use in our server entry point.
module.exports = { VJSServerRouter };