Files
rspade_system/node_modules/@jqhtml/ssr/README.md
2025-12-23 09:18:10 +00:00

465 lines
13 KiB
Markdown

# JQHTML Server-Side Rendering (SSR)
> **See [SPECIFICATION.md](./SPECIFICATION.md) for the complete technical specification.**
## Overview
The JQHTML SSR system renders components to HTML on the server for SEO purposes. Unlike React/Vue/Angular which require separate data-fetching abstractions, JQHTML SSR runs the exact same component code - including `on_load()` with real HTTP requests.
**Primary use case:** SEO - rendering meaningful HTML for search engine crawlers.
## Installation
```bash
npm install @jqhtml/ssr
```
Or install globally for CLI access:
```bash
npm install -g @jqhtml/ssr
```
---
## Quick Start
### Starting the Server
```bash
cd /var/www/html/jqhtml/aux/ssr
npm install
# Start on TCP port
node src/server.js --tcp 9876
# Or with Unix socket (better performance, local only)
node src/server.js --socket /tmp/jqhtml-ssr.sock
```
### Server 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 help
```
---
## Reference CLI Example
The package includes `jqhtml-ssr-example`, a complete reference implementation that demonstrates the full SSR workflow. **Use this as the canonical source of truth for building integrations in any language.**
### Basic Usage
```bash
# Start the SSR server in one terminal
jqhtml-ssr --tcp 9876
# In another terminal, run the example
jqhtml-ssr-example \
--vendor ./bundles/vendor.js \
--app ./bundles/app.js \
--component Dashboard_Index_Action \
--base-url http://localhost:3000
```
### Example Options
```
REQUIRED:
--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
OPTIONS:
--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 help message
```
### Output Formats
**Pretty (default)** - Human-readable output showing:
- Rendered HTML (formatted)
- localStorage cache entries
- sessionStorage cache entries
- Timing information
```bash
jqhtml-ssr-example -v vendor.js -a app.js -c My_Component
```
**JSON** - Raw server response for programmatic use:
```bash
jqhtml-ssr-example -v vendor.js -a app.js -c My_Component --format json > result.json
```
**HTML-only** - Just the rendered HTML:
```bash
jqhtml-ssr-example -v vendor.js -a app.js -c My_Component --format html-only > output.html
```
### Example with Component Arguments
```bash
jqhtml-ssr-example \
-v ./bundles/vendor.js \
-a ./bundles/app.js \
-c User_Profile \
--args '{"user_id": 123, "show_avatar": true}'
```
### Integration Reference
The `jqhtml-ssr-example` source code (`bin/jqhtml-ssr-example.js`) is the **canonical reference** for building integrations. Key implementation details:
1. **Connection** - TCP socket to `localhost:PORT` or Unix socket
2. **Request format** - Newline-delimited JSON
3. **Bundle loading** - Read JS files, send as `content` strings
4. **Request structure** - See the commented request object in the source
5. **Response parsing** - JSON with `status`, `payload`, and `error` fields
When building integrations in PHP, Python, Go, etc., refer to this example for the exact protocol and data structures.
---
## Integration
### Protocol
The server uses a simple newline-delimited JSON protocol over TCP or Unix socket.
**Request format:**
```json
{
"id": "unique-request-id",
"type": "render",
"payload": {
"bundles": [
{ "id": "vendor", "content": "..." },
{ "id": "app", "content": "..." }
],
"component": "Dashboard_Index_Action",
"args": { "user_id": 123 },
"options": {
"baseUrl": "http://localhost:3000",
"timeout": 30000
}
}
}
```
**Response format:**
```json
{
"id": "unique-request-id",
"status": "success",
"payload": {
"html": "<div class=\"Dashboard_Index_Action Component\">...</div>",
"cache": {
"localStorage": {},
"sessionStorage": {}
},
"timing": {
"total_ms": 231,
"bundle_load_ms": 49,
"render_ms": 99
}
}
}
```
### PHP Integration Example
```php
<?php
class JqhtmlSSR {
private $socket;
public function __construct(string $socketPath = '/tmp/jqhtml-ssr.sock') {
$this->socket = stream_socket_client("unix://$socketPath", $errno, $errstr, 5);
if (!$this->socket) {
throw new Exception("SSR connection failed: $errstr");
}
}
public function render(string $component, array $args = [], array $bundles = []): array {
$request = json_encode([
'id' => uniqid('ssr-'),
'type' => 'render',
'payload' => [
'bundles' => $bundles,
'component' => $component,
'args' => $args,
'options' => [
'baseUrl' => 'http://localhost',
'timeout' => 30000
]
]
]) . "\n";
fwrite($this->socket, $request);
$response = fgets($this->socket);
return json_decode($response, true);
}
public function ping(): bool {
$request = json_encode([
'id' => 'ping',
'type' => 'ping',
'payload' => []
]) . "\n";
fwrite($this->socket, $request);
$response = json_decode(fgets($this->socket), true);
return $response['status'] === 'success';
}
}
// Usage
$ssr = new JqhtmlSSR();
$result = $ssr->render('Dashboard_Index_Action', ['user_id' => 123], $bundles);
if ($result['status'] === 'success') {
echo $result['payload']['html'];
}
```
### Node.js Client Example
```javascript
const net = require('net');
function sendSSRRequest(port, request) {
return new Promise((resolve, reject) => {
const client = net.createConnection({ port }, () => {
client.write(JSON.stringify(request) + '\n');
});
let data = '';
client.on('data', (chunk) => {
data += chunk.toString();
if (data.includes('\n')) {
client.end();
resolve(JSON.parse(data.trim()));
}
});
client.on('error', reject);
setTimeout(() => {
client.end();
reject(new Error('Request timeout'));
}, 30000);
});
}
// Usage
const response = await sendSSRRequest(9876, {
id: 'render-1',
type: 'render',
payload: {
bundles: [
{ id: 'vendor', content: vendorBundleContent },
{ id: 'app', content: appBundleContent }
],
component: 'Dashboard_Index_Action',
args: { user_id: 123 },
options: { baseUrl: 'http://localhost:3000' }
}
});
console.log(response.payload.html);
```
---
## Request Types
### ping
Health check.
```json
{ "id": "1", "type": "ping", "payload": {} }
```
Response includes `uptime_ms`.
### render
Render a component to HTML.
Required payload fields:
- `bundles` - Array of `{ id, content }` objects
- `component` - Component name to render
- `options.baseUrl` - Base URL for relative fetch/XHR requests
Optional:
- `args` - Component arguments (default: `{}`)
- `options.timeout` - Render timeout in ms (default: 30000)
### flush_cache
Clear bundle cache.
```json
{ "id": "1", "type": "flush_cache", "payload": {} }
```
Or flush specific bundle:
```json
{ "id": "1", "type": "flush_cache", "payload": { "bundle_id": "app" } }
```
---
## Error Codes
| Code | Description |
|------|-------------|
| `PARSE_ERROR` | Invalid JSON or malformed request |
| `BUNDLE_ERROR` | JavaScript syntax error in bundle |
| `COMPONENT_NOT_FOUND` | Component not registered after loading bundles |
| `RENDER_ERROR` | Error during component lifecycle |
| `RENDER_TIMEOUT` | Component did not reach ready state in time |
| `INTERNAL_ERROR` | Unexpected server error |
---
## Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ SSR Server (long-running Node.js process) │
│ │
│ 1. Receive request (component name, args, bundles) │
│ 2. Load bundles (cached) into isolated jsdom environment │
│ 3. Execute component lifecycle (including real fetch()) │
│ 4. Wait for on_ready │
│ 5. Return HTML + localStorage/sessionStorage cache dump │
└─────────────────────────────────────────────────────────────────┘
```
**Key features:**
- **Real data fetching** - Components make actual HTTP requests during SSR
- **Cache export** - Server exports storage state for instant client hydration
- **Bundle caching** - Prepared bundle code cached with LRU eviction
- **Request isolation** - Each render gets fresh jsdom environment
- **URL rewriting** - Relative URLs resolved against baseUrl
---
## File Structure
```
aux/ssr/
├── SPECIFICATION.md # Complete technical specification
├── README.md # This file
├── package.json
├── bin/
│ └── jqhtml-ssr-example.js # Reference CLI example (canonical integration source)
├── src/
│ ├── index.js # Package exports
│ ├── server.js # TCP/Unix socket server (jqhtml-ssr CLI)
│ ├── environment.js # jsdom + jQuery environment setup
│ ├── bundle-cache.js # Bundle caching with LRU eviction
│ ├── http-intercept.js # fetch/XHR URL rewriting
│ ├── storage.js # Fake localStorage/sessionStorage
│ └── protocol.js # Message parsing/formatting
├── test/
│ ├── test-protocol.js # Protocol unit tests (16 tests)
│ ├── test-storage.js # Storage unit tests (13 tests)
│ └── test-server.js # Server integration tests (6 tests)
├── test-manual.js # Manual test with real bundles
└── test-debug.js # Debug script for troubleshooting
```
---
## Running Tests
```bash
# Unit tests
node test/test-protocol.js
node test/test-storage.js
node test/test-server.js
# Manual test with real bundles (requires bundles_for_ssr_test/)
node test-manual.js
```
---
## Critical Technical Discoveries
### 1. jQuery Module Load Order (CRITICAL)
**jQuery MUST be `require()`'d BEFORE `global.window` is set.**
```javascript
// CORRECT - jQuery returns a factory function
const jqueryFactory = require('jquery'); // First!
global.window = jsdomWindow; // Second
const $ = jqueryFactory(jsdomWindow); // Returns working jQuery
// WRONG - jQuery auto-initializes and returns an object
global.window = jsdomWindow; // First (BAD)
const jqueryFactory = require('jquery'); // jQuery sees window, auto-binds
const $ = jqueryFactory(jsdomWindow); // Returns object, not function!
```
**Why:** jQuery checks for `global.window` at require-time. If it exists, jQuery auto-initializes against that window instead of returning a factory.
### 2. Use `vm.runInThisContext` Not `vm.createContext`
Using `vm.createContext` creates VM context boundary issues where function references don't work across contexts. jQuery wrapper functions fail because closures can't access variables across the boundary.
**Solution:** Use `vm.runInThisContext` which executes in Node's global context.
### 3. Bundle Loading Order
Load bundles in dependency order:
1. **Vendor bundle** - Contains `@jqhtml/core`, initializes `window.jqhtml`
2. **App bundle** - Contains templates and component classes
### 4. Global Cleanup
When destroying environments, set globals to `undefined` instead of using `delete`:
```javascript
global.window = undefined; // Safe
// delete global.window; // Can cause issues
```
---
## Dependencies
```json
{
"jsdom": "^24.0.0",
"jquery": "^3.7.1"
}
```
---
## References
- [SPECIFICATION.md](./SPECIFICATION.md) - Complete technical specification
- [/packages/core/src/component.ts](/packages/core/src/component.ts) - Component lifecycle
- [/docs/official/18_boot.md](/docs/official/18_boot.md) - Client-side boot/hydration
- [jsdom documentation](https://github.com/jsdom/jsdom)
- [jQuery in Node.js](https://www.npmjs.com/package/jquery)