UNPKG

vjsrouter

Version:

A modern, file-system based router for vanilla JavaScript with SSR support.

150 lines (130 loc) 4.75 kB
// 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 };