Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion docs/advanced_usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
55 changes: 55 additions & 0 deletions docs/custom_theme_builders.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
24 changes: 17 additions & 7 deletions src/Service/GruntTaskRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down
107 changes: 78 additions & 29 deletions src/Service/ThemeBuilder/MagentoStandard/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
) {
}

Expand All @@ -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
Expand All @@ -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 = '.';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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/');
}
}
Loading