UNPKG

vjsrouter

Version:

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

89 lines (75 loc) 3.4 kB
// File: src/server/renderer.js const fs = require('fs'); const path = require('path'); const { JSDOM } = require('jsdom'); // The library to simulate a DOM on the server. /** * Renders a page component and its layouts into a JSDOM instance. * @param {string} projectRoot - The absolute path to the project's root directory. * @param {Object} resolution - The resolved route object from VJSServerRouter. * @returns {Promise<{appHtml: string, initialData: Object}>} The rendered HTML of the app and the initial data for hydration. */ async function renderComponentTree(projectRoot, resolution) { const { route, params, data, error } = resolution; // Create a new JSDOM instance for each request. This provides a clean, isolated DOM. const dom = new JSDOM(); const { document } = dom.window; // --- Render the Page Component --- const pageModulePath = path.join(projectRoot, route.file); const pageModule = await import(pageModulePath); const PageComponent = pageModule.default; if (typeof PageComponent !== 'function') { throw new Error(`Default export for page ${route.file} is not a function.`); } const props = { data, params, error }; let finalElement = PageComponent(props); if (!(finalElement instanceof document.defaultView.HTMLElement)) { throw new Error(`Component for ${route.path} did not return an HTMLElement.`); } // --- Render Layouts (if any) --- if (route.layouts && route.layouts.length > 0) { for (let i = route.layouts.length - 1; i >= 0; i--) { const layoutPath = route.layouts[i]; const layoutModulePath = path.join(projectRoot, layoutPath); const layoutModule = await import(layoutModulePath); const LayoutComponent = layoutModule.default; if (typeof LayoutComponent !== 'function') { throw new Error(`Default export for layout ${layoutPath} is not a function.`); } finalElement = LayoutComponent(finalElement); if (!(finalElement instanceof document.defaultView.HTMLElement)) { throw new Error(`Layout ${layoutPath} did not return an HTMLElement.`); } } } // The final rendered HTML content of the #app div. const appHtml = finalElement.outerHTML; // The data to be injected for client-side hydration. const initialData = { data, params, error }; return { appHtml, initialData }; } /** * The main server-side rendering function. * It takes a base HTML template and injects the rendered app and data into it. * @param {string} template - The content of the main index.html file. * @param {string} appHtml - The rendered HTML of the vjsrouter application. * @param {Object} initialData - The data to be hydrated on the client. * @returns {string} The final, complete HTML page to be sent to the browser. */ function renderFullPage(template, appHtml, initialData) { // Inject the rendered app HTML into the <div id="app"></div>. const pageWithApp = template.replace( '<div id="app"></div>', `<div id="app">${appHtml}</div>` ); // Inject the initial data script just before the closing </body> tag. // Using JSON.stringify prevents XSS attacks by escaping special characters. const finalHtml = pageWithApp.replace( '</body>', `<script>window.__INITIAL_DATA__ = ${JSON.stringify(initialData)};</script></body>` ); return finalHtml; } module.exports = { renderComponentTree, renderFullPage };