file_path (without extension) $blade_ids = []; // id => file_path (without extension) foreach ($files as $file => $file_metadata) { $extension = $file_metadata['extension'] ?? ''; // Collect Component subclasses (includes Spa_Action, Spa_Layout, and direct Component subclasses) if ($extension === 'js') { $class_name = $file_metadata['class'] ?? null; if ($class_name) { try { if (Manifest::js_is_subclass_of($class_name, 'Component')) { $components[$class_name] = pathinfo($file, PATHINFO_FILENAME); } } catch (\Exception $e) { // Class not found in inheritance chain, skip } } } // Collect Blade @rsx_id values if ($extension === 'blade.php') { $id = $file_metadata['id'] ?? null; if ($id) { // Remove .blade.php to get base filename $filename = basename($file, '.blade.php'); $blade_ids[$id] = $filename; } } // Collect jqhtml component IDs (from ) // These are Components without a companion .js file if ($extension === 'jqhtml') { $id = $file_metadata['id'] ?? null; if ($id) { // Check if there's already a .js file for this component // If so, skip - the .js file takes precedence if (!isset($components[$id])) { $filename = pathinfo($file, PATHINFO_FILENAME); $components[$id] = $filename; } } } } // Build map of wrapper classes that have a primary SCSS file (filename matches) // This allows supplemental SCSS files with different names for the same wrapper class $wrapper_classes_with_primary_scss = []; foreach ($files as $file => $file_metadata) { $extension = $file_metadata['extension'] ?? ''; if ($extension !== 'scss') { continue; } $wrapper_class = $file_metadata['scss_wrapper_class'] ?? null; if ($wrapper_class === null) { continue; } $scss_filename = pathinfo($file, PATHINFO_FILENAME); // Check if this SCSS file's name matches its wrapper class's associated file $matched_filename = $components[$wrapper_class] ?? $blade_ids[$wrapper_class] ?? null; if ($matched_filename !== null && strcasecmp($scss_filename, $matched_filename) === 0) { $wrapper_classes_with_primary_scss[$wrapper_class] = true; } } // Now validate SCSS files foreach ($files as $file => $file_metadata) { $extension = $file_metadata['extension'] ?? ''; if ($extension !== 'scss') { continue; } // Determine which validation to apply based on path $is_rsx_app = str_starts_with($file, 'rsx/app/'); $is_theme_component = str_starts_with($file, 'rsx/theme/components/'); // Skip files outside our enforcement paths if (!$is_rsx_app && !$is_theme_component) { continue; } // NO EXEMPTIONS - all files in these paths must follow the convention // Skip files that contain only variables and comments (no actual rules) if (!empty($file_metadata['scss_variables_only'])) { continue; } $wrapper_class = $file_metadata['scss_wrapper_class'] ?? null; $scss_filename = pathinfo($file, PATHINFO_FILENAME); $this->validate_scss_file($file, $wrapper_class, $scss_filename, $components, $blade_ids, $wrapper_classes_with_primary_scss); } } /** * Validate SCSS file against Component classes and Blade @rsx_id values */ private function validate_scss_file( string $file, ?string $wrapper_class, string $scss_filename, array $components, array $blade_ids, array $wrapper_classes_with_primary_scss ): void { // Check if file has a wrapper class if ($wrapper_class === null) { $this->add_violation( $file, 1, "SCSS file must be fully enclosed in a single class rule (e.g., .My_Component { ... })", basename($file), $this->build_no_wrapper_suggestion($file), 'critical' ); return; } // Check if wrapper class matches a Component or Blade @rsx_id $matched_filename = null; $match_type = null; if (isset($components[$wrapper_class])) { $matched_filename = $components[$wrapper_class]; $match_type = 'Component'; } elseif (isset($blade_ids[$wrapper_class])) { $matched_filename = $blade_ids[$wrapper_class]; $match_type = 'Blade @rsx_id'; } if ($matched_filename === null) { $this->add_violation( $file, 1, "SCSS wrapper class '{$wrapper_class}' does not match any Component class or Blade @rsx_id", ".{$wrapper_class} { ... }", $this->build_no_match_suggestion($file, $wrapper_class), 'critical' ); return; } // Check if filename matches if (strcasecmp($scss_filename, $matched_filename) !== 0) { // Allow supplemental SCSS files if a primary file already exists for this wrapper class if (isset($wrapper_classes_with_primary_scss[$wrapper_class])) { // This is a supplemental file - filename mismatch is allowed return; } $this->add_violation( $file, 1, "SCSS filename '{$scss_filename}.scss' must match associated {$match_type} file '{$matched_filename}'. " . "Supplemental files with different names are allowed only if a primary file ('{$matched_filename}.scss') exists. " . "See: php artisan rsx:man scss", basename($file), $this->build_filename_mismatch_suggestion($file, $scss_filename, $matched_filename, $match_type), 'critical' ); } } /** * Build suggestion for files without a wrapper class */ private function build_no_wrapper_suggestion(string $file): string { $lines = []; $lines[] = "SCSS files in rsx/app/ and rsx/theme/components/ must be fully enclosed"; $lines[] = "in a single top-level class selector that matches their associated"; $lines[] = "action, layout, component, or Blade view."; $lines[] = ""; $lines[] = "This prevents CSS conflicts and ensures styles are scoped to the"; $lines[] = "element they are intended to style."; $lines[] = ""; $lines[] = "VALID ASSOCIATIONS:"; $lines[] = " - Spa_Action class (e.g., Frontend_Dashboard extends Spa_Action)"; $lines[] = " - Spa_Layout class (e.g., Frontend_Layout extends Spa_Layout)"; $lines[] = " - Component class (e.g., Sidebar_Nav extends Component)"; $lines[] = " - Blade @rsx_id (e.g., @rsx_id('Login_Index'))"; $lines[] = ""; $lines[] = "TO FIX:"; $lines[] = " Wrap ALL rules in the associated class:"; $lines[] = ""; $lines[] = " .My_Component {"; $lines[] = " // ALL styles go here"; $lines[] = " .card { ... }"; $lines[] = " .button { ... }"; $lines[] = " }"; $lines[] = ""; $lines[] = "NO EXEMPTIONS are allowed in these directories. If this file cannot"; $lines[] = "be associated to an action/layout/component/view, it may need to be"; $lines[] = "moved to rsx/theme/ (outside components/) - but this is rare and"; $lines[] = "requires explicit developer approval."; $lines[] = ""; $lines[] = "See: php artisan rsx:man scss"; return implode("\n", $lines); } /** * Build suggestion for wrapper class that doesn't match anything */ private function build_no_match_suggestion(string $file, string $wrapper_class): string { $lines = []; $lines[] = "The wrapper class '{$wrapper_class}' does not match any known"; $lines[] = "action, layout, component, or Blade view."; $lines[] = ""; $lines[] = "VALID ASSOCIATIONS:"; $lines[] = " - Spa_Action class (e.g., Frontend_Dashboard extends Spa_Action)"; $lines[] = " - Spa_Layout class (e.g., Frontend_Layout extends Spa_Layout)"; $lines[] = " - Component class (e.g., Sidebar_Nav extends Component)"; $lines[] = " - Blade @rsx_id (e.g., @rsx_id('Login_Index'))"; $lines[] = ""; $lines[] = "TO FIX:"; $lines[] = " 1. Rename the wrapper class to match your action/layout/component/view"; $lines[] = " 2. Or create the missing .js or .blade.php file with this class/id"; $lines[] = ""; $lines[] = "NO EXEMPTIONS are allowed. If this file provides shared styles that"; $lines[] = "cannot be scoped to a single element, it may need to be moved to"; $lines[] = "rsx/theme/ (outside components/) - but this is rare and requires"; $lines[] = "explicit developer approval."; $lines[] = ""; $lines[] = "See: php artisan rsx:man scss"; return implode("\n", $lines); } /** * Build suggestion for filename mismatch */ private function build_filename_mismatch_suggestion( string $file, string $scss_filename, string $expected_filename, string $match_type ): string { $lines = []; $lines[] = "SCSS filename must match the associated {$match_type} file."; $lines[] = ""; $lines[] = "Current: {$scss_filename}.scss"; $lines[] = "Expected: {$expected_filename}.scss"; $lines[] = ""; $lines[] = "TO FIX:"; $lines[] = " Rename the SCSS file:"; $lines[] = " mv {$scss_filename}.scss {$expected_filename}.scss"; $lines[] = ""; $lines[] = "See: php artisan rsx:man scss"; return implode("\n", $lines); } }