compiler_path = base_path('node_modules/@jqhtml/parser/bin/jqhtml-compile'); $this->cache_dir = storage_path('rsx-tmp/jqhtml-cache'); // Ensure cache directory exists if (!is_dir($this->cache_dir)) { mkdir($this->cache_dir, 0755, true); } // Validate compiler exists - MUST exist if (!file_exists($this->compiler_path)) { throw new \RuntimeException( "Official JQHTML CLI compiler not found at: {$this->compiler_path}. " . "Run 'npm install @jqhtml/parser@^2.2.59' to install the official CLI compiler." ); } } /** * Compile a single JQHTML template file * * @param string $file_path Path to .jqhtml file * @return string Compiled JavaScript code * @throws \RuntimeException On compilation failure */ public function compile_file(string $file_path): string { if (!file_exists($file_path)) { throw new \RuntimeException("JQHTML template not found: {$file_path}"); } // Get file modification time for cache key $mtime = filemtime($file_path); $cache_key = md5($file_path) . '_' . $mtime; $cache_file = $this->cache_dir . '/' . $cache_key . '.js'; // Check if cached version exists if (file_exists($cache_file)) { console_debug("JQHTML", "Using cached JQHTML template: {$file_path}"); return file_get_contents($cache_file); } console_debug("JQHTML", "Compiling JQHTML template: {$file_path}"); // Execute official CLI compiler with IIFE format for self-registering templates // CRITICAL: Must include --sourcemap for proper error mapping in bundles // JQHTML v2.2.65+ uses Mozilla source-map library for reliable concatenation // IMPORTANT: Using file redirection instead of proc_open() to avoid pipe buffer truncation // proc_open() has race conditions with feof() that cause silent data loss on large outputs (35KB+) // Generate temp file for output $temp_file = storage_path('rsx-tmp/jqhtml_compile_' . uniqid() . '.js'); // Redirect stdout to file, stderr to stdout for error capture, then echo exit code $command = sprintf( '%s compile %s --format iife --sourcemap > %s 2>&1; echo $?', escapeshellarg($this->compiler_path), escapeshellarg($file_path), escapeshellarg($temp_file) ); // Execute command synchronously - shell_exec captures the exit code from echo $? $result = shell_exec($command); $return_code = (int)trim($result); // Read the compiled output from file $compiled_js = ''; if (file_exists($temp_file)) { $compiled_js = file_get_contents($temp_file); unlink($temp_file); // Clean up temp file } // If there was an error, the output file will contain the error message $output_str = $compiled_js; // Check for compilation errors if ($return_code !== 0) { // Error output captured in output_str via 2>&1 redirection // Try multiple error formats // Format 1: "at filename:line:column" (newer format) if (preg_match('/at [^:]+:(\d+):(\d+)/i', $output_str, $matches)) { $line = (int)$matches[1]; $column = (int)$matches[2]; throw new Jqhtml_Exception_ViewException( "JQHTML compilation failed:\n{$output_str}", $file_path, $line, $column ); } // Format 2: "Error at line X, column Y:" (older format) if (preg_match('/Error at line (\d+), column (\d+):/i', $output_str, $matches)) { $line = (int)$matches[1]; $column = (int)$matches[2]; throw new Jqhtml_Exception_ViewException( "JQHTML compilation failed:\n{$output_str}", $file_path, $line, $column ); } // Format 3: No line number found - generic error throw new \RuntimeException( "JQHTML compilation failed for {$file_path}:\n{$output_str}" ); } // Success - compiled_js already contains the output from the temp file // Don't add any comments - they break sourcemap line offsets // Just use the compiler output as-is $wrapped_js = $compiled_js; // Ensure proper newline at end if (!str_ends_with($wrapped_js, "\n")) { $wrapped_js .= "\n"; } // Cache the compiled result file_put_contents($cache_file, $wrapped_js); // Clean up old cache files for this template $this->cleanup_old_cache($file_path, $cache_key); return $wrapped_js; } /** * Compile multiple JQHTML template files * * @param array $files Array of file paths * @return array Compiled JavaScript code keyed by file path */ public function compile_files(array $files): array { $compiled = []; foreach ($files as $file) { try { $compiled[$file] = $this->compile_file($file); } catch (\Exception $e) { // FAIL LOUD - don't continue on error throw new \RuntimeException( "Failed to compile JQHTML templates: " . $e->getMessage() ); } } return $compiled; } /** * Extract component name from file path * * @param string $file_path Path to .jqhtml file * @return string Component name */ protected function extract_component_name(string $file_path): string { // Remove base path and extension $relative = str_replace(base_path() . '/', '', $file_path); $relative = preg_replace('/\.jqhtml$/i', '', $relative); // Convert path to component name (e.g., rsx/app/components/MyComponent) // to MyComponent or components/MyComponent $parts = explode('/', $relative); // Use the filename as the component name return basename($relative); } /** * Clean up old cache files for a template * * @param string $file_path Original template path * @param string $current_cache_key Current cache key to keep */ protected function cleanup_old_cache(string $file_path, string $current_cache_key): void { $file_hash = md5($file_path); $pattern = $this->cache_dir . '/' . $file_hash . '_*.js'; foreach (glob($pattern) as $cache_file) { $cache_key = basename($cache_file, '.js'); if ($cache_key !== $current_cache_key) { unlink($cache_file); } } } /** * Clear all cached templates */ public function clear_cache(): void { $pattern = $this->cache_dir . '/*.js'; foreach (glob($pattern) as $cache_file) { unlink($cache_file); } console_debug("JQHTML", "Cleared JQHTML template cache"); } /** * Get cache statistics * * @return array Cache statistics */ public function get_cache_stats(): array { $pattern = $this->cache_dir . '/*.js'; $files = glob($pattern); $total_size = 0; foreach ($files as $file) { $total_size += filesize($file); } return [ 'cache_dir' => $this->cache_dir, 'cached_files' => count($files), 'total_size' => $total_size, 'total_size_human' => $this->format_bytes($total_size) ]; } /** * Format bytes to human readable * * @param int $bytes * @return string */ protected function format_bytes(int $bytes): string { $units = ['B', 'KB', 'MB', 'GB']; $i = 0; while ($bytes >= 1024 && $i < count($units) - 1) { $bytes /= 1024; $i++; } return round($bytes, 2) . ' ' . $units[$i]; } }