Add Spa.load_detached_action, decorator identifier rule, npm updates

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-12-10 20:50:57 +00:00
parent d047b49d39
commit 2f2cf41139
26 changed files with 492 additions and 76 deletions

View File

@@ -0,0 +1,210 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
/**
* DecoratorIdentifierParamRule - Prohibits identifier (class name) parameters in decorators
*
* JavaScript decorators cannot reliably use class name references as parameters because
* the framework cannot guarantee class definition order in the compiled bundle output.
* Class ordering is based on extends hierarchies and other factors, but decorator
* parameter dependencies are not tracked.
*
* Use string literals instead of class references in decorator parameters.
*/
class DecoratorIdentifierParam_CodeQualityRule extends CodeQualityRule_Abstract
{
/**
* Get the unique rule identifier
*/
public function get_id(): string
{
return 'JS-DECORATOR-IDENT-01';
}
/**
* Get human-readable rule name
*/
public function get_name(): string
{
return 'Decorator Identifier Parameter';
}
/**
* Get rule description
*/
public function get_description(): string
{
return 'Prohibits identifier (class name) references as decorator parameters - use string literals instead';
}
/**
* Get file patterns this rule applies to
*/
public function get_file_patterns(): array
{
return ['*.js'];
}
/**
* This rule runs during manifest scan to catch issues early
*/
public function is_called_during_manifest_scan(): bool
{
return true;
}
/**
* Check the manifest for decorator identifier parameter violations
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
static $already_checked = false;
// Only check once per manifest build
if ($already_checked) {
return;
}
$already_checked = true;
// Get all manifest files
$files = \App\RSpade\Core\Manifest\Manifest::get_all();
if (empty($files)) {
return;
}
// Check all JavaScript files with decorators
foreach ($files as $file => $file_metadata) {
// Skip if not a JavaScript file
if (($file_metadata['extension'] ?? '') !== 'js') {
continue;
}
// Skip if no class (non-class JS files)
if (!isset($file_metadata['class'])) {
continue;
}
// Check class-level decorators
if (!empty($file_metadata['decorators'])) {
$this->check_decorators($file, $file_metadata['class'], $file_metadata['decorators'], 'class');
}
// Check method-level decorators
// Method decorators are stored under each method in public_static_methods and public_instance_methods
$all_methods = array_merge(
$file_metadata['public_static_methods'] ?? [],
$file_metadata['public_instance_methods'] ?? []
);
foreach ($all_methods as $method_name => $method_info) {
if (!empty($method_info['decorators'])) {
$this->check_decorators(
$file,
$file_metadata['class'],
$method_info['decorators'],
'method',
$method_name,
$method_info['line'] ?? 1
);
}
}
}
}
/**
* Check a set of decorators for identifier parameters
*
* @param string $file File path
* @param string $class_name Class name for context
* @param array $decorators Decorators in compact format: [[name, [args]], ...]
* @param string $context 'class' or 'method'
* @param string|null $method_name Method name if context is 'method'
* @param int $line Line number
*/
private function check_decorators(
string $file,
string $class_name,
array $decorators,
string $context,
?string $method_name = null,
int $line = 1
): void {
foreach ($decorators as $decorator) {
$decorator_name = $decorator[0] ?? 'unknown';
$decorator_args = $decorator[1] ?? [];
// Check each argument for identifier usage
$this->check_args_for_identifiers(
$file,
$class_name,
$decorator_name,
$decorator_args,
$context,
$method_name,
$line
);
}
}
/**
* Recursively check decorator arguments for identifier values
*/
private function check_args_for_identifiers(
string $file,
string $class_name,
string $decorator_name,
array $args,
string $context,
?string $method_name,
int $line,
string $path = ''
): void {
foreach ($args as $index => $arg) {
$current_path = $path ? "{$path}[{$index}]" : "argument {$index}";
// Check if this argument is an identifier
if (is_array($arg) && isset($arg['identifier'])) {
$identifier = $arg['identifier'];
// Build context description
if ($context === 'class') {
$location = "class '{$class_name}'";
} else {
$location = "method '{$class_name}::{$method_name}()'";
}
$this->add_violation(
$file,
$line,
"Decorator @{$decorator_name} on {$location} uses identifier '{$identifier}' as a parameter. " .
"Class name references are not allowed in decorator parameters because the framework cannot " .
"guarantee the referenced class will be defined before this decorator is evaluated.",
"@{$decorator_name}(...{$identifier}...)",
"Use a string literal instead of the class reference:\n" .
" - Change: @{$decorator_name}({$identifier})\n" .
" - To: @{$decorator_name}('{$identifier}')\n\n" .
"If you need the actual class at runtime, resolve it from the string:\n" .
" const cls = Manifest.get_class_by_name('{$identifier}');",
'critical'
);
}
// Recursively check nested arrays
if (is_array($arg) && !isset($arg['identifier'])) {
$this->check_args_for_identifiers(
$file,
$class_name,
$decorator_name,
$arg,
$context,
$method_name,
$line,
$current_path
);
}
}
}
}

View File

@@ -941,4 +941,75 @@ class Spa {
static spa_unknown_route_fatal(path) {
console.error(`Unknown route for path ${path} - this shouldn't happen`);
}
/**
* Load an action in detached mode without affecting the live SPA state
*
* This method resolves a URL to an action, instantiates it on a detached DOM element
* (not in the actual document), runs its full lifecycle including on_load(), and
* returns the fully-initialized component instance.
*
* Use cases:
* - Getting action metadata (title, breadcrumbs) for navigation UI
* - Pre-fetching action data before navigation
* - Inspecting action state without displaying it
*
* IMPORTANT: The caller is responsible for calling action.stop() when done
* to prevent memory leaks. The detached action holds references and may have
* event listeners that need cleanup.
*
* @param {string} url - The URL to resolve and load
* @param {object} extra_args - Optional extra parameters to pass to the action component.
* These are merged with URL-extracted args (extra_args take precedence).
* Pass {use_cached_data: true} to have the action load with cached data
* without revalidation if cached data is available.
* @returns {Promise<Spa_Action|null>} The fully-loaded action instance, or null if route not found
*
* @example
* // Basic usage
* const action = await Spa.load_detached_action('/contacts/123');
* if (action) {
* const title = action.get_title?.() ?? action.constructor.name;
* console.log('Page title:', title);
* action.stop(); // Clean up when done
* }
*
* @example
* // With cached data (faster, no network request if cached)
* const action = await Spa.load_detached_action('/contacts/123', {use_cached_data: true});
*/
static async load_detached_action(url, extra_args = {}) {
// Parse URL and match to route
const parsed = Spa.parse_url(url);
const url_without_hash = parsed.path + parsed.search;
const route_match = Spa.match_url_to_route(url_without_hash);
if (!route_match) {
console_debug('Spa', 'load_detached_action: No route match for ' + url);
return null;
}
const action_class = route_match.action_class;
const action_name = action_class.name;
// Merge URL args with extra_args (extra_args take precedence)
const args = { ...route_match.args, ...extra_args };
console_debug('Spa', `load_detached_action: Loading ${action_name} with args:`, args);
// Create a detached container (not in DOM)
const $detached = $('<div>');
// Instantiate the action on the detached element
// This triggers the full component lifecycle: on_create -> render -> on_render -> on_load -> on_ready
$detached.component(action_name, args);
const action = $detached.component();
// Wait for the action to be fully ready (including on_load completion)
await action.ready();
console_debug('Spa', `load_detached_action: ${action_name} ready`);
return action;
}
}