Fix code quality violations and rename select input components

Move small tasks from wishlist to todo, update npm packages
Replace #[Auth] attributes with manual auth checks and code quality rule
Remove on_jqhtml_ready lifecycle method from framework
Complete ACL system with 100-based role indexing and /dev/acl tester
WIP: ACL system implementation with debug instrumentation
Convert rsx:check JS linting to RPC socket server
Clean up docs and fix $id→$sid in man pages, remove SSR/FPC feature
Reorganize wishlists: priority order, mark sublayouts complete, add email
Update model_fetch docs: mark MVP complete, fix enum docs, reorganize
Comprehensive documentation overhaul: clarity, compression, and critical rules
Convert Contacts/Projects CRUD to Model.fetch() and add fetch_or_null()
Add JS ORM relationship lazy-loading and fetch array handling
Add JS ORM relationship fetching and CRUD documentation
Fix ORM hydration and add IDE resolution for Base_* model stubs
Rename Json_Tree_Component to JS_Tree_Debug_Component and move to framework
Enhance JS ORM infrastructure and add Json_Tree class name badges

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-11-23 21:39:43 +00:00
parent 78553d4edf
commit 84ca3dfe42
167 changed files with 7538 additions and 49164 deletions

View File

@@ -7,6 +7,8 @@ use RecursiveCallbackFilterIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RuntimeException;
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;
@@ -91,6 +93,12 @@ class BundleCompiler
*/
protected array $resolved_includes = [];
/**
* The root module bundle class being compiled
* Used for validation error messages
*/
protected string $root_bundle_class = '';
/**
* Compiled jqhtml files (separated during ordering for special placement)
*/
@@ -129,6 +137,7 @@ class BundleCompiler
// Step 2: Mark the bundle we're compiling as already resolved
$this->resolved_includes[$bundle_class] = true;
$this->root_bundle_class = $bundle_class;
// Step 3: Process required bundles first
$this->_process_required_bundles();
@@ -467,18 +476,73 @@ class BundleCompiler
$this->_process_include_item($bundle_aliases[$alias]);
}
}
// Include custom JS model base class if configured
// This allows users to define application-wide model functionality
$js_model_base_class = config('rsx.js_model_base_class');
if ($js_model_base_class) {
$this->_include_js_model_base_class($js_model_base_class);
}
}
/**
* Include the custom JS model base class file in the bundle
*
* Finds the JS file by class name in the manifest and adds it to the bundle.
* Validates that the class extends Rsx_Js_Model.
*/
protected function _include_js_model_base_class(string $class_name): void
{
// Find the JS file in the manifest by class name
try {
$file_path = Manifest::js_find_class($class_name);
} catch (\RuntimeException $e) {
throw new \RuntimeException(
"JavaScript model base class '{$class_name}' configured in rsx.js_model_base_class not found in manifest.\n" .
"Ensure the class is defined in a .js file within your application (e.g., rsx/lib/{$class_name}.js)"
);
}
// Get metadata to verify it extends Rsx_Js_Model
$metadata = Manifest::get_file($file_path);
$extends = $metadata['extends'] ?? null;
if ($extends !== 'Rsx_Js_Model') {
throw new \RuntimeException(
"JavaScript model base class '{$class_name}' must extend Rsx_Js_Model.\n" .
"Found: extends {$extends}\n" .
"File: {$file_path}"
);
}
// Add the file to the bundle by processing it as a path
$this->_process_include_item($file_path);
}
/**
* Resolve bundle and all its includes
*
* @param string $bundle_class The bundle class to resolve
* @param bool $discovered_via_scan Whether this bundle was discovered via directory scan
*/
protected function _resolve_bundle(string $bundle_class): void
protected function _resolve_bundle(string $bundle_class, bool $discovered_via_scan = false): void
{
// Get bundle definition
if (!method_exists($bundle_class, 'define')) {
throw new Exception("Bundle {$bundle_class} missing define() method");
}
// Validate module bundle doesn't include another module bundle
if (Manifest::php_is_subclass_of($bundle_class, 'Rsx_Module_Bundle_Abstract') &&
$bundle_class !== $this->root_bundle_class) {
Rsx_Module_Bundle_Abstract::validate_include($bundle_class, $this->root_bundle_class);
}
// Validate asset bundles discovered via scan don't have directory paths
if ($discovered_via_scan && Manifest::php_is_subclass_of($bundle_class, 'Rsx_Asset_Bundle_Abstract')) {
Rsx_Asset_Bundle_Abstract::validate_no_directory_scanning($bundle_class, $this->root_bundle_class);
}
$definition = $bundle_class::define();
// Process bundle includes
@@ -732,6 +796,8 @@ class BundleCompiler
/**
* Add all files from a directory
*
* Also auto-discovers Asset Bundles in the directory and processes them.
*/
protected function _add_directory(string $path): void
{
@@ -742,6 +808,9 @@ class BundleCompiler
// Get excluded directories from config
$excluded_dirs = config('rsx.manifest.excluded_dirs', ['vendor', 'node_modules', 'storage', '.git', 'public', 'resource']);
// Track discovered asset bundles to process after file collection
$discovered_bundles = [];
// Create a recursive directory iterator with filtering
$directory = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS);
$filter = new RecursiveCallbackFilterIterator($directory, function ($current, $key, $iterator) use ($excluded_dirs) {
@@ -763,7 +832,42 @@ class BundleCompiler
foreach ($iterator as $file) {
if ($file->isFile()) {
$this->_add_file($file->getPathname());
$filepath = $file->getPathname();
$extension = strtolower(pathinfo($filepath, PATHINFO_EXTENSION));
// For PHP files, check if it's an asset bundle via manifest
if ($extension === 'php') {
$relative_path = str_replace(base_path() . '/', '', $filepath);
// Get file metadata from manifest to check if it's an asset bundle
try {
$file_meta = Manifest::get_file($relative_path);
$class_name = $file_meta['class'] ?? null;
// Use manifest to check if this PHP class is an asset bundle
if ($class_name && Manifest::php_is_subclass_of($class_name, 'Rsx_Asset_Bundle_Abstract')) {
$fqcn = $file_meta['fqcn'] ?? null;
if ($fqcn && !isset($this->resolved_includes[$fqcn])) {
$discovered_bundles[] = $fqcn;
console_debug('BUNDLE', "Auto-discovered asset bundle: {$fqcn}");
}
// Don't add bundle file itself to file list - we'll process it as a bundle
continue;
}
} catch (RuntimeException $e) {
// File not in manifest, just add it normally
}
}
$this->_add_file($filepath);
}
}
// Process discovered asset bundles (marked as discovered via scan)
foreach ($discovered_bundles as $bundle_fqcn) {
if (!isset($this->resolved_includes[$bundle_fqcn])) {
$this->resolved_includes[$bundle_fqcn] = true;
$this->_resolve_bundle($bundle_fqcn, true); // true = discovered via scan
}
}
}
@@ -1059,6 +1163,154 @@ class BundleCompiler
return array_unique($stubs);
}
/**
* Generate concrete model classes for PHP models in the bundle
*
* For each PHP model (subclass of Rsx_Model_Abstract) in the bundle:
* 1. Check if a user-defined JS class with the same name exists
* 2. If user-defined class exists:
* - Validate it extends Base_{ModelName} directly
* - If it exists in manifest but not in bundle, throw error
* 3. If no user-defined class exists:
* - Auto-generate: class ModelName extends Base_ModelName {}
*
* @param array $current_js_files JS files already in the bundle (to check for user classes)
* @return string|null Path to temp file containing generated classes, or null if none needed
*/
protected function _generate_concrete_model_classes(array $current_js_files): ?string
{
$manifest = Manifest::get_full_manifest();
$manifest_files = $manifest['data']['files'] ?? [];
// Get all files from all bundles to find PHP models
$all_bundle_files = [];
foreach ($this->bundle_files as $type => $files) {
if (is_array($files)) {
$all_bundle_files = array_merge($all_bundle_files, $files);
}
}
// Build a set of JS class names currently in the bundle for quick lookup
$js_classes_in_bundle = [];
foreach ($current_js_files as $js_file) {
$relative = str_replace(base_path() . '/', '', $js_file);
if (isset($manifest_files[$relative]['class'])) {
$js_classes_in_bundle[$manifest_files[$relative]['class']] = $relative;
}
}
// Find all PHP models in the bundle
$models_in_bundle = [];
foreach ($all_bundle_files as $file) {
$relative = str_replace(base_path() . '/', '', $file);
// Check if this is a PHP file with a class
if (!isset($manifest_files[$relative]['class'])) {
continue;
}
$class_name = $manifest_files[$relative]['class'];
// Check if this class is a subclass of Rsx_Model_Abstract (but not system models)
if (!Manifest::php_is_subclass_of($class_name, 'Rsx_Model_Abstract')) {
continue;
}
// Skip abstract model classes - only concrete models get JS stubs
if (Manifest::php_is_abstract($class_name)) {
continue;
}
$models_in_bundle[$class_name] = $relative;
}
if (empty($models_in_bundle)) {
return null;
}
console_debug('BUNDLE', 'Found ' . count($models_in_bundle) . ' PHP models in bundle: ' . implode(', ', array_keys($models_in_bundle)));
// Process each model
$generated_classes = [];
$base_class_name = config('rsx.js_model_base_class');
foreach ($models_in_bundle as $model_name => $model_path) {
$expected_base_class = 'Base_' . $model_name;
// Check if user has defined a JS class with this model name
$user_js_class_path = null;
foreach ($manifest_files as $file_path => $meta) {
if (isset($meta['class']) && $meta['class'] === $model_name && isset($meta['extension']) && $meta['extension'] === 'js') {
// Make sure it's not a generated stub
if (!isset($meta['is_model_stub']) && !isset($meta['is_stub'])) {
$user_js_class_path = $file_path;
break;
}
}
}
if ($user_js_class_path) {
// User has defined a JS class for this model - validate it
console_debug('BUNDLE', "Found user-defined JS class for {$model_name} at {$user_js_class_path}");
// Check if it's in the bundle
if (!isset($js_classes_in_bundle[$model_name])) {
throw new RuntimeException(
"PHP model '{$model_name}' is included in bundle (at {$model_path}) " .
"but its custom JavaScript implementation exists at '{$user_js_class_path}' " .
"and is NOT included in the bundle.\n\n" .
"Either:\n" .
"1. Add the JS file's directory to the bundle's include paths, or\n" .
"2. Remove the custom JS implementation to use auto-generated class"
);
}
// Validate it extends the Base_ class directly
$user_meta = $manifest_files[$user_js_class_path] ?? [];
$user_extends = $user_meta['extends'] ?? null;
if ($user_extends !== $expected_base_class) {
throw new RuntimeException(
"JavaScript model class '{$model_name}' at '{$user_js_class_path}' " .
"must extend '{$expected_base_class}' directly.\n" .
"Found: extends " . ($user_extends ?: '(nothing)') . "\n\n" .
"Correct usage:\n" .
"class {$model_name} extends {$expected_base_class} {\n" .
" // Your custom model methods\n" .
"}"
);
}
console_debug('BUNDLE', "Validated {$model_name} extends {$expected_base_class}");
} else {
// No user-defined class - auto-generate one
console_debug('BUNDLE', "Auto-generating concrete class for {$model_name}");
$generated_classes[] = "class {$model_name} extends {$expected_base_class} {}";
}
}
if (empty($generated_classes)) {
return null;
}
// Write all generated classes to a single temp file using standard temp file pattern
$content = "/**\n";
$content .= " * Auto-generated concrete model classes\n";
$content .= " * These classes extend the Base_* stubs to provide usable model classes\n";
$content .= " * when no custom implementation is defined by the developer.\n";
$content .= " */\n\n";
$content .= implode("\n\n", $generated_classes) . "\n";
// Use content hash for idempotent file naming, with recognizable prefix for detection
$hash = substr(md5($content), 0, 8);
$temp_file = storage_path('rsx-tmp/bundle_generated_models_' . $this->bundle_name . '_' . $hash . '.js');
file_put_contents($temp_file, $content);
console_debug('BUNDLE', 'Generated ' . count($generated_classes) . ' concrete model classes');
return $temp_file;
}
/**
* Order JavaScript files by class dependency
*
@@ -1109,6 +1361,29 @@ class BundleCompiler
continue;
}
// Check if this is a JS stub file (not in manifest, needs parsing)
// Stub files are in storage/rsx-build/js-stubs/ or storage/rsx-build/js-model-stubs/
if (str_contains($file, 'storage/rsx-build/js-stubs/') || str_contains($file, 'storage/rsx-build/js-model-stubs/')) {
// Use simple regex extraction - stub files have known format and can't use
// the strict JS parser (stubs may have code after class declaration)
$stub_content = file_get_contents($file);
$stub_metadata = $this->_extract_stub_class_info($stub_content);
if (!empty($stub_metadata['class'])) {
$class_files[] = $file;
$class_info[$file] = [
'class' => $stub_metadata['class'],
'extends' => $stub_metadata['extends'],
'decorators' => [],
'method_decorators' => [],
];
console_debug('BUNDLE_SORT', "Parsed stub file: {$stub_metadata['class']} extends " . ($stub_metadata['extends'] ?? 'nothing'));
} else {
$non_class_files[] = $file;
}
continue;
}
// Get file info from manifest
$relative = str_replace(base_path() . '/', '', $file);
$file_data = $manifest_files[$relative] ?? null;
@@ -1211,6 +1486,35 @@ class BundleCompiler
return $decorators;
}
/**
* Extract class name and extends from JS stub file content
*
* Uses simple regex extraction since stub files have a known format and may
* have code after the class declaration that the strict JS parser rejects.
*
* @param string $content The stub file content
* @return array ['class' => string|null, 'extends' => string|null]
*/
protected function _extract_stub_class_info(string $content): array
{
// Remove single-line comments
$content = preg_replace('#//.*$#m', '', $content);
// Remove multi-line comments (including JSDoc)
$content = preg_replace('#/\*.*?\*/#s', '', $content);
// Match: class ClassName or class ClassName extends ParentClass
// The first match wins - we only care about the class declaration
if (preg_match('/\bclass\s+([A-Za-z_][A-Za-z0-9_]*)(?:\s+extends\s+([A-Za-z_][A-Za-z0-9_]*))?/', $content, $matches)) {
return [
'class' => $matches[1],
'extends' => $matches[2] ?? null,
];
}
return ['class' => null, 'extends' => null];
}
/**
* Topological sort for class dependencies with decorator support
*
@@ -1419,7 +1723,13 @@ implode("\n", array_map(fn ($f) => ' - ' . str_replace(base_path() . '/', '',
* Here we:
* 1. Filter to only .js and .css files
* 2. Order JS files by class dependency
* 3. Add framework code (stubs, manifest, runner)
* 3. Add framework code:
* a. JS stubs (Base_* model classes, controller stubs, etc.)
* b. Compiled jqhtml templates
* c. Concrete model classes (auto-generated or validated user-defined)
* d. Manifest definitions (registers all JS classes)
* e. Route definitions
* f. Initialization runner (LAST - starts the application)
* 4. Generate final compiled output
*/
protected function _compile_outputs(array $types_to_compile = []): array
@@ -1456,7 +1766,16 @@ implode("\n", array_map(fn ($f) => ' - ' . str_replace(base_path() . '/', '',
// Other extensions are ignored - they should have been processed into JS/CSS
}
// Order JavaScript files by class dependency BEFORE adding framework code
// Add JS stubs to app bundle only (they depend on Rsx_Js_Model which is in app)
// Add them BEFORE dependency ordering so they're properly sorted
if ($type === 'app') {
$stub_files = $this->_get_js_stubs();
foreach ($stub_files as $stub) {
$files['js'][] = $stub;
}
}
// Order JavaScript files by class dependency BEFORE adding other framework code
if (!empty($files['js'])) {
$files['js'] = $this->_order_javascript_files_by_dependency($files['js']);
}
@@ -1482,7 +1801,8 @@ implode("\n", array_map(fn ($f) => ' - ' . str_replace(base_path() . '/', '',
}
}
// Add JS stubs and framework code to app JS
// Add framework code to app JS
// Note: JS stubs are already added before dependency ordering above
if ($type === 'app') {
// Add NPM import declarations at the very beginning
if (!empty($this->npm_includes)) {
@@ -1492,19 +1812,19 @@ implode("\n", array_map(fn ($f) => ' - ' . str_replace(base_path() . '/', '',
}
}
// ALWAYS get JS stubs for ALL included files that have them
// ANY file type can have a js_stub - controllers, models, custom types, etc.
$stub_files = $this->_get_js_stubs();
foreach ($stub_files as $stub) {
$files['js'][] = $stub;
}
// Add compiled jqhtml files AFTER JS stubs
// Add compiled jqhtml files
// These are JavaScript files generated from .jqhtml templates
foreach ($this->jqhtml_compiled_files as $jqhtml_file) {
$files['js'][] = $jqhtml_file;
}
// Generate concrete model classes for PHP models in the bundle
// This validates user-defined JS model classes and auto-generates missing ones
$concrete_models_file = $this->_generate_concrete_model_classes($files['js']);
if ($concrete_models_file) {
$files['js'][] = $concrete_models_file;
}
// Generate manifest definitions for all JS classes
$manifest_file = $this->_create_javascript_manifest($files['js'] ?? []);
if ($manifest_file) {
@@ -2020,8 +2340,38 @@ implode("\n", array_map(fn ($f) => ' - ' . str_replace(base_path() . '/', '',
// Analyze each JavaScript file for class information
foreach ($js_files as $file) {
// Skip temp files
// Skip most temp files, but handle auto-generated model classes
if (str_contains($file, 'storage/rsx-tmp/')) {
// Check if this is the auto-generated model classes file
if (str_contains($file, 'bundle_generated_models_')) {
// Parse simple class declarations: class Foo extends Bar {}
$content = file_get_contents($file);
if (preg_match_all('/class\s+([A-Za-z_][A-Za-z0-9_]*)\s+extends\s+([A-Za-z_][A-Za-z0-9_]*)/', $content, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
$class_definitions[$match[1]] = [
'name' => $match[1],
'extends' => $match[2],
'decorators' => null,
];
}
}
}
continue;
}
// Check if this is a JS stub file (not in PHP manifest, needs direct parsing)
// Stub files are in storage/rsx-build/js-stubs/ or storage/rsx-build/js-model-stubs/
if (str_contains($file, 'storage/rsx-build/js-stubs/') || str_contains($file, 'storage/rsx-build/js-model-stubs/')) {
$stub_content = file_get_contents($file);
$stub_metadata = $this->_extract_stub_class_info($stub_content);
if (!empty($stub_metadata['class'])) {
$class_definitions[$stub_metadata['class']] = [
'name' => $stub_metadata['class'],
'extends' => $stub_metadata['extends'],
'decorators' => null, // Stubs don't have method decorators
];
}
continue;
}