From ffa9ac1e9cbfd90a777ab505853b056923809cf9 Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Fri, 30 Jan 2026 11:46:10 +0100 Subject: [PATCH 1/5] feat: add Node.js/Grunt setup check and improve build process for gruntless themes like backend-theme --- docs/advanced_usage.md | 22 ++++- docs/commands.md | 3 + docs/custom_theme_builders.md | 44 ++++++++++ .../ThemeBuilder/MagentoStandard/Builder.php | 82 +++++++++++++------ 4 files changed, 125 insertions(+), 26 deletions(-) diff --git a/docs/advanced_usage.md b/docs/advanced_usage.md index 31396bd..2561d4b 100644 --- a/docs/advanced_usage.md +++ b/docs/advanced_usage.md @@ -37,10 +37,30 @@ 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 +#### 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..e17e29c 100644 --- a/docs/custom_theme_builders.md +++ b/docs/custom_theme_builders.md @@ -308,6 +308,50 @@ 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'); +} + +public function build(string $themeCode, string $themePath, SymfonyStyle $io, OutputInterface $output, bool $isVerbose): bool +{ + if (!$this->detect($themePath)) { + return false; + } + + // Check if Node/Grunt setup is intentionally absent + $hasNodeSetup = $this->hasNodeSetup(); + + if ($hasNodeSetup) { + // Run Node/Grunt build steps + 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 allows themes to work without specific build tools while still supporting full builds when they are present. + ### The watch() Method This method starts a process that monitors changes to theme files and automatically rebuilds when necessary: diff --git a/src/Service/ThemeBuilder/MagentoStandard/Builder.php b/src/Service/ThemeBuilder/MagentoStandard/Builder.php index 7a2cd7c..be11ed0 100644 --- a/src/Service/ThemeBuilder/MagentoStandard/Builder.php +++ b/src/Service/ThemeBuilder/MagentoStandard/Builder.php @@ -52,37 +52,46 @@ 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; - } + // Check if Node/Grunt setup is intentionally absent + $hasNodeSetup = $this->hasNodeSetup(); - // 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 ($hasNodeSetup) { + if (!$this->autoRepair($themePath, $io, $output, $isVerbose)) { + return false; } - 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'); + // Clean symlinks in web/css/ directory before build + if (!$this->symlinkCleaner->cleanSymlinks($themePath, $io, $isVerbose)) { + return false; } - if ($isVerbose) { - $io->success('Grunt tasks completed successfully.'); + // 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'); + } + + if ($isVerbose) { + $io->success('Grunt tasks completed successfully.'); + } + } catch (\Exception $e) { + $io->error('Failed to run grunt tasks: ' . $e->getMessage()); + return false; + } + } else { + if (!$isVerbose) { + $io->success('No Grunt-Setup detected. Skipping Magento Grunt steps.'); } - } catch (\Exception $e) { - $io->error('Failed to run grunt tasks: ' . $e->getMessage()); - return false; } // Deploy static content @@ -177,6 +186,12 @@ public function watch(string $themeCode, string $themePath, SymfonyStyle $io, Ou 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 +228,21 @@ 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'); + } } From 50eaabf7400617c5e9850eca69d2d9f4b51f6eac Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Fri, 30 Jan 2026 11:57:33 +0100 Subject: [PATCH 2/5] feat: enhance build process for vendor themes and improve feedback --- docs/advanced_usage.md | 8 +++++ docs/custom_theme_builders.md | 23 +++++++++---- .../ThemeBuilder/MagentoStandard/Builder.php | 33 +++++++++++++++---- 3 files changed, 52 insertions(+), 12 deletions(-) diff --git a/docs/advanced_usage.md b/docs/advanced_usage.md index 2561d4b..3f6cc9c 100644 --- a/docs/advanced_usage.md +++ b/docs/advanced_usage.md @@ -41,6 +41,14 @@ For traditional LESS-based Magento themes, MageForge handles: - 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: diff --git a/docs/custom_theme_builders.md b/docs/custom_theme_builders.md index e17e29c..47f3045 100644 --- a/docs/custom_theme_builders.md +++ b/docs/custom_theme_builders.md @@ -323,17 +323,24 @@ private function hasNodeSetup(): bool || $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 Node/Grunt setup is intentionally absent - $hasNodeSetup = $this->hasNodeSetup(); - - if ($hasNodeSetup) { - // Run Node/Grunt build steps + // 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; } @@ -350,7 +357,11 @@ public function build(string $themeCode, string $themePath, SymfonyStyle $io, Ou } ``` -This approach allows themes to work without specific build tools while still supporting full builds when they are present. +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 diff --git a/src/Service/ThemeBuilder/MagentoStandard/Builder.php b/src/Service/ThemeBuilder/MagentoStandard/Builder.php index be11ed0..d27fa74 100644 --- a/src/Service/ThemeBuilder/MagentoStandard/Builder.php +++ b/src/Service/ThemeBuilder/MagentoStandard/Builder.php @@ -52,10 +52,11 @@ public function build(string $themeCode, string $themePath, SymfonyStyle $io, Ou return false; } - // Check if Node/Grunt setup is intentionally absent - $hasNodeSetup = $this->hasNodeSetup(); - - if ($hasNodeSetup) { + // Check if this is a vendor theme (read-only, pre-built assets) + if ($this->isVendorTheme($themePath)) { + $io->warning('Vendor theme detected. Skipping Grunt steps.'); + } elseif ($this->hasNodeSetup()) { + // Check if Node/Grunt setup exists if (!$this->autoRepair($themePath, $io, $output, $isVerbose)) { return false; } @@ -89,8 +90,8 @@ public function build(string $themeCode, string $themePath, SymfonyStyle $io, Ou return false; } } else { - if (!$isVerbose) { - $io->success('No Grunt-Setup detected. Skipping Magento Grunt steps.'); + if ($isVerbose) { + $io->note('No Node.js/Grunt setup detected. Skipping Grunt steps.'); } } @@ -186,6 +187,12 @@ 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.'); @@ -245,4 +252,18 @@ private function hasNodeSetup(): bool || $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/'); + } } From 5a9891dea1f798d146a0f8ced16c0bb694ed09eb Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Fri, 30 Jan 2026 12:04:07 +0100 Subject: [PATCH 3/5] feat: refactor Grunt task execution and integrate into theme builder --- src/Service/GruntTaskRunner.php | 24 +++++--- .../ThemeBuilder/MagentoStandard/Builder.php | 59 +++++++++---------- 2 files changed, 44 insertions(+), 39 deletions(-) diff --git a/src/Service/GruntTaskRunner.php b/src/Service/GruntTaskRunner.php index 36d9b13..84ea86f 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...'); + $this->shell->execute(self::GRUNT_PATH . ' clean'); + } else { + $this->shell->execute(self::GRUNT_PATH . ' clean --quiet'); + } + + if ($isVerbose) { + $io->text('Running grunt less...'); + $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 d27fa74..434a05c 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 ) { } @@ -56,37 +58,7 @@ public function build(string $themeCode, string $themePath, SymfonyStyle $io, Ou if ($this->isVendorTheme($themePath)) { $io->warning('Vendor theme detected. Skipping Grunt steps.'); } elseif ($this->hasNodeSetup()) { - // 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 - 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'); - } - - if ($isVerbose) { - $io->success('Grunt tasks completed successfully.'); - } - } catch (\Exception $e) { - $io->error('Failed to run grunt tasks: ' . $e->getMessage()); + if (!$this->processNodeSetup($themePath, $io, $output, $isVerbose)) { return false; } } else { @@ -108,6 +80,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 = '.'; From 041ba58fb21a33e5c1e34b9e77948ba6cd3302fc Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Fri, 30 Jan 2026 12:10:37 +0100 Subject: [PATCH 4/5] feat: add spacing for vendor theme warning in build process --- src/Service/ThemeBuilder/MagentoStandard/Builder.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Service/ThemeBuilder/MagentoStandard/Builder.php b/src/Service/ThemeBuilder/MagentoStandard/Builder.php index 434a05c..f783ac8 100644 --- a/src/Service/ThemeBuilder/MagentoStandard/Builder.php +++ b/src/Service/ThemeBuilder/MagentoStandard/Builder.php @@ -57,6 +57,7 @@ public function build(string $themeCode, string $themePath, SymfonyStyle $io, Ou // 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; From 986e397f93942034d595049b3305ddf4a3310ed3 Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Fri, 30 Jan 2026 12:23:06 +0100 Subject: [PATCH 5/5] feat: update output handling for grunt task execution in verbose mode --- src/Service/GruntTaskRunner.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Service/GruntTaskRunner.php b/src/Service/GruntTaskRunner.php index 84ea86f..395ec66 100644 --- a/src/Service/GruntTaskRunner.php +++ b/src/Service/GruntTaskRunner.php @@ -25,14 +25,14 @@ public function runTasks( try { if ($isVerbose) { $io->text('Running grunt clean...'); - $this->shell->execute(self::GRUNT_PATH . ' 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...'); - $this->shell->execute(self::GRUNT_PATH . ' less'); + $output->writeln($this->shell->execute(self::GRUNT_PATH . ' less')); } else { $this->shell->execute(self::GRUNT_PATH . ' less --quiet'); }