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>
320 lines
10 KiB
PHP
Executable File
320 lines
10 KiB
PHP
Executable File
<?php
|
|
|
|
namespace App\RSpade\Integrations\Jqhtml;
|
|
|
|
use App\RSpade\Integrations\Jqhtml\Jqhtml_Exception_ViewException;
|
|
|
|
/**
|
|
* JqhtmlWebpackCompiler - Compiles JQHTML templates to JavaScript during bundle build
|
|
*
|
|
* This service handles the compilation of .jqhtml template files into JavaScript
|
|
* during the bundle build process. It uses the @jqhtml/parser NPM package to
|
|
* perform the compilation and caches the results based on file modification times.
|
|
*
|
|
* Features:
|
|
* - Uses @jqhtml/parser for template compilation
|
|
* - Caches compiled templates based on file mtime
|
|
* - Throws fatal errors on compilation failures (fail loud)
|
|
* - Integrates with the bundle compilation pipeline
|
|
*/
|
|
#[Instantiatable]
|
|
class JqhtmlWebpackCompiler
|
|
{
|
|
/**
|
|
* Path to the jqhtml-compile binary
|
|
*/
|
|
protected string $compiler_path;
|
|
|
|
/**
|
|
* Cache directory for compiled templates
|
|
*/
|
|
protected string $cache_dir;
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
public function __construct()
|
|
{
|
|
// Use official jqhtml CLI compiler from npm package
|
|
$this->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 proc_open() instead of \exec_safe() to handle large template outputs
|
|
// \exec_safe() can truncate output for complex templates due to line-by-line buffering
|
|
$command = sprintf(
|
|
'%s compile %s --format iife --sourcemap',
|
|
escapeshellarg($this->compiler_path),
|
|
escapeshellarg($file_path)
|
|
);
|
|
|
|
$descriptors = [
|
|
0 => ['pipe', 'r'], // stdin
|
|
1 => ['pipe', 'w'], // stdout
|
|
2 => ['pipe', 'w'] // stderr
|
|
];
|
|
|
|
$process = proc_open($command, $descriptors, $pipes);
|
|
|
|
if (!is_resource($process)) {
|
|
throw new \RuntimeException("Failed to execute jqhtml compiler");
|
|
}
|
|
|
|
// Close stdin
|
|
fclose($pipes[0]);
|
|
|
|
// Set blocking mode to ensure complete reads
|
|
stream_set_blocking($pipes[1], true);
|
|
stream_set_blocking($pipes[2], true);
|
|
|
|
// Read stdout and stderr completely in chunks
|
|
// CRITICAL: Use feof() as loop condition to prevent race condition truncation
|
|
// Checking feof() AFTER empty reads can cause 8192-byte truncation bug
|
|
$output_str = '';
|
|
$error_str = '';
|
|
|
|
// Read stdout until EOF
|
|
while (!feof($pipes[1])) {
|
|
$chunk = fread($pipes[1], 8192);
|
|
if ($chunk !== false) {
|
|
$output_str .= $chunk;
|
|
}
|
|
}
|
|
|
|
// Read stderr until EOF
|
|
while (!feof($pipes[2])) {
|
|
$chunk = fread($pipes[2], 8192);
|
|
if ($chunk !== false) {
|
|
$error_str .= $chunk;
|
|
}
|
|
}
|
|
|
|
fclose($pipes[1]);
|
|
fclose($pipes[2]);
|
|
|
|
// Get return code
|
|
$return_code = proc_close($process);
|
|
|
|
// Combine stdout and stderr for error messages
|
|
if ($return_code !== 0 && !empty($error_str)) {
|
|
$output_str = $error_str . "\n" . $output_str;
|
|
}
|
|
|
|
// Check for compilation errors
|
|
if ($return_code !== 0) {
|
|
// Official CLI outputs errors to stderr (captured in stdout with 2>&1)
|
|
// 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 - the output is the compiled JavaScript
|
|
$compiled_js = $output_str;
|
|
|
|
// 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];
|
|
}
|
|
} |