From 062bd02611416abd885917f32b450a9a1ff18408 Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Tue, 17 Feb 2026 11:53:05 +0100 Subject: [PATCH 1/3] feat: add NodeSetupValidator for validating Magento default setup files --- src/Service/NodeSetupValidator.php | 328 ++++++++++++++++++ .../ThemeBuilder/MagentoStandard/Builder.php | 11 +- src/etc/di.xml | 8 + 3 files changed, 346 insertions(+), 1 deletion(-) create mode 100644 src/Service/NodeSetupValidator.php diff --git a/src/Service/NodeSetupValidator.php b/src/Service/NodeSetupValidator.php new file mode 100644 index 0000000..25bfced --- /dev/null +++ b/src/Service/NodeSetupValidator.php @@ -0,0 +1,328 @@ + 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 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 $missing List of missing files/directories + * @return array 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 $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 $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 $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 $restored List of successfully restored files + * @param array $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 $restored List of restored files + * @param array $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; + } +} diff --git a/src/Service/ThemeBuilder/MagentoStandard/Builder.php b/src/Service/ThemeBuilder/MagentoStandard/Builder.php index 722d49f..ce1db8d 100644 --- a/src/Service/ThemeBuilder/MagentoStandard/Builder.php +++ b/src/Service/ThemeBuilder/MagentoStandard/Builder.php @@ -9,6 +9,7 @@ use OpenForgeProject\MageForge\Service\CacheCleaner; use OpenForgeProject\MageForge\Service\GruntTaskRunner; use OpenForgeProject\MageForge\Service\NodePackageManager; +use OpenForgeProject\MageForge\Service\NodeSetupValidator; use OpenForgeProject\MageForge\Service\StaticContentCleaner; use OpenForgeProject\MageForge\Service\StaticContentDeployer; use OpenForgeProject\MageForge\Service\SymlinkCleaner; @@ -28,7 +29,8 @@ public function __construct( private readonly CacheCleaner $cacheCleaner, private readonly SymlinkCleaner $symlinkCleaner, private readonly NodePackageManager $nodePackageManager, - private readonly GruntTaskRunner $gruntTaskRunner + private readonly GruntTaskRunner $gruntTaskRunner, + private readonly NodeSetupValidator $nodeSetupValidator ) { } @@ -90,6 +92,13 @@ private function processNodeSetup( OutputInterface $output, bool $isVerbose ): bool { + $rootPath = '.'; + + // Validate and restore Node.js setup files if needed + if (!$this->nodeSetupValidator->validateAndRestore($rootPath, $io, $isVerbose)) { + return false; + } + // Check if Node/Grunt setup exists if (!$this->autoRepair($themePath, $io, $output, $isVerbose)) { return false; diff --git a/src/etc/di.xml b/src/etc/di.xml index 2ea49d6..2fe48a7 100644 --- a/src/etc/di.xml +++ b/src/etc/di.xml @@ -72,4 +72,12 @@ Magento\Framework\Filesystem\Driver\File + + + + + Magento\Framework\Filesystem\Driver\File + OpenForgeProject\MageForge\Service\NodePackageManager + + From 1b6b379c69ae604ad147110d7488b973a590033f Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Tue, 17 Feb 2026 13:29:27 +0100 Subject: [PATCH 2/3] Update src/Service/NodeSetupValidator.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Service/NodeSetupValidator.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Service/NodeSetupValidator.php b/src/Service/NodeSetupValidator.php index 25bfced..7b8bf7b 100644 --- a/src/Service/NodeSetupValidator.php +++ b/src/Service/NodeSetupValidator.php @@ -6,7 +6,6 @@ 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 From 0bbe70448b3c511d95d8ad3e2234809444037d3a Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Tue, 17 Feb 2026 13:41:18 +0100 Subject: [PATCH 3/3] chore: add missing import for Laravel prompt confirmation function --- src/Service/NodeSetupValidator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service/NodeSetupValidator.php b/src/Service/NodeSetupValidator.php index 7b8bf7b..5bfac02 100644 --- a/src/Service/NodeSetupValidator.php +++ b/src/Service/NodeSetupValidator.php @@ -6,7 +6,7 @@ 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 */