Fix code quality violations and add VS Code extension features
Fix VS Code extension storage paths for new directory structure Fix jqhtml compiled files missing from bundle Fix bundle babel transformation and add rsxrealpath() function 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -46,7 +46,7 @@ SHOW_CONSOLE_DEBUG_HTTP=false
|
||||
# FORCE_REBUILD_EVERY_REQUEST: Clear build cache on each request (development only)
|
||||
|
||||
# Gatekeeper Development Preview Authentication
|
||||
GATEKEEPER_ENABLED=true
|
||||
GATEKEEPER_ENABLED=false
|
||||
GATEKEEPER_PASSWORD=preview123
|
||||
GATEKEEPER_TITLE="Development Preview"
|
||||
GATEKEEPER_SUBTITLE="This is a restricted development preview site. Please enter the access password to continue."
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -38,8 +38,7 @@ supervisord.pid*
|
||||
# RSX Framework
|
||||
.rsx-manifest-cache
|
||||
.migrating
|
||||
_rsx_helper.php
|
||||
rsx/_rsx_helper.php
|
||||
._rsx_helper.php
|
||||
_ide_helper.php
|
||||
|
||||
# Build artifacts
|
||||
|
||||
@@ -25,11 +25,6 @@ class Gatekeeper
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// Always allow IDE helper endpoints for VS Code extension integration
|
||||
if (str_starts_with($request->path(), '_idehelper')) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// Check if request is whitelisted (localhost without reverse proxy headers)
|
||||
if ($this->is_whitelisted($request)) {
|
||||
return $next($request);
|
||||
|
||||
@@ -186,6 +186,11 @@ class HardcodedInternalUrl_CodeQualityRule extends CodeQualityRule_Abstract
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allow exactly "/" (root/home URL) - common and acceptable
|
||||
if ($url === '/') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip absolute URLs (with protocol)
|
||||
if (preg_match('#^//#', $url)) {
|
||||
return false;
|
||||
|
||||
@@ -98,6 +98,12 @@ class JQueryLengthCheck_CodeQualityRule extends CodeQualityRule_Abstract
|
||||
}
|
||||
|
||||
if ($found) {
|
||||
// Check if .length is followed by comparison or assignment operators
|
||||
// These are valid uses: .length > 1, .length = x, etc.
|
||||
if (preg_match('/\.length\s*([><=!]+|[+\-*\/]=)/', $sanitized_line)) {
|
||||
continue; // Skip - this is a numeric comparison or assignment
|
||||
}
|
||||
|
||||
$original_line = $original_lines[$line_num] ?? $sanitized_line;
|
||||
|
||||
$this->add_violation(
|
||||
|
||||
@@ -51,13 +51,20 @@ class JQueryVariableNaming_CodeQualityRule extends CodeQualityRule_Abstract
|
||||
];
|
||||
|
||||
/**
|
||||
* jQuery methods that return scalar values (not jQuery objects)
|
||||
* jQuery methods that return scalar values ONLY when called as getters (no arguments)
|
||||
* When called with arguments, these return jQuery object for chaining
|
||||
*/
|
||||
private const SCALAR_METHODS = [
|
||||
private const GETTER_METHODS = [
|
||||
'data', 'attr', 'val', 'text', 'html', 'prop', 'css',
|
||||
'offset', 'position', 'scrollTop', 'scrollLeft',
|
||||
'width', 'height', 'innerWidth', 'innerHeight',
|
||||
'outerWidth', 'outerHeight',
|
||||
];
|
||||
|
||||
/**
|
||||
* jQuery methods that ALWAYS return scalar values
|
||||
*/
|
||||
private const SCALAR_METHODS = [
|
||||
'index', 'size', 'length', 'get', 'toArray',
|
||||
'serialize', 'serializeArray',
|
||||
'is', 'hasClass', 'is_visible' // Custom RSpade methods
|
||||
@@ -155,7 +162,23 @@ class JQueryVariableNaming_CodeQualityRule extends CodeQualityRule_Abstract
|
||||
|
||||
// Direct jQuery selector: $(...)
|
||||
if (preg_match('/^\$\s*\(/', $expr)) {
|
||||
// Check if followed by method chain
|
||||
// Check if it's creating an element: $('<element>')
|
||||
if (preg_match('/^\$\s*\(\s*[\'"]</', $expr)) {
|
||||
// Creating jQuery element - always returns jQuery object
|
||||
// Even with method chains like .text() or .attr(), the chaining continues
|
||||
// We only care about method chains that END with scalar methods
|
||||
if (preg_match('/^\$\s*\([^)]*\)(.*)/', $expr, $matches)) {
|
||||
$chain = trim($matches[1]);
|
||||
if ($chain === '') {
|
||||
return 'jquery'; // Just $('<element>') with no methods
|
||||
}
|
||||
// Only check if chain ENDS with a scalar method
|
||||
return $this->analyze_method_chain($chain);
|
||||
}
|
||||
return 'jquery';
|
||||
}
|
||||
|
||||
// Regular selector or other jQuery call
|
||||
if (preg_match('/^\$\s*\([^)]*\)(.*)/', $expr, $matches)) {
|
||||
$chain = trim($matches[1]);
|
||||
if ($chain === '') {
|
||||
@@ -201,16 +224,19 @@ class JQueryVariableNaming_CodeQualityRule extends CodeQualityRule_Abstract
|
||||
|
||||
// Find the last method call in the chain
|
||||
// Match patterns like .method() or .method(args)
|
||||
// Also capture what's inside the parentheses
|
||||
$methods = [];
|
||||
preg_match_all('/\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\([^)]*\)/', $chain, $methods);
|
||||
preg_match_all('/\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\(([^)]*)\)/', $chain, $methods, PREG_SET_ORDER);
|
||||
|
||||
if (empty($methods[1])) {
|
||||
if (empty($methods)) {
|
||||
// No method calls found
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
// Check the last method to determine return type
|
||||
$last_method = end($methods[1]);
|
||||
$last_method_data = end($methods);
|
||||
$last_method = $last_method_data[1];
|
||||
$last_args = trim($last_method_data[2] ?? '');
|
||||
|
||||
if (in_array($last_method, self::JQUERY_OBJECT_METHODS, true)) {
|
||||
return 'jquery';
|
||||
@@ -220,6 +246,35 @@ class JQueryVariableNaming_CodeQualityRule extends CodeQualityRule_Abstract
|
||||
return 'scalar';
|
||||
}
|
||||
|
||||
// Check getter methods - return scalar for getters, jQuery for setters
|
||||
if (in_array($last_method, self::GETTER_METHODS, true)) {
|
||||
// Count arguments by splitting on commas (simple heuristic)
|
||||
// Note: This won't handle nested function calls perfectly, but works for common cases
|
||||
$arg_count = $last_args === '' ? 0 : (substr_count($last_args, ',') + 1);
|
||||
|
||||
// Special handling for methods that take a key parameter
|
||||
// .data('key') - 1 arg = getter (returns value)
|
||||
// .data('key', value) - 2 args = setter (returns jQuery)
|
||||
// .attr('name') - 1 arg = getter (returns attribute value)
|
||||
// .attr('name', value) - 2 args = setter (returns jQuery)
|
||||
if (in_array($last_method, ['data', 'attr', 'prop', 'css'], true)) {
|
||||
if ($arg_count <= 1) {
|
||||
return 'scalar'; // Getter with key - returns scalar value
|
||||
} else {
|
||||
return 'jquery'; // Setter with key and value - returns jQuery for chaining
|
||||
}
|
||||
}
|
||||
|
||||
// For other getter methods (text, html, val, etc.)
|
||||
// .text() - no args = getter (returns text)
|
||||
// .text('value') - 1 arg = setter (returns jQuery)
|
||||
if ($last_args === '') {
|
||||
return 'scalar'; // Getter mode - returns scalar
|
||||
} else {
|
||||
return 'jquery'; // Setter mode - returns jQuery object for chaining
|
||||
}
|
||||
}
|
||||
|
||||
// Unknown method - could be custom plugin
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
@@ -7,23 +7,24 @@ use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
|
||||
/**
|
||||
* JavaScript 'this' Usage Rule
|
||||
*
|
||||
* PHILOSOPHY: Remove ambiguity about what 'this' refers to in all contexts.
|
||||
* PHILOSOPHY: Enforce clear 'this' patterns in anonymous functions and static methods.
|
||||
*
|
||||
* RULES:
|
||||
* 1. Anonymous functions: Can use 'const $var = $(this)' as first line (jQuery pattern)
|
||||
* 2. Instance methods: Must use 'const that = this' as first line (constructors exempt)
|
||||
* 3. Static methods: Use Class_Name OR 'const CurrentClass = this' for polymorphism
|
||||
* 4. Arrow functions: Ignored (they inherit 'this' context)
|
||||
* 5. Constructors: Direct 'this' usage allowed for property assignment
|
||||
* 1. Anonymous functions: MUST use 'const $element = $(this)' or 'const that = this' as first line
|
||||
* 2. Static methods: MUST NOT use naked 'this' - use Class_Name or 'const CurrentClass = this'
|
||||
* 3. Instance methods: EXEMPT - can use 'this' directly (no aliasing required)
|
||||
* 4. Arrow functions: EXEMPT - they inherit 'this' context
|
||||
* 5. Constructors: EXEMPT - 'this' allowed directly for property assignment
|
||||
*
|
||||
* PATTERNS:
|
||||
* - jQuery: const $element = $(this) // Variable must start with $
|
||||
* - Instance: const that = this // Standard instance aliasing
|
||||
* - jQuery callback: const $element = $(this) // Variable must start with $
|
||||
* - Anonymous function: const that = this // Instance context aliasing
|
||||
* - Static (exact): Use Class_Name // When you need exact class
|
||||
* - Static (polymorphic): const CurrentClass = this // When inherited classes need different behavior
|
||||
*
|
||||
* This rule does NOT try to detect all jQuery callbacks - it offers the jQuery
|
||||
* pattern as an option when 'this' violations are found in anonymous functions.
|
||||
* INSTANCE METHODS POLICY:
|
||||
* Instance methods (on_ready, on_load, etc.) can use 'this' directly.
|
||||
* This rule only enforces aliasing for anonymous functions and prohibits naked 'this' in static methods.
|
||||
*/
|
||||
class ThisUsage_CodeQualityRule extends CodeQualityRule_Abstract
|
||||
{
|
||||
|
||||
@@ -159,7 +159,22 @@ function analyzeFile(filePath) {
|
||||
let isAnonymousFunc = false;
|
||||
let isStaticMethod = false;
|
||||
let isConstructor = false;
|
||||
let isInstanceMethod = false;
|
||||
let hasMethodDefinition = false;
|
||||
|
||||
// First pass: check if we're in a MethodDefinition
|
||||
for (let i = ancestors.length - 1; i >= 0; i--) {
|
||||
const ancestor = ancestors[i];
|
||||
if (ancestor.type === 'MethodDefinition') {
|
||||
hasMethodDefinition = true;
|
||||
isStaticMethod = ancestor.static;
|
||||
isConstructor = ancestor.kind === 'constructor';
|
||||
isInstanceMethod = !ancestor.static && ancestor.kind !== 'constructor';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: find function and class
|
||||
for (let i = ancestors.length - 1; i >= 0; i--) {
|
||||
const ancestor = ancestors[i];
|
||||
|
||||
@@ -168,7 +183,8 @@ function analyzeFile(filePath) {
|
||||
ancestor.type === 'FunctionDeclaration'
|
||||
)) {
|
||||
containingFunc = ancestor;
|
||||
isAnonymousFunc = ancestor.type === 'FunctionExpression';
|
||||
// Only mark as anonymous if NOT inside a MethodDefinition
|
||||
isAnonymousFunc = ancestor.type === 'FunctionExpression' && !hasMethodDefinition;
|
||||
}
|
||||
|
||||
if (!containingClass && (
|
||||
@@ -177,11 +193,6 @@ function analyzeFile(filePath) {
|
||||
)) {
|
||||
containingClass = ancestor;
|
||||
}
|
||||
|
||||
if (ancestor.type === 'MethodDefinition') {
|
||||
isStaticMethod = ancestor.static;
|
||||
isConstructor = ancestor.kind === 'constructor';
|
||||
}
|
||||
}
|
||||
|
||||
if (!containingFunc) {
|
||||
@@ -193,6 +204,12 @@ function analyzeFile(filePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip instance methods - 'this' is allowed directly in instance methods
|
||||
// Only enforce aliasing for anonymous functions and static methods
|
||||
if (isInstanceMethod) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is part of the allowed first-line pattern with const
|
||||
const parent = ancestors[ancestors.length - 2];
|
||||
const firstStmt = containingFunc.body?.body?.[0];
|
||||
@@ -323,23 +340,9 @@ function analyzeFile(filePath) {
|
||||
if (isAnonymousFunc && !pattern) {
|
||||
remediation += `\nException: If this is a jQuery callback, add 'const $element = $(this);' as the first line.`;
|
||||
}
|
||||
} else {
|
||||
// Instance method
|
||||
if (!pattern) {
|
||||
message = `Instance method in '${className}' must alias 'this' for clarity.`;
|
||||
remediation = `Add 'const that = this;' as the first line of this method, then use 'that' instead of 'this'.\n` +
|
||||
`This applies even to ORM models and similar classes where direct property access is common.\n` +
|
||||
`Note: Constructors are exempt - 'this' is allowed directly in constructors for property assignment.\n` +
|
||||
`Example: Instead of 'return this.name;' use 'const that = this; return that.name;'`;
|
||||
} else if (pattern === 'that-pattern') {
|
||||
message = `'this' used after aliasing to 'that'. Use 'that' instead.`;
|
||||
remediation = `You already have 'const that = this'. Use 'that' consistently throughout the method.\n` +
|
||||
`All property access should use 'that.property' not 'this.property'.`;
|
||||
} else if (pattern === 'that-pattern-wrong-kind') {
|
||||
message = `Instance alias must use 'const', not 'let' or 'var'.`;
|
||||
remediation = `Change to 'const that = this;' - the instance reference should never be reassigned.`;
|
||||
}
|
||||
}
|
||||
// NOTE: Instance methods are exempt from this rule - they can use 'this' directly
|
||||
// The check returns early for instance methods, so this else block is unreachable for them
|
||||
|
||||
if (message) {
|
||||
violations.push({
|
||||
|
||||
@@ -99,7 +99,7 @@ class ModelEnumColumns_CodeQualityRule extends CodeQualityRule_Abstract
|
||||
$this->add_violation(
|
||||
$file_path,
|
||||
$line_number,
|
||||
"Enum field '{$column}' does not exist as a column in table '{$table_name}'",
|
||||
"Enum field '{$column}' does not exist as a column in table '{$table_name}' (Have migrations been run yet?)",
|
||||
$lines[$line_number - 1] ?? '',
|
||||
"Remove the enum definition for '{$column}' or add the column to the database table",
|
||||
'high'
|
||||
|
||||
@@ -85,15 +85,15 @@ class ModelEnums_CodeQualityRule extends CodeQualityRule_Abstract
|
||||
"Model {$class_name} is missing public static \$enums property",
|
||||
$lines[$class_line - 1] ?? '',
|
||||
"Add: public static \$enums = [];\n\n" .
|
||||
"For models with enum fields (fields ending in _id that reference lookup tables), use:\n" .
|
||||
"For models with enum fields (fields that reference lookup tables), use:\n" .
|
||||
"public static \$enums = [\n" .
|
||||
" 'role_id' => [\n" .
|
||||
" 1 => ['constant' => 'ROLE_OWNER', 'label' => 'Owner'],\n" .
|
||||
" 2 => ['constant' => 'ROLE_ADMIN', 'label' => 'Admin'],\n" .
|
||||
" 'status' => [\n" .
|
||||
" 1 => ['constant' => 'STATUS_ACTIVE', 'label' => 'Active'],\n" .
|
||||
" 2 => ['constant' => 'STATUS_INACTIVE', 'label' => 'Inactive'],\n" .
|
||||
" ]\n" .
|
||||
"];\n\n" .
|
||||
"Note: Top-level keys must match column names ending with '_id'. " .
|
||||
"Second-level keys are the integer values. Third-level arrays must have 'constant' and 'label' fields.",
|
||||
"Note: Top-level keys are column names. " .
|
||||
"Second-level keys must be integers. Third-level arrays must have 'constant' and 'label' fields.",
|
||||
'medium'
|
||||
);
|
||||
|
||||
@@ -107,9 +107,9 @@ class ModelEnums_CodeQualityRule extends CodeQualityRule_Abstract
|
||||
if (trim($enums_content) !== '[]') {
|
||||
// Parse the enums array more carefully
|
||||
// We need to identify the structure:
|
||||
// 'field_id' => [ value_id => ['constant' => ..., 'label' => ...], ... ]
|
||||
// 'field_name' => [ integer_value => ['constant' => ..., 'label' => ...], ... ]
|
||||
|
||||
// First, find the top-level keys (should end with _id or start with is_)
|
||||
// First, find the top-level keys (any field name allowed, special handling for is_ prefix)
|
||||
// We'll look for patterns like 'key' => [ or "key" => [
|
||||
$pattern = '/[\'"]([^\'"]+)[\'\"]\s*=>\s*\[/';
|
||||
if (preg_match_all($pattern, $enums_content, $field_matches, PREG_OFFSET_CAPTURE)) {
|
||||
@@ -117,32 +117,9 @@ class ModelEnums_CodeQualityRule extends CodeQualityRule_Abstract
|
||||
$field = $field_match[0];
|
||||
$offset = $field_match[1];
|
||||
|
||||
// Check that top-level field names end with _id or start with is_
|
||||
if (!str_ends_with($field, '_id') && !str_starts_with($field, 'is_')) {
|
||||
// Find the line number for this field
|
||||
$line_number = 1;
|
||||
$chars_before = substr($enums_content, 0, $offset);
|
||||
$line_number += substr_count($chars_before, "\n");
|
||||
|
||||
// Find actual line in original file
|
||||
foreach ($lines as $i => $line) {
|
||||
if ((str_contains($line, "'{$field}'") || str_contains($line, "\"{$field}\""))
|
||||
&& str_contains($line, '=>')) {
|
||||
$line_number = $i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->add_violation(
|
||||
$file_path,
|
||||
$line_number,
|
||||
"Enum field '{$field}' must end with '_id' or start with 'is_'",
|
||||
$lines[$line_number - 1] ?? '',
|
||||
"Rename enum field to either '{$field}_id' or 'is_{$field}'. " .
|
||||
"Enum field names must end with '_id' for ID fields or start with 'is_' for boolean fields.",
|
||||
'medium'
|
||||
);
|
||||
}
|
||||
// No naming convention enforcement - any field name is allowed
|
||||
// Only requirement: integer keys for values (checked below)
|
||||
// Special handling for is_ prefix (boolean fields) is kept
|
||||
|
||||
// Now check the structure under this field
|
||||
// We need to find the content of this particular field's array
|
||||
|
||||
105
app/RSpade/CodeQuality/Rules/PHP/RealpathUsage_CodeQualityRule.php
Executable file
105
app/RSpade/CodeQuality/Rules/PHP/RealpathUsage_CodeQualityRule.php
Executable file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace App\RSpade\CodeQuality\Rules\PHP;
|
||||
|
||||
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
|
||||
|
||||
class RealpathUsage_CodeQualityRule extends CodeQualityRule_Abstract
|
||||
{
|
||||
public function get_id(): string
|
||||
{
|
||||
return 'PHP-REALPATH-01';
|
||||
}
|
||||
|
||||
public function get_name(): string
|
||||
{
|
||||
return 'Realpath Usage Check';
|
||||
}
|
||||
|
||||
public function get_description(): string
|
||||
{
|
||||
return 'Prohibits realpath() usage in app/RSpade - enforces rsxrealpath() for symlink-aware path normalization';
|
||||
}
|
||||
|
||||
public function get_file_patterns(): array
|
||||
{
|
||||
return ['*.php'];
|
||||
}
|
||||
|
||||
public function get_default_severity(): string
|
||||
{
|
||||
return 'high';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for realpath() usage in app/RSpade directory
|
||||
*/
|
||||
public function check(string $file_path, string $contents, array $metadata = []): void
|
||||
{
|
||||
// Only check files in app/RSpade directory
|
||||
if (!str_contains($file_path, 'app/RSpade/')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip vendor and node_modules
|
||||
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip this rule file itself
|
||||
if (str_contains($file_path, 'RealpathUsage_CodeQualityRule.php')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Read raw file content for exception checking (not sanitized)
|
||||
$raw_contents = file_get_contents($file_path);
|
||||
|
||||
// If file has @REALPATH-EXCEPTION marker anywhere, skip entire file
|
||||
if (str_contains($raw_contents, '@REALPATH-EXCEPTION')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$lines = explode("\n", $contents);
|
||||
|
||||
for ($i = 0; $i < count($lines); $i++) {
|
||||
$line = $lines[$i];
|
||||
$line_number = $i + 1;
|
||||
|
||||
// Skip comment lines first (but not inline realpath in code)
|
||||
$trimmed = trim($line);
|
||||
if (str_starts_with($trimmed, '//') || str_starts_with($trimmed, '*') || str_starts_with($trimmed, '/*')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Look for realpath( usage (function call, not in string)
|
||||
if (preg_match('/\brealpath\s*\(/', $line)) {
|
||||
// Get code snippet (current line and surrounding context)
|
||||
$snippet_start = max(0, $i - 2);
|
||||
$snippet_end = min(count($lines) - 1, $i + 2);
|
||||
$snippet_lines = array_slice($lines, $snippet_start, $snippet_end - $snippet_start + 1);
|
||||
$snippet = implode("\n", $snippet_lines);
|
||||
|
||||
$this->add_violation(
|
||||
$file_path,
|
||||
$line_number,
|
||||
"Use rsxrealpath() instead of realpath()
|
||||
|
||||
RSpade uses symlinks for the /rsx/ directory mapping:
|
||||
- /var/www/html/system/rsx -> /var/www/html/rsx (symlink)
|
||||
|
||||
realpath() resolves symlinks to their physical paths, which breaks path-based comparisons and deduplication when files are accessed via different symlink paths.
|
||||
|
||||
rsxrealpath() normalizes paths without resolving symlinks, ensuring consistent path handling throughout the framework.",
|
||||
$snippet,
|
||||
"Replace: realpath(\$path)
|
||||
With: rsxrealpath(\$path)
|
||||
|
||||
If you need symlink resolution for security (path traversal prevention), add:
|
||||
// @REALPATH-EXCEPTION - Security: path traversal prevention
|
||||
\$real_path = realpath(\$path);",
|
||||
'high'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,7 +163,7 @@ class Rsx_Check_Command extends Command
|
||||
$manifest = Manifest::get_all();
|
||||
$found_in_manifest = false;
|
||||
foreach ($manifest as $manifest_path => $metadata) {
|
||||
if (realpath($full_path) === realpath(base_path($manifest_path))) {
|
||||
if (rsxrealpath($full_path) === rsxrealpath(base_path($manifest_path))) {
|
||||
$found_in_manifest = true;
|
||||
break;
|
||||
}
|
||||
@@ -247,70 +247,33 @@ class Rsx_Check_Command extends Command
|
||||
}
|
||||
$is_rsx_dir = ($path === 'rsx');
|
||||
|
||||
// Collect PHP files
|
||||
$php_finder = new Finder();
|
||||
$php_finder->files()
|
||||
->in($full_path)
|
||||
->name('*.php')
|
||||
->exclude(['vendor', 'node_modules', 'storage', 'bootstrap/cache']);
|
||||
// Get all manifest files for this directory
|
||||
$manifest = Manifest::get_all();
|
||||
|
||||
foreach ($php_finder as $file) {
|
||||
foreach ($manifest as $manifest_path => $metadata) {
|
||||
// Convert manifest path to full path
|
||||
$manifest_full_path = base_path($manifest_path);
|
||||
|
||||
// Check if this manifest file is within the requested directory
|
||||
// Either the file is directly in the directory or in a subdirectory
|
||||
if (str_starts_with($manifest_path, $relative_dir . '/') || $manifest_path === $relative_dir) {
|
||||
// Only include files that exist
|
||||
if (file_exists($manifest_full_path)) {
|
||||
// Avoid duplicates
|
||||
if (!in_array($manifest_full_path, $files_to_check)) {
|
||||
$total_files++;
|
||||
$files_to_check[] = $file->getPathname();
|
||||
$files_to_check[] = $manifest_full_path;
|
||||
|
||||
// Count controller/model files for stats
|
||||
// Count controller/model files for stats (if in RSX directory)
|
||||
if ($is_rsx_dir) {
|
||||
if ($this->is_controller_file($file->getPathname()) || $this->is_model_file($file->getPathname())) {
|
||||
if ($this->is_controller_file($manifest_full_path) || $this->is_model_file($manifest_full_path)) {
|
||||
$controller_files++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === Additional File Collection ===
|
||||
// Collect all files for filename case checks (RSX only)
|
||||
if ($is_rsx_dir) {
|
||||
$all_files_finder = new Finder();
|
||||
$all_files_finder->files()
|
||||
->in($full_path)
|
||||
->exclude(['vendor', 'node_modules', 'storage', 'bootstrap/cache', 'resource']);
|
||||
|
||||
foreach ($all_files_finder as $file) {
|
||||
$file_path = $file->getPathname();
|
||||
if (!in_array($file_path, $files_to_check)) {
|
||||
$files_to_check[] = $file_path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect JavaScript files for all directories
|
||||
$js_finder = new Finder();
|
||||
$js_finder->files()
|
||||
->in($full_path)
|
||||
->name('*.js')
|
||||
->exclude(['vendor', 'node_modules', 'storage', 'public', 'bootstrap/cache']);
|
||||
|
||||
foreach ($js_finder as $file) {
|
||||
$file_path = $file->getPathname();
|
||||
if (!in_array($file_path, $files_to_check)) {
|
||||
$files_to_check[] = $file_path;
|
||||
$total_files++;
|
||||
}
|
||||
}
|
||||
|
||||
// Collect JSON files for all directories
|
||||
$json_finder = new Finder();
|
||||
$json_finder->files()
|
||||
->in($full_path)
|
||||
->name('*.json')
|
||||
->exclude(['vendor', 'node_modules', 'storage', 'bootstrap/cache']);
|
||||
|
||||
foreach ($json_finder as $file) {
|
||||
$file_path = $file->getPathname();
|
||||
if (!in_array($file_path, $files_to_check)) {
|
||||
$files_to_check[] = $file_path;
|
||||
$total_files++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If using default paths, also include Console Commands for rules that support them
|
||||
|
||||
@@ -89,6 +89,12 @@ class BundleCompiler
|
||||
*/
|
||||
protected array $jqhtml_compiled_files = [];
|
||||
|
||||
/**
|
||||
* Mapping from babel-transformed files to their original source files
|
||||
* ['storage/rsx-tmp/babel_xxx.js' => 'app/RSpade/Core/Js/SomeFile.js']
|
||||
*/
|
||||
protected array $babel_file_mapping = [];
|
||||
|
||||
/**
|
||||
* Compile a bundle
|
||||
*/
|
||||
@@ -607,7 +613,8 @@ class BundleCompiler
|
||||
*/
|
||||
protected function _add_file(string $path): void
|
||||
{
|
||||
$normalized = realpath($path);
|
||||
$normalized = rsxrealpath($path);
|
||||
|
||||
if (!$normalized) {
|
||||
return;
|
||||
}
|
||||
@@ -694,7 +701,7 @@ class BundleCompiler
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isFile()) {
|
||||
$normalized = realpath($file->getPathname());
|
||||
$normalized = rsxrealpath($file->getPathname());
|
||||
if (!isset($this->included_files[$normalized])) {
|
||||
if (!isset($this->watch_files['all'])) {
|
||||
$this->watch_files['all'] = [];
|
||||
@@ -977,6 +984,8 @@ class BundleCompiler
|
||||
*/
|
||||
protected function _order_javascript_files_by_dependency(array $js_files): array
|
||||
{
|
||||
console_debug('BUNDLE_SORT', 'Starting dependency sort with ' . count($js_files) . ' files');
|
||||
|
||||
$manifest = Manifest::get_full_manifest();
|
||||
$manifest_files = $manifest['data']['files'] ?? [];
|
||||
|
||||
@@ -994,7 +1003,8 @@ class BundleCompiler
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip other temp files (they won't be in the manifest)
|
||||
// Skip ALL temp files - they won't be in manifest
|
||||
// Babel and other transformations should have been applied to original files
|
||||
if (str_contains($file, 'storage/rsx-tmp/')) {
|
||||
$non_class_files[] = $file;
|
||||
continue;
|
||||
@@ -1278,24 +1288,26 @@ implode("\n", array_map(fn ($f) => ' - ' . str_replace(base_path() . '/', '',
|
||||
};
|
||||
|
||||
// Process all class files
|
||||
try {
|
||||
foreach ($class_files as $file) {
|
||||
if (!isset($visited[$file])) {
|
||||
$visit($file);
|
||||
$visit($file); //??
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException $e) {
|
||||
// Re-throw with bundle context if available
|
||||
if (!empty($this->bundle_class)) {
|
||||
throw new RuntimeException(
|
||||
"Bundle compilation failed for {$this->bundle_class}:\n" . $e->getMessage(),
|
||||
0,
|
||||
$e
|
||||
);
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
// try {
|
||||
// (code above was here)
|
||||
// } catch (RuntimeException $e) {
|
||||
// // Re-throw with bundle context if available
|
||||
// if (!empty($this->bundle_class)) {
|
||||
// throw new RuntimeException(
|
||||
// "Bundle compilation failed for {$this->bundle_class}:\n" . $e->getMessage(),
|
||||
// 0,
|
||||
// $e
|
||||
// );
|
||||
// }
|
||||
|
||||
// throw $e;
|
||||
// }
|
||||
|
||||
return $sorted;
|
||||
}
|
||||
@@ -1744,12 +1756,12 @@ implode("\n", array_map(fn ($f) => ' - ' . str_replace(base_path() . '/', '',
|
||||
|
||||
if ($babel_enabled && $decorators_enabled) {
|
||||
// Use the JavaScript Transformer to transpile files with decorators
|
||||
$transformed_files = [];
|
||||
// IMPORTANT: We populate $babel_file_mapping but DON'T modify $files array
|
||||
// This preserves dependency sort order - we substitute babel versions during concat
|
||||
|
||||
foreach ($files as $file) {
|
||||
// Skip temp files and already processed files
|
||||
if (str_contains($file, 'storage/rsx-tmp/') || str_contains($file, 'storage/rsx-build/')) {
|
||||
$transformed_files[] = $file;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1757,11 +1769,13 @@ implode("\n", array_map(fn ($f) => ' - ' . str_replace(base_path() . '/', '',
|
||||
try {
|
||||
$transformed_code = \App\RSpade\Core\JavaScript\Js_Transformer::transform($file);
|
||||
|
||||
// Write transformed code to a temp file for concatenation
|
||||
// Write transformed code to a temp file
|
||||
$temp_file = storage_path('rsx-tmp/babel_' . md5($file) . '_' . uniqid() . '.js');
|
||||
file_put_contents($temp_file, $transformed_code);
|
||||
|
||||
$transformed_files[] = $temp_file;
|
||||
// Store mapping: original file => babel file
|
||||
// During concatenation we'll use the babel version
|
||||
$this->babel_file_mapping[$file] = $temp_file;
|
||||
console_debug('BUNDLE', 'Transformed ' . str_replace(base_path() . '/', '', $file));
|
||||
} catch (Exception $e) {
|
||||
// FAIL LOUD - Never allow untransformed decorators through
|
||||
@@ -1773,8 +1787,6 @@ implode("\n", array_map(fn ($f) => ' - ' . str_replace(base_path() . '/', '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$files = $transformed_files;
|
||||
}
|
||||
|
||||
// Add all the JS files
|
||||
@@ -1791,7 +1803,9 @@ implode("\n", array_map(fn ($f) => ' - ' . str_replace(base_path() . '/', '',
|
||||
escapeshellarg($output_file),
|
||||
];
|
||||
foreach ($files_to_concat as $file) {
|
||||
$cmd_parts[] = escapeshellarg($file);
|
||||
// Use babel-transformed version if it exists, otherwise use original
|
||||
$file_to_use = $this->babel_file_mapping[$file] ?? $file;
|
||||
$cmd_parts[] = escapeshellarg($file_to_use);
|
||||
}
|
||||
$cmd = implode(' ', $cmd_parts);
|
||||
|
||||
@@ -1861,6 +1875,7 @@ implode("\n", array_map(fn ($f) => ' - ' . str_replace(base_path() . '/', '',
|
||||
|
||||
if ($return_var !== 0) {
|
||||
$error_msg = implode("\n", $output);
|
||||
|
||||
throw new RuntimeException('Failed to concatenate CSS files: ' . $error_msg);
|
||||
}
|
||||
|
||||
@@ -2161,4 +2176,27 @@ JS;
|
||||
// Write to temporary file
|
||||
return $this->_write_temp_file($js_code, 'js');
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively scan directory for files with specific extension
|
||||
*/
|
||||
protected function _scan_directory_recursive(string $path, string $extension): array
|
||||
{
|
||||
$files = [];
|
||||
|
||||
if (!is_dir($path)) {
|
||||
return $files;
|
||||
}
|
||||
|
||||
$directory = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS);
|
||||
$iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST);
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isFile() && $file->getExtension() === $extension) {
|
||||
$files[] = $file->getPathname();
|
||||
}
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,6 +391,7 @@ class AssetHandler
|
||||
*/
|
||||
protected static function __find_public_directory($full_path)
|
||||
{
|
||||
// @REALPATH-EXCEPTION - Security: path traversal prevention requires symlink resolution
|
||||
$real_path = realpath($full_path);
|
||||
if (!$real_path) {
|
||||
return null;
|
||||
@@ -398,6 +399,7 @@ class AssetHandler
|
||||
|
||||
// Find the public directory that contains this file
|
||||
foreach (static::$public_directories as $directory) {
|
||||
// @REALPATH-EXCEPTION - Security: path traversal prevention requires symlink resolution
|
||||
$real_directory = realpath($directory);
|
||||
if ($real_directory && str_starts_with($real_path, $real_directory)) {
|
||||
return $real_directory;
|
||||
@@ -477,6 +479,7 @@ class AssetHandler
|
||||
*/
|
||||
protected static function __is_safe_path($path)
|
||||
{
|
||||
// @REALPATH-EXCEPTION - Security: path traversal prevention requires symlink resolution
|
||||
$real_path = realpath($path);
|
||||
|
||||
if ($real_path === false) {
|
||||
@@ -485,6 +488,7 @@ class AssetHandler
|
||||
|
||||
// Check if real path is within any allowed directory
|
||||
foreach (static::$public_directories as $directory) {
|
||||
// @REALPATH-EXCEPTION - Security: path traversal prevention requires symlink resolution
|
||||
$real_directory = realpath($directory);
|
||||
|
||||
if (str_starts_with($real_path, $real_directory)) {
|
||||
|
||||
@@ -2369,9 +2369,9 @@ class Manifest
|
||||
|
||||
// Get absolute path - rsx/ files are in project root, not system/
|
||||
if (str_starts_with($file, 'rsx/')) {
|
||||
$absolute_path = realpath(base_path('../' . $file));
|
||||
$absolute_path = rsxrealpath(base_path('../' . $file));
|
||||
} else {
|
||||
$absolute_path = realpath(base_path($file));
|
||||
$absolute_path = rsxrealpath(base_path($file));
|
||||
}
|
||||
|
||||
// Check if this file changed
|
||||
@@ -2904,8 +2904,8 @@ class Manifest
|
||||
// Extract ONLY public static methods - the only methods we care about in RSX
|
||||
$public_static_methods = [];
|
||||
|
||||
// Normalize file_path for comparison (resolves symlinks)
|
||||
$normalized_file_path = realpath($file_path);
|
||||
// Normalize file_path for comparison (without resolving symlinks)
|
||||
$normalized_file_path = rsxrealpath($file_path);
|
||||
|
||||
foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_STATIC) as $method) {
|
||||
// Include methods from:
|
||||
@@ -2915,8 +2915,8 @@ class Manifest
|
||||
$is_from_trait = in_array($method_file, $trait_files);
|
||||
|
||||
// Skip inherited methods from parent classes (but include trait methods)
|
||||
// Use realpath for comparison to handle symlinks correctly
|
||||
if (realpath($method_file) !== $normalized_file_path && !$is_from_trait) {
|
||||
// Use rsxrealpath for comparison to avoid symlink resolution
|
||||
if (rsxrealpath($method_file) !== $normalized_file_path && !$is_from_trait) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -3559,7 +3559,7 @@ class Manifest
|
||||
{
|
||||
// Generate to project root (parent of system/)
|
||||
$project_root = dirname(base_path());
|
||||
$stub_file = $project_root . '/_rsx_helper.php';
|
||||
$stub_file = $project_root . '/._rsx_helper.php';
|
||||
|
||||
$output = "<?php\n";
|
||||
$output .= "/* @noinspection ALL */\n";
|
||||
|
||||
@@ -545,16 +545,16 @@ class Php_Fixer
|
||||
return true;
|
||||
}
|
||||
|
||||
// HACKY CHECK: If this is a simple class name (no namespace), check if it's defined in _rsx_helper.php
|
||||
// HACKY CHECK: If this is a simple class name (no namespace), check if it's defined in ._rsx_helper.php
|
||||
// This catches attribute stub classes that IDEs auto-import incorrectly
|
||||
if (strpos($use_fqcn, '\\') === false) {
|
||||
$helper_file = base_path('rsx/_rsx_helper.php');
|
||||
$helper_file = dirname(base_path()) . '/._rsx_helper.php';
|
||||
if (file_exists($helper_file)) {
|
||||
$helper_content = file_get_contents($helper_file);
|
||||
// Look for class declaration pattern: " class ClassName {\n"
|
||||
$pattern = '/^\s+class\s+' . preg_quote($use_fqcn, '/') . '\s+\{/m';
|
||||
if (preg_match($pattern, $helper_content)) {
|
||||
return true; // Remove - it's an attribute stub from _rsx_helper.php
|
||||
return true; // Remove - it's an attribute stub from ._rsx_helper.php
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,9 @@ error_reporting(E_ALL);
|
||||
ini_set('display_errors', 0);
|
||||
|
||||
// Base path - framework is in system/ subdirectory
|
||||
// @REALPATH-EXCEPTION - Bootstrap file: runs before helpers loaded, needs PHP's realpath()
|
||||
$system_path = realpath(__DIR__ . '/../../../..');
|
||||
// @REALPATH-EXCEPTION - Bootstrap file: runs before helpers loaded, needs PHP's realpath()
|
||||
define('IDE_BASE_PATH', realpath($system_path . '/..')); // Project root
|
||||
define('IDE_SYSTEM_PATH', $system_path); // Framework root
|
||||
|
||||
@@ -27,6 +29,13 @@ function ide_framework_path($relative_path) {
|
||||
return IDE_SYSTEM_PATH . '/' . ltrim($relative_path, '/');
|
||||
}
|
||||
|
||||
// Define Laravel helper that exec_safe() needs
|
||||
// This standalone handler never runs with Laravel, so no conflict
|
||||
function storage_path($path = '') {
|
||||
$base = IDE_SYSTEM_PATH . '/storage';
|
||||
return $path ? $base . '/' . ltrim($path, '/') : $base;
|
||||
}
|
||||
|
||||
// Load exec_safe() function
|
||||
require_once ide_framework_path('app/RSpade/helpers.php');
|
||||
|
||||
@@ -74,6 +83,11 @@ $service_path = trim($service_path, '/');
|
||||
$request_body = file_get_contents('php://input');
|
||||
$request_data = json_decode($request_body, true);
|
||||
|
||||
// Merge GET parameters (for backward compatibility with /_idehelper endpoints)
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||
$request_data = array_merge($request_data ?? [], $_GET);
|
||||
}
|
||||
|
||||
// Handle authentication
|
||||
$auth_data = [];
|
||||
$session_header = $_SERVER['HTTP_X_SESSION'] ?? null;
|
||||
@@ -176,6 +190,18 @@ switch ($service_path) {
|
||||
json_response(['success' => true, 'service' => 'ide', 'version' => '1.0.0']);
|
||||
break;
|
||||
|
||||
case 'resolve_class':
|
||||
handle_resolve_class_service($request_data);
|
||||
break;
|
||||
|
||||
case 'js_lineage':
|
||||
handle_js_lineage_service($request_data);
|
||||
break;
|
||||
|
||||
case 'resolve_url':
|
||||
handle_resolve_url_service($request_data);
|
||||
break;
|
||||
|
||||
default:
|
||||
error_response('Unknown service: ' . $service_path, 404);
|
||||
}
|
||||
@@ -203,6 +229,7 @@ function handle_format_service($data) {
|
||||
|
||||
// Get real directory path
|
||||
if (is_dir($dir)) {
|
||||
// @REALPATH-EXCEPTION - IDE service: runs standalone without framework helpers
|
||||
$real_dir = realpath($dir);
|
||||
if ($real_dir) {
|
||||
$full_path = $real_dir . '/' . $filename;
|
||||
@@ -633,6 +660,7 @@ function handle_git_diff_service($data) {
|
||||
$filename = basename($full_path);
|
||||
|
||||
if (is_dir($dir)) {
|
||||
// @REALPATH-EXCEPTION - IDE service: runs standalone without framework helpers
|
||||
$real_dir = realpath($dir);
|
||||
if ($real_dir) {
|
||||
$full_path = $real_dir . '/' . $filename;
|
||||
@@ -755,3 +783,471 @@ function handle_command_service($data) {
|
||||
'exit_code' => $return_var
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle resolve_class service - resolve class/view/component definitions
|
||||
* This is a port of the Ide_Helper_Controller::resolve_class() method
|
||||
*/
|
||||
function handle_resolve_class_service($data) {
|
||||
$identifier = $data['class'] ?? $data['identifier'] ?? null;
|
||||
$method_name = $data['method'] ?? null;
|
||||
$type = $data['type'] ?? null;
|
||||
|
||||
if (!$identifier) {
|
||||
json_response([
|
||||
'error' => 'Missing required parameter: class or identifier',
|
||||
'found' => false,
|
||||
], 400);
|
||||
}
|
||||
|
||||
// Load manifest data
|
||||
$manifest_file = ide_framework_path('storage/rsx-build/manifest_data.php');
|
||||
if (!file_exists($manifest_file)) {
|
||||
error_response('Manifest not found - run php artisan rsx:manifest:build', 500);
|
||||
}
|
||||
|
||||
$manifest_raw = include $manifest_file;
|
||||
$manifest_data = $manifest_raw['data'] ?? $manifest_raw;
|
||||
$files = $manifest_data['files'] ?? [];
|
||||
|
||||
// Helper function to find PHP class in manifest
|
||||
$find_php_class = function($class_name) use ($files) {
|
||||
foreach ($files as $file_path => $file_data) {
|
||||
if (isset($file_data['class']) && $file_data['class'] === $class_name) {
|
||||
return $file_data;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Helper function to find view in manifest
|
||||
$find_view = function($view_name) use ($files) {
|
||||
foreach ($files as $file_path => $file_data) {
|
||||
if (isset($file_data['id']) && $file_data['id'] === $view_name) {
|
||||
return $file_data;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Helper function to convert PascalCase to snake_case
|
||||
$camel_to_snake = function($input) {
|
||||
$result = preg_replace('/(?<!^)[A-Z]/', '_$0', $input);
|
||||
return strtolower($result);
|
||||
};
|
||||
|
||||
// Helper function to convert snake_case to PascalCase
|
||||
$snake_to_pascal = function($input) {
|
||||
$parts = explode('_', $input);
|
||||
return implode('_', array_map('ucfirst', $parts));
|
||||
};
|
||||
|
||||
// Priority 1: Try as PHP class
|
||||
if (!$type || $type === 'class' || preg_match('/^[A-Z][A-Za-z0-9_]*$/', $identifier)) {
|
||||
$class_data = $find_php_class($identifier);
|
||||
|
||||
if ($class_data) {
|
||||
$file_path = $class_data['file'];
|
||||
$line_number = 1;
|
||||
$absolute_path = IDE_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
|
||||
if (isset($class_data['public_static_methods'][$method_name])) {
|
||||
$method_metadata = $class_data['public_static_methods'][$method_name];
|
||||
if (isset($method_metadata['line'])) {
|
||||
$line_number = $method_metadata['line'];
|
||||
}
|
||||
} else if (isset($class_data['public_instance_methods'][$method_name])) {
|
||||
$method_metadata = $class_data['public_instance_methods'][$method_name];
|
||||
if (isset($method_metadata['line'])) {
|
||||
$line_number = $method_metadata['line'];
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't have line number from metadata, search manually
|
||||
if ($line_number === 1 && !empty($lines)) {
|
||||
$in_class_or_trait = false;
|
||||
$brace_count = 0;
|
||||
|
||||
foreach ($lines as $index => $line) {
|
||||
if (preg_match('/^\s*(class|trait)\s+\w+(\s|$)/', $line)) {
|
||||
$in_class_or_trait = true;
|
||||
}
|
||||
|
||||
if ($in_class_or_trait) {
|
||||
$brace_count += substr_count($line, '{');
|
||||
$brace_count -= substr_count($line, '}');
|
||||
|
||||
if (preg_match('/^\s*(public|protected|private)?\s*(static\s+)?function\s+' . preg_quote($method_name, '/') . '\s*\(/', $line)) {
|
||||
$line_number = $index + 1;
|
||||
break;
|
||||
}
|
||||
|
||||
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' => $class_data['namespace'] ?? null,
|
||||
'extends' => $class_data['extends'] ?? null,
|
||||
'fqcn' => $class_data['fqcn'] ?? null,
|
||||
],
|
||||
];
|
||||
|
||||
if ($method_name) {
|
||||
$response_data['method'] = $method_name;
|
||||
}
|
||||
|
||||
json_response($response_data);
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 2: Try as RSX blade view
|
||||
if (!$type || $type === 'view' || !preg_match('/Controller$/', $identifier)) {
|
||||
$view_data = $find_view($identifier);
|
||||
|
||||
if ($view_data) {
|
||||
$file_path = $view_data['file'];
|
||||
$line_number = 1;
|
||||
$absolute_path = IDE_BASE_PATH . '/' . $file_path;
|
||||
|
||||
if (file_exists($absolute_path)) {
|
||||
$content = file_get_contents($absolute_path);
|
||||
$lines = explode("\n", $content);
|
||||
|
||||
// Look for @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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
json_response([
|
||||
'found' => true,
|
||||
'type' => 'view',
|
||||
'file' => $file_path,
|
||||
'line' => $line_number,
|
||||
'identifier' => $identifier,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 3: Try as bundle alias
|
||||
if (!$type || $type === 'bundle_alias' || preg_match('/^[a-z0-9]+$/', $identifier)) {
|
||||
$config_path = IDE_SYSTEM_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];
|
||||
|
||||
$class_parts = explode('\\', $bundle_class);
|
||||
$class_name = end($class_parts);
|
||||
|
||||
$class_data = $find_php_class($class_name);
|
||||
if ($class_data) {
|
||||
$file_path = $class_data['file'];
|
||||
$line_number = 1;
|
||||
$absolute_path = IDE_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
json_response([
|
||||
'found' => true,
|
||||
'type' => 'bundle_alias',
|
||||
'file' => $file_path,
|
||||
'line' => $line_number,
|
||||
'identifier' => $identifier,
|
||||
'resolved_class' => $bundle_class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 4: jqhtml template files
|
||||
if ($type === 'jqhtml_template') {
|
||||
$component_snake = $camel_to_snake($identifier);
|
||||
|
||||
foreach ($files as $file_path => $file_data) {
|
||||
if (str_ends_with($file_path, '.jqhtml')) {
|
||||
$basename = basename($file_path, '.jqhtml');
|
||||
|
||||
if ($basename === $component_snake || $snake_to_pascal($basename) === $identifier) {
|
||||
$line_number = 1;
|
||||
$absolute_path = IDE_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
json_response([
|
||||
'found' => true,
|
||||
'type' => 'jqhtml_template',
|
||||
'file' => $file_path,
|
||||
'line' => $line_number,
|
||||
'identifier' => $identifier,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 5: jqhtml JavaScript classes
|
||||
if ($type === 'jqhtml_class') {
|
||||
foreach ($files as $file_path => $file_data) {
|
||||
if (str_ends_with($file_path, '.js')) {
|
||||
$absolute_path = IDE_BASE_PATH . '/' . $file_path;
|
||||
|
||||
if (file_exists($absolute_path)) {
|
||||
$content = file_get_contents($absolute_path);
|
||||
|
||||
if (preg_match('/class\s+' . preg_quote($identifier, '/') . '\s+extends\s+[A-Za-z_]*Jqhtml_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;
|
||||
}
|
||||
}
|
||||
|
||||
json_response([
|
||||
'found' => true,
|
||||
'type' => 'jqhtml_class',
|
||||
'file' => $file_path,
|
||||
'line' => $line_number,
|
||||
'identifier' => $identifier,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 6: jqhtml class methods
|
||||
if ($type === 'jqhtml_class_method' && $method_name) {
|
||||
$search_method = ($method_name === 'data') ? 'on_load' : $method_name;
|
||||
|
||||
foreach ($files as $file_path => $file_data) {
|
||||
if (str_ends_with($file_path, '.js')) {
|
||||
$absolute_path = IDE_BASE_PATH . '/' . $file_path;
|
||||
|
||||
if (file_exists($absolute_path)) {
|
||||
$content = file_get_contents($absolute_path);
|
||||
|
||||
if (preg_match('/class\s+' . preg_quote($identifier, '/') . '\s+extends\s+[A-Za-z_]*Jqhtml_Component/', $content)) {
|
||||
$lines = explode("\n", $content);
|
||||
$line_number = 1;
|
||||
$in_class = false;
|
||||
$brace_count = 0;
|
||||
|
||||
foreach ($lines as $index => $line) {
|
||||
if (preg_match('/class\s+' . preg_quote($identifier, '/') . '\s+extends/', $line)) {
|
||||
$in_class = true;
|
||||
}
|
||||
|
||||
if ($in_class) {
|
||||
$brace_count += substr_count($line, '{');
|
||||
$brace_count -= substr_count($line, '}');
|
||||
|
||||
if (preg_match('/(?:async\s+)?' . preg_quote($search_method, '/') . '\s*\(/', $line)) {
|
||||
$line_number = $index + 1;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($brace_count === 0 && strpos($line, '}') !== false) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
json_response([
|
||||
'found' => true,
|
||||
'type' => 'jqhtml_class_method',
|
||||
'file' => $file_path,
|
||||
'line' => $line_number,
|
||||
'identifier' => $identifier,
|
||||
'method' => $method_name,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing found
|
||||
json_response([
|
||||
'found' => false,
|
||||
'error' => 'Identifier not found in manifest',
|
||||
'identifier' => $identifier,
|
||||
'searched_types' => ['class', 'view', 'bundle_alias', 'jqhtml_template', 'jqhtml_class', 'jqhtml_class_method'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle js_lineage service - get JavaScript class inheritance chain
|
||||
* This is a port of the Ide_Helper_Controller::js_lineage() method
|
||||
*/
|
||||
function handle_js_lineage_service($data) {
|
||||
$class_name = $data['class'] ?? null;
|
||||
|
||||
if (!$class_name) {
|
||||
json_response([
|
||||
'error' => 'Missing required parameter: class',
|
||||
], 400);
|
||||
}
|
||||
|
||||
// Load manifest data
|
||||
$manifest_file = ide_framework_path('storage/rsx-build/manifest_data.php');
|
||||
if (!file_exists($manifest_file)) {
|
||||
error_response('Manifest not found - run php artisan rsx:manifest:build', 500);
|
||||
}
|
||||
|
||||
$manifest_raw = include $manifest_file;
|
||||
$manifest_data = $manifest_raw['data'] ?? $manifest_raw;
|
||||
$files = $manifest_data['files'] ?? [];
|
||||
|
||||
// Find the JavaScript class and trace its lineage
|
||||
$lineage = [];
|
||||
$current_class = $class_name;
|
||||
|
||||
// Helper to find extends clause in JS file
|
||||
$find_extends = function($file_path) {
|
||||
$absolute_path = IDE_BASE_PATH . '/' . $file_path;
|
||||
if (file_exists($absolute_path)) {
|
||||
$content = file_get_contents($absolute_path);
|
||||
if (preg_match('/class\s+\w+\s+extends\s+([A-Za-z_][A-Za-z0-9_]*)/', $content, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Trace up to 10 levels to prevent infinite loops
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$found = false;
|
||||
|
||||
foreach ($files as $file_path => $file_data) {
|
||||
if (str_ends_with($file_path, '.js')) {
|
||||
$absolute_path = IDE_BASE_PATH . '/' . $file_path;
|
||||
|
||||
if (file_exists($absolute_path)) {
|
||||
$content = file_get_contents($absolute_path);
|
||||
|
||||
// Check if this file defines the current class
|
||||
if (preg_match('/class\s+' . preg_quote($current_class, '/') . '\s+extends\s+([A-Za-z_][A-Za-z0-9_]*)/', $content, $matches)) {
|
||||
$parent_class = $matches[1];
|
||||
$lineage[] = $parent_class;
|
||||
$current_class = $parent_class;
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$found) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
json_response([
|
||||
'class' => $class_name,
|
||||
'lineage' => $lineage,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle resolve_url service - resolve URL to controller/method
|
||||
* Takes a URL path and returns the controller and method that handles it
|
||||
*/
|
||||
function handle_resolve_url_service($data) {
|
||||
$url = $data['url'] ?? null;
|
||||
|
||||
if (!$url) {
|
||||
json_response([
|
||||
'error' => 'Missing required parameter: url',
|
||||
'found' => false,
|
||||
], 400);
|
||||
}
|
||||
|
||||
// Load manifest to get routes
|
||||
$manifest_file = ide_framework_path('storage/rsx-build/manifest_data.php');
|
||||
if (!file_exists($manifest_file)) {
|
||||
error_response('Manifest not found - run php artisan rsx:manifest:build', 500);
|
||||
}
|
||||
|
||||
$manifest_raw = include $manifest_file;
|
||||
$manifest_data = $manifest_raw['data'] ?? $manifest_raw;
|
||||
|
||||
// Get routes from manifest
|
||||
$routes = $manifest_data['php']['routes'] ?? [];
|
||||
|
||||
// Try to find matching route
|
||||
foreach ($routes as $route) {
|
||||
$pattern = $route['pattern'] ?? '';
|
||||
$controller = $route['class'] ?? '';
|
||||
$method = $route['method'] ?? '';
|
||||
|
||||
// Simple pattern matching - exact match for now
|
||||
// TODO: Support route parameters like /users/:id
|
||||
if ($pattern === $url) {
|
||||
json_response([
|
||||
'found' => true,
|
||||
'controller' => $controller,
|
||||
'method' => $method,
|
||||
'pattern' => $pattern,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Not found
|
||||
json_response([
|
||||
'found' => false,
|
||||
'url' => $url,
|
||||
]);
|
||||
}
|
||||
@@ -224,8 +224,8 @@ class Scss_BundleProcessor extends BundleProcessor_Abstract
|
||||
|
||||
// Try each variation
|
||||
foreach ($variations as $candidate) {
|
||||
$normalized = realpath($candidate);
|
||||
if ($normalized && file_exists($normalized)) {
|
||||
$normalized = rsxrealpath($candidate);
|
||||
if ($normalized) {
|
||||
return $normalized;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1243,3 +1243,68 @@ function text_to_html_with_whitespace(string $text): string
|
||||
// Join lines with <br>\n
|
||||
return implode("<br>\n", $processed_lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a path resolving . and .. components without resolving symlinks
|
||||
*
|
||||
* Unlike PHP's realpath(), this function normalizes paths by resolving . and ..
|
||||
* components but does NOT follow symlinks. This is important when working with
|
||||
* symlinked directories where you need the logical path, not the physical path.
|
||||
*
|
||||
* Behavior:
|
||||
* - Resolves . (current directory) and .. (parent directory) components
|
||||
* - Does NOT resolve symlinks (unlike realpath())
|
||||
* - Converts relative paths to absolute by prepending base_path()
|
||||
* - Returns normalized absolute path, or false if file doesn't exist
|
||||
*
|
||||
* Examples:
|
||||
* - rsxrealpath('/var/www/html/system/rsx/foo/../bar')
|
||||
* => '/var/www/html/system/rsx/bar'
|
||||
*
|
||||
* - rsxrealpath('rsx/foo/../bar')
|
||||
* => '/var/www/html/rsx/foo/../bar' (after base_path prepend)
|
||||
* => '/var/www/html/rsx/bar'
|
||||
*
|
||||
* - If /var/www/html/system/rsx is a symlink to /var/www/html/rsx:
|
||||
* rsxrealpath('/var/www/html/system/rsx/bar')
|
||||
* => '/var/www/html/system/rsx/bar' (keeps symlink path, unlike realpath)
|
||||
*
|
||||
* @param string $path The path to normalize
|
||||
* @return string|false The normalized absolute path, or false if file doesn't exist
|
||||
*/
|
||||
function rsxrealpath(string $path): string|false
|
||||
{
|
||||
// Convert relative path to absolute
|
||||
if (!str_starts_with($path, '/')) {
|
||||
$path = base_path() . '/' . $path;
|
||||
}
|
||||
|
||||
// Split path into components
|
||||
$parts = explode('/', $path);
|
||||
$result = [];
|
||||
|
||||
foreach ($parts as $part) {
|
||||
if ($part === '' || $part === '.') {
|
||||
// Skip empty parts and current directory references
|
||||
continue;
|
||||
} elseif ($part === '..') {
|
||||
// Go up one directory (remove last component)
|
||||
if (!empty($result)) {
|
||||
array_pop($result);
|
||||
}
|
||||
} else {
|
||||
// Regular path component
|
||||
$result[] = $part;
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild path with leading slash
|
||||
$normalized = '/' . implode('/', $result);
|
||||
|
||||
// Check if path exists (like realpath does)
|
||||
if (!file_exists($normalized)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
0
app/RSpade/resource/vscode_extension/out/auto_rename_provider.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/auto_rename_provider.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/auto_rename_provider.js.map
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/auto_rename_provider.js.map
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/blade_client.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/blade_client.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/blade_client.js.map
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/blade_client.js.map
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/blade_component_provider.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/blade_component_provider.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/blade_component_provider.js.map
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/blade_component_provider.js.map
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/blade_spacer.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/blade_spacer.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/blade_spacer.js.map
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/blade_spacer.js.map
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/class_refactor_code_actions.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/class_refactor_code_actions.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/class_refactor_code_actions.js.map
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/class_refactor_code_actions.js.map
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/class_refactor_provider.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/class_refactor_provider.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/class_refactor_provider.js.map
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/class_refactor_provider.js.map
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/config.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/config.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/config.js.map
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/config.js.map
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/convention_method_provider.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/convention_method_provider.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/convention_method_provider.js.map
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/convention_method_provider.js.map
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/debug_client.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/debug_client.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/debug_client.js.map
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/debug_client.js.map
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/decoration_provider.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/decoration_provider.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/decoration_provider.js.map
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/decoration_provider.js.map
Executable file → Normal file
51
app/RSpade/resource/vscode_extension/out/definition_provider.js
Executable file → Normal file
51
app/RSpade/resource/vscode_extension/out/definition_provider.js
Executable file → Normal file
@@ -36,16 +36,16 @@ class RspadeDefinitionProvider {
|
||||
this.jqhtml_api = jqhtml_api;
|
||||
}
|
||||
/**
|
||||
* Find the RSpade project root folder (contains app/RSpade/)
|
||||
* Find the RSpade project root folder (contains system/app/RSpade/)
|
||||
* Works in both single-folder and multi-root workspace modes
|
||||
*/
|
||||
find_rspade_root() {
|
||||
if (!vscode.workspace.workspaceFolders) {
|
||||
return undefined;
|
||||
}
|
||||
// Check each workspace folder for app/RSpade/
|
||||
// Check each workspace folder for system/app/RSpade/
|
||||
for (const folder of vscode.workspace.workspaceFolders) {
|
||||
const app_rspade = path.join(folder.uri.fsPath, 'app', 'RSpade');
|
||||
const app_rspade = path.join(folder.uri.fsPath, 'system', 'app', 'RSpade');
|
||||
if (fs.existsSync(app_rspade)) {
|
||||
return folder.uri.fsPath;
|
||||
}
|
||||
@@ -81,6 +81,13 @@ class RspadeDefinitionProvider {
|
||||
if (routeResult) {
|
||||
return routeResult;
|
||||
}
|
||||
// Check for href="/" pattern in Blade/Jqhtml files
|
||||
if (fileName.endsWith('.blade.php') || fileName.endsWith('.jqhtml')) {
|
||||
const hrefResult = await this.handleHrefPattern(document, position);
|
||||
if (hrefResult) {
|
||||
return hrefResult;
|
||||
}
|
||||
}
|
||||
// Handle "this.xxx" references in .jqhtml files (highest priority for jqhtml files)
|
||||
if (fileName.endsWith('.jqhtml')) {
|
||||
const thisResult = await this.handleThisReference(document, position);
|
||||
@@ -189,6 +196,37 @@ class RspadeDefinitionProvider {
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
/**
|
||||
* Handle href="/" pattern in Blade/Jqhtml files
|
||||
* Detects when cursor is on "/" within href attribute
|
||||
* Resolves to the controller action that handles the root URL
|
||||
*/
|
||||
async handleHrefPattern(document, position) {
|
||||
const line = document.lineAt(position.line).text;
|
||||
// Match href="/" or href='/'
|
||||
const hrefPattern = /href\s*=\s*(['"])\/\1/g;
|
||||
let match;
|
||||
while ((match = hrefPattern.exec(line)) !== null) {
|
||||
const matchStart = match.index + match[0].indexOf('/');
|
||||
const matchEnd = matchStart + 1; // Just the "/" character
|
||||
// Check if cursor is on the "/"
|
||||
if (position.character >= matchStart && position.character <= matchEnd) {
|
||||
try {
|
||||
// Query IDE bridge to resolve "/" URL to route
|
||||
const result = await this.ide_bridge.queryUrl('/');
|
||||
if (result && result.found && result.controller && result.method) {
|
||||
// Resolved to controller/method - navigate to it
|
||||
const phpResult = await this.queryIdeHelper(result.controller, result.method, 'class');
|
||||
return this.createLocationFromResult(phpResult);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error resolving href="/" to route:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
/**
|
||||
* Handle "this.xxx" references in .jqhtml files
|
||||
* Only handles patterns where cursor is on a word after "this."
|
||||
@@ -426,14 +464,9 @@ class RspadeDefinitionProvider {
|
||||
}
|
||||
if (isRsxView && stringContent) {
|
||||
// Query as a view
|
||||
try {
|
||||
const result = await this.queryIdeHelper(stringContent, undefined, 'view');
|
||||
return this.createLocationFromResult(result);
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error querying IDE helper for view:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
// If not in a string, check for class references (like in PHP files)
|
||||
// But skip this if we're inside a Route() call
|
||||
@@ -558,7 +591,7 @@ class RspadeDefinitionProvider {
|
||||
params.type = type;
|
||||
}
|
||||
try {
|
||||
const result = await this.ide_bridge.request('/_idehelper', params);
|
||||
const result = await this.ide_bridge.request('/_ide/service/resolve_class', params);
|
||||
return result;
|
||||
}
|
||||
catch (error) {
|
||||
|
||||
2
app/RSpade/resource/vscode_extension/out/definition_provider.js.map
Executable file → Normal file
2
app/RSpade/resource/vscode_extension/out/definition_provider.js.map
Executable file → Normal file
File diff suppressed because one or more lines are too long
0
app/RSpade/resource/vscode_extension/out/extension.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/extension.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/extension.js.map
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/extension.js.map
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/file_watcher.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/file_watcher.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/file_watcher.js.map
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/file_watcher.js.map
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/folder_color_provider.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/folder_color_provider.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/folder_color_provider.js.map
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/folder_color_provider.js.map
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/folding_provider.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/folding_provider.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/folding_provider.js.map
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/folding_provider.js.map
Executable file → Normal file
10
app/RSpade/resource/vscode_extension/out/formatting_provider.js
Executable file → Normal file
10
app/RSpade/resource/vscode_extension/out/formatting_provider.js
Executable file → Normal file
@@ -6,7 +6,7 @@
|
||||
* All formatting is performed on the server - no local PHP execution.
|
||||
*
|
||||
* Authentication Flow:
|
||||
* 1. Reads domain from storage/rsx-ide-bridge/domain.txt (auto-discovered)
|
||||
* 1. Reads domain from system/storage/rsx-ide-bridge/domain.txt (auto-discovered)
|
||||
* 2. Creates session with auth tokens on first use
|
||||
* 3. Signs all requests with SHA1(body + client_key)
|
||||
* 4. Validates server responses with SHA1(body + server_key)
|
||||
@@ -286,7 +286,7 @@ class RspadeFormattingProvider {
|
||||
// Try new structure first
|
||||
const system_app_rspade = path.join(folder.uri.fsPath, 'system', 'app', 'RSpade');
|
||||
if (fs.existsSync(system_app_rspade)) {
|
||||
rspade_root = path.join(folder.uri.fsPath, 'system');
|
||||
rspade_root = folder.uri.fsPath; // Project root, not system subdirectory
|
||||
break;
|
||||
}
|
||||
// Fall back to legacy structure
|
||||
@@ -301,7 +301,7 @@ class RspadeFormattingProvider {
|
||||
this.output_channel.appendLine('ERROR: RSpade project root not found');
|
||||
throw new Error('RSpade project root not found');
|
||||
}
|
||||
const domain_file = path.join(rspade_root, 'storage', 'rsx-ide-bridge', 'domain.txt');
|
||||
const domain_file = path.join(rspade_root, 'system', 'storage', 'rsx-ide-bridge', 'domain.txt');
|
||||
this.output_channel.appendLine(`Checking for domain file: ${domain_file}`);
|
||||
if (await exists(domain_file)) {
|
||||
const domain = await read_file(domain_file, 'utf8');
|
||||
@@ -317,14 +317,14 @@ class RspadeFormattingProvider {
|
||||
this.output_channel.appendLine('\nThe extension needs to know your development server URL.');
|
||||
this.output_channel.appendLine('\nPlease do ONE of the following:\n');
|
||||
this.output_channel.appendLine('1. Load your site in a web browser');
|
||||
this.output_channel.appendLine(' This will auto-create: storage/rsx-ide-bridge/domain.txt\n');
|
||||
this.output_channel.appendLine(' This will auto-create: system/storage/rsx-ide-bridge/domain.txt\n');
|
||||
this.output_channel.appendLine('2. Set VS Code setting: rspade.serverUrl');
|
||||
this.output_channel.appendLine(' File → Preferences → Settings → Search "rspade"');
|
||||
this.output_channel.appendLine(' Set to your development URL (e.g., https://myapp.test)\n');
|
||||
this.output_channel.appendLine('3. Enable IDE integration in Laravel config');
|
||||
this.output_channel.appendLine(' In config/rsx.php, set ide_integration.enabled = true');
|
||||
this.output_channel.appendLine('\n════════════════════════════════════════════════════════');
|
||||
throw new Error('RSpade: storage/rsx-ide-bridge/domain.txt not found. Please load site in browser or configure server URL.');
|
||||
throw new Error('RSpade: system/storage/rsx-ide-bridge/domain.txt not found. Please load site in browser or configure server URL.');
|
||||
}
|
||||
async make_authenticated_request(endpoint, data, retry_count = 0) {
|
||||
this.output_channel.appendLine(`\n--- AUTHENTICATED REQUEST ${retry_count > 0 ? '(RETRY)' : ''} ---`);
|
||||
|
||||
2
app/RSpade/resource/vscode_extension/out/formatting_provider.js.map
Executable file → Normal file
2
app/RSpade/resource/vscode_extension/out/formatting_provider.js.map
Executable file → Normal file
File diff suppressed because one or more lines are too long
4
app/RSpade/resource/vscode_extension/out/git_diff_provider.js
Executable file → Normal file
4
app/RSpade/resource/vscode_extension/out/git_diff_provider.js
Executable file → Normal file
@@ -231,7 +231,7 @@ class GitDiffProvider {
|
||||
this.server_url = await this.negotiate_protocol(configured_url);
|
||||
return this.server_url;
|
||||
}
|
||||
const domain_file = path.join(this.rspade_root, 'storage', 'rsx-ide-bridge', 'domain.txt');
|
||||
const domain_file = path.join(this.rspade_root, 'system', 'storage', 'rsx-ide-bridge', 'domain.txt');
|
||||
if (fs.existsSync(domain_file)) {
|
||||
const domain = fs.readFileSync(domain_file, 'utf8').trim();
|
||||
this.server_url = await this.negotiate_protocol(domain);
|
||||
@@ -329,7 +329,7 @@ class GitDiffProvider {
|
||||
if (!this.rspade_root) {
|
||||
return;
|
||||
}
|
||||
const domain_file = path.join(this.rspade_root, 'storage', 'rsx-ide-bridge', 'domain.txt');
|
||||
const domain_file = path.join(this.rspade_root, 'system', 'storage', 'rsx-ide-bridge', 'domain.txt');
|
||||
const domain_dir = path.dirname(domain_file);
|
||||
// Watch the directory (file might not exist yet)
|
||||
if (fs.existsSync(domain_dir)) {
|
||||
|
||||
2
app/RSpade/resource/vscode_extension/out/git_diff_provider.js.map
Executable file → Normal file
2
app/RSpade/resource/vscode_extension/out/git_diff_provider.js.map
Executable file → Normal file
File diff suppressed because one or more lines are too long
4
app/RSpade/resource/vscode_extension/out/git_status_provider.js
Executable file → Normal file
4
app/RSpade/resource/vscode_extension/out/git_status_provider.js
Executable file → Normal file
@@ -129,7 +129,7 @@ class GitStatusProvider {
|
||||
return this.server_url;
|
||||
}
|
||||
// Try to auto-discover from domain.txt
|
||||
const domain_file = path.join(this.rspade_root, 'storage', 'rsx-ide-bridge', 'domain.txt');
|
||||
const domain_file = path.join(this.rspade_root, 'system', 'storage', 'rsx-ide-bridge', 'domain.txt');
|
||||
if (fs.existsSync(domain_file)) {
|
||||
const domain = fs.readFileSync(domain_file, 'utf8').trim();
|
||||
this.server_url = await this.negotiate_protocol(domain);
|
||||
@@ -227,7 +227,7 @@ class GitStatusProvider {
|
||||
if (!this.rspade_root) {
|
||||
return;
|
||||
}
|
||||
const domain_file = path.join(this.rspade_root, 'storage', 'rsx-ide-bridge', 'domain.txt');
|
||||
const domain_file = path.join(this.rspade_root, 'system', 'storage', 'rsx-ide-bridge', 'domain.txt');
|
||||
const domain_dir = path.dirname(domain_file);
|
||||
// Watch the directory (file might not exist yet)
|
||||
if (fs.existsSync(domain_dir)) {
|
||||
|
||||
2
app/RSpade/resource/vscode_extension/out/git_status_provider.js.map
Executable file → Normal file
2
app/RSpade/resource/vscode_extension/out/git_status_provider.js.map
Executable file → Normal file
File diff suppressed because one or more lines are too long
23
app/RSpade/resource/vscode_extension/out/ide_bridge_client.js
Executable file → Normal file
23
app/RSpade/resource/vscode_extension/out/ide_bridge_client.js
Executable file → Normal file
@@ -11,7 +11,7 @@
|
||||
* 4. Auto-retries with refreshed URL on connection failure
|
||||
*
|
||||
* AUTHENTICATION FLOW:
|
||||
* 1. Client requests session from /_idehelper/auth/create
|
||||
* 1. Client requests session from /_ide/service/auth/create
|
||||
* 2. Server generates session ID, client_key, server_key
|
||||
* 3. Client signs requests: SHA1(body + client_key)
|
||||
* 4. Server validates signature and signs response: SHA1(body + server_key)
|
||||
@@ -32,7 +32,7 @@
|
||||
* const client = new IdeBridgeClient(output_channel);
|
||||
*
|
||||
* // Make request to IDE helper endpoint
|
||||
* const response = await client.request('/_idehelper/your_endpoint', {
|
||||
* const response = await client.request('/_ide/service/your_endpoint', {
|
||||
* param1: 'value1',
|
||||
* param2: 'value2'
|
||||
* });
|
||||
@@ -49,10 +49,10 @@
|
||||
* ADDING NEW IDE HELPER ENDPOINTS:
|
||||
*
|
||||
* Backend (PHP):
|
||||
* 1. Add method to /app/RSpade/Ide/Helper/Ide_Helper_Controller.php
|
||||
* 2. Register route in /routes/web.php:
|
||||
* Route::get('/_idehelper/your_endpoint', [Ide_Helper_Controller::class, 'your_method']);
|
||||
* 3. Return JsonResponse with data
|
||||
* 1. Add handler function to /app/RSpade/Ide/Services/handler.php
|
||||
* 2. Add case to switch statement in handler.php:
|
||||
* case 'your_endpoint': handle_your_endpoint_service($request_data); break;
|
||||
* 3. Return JSON via json_response() helper
|
||||
*
|
||||
* Frontend (TypeScript):
|
||||
* 1. Use IdeBridgeClient.request() to call endpoint
|
||||
@@ -119,6 +119,15 @@ class IdeBridgeClient {
|
||||
this.output_channel.appendLine(`Time: ${new Date().toISOString()}`);
|
||||
return this.make_request_with_retry(endpoint, data, method, 0);
|
||||
}
|
||||
/**
|
||||
* Query URL to resolve it to a route (controller/method)
|
||||
*
|
||||
* @param url The URL path to resolve (e.g., "/", "/users/123")
|
||||
* @returns Promise with { found: boolean, controller?: string, method?: string }
|
||||
*/
|
||||
async queryUrl(url) {
|
||||
return this.request('/_ide/service/resolve_url', { url }, 'GET');
|
||||
}
|
||||
async make_request_with_retry(endpoint, data, method, retry_count) {
|
||||
if (retry_count > 0) {
|
||||
this.output_channel.appendLine(`\n--- RETRY ATTEMPT ${retry_count} ---`);
|
||||
@@ -279,7 +288,7 @@ class IdeBridgeClient {
|
||||
this.output_channel.appendLine('\n--- AUTHENTICATION ---');
|
||||
this.output_channel.appendLine('Creating new auth session...');
|
||||
// Request new session (this endpoint doesn't require auth)
|
||||
const response = await this.make_http_request('/auth/create', {}, 'POST', false);
|
||||
const response = await this.make_http_request('/_ide/service/auth/create', {}, 'POST', false);
|
||||
if (!response.success) {
|
||||
throw new Error('Failed to create auth session');
|
||||
}
|
||||
|
||||
2
app/RSpade/resource/vscode_extension/out/ide_bridge_client.js.map
Executable file → Normal file
2
app/RSpade/resource/vscode_extension/out/ide_bridge_client.js.map
Executable file → Normal file
File diff suppressed because one or more lines are too long
2
app/RSpade/resource/vscode_extension/out/jqhtml_lifecycle_provider.js
Executable file → Normal file
2
app/RSpade/resource/vscode_extension/out/jqhtml_lifecycle_provider.js
Executable file → Normal file
@@ -77,7 +77,7 @@ async function get_js_lineage(class_name) {
|
||||
ide_bridge_client = new ide_bridge_client_1.IdeBridgeClient(output_channel);
|
||||
}
|
||||
try {
|
||||
const response = await ide_bridge_client.request('/_idehelper/js_lineage', { class: class_name });
|
||||
const response = await ide_bridge_client.request('/_ide/service/js_lineage', { class: class_name });
|
||||
const lineage = response.lineage || [];
|
||||
// Cache the result
|
||||
lineage_cache.set(class_name, lineage);
|
||||
|
||||
2
app/RSpade/resource/vscode_extension/out/jqhtml_lifecycle_provider.js.map
Executable file → Normal file
2
app/RSpade/resource/vscode_extension/out/jqhtml_lifecycle_provider.js.map
Executable file → Normal file
File diff suppressed because one or more lines are too long
0
app/RSpade/resource/vscode_extension/out/laravel_completion_provider.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/laravel_completion_provider.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/laravel_completion_provider.js.map
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/laravel_completion_provider.js.map
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/php_attribute_provider.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/php_attribute_provider.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/php_attribute_provider.js.map
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/php_attribute_provider.js.map
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/refactor_code_actions.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/refactor_code_actions.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/refactor_code_actions.js.map
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/refactor_code_actions.js.map
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/refactor_provider.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/refactor_provider.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/refactor_provider.js.map
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/refactor_provider.js.map
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/sort_class_methods_provider.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/sort_class_methods_provider.js
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/sort_class_methods_provider.js.map
Executable file → Normal file
0
app/RSpade/resource/vscode_extension/out/sort_class_methods_provider.js.map
Executable file → Normal file
@@ -2,7 +2,7 @@
|
||||
"name": "rspade-framework",
|
||||
"displayName": "RSpade Framework Support",
|
||||
"description": "VS Code extension for RSpade framework with code folding, formatting, and namespace management",
|
||||
"version": "0.1.170",
|
||||
"version": "0.1.182",
|
||||
"publisher": "rspade",
|
||||
"engines": {
|
||||
"vscode": "^1.74.0"
|
||||
@@ -13,7 +13,7 @@
|
||||
"Other"
|
||||
],
|
||||
"activationEvents": [
|
||||
"workspaceContains:**/app/RSpade/**"
|
||||
"onStartupFinished"
|
||||
],
|
||||
"main": "./out/extension.js",
|
||||
"contributes": {
|
||||
|
||||
Binary file not shown.
@@ -28,7 +28,7 @@ export class RspadeDefinitionProvider implements vscode.DefinitionProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the RSpade project root folder (contains app/RSpade/)
|
||||
* Find the RSpade project root folder (contains system/app/RSpade/)
|
||||
* Works in both single-folder and multi-root workspace modes
|
||||
*/
|
||||
private find_rspade_root(): string | undefined {
|
||||
@@ -36,9 +36,9 @@ export class RspadeDefinitionProvider implements vscode.DefinitionProvider {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Check each workspace folder for app/RSpade/
|
||||
// Check each workspace folder for system/app/RSpade/
|
||||
for (const folder of vscode.workspace.workspaceFolders) {
|
||||
const app_rspade = path.join(folder.uri.fsPath, 'app', 'RSpade');
|
||||
const app_rspade = path.join(folder.uri.fsPath, 'system', 'app', 'RSpade');
|
||||
if (fs.existsSync(app_rspade)) {
|
||||
return folder.uri.fsPath;
|
||||
}
|
||||
@@ -86,6 +86,14 @@ export class RspadeDefinitionProvider implements vscode.DefinitionProvider {
|
||||
return routeResult;
|
||||
}
|
||||
|
||||
// Check for href="/" pattern in Blade/Jqhtml files
|
||||
if (fileName.endsWith('.blade.php') || fileName.endsWith('.jqhtml')) {
|
||||
const hrefResult = await this.handleHrefPattern(document, position);
|
||||
if (hrefResult) {
|
||||
return hrefResult;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle "this.xxx" references in .jqhtml files (highest priority for jqhtml files)
|
||||
if (fileName.endsWith('.jqhtml')) {
|
||||
const thisResult = await this.handleThisReference(document, position);
|
||||
@@ -206,6 +214,45 @@ export class RspadeDefinitionProvider implements vscode.DefinitionProvider {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle href="/" pattern in Blade/Jqhtml files
|
||||
* Detects when cursor is on "/" within href attribute
|
||||
* Resolves to the controller action that handles the root URL
|
||||
*/
|
||||
private async handleHrefPattern(
|
||||
document: vscode.TextDocument,
|
||||
position: vscode.Position
|
||||
): Promise<vscode.Definition | undefined> {
|
||||
const line = document.lineAt(position.line).text;
|
||||
|
||||
// Match href="/" or href='/'
|
||||
const hrefPattern = /href\s*=\s*(['"])\/\1/g;
|
||||
let match;
|
||||
|
||||
while ((match = hrefPattern.exec(line)) !== null) {
|
||||
const matchStart = match.index + match[0].indexOf('/');
|
||||
const matchEnd = matchStart + 1; // Just the "/" character
|
||||
|
||||
// Check if cursor is on the "/"
|
||||
if (position.character >= matchStart && position.character <= matchEnd) {
|
||||
try {
|
||||
// Query IDE bridge to resolve "/" URL to route
|
||||
const result = await this.ide_bridge.queryUrl('/');
|
||||
|
||||
if (result && result.found && result.controller && result.method) {
|
||||
// Resolved to controller/method - navigate to it
|
||||
const phpResult = await this.queryIdeHelper(result.controller, result.method, 'class');
|
||||
return this.createLocationFromResult(phpResult);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error resolving href="/" to route:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle "this.xxx" references in .jqhtml files
|
||||
* Only handles patterns where cursor is on a word after "this."
|
||||
@@ -488,12 +535,8 @@ export class RspadeDefinitionProvider implements vscode.DefinitionProvider {
|
||||
|
||||
if (isRsxView && stringContent) {
|
||||
// Query as a view
|
||||
try {
|
||||
const result = await this.queryIdeHelper(stringContent, undefined, 'view');
|
||||
return this.createLocationFromResult(result);
|
||||
} catch (error) {
|
||||
console.error('Error querying IDE helper for view:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -641,7 +684,7 @@ export class RspadeDefinitionProvider implements vscode.DefinitionProvider {
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.ide_bridge.request('/_idehelper', params);
|
||||
const result = await this.ide_bridge.request('/_ide/service/resolve_class', params);
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
this.show_error_status('IDE helper request failed');
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* All formatting is performed on the server - no local PHP execution.
|
||||
*
|
||||
* Authentication Flow:
|
||||
* 1. Reads domain from storage/rsx-ide-bridge/domain.txt (auto-discovered)
|
||||
* 1. Reads domain from system/storage/rsx-ide-bridge/domain.txt (auto-discovered)
|
||||
* 2. Creates session with auth tokens on first use
|
||||
* 3. Signs all requests with SHA1(body + client_key)
|
||||
* 4. Validates server responses with SHA1(body + server_key)
|
||||
@@ -321,7 +321,7 @@ export class RspadeFormattingProvider implements vscode.DocumentFormattingEditPr
|
||||
// Try new structure first
|
||||
const system_app_rspade = path.join(folder.uri.fsPath, 'system', 'app', 'RSpade');
|
||||
if (fs.existsSync(system_app_rspade)) {
|
||||
rspade_root = path.join(folder.uri.fsPath, 'system');
|
||||
rspade_root = folder.uri.fsPath; // Project root, not system subdirectory
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -339,7 +339,7 @@ export class RspadeFormattingProvider implements vscode.DocumentFormattingEditPr
|
||||
throw new Error('RSpade project root not found');
|
||||
}
|
||||
|
||||
const domain_file = path.join(rspade_root, 'storage', 'rsx-ide-bridge', 'domain.txt');
|
||||
const domain_file = path.join(rspade_root, 'system', 'storage', 'rsx-ide-bridge', 'domain.txt');
|
||||
this.output_channel.appendLine(`Checking for domain file: ${domain_file}`);
|
||||
|
||||
if (await exists(domain_file)) {
|
||||
@@ -357,7 +357,7 @@ export class RspadeFormattingProvider implements vscode.DocumentFormattingEditPr
|
||||
this.output_channel.appendLine('\nThe extension needs to know your development server URL.');
|
||||
this.output_channel.appendLine('\nPlease do ONE of the following:\n');
|
||||
this.output_channel.appendLine('1. Load your site in a web browser');
|
||||
this.output_channel.appendLine(' This will auto-create: storage/rsx-ide-bridge/domain.txt\n');
|
||||
this.output_channel.appendLine(' This will auto-create: system/storage/rsx-ide-bridge/domain.txt\n');
|
||||
this.output_channel.appendLine('2. Set VS Code setting: rspade.serverUrl');
|
||||
this.output_channel.appendLine(' File → Preferences → Settings → Search "rspade"');
|
||||
this.output_channel.appendLine(' Set to your development URL (e.g., https://myapp.test)\n');
|
||||
@@ -365,7 +365,7 @@ export class RspadeFormattingProvider implements vscode.DocumentFormattingEditPr
|
||||
this.output_channel.appendLine(' In config/rsx.php, set ide_integration.enabled = true');
|
||||
this.output_channel.appendLine('\n════════════════════════════════════════════════════════');
|
||||
|
||||
throw new Error('RSpade: storage/rsx-ide-bridge/domain.txt not found. Please load site in browser or configure server URL.');
|
||||
throw new Error('RSpade: system/storage/rsx-ide-bridge/domain.txt not found. Please load site in browser or configure server URL.');
|
||||
}
|
||||
|
||||
private async make_authenticated_request(
|
||||
|
||||
@@ -265,7 +265,7 @@ export class GitDiffProvider {
|
||||
return this.server_url;
|
||||
}
|
||||
|
||||
const domain_file = path.join(this.rspade_root!, 'storage', 'rsx-ide-bridge', 'domain.txt');
|
||||
const domain_file = path.join(this.rspade_root!, 'system', 'storage', 'rsx-ide-bridge', 'domain.txt');
|
||||
|
||||
if (fs.existsSync(domain_file)) {
|
||||
const domain = fs.readFileSync(domain_file, 'utf8').trim();
|
||||
@@ -380,7 +380,7 @@ export class GitDiffProvider {
|
||||
return;
|
||||
}
|
||||
|
||||
const domain_file = path.join(this.rspade_root, 'storage', 'rsx-ide-bridge', 'domain.txt');
|
||||
const domain_file = path.join(this.rspade_root, 'system', 'storage', 'rsx-ide-bridge', 'domain.txt');
|
||||
const domain_dir = path.dirname(domain_file);
|
||||
|
||||
// Watch the directory (file might not exist yet)
|
||||
|
||||
@@ -134,7 +134,7 @@ export class GitStatusProvider implements vscode.FileDecorationProvider {
|
||||
}
|
||||
|
||||
// Try to auto-discover from domain.txt
|
||||
const domain_file = path.join(this.rspade_root!, 'storage', 'rsx-ide-bridge', 'domain.txt');
|
||||
const domain_file = path.join(this.rspade_root!, 'system', 'storage', 'rsx-ide-bridge', 'domain.txt');
|
||||
|
||||
if (fs.existsSync(domain_file)) {
|
||||
const domain = fs.readFileSync(domain_file, 'utf8').trim();
|
||||
@@ -249,7 +249,7 @@ export class GitStatusProvider implements vscode.FileDecorationProvider {
|
||||
return;
|
||||
}
|
||||
|
||||
const domain_file = path.join(this.rspade_root, 'storage', 'rsx-ide-bridge', 'domain.txt');
|
||||
const domain_file = path.join(this.rspade_root, 'system', 'storage', 'rsx-ide-bridge', 'domain.txt');
|
||||
const domain_dir = path.dirname(domain_file);
|
||||
|
||||
// Watch the directory (file might not exist yet)
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* 4. Auto-retries with refreshed URL on connection failure
|
||||
*
|
||||
* AUTHENTICATION FLOW:
|
||||
* 1. Client requests session from /_idehelper/auth/create
|
||||
* 1. Client requests session from /_ide/service/auth/create
|
||||
* 2. Server generates session ID, client_key, server_key
|
||||
* 3. Client signs requests: SHA1(body + client_key)
|
||||
* 4. Server validates signature and signs response: SHA1(body + server_key)
|
||||
@@ -31,7 +31,7 @@
|
||||
* const client = new IdeBridgeClient(output_channel);
|
||||
*
|
||||
* // Make request to IDE helper endpoint
|
||||
* const response = await client.request('/_idehelper/your_endpoint', {
|
||||
* const response = await client.request('/_ide/service/your_endpoint', {
|
||||
* param1: 'value1',
|
||||
* param2: 'value2'
|
||||
* });
|
||||
@@ -48,10 +48,10 @@
|
||||
* ADDING NEW IDE HELPER ENDPOINTS:
|
||||
*
|
||||
* Backend (PHP):
|
||||
* 1. Add method to /app/RSpade/Ide/Helper/Ide_Helper_Controller.php
|
||||
* 2. Register route in /routes/web.php:
|
||||
* Route::get('/_idehelper/your_endpoint', [Ide_Helper_Controller::class, 'your_method']);
|
||||
* 3. Return JsonResponse with data
|
||||
* 1. Add handler function to /app/RSpade/Ide/Services/handler.php
|
||||
* 2. Add case to switch statement in handler.php:
|
||||
* case 'your_endpoint': handle_your_endpoint_service($request_data); break;
|
||||
* 3. Return JSON via json_response() helper
|
||||
*
|
||||
* Frontend (TypeScript):
|
||||
* 1. Use IdeBridgeClient.request() to call endpoint
|
||||
@@ -109,6 +109,16 @@ export class IdeBridgeClient {
|
||||
return this.make_request_with_retry(endpoint, data, method, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query URL to resolve it to a route (controller/method)
|
||||
*
|
||||
* @param url The URL path to resolve (e.g., "/", "/users/123")
|
||||
* @returns Promise with { found: boolean, controller?: string, method?: string }
|
||||
*/
|
||||
public async queryUrl(url: string): Promise<{ found: boolean; controller?: string; method?: string }> {
|
||||
return this.request('/_ide/service/resolve_url', { url }, 'GET');
|
||||
}
|
||||
|
||||
private async make_request_with_retry(
|
||||
endpoint: string,
|
||||
data: any,
|
||||
@@ -309,7 +319,7 @@ export class IdeBridgeClient {
|
||||
this.output_channel.appendLine('Creating new auth session...');
|
||||
|
||||
// Request new session (this endpoint doesn't require auth)
|
||||
const response = await this.make_http_request('/auth/create', {}, 'POST', false);
|
||||
const response = await this.make_http_request('/_ide/service/auth/create', {}, 'POST', false);
|
||||
|
||||
if (!response.success) {
|
||||
throw new Error('Failed to create auth session');
|
||||
|
||||
@@ -59,7 +59,7 @@ async function get_js_lineage(class_name: string): Promise<string[]> {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await ide_bridge_client.request('/_idehelper/js_lineage', { class: class_name });
|
||||
const response = await ide_bridge_client.request('/_ide/service/js_lineage', { class: class_name });
|
||||
const lineage = response.lineage || [];
|
||||
|
||||
// Cache the result
|
||||
|
||||
@@ -297,7 +297,7 @@ return [
|
||||
'desktop.ini', // Windows folder settings
|
||||
'.gitkeep', // Git empty directory markers
|
||||
'.gitattributes', // Git attributes config
|
||||
'_rsx_helper.php', // IDE helper stubs (auto-generated)
|
||||
'._rsx_helper.php', // IDE helper stubs (auto-generated)
|
||||
],
|
||||
],
|
||||
|
||||
@@ -345,7 +345,7 @@ return [
|
||||
'webpack.config.js',
|
||||
'webpack.mix.js',
|
||||
'_ide_helper.php', // Laravel IDE Helper
|
||||
'_rsx_helper.php', // RSX IDE Helper
|
||||
'._rsx_helper.php', // RSX IDE Helper
|
||||
'.phpstorm.meta.php', // PhpStorm metadata
|
||||
],
|
||||
|
||||
|
||||
@@ -155,6 +155,10 @@ is this?", you must never commit to the framework repo.
|
||||
**Framework repo:** `/var/www/html/.git` (read-only, managed by RSpade team - DO NOT TOUCH)
|
||||
**Application repo:** `/var/www/html/rsx/.git` (your code, you control)
|
||||
|
||||
### Commit Discipline
|
||||
|
||||
**NEVER commit unless explicitly asked** - You are UNQUALIFIED to decide when to commit. ONLY commit when the user explicitly says "commit" or gives a clear instruction to commit. Commits are MAJOR MILESTONES (like completing all history homework), NOT individual changes (like changing one answer on one assignment). Wait for the user to tell you when to commit.
|
||||
|
||||
### Working Directory Rules
|
||||
|
||||
**ALWAYS work from `/var/www/html/rsx` for application code:**
|
||||
@@ -2451,6 +2455,8 @@ php artisan rsx:refactor:rename_php_class_function Class old new
|
||||
php artisan rsx:refactor:sort_php_class_functions rsx/path/to/class.php
|
||||
```
|
||||
|
||||
**#[Instantiatable]**: Applied to abstract classes to whitelist all child classes for instantiation by the framework.
|
||||
|
||||
---
|
||||
|
||||
## RSX:MAN DOCUMENTATION
|
||||
|
||||
@@ -15,7 +15,5 @@ Route::get('/test-bundle-facade', function() {
|
||||
// All RSX routes are handled through the 404 exception handler
|
||||
// This allows Laravel routes to have priority
|
||||
|
||||
// IDE Helper endpoints - provide symbol resolution for VS Code extension
|
||||
// These use the traditional Laravel controller for complex manifest lookups
|
||||
Route::get('/_idehelper', [\App\RSpade\Ide\Helper\Ide_Helper_Controller::class, 'resolve_class']);
|
||||
Route::get('/_idehelper/js_lineage', [\App\RSpade\Ide\Helper\Ide_Helper_Controller::class, 'js_lineage']);
|
||||
// Note: IDE Helper endpoints have been migrated to /_ide/service/* (standalone handler.php)
|
||||
// This provides better performance by bypassing Laravel for IDE integration requests
|
||||
Reference in New Issue
Block a user