UNPKG

@ordojs/core

Version:

Core compiler and runtime for OrdoJS framework

258 lines 8.79 kB
/** * @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