@ordojs/core
Version:
Core compiler and runtime for OrdoJS framework
258 lines • 8.79 kB
JavaScript
/**
* @fileoverview OrdoJS SSR Engine - Server-side rendering implementation
*/
import {} from '../types/index.js';
import { OrdoJSCodeGenerator } from './code-generator-fixed.js';
/**
* Default SSR options
*/
const DEFAULT_SSR_OPTIONS = {
includeHydrationMarkers: true,
includeHydrationData: true
};
/**
* OrdoJS SSR Engine
* Handles server-side rendering of components
*/
export class OrdoJSSSR {
options;
codeGenerator;
componentRegistry = new Map();
constructor(options = {}) {
this.options = { ...DEFAULT_SSR_OPTIONS, ...options };
this.codeGenerator = new OrdoJSCodeGenerator({
minify: false,
sourceMaps: false,
target: 'production'
});
}
/**
* Register a component for SSR
*/
registerComponent(ast) {
this.componentRegistry.set(ast.component.name, ast);
}
/**
* Register multiple components for SSR
*/
registerComponents(components) {
for (const component of components) {
this.registerComponent(component);
}
}
/**
* Render a component to HTML string
*/
async renderComponent(componentName, props = {}) {
const ast = this.componentRegistry.get(componentName);
if (!ast) {
throw new Error(`Component "${componentName}" not found in registry`);
}
// Fetch data if needed
let componentData = {};
if (this.options.dataFetcher) {
componentData = await this.options.dataFetcher(componentName, props);
}
// Generate HTML with hydration markers
const html = this.codeGenerator.generateHTML(ast, { ...props, ...componentData });
// Generate hydration data if needed
if (this.options.includeHydrationData) {
const hydrationData = this.generateHydrationData(ast, { ...props, ...componentData });
return this.injectHydrationData(html, hydrationData);
}
return html;
}
/**
* Generate hydration data for a component
*/
generateHydrationData(ast, data = {}) {
return {
componentName: ast.component.name,
componentId: `ordojs_${ast.component.name}_${Date.now().toString(36)}`,
props: data,
initialState: this.extractInitialState(ast),
version: '1.0'
};
}
/**
* Extract initial state from component AST
*/
extractInitialState(ast) {
const initialState = {};
if (ast.component.clientBlock) {
for (const variable of ast.component.clientBlock.reactiveVariables) {
if (variable.initialValue && variable.initialValue.expressionType === 'LITERAL') {
initialState[variable.name] = variable.initialValue.value;
}
else {
// For non-literal initial values, we'll use null as a placeholder
initialState[variable.name] = null;
}
}
}
return initialState;
}
/**
* Inject hydration data into HTML
*/
injectHydrationData(html, hydrationData) {
const hydrationScript = `
<script type="application/json" id="ordojs-hydration-data">
${JSON.stringify(hydrationData, null, 2)}
</script>
`;
// Insert before closing body tag if present, otherwise append
if (html.includes('</body>')) {
return html.replace('</body>', `${hydrationScript}</body>`);
}
return html + hydrationScript;
}
/**
* Handle data fetching for SSR
*/
async handleDataFetching(componentName, props = {}) {
const ast = this.componentRegistry.get(componentName);
if (!ast || !ast.component.serverBlock) {
return { props };
}
// Find data fetching functions in server block
const dataFetchers = ast.component.serverBlock.functions.filter(f => f.name === 'getServerSideProps' || f.name === 'getStaticProps');
if (dataFetchers.length === 0) {
return { props };
}
// Execute data fetcher function if available
if (this.options.dataFetcher) {
const data = await this.options.dataFetcher(componentName, props);
return {
props: { ...props, ...data }
};
}
return { props };
}
/**
* Render a route to HTML
*/
async renderRoute(url) {
if (!this.options.routes || this.options.routes.length === 0) {
throw new Error('No routes configured for SSR');
}
// Parse URL
const parsedUrl = new URL(url, 'http://localhost');
const pathname = parsedUrl.pathname;
// Find matching route
const route = this.findMatchingRoute(pathname);
if (!route) {
throw new Error(`No route found for path: ${pathname}`);
}
// Extract route params and query params
const params = this.extractRouteParams(route.path, pathname);
const query = this.extractQueryParams(parsedUrl.searchParams);
// Fetch data for route
let routeData = {};
if (route.dataFetcher) {
routeData = await route.dataFetcher(params, query);
}
// Render component with data
const componentHtml = await this.renderComponent(route.component, {
...routeData,
routeParams: params,
queryParams: query
});
// Wrap with layout if specified
if (route.layout) {
return this.wrapWithLayout(componentHtml, route.layout, {
...routeData,
routeParams: params,
queryParams: query
});
}
return componentHtml;
}
/**
* Find a matching route for a path
*/
findMatchingRoute(pathname) {
if (!this.options.routes)
return undefined;
return this.options.routes.find(route => {
// Convert route path to regex
const pattern = route.path
.replace(/:[^/]+/g, '([^/]+)')
.replace(/\*/g, '.*');
const regex = new RegExp(`^${pattern}$`);
return regex.test(pathname);
});
}
/**
* Extract route parameters from path
*/
extractRouteParams(routePath, pathname) {
const params = {};
// Extract param names from route path
const paramNames = (routePath.match(/:[^/]+/g) || [])
.map(param => param.substring(1));
// Extract param values from pathname
const pattern = routePath
.replace(/:[^/]+/g, '([^/]+)')
.replace(/\*/g, '.*');
const regex = new RegExp(`^${pattern}$`);
const matches = pathname.match(regex);
if (matches && matches.length > 1 && paramNames.length > 0) {
// Skip the first match (full string)
for (let i = 0; i < paramNames.length; i++) {
if (i < matches.length - 1) {
params[paramNames[i]] = matches[i + 1];
}
}
}
return params;
}
/**
* Extract query parameters from URL
*/
extractQueryParams(searchParams) {
const query = {};
searchParams.forEach((value, key) => {
query[key] = value;
});
return query;
}
/**
* Wrap component HTML with a layout
*/
async wrapWithLayout(componentHtml, layoutName, data) {
const ast = this.componentRegistry.get(layoutName);
if (!ast) {
throw new Error(`Layout "${layoutName}" not found in registry`);
}
// Replace layout content placeholder with component HTML
const layoutHtml = this.codeGenerator.generateHTML(ast, data);
// Replace content placeholder with component HTML
return layoutHtml.replace(/<div[^>]*data-content-placeholder[^>]*>.*?<\/div>/i, componentHtml);
}
/**
* Generate a complete HTML document with SSR content
*/
generateDocument(content, title = 'OrdoJS App', scripts = [], styles = []) {
const scriptTags = scripts
.map(src => `<script src="${src}" defer></script>`)
.join('\n ');
const styleTags = styles
.map(href => `<link rel="stylesheet" href="${href}">`)
.join('\n ');
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${title}</title>
${styleTags}
</head>
<body>
<div id="app">${content}</div>
${scriptTags}
</body>
</html>`;
}
}
//# sourceMappingURL=ssr-engine.js.map