Document application modes (development/debug/production) Add global file drop handler, order column normalization, SPA hash fix Serve CDN assets via /_vendor/ URLs instead of merging into bundles Add production minification with license preservation Improve JSON formatting for debugging and production optimization Add CDN asset caching with CSS URL inlining for production builds Add three-mode system (development, debug, production) Update Manifest CLAUDE.md to reflect helper class architecture Refactor Manifest.php into helper classes for better organization Pre-manifest-refactor checkpoint: Add app_mode documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
307 lines
8.5 KiB
PHP
Executable File
307 lines
8.5 KiB
PHP
Executable File
<?php
|
|
/**
|
|
* JavaScript and CSS Minifier
|
|
*
|
|
* Uses a persistent RPC server for efficient minification of multiple files.
|
|
* Terser for JavaScript, cssnano for CSS.
|
|
* Only used in production mode - debug mode retains readable code.
|
|
*/
|
|
|
|
namespace App\RSpade\Core\Bundle;
|
|
|
|
use Symfony\Component\Process\Process;
|
|
use RuntimeException;
|
|
|
|
class Minifier
|
|
{
|
|
/**
|
|
* RPC server script path
|
|
*/
|
|
protected const RPC_SERVER_SCRIPT = 'app/RSpade/Core/Bundle/resource/minify-server.js';
|
|
|
|
/**
|
|
* RPC server socket path
|
|
*/
|
|
protected const RPC_SOCKET = 'storage/rsx-tmp/minify-server.sock';
|
|
|
|
/**
|
|
* RPC server process
|
|
*/
|
|
protected static $rpc_server_process = null;
|
|
|
|
/**
|
|
* RPC request ID counter
|
|
*/
|
|
protected static int $request_id = 0;
|
|
|
|
/**
|
|
* Whether server has been started
|
|
*/
|
|
protected static bool $server_started = false;
|
|
|
|
/**
|
|
* Minify JavaScript content
|
|
*
|
|
* @param string $content JavaScript content to minify
|
|
* @param string $filename Filename for error reporting
|
|
* @return string Minified content
|
|
*/
|
|
public static function minify_js(string $content, string $filename = 'bundle.js'): string
|
|
{
|
|
return static::_minify_via_rpc($content, 'js', $filename);
|
|
}
|
|
|
|
/**
|
|
* Minify CSS content
|
|
*
|
|
* @param string $content CSS content to minify
|
|
* @param string $filename Filename for error reporting
|
|
* @return string Minified content
|
|
*/
|
|
public static function minify_css(string $content, string $filename = 'bundle.css'): string
|
|
{
|
|
return static::_minify_via_rpc($content, 'css', $filename);
|
|
}
|
|
|
|
/**
|
|
* Minify content via RPC server
|
|
*/
|
|
protected static function _minify_via_rpc(string $content, string $type, string $filename): string
|
|
{
|
|
// Ensure server is running
|
|
if (!static::$server_started) {
|
|
static::start_rpc_server();
|
|
}
|
|
|
|
$socket_path = base_path(self::RPC_SOCKET);
|
|
|
|
$socket = @stream_socket_client('unix://' . $socket_path, $errno, $errstr, 30);
|
|
if (!$socket) {
|
|
throw new RuntimeException("Failed to connect to minify RPC server: {$errstr}");
|
|
}
|
|
|
|
stream_set_blocking($socket, true);
|
|
|
|
static::$request_id++;
|
|
$request = json_encode([
|
|
'id' => static::$request_id,
|
|
'method' => 'minify',
|
|
'files' => [
|
|
[
|
|
'type' => $type,
|
|
'content' => $content,
|
|
'filename' => $filename
|
|
]
|
|
]
|
|
]) . "\n";
|
|
|
|
fwrite($socket, $request);
|
|
|
|
$response = fgets($socket);
|
|
fclose($socket);
|
|
|
|
if (!$response) {
|
|
throw new RuntimeException("No response from minify RPC server");
|
|
}
|
|
|
|
$result = json_decode($response, true);
|
|
|
|
if (!isset($result['results'][$filename])) {
|
|
throw new RuntimeException("Invalid response from minify RPC server");
|
|
}
|
|
|
|
$file_result = $result['results'][$filename];
|
|
|
|
if ($file_result['status'] === 'error') {
|
|
$error = $file_result['error'];
|
|
throw new RuntimeException(
|
|
"Minification failed for {$filename}: {$error['message']}"
|
|
);
|
|
}
|
|
|
|
return $file_result['result'];
|
|
}
|
|
|
|
/**
|
|
* Force restart the RPC server
|
|
*
|
|
* Call this before production builds to ensure code changes take effect.
|
|
*/
|
|
public static function force_restart(): void
|
|
{
|
|
static::stop_rpc_server(force: true);
|
|
static::$server_started = false;
|
|
static::start_rpc_server();
|
|
}
|
|
|
|
/**
|
|
* Start RPC server for minification
|
|
*/
|
|
public static function start_rpc_server(): void
|
|
{
|
|
$socket_path = base_path(self::RPC_SOCKET);
|
|
$server_script = base_path(self::RPC_SERVER_SCRIPT);
|
|
|
|
if (!file_exists($server_script)) {
|
|
throw new RuntimeException("Minify RPC server script not found at {$server_script}");
|
|
}
|
|
|
|
// If socket exists, check if it's stale
|
|
if (file_exists($socket_path)) {
|
|
if (static::ping_rpc_server()) {
|
|
static::$server_started = true;
|
|
return;
|
|
}
|
|
static::stop_rpc_server(force: true);
|
|
}
|
|
|
|
// Ensure socket directory exists
|
|
$socket_dir = dirname($socket_path);
|
|
if (!is_dir($socket_dir)) {
|
|
mkdir($socket_dir, 0755, true);
|
|
}
|
|
|
|
// Start RPC server
|
|
$process = new Process([
|
|
'node',
|
|
$server_script,
|
|
'--socket=' . $socket_path
|
|
]);
|
|
|
|
$process->setWorkingDirectory(base_path());
|
|
$process->setTimeout(null);
|
|
$process->start();
|
|
|
|
static::$rpc_server_process = $process;
|
|
|
|
// Wait for server to be ready
|
|
$max_wait_ms = 10000;
|
|
$wait_interval_ms = 50;
|
|
$iterations = $max_wait_ms / $wait_interval_ms;
|
|
|
|
for ($i = 0; $i < $iterations; $i++) {
|
|
usleep($wait_interval_ms * 1000);
|
|
|
|
if (static::ping_rpc_server()) {
|
|
static::$server_started = true;
|
|
register_shutdown_function([self::class, 'stop_rpc_server']);
|
|
return;
|
|
}
|
|
}
|
|
|
|
throw new RuntimeException(
|
|
"Minify RPC server failed to start within {$max_wait_ms}ms.\n" .
|
|
"Check that Node.js, Terser, and cssnano are installed."
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Ping the RPC server
|
|
*/
|
|
public static function ping_rpc_server(): bool
|
|
{
|
|
$socket_path = base_path(self::RPC_SOCKET);
|
|
|
|
if (!file_exists($socket_path)) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
$socket = @stream_socket_client('unix://' . $socket_path, $errno, $errstr, 1);
|
|
if (!$socket) {
|
|
return false;
|
|
}
|
|
|
|
stream_set_blocking($socket, true);
|
|
|
|
static::$request_id++;
|
|
$request = json_encode([
|
|
'id' => static::$request_id,
|
|
'method' => 'ping'
|
|
]) . "\n";
|
|
|
|
fwrite($socket, $request);
|
|
$response = fgets($socket);
|
|
fclose($socket);
|
|
|
|
if (!$response) {
|
|
return false;
|
|
}
|
|
|
|
$result = json_decode($response, true);
|
|
return isset($result['result']) && $result['result'] === 'pong';
|
|
} catch (\Exception $e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stop the RPC server
|
|
*/
|
|
public static function stop_rpc_server(bool $force = false): void
|
|
{
|
|
$socket_path = base_path(self::RPC_SOCKET);
|
|
|
|
if ($force) {
|
|
if (file_exists($socket_path)) {
|
|
try {
|
|
$socket = @stream_socket_client('unix://' . $socket_path, $errno, $errstr, 1);
|
|
if ($socket) {
|
|
stream_set_blocking($socket, true);
|
|
|
|
static::$request_id++;
|
|
$request = json_encode([
|
|
'id' => static::$request_id,
|
|
'method' => 'shutdown'
|
|
]) . "\n";
|
|
|
|
fwrite($socket, $request);
|
|
fclose($socket);
|
|
}
|
|
} catch (\Exception $e) {
|
|
// Ignore errors during force shutdown
|
|
}
|
|
|
|
usleep(100000); // 100ms
|
|
|
|
if (file_exists($socket_path)) {
|
|
@unlink($socket_path);
|
|
}
|
|
}
|
|
|
|
if (static::$rpc_server_process) {
|
|
if (static::$rpc_server_process->isRunning()) {
|
|
static::$rpc_server_process->stop(1, SIGTERM);
|
|
}
|
|
static::$rpc_server_process = null;
|
|
}
|
|
|
|
static::$server_started = false;
|
|
return;
|
|
}
|
|
|
|
// Graceful shutdown
|
|
if (file_exists($socket_path)) {
|
|
try {
|
|
$socket = @stream_socket_client('unix://' . $socket_path, $errno, $errstr, 1);
|
|
if ($socket) {
|
|
stream_set_blocking($socket, true);
|
|
|
|
static::$request_id++;
|
|
$request = json_encode([
|
|
'id' => static::$request_id,
|
|
'method' => 'shutdown'
|
|
]) . "\n";
|
|
|
|
fwrite($socket, $request);
|
|
fclose($socket);
|
|
}
|
|
} catch (\Exception $e) {
|
|
// Ignore errors
|
|
}
|
|
}
|
|
|
|
static::$server_started = false;
|
|
}
|
|
}
|