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>
This commit is contained in:
root
2025-12-16 04:43:47 +00:00
parent ba3268caca
commit 14dd2fd223
1456 changed files with 136243 additions and 7631 deletions

141
node_modules/@jqhtml/ssr/src/bundle-cache.js generated vendored Normal file
View File

@@ -0,0 +1,141 @@
/**
* JQHTML SSR Bundle Cache
*
* Caches parsed bundle code to avoid re-parsing on every request.
* Uses LRU (Least Recently Used) eviction strategy.
*/
/**
* LRU Cache for bundle sets
*/
class BundleCache {
constructor(maxSize = 10) {
this.maxSize = maxSize;
this.cache = new Map(); // bundleSetId -> { code, lastUsed }
}
/**
* Generate a cache key from bundle array
* @param {Array<{id: string, content: string}>} bundles
* @returns {string} Cache key
*/
static generateKey(bundles) {
// Key is concatenation of all bundle IDs in order
return bundles.map(b => b.id).join('|');
}
/**
* Get cached bundle code if available
* @param {string} key - Cache key from generateKey()
* @returns {string|null} Concatenated bundle code or null if not cached
*/
get(key) {
const entry = this.cache.get(key);
if (entry) {
entry.lastUsed = Date.now();
return entry.code;
}
return null;
}
/**
* Store bundle code in cache
* @param {string} key - Cache key
* @param {string} code - Concatenated bundle code
*/
set(key, code) {
// Evict if at capacity
if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
this._evictLRU();
}
this.cache.set(key, {
code,
lastUsed: Date.now()
});
}
/**
* Check if a bundle set is cached
* @param {string} key - Cache key
* @returns {boolean}
*/
has(key) {
return this.cache.has(key);
}
/**
* Remove a specific bundle set from cache
* @param {string} key - Cache key
* @returns {boolean} True if entry was removed
*/
delete(key) {
return this.cache.delete(key);
}
/**
* Clear all cached bundles
*/
clear() {
this.cache.clear();
}
/**
* Get cache statistics
* @returns {{ size: number, maxSize: number, keys: string[] }}
*/
stats() {
return {
size: this.cache.size,
maxSize: this.maxSize,
keys: Array.from(this.cache.keys())
};
}
/**
* Evict the least recently used entry
* @private
*/
_evictLRU() {
let oldestKey = null;
let oldestTime = Infinity;
for (const [key, entry] of this.cache) {
if (entry.lastUsed < oldestTime) {
oldestTime = entry.lastUsed;
oldestKey = key;
}
}
if (oldestKey) {
this.cache.delete(oldestKey);
}
}
}
/**
* Prepare bundle code for execution
* Concatenates bundles and removes problematic patterns
* @param {Array<{id: string, content: string}>} bundles
* @returns {string} Prepared code
*/
function prepareBundleCode(bundles) {
const codeChunks = [];
for (const bundle of bundles) {
// Remove sourcemap comments
let code = bundle.content.replace(/\/\/# sourceMappingURL=.*/g, '');
// Add bundle marker comment for debugging
code = `\n/* === Bundle: ${bundle.id} === */\n${code}`;
codeChunks.push(code);
}
return codeChunks.join('\n');
}
module.exports = {
BundleCache,
prepareBundleCode
};

268
node_modules/@jqhtml/ssr/src/environment.js generated vendored Normal file
View File

@@ -0,0 +1,268 @@
/**
* 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 };

171
node_modules/@jqhtml/ssr/src/http-intercept.js generated vendored Normal file
View File

@@ -0,0 +1,171 @@
/**
* JQHTML SSR HTTP Interception
*
* Intercepts fetch() and XMLHttpRequest to rewrite relative URLs
* using the configured baseUrl.
*/
const http = require('http');
const https = require('https');
/**
* Resolve a URL relative to a base URL
* @param {string} url - URL to resolve (may be relative)
* @param {string} baseUrl - Base URL (e.g., "https://example.com")
* @returns {string} Absolute URL
*/
function resolveUrl(url, baseUrl) {
// Already absolute with protocol
if (url.startsWith('http://') || url.startsWith('https://')) {
return url;
}
// Protocol-relative (//cdn.example.com/...)
if (url.startsWith('//')) {
const baseProtocol = new URL(baseUrl).protocol;
return baseProtocol + url;
}
// Absolute path (/api/users)
if (url.startsWith('/')) {
const base = new URL(baseUrl);
return `${base.protocol}//${base.host}${url}`;
}
// Relative path (api/users)
const base = new URL(baseUrl);
const basePath = base.pathname.endsWith('/') ? base.pathname : base.pathname + '/';
return `${base.protocol}//${base.host}${basePath}${url}`;
}
/**
* Create a fetch function that rewrites URLs
* @param {string} baseUrl - Base URL for relative URL resolution
* @returns {function} Configured fetch function
*/
function createFetch(baseUrl) {
// Use native fetch if available (Node 18+), otherwise use a simple implementation
const nativeFetch = globalThis.fetch;
if (nativeFetch) {
return async function ssrFetch(url, options = {}) {
const resolvedUrl = resolveUrl(String(url), baseUrl);
// Remove any auth-related headers for SSR (SEO mode)
const safeHeaders = { ...(options.headers || {}) };
delete safeHeaders['Authorization'];
delete safeHeaders['authorization'];
delete safeHeaders['Cookie'];
delete safeHeaders['cookie'];
return nativeFetch(resolvedUrl, {
...options,
headers: safeHeaders
});
};
}
// Fallback for older Node versions - basic fetch implementation
return function ssrFetch(url, options = {}) {
return new Promise((resolve, reject) => {
const resolvedUrl = resolveUrl(String(url), baseUrl);
const parsedUrl = new URL(resolvedUrl);
const isHttps = parsedUrl.protocol === 'https:';
const lib = isHttps ? https : http;
const requestOptions = {
hostname: parsedUrl.hostname,
port: parsedUrl.port || (isHttps ? 443 : 80),
path: parsedUrl.pathname + parsedUrl.search,
method: options.method || 'GET',
headers: options.headers || {}
};
// Remove auth headers
delete requestOptions.headers['Authorization'];
delete requestOptions.headers['authorization'];
delete requestOptions.headers['Cookie'];
delete requestOptions.headers['cookie'];
const req = lib.request(requestOptions, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
resolve({
ok: res.statusCode >= 200 && res.statusCode < 300,
status: res.statusCode,
statusText: res.statusMessage,
headers: new Map(Object.entries(res.headers)),
text: () => Promise.resolve(data),
json: () => Promise.resolve(JSON.parse(data))
});
});
});
req.on('error', reject);
if (options.body) {
req.write(options.body);
}
req.end();
});
};
}
/**
* Create an XMLHttpRequest class that rewrites URLs
* This is needed for jQuery $.ajax compatibility
* @param {string} baseUrl - Base URL for relative URL resolution
* @param {function} OriginalXHR - Original XMLHttpRequest constructor from jsdom
* @returns {function} Wrapped XMLHttpRequest constructor
*/
function createXHRWrapper(baseUrl, OriginalXHR) {
return class SSR_XMLHttpRequest extends OriginalXHR {
open(method, url, async = true, user, password) {
const resolvedUrl = resolveUrl(String(url), baseUrl);
return super.open(method, resolvedUrl, async, user, password);
}
setRequestHeader(name, value) {
// Block auth headers for SSR
const lowerName = name.toLowerCase();
if (lowerName === 'authorization' || lowerName === 'cookie') {
return;
}
return super.setRequestHeader(name, value);
}
};
}
/**
* Install HTTP interception on a jsdom window
* @param {Window} window - jsdom window object
* @param {string} baseUrl - Base URL for URL resolution
*/
function installHttpIntercept(window, baseUrl) {
// Override fetch
window.fetch = createFetch(baseUrl);
// Also set on global for code that accesses global.fetch
if (typeof global !== 'undefined') {
global.fetch = window.fetch;
}
// Override XMLHttpRequest if it exists
if (window.XMLHttpRequest) {
const WrappedXHR = createXHRWrapper(baseUrl, window.XMLHttpRequest);
window.XMLHttpRequest = WrappedXHR;
if (typeof global !== 'undefined') {
global.XMLHttpRequest = WrappedXHR;
}
}
}
module.exports = {
resolveUrl,
createFetch,
createXHRWrapper,
installHttpIntercept
};

52
node_modules/@jqhtml/ssr/src/index.js generated vendored Normal file
View File

@@ -0,0 +1,52 @@
/**
* JQHTML SSR - Server-Side Rendering
*
* @module @jqhtml/ssr
*/
const { SSRServer } = require('./server.js');
const { SSREnvironment } = require('./environment.js');
const { BundleCache, prepareBundleCode } = require('./bundle-cache.js');
const { createStoragePair, SSR_Storage } = require('./storage.js');
const { resolveUrl, createFetch, installHttpIntercept } = require('./http-intercept.js');
const {
ErrorCodes,
parseRequest,
successResponse,
errorResponse,
renderResponse,
pingResponse,
flushCacheResponse,
MessageBuffer
} = require('./protocol.js');
module.exports = {
// Main server
SSRServer,
// Environment for direct use
SSREnvironment,
// Bundle caching
BundleCache,
prepareBundleCode,
// Storage
SSR_Storage,
createStoragePair,
// HTTP interception
resolveUrl,
createFetch,
installHttpIntercept,
// Protocol
ErrorCodes,
parseRequest,
successResponse,
errorResponse,
renderResponse,
pingResponse,
flushCacheResponse,
MessageBuffer
};

338
node_modules/@jqhtml/ssr/src/protocol.js generated vendored Normal file
View File

@@ -0,0 +1,338 @@
/**
* JQHTML SSR Protocol
*
* JSON-based protocol with newline-delimited messages for TCP/Unix socket communication.
*
* Request format:
* {
* "id": "request-uuid",
* "type": "render" | "ping" | "flush_cache",
* "payload": { ... }
* }
*
* Response format:
* {
* "id": "request-uuid",
* "status": "success" | "error",
* "payload": { ... } | "error": { code, message, stack }
* }
*/
// Error codes matching HTTP semantics
const ErrorCodes = {
PARSE_ERROR: 'PARSE_ERROR', // 400 - Invalid request JSON
BUNDLE_ERROR: 'BUNDLE_ERROR', // 400 - Bundle failed to parse/execute
COMPONENT_NOT_FOUND: 'COMPONENT_NOT_FOUND', // 404 - Component not registered
RENDER_ERROR: 'RENDER_ERROR', // 500 - Component threw during lifecycle
RENDER_TIMEOUT: 'RENDER_TIMEOUT', // 504 - Render exceeded timeout
INTERNAL_ERROR: 'INTERNAL_ERROR' // 500 - Unexpected server error
};
/**
* Parse a request message from JSON string
* @param {string} message - Raw JSON string
* @returns {{ ok: true, request: object } | { ok: false, error: object }}
*/
function parseRequest(message) {
let parsed;
try {
parsed = JSON.parse(message);
} catch (e) {
return {
ok: false,
error: {
code: ErrorCodes.PARSE_ERROR,
message: `Invalid JSON: ${e.message}`,
stack: e.stack
}
};
}
// Validate required fields
if (!parsed.id || typeof parsed.id !== 'string') {
return {
ok: false,
error: {
code: ErrorCodes.PARSE_ERROR,
message: 'Missing or invalid "id" field (must be string)'
}
};
}
if (!parsed.type || typeof parsed.type !== 'string') {
return {
ok: false,
error: {
code: ErrorCodes.PARSE_ERROR,
message: 'Missing or invalid "type" field (must be string)'
}
};
}
const validTypes = ['render', 'ping', 'flush_cache'];
if (!validTypes.includes(parsed.type)) {
return {
ok: false,
error: {
code: ErrorCodes.PARSE_ERROR,
message: `Invalid request type "${parsed.type}". Must be one of: ${validTypes.join(', ')}`
}
};
}
// Type-specific validation
if (parsed.type === 'render') {
const validation = validateRenderPayload(parsed.payload);
if (!validation.ok) {
return validation;
}
}
return { ok: true, request: parsed };
}
/**
* Validate render request payload
* @param {object} payload
* @returns {{ ok: true } | { ok: false, error: object }}
*/
function validateRenderPayload(payload) {
if (!payload || typeof payload !== 'object') {
return {
ok: false,
error: {
code: ErrorCodes.PARSE_ERROR,
message: 'Missing or invalid "payload" field for render request'
}
};
}
// Validate bundles
if (!Array.isArray(payload.bundles) || payload.bundles.length === 0) {
return {
ok: false,
error: {
code: ErrorCodes.PARSE_ERROR,
message: 'payload.bundles must be a non-empty array'
}
};
}
for (let i = 0; i < payload.bundles.length; i++) {
const bundle = payload.bundles[i];
if (!bundle.id || typeof bundle.id !== 'string') {
return {
ok: false,
error: {
code: ErrorCodes.PARSE_ERROR,
message: `payload.bundles[${i}].id must be a string`
}
};
}
if (!bundle.content || typeof bundle.content !== 'string') {
return {
ok: false,
error: {
code: ErrorCodes.PARSE_ERROR,
message: `payload.bundles[${i}].content must be a string`
}
};
}
}
// Validate component name
if (!payload.component || typeof payload.component !== 'string') {
return {
ok: false,
error: {
code: ErrorCodes.PARSE_ERROR,
message: 'payload.component must be a string'
}
};
}
// Validate options
if (payload.options) {
if (typeof payload.options !== 'object') {
return {
ok: false,
error: {
code: ErrorCodes.PARSE_ERROR,
message: 'payload.options must be an object'
}
};
}
if (!payload.options.baseUrl || typeof payload.options.baseUrl !== 'string') {
return {
ok: false,
error: {
code: ErrorCodes.PARSE_ERROR,
message: 'payload.options.baseUrl is required and must be a string'
}
};
}
if (payload.options.timeout !== undefined && typeof payload.options.timeout !== 'number') {
return {
ok: false,
error: {
code: ErrorCodes.PARSE_ERROR,
message: 'payload.options.timeout must be a number'
}
};
}
} else {
return {
ok: false,
error: {
code: ErrorCodes.PARSE_ERROR,
message: 'payload.options is required (must include baseUrl)'
}
};
}
// args is optional, but if present must be object
if (payload.args !== undefined && (typeof payload.args !== 'object' || payload.args === null)) {
return {
ok: false,
error: {
code: ErrorCodes.PARSE_ERROR,
message: 'payload.args must be an object if provided'
}
};
}
return { ok: true };
}
/**
* Create a success response
* @param {string} id - Request ID
* @param {object} payload - Response payload
* @returns {string} JSON string with newline
*/
function successResponse(id, payload) {
return JSON.stringify({
id,
status: 'success',
payload
}) + '\n';
}
/**
* Create an error response
* @param {string} id - Request ID (or null if unknown)
* @param {string} code - Error code from ErrorCodes
* @param {string} message - Human-readable error message
* @param {string} [stack] - Optional stack trace
* @returns {string} JSON string with newline
*/
function errorResponse(id, code, message, stack) {
return JSON.stringify({
id: id || 'unknown',
status: 'error',
error: {
code,
message,
...(stack && { stack })
}
}) + '\n';
}
/**
* Create a render success response
* @param {string} id - Request ID
* @param {string} html - Rendered HTML
* @param {object} cache - Cache state { localStorage, sessionStorage }
* @param {object} timing - Timing info { total_ms, bundle_load_ms, render_ms }
* @returns {string} JSON string with newline
*/
function renderResponse(id, html, cache, timing) {
return successResponse(id, {
html,
cache,
timing
});
}
/**
* Create a ping response
* @param {string} id - Request ID
* @param {number} uptime_ms - Server uptime in milliseconds
* @returns {string} JSON string with newline
*/
function pingResponse(id, uptime_ms) {
return successResponse(id, { uptime_ms });
}
/**
* Create a flush_cache response
* @param {string} id - Request ID
* @param {boolean} flushed - Whether flush was successful
* @returns {string} JSON string with newline
*/
function flushCacheResponse(id, flushed) {
return successResponse(id, { flushed });
}
/**
* MessageBuffer - Handles newline-delimited message framing
*
* TCP streams don't guarantee message boundaries. This class buffers
* incoming data and emits complete messages (delimited by newlines).
*/
class MessageBuffer {
constructor() {
this.buffer = '';
}
/**
* Add data to buffer and return any complete messages
* @param {string} data - Incoming data chunk
* @returns {string[]} Array of complete messages
*/
push(data) {
this.buffer += data;
const messages = [];
let newlineIndex;
while ((newlineIndex = this.buffer.indexOf('\n')) !== -1) {
const message = this.buffer.slice(0, newlineIndex);
this.buffer = this.buffer.slice(newlineIndex + 1);
if (message.trim()) {
messages.push(message);
}
}
return messages;
}
/**
* Check if there's incomplete data in the buffer
* @returns {boolean}
*/
hasIncomplete() {
return this.buffer.trim().length > 0;
}
/**
* Clear the buffer
*/
clear() {
this.buffer = '';
}
}
module.exports = {
ErrorCodes,
parseRequest,
validateRenderPayload,
successResponse,
errorResponse,
renderResponse,
pingResponse,
flushCacheResponse,
MessageBuffer
};

410
node_modules/@jqhtml/ssr/src/server.js generated vendored Executable file
View File

@@ -0,0 +1,410 @@
#!/usr/bin/env node
/**
* JQHTML SSR Server
*
* Long-running Node.js process that accepts render requests via TCP or Unix socket.
* See SPECIFICATION.md for protocol details.
*/
const net = require('net');
const fs = require('fs');
const path = require('path');
const {
ErrorCodes,
parseRequest,
renderResponse,
pingResponse,
flushCacheResponse,
errorResponse,
MessageBuffer
} = require('./protocol.js');
const { SSREnvironment } = require('./environment.js');
const { BundleCache, prepareBundleCode } = require('./bundle-cache.js');
/**
* SSR Server
*/
class SSRServer {
constructor(options = {}) {
this.options = {
maxBundles: options.maxBundles || 10,
defaultTimeout: options.defaultTimeout || 30000,
...options
};
this.server = null;
this.bundleCache = new BundleCache(this.options.maxBundles);
this.startTime = Date.now();
this.requestCount = 0;
}
/**
* Start server on TCP port
* @param {number} port
* @returns {Promise<void>}
*/
listenTCP(port) {
return new Promise((resolve, reject) => {
this.server = net.createServer((socket) => this._handleConnection(socket));
this.server.on('error', reject);
this.server.listen(port, () => {
console.log(`[SSR] Server listening on tcp://0.0.0.0:${port}`);
resolve();
});
});
}
/**
* Start server on Unix socket
* @param {string} socketPath
* @returns {Promise<void>}
*/
listenUnix(socketPath) {
return new Promise((resolve, reject) => {
// Remove existing socket file if present
if (fs.existsSync(socketPath)) {
fs.unlinkSync(socketPath);
}
this.server = net.createServer((socket) => this._handleConnection(socket));
this.server.on('error', reject);
this.server.listen(socketPath, () => {
// Set socket permissions (owner read/write only)
fs.chmodSync(socketPath, 0o600);
console.log(`[SSR] Server listening on unix://${socketPath}`);
resolve();
});
});
}
/**
* Handle incoming connection
* @param {net.Socket} socket
* @private
*/
_handleConnection(socket) {
const messageBuffer = new MessageBuffer();
const remoteId = `${socket.remoteAddress || 'unix'}:${socket.remotePort || 'local'}`;
socket.on('data', async (data) => {
const messages = messageBuffer.push(data.toString());
for (const message of messages) {
await this._handleMessage(socket, message);
}
});
socket.on('error', (err) => {
console.error(`[SSR] Socket error (${remoteId}):`, err.message);
});
socket.on('close', () => {
if (messageBuffer.hasIncomplete()) {
console.warn(`[SSR] Connection closed with incomplete message (${remoteId})`);
}
});
}
/**
* Handle a single message
* @param {net.Socket} socket
* @param {string} message
* @private
*/
async _handleMessage(socket, message) {
this.requestCount++;
const requestId = this.requestCount;
// Parse request
const parseResult = parseRequest(message);
if (!parseResult.ok) {
const response = errorResponse(
null,
parseResult.error.code,
parseResult.error.message,
parseResult.error.stack
);
socket.write(response);
return;
}
const request = parseResult.request;
try {
let response;
switch (request.type) {
case 'ping':
response = this._handlePing(request);
break;
case 'flush_cache':
response = this._handleFlushCache(request);
break;
case 'render':
response = await this._handleRender(request);
break;
default:
response = errorResponse(
request.id,
ErrorCodes.PARSE_ERROR,
`Unknown request type: ${request.type}`
);
}
socket.write(response);
} catch (err) {
console.error(`[SSR] Error handling request ${request.id}:`, err);
const response = errorResponse(
request.id,
ErrorCodes.INTERNAL_ERROR,
err.message,
err.stack
);
socket.write(response);
}
}
/**
* Handle ping request
* @param {object} request
* @returns {string} Response
* @private
*/
_handlePing(request) {
const uptime_ms = Date.now() - this.startTime;
return pingResponse(request.id, uptime_ms);
}
/**
* Handle flush_cache request
* @param {object} request
* @returns {string} Response
* @private
*/
_handleFlushCache(request) {
const bundleId = request.payload?.bundle_id;
if (bundleId) {
// Flush specific bundle - need to find all cache keys containing this bundle
const stats = this.bundleCache.stats();
let flushed = false;
for (const key of stats.keys) {
if (key.includes(bundleId)) {
this.bundleCache.delete(key);
flushed = true;
}
}
return flushCacheResponse(request.id, flushed);
} else {
// Flush all
this.bundleCache.clear();
return flushCacheResponse(request.id, true);
}
}
/**
* Handle render request
* @param {object} request
* @returns {Promise<string>} Response
* @private
*/
async _handleRender(request) {
const startTime = Date.now();
const payload = request.payload;
// Get or prepare bundle code
const cacheKey = BundleCache.generateKey(payload.bundles);
let bundleCode = this.bundleCache.get(cacheKey);
let bundleLoadMs = 0;
if (!bundleCode) {
const bundleStartTime = Date.now();
bundleCode = prepareBundleCode(payload.bundles);
this.bundleCache.set(cacheKey, bundleCode);
bundleLoadMs = Date.now() - bundleStartTime;
}
// Create fresh environment for this request
const env = new SSREnvironment({
baseUrl: payload.options.baseUrl,
timeout: payload.options.timeout || this.options.defaultTimeout
});
try {
// Initialize environment
env.init();
// Execute bundle code
const execStartTime = Date.now();
env.execute(bundleCode, `bundles:${cacheKey}`);
bundleLoadMs += Date.now() - execStartTime;
// Check if jqhtml loaded
if (!env.isJqhtmlReady()) {
throw new Error('jqhtml runtime not available after loading bundles');
}
// Check if component is registered
const templates = env.getRegisteredTemplates();
if (!templates.includes(payload.component)) {
return errorResponse(
request.id,
ErrorCodes.COMPONENT_NOT_FOUND,
`Component "${payload.component}" not found. Available: ${templates.slice(0, 10).join(', ')}${templates.length > 10 ? '...' : ''}`
);
}
// Render component
const renderStartTime = Date.now();
const result = await env.render(payload.component, payload.args || {});
const renderMs = Date.now() - renderStartTime;
const totalMs = Date.now() - startTime;
return renderResponse(request.id, result.html, result.cache, {
total_ms: totalMs,
bundle_load_ms: bundleLoadMs,
render_ms: renderMs
});
} catch (err) {
// Determine error type
let errorCode = ErrorCodes.RENDER_ERROR;
if (err.message.includes('timeout')) {
errorCode = ErrorCodes.RENDER_TIMEOUT;
} else if (err.message.includes('SyntaxError') || err.message.includes('parse')) {
errorCode = ErrorCodes.BUNDLE_ERROR;
}
return errorResponse(request.id, errorCode, err.message, err.stack);
} finally {
// Always destroy environment to free resources
env.destroy();
}
}
/**
* Stop the server
* @returns {Promise<void>}
*/
stop() {
return new Promise((resolve) => {
if (this.server) {
this.server.close(() => {
console.log('[SSR] Server stopped');
resolve();
});
} else {
resolve();
}
});
}
/**
* Get server statistics
* @returns {object}
*/
stats() {
return {
uptime_ms: Date.now() - this.startTime,
request_count: this.requestCount,
bundle_cache: this.bundleCache.stats()
};
}
}
module.exports = { SSRServer };
// CLI entry point
if (require.main === module) {
const args = process.argv.slice(2);
let tcpPort = null;
let socketPath = null;
let maxBundles = 10;
let timeout = 30000;
// Parse arguments
for (let i = 0; i < args.length; i++) {
switch (args[i]) {
case '--tcp':
tcpPort = parseInt(args[++i], 10);
break;
case '--socket':
socketPath = args[++i];
break;
case '--max-bundles':
maxBundles = parseInt(args[++i], 10);
break;
case '--timeout':
timeout = parseInt(args[++i], 10);
break;
case '--help':
console.log(`
JQHTML SSR Server
Usage:
node server.js --tcp <port>
node server.js --socket <path>
Options:
--tcp <port> Listen on TCP port
--socket <path> Listen on Unix socket
--max-bundles <n> Max cached bundle sets (default: 10)
--timeout <ms> Default render timeout (default: 30000)
--help Show this help
`);
process.exit(0);
}
}
if (!tcpPort && !socketPath) {
console.error('Error: Must specify --tcp <port> or --socket <path>');
process.exit(1);
}
const server = new SSRServer({ maxBundles, defaultTimeout: timeout });
// Start server
(async () => {
try {
if (tcpPort) {
await server.listenTCP(tcpPort);
} else {
await server.listenUnix(socketPath);
}
} catch (err) {
console.error('[SSR] Failed to start server:', err);
process.exit(1);
}
})();
// Graceful shutdown
process.on('SIGINT', async () => {
console.log('\n[SSR] Shutting down...');
await server.stop();
process.exit(0);
});
process.on('SIGTERM', async () => {
console.log('\n[SSR] Shutting down...');
await server.stop();
process.exit(0);
});
}

90
node_modules/@jqhtml/ssr/src/storage.js generated vendored Normal file
View File

@@ -0,0 +1,90 @@
/**
* JQHTML SSR Fake Storage
*
* Implements localStorage/sessionStorage API that captures all writes
* for export at the end of SSR render.
*/
class SSR_Storage {
constructor(name = 'storage') {
this._name = name;
this._data = new Map();
}
getItem(key) {
const value = this._data.get(key);
return value !== undefined ? value : null;
}
setItem(key, value) {
this._data.set(key, String(value));
}
removeItem(key) {
this._data.delete(key);
}
clear() {
this._data.clear();
}
get length() {
return this._data.size;
}
key(index) {
const keys = Array.from(this._data.keys());
return index < keys.length ? keys[index] : null;
}
/**
* SSR-specific: Export all data as plain object
* @returns {Object} Key-value pairs
*/
_export() {
return Object.fromEntries(this._data);
}
/**
* SSR-specific: Import data (for testing or pre-population)
* @param {Object} data - Key-value pairs to import
*/
_import(data) {
for (const [key, value] of Object.entries(data)) {
this._data.set(key, String(value));
}
}
/**
* SSR-specific: Check if any data was written
* @returns {boolean}
*/
_hasData() {
return this._data.size > 0;
}
}
/**
* Create a pair of storage instances for an SSR request
* @returns {{ localStorage: SSR_Storage, sessionStorage: SSR_Storage, exportAll: function }}
*/
function createStoragePair() {
const localStorage = new SSR_Storage('localStorage');
const sessionStorage = new SSR_Storage('sessionStorage');
return {
localStorage,
sessionStorage,
exportAll() {
return {
localStorage: localStorage._export(),
sessionStorage: sessionStorage._export()
};
}
};
}
module.exports = {
SSR_Storage,
createStoragePair
};