Implement JQHTML function cache ID system and fix bundle compilation Implement underscore prefix for system tables Fix JS syntax linter to support decorators and grant exception to Task system SPA: Update planning docs and wishlists with remaining features SPA: Document Navigation API abandonment and future enhancements Implement SPA browser integration with History API (Phase 1) Convert contacts view page to SPA action Convert clients pages to SPA actions and document conversion procedure SPA: Merge GET parameters and update documentation Implement SPA route URL generation in JavaScript and PHP Implement SPA bootstrap controller architecture Add SPA routing manual page (rsx:man spa) Add SPA routing documentation to CLAUDE.md Phase 4 Complete: Client-side SPA routing implementation Update get_routes() consumers for unified route structure Complete SPA Phase 3: PHP-side route type detection and is_spa flag Restore unified routes structure and Manifest_Query class Refactor route indexing and add SPA infrastructure Phase 3 Complete: SPA route registration in manifest Implement SPA Phase 2: Extract router code and test decorators Rename Jqhtml_Component to Component and complete SPA foundation setup 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
488 lines
21 KiB
PHP
Executable File
488 lines
21 KiB
PHP
Executable File
<?php
|
|
|
|
namespace App\RSpade\Ide\Helper;
|
|
|
|
use App\RSpade\Core\Manifest\Manifest;
|
|
use Exception;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use RuntimeException;
|
|
|
|
// Standalone controller for IDE integration - doesn't extend any base class
|
|
class Ide_Helper_Controller
|
|
{
|
|
/**
|
|
* Get git status for VS Code integration
|
|
*/
|
|
public function git_status(Request $request): JsonResponse
|
|
{
|
|
if (app()->environment('production')) {
|
|
return response()->json(['error' => 'Not available in production'], 403);
|
|
}
|
|
|
|
$base_path = base_path();
|
|
$output = [];
|
|
$return_code = 0;
|
|
|
|
\exec_safe("cd {$base_path} && git status --porcelain 2>&1", $output, $return_code);
|
|
|
|
if ($return_code !== 0) {
|
|
return response()->json(['error' => 'Git command failed', 'output' => implode("\n", $output)], 500);
|
|
}
|
|
|
|
$modified = [];
|
|
$added = [];
|
|
$conflicts = [];
|
|
|
|
foreach ($output as $line) {
|
|
if (strlen($line) < 4) continue;
|
|
|
|
$status = substr($line, 0, 2);
|
|
$file = trim(substr($line, 3));
|
|
|
|
// Parse git status codes
|
|
if (str_contains($status, 'U') || str_contains($status, 'A') && str_contains($status, 'A')) {
|
|
$conflicts[] = $file;
|
|
} elseif ($status[0] === 'A' || $status[1] === 'A' || $status === '??') {
|
|
$added[] = $file;
|
|
} elseif ($status[0] === 'M' || $status[1] === 'M' || $status[0] === 'R') {
|
|
$modified[] = $file;
|
|
}
|
|
}
|
|
|
|
return response()->json([
|
|
'modified' => $modified,
|
|
'added' => $added,
|
|
'conflicts' => $conflicts,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Resolve various RSX identifiers to their file locations
|
|
* This endpoint is only available in development mode for IDE integration
|
|
*
|
|
* Priority order for resolution:
|
|
* 1. PHP class lookup (for Ajax_Endpoint routes and controllers)
|
|
* 2. RSX blade view lookup (for @rsx_extends, @rsx_include, rsx_view(), etc.)
|
|
* 3. Future: JavaScript class lookup
|
|
* 4. Future: Bundle/Module lookup
|
|
*/
|
|
public function resolve_class(Request $request): JsonResponse
|
|
{
|
|
// Double-check we're not in production
|
|
if (app()->environment('production')) {
|
|
return response()->json([
|
|
'error' => 'This endpoint is not available in production',
|
|
], 403);
|
|
}
|
|
|
|
$identifier = $request->query('class') ?? $request->query('identifier');
|
|
$method_name = $request->query('method');
|
|
$type = $request->query('type'); // Optional type hint: 'class', 'view', etc.
|
|
|
|
if (!$identifier) {
|
|
return response()->json([
|
|
'error' => 'Missing required parameter: class or identifier',
|
|
'found' => false,
|
|
], 400);
|
|
}
|
|
|
|
// Priority 1: Try as PHP class (if looks like a class name or type hint suggests it)
|
|
if (!$type || $type === 'class' || preg_match('/^[A-Z][A-Za-z0-9_]*$/', $identifier)) {
|
|
try {
|
|
$file_path = Manifest::php_find_class($identifier);
|
|
$metadata = Manifest::php_get_metadata_by_class($identifier);
|
|
|
|
// Try to find the line number where the class or method is defined
|
|
$line_number = 1;
|
|
$absolute_path = base_path($file_path);
|
|
|
|
if (file_exists($absolute_path)) {
|
|
$content = file_get_contents($absolute_path);
|
|
$lines = explode("\n", $content);
|
|
|
|
if ($method_name) {
|
|
// Check if method metadata exists and has a different file (trait method)
|
|
$method_file_path = null;
|
|
if (isset($metadata['public_static_methods'][$method_name])) {
|
|
$method_metadata = $metadata['public_static_methods'][$method_name];
|
|
if (isset($method_metadata['file'])) {
|
|
// Method is from a trait - use trait file instead
|
|
$method_file_path = $method_metadata['file'];
|
|
$file_path = $method_file_path;
|
|
$absolute_path = base_path($file_path);
|
|
$content = file_exists($absolute_path) ? file_get_contents($absolute_path) : '';
|
|
$lines = explode("\n", $content);
|
|
}
|
|
// Use line number from metadata if available
|
|
if (isset($method_metadata['line'])) {
|
|
$line_number = $method_metadata['line'];
|
|
}
|
|
}
|
|
|
|
// If we don't have metadata or line number, search manually
|
|
if ($line_number === 1 && !empty($lines)) {
|
|
// Look for the specific method
|
|
$in_class_or_trait = false;
|
|
$brace_count = 0;
|
|
|
|
foreach ($lines as $index => $line) {
|
|
// Check if we're entering the class or trait
|
|
if (preg_match('/^\s*(class|trait)\s+\w+(\s|$)/', $line)) {
|
|
$in_class_or_trait = true;
|
|
}
|
|
|
|
// Track braces to know when we've left the class/trait
|
|
if ($in_class_or_trait) {
|
|
$brace_count += substr_count($line, '{');
|
|
$brace_count -= substr_count($line, '}');
|
|
|
|
// Look for the method definition (public/protected/private static function method_name)
|
|
if (preg_match('/^\s*(public|protected|private)?\s*(static\s+)?function\s+' . preg_quote($method_name, '/') . '\s*\(/', $line)) {
|
|
$line_number = $index + 1;
|
|
break;
|
|
}
|
|
|
|
// If we've left the class/trait, stop looking
|
|
if ($brace_count === 0 && strpos($line, '}') !== false) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Find the line with the class definition
|
|
foreach ($lines as $index => $line) {
|
|
if (preg_match('/^\s*class\s+' . preg_quote($identifier, '/') . '(\s|$)/', $line)) {
|
|
$line_number = $index + 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$response_data = [
|
|
'found' => true,
|
|
'type' => 'class',
|
|
'file' => $file_path,
|
|
'line' => $line_number,
|
|
'metadata' => [
|
|
'namespace' => $metadata['namespace'] ?? null,
|
|
'extends' => $metadata['extends'] ?? null,
|
|
'fqcn' => $metadata['fqcn'] ?? null,
|
|
],
|
|
];
|
|
|
|
if ($method_name) {
|
|
$response_data['method'] = $method_name;
|
|
}
|
|
|
|
return response()->json($response_data);
|
|
} catch (RuntimeException $e) {
|
|
// Not a PHP class, continue to next priority
|
|
}
|
|
}
|
|
|
|
// Priority 2: Try as RSX blade view (if type hint suggests it or doesn't look like a class)
|
|
if (!$type || $type === 'view' || !preg_match('/Controller$/', $identifier)) {
|
|
try {
|
|
$file_path = Manifest::find_view($identifier);
|
|
|
|
// Find the line with @rsx_id definition
|
|
$line_number = 1;
|
|
$absolute_path = base_path($file_path);
|
|
|
|
if (file_exists($absolute_path)) {
|
|
$content = file_get_contents($absolute_path);
|
|
$lines = explode("\n", $content);
|
|
|
|
// Look for @rsx_id('identifier') or @rsx_id("identifier")
|
|
foreach ($lines as $index => $line) {
|
|
if (preg_match('/@rsx_id\s*\(\s*[\'"]' . preg_quote($identifier, '/') . '[\'"]\s*\)/', $line)) {
|
|
$line_number = $index + 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return response()->json([
|
|
'found' => true,
|
|
'type' => 'view',
|
|
'file' => $file_path,
|
|
'line' => $line_number,
|
|
'identifier' => $identifier,
|
|
]);
|
|
} catch (RuntimeException $e) {
|
|
// Not a view, continue to next priority
|
|
}
|
|
}
|
|
|
|
// Priority 3: Try as bundle alias (if type hint suggests it or single lowercase word)
|
|
if (!$type || $type === 'bundle_alias' || preg_match('/^[a-z0-9]+$/', $identifier)) {
|
|
try {
|
|
// Load the rsx config to check bundle_aliases
|
|
$config_path = base_path('config/rsx.php');
|
|
if (file_exists($config_path)) {
|
|
$config = include $config_path;
|
|
if (isset($config['bundle_aliases'][$identifier])) {
|
|
$bundle_class = $config['bundle_aliases'][$identifier];
|
|
|
|
// Strip namespace and get just the class name
|
|
$class_parts = explode('\\', $bundle_class);
|
|
$class_name = end($class_parts);
|
|
|
|
// Now try to find this class in the manifest
|
|
try {
|
|
$file_path = Manifest::php_find_class($class_name);
|
|
|
|
// Find the line with the class definition
|
|
$line_number = 1;
|
|
$absolute_path = base_path($file_path);
|
|
|
|
if (file_exists($absolute_path)) {
|
|
$content = file_get_contents($absolute_path);
|
|
$lines = explode("\n", $content);
|
|
|
|
foreach ($lines as $index => $line) {
|
|
if (preg_match('/^\s*class\s+' . preg_quote($class_name, '/') . '(\s|$)/', $line)) {
|
|
$line_number = $index + 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return response()->json([
|
|
'found' => true,
|
|
'type' => 'bundle_alias',
|
|
'file' => $file_path,
|
|
'line' => $line_number,
|
|
'identifier' => $identifier,
|
|
'resolved_class' => $bundle_class,
|
|
]);
|
|
} catch (RuntimeException $e) {
|
|
// Class not found in manifest
|
|
}
|
|
}
|
|
}
|
|
} catch (Exception $e) {
|
|
// Error reading config
|
|
}
|
|
}
|
|
|
|
// Priority 4: jqhtml template files (when type is 'jqhtml_template')
|
|
if ($type === 'jqhtml_template') {
|
|
try {
|
|
// Look for .jqhtml file with matching component name
|
|
$files = Manifest::get_all();
|
|
$component_snake = $this->camel_to_snake($identifier);
|
|
|
|
// Search for the .jqhtml file in manifest
|
|
foreach ($files as $file_path => $file_data) {
|
|
if (str_ends_with($file_path, '.jqhtml')) {
|
|
$basename = basename($file_path, '.jqhtml');
|
|
|
|
// Check if filename matches (either exact or snake_case version)
|
|
if ($basename === $component_snake ||
|
|
$this->snake_to_pascal($basename) === $identifier) {
|
|
// Find line with <Define:ComponentName> if present
|
|
$line_number = 1;
|
|
$absolute_path = base_path($file_path);
|
|
|
|
if (file_exists($absolute_path)) {
|
|
$content = file_get_contents($absolute_path);
|
|
$lines = explode("\n", $content);
|
|
|
|
foreach ($lines as $index => $line) {
|
|
if (preg_match('/<Define:\s*' . preg_quote($identifier, '/') . '/', $line)) {
|
|
$line_number = $index + 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return response()->json([
|
|
'found' => true,
|
|
'type' => 'jqhtml_template',
|
|
'file' => $file_path,
|
|
'line' => $line_number,
|
|
'identifier' => $identifier,
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
} catch (Exception $e) {
|
|
// Not found as jqhtml template
|
|
}
|
|
}
|
|
|
|
// Priority 5: jqhtml JavaScript classes (when type is 'jqhtml_class')
|
|
if ($type === 'jqhtml_class') {
|
|
try {
|
|
// Look for JS class extending Component
|
|
$files = Manifest::get_all();
|
|
|
|
// Search for .js files containing the class
|
|
foreach ($files as $file_path => $file_data) {
|
|
if (str_ends_with($file_path, '.js')) {
|
|
$absolute_path = base_path($file_path);
|
|
|
|
if (file_exists($absolute_path)) {
|
|
$content = file_get_contents($absolute_path);
|
|
|
|
// Check if this file contains a class with the right name extending Component
|
|
// Look for patterns like: class ComponentName extends Component
|
|
if (preg_match('/class\s+' . preg_quote($identifier, '/') . '\s+extends\s+[A-Za-z_]*Component/', $content)) {
|
|
$lines = explode("\n", $content);
|
|
$line_number = 1;
|
|
|
|
foreach ($lines as $index => $line) {
|
|
if (preg_match('/class\s+' . preg_quote($identifier, '/') . '\s+extends/', $line)) {
|
|
$line_number = $index + 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return response()->json([
|
|
'found' => true,
|
|
'type' => 'jqhtml_class',
|
|
'file' => $file_path,
|
|
'line' => $line_number,
|
|
'identifier' => $identifier,
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (Exception $e) {
|
|
// Not found as jqhtml class
|
|
}
|
|
}
|
|
|
|
// Priority 6: jqhtml class methods (when type is 'jqhtml_class_method')
|
|
if ($type === 'jqhtml_class_method' && $method_name) {
|
|
try {
|
|
// Special case: if method_name is 'data', look for 'on_load' instead
|
|
$search_method = ($method_name === 'data') ? 'on_load' : $method_name;
|
|
|
|
// Look for JS class extending Component and find the method
|
|
$files = Manifest::get_all();
|
|
|
|
// Search for .js files containing the class
|
|
foreach ($files as $file_path => $file_data) {
|
|
if (str_ends_with($file_path, '.js')) {
|
|
$absolute_path = base_path($file_path);
|
|
|
|
if (file_exists($absolute_path)) {
|
|
$content = file_get_contents($absolute_path);
|
|
|
|
// Check if this file contains a class with the right name extending Component
|
|
if (preg_match('/class\s+' . preg_quote($identifier, '/') . '\s+extends\s+[A-Za-z_]*Component/', $content)) {
|
|
$lines = explode("\n", $content);
|
|
$line_number = 1;
|
|
$in_class = false;
|
|
$brace_count = 0;
|
|
|
|
foreach ($lines as $index => $line) {
|
|
// Check if we're entering the class
|
|
if (preg_match('/class\s+' . preg_quote($identifier, '/') . '\s+extends/', $line)) {
|
|
$in_class = true;
|
|
}
|
|
|
|
// Track braces to know when we've left the class
|
|
if ($in_class) {
|
|
$brace_count += substr_count($line, '{');
|
|
$brace_count -= substr_count($line, '}');
|
|
|
|
// Look for the method definition
|
|
// Match patterns like: methodName() { or async methodName() {
|
|
if (preg_match('/(?:async\s+)?' . preg_quote($search_method, '/') . '\s*\(/', $line)) {
|
|
$line_number = $index + 1;
|
|
break;
|
|
}
|
|
|
|
// If we've left the class, stop looking
|
|
if ($brace_count === 0 && strpos($line, '}') !== false) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return response()->json([
|
|
'found' => true,
|
|
'type' => 'jqhtml_class_method',
|
|
'file' => $file_path,
|
|
'line' => $line_number,
|
|
'identifier' => $identifier,
|
|
'method' => $method_name,
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (Exception $e) {
|
|
// Not found as jqhtml class method
|
|
}
|
|
}
|
|
|
|
// Priority 7: Future - other types
|
|
|
|
// Nothing found
|
|
return response()->json([
|
|
'found' => false,
|
|
'error' => 'Identifier not found in manifest',
|
|
'identifier' => $identifier,
|
|
'searched_types' => ['class', 'view', 'bundle_alias', 'jqhtml_template', 'jqhtml_class', 'jqhtml_class_method'],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Convert PascalCase or camelCase to snake_case
|
|
*/
|
|
private function camel_to_snake(string $input): string
|
|
{
|
|
// Insert underscore before uppercase letters
|
|
$result = preg_replace('/(?<!^)[A-Z]/', '_$0', $input);
|
|
|
|
return strtolower($result);
|
|
}
|
|
|
|
/**
|
|
* Convert snake_case to PascalCase
|
|
*/
|
|
private function snake_to_pascal(string $input): string
|
|
{
|
|
$parts = explode('_', $input);
|
|
|
|
return implode('_', array_map('ucfirst', $parts));
|
|
}
|
|
|
|
/**
|
|
* Get the inheritance lineage for a JavaScript class
|
|
* Returns array of parent class names from immediate parent to root
|
|
*/
|
|
public function js_lineage(Request $request): JsonResponse
|
|
{
|
|
// Double-check we're not in production
|
|
if (app()->environment('production')) {
|
|
return response()->json([
|
|
'error' => 'This endpoint is not available in production',
|
|
], 403);
|
|
}
|
|
|
|
$class_name = $request->query('class');
|
|
|
|
if (!$class_name) {
|
|
return response()->json([
|
|
'error' => 'Missing required parameter: class',
|
|
], 400);
|
|
}
|
|
|
|
$lineage = Manifest::js_get_lineage($class_name);
|
|
|
|
return response()->json([
|
|
'class' => $class_name,
|
|
'lineage' => $lineage,
|
|
]);
|
|
}
|
|
}
|