diff --git a/.github/workflows/magento-compatibility.yml b/.github/workflows/magento-compatibility.yml index c4edaf9..f695098 100644 --- a/.github/workflows/magento-compatibility.yml +++ b/.github/workflows/magento-compatibility.yml @@ -134,6 +134,19 @@ jobs: echo "Test MageForge Theme List command:" bin/magento mageforge:theme:list + echo "Test MageForge Hyvä Compatibility Check command:" + bin/magento mageforge:hyva:compatibility:check --help + bin/magento m:h:c:c --help + + echo "Test MageForge Hyvä Compatibility Check - Show all modules:" + bin/magento mageforge:hyva:compatibility:check --show-all + + echo "Test MageForge Hyvä Compatibility Check - Third party only:" + bin/magento m:h:c:c --third-party-only + + echo "Test MageForge Hyvä Compatibility Check - Detailed output:" + bin/magento m:h:c:c --show-all --detailed + - name: Test Summary run: | echo "MageForge module compatibility test with Magento ${{ matrix.magento-version }} completed" @@ -255,6 +268,19 @@ jobs: echo "Test MageForge Theme List command:" bin/magento mageforge:theme:list + echo "Test MageForge Hyvä Compatibility Check command:" + bin/magento mageforge:hyva:compatibility:check --help + bin/magento m:h:c:c --help + + echo "Test MageForge Hyvä Compatibility Check - Show all modules:" + bin/magento mageforge:hyva:compatibility:check --show-all + + echo "Test MageForge Hyvä Compatibility Check - Third party only:" + bin/magento m:h:c:c --third-party-only + + echo "Test MageForge Hyvä Compatibility Check - Detailed output:" + bin/magento m:h:c:c --show-all --detailed + - name: Test Summary run: | echo "MageForge module compatibility test with Magento 2.4.8 completed" diff --git a/CHANGELOG.md b/CHANGELOG.md index f8795ee..427dcdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ All notable changes to this project will be documented in this file. ## UNRELEASED +### Added + +- feat: add Hyvä compatibility checker command (`mageforge:hyva:compatibility:check`) + - Scans Magento modules for Hyvä theme compatibility issues + - Detects RequireJS, Knockout.js, jQuery, and UI Components usage + - Interactive menu with Laravel Prompts for scan options + - Options: `--show-all`, `--third-party-only`, `--include-vendor`, `--detailed` + - Color-coded output (✓ Compatible, ⚠ Warnings, ✗ Incompatible) + - Detailed file-level issues with line numbers + - Exit code 1 for critical issues, 0 for success + - Command aliases: `m:h:c:c`, `hyva:check` + ## Latest Release ### [0.2.2] - 2025-06-05 diff --git a/docs/commands.md b/docs/commands.md index c4301e7..3ce7099 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -19,14 +19,17 @@ All commands in MageForge follow a consistent structure based on Symfony's Conso **File**: `/src/Console/Command/ListThemeCommand.php` **Dependencies**: + - `ThemeList` - Service to retrieve theme information **Usage**: + ```bash bin/magento mageforge:theme:list ``` **Implementation Details**: + - Retrieves all themes from the `ThemeList` service - Displays a formatted table with theme information (code, title, path) - Returns success status code @@ -40,16 +43,19 @@ bin/magento mageforge:theme:list **File**: `/src/Console/Command/BuildThemeCommand.php` **Dependencies**: + - `ThemePath` - Service to resolve theme paths - `ThemeList` - Service to retrieve theme information - `BuilderPool` - Service to get appropriate builders for themes **Usage**: + ```bash bin/magento mageforge:theme:build [...] ``` **Implementation Details**: + - If no theme codes are provided, displays an interactive prompt to select themes - For each selected theme: 1. Resolves the theme path @@ -67,16 +73,19 @@ bin/magento mageforge:theme:build [...] **File**: `/src/Console/Command/ThemeWatchCommand.php` **Dependencies**: + - `BuilderPool` - Service to get appropriate builders for themes - `ThemeList` - Service to retrieve theme information - `ThemePath` - Service to resolve theme paths **Usage**: + ```bash bin/magento mageforge:theme:watch [--theme=THEME] ``` **Implementation Details**: + - If no theme code is provided, displays an interactive prompt to select a theme - Resolves the theme path - Determines the appropriate builder for the theme type @@ -92,15 +101,18 @@ bin/magento mageforge:theme:watch [--theme=THEME] **File**: `/src/Console/Command/SystemCheckCommand.php` **Dependencies**: + - `ProductMetadataInterface` - For retrieving Magento version - `Escaper` - For HTML escaping output **Usage**: + ```bash bin/magento mageforge:system:check ``` **Implementation Details**: + - Retrieves and displays: - PHP version - Node.js version (with comparison to latest LTS) @@ -118,35 +130,176 @@ bin/magento mageforge:system:check **File**: `/src/Console/Command/VersionCommand.php` **Dependencies**: + - `File` - Filesystem driver for reading files **Usage**: + ```bash bin/magento mageforge:version ``` **Implementation Details**: + - Reads the current module version from `composer.lock` - Fetches the latest version from GitHub API - Displays both versions for comparison +--- + +### 6. CompatibilityCheckCommand (`mageforge:hyva:compatibility:check`) + +**Purpose**: Scans all Magento modules for Hyvä theme compatibility issues such as RequireJS, Knockout.js, jQuery, and UI Components usage. + +**File**: `/src/Console/Command/Hyva/CompatibilityCheckCommand.php` + +**Dependencies**: + +- `CompatibilityChecker` - Main orchestrator service for scanning modules + +**Usage**: + +```bash +bin/magento mageforge:hyva:compatibility:check [options] +``` + +**Aliases**: + +- `m:h:c:c` +- `hyva:check` + +**Options**: + +- `--show-all` / `-a` - Show all modules including compatible ones +- `--third-party-only` / `-t` - Check only third-party modules (exclude Magento\_\* modules) +- `--include-vendor` - Include Magento core modules in scan (default: third-party only) +- `--detailed` / `-d` - Show detailed file-level issues for incompatible modules + +**Interactive Mode**: +When running **without any options**, the command launches an interactive menu (using Laravel Prompts): + +```bash +# Launch interactive menu +bin/magento m:h:c:c +``` + +The menu allows you to select: + +- ☐ Show all modules including compatible ones +- ☐ Show only incompatible modules (default behavior) +- ☐ Include Magento core modules (default: third-party only) +- ☐ Show detailed file-level issues with line numbers + +Use **Space** to toggle options, **Enter** to confirm and start the scan. + +**Default Behavior**: +Without any flags, the command scans **third-party modules only** (excludes `Magento_*` modules but includes vendor third-party like Hyva, PayPal, Mollie, etc.). + +**Examples**: + +```bash +# Basic scan (third-party modules only - DEFAULT) +bin/magento m:h:c:c + +# Include Magento core modules +bin/magento m:h:c:c --include-vendor + +# Show all modules including compatible ones +bin/magento m:h:c:c -a + +# Show detailed file-level issues +bin/magento m:h:c:c -d + +# Using full command name +bin/magento mageforge:hyva:compatibility:check --detailed +``` + +**Implementation Details**: + +- Scans module directories for JS, XML, and PHTML files +- Detects incompatibility patterns: + - **Critical Issues**: + - RequireJS `define()` and `require()` usage + - Knockout.js observables and computed properties + - Magento UI Components in XML + - `data-mage-init` and `x-magento-init` in templates + - **Warnings**: + - jQuery AJAX direct usage + - jQuery DOM manipulation + - Block removal in layout XML (review needed) +- Displays results in formatted tables with color-coded status: + - ✓ Green: Compatible modules + - ⚠ Yellow: Warnings (non-critical issues) + - ✗ Red: Incompatible (critical issues) + - ✓ Hyvä-Aware: Modules with Hyvä compatibility packages +- Provides summary statistics: + - Total modules scanned + - Compatible vs. incompatible count + - Hyvä-aware modules count + - Critical issues and warnings count +- Shows detailed file paths and line numbers with `--detailed` flag +- Provides helpful recommendations for resolving issues +- Returns exit code 1 if critical issues are found, 0 otherwise (even if warnings exist) + +**Detected Patterns**: + +_JavaScript Files (.js)_: + +- `define([` - RequireJS module definition +- `require([` - RequireJS dependency loading +- `ko.observable` / `ko.observableArray` / `ko.computed` - Knockout.js +- `$.ajax` / `jQuery.ajax` - jQuery AJAX +- `mage/` - Magento RequireJS module references + +_XML Files (.xml)_: + +- `setName($this->getCommandName('hyva', 'compatibility:check')) + ->setDescription('Check modules for Hyvä theme compatibility issues') + ->setAliases(['m:h:c:c', 'hyva:check']) + ->addOption( + self::OPTION_SHOW_ALL, + 'a', + InputOption::VALUE_NONE, + 'Show all modules including compatible ones' + ) + ->addOption( + self::OPTION_THIRD_PARTY_ONLY, + 't', + InputOption::VALUE_NONE, + 'Check only third-party modules (exclude Magento_* modules)' + ) + ->addOption( + self::OPTION_INCLUDE_VENDOR, + null, + InputOption::VALUE_NONE, + 'Include vendor modules in scan (default: excluded)' + ) + ->addOption( + self::OPTION_DETAILED, + 'd', + InputOption::VALUE_NONE, + 'Show detailed file-level issues for incompatible modules' + ); + } + + protected function executeCommand(InputInterface $input, OutputInterface $output): int + { + // Check if we're in interactive mode (no options provided) + $hasOptions = $input->getOption(self::OPTION_SHOW_ALL) + || $input->getOption(self::OPTION_THIRD_PARTY_ONLY) + || $input->getOption(self::OPTION_INCLUDE_VENDOR) + || $input->getOption(self::OPTION_DETAILED); + + if (!$hasOptions && $this->isInteractiveTerminal($output)) { + return $this->runInteractiveMode($input, $output); + } + + return $this->runDirectMode($input, $output); + } + + /** + * Run interactive mode with Laravel Prompts + */ + private function runInteractiveMode(InputInterface $input, OutputInterface $output): int + { + $this->io->title('Hyvä Theme Compatibility Check'); + + // Set environment variables for Laravel Prompts + $this->setPromptEnvironment(); + + try { + $scanOptionsPrompt = new MultiSelectPrompt( + label: 'Select scan options', + options: [ + 'show-all' => 'Show all modules including compatible ones', + 'incompatible-only' => 'Show only incompatible modules (default behavior)', + 'include-vendor' => 'Include Magento core modules (default: third-party only)', + 'detailed' => 'Show detailed file-level issues with line numbers', + ], + default: [], + hint: 'Space to toggle, Enter to confirm. Default: third-party modules only', + required: false, + ); + + $selectedOptions = $scanOptionsPrompt->prompt(); + \Laravel\Prompts\Prompt::terminal()->restoreTty(); + $this->resetPromptEnvironment(); + + // Apply selected options to input + $showAll = in_array('show-all', $selectedOptions); + $incompatibleOnly = in_array('incompatible-only', $selectedOptions); + $includeVendor = in_array('include-vendor', $selectedOptions); + $detailed = in_array('detailed', $selectedOptions); + $thirdPartyOnly = false; // Not needed in interactive mode + + // Show selected configuration + $this->io->newLine(); + $config = []; + if ($showAll) { + $config[] = 'Show all modules'; + } elseif ($incompatibleOnly) { + $config[] = 'Show incompatible only'; + } else { + $config[] = 'Show modules with issues'; + } + if ($includeVendor) { + $config[] = 'Include Magento core'; + } else { + $config[] = 'Third-party modules only'; + } + if ($detailed) { + $config[] = 'Detailed issues'; + } + $this->io->comment('Configuration: ' . implode(', ', $config)); + $this->io->newLine(); + + // Run scan with selected options (pass incompatibleOnly flag) + return $this->runScan($showAll, $thirdPartyOnly, $includeVendor, $detailed, $incompatibleOnly, $output); + } catch (\Exception $e) { + $this->resetPromptEnvironment(); + $this->io->error('Interactive mode failed: ' . $e->getMessage()); + $this->io->info('Falling back to default scan (third-party modules only)...'); + $this->io->newLine(); + return $this->runDirectMode($input, $output); + } + } + + /** + * Run direct mode with command line options + */ + private function runDirectMode(InputInterface $input, OutputInterface $output): int + { + $showAll = $input->getOption(self::OPTION_SHOW_ALL); + $thirdPartyOnly = $input->getOption(self::OPTION_THIRD_PARTY_ONLY); + $includeVendor = $input->getOption(self::OPTION_INCLUDE_VENDOR); + $detailed = $input->getOption(self::OPTION_DETAILED); + + $this->io->title('Hyvä Theme Compatibility Check'); + + return $this->runScan($showAll, $thirdPartyOnly, $includeVendor, $detailed, false, $output); + } + + /** + * Run the actual compatibility scan + */ + private function runScan( + bool $showAll, + bool $thirdPartyOnly, + bool $includeVendor, + bool $detailed, + bool $incompatibleOnly, + OutputInterface $output + ): int { + + // Determine filter logic: + // - Default (no flags): Scan third-party only (exclude Magento_* but include vendor third-party) + // - With --include-vendor: Scan everything including Magento_* + // - With --third-party-only: Explicitly scan only third-party + $scanThirdPartyOnly = $thirdPartyOnly || (!$includeVendor && !$thirdPartyOnly); + $excludeVendor = false; // Always include vendor for third-party scanning + + // Run the compatibility check + $results = $this->compatibilityChecker->check( + $this->io, + $output, + $showAll, + $scanThirdPartyOnly, + $excludeVendor + ); + + // Determine display mode based on flags + // If incompatibleOnly is set, only show modules with issues + // If showAll is set, show everything + // Otherwise, show default (incompatible only) + $displayShowAll = $showAll; + if ($incompatibleOnly && !$showAll) { + $displayShowAll = false; // Only show incompatible + } + + // Display results + $this->displayResults($results, $displayShowAll || $detailed); + + // Display detailed issues if requested + if ($detailed && $results['hasIncompatibilities']) { + $this->displayDetailedIssues($results); + } + + // Display summary + $this->displaySummary($results); + + // Display recommendations if there are issues + if ($results['hasIncompatibilities']) { + $this->displayRecommendations(); + } + + // Return appropriate exit code + return $results['summary']['criticalIssues'] > 0 + ? Cli::RETURN_FAILURE + : Cli::RETURN_SUCCESS; + } + + /** + * Display compatibility check results + */ + private function displayResults(array $results, bool $showAll): void + { + $this->io->section('Compatibility Results'); + + $tableData = $this->compatibilityChecker->formatResultsForDisplay($results, $showAll); + + if (empty($tableData)) { + $this->io->success('All scanned modules are compatible with Hyvä!'); + return; + } + + $this->io->table( + ['Module', 'Status', 'Issues'], + $tableData + ); + } + + /** + * Display detailed file-level issues + */ + private function displayDetailedIssues(array $results): void + { + $this->io->section('Detailed Issues'); + + foreach ($results['modules'] as $moduleName => $moduleData) { + // Only show modules with issues + if ($moduleData['compatible'] && !$moduleData['hasWarnings']) { + continue; + } + + $this->io->text(sprintf('%s', $moduleName)); + + $detailedIssues = $this->compatibilityChecker->getDetailedIssues($moduleName, $moduleData); + + foreach ($detailedIssues as $fileData) { + $this->io->text(sprintf(' %s', $fileData['file'])); + + foreach ($fileData['issues'] as $issue) { + $color = $issue['severity'] === 'critical' ? 'red' : 'yellow'; + $symbol = $issue['severity'] === 'critical' ? '✗' : '⚠'; + + $this->io->text(sprintf( + ' %s Line %d: %s', + $color, + $symbol, + $issue['line'], + $issue['description'] + )); + } + } + + $this->io->newLine(); + } + } + + /** + * Display summary statistics + */ + private function displaySummary(array $results): void + { + $summary = $results['summary']; + + $this->io->section('Summary'); + + $summaryData = [ + ['Total Modules Scanned', $summary['total']], + new TableSeparator(), + ['Compatible', sprintf('%d', $summary['compatible'])], + ['Incompatible', sprintf('%d', $summary['incompatible'])], + ['Hyvä-Aware Modules', sprintf('%d', $summary['hyvaAware'])], + new TableSeparator(), + ['Critical Issues', sprintf('%d', $summary['criticalIssues'])], + ['Warnings', sprintf('%d', $summary['warningIssues'])], + ]; + + $this->io->table([], $summaryData); + + // Final message + if ($summary['criticalIssues'] > 0) { + $this->io->error(sprintf( + 'Found %d critical compatibility issue(s) that need attention.', + $summary['criticalIssues'] + )); + } elseif ($summary['warningIssues'] > 0) { + $this->io->warning(sprintf( + 'Found %d warning(s). Review these for potential issues.', + $summary['warningIssues'] + )); + } else { + $this->io->success('All scanned modules are Hyvä compatible!'); + } + } + + /** + * Display helpful recommendations + */ + private function displayRecommendations(): void + { + + $this->io->section('Recommendations'); + + $recommendations = [ + '• Check if Hyvä compatibility packages exist for incompatible modules', + '• Review https://hyva.io/compatibility for known solutions', + '• Consider refactoring RequireJS/Knockout code to Alpine.js', + '• Contact module vendors for Hyvä-compatible versions', + ]; + + foreach ($recommendations as $recommendation) { + $this->io->text($recommendation); + } + } + + /** + * Check if running in an interactive terminal + */ + private function isInteractiveTerminal(OutputInterface $output): bool + { + // Check if output is decorated (supports ANSI codes) + if (!$output->isDecorated()) { + return false; + } + + // Check if STDIN is available and readable + if (!defined('STDIN') || !is_resource(STDIN)) { + return false; + } + + // Check for common non-interactive environments + $nonInteractiveEnvs = [ + 'CI', + 'GITHUB_ACTIONS', + 'GITLAB_CI', + 'JENKINS_URL', + 'TEAMCITY_VERSION', + ]; + + foreach ($nonInteractiveEnvs as $env) { + if ($this->getEnvVar($env) || $this->getServerVar($env)) { + return false; + } + } + + // Additional check: try to detect if running in a proper TTY + $sttyOutput = shell_exec('stty -g 2>/dev/null'); + return !empty($sttyOutput); + } + + /** + * Set environment for Laravel Prompts to work properly in Docker/DDEV + */ + private function setPromptEnvironment(): void + { + // Store original values for reset + $this->originalEnv = [ + 'COLUMNS' => $this->getEnvVar('COLUMNS'), + 'LINES' => $this->getEnvVar('LINES'), + 'TERM' => $this->getEnvVar('TERM'), + ]; + + // Set terminal environment variables using safe method + $this->setEnvVar('COLUMNS', '100'); + $this->setEnvVar('LINES', '40'); + $this->setEnvVar('TERM', 'xterm-256color'); + } + + /** + * Reset terminal environment after prompts + */ + private function resetPromptEnvironment(): void + { + // Reset environment variables to original state using secure methods + foreach ($this->originalEnv as $key => $value) { + if ($value === null) { + // Remove from our secure cache + $this->removeSecureEnvironmentValue($key); + } else { + // Restore original value using secure method + $this->setEnvVar($key, $value); + } + } + } + + /** + * Securely remove environment variable from cache + */ + private function removeSecureEnvironmentValue(string $name): void + { + // Remove the specific variable from our secure storage + unset($this->secureEnvStorage[$name]); + + // Clear the static cache to force refresh on next access + $this->clearEnvironmentCache(); + } + + /** + * Simplified environment variable getter + */ + private function getEnvVar(string $name): ?string + { + // Check secure storage first + if (isset($this->secureEnvStorage[$name])) { + return $this->secureEnvStorage[$name]; + } + + // Fall back to system environment + $value = getenv($name); + return $value !== false ? $value : null; + } + + /** + * Simplified server variable getter + */ + private function getServerVar(string $name): ?string + { + return $_SERVER[$name] ?? null; + } + + /** + * Simplified environment variable setter + */ + private function setEnvVar(string $name, string $value): void + { + $this->secureEnvStorage[$name] = $value; + putenv("$name=$value"); + } + + /** + * Clear environment cache + */ + private function clearEnvironmentCache(): void + { + // Force refresh on next access by clearing our storage + $this->secureEnvStorage = array_filter( + $this->secureEnvStorage, + fn($key) => in_array($key, ['COLUMNS', 'LINES', 'TERM']), + ARRAY_FILTER_USE_KEY + ); + } +} diff --git a/src/Service/Hyva/CompatibilityChecker.php b/src/Service/Hyva/CompatibilityChecker.php new file mode 100644 index 0000000..0b23735 --- /dev/null +++ b/src/Service/Hyva/CompatibilityChecker.php @@ -0,0 +1,200 @@ + [], 'summary' => [], 'hasIncompatibilities' => bool] + */ + public function check( + SymfonyStyle $io, + OutputInterface $output, + bool $showAll = false, + bool $thirdPartyOnly = false, + bool $excludeVendor = true + ): array { + $modules = $this->componentRegistrar->getPaths(ComponentRegistrar::MODULE); + $results = [ + 'modules' => [], + 'summary' => [ + 'total' => 0, + 'compatible' => 0, + 'incompatible' => 0, + 'hyvaAware' => 0, + 'criticalIssues' => 0, + 'warningIssues' => 0, + ], + 'hasIncompatibilities' => false, + ]; + + $io->text(sprintf('Scanning %d modules for Hyvä compatibility...', count($modules))); + $io->newLine(); + + foreach ($modules as $moduleName => $modulePath) { + // Filter by options + if ($excludeVendor && $this->isVendorModule($modulePath)) { + continue; + } + + if ($thirdPartyOnly && $this->isMagentoModule($moduleName)) { + continue; + } + + $results['summary']['total']++; + + if ($showAll) { + $io->text(sprintf(' Scanning: %s', $moduleName)); + } + + $scanResult = $this->moduleScanner->scanModule($modulePath); + $moduleInfo = $this->moduleScanner->getModuleInfo($modulePath); + + $isCompatible = $scanResult['criticalIssues'] === 0; + $hasWarnings = $scanResult['totalIssues'] > $scanResult['criticalIssues']; + + $results['modules'][$moduleName] = [ + 'path' => $modulePath, + 'compatible' => $isCompatible, + 'hasWarnings' => $hasWarnings, + 'scanResult' => $scanResult, + 'moduleInfo' => $moduleInfo, + ]; + + // Update summary + if ($isCompatible && !$hasWarnings) { + $results['summary']['compatible']++; + } else { + $results['summary']['incompatible']++; + $results['hasIncompatibilities'] = true; + } + + if ($moduleInfo['isHyvaAware']) { + $results['summary']['hyvaAware']++; + } + + $results['summary']['criticalIssues'] += $scanResult['criticalIssues']; + $results['summary']['warningIssues'] += ($scanResult['totalIssues'] - $scanResult['criticalIssues']); + } + + return $results; + } + + /** + * Check if module is a vendor module + */ + private function isVendorModule(string $modulePath): bool + { + return str_contains($modulePath, '/vendor/'); + } + + /** + * Check if module is a core Magento module + */ + private function isMagentoModule(string $moduleName): bool + { + return str_starts_with($moduleName, 'Magento_'); + } + + /** + * Format results for display + */ + public function formatResultsForDisplay(array $results, bool $showAll = false): array + { + $tableData = []; + + foreach ($results['modules'] as $moduleName => $data) { + $status = $this->getStatusDisplay($data); + $issues = $this->getIssuesDisplay($data); + + if ($showAll || !$data['compatible'] || $data['hasWarnings']) { + $tableData[] = [ + $moduleName, + $status, + $issues, + ]; + } + } + + return $tableData; + } + + /** + * Get status display string with colors + */ + private function getStatusDisplay(array $moduleData): string + { + if ($moduleData['moduleInfo']['isHyvaAware']) { + return '✓ Hyvä-Aware'; + } + + if ($moduleData['compatible'] && !$moduleData['hasWarnings']) { + return '✓ Compatible'; + } + + if ($moduleData['compatible'] && $moduleData['hasWarnings']) { + return '⚠ Warnings'; + } + + return '✗ Incompatible'; + } + + /** + * Get issues display string + */ + private function getIssuesDisplay(array $moduleData): string + { + $scanResult = $moduleData['scanResult']; + + if ($scanResult['totalIssues'] === 0) { + return 'None'; + } + + $parts = []; + + if ($scanResult['criticalIssues'] > 0) { + $parts[] = sprintf('%d critical', $scanResult['criticalIssues']); + } + + $warnings = $scanResult['totalIssues'] - $scanResult['criticalIssues']; + if ($warnings > 0) { + $parts[] = sprintf('%d warning(s)', $warnings); + } + + return implode(', ', $parts); + } + + /** + * Get detailed file issues for a module + */ + public function getDetailedIssues(string $moduleName, array $moduleData): array + { + $files = $moduleData['scanResult']['files'] ?? []; + $details = []; + + foreach ($files as $filePath => $issues) { + $details[] = [ + 'file' => $filePath, + 'issues' => $issues, + ]; + } + + return $details; + } +} diff --git a/src/Service/Hyva/IncompatibilityDetector.php b/src/Service/Hyva/IncompatibilityDetector.php new file mode 100644 index 0000000..3ee906a --- /dev/null +++ b/src/Service/Hyva/IncompatibilityDetector.php @@ -0,0 +1,183 @@ + [ + [ + 'pattern' => '/define\s*\(\s*\[/', + 'description' => 'RequireJS define() usage', + 'severity' => self::SEVERITY_CRITICAL, + ], + [ + 'pattern' => '/require\s*\(\s*\[/', + 'description' => 'RequireJS require() usage', + 'severity' => self::SEVERITY_CRITICAL, + ], + [ + 'pattern' => '/ko\.observable|ko\.observableArray|ko\.computed/', + 'description' => 'Knockout.js usage', + 'severity' => self::SEVERITY_CRITICAL, + ], + [ + 'pattern' => '/\$\.ajax|jQuery\.ajax/', + 'description' => 'jQuery AJAX direct usage', + 'severity' => self::SEVERITY_WARNING, + ], + [ + 'pattern' => '/mage\//', + 'description' => 'Magento RequireJS module reference', + 'severity' => self::SEVERITY_CRITICAL, + ], + ], + 'xml' => [ + [ + 'pattern' => '/ 'UI Component usage', + 'severity' => self::SEVERITY_CRITICAL, + ], + [ + 'pattern' => '/component="uiComponent"/', + 'description' => 'uiComponent reference', + 'severity' => self::SEVERITY_CRITICAL, + ], + [ + 'pattern' => '/component="Magento_Ui\/js\//', + 'description' => 'Magento UI JS component', + 'severity' => self::SEVERITY_CRITICAL, + ], + [ + 'pattern' => '//', + 'description' => 'Block removal (review for Hyvä compatibility)', + 'severity' => self::SEVERITY_WARNING, + ], + ], + 'phtml' => [ + [ + 'pattern' => '/data-mage-init\s*=/', + 'description' => 'data-mage-init JavaScript initialization', + 'severity' => self::SEVERITY_CRITICAL, + ], + [ + 'pattern' => '/x-magento-init/', + 'description' => 'x-magento-init JavaScript initialization', + 'severity' => self::SEVERITY_CRITICAL, + ], + [ + 'pattern' => '/\$\(.*\)\..*\(/', + 'description' => 'jQuery DOM manipulation', + 'severity' => self::SEVERITY_WARNING, + ], + [ + 'pattern' => '/require\s*\(\s*\[/', + 'description' => 'RequireJS in template', + 'severity' => self::SEVERITY_CRITICAL, + ], + ], + ]; + + public function __construct( + private readonly File $fileDriver + ) { + } + + /** + * Detect incompatibilities in a file + * + * @return array Array of issues with keys: pattern, description, severity, line + */ + public function detectInFile(string $filePath): array + { + if (!$this->fileDriver->isExists($filePath)) { + return []; + } + + $extension = pathinfo($filePath, PATHINFO_EXTENSION); + $fileType = $this->mapExtensionToType($extension); + + if (!isset(self::INCOMPATIBLE_PATTERNS[$fileType])) { + return []; + } + + try { + $content = $this->fileDriver->fileGetContents($filePath); + $lines = explode("\n", $content); + + return $this->scanContentForPatterns($lines, self::INCOMPATIBLE_PATTERNS[$fileType]); + } catch (\Exception $e) { + return []; + } + } + + /** + * Map file extension to pattern type + */ + private function mapExtensionToType(string $extension): string + { + return match ($extension) { + 'js' => 'js', + 'xml' => 'xml', + 'phtml' => 'phtml', + default => 'unknown', + }; + } + + /** + * Scan content lines for pattern matches + */ + private function scanContentForPatterns(array $lines, array $patterns): array + { + $issues = []; + + foreach ($patterns as $patternConfig) { + foreach ($lines as $lineNumber => $lineContent) { + if (preg_match($patternConfig['pattern'], $lineContent)) { + $issues[] = [ + 'description' => $patternConfig['description'], + 'severity' => $patternConfig['severity'], + 'line' => $lineNumber + 1, // Convert to 1-based line numbers + 'pattern' => $patternConfig['pattern'], + ]; + } + } + } + + return $issues; + } + + /** + * Get severity color for console output + */ + public function getSeverityColor(string $severity): string + { + return match ($severity) { + self::SEVERITY_CRITICAL => 'red', + self::SEVERITY_WARNING => 'yellow', + default => 'white', + }; + } + + /** + * Get severity symbol + */ + public function getSeveritySymbol(string $severity): string + { + return match ($severity) { + self::SEVERITY_CRITICAL => '✗', + self::SEVERITY_WARNING => '⚠', + default => 'ℹ', + }; + } +} diff --git a/src/Service/Hyva/ModuleScanner.php b/src/Service/Hyva/ModuleScanner.php new file mode 100644 index 0000000..a39ae80 --- /dev/null +++ b/src/Service/Hyva/ModuleScanner.php @@ -0,0 +1,159 @@ + [], 'totalIssues' => int, 'criticalIssues' => int] + */ + public function scanModule(string $modulePath): array + { + if (!$this->fileDriver->isDirectory($modulePath)) { + return ['files' => [], 'totalIssues' => 0, 'criticalIssues' => 0]; + } + + $filesWithIssues = []; + $totalIssues = 0; + $criticalIssues = 0; + + $files = $this->findRelevantFiles($modulePath); + + foreach ($files as $file) { + $issues = $this->incompatibilityDetector->detectInFile($file); + + if (!empty($issues)) { + $relativePath = str_replace($modulePath . '/', '', $file); + $filesWithIssues[$relativePath] = $issues; + $totalIssues += count($issues); + + foreach ($issues as $issue) { + if ($issue['severity'] === 'critical') { + $criticalIssues++; + } + } + } + } + + return [ + 'files' => $filesWithIssues, + 'totalIssues' => $totalIssues, + 'criticalIssues' => $criticalIssues, + ]; + } + + /** + * Recursively find all relevant files in a directory + */ + private function findRelevantFiles(string $directory): array + { + $relevantFiles = []; + + try { + $items = $this->fileDriver->readDirectory($directory); + + foreach ($items as $item) { + $basename = basename($item); + + // Skip excluded directories + if ($this->fileDriver->isDirectory($item)) { + if (in_array($basename, self::EXCLUDE_DIRECTORIES, true)) { + continue; + } + // Recursively scan subdirectories + $relevantFiles = array_merge($relevantFiles, $this->findRelevantFiles($item)); + continue; + } + + // Check if file has relevant extension + $extension = pathinfo($item, PATHINFO_EXTENSION); + if (in_array($extension, self::SCAN_EXTENSIONS, true)) { + $relevantFiles[] = $item; + } + } + } catch (\Exception $e) { + // Silently skip directories that can't be read + } + + return $relevantFiles; + } + + /** + * Check if module has Hyvä compatibility package + */ + public function hasHyvaCompatibilityPackage(string $modulePath): bool + { + $composerPath = $modulePath . '/composer.json'; + + if (!$this->fileDriver->isExists($composerPath)) { + return false; + } + + try { + $content = $this->fileDriver->fileGetContents($composerPath); + $composerData = json_decode($content, true); + + if (!is_array($composerData)) { + return false; + } + + // Check if this IS a Hyvä compatibility package + $packageName = $composerData['name'] ?? ''; + if (str_starts_with($packageName, 'hyva-themes/') && str_contains($packageName, '-compat')) { + return true; + } + + // Check dependencies for Hyvä packages + $requires = $composerData['require'] ?? []; + foreach ($requires as $package => $version) { + if (str_starts_with($package, 'hyva-themes/')) { + return true; + } + } + } catch (\Exception $e) { + return false; + } + + return false; + } + + /** + * Get module info from composer.json + */ + public function getModuleInfo(string $modulePath): array + { + $composerPath = $modulePath . '/composer.json'; + + if (!$this->fileDriver->isExists($composerPath)) { + return ['name' => 'Unknown', 'version' => 'Unknown', 'isHyvaAware' => false]; + } + + try { + $content = $this->fileDriver->fileGetContents($composerPath); + $composerData = json_decode($content, true); + + return [ + 'name' => $composerData['name'] ?? 'Unknown', + 'version' => $composerData['version'] ?? 'Unknown', + 'isHyvaAware' => $this->hasHyvaCompatibilityPackage($modulePath), + ]; + } catch (\Exception $e) { + return ['name' => 'Unknown', 'version' => 'Unknown', 'isHyvaAware' => false]; + } + } +} diff --git a/src/etc/di.xml b/src/etc/di.xml index a30e947..1050d40 100644 --- a/src/etc/di.xml +++ b/src/etc/di.xml @@ -1,49 +1,61 @@ - - - - OpenForgeProject\MageForge\Console\Command\System\VersionCommand - OpenForgeProject\MageForge\Console\Command\System\CheckCommand - OpenForgeProject\MageForge\Console\Command\Theme\ListCommand - OpenForgeProject\MageForge\Console\Command\Theme\BuildCommand - OpenForgeProject\MageForge\Console\Command\Theme\WatchCommand - - - + + + + + OpenForgeProject\MageForge\Console\Command\System\VersionCommand + + OpenForgeProject\MageForge\Console\Command\System\CheckCommand + + OpenForgeProject\MageForge\Console\Command\Theme\ListCommand + + OpenForgeProject\MageForge\Console\Command\Theme\BuildCommand + + OpenForgeProject\MageForge\Console\Command\Theme\WatchCommand + + OpenForgeProject\MageForge\Console\Command\Hyva\CompatibilityCheckCommand + + + - - - - - OpenForgeProject\MageForge\Service\ThemeBuilder\HyvaThemes\Builder - OpenForgeProject\MageForge\Service\ThemeBuilder\MagentoStandard\Builder - OpenForgeProject\MageForge\Service\ThemeBuilder\TailwindCSS\Builder - - - + + + + + + OpenForgeProject\MageForge\Service\ThemeBuilder\HyvaThemes\Builder + + OpenForgeProject\MageForge\Service\ThemeBuilder\MagentoStandard\Builder + + OpenForgeProject\MageForge\Service\ThemeBuilder\TailwindCSS\Builder + + +