vjsrouter
Version:
A modern, file-system based router for vanilla JavaScript with SSR support.
89 lines (75 loc) • 3.4 kB
JavaScript
// 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
};