Framework updates

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2026-03-04 23:20:19 +00:00
parent a89daf3d43
commit 3ed8517b2a
891 changed files with 11126 additions and 9600 deletions

View File

@@ -317,6 +317,7 @@ export declare class Jqhtml_Component {
on_render(): void;
on_create(): void;
on_load(): void | Promise<void>;
after_load(): void | Promise<void>;
on_ready(): Promise<void>;
on_stop(): void;
/**

View File

@@ -1 +1 @@
{"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../src/component.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../src/component.ts"],"names":[],"mappings":"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"}

View File

@@ -2800,6 +2800,9 @@ class Jqhtml_Component {
Jqhtml_Local_Storage.set(cache_key, this.data);
}
}
// Call after_load() on the real component (this.data frozen, full access to this.$, this.state)
await this._call_lifecycle('after_load');
this.trigger('after_load');
return data_changed;
}
/**
@@ -2943,6 +2946,8 @@ class Jqhtml_Component {
this._update_debug_attrs();
this._log_lifecycle('load', 'complete (use_cached_data - skipped on_load)');
this.trigger('load');
await this._call_lifecycle('after_load');
this.trigger('after_load');
return;
}
// Check if component implements cache_id() for custom cache key
@@ -3019,6 +3024,8 @@ class Jqhtml_Component {
this._update_debug_attrs();
this._log_lifecycle('load', 'complete (follower)');
this.trigger('load');
await this._call_lifecycle('after_load');
this.trigger('after_load');
return;
}
// This component is a leader - execute on_load() on detached proxy
@@ -3239,6 +3246,11 @@ class Jqhtml_Component {
this._log_lifecycle('load', 'complete');
// Emit lifecycle event
this.trigger('load');
// Call after_load() - runs on the REAL component (not detached proxy)
// this.data is frozen (read-only), but this.$, this.state, this.args are accessible
// Primary use case: clone this.data to this.state for widgets with complex in-memory manipulations
await this._call_lifecycle('after_load');
this.trigger('after_load');
}
finally {
// Signal next in queue
@@ -3714,6 +3726,7 @@ class Jqhtml_Component {
on_render() { }
on_create() { }
on_load() { } // Override to fetch data asynchronously
after_load() { } // Override to process loaded data (e.g., clone this.data to this.state)
async on_ready() { }
on_stop() { }
/**
@@ -5097,7 +5110,7 @@ function init(jQuery) {
}
}
// Version - will be replaced during build with actual version from package.json
const version = '2.3.36';
const version = '2.3.37';
// Default export with all functionality
const jqhtml = {
// Core

File diff suppressed because one or more lines are too long

View File

@@ -2796,6 +2796,9 @@ class Jqhtml_Component {
Jqhtml_Local_Storage.set(cache_key, this.data);
}
}
// Call after_load() on the real component (this.data frozen, full access to this.$, this.state)
await this._call_lifecycle('after_load');
this.trigger('after_load');
return data_changed;
}
/**
@@ -2939,6 +2942,8 @@ class Jqhtml_Component {
this._update_debug_attrs();
this._log_lifecycle('load', 'complete (use_cached_data - skipped on_load)');
this.trigger('load');
await this._call_lifecycle('after_load');
this.trigger('after_load');
return;
}
// Check if component implements cache_id() for custom cache key
@@ -3015,6 +3020,8 @@ class Jqhtml_Component {
this._update_debug_attrs();
this._log_lifecycle('load', 'complete (follower)');
this.trigger('load');
await this._call_lifecycle('after_load');
this.trigger('after_load');
return;
}
// This component is a leader - execute on_load() on detached proxy
@@ -3235,6 +3242,11 @@ class Jqhtml_Component {
this._log_lifecycle('load', 'complete');
// Emit lifecycle event
this.trigger('load');
// Call after_load() - runs on the REAL component (not detached proxy)
// this.data is frozen (read-only), but this.$, this.state, this.args are accessible
// Primary use case: clone this.data to this.state for widgets with complex in-memory manipulations
await this._call_lifecycle('after_load');
this.trigger('after_load');
}
finally {
// Signal next in queue
@@ -3710,6 +3722,7 @@ class Jqhtml_Component {
on_render() { }
on_create() { }
on_load() { } // Override to fetch data asynchronously
after_load() { } // Override to process loaded data (e.g., clone this.data to this.state)
async on_ready() { }
on_stop() { }
/**
@@ -5093,7 +5106,7 @@ function init(jQuery) {
}
}
// Version - will be replaced during build with actual version from package.json
const version = '2.3.36';
const version = '2.3.37';
// Default export with all functionality
const jqhtml = {
// Core

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
/**
* JQHTML Core v2.3.36
* JQHTML Core v2.3.37
* (c) 2025 JQHTML Team
* Released under the MIT License
*/
@@ -2801,6 +2801,9 @@ class Jqhtml_Component {
Jqhtml_Local_Storage.set(cache_key, this.data);
}
}
// Call after_load() on the real component (this.data frozen, full access to this.$, this.state)
await this._call_lifecycle('after_load');
this.trigger('after_load');
return data_changed;
}
/**
@@ -2944,6 +2947,8 @@ class Jqhtml_Component {
this._update_debug_attrs();
this._log_lifecycle('load', 'complete (use_cached_data - skipped on_load)');
this.trigger('load');
await this._call_lifecycle('after_load');
this.trigger('after_load');
return;
}
// Check if component implements cache_id() for custom cache key
@@ -3020,6 +3025,8 @@ class Jqhtml_Component {
this._update_debug_attrs();
this._log_lifecycle('load', 'complete (follower)');
this.trigger('load');
await this._call_lifecycle('after_load');
this.trigger('after_load');
return;
}
// This component is a leader - execute on_load() on detached proxy
@@ -3240,6 +3247,11 @@ class Jqhtml_Component {
this._log_lifecycle('load', 'complete');
// Emit lifecycle event
this.trigger('load');
// Call after_load() - runs on the REAL component (not detached proxy)
// this.data is frozen (read-only), but this.$, this.state, this.args are accessible
// Primary use case: clone this.data to this.state for widgets with complex in-memory manipulations
await this._call_lifecycle('after_load');
this.trigger('after_load');
}
finally {
// Signal next in queue
@@ -3715,6 +3727,7 @@ class Jqhtml_Component {
on_render() { }
on_create() { }
on_load() { } // Override to fetch data asynchronously
after_load() { } // Override to process loaded data (e.g., clone this.data to this.state)
async on_ready() { }
on_stop() { }
/**
@@ -5098,7 +5111,7 @@ function init(jQuery) {
}
}
// Version - will be replaced during build with actual version from package.json
const version = '2.3.36';
const version = '2.3.37';
// Default export with all functionality
const jqhtml = {
// Core

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
{
"name": "@jqhtml/core",
"version": "2.3.36",
"version": "2.3.37",
"description": "Core runtime library for JQHTML",
"type": "module",
"main": "./dist/index.js",

View File

@@ -1385,7 +1385,7 @@ export class CodeGenerator {
for (const [name, component] of this.components) {
code += `// Component: ${name}\n`;
code += `jqhtml_components.set('${name}', {\n`;
code += ` _jqhtml_version: '2.3.36',\n`; // Version will be replaced during build
code += ` _jqhtml_version: '2.3.37',\n`; // Version will be replaced during build
code += ` name: '${name}',\n`;
code += ` tag: '${component.tagName}',\n`;
code += ` defaultAttributes: ${this.serializeAttributeObject(component.defaultAttributes)},\n`;

View File

@@ -1,6 +1,6 @@
{
"name": "@jqhtml/parser",
"version": "2.3.36",
"version": "2.3.37",
"description": "JQHTML template parser - converts templates to JavaScript",
"type": "module",
"main": "dist/index.js",

View File

@@ -5,8 +5,8 @@
Create `.npmrc` in your project root:
```
@jqhtml:registry=https://privatenpm.hanson.xyz/
//privatenpm.hanson.xyz/:_auth=anFodG1sOkFHbTMyNStFdklOQXdUMnN0S0g2cXc9PQ==
@jqhtml:registry=https://npm.internal.hanson.xyz/
//npm.internal.hanson.xyz/:_auth=anFodG1sOkFHbTMyNStFdklOQXdUMnN0S0g2cXc9PQ==
```
## 2. Install

View File

@@ -1,6 +1,6 @@
{
"name": "@jqhtml/ssr",
"version": "2.3.36",
"version": "2.3.37",
"description": "Server-Side Rendering for JQHTML components - renders components to HTML for SEO",
"main": "src/index.js",
"bin": {
@@ -10,7 +10,7 @@
"scripts": {
"start": "node src/server.js --tcp 9876",
"start:socket": "node src/server.js --socket /tmp/jqhtml-ssr.sock",
"test": "node test/test-protocol.js && node test/test-storage.js && node test/test-server.js",
"test": "node test/test-protocol.js && node test/test-storage.js && node test/test-server.js && node test/test-ajax-transport.js && node test/test-render-spa.js",
"example": "node bin/jqhtml-ssr-example.js --help"
},
"keywords": [

View File

@@ -5,6 +5,8 @@
* Uses LRU (Least Recently Used) eviction strategy.
*/
const fs = require('fs');
/**
* LRU Cache for bundle sets
*/
@@ -123,8 +125,19 @@ function prepareBundleCode(bundles) {
const codeChunks = [];
for (const bundle of bundles) {
// Load code from filesystem path or use inline content
let raw;
if (bundle.path) {
if (!fs.existsSync(bundle.path)) {
throw new Error(`Bundle file not found: ${bundle.path}`);
}
raw = fs.readFileSync(bundle.path, 'utf-8');
} else {
raw = bundle.content;
}
// Remove sourcemap comments
let code = bundle.content.replace(/\/\/# sourceMappingURL=.*/g, '');
let code = raw.replace(/\/\/# sourceMappingURL=.*/g, '');
// Add bundle marker comment for debugging
code = `\n/* === Bundle: ${bundle.id} === */\n${code}`;

View File

@@ -8,7 +8,7 @@
const { JSDOM } = require('jsdom');
const vm = require('vm');
const { createStoragePair } = require('./storage.js');
const { installHttpIntercept } = require('./http-intercept.js');
const { installHttpIntercept, installAjaxTransport } = 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
@@ -21,7 +21,11 @@ class SSREnvironment {
constructor(options = {}) {
this.options = {
baseUrl: options.baseUrl || 'http://localhost',
timeout: options.timeout || 30000
timeout: options.timeout || 30000,
ssrToken: options.ssrToken || null,
rsxapp: options.rsxapp || null,
extractMeta: options.extractMeta || false,
readySelector: options.readySelector || '#spa-root > *:first-child'
};
this.dom = null;
@@ -94,8 +98,8 @@ class SSREnvironment {
this.window.__SSR__ = true;
this.window.__JQHTML_SSR_MODE__ = true;
// Install HTTP interception (fetch + XHR URL rewriting)
installHttpIntercept(this.window, this.options.baseUrl);
// Install HTTP interception (fetch + XHR URL rewriting + optional SSR token)
installHttpIntercept(this.window, this.options.baseUrl, this.options.ssrToken);
// Create jQuery bound to jsdom window
this.$ = jqueryFactory(this.window);
@@ -106,12 +110,20 @@ class SSREnvironment {
this.window.$ = this.$;
this.window.jQuery = this.$;
// Install $.ajaxTransport for real HTTP requests via jQuery $.ajax()
// This ensures $.ajax() → Node fetch() instead of jsdom's limited XHR
installAjaxTransport(this.window, this.options.baseUrl, this.options.ssrToken);
// 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 || {};
// rsxapp config object - inject from request or create empty default
if (this.options.rsxapp) {
this.window.rsxapp = this.options.rsxapp;
} else {
this.window.rsxapp = this.window.rsxapp || {};
}
global.rsxapp = this.window.rsxapp;
this.initialized = true;
@@ -208,6 +220,145 @@ class SSREnvironment {
return [];
}
/**
* Render a SPA page by URL
*
* Assumes bundles have already been executed via execute(), which triggers
* SPA framework boot (rsxapp.is_spa = true → route dispatch to URL).
* Waits for the root component to reach ready state, then extracts HTML.
*
* @param {string} url - The URL path being rendered (for logging/errors)
* @returns {Promise<{ html: string, meta: object, cache: object }>}
*/
async renderSpa(url) {
if (!this.initialized) {
throw new Error('SSR environment not initialized. Call init() first.');
}
const $ = global.$;
const readySelector = this.options.readySelector;
// Poll for root component to appear (SPA dispatch may be async)
const component = await this._waitForComponent(readySelector);
// Wait for component ready (full lifecycle including children)
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(`SPA render exceeded ${this.options.timeout}ms timeout`));
}, this.options.timeout);
});
await Promise.race([
component.ready(),
timeoutPromise
]);
// Extract HTML from #spa-root or the component itself
const $root = $('#spa-root');
const html = $root.length ? $root.html() : component.$.prop('outerHTML');
// Extract metadata if requested
let meta = {};
if (this.options.extractMeta) {
meta = this._extractMeta(component);
}
// Export cache state
const cache = this.storage.exportAll();
return { html, meta, cache };
}
/**
* Poll for a jqhtml component on the given selector
* @param {string} selector - CSS selector to find the component element
* @returns {Promise<object>} Component instance
* @private
*/
_waitForComponent(selector) {
const $ = global.$;
const timeout = this.options.timeout;
const startTime = Date.now();
return new Promise((resolve, reject) => {
const check = () => {
const elapsed = Date.now() - startTime;
if (elapsed > timeout) {
reject(new Error(`SPA component not found on "${selector}" within ${timeout}ms timeout`));
return;
}
const $el = $(selector);
if ($el.length > 0) {
const component = $el.component ? $el.component() : null;
if (component) {
resolve(component);
return;
}
}
setTimeout(check, 10);
};
check();
});
}
/**
* Extract page metadata from the rendered DOM and component
* @param {object} component - The root component instance
* @returns {{ title: string|null, description: string|null, og_image: string|null }}
* @private
*/
_extractMeta(component) {
const doc = global.document;
const meta = {
title: null,
description: null,
og_image: null
};
// Check document.title
if (doc && doc.title) {
meta.title = doc.title;
}
// Check if component has a page_title method
if (component && typeof component.page_title === 'function') {
try {
const title = component.page_title();
if (title && typeof title === 'string') {
meta.title = title;
}
} catch (e) {
// page_title() may throw - ignore
}
}
// Check meta tags in DOM
if (doc) {
const descEl = doc.querySelector('meta[name="description"]');
if (descEl) {
meta.description = descEl.getAttribute('content');
}
const ogImageEl = doc.querySelector('meta[property="og:image"]');
if (ogImageEl) {
meta.og_image = ogImageEl.getAttribute('content');
}
// Also check og:description as fallback
if (!meta.description) {
const ogDescEl = doc.querySelector('meta[property="og:description"]');
if (ogDescEl) {
meta.description = ogDescEl.getAttribute('content');
}
}
}
return meta;
}
/**
* Destroy the environment and clean up globals
*

View File

@@ -138,12 +138,132 @@ function createXHRWrapper(baseUrl, OriginalXHR) {
};
}
/**
* Wrap an existing fetch function to inject the SSR token header on all requests
* @param {function} fetchFn - The fetch function to wrap
* @param {string} ssrToken - SSR token value
* @returns {function} Wrapped fetch function
*/
function wrapFetchWithSsrToken(fetchFn, ssrToken) {
return function ssrTokenFetch(url, options = {}) {
const headers = { ...(options.headers || {}) };
headers['X-SSR-Token'] = ssrToken;
return fetchFn(url, { ...options, headers });
};
}
/**
* Install SSR token injection on fetch and XHR
* @param {Window} window - jsdom window object
* @param {string} ssrToken - SSR token value
*/
function installSsrTokenIntercept(window, ssrToken) {
if (!ssrToken) return;
// Wrap fetch with SSR token
const currentFetch = window.fetch;
window.fetch = wrapFetchWithSsrToken(currentFetch, ssrToken);
if (typeof global !== 'undefined') {
global.fetch = window.fetch;
}
}
/**
* Register a custom jQuery $.ajaxTransport that uses Node's fetch() for real HTTP.
*
* This solves the problem of jsdom's XMLHttpRequest not making real network requests.
* jQuery's $.ajax() will use this transport instead of XHR, so the chain becomes:
* $.ajax() → ajaxTransport → Node fetch() → real HTTP request
*
* @param {Window} window - jsdom window object (must have jQuery on window.$)
* @param {string} baseUrl - Base URL for resolving relative URLs
* @param {string} [ssrToken] - Optional SSR token to include in requests
*/
function installAjaxTransport(window, baseUrl, ssrToken) {
const $ = window.$;
if (!$ || !$.ajaxTransport) return;
$.ajaxTransport('+*', function(options, originalOptions, jqXHR) {
return {
send: function(headers, completeCallback) {
// Build absolute URL
const url = resolveUrl(options.url || '', baseUrl);
// Build fetch options
const fetchHeaders = {};
// Copy headers from jQuery, stripping auth headers
for (const [name, value] of Object.entries(headers)) {
const lowerName = name.toLowerCase();
if (lowerName === 'authorization' || lowerName === 'cookie') continue;
fetchHeaders[name] = value;
}
// Inject SSR token if provided
if (ssrToken) {
fetchHeaders['X-SSR-Token'] = ssrToken;
}
// Set content type if not already set
if (options.contentType && !fetchHeaders['Content-Type']) {
fetchHeaders['Content-Type'] = options.contentType;
}
const fetchOptions = {
method: options.type || 'GET',
headers: fetchHeaders
};
// Add body for non-GET requests
if (options.data && options.type && options.type.toUpperCase() !== 'GET') {
fetchOptions.body = typeof options.data === 'string'
? options.data
: JSON.stringify(options.data);
}
// Use the globally available fetch (already has URL rewriting from createFetch)
const fetchFn = globalThis.fetch || window.fetch;
fetchFn(url, fetchOptions)
.then(async (response) => {
const responseText = await response.text();
const responseHeaders = {};
if (response.headers && typeof response.headers.forEach === 'function') {
response.headers.forEach((value, name) => {
responseHeaders[name] = value;
});
}
const headerString = Object.entries(responseHeaders)
.map(([k, v]) => `${k}: ${v}`)
.join('\r\n');
completeCallback(
response.status,
response.statusText,
{ text: responseText },
headerString
);
})
.catch((err) => {
completeCallback(0, 'Network Error: ' + err.message, {}, '');
});
},
abort: function() {
// Cannot abort native fetch, but jQuery expects this method
}
};
});
}
/**
* Install HTTP interception on a jsdom window
* @param {Window} window - jsdom window object
* @param {string} baseUrl - Base URL for URL resolution
* @param {string} [ssrToken] - Optional SSR token for outgoing requests
*/
function installHttpIntercept(window, baseUrl) {
function installHttpIntercept(window, baseUrl, ssrToken) {
// Override fetch
window.fetch = createFetch(baseUrl);
@@ -161,11 +281,18 @@ function installHttpIntercept(window, baseUrl) {
global.XMLHttpRequest = WrappedXHR;
}
}
// Install SSR token on fetch/XHR if provided
if (ssrToken) {
installSsrTokenIntercept(window, ssrToken);
}
}
module.exports = {
resolveUrl,
createFetch,
createXHRWrapper,
installHttpIntercept
installHttpIntercept,
installSsrTokenIntercept,
installAjaxTransport
};

View File

@@ -8,13 +8,14 @@ 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 { resolveUrl, createFetch, installHttpIntercept, installAjaxTransport, installSsrTokenIntercept } = require('./http-intercept.js');
const {
ErrorCodes,
parseRequest,
successResponse,
errorResponse,
renderResponse,
renderSpaResponse,
pingResponse,
flushCacheResponse,
MessageBuffer
@@ -39,6 +40,8 @@ module.exports = {
resolveUrl,
createFetch,
installHttpIntercept,
installAjaxTransport,
installSsrTokenIntercept,
// Protocol
ErrorCodes,
@@ -46,6 +49,7 @@ module.exports = {
successResponse,
errorResponse,
renderResponse,
renderSpaResponse,
pingResponse,
flushCacheResponse,
MessageBuffer

View File

@@ -25,7 +25,11 @@ const ErrorCodes = {
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
INTERNAL_ERROR: 'INTERNAL_ERROR', // 500 - Unexpected server error
ROUTE_NOT_FOUND: 'ROUTE_NOT_FOUND', // 404 - No SPA action matched the URL
BUNDLE_LOAD_ERROR: 'BUNDLE_LOAD_ERROR', // 400 - Failed to read bundle from filesystem
SPA_BOOT_ERROR: 'SPA_BOOT_ERROR', // 500 - SPA framework failed to initialize
DATA_FETCH_ERROR: 'DATA_FETCH_ERROR' // 502 - on_load() HTTP request(s) failed
};
/**
@@ -70,7 +74,7 @@ function parseRequest(message) {
};
}
const validTypes = ['render', 'ping', 'flush_cache'];
const validTypes = ['render', 'render_spa', 'ping', 'flush_cache'];
if (!validTypes.includes(parsed.type)) {
return {
ok: false,
@@ -89,6 +93,13 @@ function parseRequest(message) {
}
}
if (parsed.type === 'render_spa') {
const validation = validateRenderSpaPayload(parsed.payload);
if (!validation.ok) {
return validation;
}
}
return { ok: true, request: parsed };
}
@@ -207,6 +218,142 @@ function validateRenderPayload(payload) {
return { ok: true };
}
/**
* Validate render_spa request payload
* @param {object} payload
* @returns {{ ok: true } | { ok: false, error: object }}
*/
function validateRenderSpaPayload(payload) {
if (!payload || typeof payload !== 'object') {
return {
ok: false,
error: {
code: ErrorCodes.PARSE_ERROR,
message: 'Missing or invalid "payload" field for render_spa 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`
}
};
}
// Must have either path or content
const hasPath = bundle.path && typeof bundle.path === 'string';
const hasContent = bundle.content && typeof bundle.content === 'string';
if (!hasPath && !hasContent) {
return {
ok: false,
error: {
code: ErrorCodes.PARSE_ERROR,
message: `payload.bundles[${i}] must have either "path" (string) or "content" (string)`
}
};
}
}
// Validate URL
if (!payload.url || typeof payload.url !== 'string') {
return {
ok: false,
error: {
code: ErrorCodes.PARSE_ERROR,
message: 'payload.url must be a string'
}
};
}
// Validate rsxapp
if (!payload.rsxapp || typeof payload.rsxapp !== 'object') {
return {
ok: false,
error: {
code: ErrorCodes.PARSE_ERROR,
message: 'payload.rsxapp must be an object'
}
};
}
// Validate options
if (!payload.options || typeof payload.options !== 'object') {
return {
ok: false,
error: {
code: ErrorCodes.PARSE_ERROR,
message: 'payload.options is required (must include baseUrl)'
}
};
}
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'
}
};
}
if (payload.options.ssr_token !== undefined && typeof payload.options.ssr_token !== 'string') {
return {
ok: false,
error: {
code: ErrorCodes.PARSE_ERROR,
message: 'payload.options.ssr_token must be a string'
}
};
}
return { ok: true };
}
/**
* Create a render_spa success response
* @param {string} id - Request ID
* @param {string} html - Rendered HTML
* @param {object} meta - Page metadata { title, description, og_image }
* @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 renderSpaResponse(id, html, meta, cache, timing) {
return successResponse(id, {
html,
meta,
cache,
timing
});
}
/**
* Create a success response
* @param {string} id - Request ID
@@ -329,9 +476,11 @@ module.exports = {
ErrorCodes,
parseRequest,
validateRenderPayload,
validateRenderSpaPayload,
successResponse,
errorResponse,
renderResponse,
renderSpaResponse,
pingResponse,
flushCacheResponse,
MessageBuffer

View File

@@ -14,6 +14,7 @@ const {
ErrorCodes,
parseRequest,
renderResponse,
renderSpaResponse,
pingResponse,
flushCacheResponse,
errorResponse,
@@ -153,6 +154,10 @@ class SSRServer {
response = await this._handleRender(request);
break;
case 'render_spa':
response = await this._handleRenderSpa(request);
break;
default:
response = errorResponse(
request.id,
@@ -299,6 +304,93 @@ class SSRServer {
}
}
/**
* Handle render_spa request
* @param {object} request
* @returns {Promise<string>} Response
* @private
*/
async _handleRenderSpa(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();
try {
bundleCode = prepareBundleCode(payload.bundles);
} catch (err) {
return errorResponse(
request.id,
ErrorCodes.BUNDLE_LOAD_ERROR,
err.message,
err.stack
);
}
this.bundleCache.set(cacheKey, bundleCode);
bundleLoadMs = Date.now() - bundleStartTime;
}
// Build the jsdom URL from baseUrl + requested URL path
const baseUrlObj = new URL(payload.options.baseUrl);
const spaUrl = `${baseUrlObj.protocol}//${baseUrlObj.host}${payload.url}`;
// Create fresh environment with SPA options
const env = new SSREnvironment({
baseUrl: spaUrl,
timeout: payload.options.timeout || this.options.defaultTimeout,
ssrToken: payload.options.ssr_token || null,
rsxapp: payload.rsxapp || {},
extractMeta: payload.options.extract_meta || false,
readySelector: payload.options.ready_selector || '#spa-root > *:first-child'
});
try {
// Initialize environment
env.init();
// Execute bundle code (SPA framework boots if rsxapp.is_spa === true)
const execStartTime = Date.now();
env.execute(bundleCode, `spa-bundles:${cacheKey}`);
bundleLoadMs += Date.now() - execStartTime;
// Render SPA (waits for route dispatch + component ready)
const renderStartTime = Date.now();
const result = await env.renderSpa(payload.url);
const renderMs = Date.now() - renderStartTime;
const totalMs = Date.now() - startTime;
return renderSpaResponse(request.id, result.html, result.meta, 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('Bundle file not found')) {
errorCode = ErrorCodes.BUNDLE_LOAD_ERROR;
} else if (err.message.includes('SPA')) {
errorCode = ErrorCodes.SPA_BOOT_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>}

View File

@@ -1 +1 @@
2.3.36
2.3.37

View File

@@ -2,12 +2,12 @@
"name": "@jqhtml/vscode-extension",
"displayName": "JQHTML",
"description": "Syntax highlighting and language support for JQHTML template files",
"version": "2.3.36",
"version": "2.3.37",
"publisher": "jqhtml",
"license": "MIT",
"publishConfig": {
"access": "public",
"registry": "https://privatenpm.hanson.xyz/"
"registry": "https://npm.internal.hanson.xyz/"
},
"scripts": {
"vscode:prepublish": "echo Skipping prepublish",