Files
rspade_system/app/RSpade/Commands/Rsx/resource/playwright/generate-static-cache.js
root f6fac6c4bc Fix bin/publish: copy docs.dist from project root
Fix bin/publish: use correct .env path for rspade_system
Fix bin/publish script: prevent grep exit code 1 from terminating script

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 02:08:33 +00:00

261 lines
8.0 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* SSR FPC (Full Page Cache) Generation Script
* Generates static pre-rendered HTML cache using Playwright
*
* Usage: node generate-static-cache.js <route> [options]
*
* Arguments:
* route The route to generate cache for (e.g., /about)
*
* Options:
* --timeout=<ms> Navigation timeout in milliseconds (default 30000ms)
* --help Show this help message
*/
const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
// Parse command line arguments
function parse_args() {
const args = process.argv.slice(2);
if (args.length === 0 || args.includes('--help')) {
console.log('SSR FPC Generation - Generate static pre-rendered HTML cache');
console.log('');
console.log('Usage: node generate-static-cache.js <route> [options]');
console.log('');
console.log('Arguments:');
console.log(' route The route to generate cache for (e.g., /about)');
console.log('');
console.log('Options:');
console.log(' --timeout=<ms> Navigation timeout in milliseconds (default 30000ms)');
console.log(' --help Show this help message');
process.exit(0);
}
const options = {
route: null,
timeout: 30000,
};
for (const arg of args) {
if (arg.startsWith('--timeout=')) {
options.timeout = parseInt(arg.substring(10));
if (options.timeout < 30000) {
console.error('Error: Timeout value is in milliseconds and must be no less than 30000 milliseconds (30 seconds)');
process.exit(1);
}
} else if (!arg.startsWith('--')) {
options.route = arg;
}
}
if (!options.route) {
console.error('Error: Route argument is required');
process.exit(1);
}
// Ensure route starts with /
if (!options.route.startsWith('/')) {
options.route = '/' + options.route;
}
return options;
}
// Log error to file
function log_error(url, error, details = {}) {
const log_dir = path.join(process.cwd(), 'storage', 'logs');
const log_file = path.join(log_dir, 'ssr-fpc-errors.log');
const timestamp = new Date().toISOString();
const log_entry = {
timestamp,
url,
error: error.message || String(error),
stack: error.stack || null,
...details
};
const log_line = JSON.stringify(log_entry) + '\n';
try {
if (!fs.existsSync(log_dir)) {
fs.mkdirSync(log_dir, { recursive: true });
}
fs.appendFileSync(log_file, log_line);
} catch (e) {
console.error('Failed to write error log:', e.message);
}
}
// Main execution
(async () => {
const options = parse_args();
const baseUrl = process.env.BASE_URL || 'http://localhost';
const fullUrl = baseUrl + options.route;
let browser;
let page;
try {
// Launch browser (always headless)
browser = await chromium.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const context = await browser.newContext({
ignoreHTTPSErrors: true
});
page = await context.newPage();
// Set up headers for FPC generation request
const extraHeaders = {
'X-RSpade-FPC-Client': '1', // Identifies this as FPC generation request
};
// Use route interception to add headers
await page.route('**/*', async (route, request) => {
const url = request.url();
// Only add headers to local requests
if (url.startsWith(baseUrl)) {
await route.continue({
headers: {
...request.headers(),
...extraHeaders
}
});
} else {
await route.continue();
}
});
// Navigate to the route
let response;
try {
response = await page.goto(fullUrl, {
waitUntil: 'networkidle',
timeout: options.timeout
});
} catch (error) {
log_error(fullUrl, error, {
phase: 'navigation',
timeout: options.timeout
});
throw error;
}
if (!response) {
const error = new Error('Navigation failed - no response');
log_error(fullUrl, error, { phase: 'navigation' });
throw error;
}
// Check for redirect (300-399 status codes)
const status = response.status();
if (status >= 300 && status < 400) {
// Redirect response - cache the redirect
const redirect_location = response.headers()['location'];
if (!redirect_location) {
const error = new Error(`Redirect response (${status}) but no Location header found`);
log_error(fullUrl, error, {
phase: 'redirect_check',
status,
headers: response.headers()
});
throw error;
}
// Output redirect response as JSON
const result = {
url: options.route,
code: status,
redirect: redirect_location,
page_dom: null
};
console.log(JSON.stringify(result));
await browser.close();
process.exit(0);
}
// Wait for RSX framework and jqhtml components to complete initialization
try {
await page.evaluate(() => {
return new Promise((resolve) => {
// Check if RSX framework with _debug_ready event is available
if (window.Rsx && window.Rsx.on) {
// Use Rsx._debug_ready event which fires after all jqhtml components complete lifecycle
window.Rsx.on('_debug_ready', function() {
resolve();
});
} else {
// Fallback for non-RSX pages: wait for DOMContentLoaded + 200ms
if (document.readyState === 'complete' || document.readyState === 'interactive') {
setTimeout(function() { resolve(); }, 200);
} else {
document.addEventListener('DOMContentLoaded', function() {
setTimeout(function() { resolve(); }, 200);
});
}
}
});
});
} catch (error) {
log_error(fullUrl, error, {
phase: 'wait_for_ready',
has_rsx: await page.evaluate(() => typeof window.Rsx !== 'undefined')
});
throw error;
}
// Additional 10ms wait for final network settle
await new Promise(resolve => setTimeout(resolve, 10));
// Get the fully rendered DOM
let page_dom;
try {
page_dom = await page.content();
} catch (error) {
log_error(fullUrl, error, { phase: 'get_dom' });
throw error;
}
// Output success response as JSON
const result = {
url: options.route,
code: status,
page_dom: page_dom,
redirect: null
};
console.log(JSON.stringify(result));
await browser.close();
process.exit(0);
} catch (error) {
if (browser) {
await browser.close();
}
// Log error if not already logged
if (!error.logged) {
log_error(fullUrl, error, { phase: 'unknown' });
}
console.error('FATAL: SSR FPC generation failed');
console.error(error.message);
if (error.stack) {
console.error(error.stack);
}
process.exit(1);
}
})();