diff --git a/docs/advanced_usage.md b/docs/advanced_usage.md index 31396bd..3f6cc9c 100644 --- a/docs/advanced_usage.md +++ b/docs/advanced_usage.md @@ -37,10 +37,38 @@ This document provides detailed information and advanced tips for using MageForg ### Standard Magento Themes (LESS) For traditional LESS-based Magento themes, MageForge handles: -- LESS compilation +- LESS compilation via Grunt - Source map generation - Minification for production +#### Vendor Themes + +MageForge automatically detects themes installed via Composer (located in `vendor/` directory): +- **Build mode**: Skips all Grunt/Node.js steps as vendors themes have pre-built assets +- **Watch mode**: Returns an error as vendor themes are read-only and cannot be modified + +This prevents accidental modification attempts and ensures build process stability. + +#### Themes Without Node.js/Grunt Setup + +MageForge automatically detects if a Magento Standard theme intentionally omits Node.js/Grunt setup. If none of the following files exist: +- `package.json` +- `package-lock.json` +- `gruntfile.js` +- `grunt-config.json` + +The builder will skip all Node/Grunt-related steps and only: +- Clean static content (if in developer mode) +- Deploy static content +- Clean cache + +This is useful for: +- Themes that use pre-compiled CSS +- Minimal themes without custom LESS +- Simple theme inheritance without asset compilation + +**Note**: Watch mode requires Node.js/Grunt setup and will return an error if these files are missing. + ### Hyvä Themes (Tailwind CSS) MageForge streamlines Hyvä theme development with: diff --git a/docs/commands.md b/docs/commands.md index ecc2795..07c2606 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -470,6 +470,9 @@ The commands rely on several services for their functionality: - `BuilderPool`: Manages theme builders and selects appropriate builders for themes - `BuilderInterface`: Implemented by all theme builders - `MagentoStandard\Builder`: Processes standard Magento LESS-based themes + - Automatically detects if Node.js/Grunt setup is present + - Skips Node/Grunt steps if intentionally omitted (no package.json, package-lock.json, gruntfile.js or grunt-config.json) + - Only performs static content deployment and cache cleaning for themes without build tools - Various other builders for different theme types ### Theme Services diff --git a/docs/custom_theme_builders.md b/docs/custom_theme_builders.md index ba52cf9..47f3045 100644 --- a/docs/custom_theme_builders.md +++ b/docs/custom_theme_builders.md @@ -308,6 +308,61 @@ public function autoRepair(string $themePath, SymfonyStyle $io, OutputInterface } ``` +### Best Practice: Optional Build Tool Setup + +If your builder uses optional build tools (like Node.js, Grunt, Webpack), consider checking if the setup exists before requiring it. This allows themes to intentionally skip certain build steps: + +```php +private function hasNodeSetup(): bool +{ + $rootPath = '.'; + + return $this->fileDriver->isExists($rootPath . '/package.json') + || $this->fileDriver->isExists($rootPath . '/package-lock.json') + || $this->fileDriver->isExists($rootPath . '/gruntfile.js') + || $this->fileDriver->isExists($rootPath . '/grunt-config.json'); +} + +private function isVendorTheme(string $themePath): bool +{ + return str_contains($themePath, '/vendor/'); +} + +public function build(string $themeCode, string $themePath, SymfonyStyle $io, OutputInterface $output, bool $isVerbose): bool +{ + if (!$this->detect($themePath)) { + return false; + } + + // Check if this is a vendor theme (read-only, pre-built assets) + if ($this->isVendorTheme($themePath)) { + if ($isVerbose) { + $io->note('Vendor theme detected. Skipping build steps (pre-built assets expected).'); + } + } elseif ($this->hasNodeSetup()) { + // Check if Node/Grunt setup exists + if (!$this->autoRepair($themePath, $io, $output, $isVerbose)) { + return false; + } + + // Execute build commands... + } else { + if ($isVerbose) { + $io->note('No Node.js setup detected. Skipping Node/Grunt steps.'); + } + } + + // Continue with other build steps (deploy, cache, etc.) + return true; +} +``` + +This approach: +- Prevents modification attempts on read-only vendor themes +- Allows themes to work without specific build tools +- Still supports full builds when tools are present +- Provides clear feedback about what's being skipped + ### The watch() Method This method starts a process that monitors changes to theme files and automatically rebuilds when necessary: diff --git a/src/Service/GruntTaskRunner.php b/src/Service/GruntTaskRunner.php index 36d9b13..395ec66 100644 --- a/src/Service/GruntTaskRunner.php +++ b/src/Service/GruntTaskRunner.php @@ -23,16 +23,26 @@ public function runTasks( bool $isVerbose ): bool { try { - foreach (['clean', 'less'] as $task) { - $shellOutput = $this->shell->execute(self::GRUNT_PATH . ' ' . $task . ' --quiet'); - if ($isVerbose) { - $output->writeln($shellOutput); - $io->success("'grunt $task' has been successfully executed."); - } + if ($isVerbose) { + $io->text('Running grunt clean...'); + $output->writeln($this->shell->execute(self::GRUNT_PATH . ' clean')); + } else { + $this->shell->execute(self::GRUNT_PATH . ' clean --quiet'); + } + + if ($isVerbose) { + $io->text('Running grunt less...'); + $output->writeln($this->shell->execute(self::GRUNT_PATH . ' less')); + } else { + $this->shell->execute(self::GRUNT_PATH . ' less --quiet'); + } + + if ($isVerbose) { + $io->success('Grunt tasks completed successfully.'); } return true; } catch (\Exception $e) { - $io->error($e->getMessage()); + $io->error('Failed to run grunt tasks: ' . $e->getMessage()); return false; } } diff --git a/src/Service/ThemeBuilder/MagentoStandard/Builder.php b/src/Service/ThemeBuilder/MagentoStandard/Builder.php index 7a2cd7c..f783ac8 100644 --- a/src/Service/ThemeBuilder/MagentoStandard/Builder.php +++ b/src/Service/ThemeBuilder/MagentoStandard/Builder.php @@ -7,6 +7,7 @@ use Magento\Framework\Filesystem\Driver\File; use Magento\Framework\Shell; use OpenForgeProject\MageForge\Service\CacheCleaner; +use OpenForgeProject\MageForge\Service\GruntTaskRunner; use OpenForgeProject\MageForge\Service\NodePackageManager; use OpenForgeProject\MageForge\Service\StaticContentCleaner; use OpenForgeProject\MageForge\Service\StaticContentDeployer; @@ -26,7 +27,8 @@ public function __construct( private readonly StaticContentCleaner $staticContentCleaner, private readonly CacheCleaner $cacheCleaner, private readonly SymlinkCleaner $symlinkCleaner, - private readonly NodePackageManager $nodePackageManager + private readonly NodePackageManager $nodePackageManager, + private readonly GruntTaskRunner $gruntTaskRunner ) { } @@ -52,37 +54,18 @@ public function build(string $themeCode, string $themePath, SymfonyStyle $io, Ou return false; } - if (!$this->autoRepair($themePath, $io, $output, $isVerbose)) { - return false; - } - - // Clean symlinks in web/css/ directory before build - if (!$this->symlinkCleaner->cleanSymlinks($themePath, $io, $isVerbose)) { - return false; - } - - // Run grunt tasks - try { - if ($isVerbose) { - $io->text('Running grunt clean...'); - $this->shell->execute('node_modules/.bin/grunt clean'); - } else { - $this->shell->execute('node_modules/.bin/grunt clean --quiet'); - } - - if ($isVerbose) { - $io->text('Running grunt less...'); - $this->shell->execute('node_modules/.bin/grunt less'); - } else { - $this->shell->execute('node_modules/.bin/grunt less --quiet'); + // Check if this is a vendor theme (read-only, pre-built assets) + if ($this->isVendorTheme($themePath)) { + $io->warning('Vendor theme detected. Skipping Grunt steps.'); + $io->newLine(2); + } elseif ($this->hasNodeSetup()) { + if (!$this->processNodeSetup($themePath, $io, $output, $isVerbose)) { + return false; } - + } else { if ($isVerbose) { - $io->success('Grunt tasks completed successfully.'); + $io->note('No Node.js/Grunt setup detected. Skipping Grunt steps.'); } - } catch (\Exception $e) { - $io->error('Failed to run grunt tasks: ' . $e->getMessage()); - return false; } // Deploy static content @@ -98,6 +81,29 @@ public function build(string $themeCode, string $themePath, SymfonyStyle $io, Ou return true; } + /** + * Process Node.js and Grunt setup + */ + private function processNodeSetup( + string $themePath, + SymfonyStyle $io, + OutputInterface $output, + bool $isVerbose + ): bool { + // Check if Node/Grunt setup exists + if (!$this->autoRepair($themePath, $io, $output, $isVerbose)) { + return false; + } + + // Clean symlinks in web/css/ directory before build + if (!$this->symlinkCleaner->cleanSymlinks($themePath, $io, $isVerbose)) { + return false; + } + + // Run grunt tasks + return $this->gruntTaskRunner->runTasks($io, $output, $isVerbose); + } + public function autoRepair(string $themePath, SymfonyStyle $io, OutputInterface $output, bool $isVerbose): bool { $rootPath = '.'; @@ -177,6 +183,18 @@ public function watch(string $themeCode, string $themePath, SymfonyStyle $io, Ou return false; } + // Vendor themes cannot be watched (read-only) + if ($this->isVendorTheme($themePath)) { + $io->error('Watch mode is not supported for vendor themes. Vendor themes are read-only and should have pre-built assets.'); + return false; + } + + // Check if Node/Grunt setup is intentionally absent + if (!$this->hasNodeSetup()) { + $io->error('Watch mode requires Node.js/Grunt setup. No package.json, package-lock.json, node_modules, or grunt-config.json found.'); + return false; + } + // Clean static content if in developer mode if (!$this->staticContentCleaner->cleanIfNeeded($themeCode, $io, $output, $isVerbose)) { return false; @@ -213,4 +231,35 @@ public function getName(): string { return self::THEME_NAME; } + + /** + * Check if Node.js/Grunt setup exists + * + * Returns true if at least one of the required files exists + * + * @return bool + */ + private function hasNodeSetup(): bool + { + $rootPath = '.'; + + return $this->fileDriver->isExists($rootPath . '/package.json') + || $this->fileDriver->isExists($rootPath . '/package-lock.json') + || $this->fileDriver->isExists($rootPath . '/gruntfile.js') + || $this->fileDriver->isExists($rootPath . '/grunt-config.json'); + } + + /** + * Check if theme is from vendor directory + * + * Vendor themes are installed via Composer and should not be modified. + * They typically have pre-built assets and don't require compilation. + * + * @param string $themePath + * @return bool + */ + private function isVendorTheme(string $themePath): bool + { + return str_contains($themePath, '/vendor/'); + } }