environment('production')) { throw new \RuntimeException('FATAL: rsx:ssr_fpc:create command is not available in production environment. This is a development-only tool.'); } // Check if SSR FPC is enabled if (!config('rsx.ssr_fpc.enabled', false)) { $this->error('SSR FPC is disabled. Enable it in config/rsx.php or set SSR_FPC_ENABLED=true in .env'); return 1; } // Get the URL to generate cache for $url = $this->argument('url'); // Strip query parameters from URL (static pages ignore query strings) $url = parse_url($url, PHP_URL_PATH) ?: $url; // Ensure URL starts with / if (!str_starts_with($url, '/')) { $url = '/' . $url; } $this->info("Generating static cache for: {$url}"); // Check if Playwright script exists $playwright_script = base_path('app/RSpade/Commands/Rsx/resource/playwright/generate-static-cache.js'); if (!file_exists($playwright_script)) { $this->error("❌ Playwright script not found: {$playwright_script}"); $this->error('Please create the script or check your installation'); return 1; } // Check if node/npm is available $node_check = new Process(['node', '--version']); $node_check->run(); if (!$node_check->isSuccessful()) { $this->error('❌ Node.js is not installed or not in PATH'); return 1; } // Check if playwright is installed $playwright_check = new Process(['node', '-e', "require('playwright')"], base_path()); $playwright_check->run(); if (!$playwright_check->isSuccessful()) { $this->warn('⚠️ Playwright not installed. Installing now...'); $npm_install = new Process(['npm', 'install', 'playwright'], base_path()); $npm_install->run(function ($type, $buffer) { echo $buffer; }); if (!$npm_install->isSuccessful()) { $this->error('❌ Failed to install Playwright'); return 1; } $this->info('✅ Playwright installed'); $this->info(''); } // Check if chromium browser is installed and up to date $browser_check_script = "const {chromium} = require('playwright'); chromium.launch({headless:true}).then(b => {b.close(); process.exit(0);}).catch(e => {console.error(e.message); process.exit(1);});"; $browser_check = new Process(['node', '-e', $browser_check_script], base_path(), $_ENV, null, 10); $browser_check->run(); if (!$browser_check->isSuccessful()) { $error_output = $browser_check->getErrorOutput() . $browser_check->getOutput(); // Check if it's a browser not installed or out of date error if (str_contains($error_output, "Executable doesn't exist") || str_contains($error_output, "browserType.launch") || str_contains($error_output, "Playwright was just installed or updated")) { $this->info('Installing/updating Chromium browser...'); $browser_install = new Process(['npx', 'playwright', 'install', 'chromium'], base_path()); $browser_install->setTimeout(300); // 5 minute timeout for download $browser_install->run(function ($type, $buffer) { // Silent - downloads can be verbose }); if (!$browser_install->isSuccessful()) { $this->error('❌ Failed to install Chromium browser'); $this->error('Run manually: npx playwright install chromium'); return 1; } $this->info('✅ Chromium browser installed/updated'); $this->info(''); } else { $this->error('❌ Browser check failed: ' . trim($error_output)); return 1; } } // Get timeout from config $timeout = config('rsx.ssr_fpc.generation_timeout', 30000); // Build command arguments $command_args = ['node', $playwright_script, $url, "--timeout={$timeout}"]; $env = array_merge($_ENV, [ 'BASE_URL' => config('app.url') ]); // Convert timeout from milliseconds to seconds for Process timeout // Add 10 seconds buffer to the Process timeout to allow Playwright to timeout first $process_timeout = ($timeout / 1000) + 10; // Release the application lock before running Playwright to prevent lock contention // The artisan command holds a WRITE lock which would block the web request's READ lock \App\RSpade\Core\Bootstrap\RsxBootstrap::temporarily_release_lock(); $process = new Process( $command_args, base_path(), $env, null, $process_timeout ); $output = ''; $process->run(function ($type, $buffer) use (&$output) { echo $buffer; $output .= $buffer; }); if (!$process->isSuccessful()) { $this->error('❌ Failed to generate static cache'); $this->error('Check storage/logs/ssr-fpc-errors.log for details'); return 1; } // Parse JSON output from Playwright script try { $result = json_decode($output, true); if (!$result) { throw new \Exception('Invalid JSON response from Playwright'); } } catch (\Exception $e) { $this->error('❌ Failed to parse Playwright output: ' . $e->getMessage()); $this->error('Raw output: ' . $output); return 1; } // Get build key from manifest $build_key = \App\RSpade\Core\Manifest\Manifest::get_build_key(); // Generate Redis cache key $url_hash = sha1($url); $redis_key = "ssr_fpc:{$build_key}:{$url_hash}"; // Generate ETag (first 30 chars of SHA1) $content_for_etag = $build_key . $url . ($result['page_dom'] ?? $result['redirect'] ?? ''); $etag = substr(sha1($content_for_etag), 0, 30); // Build cache entry $cache_entry = [ 'url' => $url, 'code' => $result['code'], 'build_key' => $build_key, 'etag' => $etag, 'generated_at' => time(), ]; if ($result['code'] >= 300 && $result['code'] < 400) { // Redirect response $cache_entry['redirect'] = $result['redirect']; $cache_entry['page_dom'] = null; } else { // Normal response $cache_entry['page_dom'] = $result['page_dom']; $cache_entry['redirect'] = null; } // Store in Redis as JSON try { Redis::set($redis_key, json_encode($cache_entry)); $this->info("✅ Static cache generated successfully"); $this->info(" Redis key: {$redis_key}"); $this->info(" ETag: {$etag}"); $this->info(" Status: {$result['code']}"); } catch (\Exception $e) { $this->error('❌ Failed to store cache in Redis: ' . $e->getMessage()); return 1; } return 0; } }