Fix code quality violations and exclude Manifest from checks

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>
This commit is contained in:
root
2026-01-14 10:38:22 +00:00
parent bb9046af1b
commit d523f0f600
2355 changed files with 231384 additions and 32223 deletions

View File

@@ -7,10 +7,13 @@ use RecursiveCallbackFilterIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RuntimeException;
use App\RSpade\Core\Bundle\Cdn_Cache;
use App\RSpade\Core\Bundle\Minifier;
use App\RSpade\Core\Bundle\Rsx_Asset_Bundle_Abstract;
use App\RSpade\Core\Bundle\Rsx_Module_Bundle_Abstract;
use App\RSpade\Core\Locks\RsxLocks;
use App\RSpade\Core\Manifest\Manifest;
use App\RSpade\Core\Mode\Rsx_Mode;
/**
* BundleCompiler - Compiles RSX bundles into JS and CSS files
@@ -121,18 +124,25 @@ class BundleCompiler
public function compile(string $bundle_class, array $options = []): array
{
$this->bundle_name = $this->_get_bundle_name($bundle_class);
$this->is_production = app()->environment('production');
$this->is_production = Rsx_Mode::is_production_like();
$force_build = $options['force_build'] ?? false;
console_debug('BUNDLE', "Compiling {$this->bundle_name} (production: " . ($this->is_production ? 'yes' : 'no') . ')');
console_debug('BUNDLE', "Compiling {$this->bundle_name} (mode: " . Rsx_Mode::get() . ')');
// Step 1: Check production cache
if ($this->is_production) {
// Step 1: In production-like modes, require pre-built bundles (unless force_build)
if ($this->is_production && !$force_build) {
$existing = $this->_check_production_cache();
if ($existing) {
console_debug('BUNDLE', 'Using existing production bundle');
return $existing;
}
// In production-like modes, don't auto-rebuild - error instead
throw new RuntimeException(
"Bundle '{$this->bundle_name}' not compiled for production mode. " .
'Run: php artisan rsx:prod:build'
);
}
// Step 2: Mark the bundle we're compiling as already resolved
@@ -148,9 +158,18 @@ class BundleCompiler
// Step 5: Always split into vendor/app
$this->_split_vendor_app();
// Step 6: Check individual bundle caches
$need_compile = $this->_check_bundle_caches();
console_debug('BUNDLE', 'Need compile: ' . json_encode($need_compile));
// Step 6: Check individual bundle caches (or force all if force_build)
if ($force_build) {
$need_compile = ['vendor', 'app'];
$this->cache_keys = [];
foreach ($need_compile as $type) {
$this->cache_keys[$type] = $this->_get_cache_key($type);
}
console_debug('BUNDLE', 'Force build - compiling all types');
} else {
$need_compile = $this->_check_bundle_caches();
console_debug('BUNDLE', 'Need compile: ' . json_encode($need_compile));
}
// Step 7-10: Process bundles that need compilation
if (!empty($need_compile)) {
@@ -205,15 +224,16 @@ class BundleCompiler
// Step 13: Return data for render() method (not CLI)
// Include CDN assets and proper file paths for HTML generation
$bundle_dir = storage_path('rsx-build/bundles');
$result = [];
// Add CDN assets
// CDN assets are always output as separate tags
// In development mode: loaded directly from CDN URLs
// In production-like modes: served from /_vendor/ (cached locally)
if (!empty($this->cdn_assets['js'])) {
$result['cdn_js'] = $this->cdn_assets['js'];
$result['cdn_js'] = $this->_prepare_cdn_assets($this->cdn_assets['js'], 'js');
}
if (!empty($this->cdn_assets['css'])) {
$result['cdn_css'] = $this->cdn_assets['css'];
$result['cdn_css'] = $this->_prepare_cdn_assets($this->cdn_assets['css'], 'css');
}
// Add public directory assets
@@ -224,60 +244,18 @@ class BundleCompiler
$result['public_css'] = $this->public_assets['css'];
}
// Add bundle file paths for development
if (!$this->is_production) {
if (isset($outputs['vendor_js'])) {
$result['vendor_js_bundle_path'] = $outputs['vendor_js'];
}
if (isset($outputs['app_js'])) {
$result['app_js_bundle_path'] = $outputs['app_js'];
}
if (isset($outputs['vendor_css'])) {
$result['vendor_css_bundle_path'] = $outputs['vendor_css'];
}
if (isset($outputs['app_css'])) {
$result['app_css_bundle_path'] = $outputs['app_css'];
}
} else {
// Production mode - simple concatenation of vendor + app
$js_content = '';
if (isset($outputs['vendor_js'])) {
$js_content = file_get_contents("{$bundle_dir}/{$outputs['vendor_js']}");
}
if (isset($outputs['app_js'])) {
// Simple concatenation with newline separator
if ($js_content) {
$js_content .= "\n";
}
$js_content .= file_get_contents("{$bundle_dir}/{$outputs['app_js']}");
}
$css_content = '';
if (isset($outputs['vendor_css'])) {
$css_content = file_get_contents("{$bundle_dir}/{$outputs['vendor_css']}");
}
if (isset($outputs['app_css'])) {
// Simple concatenation with newline separator
if ($css_content) {
$css_content .= "\n";
}
$css_content .= file_get_contents("{$bundle_dir}/{$outputs['app_css']}");
}
// Write combined files with content hash
if ($js_content) {
$js_hash = substr(md5($js_content), 0, 16);
$js_file = "app.{$js_hash}.js";
file_put_contents("{$bundle_dir}/{$js_file}", $js_content);
$result['js_bundle_path'] = $js_file;
}
if ($css_content) {
$css_hash = substr(md5($css_content), 0, 16);
$css_file = "app.{$css_hash}.css";
file_put_contents("{$bundle_dir}/{$css_file}", $css_content);
$result['css_bundle_path'] = $css_file;
}
// Always return vendor/app split paths
if (isset($outputs['vendor_js'])) {
$result['vendor_js_bundle_path'] = $outputs['vendor_js'];
}
if (isset($outputs['app_js'])) {
$result['app_js_bundle_path'] = $outputs['app_js'];
}
if (isset($outputs['vendor_css'])) {
$result['vendor_css_bundle_path'] = $outputs['vendor_css'];
}
if (isset($outputs['app_css'])) {
$result['app_css_bundle_path'] = $outputs['app_css'];
}
// Add config if present
@@ -441,28 +419,137 @@ class BundleCompiler
return end($parts);
}
/**
* Get bundle FQCN from simple class name
*/
protected function _get_bundle_fqcn(string $bundle_name): string
{
// If already a FQCN, return as-is
if (str_contains($bundle_name, '\\')) {
return $bundle_name;
}
// Look up in Manifest
$metadata = Manifest::php_get_metadata_by_class($bundle_name);
return $metadata['fqcn'];
}
/**
* Check if production bundle already exists
*/
protected function _check_production_cache(): ?array
{
$bundle_dir = storage_path('rsx-build/bundles');
$js_pattern = "{$bundle_dir}/app.*.js";
$css_pattern = "{$bundle_dir}/app.*.css";
$js_files = glob($js_pattern);
$css_files = glob($css_pattern);
// Look for split vendor/app files (current output format)
// Future: support merged files when Rsx_Mode::should_merge_bundles()
$vendor_js_pattern = "{$bundle_dir}/{$this->bundle_name}__vendor.*.js";
$app_js_pattern = "{$bundle_dir}/{$this->bundle_name}__app.*.js";
$vendor_css_pattern = "{$bundle_dir}/{$this->bundle_name}__vendor.*.css";
$app_css_pattern = "{$bundle_dir}/{$this->bundle_name}__app.*.css";
if (!empty($js_files) || !empty($css_files)) {
return [
'js_bundle_path' => !empty($js_files) ? basename($js_files[0]) : null,
'css_bundle_path' => !empty($css_files) ? basename($css_files[0]) : null,
$vendor_js_files = glob($vendor_js_pattern);
$app_js_files = glob($app_js_pattern);
$vendor_css_files = glob($vendor_css_pattern);
$app_css_files = glob($app_css_pattern);
// Need at least one app file (JS is typically required)
if (!empty($app_js_files)) {
$result = [
'vendor_js_bundle_path' => !empty($vendor_js_files) ? basename($vendor_js_files[0]) : null,
'app_js_bundle_path' => !empty($app_js_files) ? basename($app_js_files[0]) : null,
'vendor_css_bundle_path' => !empty($vendor_css_files) ? basename($vendor_css_files[0]) : null,
'app_css_bundle_path' => !empty($app_css_files) ? basename($app_css_files[0]) : null,
];
// Also resolve CDN assets - they're served separately via /_vendor/ URLs
// We need to resolve the bundle includes to get CDN asset definitions
$this->_resolve_cdn_assets_only();
if (!empty($this->cdn_assets['js'])) {
$result['cdn_js'] = $this->_prepare_cdn_assets($this->cdn_assets['js'], 'js');
}
if (!empty($this->cdn_assets['css'])) {
$result['cdn_css'] = $this->_prepare_cdn_assets($this->cdn_assets['css'], 'css');
}
return $result;
}
return null;
}
/**
* Resolve only CDN assets without full bundle compilation
*
* Used when serving from production cache to get CDN asset URLs
* without re-resolving and re-compiling all bundle files.
*/
protected function _resolve_cdn_assets_only(): void
{
// Process required bundles first (they may have CDN assets)
$required_bundles = config('rsx.required_bundles', []);
$bundle_aliases = config('rsx.bundle_aliases', []);
foreach ($required_bundles as $alias) {
if (isset($bundle_aliases[$alias])) {
$this->_collect_cdn_assets_from_include($bundle_aliases[$alias]);
}
}
// Get the bundle's own CDN assets
$fqcn = $this->_get_bundle_fqcn($this->bundle_name);
$definition = $fqcn::define();
// Add CDN assets from the bundle definition (same format as main resolution)
if (!empty($definition['cdn_assets'])) {
if (!empty($definition['cdn_assets']['js'])) {
$this->cdn_assets['js'] = array_merge($this->cdn_assets['js'], $definition['cdn_assets']['js']);
}
if (!empty($definition['cdn_assets']['css'])) {
$this->cdn_assets['css'] = array_merge($this->cdn_assets['css'], $definition['cdn_assets']['css']);
}
}
// Also check for Asset Bundle includes that may have CDN assets
foreach ($definition['include'] ?? [] as $include) {
$this->_collect_cdn_assets_from_include($include);
}
}
/**
* Collect CDN assets from a bundle include without full resolution
*/
protected function _collect_cdn_assets_from_include($include): void
{
// Handle config array format (from bundle aliases like jquery, lodash)
if (is_array($include) && isset($include['cdn'])) {
foreach ($include['cdn'] as $cdn_item) {
// Determine type from URL extension
$url = $cdn_item['url'] ?? '';
$type = str_ends_with($url, '.css') ? 'css' : 'js';
$this->cdn_assets[$type][] = $cdn_item;
}
return;
}
// Handle class name includes (could be Asset Bundles with CDN assets)
if (is_string($include) && Manifest::php_find_class($include)) {
if (Manifest::php_is_subclass_of($include, 'Rsx_Asset_Bundle_Abstract')) {
$asset_def = $include::define();
if (!empty($asset_def['cdn_assets'])) {
if (!empty($asset_def['cdn_assets']['js'])) {
$this->cdn_assets['js'] = array_merge($this->cdn_assets['js'], $asset_def['cdn_assets']['js']);
}
if (!empty($asset_def['cdn_assets']['css'])) {
$this->cdn_assets['css'] = array_merge($this->cdn_assets['css'], $asset_def['cdn_assets']['css']);
}
}
}
}
}
/**
* Process required bundles (jquery, lodash, jqhtml)
*/
@@ -1856,7 +1943,18 @@ implode("\n", array_map(fn ($f) => ' - ' . str_replace(base_path() . '/', '',
// Compile JS
if (!empty($files['js'])) {
$js_content = $this->_compile_js_files($files['js']);
$js_files = $files['js'];
// CDN assets are served separately via /_vendor/ URLs, not merged into bundle
// This avoids complex concatenation issues with third-party code
$js_content = $this->_compile_js_files($js_files);
// Minify JS in production mode only (strips sourcemaps)
if (Rsx_Mode::is_production()) {
$js_content = Minifier::minify_js($js_content, "{$this->bundle_name}__{$type}.js");
}
$js_file = "{$this->bundle_name}__{$type}.{$hash}.js";
file_put_contents("{$bundle_dir}/{$js_file}", $js_content);
$outputs["{$type}_js"] = $js_file;
@@ -1864,7 +1962,18 @@ implode("\n", array_map(fn ($f) => ' - ' . str_replace(base_path() . '/', '',
// Compile CSS
if (!empty($files['css'])) {
$css_content = $this->_compile_css_files($files['css']);
$css_files = $files['css'];
// CDN assets are served separately via /_vendor/ URLs, not merged into bundle
// This avoids complex concatenation issues with third-party code
$css_content = $this->_compile_css_files($css_files);
// Minify CSS in production mode only (strips sourcemaps)
if (Rsx_Mode::is_production()) {
$css_content = Minifier::minify_css($css_content, "{$this->bundle_name}__{$type}.css");
}
$css_file = "{$this->bundle_name}__{$type}.{$hash}.css";
file_put_contents("{$bundle_dir}/{$css_file}", $css_content);
$outputs["{$type}_css"] = $css_file;
@@ -2189,8 +2298,11 @@ implode("\n", array_map(fn ($f) => ' - ' . str_replace(base_path() . '/', '',
// This preserves dependency sort order - we substitute babel versions during concat
foreach ($files as $file) {
// Skip temp files and already processed files
if (str_contains($file, 'storage/rsx-tmp/') || str_contains($file, 'storage/rsx-build/')) {
// Skip temp files, already processed files, and CDN cache files
// CDN files are third-party production code - don't transform them
if (str_contains($file, 'storage/rsx-tmp/') ||
str_contains($file, 'storage/rsx-build/') ||
str_contains($file, '.cdn-cache/')) {
continue;
}
@@ -2276,11 +2388,6 @@ implode("\n", array_map(fn ($f) => ' - ' . str_replace(base_path() . '/', '',
}
@unlink($output_file);
// Minify in production (TODO: preserve source maps when minifying)
if ($this->is_production) {
// TODO: Add minification that preserves source maps
}
return $js;
}
@@ -2332,12 +2439,103 @@ implode("\n", array_map(fn ($f) => ' - ' . str_replace(base_path() . '/', '',
// Clean up temp file
@unlink($output_file);
// Minify in production
if ($this->is_production) {
// TODO: Add minification that preserves source maps
return $css;
}
/**
* Prepare CDN assets for rendering
*
* In development mode: returns assets as-is (loaded from CDN URLs)
* In production-like modes: ensures assets are cached and adds cached_filename
* so rendering can use /_vendor/{filename} URLs
*
* @param array $assets CDN assets array
* @param string $type 'js' or 'css'
* @return array Prepared assets with cached_filename in production modes
*/
protected function _prepare_cdn_assets(array $assets, string $type): array
{
// In development mode, return as-is (use CDN URLs directly)
if (!$this->is_production) {
return $assets;
}
return $css;
// In production-like modes, ensure cached and add filename
$prepared = [];
foreach ($assets as $asset) {
$url = $asset['url'] ?? '';
if (empty($url)) {
continue;
}
// Ensure the asset is cached (downloads if not already)
Cdn_Cache::get($url, $type);
// Add cached filename for /_vendor/ URL generation
$asset['cached_filename'] = Cdn_Cache::get_cache_filename($url, $type);
$prepared[] = $asset;
}
return $prepared;
}
/**
* Get local file paths for cached CDN assets (DEPRECATED)
*
* Used in production-like modes to include CDN assets in concat scripts
* for proper sourcemap handling.
*
* @param string $type 'js' or 'css'
* @return array Array of local file paths to cached CDN files
* @deprecated CDN assets are now served via /_vendor/ URLs, not merged into bundles
*/
protected function _get_cdn_cache_file_paths(string $type): array
{
$file_paths = [];
$assets = $this->cdn_assets[$type] ?? [];
if (empty($assets)) {
return $file_paths;
}
// Sort assets: jQuery first, then others alphabetically
$jquery_assets = [];
$other_assets = [];
foreach ($assets as $asset) {
$url = $asset['url'] ?? '';
if (stripos($url, 'jquery') !== false) {
$jquery_assets[] = $asset;
} else {
$other_assets[] = $asset;
}
}
usort($other_assets, function ($a, $b) {
return strcmp($a['url'] ?? '', $b['url'] ?? '');
});
$sorted_assets = array_merge($jquery_assets, $other_assets);
// Get cache file path for each asset (downloads if not cached)
// If download fails, let it throw - CDN assets are required
foreach ($sorted_assets as $asset) {
$url = $asset['url'] ?? '';
if (empty($url)) {
continue;
}
// This will download and cache if not already cached
Cdn_Cache::get($url, $type);
// Get the cache file path
$cache_path = Cdn_Cache::get_cache_path($url, $type);
if (file_exists($cache_path)) {
$file_paths[] = $cache_path;
}
}
return $file_paths;
}
/**