Files
rspade_system/node_modules/@jqhtml/ssr/src/environment.js
root 14dd2fd223 Fix code quality violations for publish
Progressive breadcrumb resolution with caching, fix double headers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-16 04:43:47 +00:00

269 lines
7.8 KiB
JavaScript

/**
* 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(`
<!DOCTYPE html>
<html>
<head><title>SSR</title></head>
<body></body>
</html>
`, {
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 = $('<div>');
// 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 };