Fix bin/publish: copy docs.dist from project root

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>
This commit is contained in:
root
2025-10-21 02:08:33 +00:00
commit f6fac6c4bc
79758 changed files with 10547827 additions and 0 deletions

View File

@@ -0,0 +1,141 @@
<?php
namespace App\RSpade\Core\Validation;
use RuntimeException;
use App\RSpade\CodeQuality\RuntimeChecks\ViewErrors;
use App\RSpade\Core\Manifest\Manifest;
/**
* Validates layout chains and bundle placement in embedded layouts
*
* Ensures that:
* - Only the topmost layout in a chain has a bundle
* - No circular dependencies exist in layout chains
* - Bundle placement follows framework conventions
*/
class LayoutChainValidator
{
/**
* Validate an entire layout chain starting from a view
*
* @param string $view_id The RSX ID of the starting view
* @throws RuntimeException if validation fails
*/
public static function validate_layout_chain(string $view_id): void
{
$chain = self::_build_layout_chain($view_id);
$found_bundle = false;
$layout_with_bundle = null;
// Skip validation if no layouts in chain (just a standalone view)
if (empty($chain)) {
return;
}
// Check each layout in the chain for a bundle
foreach ($chain as $index => $layout) {
// Skip if this is the view itself, not a layout
if ($layout['is_layout'] === false) {
continue;
}
$layout_path = base_path($layout['path']);
if (file_exists($layout_path)) {
$content = file_get_contents($layout_path);
// Check for bundle render call
if (strpos($content, '_Bundle::render()') !== false) {
// Only the topmost layout should have a bundle
$is_topmost = ($index === count($chain) - 1);
if (!$is_topmost) {
// This is an intermediate layout with a bundle - not allowed
ViewErrors::intermediate_layout_has_bundle(
$layout['path'],
$layout['id']
);
}
$found_bundle = true;
$layout_with_bundle = $layout['id'];
}
}
}
// Find the topmost layout (last layout in chain)
$topmost_layout = null;
for ($i = count($chain) - 1; $i >= 0; $i--) {
if ($chain[$i]['is_layout']) {
$topmost_layout = $chain[$i];
break;
}
}
// The topmost layout MUST have a bundle (unless it's mail or print)
if ($topmost_layout && !$found_bundle) {
$filename = basename($topmost_layout['path']);
// Skip validation for mail and print layouts
if (!str_contains($filename, '.mail.') && !str_contains($filename, '.print.')) {
ViewErrors::topmost_layout_missing_bundle(
$topmost_layout['path'],
$topmost_layout['id']
);
}
}
}
/**
* Build the complete layout chain from a starting view/layout
*
* @param string $start_id The RSX ID to start from
* @return array The layout chain, from child to parent
*/
private static function _build_layout_chain(string $start_id): array
{
$chain = [];
$current_id = $start_id;
$visited = []; // Prevent infinite loops
while ($current_id) {
if (in_array($current_id, $visited)) {
throw new RuntimeException(
"Circular layout dependency detected: " .
implode(' -> ', $visited) . ' -> ' . $current_id
);
}
$visited[] = $current_id;
// Try to get view metadata first
$file_path = Manifest::find_view($current_id);
if (!$file_path) {
// If not found as view, try as layout
$file_path = Manifest::find_view_by_rsx_id($current_id);
}
if (!$file_path) {
break;
}
// Get metadata to find what this extends
$metadata = Manifest::get_file($file_path);
if (!$metadata) {
break;
}
$is_layout = isset($metadata['is_layout']) && $metadata['is_layout'];
$chain[] = [
'id' => $current_id,
'path' => $file_path,
'is_layout' => $is_layout,
'extends' => $metadata['rsx_extends'] ?? null
];
// Move to the parent layout
$current_id = $metadata['rsx_extends'] ?? null;
}
return $chain;
}
}

View File

@@ -0,0 +1,127 @@
<?php
namespace App\RSpade\Core\Validation;
use App\RSpade\CodeQuality\RuntimeChecks\ViewErrors;
/**
* Validates RSX view files for proper asset organization
*
* Enforces:
* - No inline <style> or <script> tags in views (not layouts)
* - Layouts must have rsx_body_class() in body tag
*/
class ViewValidator
{
/**
* Validate a view file for inline styles and scripts
*
* @param string $id The ID of the view
* @param string $file_path The path to the view file
* @param array $metadata The manifest metadata for the view
* @throws \RuntimeException if validation fails
*/
public static function validate_view(string $id, string $file_path, array $metadata): void
{
// Skip validation for layouts (they can have inline styles)
if (isset($metadata['is_layout']) && $metadata['is_layout']) {
return;
}
// Read the file content (file_path should be absolute)
if (!str_starts_with($file_path, '/')) {
$file_path = base_path($file_path);
}
$content = file_get_contents($file_path);
// Remove PHP blocks, blade comments, and HTML comments to avoid false positives
$cleaned_content = $content;
$cleaned_content = preg_replace('/@php.*?@endphp/s', '', $cleaned_content);
$cleaned_content = preg_replace('/{{--.*?--}}/s', '', $cleaned_content);
$cleaned_content = preg_replace('/<!--.*?-->/s', '', $cleaned_content);
// Check for inline styles or scripts
if (stripos($cleaned_content, '<style') !== false || stripos($cleaned_content, '<script') !== false) {
// Views cannot have inline assets
ViewErrors::inline_assets_not_allowed(
$file_path,
$id,
stripos($cleaned_content, '<style') !== false,
stripos($cleaned_content, '<script') !== false
);
}
}
/**
* Validate a layout file has the required body class function
*
* @param string $layout_path The path to the layout file
* @throws \RuntimeException if validation fails
*/
public static function validate_layout(string $layout_path): void
{
// Make sure path is absolute
if (!str_starts_with($layout_path, '/')) {
$layout_path = base_path($layout_path);
}
// Read the layout file
$content = file_get_contents($layout_path);
$filename = basename($layout_path);
// Check if rsx_body_class() is present near a <body tag
if (preg_match('/<body[^>]*>/i', $content, $matches)) {
$body_tag = $matches[0];
// Check if the body tag or nearby content has rsx_body_class()
$context_start = max(0, strpos($content, $body_tag) - 100);
$context_end = min(strlen($content), strpos($content, $body_tag) + strlen($body_tag) + 100);
$context = substr($content, $context_start, $context_end - $context_start);
if (strpos($context, 'rsx_body_class()') === false) {
// Layout must have body class function
ViewErrors::layout_missing_body_class($layout_path);
}
}
// Skip validation for print and mail layouts
if (str_contains($filename, '.print.') || str_contains($filename, '.mail.')) {
return;
}
// Check layout structure - must have bundle OR @rsx_extends
$has_bundle = strpos($content, '_Bundle::render()') !== false;
$has_rsx_extends = strpos($content, '@rsx_extends') !== false;
$has_html_close = strpos($content, '</html>') !== false;
// Determine error condition
if (!$has_bundle && !$has_rsx_extends) {
// No bundle, no extends - this is incomplete
if (!$has_html_close) {
// Partial layout that's incomplete
ViewErrors::layout_incomplete($layout_path);
} else {
// Full HTML doc missing bundle
$path_parts = explode('/', $layout_path);
$module_name = '';
for ($i = count($path_parts) - 2; $i >= 0; $i--) {
if ($path_parts[$i] === 'app' && isset($path_parts[$i + 1])) {
$module_name = $path_parts[$i + 1];
break;
}
}
ViewErrors::layout_missing_bundle($layout_path, $module_name);
}
} elseif ($has_bundle && $has_rsx_extends) {
// Has both bundle and extends - not allowed
if (!$has_html_close) {
// Embedded layout trying to render bundle
ViewErrors::embedded_layout_has_bundle($layout_path);
} else {
// Topmost layout using rsx_extends
ViewErrors::topmost_layout_has_extends($layout_path);
}
}
// If has only bundle or only extends, that's valid
}
}