Files
rspade_system/node_modules/@jqhtml/ssr/bin/jqhtml-ssr-example.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

468 lines
13 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* JQHTML SSR Reference Example CLI
*
* This is the canonical reference implementation for integrating with the
* JQHTML SSR server. Use this as a source of truth for external integrations.
*
* Usage:
* jqhtml-ssr-example --vendor <path> --app <path> --component <name> [options]
*
* Example:
* jqhtml-ssr-example \
* --vendor ./bundles/vendor.js \
* --app ./bundles/app.js \
* --component Dashboard_Index_Action \
* --base-url http://localhost:3000
*/
const net = require('net');
const fs = require('fs');
const path = require('path');
// ANSI colors for output
const colors = {
reset: '\x1b[0m',
bold: '\x1b[1m',
dim: '\x1b[2m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
cyan: '\x1b[36m'
};
function color(text, c) {
return `${colors[c]}${text}${colors.reset}`;
}
/**
* Parse command line arguments
*/
function parseArgs() {
const args = process.argv.slice(2);
const config = {
vendor: null,
app: null,
component: null,
componentArgs: {},
baseUrl: 'http://localhost:3000',
timeout: 30000,
port: 9876,
socket: null,
outputFormat: 'pretty', // 'pretty', 'json', 'html-only'
showHelp: false
};
for (let i = 0; i < args.length; i++) {
switch (args[i]) {
case '--vendor':
case '-v':
config.vendor = args[++i];
break;
case '--app':
case '-a':
config.app = args[++i];
break;
case '--component':
case '-c':
config.component = args[++i];
break;
case '--args':
// Parse JSON args
try {
config.componentArgs = JSON.parse(args[++i]);
} catch (e) {
console.error(color('Error: --args must be valid JSON', 'red'));
process.exit(1);
}
break;
case '--base-url':
case '-b':
config.baseUrl = args[++i];
break;
case '--timeout':
case '-t':
config.timeout = parseInt(args[++i], 10);
break;
case '--port':
case '-p':
config.port = parseInt(args[++i], 10);
break;
case '--socket':
case '-s':
config.socket = args[++i];
break;
case '--format':
case '-f':
config.outputFormat = args[++i];
break;
case '--help':
case '-h':
config.showHelp = true;
break;
}
}
return config;
}
/**
* Show help message
*/
function showHelp() {
console.log(`
${color('JQHTML SSR Reference Example', 'bold')}
This CLI demonstrates the complete SSR workflow and serves as the
canonical reference for external integrations.
${color('USAGE:', 'yellow')}
jqhtml-ssr-example --vendor <path> --app <path> --component <name> [options]
${color('REQUIRED:', 'yellow')}
--vendor, -v <path> Path to vendor bundle (contains @jqhtml/core)
--app, -a <path> Path to app bundle (contains components)
--component, -c <name> Component name to render
${color('OPTIONS:', 'yellow')}
--args <json> Component arguments as JSON (default: {})
--base-url, -b <url> Base URL for fetch requests (default: http://localhost:3000)
--timeout, -t <ms> Render timeout in milliseconds (default: 30000)
--port, -p <port> SSR server port (default: 9876)
--socket, -s <path> Use Unix socket instead of TCP
--format, -f <format> Output format: pretty, json, html-only (default: pretty)
--help, -h Show this help message
${color('EXAMPLES:', 'yellow')}
# Basic render
jqhtml-ssr-example -v ./vendor.js -a ./app.js -c Dashboard_Index_Action
# With component arguments
jqhtml-ssr-example -v ./vendor.js -a ./app.js -c User_Profile \\
--args '{"user_id": 123}'
# JSON output for piping
jqhtml-ssr-example -v ./vendor.js -a ./app.js -c Dashboard_Index_Action \\
--format json > result.json
# HTML only output
jqhtml-ssr-example -v ./vendor.js -a ./app.js -c Dashboard_Index_Action \\
--format html-only > output.html
${color('OUTPUT:', 'yellow')}
The 'pretty' format shows:
- Rendered HTML (formatted)
- localStorage cache entries
- sessionStorage cache entries
- Timing information
The 'json' format outputs the raw server response.
The 'html-only' format outputs just the HTML.
${color('INTEGRATION NOTES:', 'yellow')}
This example demonstrates the exact protocol used by the SSR server.
See the source code for implementation details that can be adapted
to any language (PHP, Python, Ruby, Go, etc.).
Key integration points:
1. Connect to TCP port or Unix socket
2. Send newline-delimited JSON request
3. Read newline-delimited JSON response
4. Parse response and extract HTML + cache
${color('SEE ALSO:', 'yellow')}
README.md - Full documentation
SPECIFICATION.md - Protocol specification
`);
}
/**
* Connect to SSR server
* @param {object} config - Connection configuration
* @returns {Promise<net.Socket>}
*/
function connect(config) {
return new Promise((resolve, reject) => {
const options = config.socket
? { path: config.socket }
: { port: config.port, host: 'localhost' };
const client = net.createConnection(options, () => {
resolve(client);
});
client.on('error', (err) => {
if (err.code === 'ECONNREFUSED') {
reject(new Error(
`Cannot connect to SSR server at ${config.socket || `localhost:${config.port}`}\n` +
`Start the server with: node src/server.js --tcp ${config.port}`
));
} else {
reject(err);
}
});
});
}
/**
* Send request and receive response
* @param {net.Socket} client - Connected socket
* @param {object} request - Request object
* @param {number} timeout - Timeout in ms
* @returns {Promise<object>}
*/
function sendRequest(client, request, timeout) {
return new Promise((resolve, reject) => {
// Set timeout
const timer = setTimeout(() => {
client.end();
reject(new Error(`Request timeout after ${timeout}ms`));
}, timeout);
// Collect response
let data = '';
client.on('data', (chunk) => {
data += chunk.toString();
if (data.includes('\n')) {
clearTimeout(timer);
client.end();
try {
resolve(JSON.parse(data.trim()));
} catch (e) {
reject(new Error(`Invalid response: ${data}`));
}
}
});
// Send request
// IMPORTANT: Request must end with newline for server to process
const requestStr = JSON.stringify(request) + '\n';
client.write(requestStr);
});
}
/**
* Load bundle file
* @param {string} filePath - Path to bundle file
* @returns {string} Bundle content
*/
function loadBundle(filePath) {
const resolvedPath = path.resolve(filePath);
if (!fs.existsSync(resolvedPath)) {
throw new Error(`Bundle file not found: ${resolvedPath}`);
}
return fs.readFileSync(resolvedPath, 'utf8');
}
/**
* Format HTML for display (basic indentation)
* @param {string} html - Raw HTML
* @returns {string} Formatted HTML
*/
function formatHtml(html) {
// Simple formatting - indent nested tags
let formatted = '';
let indent = 0;
const lines = html
.replace(/></g, '>\n<')
.split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) continue;
// Decrease indent for closing tags
if (trimmed.startsWith('</')) {
indent = Math.max(0, indent - 1);
}
formatted += ' '.repeat(indent) + trimmed + '\n';
// Increase indent for opening tags (not self-closing)
if (trimmed.startsWith('<') &&
!trimmed.startsWith('</') &&
!trimmed.endsWith('/>') &&
!trimmed.includes('</')) {
indent++;
}
}
return formatted;
}
/**
* Pretty print the result
* @param {object} response - Server response
* @param {object} config - Configuration
*/
function prettyPrint(response, config) {
console.log('\n' + color('═'.repeat(70), 'dim'));
console.log(color(' JQHTML SSR Render Result', 'bold'));
console.log(color('═'.repeat(70), 'dim'));
// Component info
console.log(`\n${color('Component:', 'cyan')} ${config.component}`);
if (Object.keys(config.componentArgs).length > 0) {
console.log(`${color('Arguments:', 'cyan')} ${JSON.stringify(config.componentArgs)}`);
}
// Status
if (response.status === 'success') {
console.log(`${color('Status:', 'cyan')} ${color('SUCCESS', 'green')}`);
} else {
console.log(`${color('Status:', 'cyan')} ${color('ERROR', 'red')}`);
console.log(`${color('Error Code:', 'red')} ${response.error.code}`);
console.log(`${color('Error Message:', 'red')} ${response.error.message}`);
if (response.error.stack) {
console.log(`${color('Stack:', 'dim')}\n${response.error.stack}`);
}
return;
}
// Timing
const timing = response.payload.timing;
console.log(`\n${color('Timing:', 'cyan')}`);
console.log(` Total: ${timing.total_ms}ms`);
console.log(` Bundle Load: ${timing.bundle_load_ms}ms`);
console.log(` Render: ${timing.render_ms}ms`);
// HTML
console.log(`\n${color('Rendered HTML:', 'cyan')} (${response.payload.html.length} bytes)`);
console.log(color('─'.repeat(70), 'dim'));
console.log(formatHtml(response.payload.html));
console.log(color('─'.repeat(70), 'dim'));
// Cache - localStorage
const localStorage = response.payload.cache.localStorage;
const localStorageKeys = Object.keys(localStorage);
console.log(`\n${color('localStorage Cache:', 'cyan')} (${localStorageKeys.length} entries)`);
if (localStorageKeys.length > 0) {
for (const key of localStorageKeys) {
const value = localStorage[key];
const preview = value.length > 100 ? value.substring(0, 100) + '...' : value;
console.log(` ${color(key, 'yellow')}: ${preview}`);
}
} else {
console.log(color(' (empty)', 'dim'));
}
// Cache - sessionStorage
const sessionStorage = response.payload.cache.sessionStorage;
const sessionStorageKeys = Object.keys(sessionStorage);
console.log(`\n${color('sessionStorage Cache:', 'cyan')} (${sessionStorageKeys.length} entries)`);
if (sessionStorageKeys.length > 0) {
for (const key of sessionStorageKeys) {
const value = sessionStorage[key];
const preview = value.length > 100 ? value.substring(0, 100) + '...' : value;
console.log(` ${color(key, 'yellow')}: ${preview}`);
}
} else {
console.log(color(' (empty)', 'dim'));
}
console.log('\n' + color('═'.repeat(70), 'dim') + '\n');
}
/**
* Main entry point
*/
async function main() {
const config = parseArgs();
if (config.showHelp) {
showHelp();
process.exit(0);
}
// Validate required arguments
if (!config.vendor || !config.app || !config.component) {
console.error(color('Error: --vendor, --app, and --component are required', 'red'));
console.error('Use --help for usage information');
process.exit(1);
}
try {
// Load bundles
console.log(color('Loading bundles...', 'dim'));
const vendorContent = loadBundle(config.vendor);
const appContent = loadBundle(config.app);
console.log(` Vendor: ${(vendorContent.length / 1024).toFixed(1)} KB`);
console.log(` App: ${(appContent.length / 1024).toFixed(1)} KB`);
// Connect to server
console.log(color('Connecting to SSR server...', 'dim'));
const client = await connect(config);
console.log(` Connected to ${config.socket || `localhost:${config.port}`}`);
// Build request
// This is the canonical request format - use this as reference for integrations
const request = {
// Unique request ID - use for correlating requests/responses
id: `render-${Date.now()}`,
// Request type: 'ping', 'render', or 'flush_cache'
type: 'render',
// Payload contains all render parameters
payload: {
// Bundles array - order matters! Vendor before app.
// Each bundle has 'id' (for caching) and 'content' (JS source)
bundles: [
{ id: 'vendor', content: vendorContent },
{ id: 'app', content: appContent }
],
// Component name to render (must be registered in bundles)
component: config.component,
// Arguments passed to component (becomes this.args)
args: config.componentArgs,
// Render options
options: {
// Base URL for resolving relative fetch/XHR URLs
baseUrl: config.baseUrl,
// Render timeout in milliseconds
timeout: config.timeout
}
}
};
// Send request and get response
console.log(color(`Rendering ${config.component}...`, 'dim'));
const response = await sendRequest(client, request, config.timeout + 5000);
// Output based on format
switch (config.outputFormat) {
case 'json':
console.log(JSON.stringify(response, null, 2));
break;
case 'html-only':
if (response.status === 'success') {
console.log(response.payload.html);
} else {
console.error(`Error: ${response.error.message}`);
process.exit(1);
}
break;
case 'pretty':
default:
prettyPrint(response, config);
break;
}
// Exit with appropriate code
process.exit(response.status === 'success' ? 0 : 1);
} catch (err) {
console.error(color(`Error: ${err.message}`, 'red'));
process.exit(1);
}
}
main();