Skip to content

Commit 5330e17

Browse files
authored
feat: Node.js/Grunt setup detection for improved build process (#114)
* feat: add Node.js/Grunt setup check and improve build process for gruntless themes like backend-theme * feat: enhance build process for vendor themes and improve feedback * feat: refactor Grunt task execution and integrate into theme builder * feat: add spacing for vendor theme warning in build process * feat: update output handling for grunt task execution in verbose mode
1 parent 3411257 commit 5330e17

File tree

5 files changed

+182
-37
lines changed

5 files changed

+182
-37
lines changed

docs/advanced_usage.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,38 @@ This document provides detailed information and advanced tips for using MageForg
3737
### Standard Magento Themes (LESS)
3838

3939
For traditional LESS-based Magento themes, MageForge handles:
40-
- LESS compilation
40+
- LESS compilation via Grunt
4141
- Source map generation
4242
- Minification for production
4343

44+
#### Vendor Themes
45+
46+
MageForge automatically detects themes installed via Composer (located in `vendor/` directory):
47+
- **Build mode**: Skips all Grunt/Node.js steps as vendors themes have pre-built assets
48+
- **Watch mode**: Returns an error as vendor themes are read-only and cannot be modified
49+
50+
This prevents accidental modification attempts and ensures build process stability.
51+
52+
#### Themes Without Node.js/Grunt Setup
53+
54+
MageForge automatically detects if a Magento Standard theme intentionally omits Node.js/Grunt setup. If none of the following files exist:
55+
- `package.json`
56+
- `package-lock.json`
57+
- `gruntfile.js`
58+
- `grunt-config.json`
59+
60+
The builder will skip all Node/Grunt-related steps and only:
61+
- Clean static content (if in developer mode)
62+
- Deploy static content
63+
- Clean cache
64+
65+
This is useful for:
66+
- Themes that use pre-compiled CSS
67+
- Minimal themes without custom LESS
68+
- Simple theme inheritance without asset compilation
69+
70+
**Note**: Watch mode requires Node.js/Grunt setup and will return an error if these files are missing.
71+
4472
### Hyvä Themes (Tailwind CSS)
4573

4674
MageForge streamlines Hyvä theme development with:

docs/commands.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,9 @@ The commands rely on several services for their functionality:
470470
- `BuilderPool`: Manages theme builders and selects appropriate builders for themes
471471
- `BuilderInterface`: Implemented by all theme builders
472472
- `MagentoStandard\Builder`: Processes standard Magento LESS-based themes
473+
- Automatically detects if Node.js/Grunt setup is present
474+
- Skips Node/Grunt steps if intentionally omitted (no package.json, package-lock.json, gruntfile.js or grunt-config.json)
475+
- Only performs static content deployment and cache cleaning for themes without build tools
473476
- Various other builders for different theme types
474477

475478
### Theme Services

docs/custom_theme_builders.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,61 @@ public function autoRepair(string $themePath, SymfonyStyle $io, OutputInterface
308308
}
309309
```
310310

311+
### Best Practice: Optional Build Tool Setup
312+
313+
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:
314+
315+
```php
316+
private function hasNodeSetup(): bool
317+
{
318+
$rootPath = '.';
319+
320+
return $this->fileDriver->isExists($rootPath . '/package.json')
321+
|| $this->fileDriver->isExists($rootPath . '/package-lock.json')
322+
|| $this->fileDriver->isExists($rootPath . '/gruntfile.js')
323+
|| $this->fileDriver->isExists($rootPath . '/grunt-config.json');
324+
}
325+
326+
private function isVendorTheme(string $themePath): bool
327+
{
328+
return str_contains($themePath, '/vendor/');
329+
}
330+
331+
public function build(string $themeCode, string $themePath, SymfonyStyle $io, OutputInterface $output, bool $isVerbose): bool
332+
{
333+
if (!$this->detect($themePath)) {
334+
return false;
335+
}
336+
337+
// Check if this is a vendor theme (read-only, pre-built assets)
338+
if ($this->isVendorTheme($themePath)) {
339+
if ($isVerbose) {
340+
$io->note('Vendor theme detected. Skipping build steps (pre-built assets expected).');
341+
}
342+
} elseif ($this->hasNodeSetup()) {
343+
// Check if Node/Grunt setup exists
344+
if (!$this->autoRepair($themePath, $io, $output, $isVerbose)) {
345+
return false;
346+
}
347+
348+
// Execute build commands...
349+
} else {
350+
if ($isVerbose) {
351+
$io->note('No Node.js setup detected. Skipping Node/Grunt steps.');
352+
}
353+
}
354+
355+
// Continue with other build steps (deploy, cache, etc.)
356+
return true;
357+
}
358+
```
359+
360+
This approach:
361+
- Prevents modification attempts on read-only vendor themes
362+
- Allows themes to work without specific build tools
363+
- Still supports full builds when tools are present
364+
- Provides clear feedback about what's being skipped
365+
311366
### The watch() Method
312367

313368
This method starts a process that monitors changes to theme files and automatically rebuilds when necessary:

src/Service/GruntTaskRunner.php

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,26 @@ public function runTasks(
2323
bool $isVerbose
2424
): bool {
2525
try {
26-
foreach (['clean', 'less'] as $task) {
27-
$shellOutput = $this->shell->execute(self::GRUNT_PATH . ' ' . $task . ' --quiet');
28-
if ($isVerbose) {
29-
$output->writeln($shellOutput);
30-
$io->success("'grunt $task' has been successfully executed.");
31-
}
26+
if ($isVerbose) {
27+
$io->text('Running grunt clean...');
28+
$output->writeln($this->shell->execute(self::GRUNT_PATH . ' clean'));
29+
} else {
30+
$this->shell->execute(self::GRUNT_PATH . ' clean --quiet');
31+
}
32+
33+
if ($isVerbose) {
34+
$io->text('Running grunt less...');
35+
$output->writeln($this->shell->execute(self::GRUNT_PATH . ' less'));
36+
} else {
37+
$this->shell->execute(self::GRUNT_PATH . ' less --quiet');
38+
}
39+
40+
if ($isVerbose) {
41+
$io->success('Grunt tasks completed successfully.');
3242
}
3343
return true;
3444
} catch (\Exception $e) {
35-
$io->error($e->getMessage());
45+
$io->error('Failed to run grunt tasks: ' . $e->getMessage());
3646
return false;
3747
}
3848
}

src/Service/ThemeBuilder/MagentoStandard/Builder.php

Lines changed: 78 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Magento\Framework\Filesystem\Driver\File;
88
use Magento\Framework\Shell;
99
use OpenForgeProject\MageForge\Service\CacheCleaner;
10+
use OpenForgeProject\MageForge\Service\GruntTaskRunner;
1011
use OpenForgeProject\MageForge\Service\NodePackageManager;
1112
use OpenForgeProject\MageForge\Service\StaticContentCleaner;
1213
use OpenForgeProject\MageForge\Service\StaticContentDeployer;
@@ -26,7 +27,8 @@ public function __construct(
2627
private readonly StaticContentCleaner $staticContentCleaner,
2728
private readonly CacheCleaner $cacheCleaner,
2829
private readonly SymlinkCleaner $symlinkCleaner,
29-
private readonly NodePackageManager $nodePackageManager
30+
private readonly NodePackageManager $nodePackageManager,
31+
private readonly GruntTaskRunner $gruntTaskRunner
3032
) {
3133
}
3234

@@ -52,37 +54,18 @@ public function build(string $themeCode, string $themePath, SymfonyStyle $io, Ou
5254
return false;
5355
}
5456

55-
if (!$this->autoRepair($themePath, $io, $output, $isVerbose)) {
56-
return false;
57-
}
58-
59-
// Clean symlinks in web/css/ directory before build
60-
if (!$this->symlinkCleaner->cleanSymlinks($themePath, $io, $isVerbose)) {
61-
return false;
62-
}
63-
64-
// Run grunt tasks
65-
try {
66-
if ($isVerbose) {
67-
$io->text('Running grunt clean...');
68-
$this->shell->execute('node_modules/.bin/grunt clean');
69-
} else {
70-
$this->shell->execute('node_modules/.bin/grunt clean --quiet');
71-
}
72-
73-
if ($isVerbose) {
74-
$io->text('Running grunt less...');
75-
$this->shell->execute('node_modules/.bin/grunt less');
76-
} else {
77-
$this->shell->execute('node_modules/.bin/grunt less --quiet');
57+
// Check if this is a vendor theme (read-only, pre-built assets)
58+
if ($this->isVendorTheme($themePath)) {
59+
$io->warning('Vendor theme detected. Skipping Grunt steps.');
60+
$io->newLine(2);
61+
} elseif ($this->hasNodeSetup()) {
62+
if (!$this->processNodeSetup($themePath, $io, $output, $isVerbose)) {
63+
return false;
7864
}
79-
65+
} else {
8066
if ($isVerbose) {
81-
$io->success('Grunt tasks completed successfully.');
67+
$io->note('No Node.js/Grunt setup detected. Skipping Grunt steps.');
8268
}
83-
} catch (\Exception $e) {
84-
$io->error('Failed to run grunt tasks: ' . $e->getMessage());
85-
return false;
8669
}
8770

8871
// Deploy static content
@@ -98,6 +81,29 @@ public function build(string $themeCode, string $themePath, SymfonyStyle $io, Ou
9881
return true;
9982
}
10083

84+
/**
85+
* Process Node.js and Grunt setup
86+
*/
87+
private function processNodeSetup(
88+
string $themePath,
89+
SymfonyStyle $io,
90+
OutputInterface $output,
91+
bool $isVerbose
92+
): bool {
93+
// Check if Node/Grunt setup exists
94+
if (!$this->autoRepair($themePath, $io, $output, $isVerbose)) {
95+
return false;
96+
}
97+
98+
// Clean symlinks in web/css/ directory before build
99+
if (!$this->symlinkCleaner->cleanSymlinks($themePath, $io, $isVerbose)) {
100+
return false;
101+
}
102+
103+
// Run grunt tasks
104+
return $this->gruntTaskRunner->runTasks($io, $output, $isVerbose);
105+
}
106+
101107
public function autoRepair(string $themePath, SymfonyStyle $io, OutputInterface $output, bool $isVerbose): bool
102108
{
103109
$rootPath = '.';
@@ -177,6 +183,18 @@ public function watch(string $themeCode, string $themePath, SymfonyStyle $io, Ou
177183
return false;
178184
}
179185

186+
// Vendor themes cannot be watched (read-only)
187+
if ($this->isVendorTheme($themePath)) {
188+
$io->error('Watch mode is not supported for vendor themes. Vendor themes are read-only and should have pre-built assets.');
189+
return false;
190+
}
191+
192+
// Check if Node/Grunt setup is intentionally absent
193+
if (!$this->hasNodeSetup()) {
194+
$io->error('Watch mode requires Node.js/Grunt setup. No package.json, package-lock.json, node_modules, or grunt-config.json found.');
195+
return false;
196+
}
197+
180198
// Clean static content if in developer mode
181199
if (!$this->staticContentCleaner->cleanIfNeeded($themeCode, $io, $output, $isVerbose)) {
182200
return false;
@@ -213,4 +231,35 @@ public function getName(): string
213231
{
214232
return self::THEME_NAME;
215233
}
234+
235+
/**
236+
* Check if Node.js/Grunt setup exists
237+
*
238+
* Returns true if at least one of the required files exists
239+
*
240+
* @return bool
241+
*/
242+
private function hasNodeSetup(): bool
243+
{
244+
$rootPath = '.';
245+
246+
return $this->fileDriver->isExists($rootPath . '/package.json')
247+
|| $this->fileDriver->isExists($rootPath . '/package-lock.json')
248+
|| $this->fileDriver->isExists($rootPath . '/gruntfile.js')
249+
|| $this->fileDriver->isExists($rootPath . '/grunt-config.json');
250+
}
251+
252+
/**
253+
* Check if theme is from vendor directory
254+
*
255+
* Vendor themes are installed via Composer and should not be modified.
256+
* They typically have pre-built assets and don't require compilation.
257+
*
258+
* @param string $themePath
259+
* @return bool
260+
*/
261+
private function isVendorTheme(string $themePath): bool
262+
{
263+
return str_contains($themePath, '/vendor/');
264+
}
216265
}

0 commit comments

Comments
 (0)