-
-
Notifications
You must be signed in to change notification settings - Fork 1
feat: add NodeSetupValidator for validating Magento default setup files #142
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,327 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace OpenForgeProject\MageForge\Service; | ||
|
|
||
| use Magento\Framework\Filesystem\Driver\File as FileDriver; | ||
| use Symfony\Component\Console\Style\SymfonyStyle; | ||
| use function Laravel\Prompts\confirm; | ||
| /** | ||
| * Service for validating and restoring Node.js setup files for Magento Standard themes | ||
| */ | ||
| class NodeSetupValidator | ||
| { | ||
| private const REQUIRED_FILES = [ | ||
| 'package.json', | ||
| 'Gruntfile.js', | ||
| 'grunt-config.json', | ||
| ]; | ||
|
|
||
| /** @var array<string> Files that will be generated by npm install */ | ||
| private const GENERATED_FILES = [ | ||
| 'package-lock.json', | ||
| ]; | ||
|
|
||
| private const REQUIRED_DIRECTORIES = [ | ||
| 'node_modules', | ||
| ]; | ||
|
|
||
| private const NODE_MODULES_DIR = 'node_modules/'; | ||
|
|
||
| private const MAGENTO_BASE_PATH = 'vendor/magento/magento2-base'; | ||
|
|
||
| /** | ||
| * Constructor | ||
| * | ||
| * @param FileDriver $fileDriver | ||
| * @param NodePackageManager $nodePackageManager | ||
| */ | ||
| public function __construct( | ||
| private readonly FileDriver $fileDriver, | ||
| private readonly NodePackageManager $nodePackageManager | ||
| ) { | ||
| } | ||
|
|
||
| /** | ||
| * Validate Node.js setup files and offer to restore missing ones | ||
| * | ||
| * @param string $rootPath Root directory to check (usually '.') | ||
| * @param SymfonyStyle $io Console IO for output | ||
| * @param bool $isVerbose Whether to show verbose output | ||
| * @return bool True if validation passed or files were restored successfully | ||
| */ | ||
| public function validateAndRestore(string $rootPath, SymfonyStyle $io, bool $isVerbose): bool | ||
| { | ||
| $missing = $this->getMissingFiles($rootPath); | ||
|
|
||
| if (empty($missing)) { | ||
| if ($isVerbose) { | ||
| $io->success('All required Node.js setup files are present.'); | ||
| } | ||
| return true; | ||
| } | ||
|
|
||
| // Separate source files from generated files/directories | ||
| $missingSourceFiles = $this->filterSourceFiles($missing); | ||
|
|
||
| // If only generated files/directories are missing, restore them directly without asking | ||
| if (empty($missingSourceFiles)) { | ||
| return $this->restoreGeneratedFilesAutomatically($rootPath, $missing, $io, $isVerbose); | ||
| } | ||
|
|
||
| // Ask user if they want to restore missing files | ||
| if (!$this->promptUserForRestoration($missing, $io)) { | ||
| $io->info('Skipping file restoration.'); | ||
| return false; | ||
| } | ||
|
|
||
| // Restore missing files | ||
| return $this->restoreMissingFiles($rootPath, $missing, $io, $isVerbose); | ||
| } | ||
|
|
||
| /** | ||
| * Get list of missing files and directories | ||
| * | ||
| * @param string $rootPath Root directory to check | ||
| * @return array<string> List of missing file/directory names | ||
| */ | ||
| private function getMissingFiles(string $rootPath): array | ||
| { | ||
| $missing = []; | ||
|
|
||
| // Check required files | ||
| foreach (self::REQUIRED_FILES as $file) { | ||
| if (!$this->fileDriver->isExists($rootPath . '/' . $file)) { | ||
| $missing[] = $file; | ||
| } | ||
| } | ||
|
|
||
| // Check generated files | ||
| foreach (self::GENERATED_FILES as $file) { | ||
| if (!$this->fileDriver->isExists($rootPath . '/' . $file)) { | ||
| $missing[] = $file; | ||
| } | ||
| } | ||
|
|
||
| // Check required directories | ||
| foreach (self::REQUIRED_DIRECTORIES as $directory) { | ||
| if (!$this->fileDriver->isDirectory($rootPath . '/' . $directory)) { | ||
| $missing[] = $directory . '/'; | ||
| } | ||
| } | ||
|
|
||
| return $missing; | ||
| } | ||
|
|
||
| /** | ||
| * Filter source files from the list of missing files | ||
| * | ||
| * Returns only files that need to be copied from Magento base | ||
| * (excludes generated files and directories) | ||
| * | ||
| * @param array<string> $missing List of missing files/directories | ||
| * @return array<string> List of source files that need to be restored | ||
| */ | ||
| private function filterSourceFiles(array $missing): array | ||
| { | ||
| return array_filter($missing, fn($item) => !$this->isGeneratedFileOrDirectory($item)); | ||
| } | ||
|
|
||
| /** | ||
| * Check if a file or directory is generated (not a source file) | ||
| * | ||
| * @param string $item File or directory name | ||
| * @return bool True if the item is generated by npm install | ||
| */ | ||
| private function isGeneratedFileOrDirectory(string $item): bool | ||
| { | ||
| return in_array($item, self::GENERATED_FILES, true) || $item === self::NODE_MODULES_DIR; | ||
| } | ||
|
|
||
| /** | ||
| * Restore only generated files/directories automatically without user prompt | ||
| * | ||
| * @param string $rootPath Root directory where files should be restored | ||
| * @param array<string> $missing List of missing files/directories | ||
| * @param SymfonyStyle $io Console IO for output | ||
| * @param bool $isVerbose Whether to show verbose output | ||
| * @return bool True if restoration was successful | ||
| */ | ||
| private function restoreGeneratedFilesAutomatically( | ||
| string $rootPath, | ||
| array $missing, | ||
| SymfonyStyle $io, | ||
| bool $isVerbose | ||
| ): bool { | ||
| if ($isVerbose) { | ||
| $io->note('Detected missing generated files/directories. Installing automatically...'); | ||
| foreach ($missing as $item) { | ||
| $io->writeln(" - {$item}"); | ||
| } | ||
| } | ||
| return $this->restoreMissingFiles($rootPath, $missing, $io, $isVerbose); | ||
| } | ||
|
|
||
| /** | ||
| * Prompt user to confirm file restoration | ||
| * | ||
| * @param array<string> $missing List of missing files/directories | ||
| * @param SymfonyStyle $io Console IO for output | ||
| * @return bool True if user confirms restoration | ||
| */ | ||
| private function promptUserForRestoration(array $missing, SymfonyStyle $io): bool | ||
| { | ||
| // Display missing files | ||
| $io->warning('The following required files/directories are missing:'); | ||
| foreach ($missing as $item) { | ||
| $suffix = $this->isGeneratedFileOrDirectory($item) | ||
| ? ' (will be generated by npm install)' | ||
| : ''; | ||
| $io->writeln(" - {$item}{$suffix}"); | ||
| } | ||
| $io->newLine(); | ||
|
|
||
| // Ask user if they want to restore | ||
| return confirm( | ||
| label: 'Would you like to restore missing files from Magento base?', | ||
| default: true, | ||
| hint: 'This will copy the standard Magento files to your project root.' | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Restore missing files from Magento base installation | ||
| * | ||
| * @param string $rootPath Root directory where files should be restored | ||
| * @param array<string> $missing List of missing files/directories | ||
| * @param SymfonyStyle $io Console IO for output | ||
| * @param bool $isVerbose Whether to show verbose output | ||
| * @return bool True if restoration was successful | ||
| */ | ||
| private function restoreMissingFiles( | ||
| string $rootPath, | ||
| array $missing, | ||
| SymfonyStyle $io, | ||
| bool $isVerbose | ||
| ): bool { | ||
| if (empty($missing)) { | ||
| return true; | ||
| } | ||
|
|
||
| $basePath = self::MAGENTO_BASE_PATH; | ||
|
|
||
| if (!$this->fileDriver->isDirectory($basePath)) { | ||
| $io->error(sprintf( | ||
| 'Magento base directory not found at: %s', | ||
| $basePath | ||
| )); | ||
| return false; | ||
| } | ||
|
|
||
| $restored = []; | ||
| $failed = []; | ||
|
|
||
| foreach ($missing as $item) { | ||
| // Skip generated files/directories - they will be created by npm install | ||
| if ($this->isGeneratedFileOrDirectory($item)) { | ||
| if ($isVerbose) { | ||
| $io->note("Skipping {$item} - will be generated by npm install"); | ||
| } | ||
| continue; | ||
| } | ||
|
|
||
| $sourcePath = $basePath . '/' . $item; | ||
| $targetPath = $rootPath . '/' . $item; | ||
|
|
||
| if (!$this->fileDriver->isExists($sourcePath)) { | ||
| if ($isVerbose) { | ||
| $io->warning("Source file not found: {$sourcePath}"); | ||
| } | ||
| $failed[] = $item; | ||
| continue; | ||
| } | ||
|
|
||
| try { | ||
| $this->fileDriver->copy($sourcePath, $targetPath); | ||
| $restored[] = $item; | ||
|
|
||
| if ($isVerbose) { | ||
| $io->writeln("✓ Restored: {$item}"); | ||
| } | ||
| } catch (\Exception $e) { | ||
| $io->error("Failed to restore {$item}: " . $e->getMessage()); | ||
| $failed[] = $item; | ||
| } | ||
| } | ||
|
|
||
| $this->displayRestorationSummary($restored, $failed, $io); | ||
|
|
||
| // If we restored any files or node_modules is missing, run npm install | ||
| if ($this->shouldRunNpmInstall($restored, $missing)) { | ||
| return $this->runNpmInstall($rootPath, $io, $isVerbose) && empty($failed); | ||
| } | ||
|
|
||
| return empty($failed); | ||
| } | ||
|
|
||
| /** | ||
| * Display summary of restoration results | ||
| * | ||
| * @param array<string> $restored List of successfully restored files | ||
| * @param array<string> $failed List of failed files | ||
| * @param SymfonyStyle $io Console IO for output | ||
| * @return void | ||
| */ | ||
| private function displayRestorationSummary(array $restored, array $failed, SymfonyStyle $io): void | ||
| { | ||
| if (!empty($restored)) { | ||
| $io->success(sprintf( | ||
| 'Restored %d file(s): %s', | ||
| count($restored), | ||
| implode(', ', $restored) | ||
| )); | ||
| } | ||
|
|
||
| if (!empty($failed)) { | ||
| $io->warning(sprintf( | ||
| 'Failed to restore %d file(s): %s', | ||
| count($failed), | ||
| implode(', ', $failed) | ||
| )); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Check if npm install should be run | ||
| * | ||
| * @param array<string> $restored List of restored files | ||
| * @param array<string> $missing List of missing files | ||
| * @return bool True if npm install should be run | ||
| */ | ||
| private function shouldRunNpmInstall(array $restored, array $missing): bool | ||
| { | ||
| return !empty($restored) || in_array(self::NODE_MODULES_DIR, $missing, true); | ||
| } | ||
|
|
||
| /** | ||
| * Run npm install to create node_modules and generated files | ||
| * | ||
| * @param string $rootPath Root directory | ||
| * @param SymfonyStyle $io Console IO for output | ||
| * @param bool $isVerbose Whether to show verbose output | ||
| * @return bool True if npm install was successful | ||
| */ | ||
| private function runNpmInstall(string $rootPath, SymfonyStyle $io, bool $isVerbose): bool | ||
| { | ||
| $io->newLine(); | ||
| $io->text('Installing Node.js dependencies...'); | ||
|
|
||
| if (!$this->nodePackageManager->installNodeModules($rootPath, $io, $isVerbose)) { | ||
| $io->error('Failed to install Node.js dependencies.'); | ||
| return false; | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Laravel Prompts
confirm()function should be replaced with$io->confirm()to be consistent with how other service classes handle user prompts. Additionally, the second parameter should be set totrueto match the default behavior shown in the Laravel Prompts call (default: true), or adjust as needed for the desired default behavior. See src/Service/DependencyChecker.php lines 52, 71, and 111 for examples of the correct pattern.