/** * JQHTML SSR Environment * * Creates isolated jsdom environments for SSR rendering. * Each render request gets a fresh environment to ensure isolation. */ const { JSDOM } = require('jsdom'); const vm = require('vm'); const { createStoragePair } = require('./storage.js'); const { installHttpIntercept } = require('./http-intercept.js'); // CRITICAL: Require jQuery BEFORE any global.window is set // This ensures jQuery returns a factory function, not an auto-bound object const jqueryFactory = require('jquery'); /** * SSR Environment - Isolated rendering context */ class SSREnvironment { constructor(options = {}) { this.options = { baseUrl: options.baseUrl || 'http://localhost', timeout: options.timeout || 30000 }; this.dom = null; this.window = null; this.$ = null; this.storage = null; this.initialized = false; this.startTime = Date.now(); } /** * Initialize the jsdom environment with jQuery and all required globals */ init() { // Create jsdom instance this.dom = new JSDOM(` SSR `, { url: this.options.baseUrl, pretendToBeVisual: true, runScripts: 'outside-only' }); this.window = this.dom.window; // Create fake storage this.storage = createStoragePair(); // Set up global references that bundles expect global.window = this.window; global.document = this.window.document; global.HTMLElement = this.window.HTMLElement; global.Element = this.window.Element; global.Node = this.window.Node; global.NodeList = this.window.NodeList; global.DOMParser = this.window.DOMParser; global.Text = this.window.Text; global.DocumentFragment = this.window.DocumentFragment; global.Event = this.window.Event; global.CustomEvent = this.window.CustomEvent; global.MutationObserver = this.window.MutationObserver; // Window-like globals global.self = this.window; global.top = this.window; global.parent = this.window; global.frames = this.window; global.location = this.window.location; global.history = this.window.history; global.screen = this.window.screen; // NOTE: Don't set global.performance - causes issues between jsdom instances // The window.performance is available for code that accesses it via window global.requestAnimationFrame = (cb) => setTimeout(cb, 16); global.cancelAnimationFrame = (id) => clearTimeout(id); global.getComputedStyle = this.window.getComputedStyle.bind(this.window); // Install fake storage global.localStorage = this.storage.localStorage; global.sessionStorage = this.storage.sessionStorage; this.window.localStorage = this.storage.localStorage; this.window.sessionStorage = this.storage.sessionStorage; // Mark as SSR environment global.__SSR__ = true; global.__JQHTML_SSR_MODE__ = true; this.window.__SSR__ = true; this.window.__JQHTML_SSR_MODE__ = true; // Install HTTP interception (fetch + XHR URL rewriting) installHttpIntercept(this.window, this.options.baseUrl); // Create jQuery bound to jsdom window this.$ = jqueryFactory(this.window); // Make jQuery available globally global.$ = this.$; global.jQuery = this.$; this.window.$ = this.$; this.window.jQuery = this.$; // Stub console_debug if bundles use it this.window.console_debug = function() {}; global.console_debug = function() {}; // rsxapp config object (some bundles expect this) this.window.rsxapp = this.window.rsxapp || {}; global.rsxapp = this.window.rsxapp; this.initialized = true; } /** * Execute JavaScript code in the environment * @param {string} code - JavaScript code to execute * @param {string} filename - Filename for error reporting */ execute(code, filename = 'bundle.js') { if (!this.initialized) { throw new Error('SSR environment not initialized. Call init() first.'); } // Remove sourcemap comments to avoid errors const cleanCode = code.replace(/\/\/# sourceMappingURL=.*/g, ''); // Execute in Node's global context vm.runInThisContext(cleanCode, { filename, displayErrors: true }); } /** * Render a component to HTML * @param {string} componentName - Component name to render * @param {object} args - Arguments to pass to component * @returns {Promise<{ html: string, cache: object }>} */ async render(componentName, args = {}) { if (!this.initialized) { throw new Error('SSR environment not initialized. Call init() first.'); } const $ = global.$; // Create container const $container = $('
'); // Create component $container.component(componentName, args); const component = $container.component(); if (!component) { throw new Error(`Failed to create component: ${componentName}`); } // Wait for ready with timeout const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { reject(new Error(`Component render exceeded ${this.options.timeout}ms timeout`)); }, this.options.timeout); }); await Promise.race([ component.ready(), timeoutPromise ]); // Get rendered HTML const html = component.$.prop('outerHTML'); // Export cache state const cache = this.storage.exportAll(); return { html, cache }; } /** * Check if jqhtml runtime is available * @returns {boolean} */ isJqhtmlReady() { return !!(global.window && global.window.jqhtml); } /** * Get list of registered templates * @returns {string[]} */ getRegisteredTemplates() { if (global.window.jqhtml) { // Real jqhtml API if (typeof global.window.jqhtml.get_registered_templates === 'function') { return global.window.jqhtml.get_registered_templates(); } // Mock bundle API (for testing) if (global.window.jqhtml._templates) { return Array.from(global.window.jqhtml._templates.keys()); } } return []; } /** * Destroy the environment and clean up globals * * NOTE: We set globals to undefined instead of deleting them, * because jsdom expects certain globals to exist (even if undefined) * when creating new windows. */ destroy() { // Close jsdom window first if (this.window) { this.window.close(); } // Clean up global references by setting to undefined // This is safer than delete because some libraries check for existence global.window = undefined; global.document = undefined; global.HTMLElement = undefined; global.Element = undefined; global.Node = undefined; global.NodeList = undefined; global.DOMParser = undefined; global.Text = undefined; global.DocumentFragment = undefined; global.Event = undefined; global.CustomEvent = undefined; global.MutationObserver = undefined; global.self = undefined; global.top = undefined; global.parent = undefined; global.frames = undefined; global.location = undefined; global.history = undefined; global.screen = undefined; // Don't touch performance - jsdom needs it global.requestAnimationFrame = undefined; global.cancelAnimationFrame = undefined; global.getComputedStyle = undefined; global.localStorage = undefined; global.sessionStorage = undefined; global.$ = undefined; global.jQuery = undefined; global.__SSR__ = undefined; global.__JQHTML_SSR_MODE__ = undefined; global.console_debug = undefined; global.rsxapp = undefined; global.fetch = undefined; global.XMLHttpRequest = undefined; this.dom = null; this.window = null; this.$ = null; this.storage = null; this.initialized = false; } } module.exports = { SSREnvironment };