Skip to content

Commit 80a6abf

Browse files
authored
feat: implement StaticContentCleaner and update theme build commands (#83)
1 parent 34640fa commit 80a6abf

10 files changed

Lines changed: 459 additions & 242 deletions

File tree

src/Console/Command/Theme/BuildCommand.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ private function buildValidatedTheme(
271271
}
272272

273273
// Build the theme
274-
if (!$builder->build($themePath, $io, $output, $isVerbose)) {
274+
if (!$builder->build($themeCode, $themePath, $io, $output, $isVerbose)) {
275275
$io->error("Failed to build theme $themeCode.");
276276
return false;
277277
}

src/Console/Command/Theme/CleanCommand.php

Lines changed: 8 additions & 223 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@
55
namespace OpenForgeProject\MageForge\Console\Command\Theme;
66

77
use Laravel\Prompts\MultiSelectPrompt;
8-
use Magento\Framework\App\Filesystem\DirectoryList;
98
use Magento\Framework\Console\Cli;
10-
use Magento\Framework\Filesystem;
119
use OpenForgeProject\MageForge\Console\Command\AbstractCommand;
1210
use OpenForgeProject\MageForge\Model\ThemeList;
1311
use OpenForgeProject\MageForge\Model\ThemePath;
12+
use OpenForgeProject\MageForge\Service\ThemeCleaner;
1413
use OpenForgeProject\MageForge\Service\ThemeSuggester;
1514
use Symfony\Component\Console\Input\InputArgument;
1615
use Symfony\Component\Console\Input\InputInterface;
@@ -27,13 +26,13 @@ class CleanCommand extends AbstractCommand
2726
private array $secureEnvStorage = [];
2827

2928
/**
30-
* @param Filesystem $filesystem
29+
* @param ThemeCleaner $themeCleaner
3130
* @param ThemeList $themeList
3231
* @param ThemePath $themePath
3332
* @param ThemeSuggester $themeSuggester
3433
*/
3534
public function __construct(
36-
private readonly Filesystem $filesystem,
35+
private readonly ThemeCleaner $themeCleaner,
3736
private readonly ThemeList $themeList,
3837
private readonly ThemePath $themePath,
3938
private readonly ThemeSuggester $themeSuggester
@@ -326,13 +325,13 @@ private function cleanThemeDirectories(string $themeName, bool $dryRun): int
326325
static $globalCleaned = false;
327326

328327
$cleaned = 0;
329-
$cleaned += $this->cleanViewPreprocessed($themeName, $dryRun);
330-
$cleaned += $this->cleanPubStatic($themeName, $dryRun);
328+
$cleaned += $this->themeCleaner->cleanViewPreprocessed($themeName, $this->io, $dryRun, true);
329+
$cleaned += $this->themeCleaner->cleanPubStatic($themeName, $this->io, $dryRun, true);
331330

332331
if (!$globalCleaned) {
333-
$cleaned += $this->cleanPageCache($dryRun);
334-
$cleaned += $this->cleanVarTmp($dryRun);
335-
$cleaned += $this->cleanGenerated($dryRun);
332+
$cleaned += $this->themeCleaner->cleanPageCache($this->io, $dryRun, true);
333+
$cleaned += $this->themeCleaner->cleanVarTmp($this->io, $dryRun, true);
334+
$cleaned += $this->themeCleaner->cleanGenerated($this->io, $dryRun, true);
336335
$globalCleaned = true;
337336
}
338337
return $cleaned;
@@ -682,218 +681,4 @@ private function clearEnvironmentCache(): void
682681
$this->secureEnvStorage = [];
683682
self::$cachedEnv = null;
684683
}
685-
686-
/**
687-
* Clean var/view_preprocessed directory for the theme
688-
*
689-
* @param string $themeName
690-
* @param bool $dryRun
691-
* @return int Number of directories cleaned
692-
*/
693-
private function cleanViewPreprocessed(string $themeName, bool $dryRun = false): int
694-
{
695-
$cleaned = 0;
696-
$varDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::VAR_DIR);
697-
698-
// Extract vendor and theme parts
699-
$themeParts = $this->parseThemeName($themeName);
700-
if ($themeParts === null) {
701-
return 0;
702-
}
703-
704-
[$vendor, $theme] = $themeParts;
705-
706-
// Check if view_preprocessed directory exists
707-
if (!$varDirectory->isDirectory('view_preprocessed')) {
708-
return 0;
709-
}
710-
711-
// Pattern: view_preprocessed/css/frontend/Vendor/theme
712-
// and view_preprocessed/source/frontend/Vendor/theme
713-
$pathsToClean = [
714-
sprintf('view_preprocessed/css/frontend/%s/%s', $vendor, $theme),
715-
sprintf('view_preprocessed/source/frontend/%s/%s', $vendor, $theme),
716-
];
717-
718-
foreach ($pathsToClean as $path) {
719-
if ($varDirectory->isDirectory($path)) {
720-
try {
721-
if (!$dryRun) {
722-
$varDirectory->delete($path);
723-
}
724-
$action = $dryRun ? 'Would clean' : 'Cleaned';
725-
$this->io->writeln(sprintf(' <fg=green>✓</> %s: var/%s', $action, $path));
726-
$cleaned++;
727-
} catch (\Exception $e) {
728-
$this->io->writeln(sprintf(' <fg=red>✗</> Failed to clean: var/%s - %s', $path, $e->getMessage()));
729-
}
730-
}
731-
}
732-
733-
return $cleaned;
734-
}
735-
736-
/**
737-
* Clean pub/static directory for the theme
738-
*
739-
* @param string $themeName
740-
* @param bool $dryRun
741-
* @return int Number of directories cleaned
742-
*/
743-
private function cleanPubStatic(string $themeName, bool $dryRun = false): int
744-
{
745-
$cleaned = 0;
746-
$staticDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::STATIC_VIEW);
747-
748-
// Extract vendor and theme parts
749-
$themeParts = $this->parseThemeName($themeName);
750-
if ($themeParts === null) {
751-
return 0;
752-
}
753-
754-
[$vendor, $theme] = $themeParts;
755-
756-
// Check if frontend directory exists in pub/static
757-
if (!$staticDirectory->isDirectory('frontend')) {
758-
return 0;
759-
}
760-
761-
// Pattern: frontend/Vendor/theme
762-
$pathToClean = sprintf('frontend/%s/%s', $vendor, $theme);
763-
764-
if ($staticDirectory->isDirectory($pathToClean)) {
765-
try {
766-
if (!$dryRun) {
767-
$staticDirectory->delete($pathToClean);
768-
}
769-
$action = $dryRun ? 'Would clean' : 'Cleaned';
770-
$this->io->writeln(sprintf(' <fg=green>✓</> %s: pub/static/%s', $action, $pathToClean));
771-
$cleaned++;
772-
} catch (\Exception $e) {
773-
$this->io->writeln(sprintf(' <fg=red>✗</> Failed to clean: pub/static/%s - %s', $pathToClean, $e->getMessage()));
774-
}
775-
}
776-
777-
return $cleaned;
778-
}
779-
780-
/**
781-
* Clean var/page_cache directory
782-
*
783-
* @param bool $dryRun
784-
* @return int Number of directories cleaned
785-
*/
786-
private function cleanPageCache(bool $dryRun = false): int
787-
{
788-
$cleaned = 0;
789-
$varDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::VAR_DIR);
790-
791-
if ($varDirectory->isDirectory('page_cache')) {
792-
try {
793-
if (!$dryRun) {
794-
$varDirectory->delete('page_cache');
795-
}
796-
$action = $dryRun ? 'Would clean' : 'Cleaned';
797-
$this->io->writeln(sprintf(' <fg=green>✓</> %s: var/page_cache', $action));
798-
$cleaned++;
799-
} catch (\Exception $e) {
800-
$this->io->writeln(sprintf(' <fg=red>✗</> Failed to clean: var/page_cache - %s', $e->getMessage()));
801-
}
802-
}
803-
804-
return $cleaned;
805-
}
806-
807-
/**
808-
* Clean var/tmp directory
809-
*
810-
* @param bool $dryRun
811-
* @return int Number of directories cleaned
812-
*/
813-
private function cleanVarTmp(bool $dryRun = false): int
814-
{
815-
$cleaned = 0;
816-
$varDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::VAR_DIR);
817-
818-
if ($varDirectory->isDirectory('tmp')) {
819-
try {
820-
if (!$dryRun) {
821-
$varDirectory->delete('tmp');
822-
}
823-
$action = $dryRun ? 'Would clean' : 'Cleaned';
824-
$this->io->writeln(sprintf(' <fg=green>✓</> %s: var/tmp', $action));
825-
$cleaned++;
826-
} catch (\Exception $e) {
827-
$this->io->writeln(sprintf(' <fg=red>✗</> Failed to clean: var/tmp - %s', $e->getMessage()));
828-
}
829-
}
830-
831-
return $cleaned;
832-
}
833-
834-
/**
835-
* Clean generated directory
836-
*
837-
* @param bool $dryRun
838-
* @return int Number of directories cleaned
839-
*/
840-
private function cleanGenerated(bool $dryRun = false): int
841-
{
842-
$cleaned = 0;
843-
$generatedDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::GENERATED);
844-
845-
try {
846-
$deletedCount = 0;
847-
848-
// Manually check for 'code' directory (the main generated content)
849-
if ($generatedDirectory->isDirectory('code')) {
850-
try {
851-
if (!$dryRun) {
852-
$generatedDirectory->delete('code');
853-
}
854-
$deletedCount++;
855-
} catch (\Exception $e) {
856-
$this->io->writeln(sprintf(' <fg=red>✗</> Failed to clean: generated/code - %s', $e->getMessage()));
857-
}
858-
}
859-
860-
// Check for 'metadata' directory
861-
if ($generatedDirectory->isDirectory('metadata')) {
862-
try {
863-
if (!$dryRun) {
864-
$generatedDirectory->delete('metadata');
865-
}
866-
$deletedCount++;
867-
} catch (\Exception $e) {
868-
$this->io->writeln(sprintf(' <fg=red>✗</> Failed to clean: generated/metadata - %s', $e->getMessage()));
869-
}
870-
}
871-
872-
if ($deletedCount > 0) {
873-
$action = $dryRun ? 'Would clean' : 'Cleaned';
874-
$this->io->writeln(sprintf(' <fg=green>✓</> %s: generated/*', $action));
875-
$cleaned++;
876-
}
877-
} catch (\Exception $e) {
878-
$this->io->writeln(sprintf(' <fg=red>✗</> Failed to clean: generated - %s', $e->getMessage()));
879-
}
880-
881-
return $cleaned;
882-
}
883-
884-
/**
885-
* Parse theme name into vendor and theme parts
886-
*
887-
* @param string $themeName
888-
* @return array|null Array with [vendor, theme] or null if invalid format
889-
*/
890-
private function parseThemeName(string $themeName): ?array
891-
{
892-
$themeParts = explode('/', $themeName);
893-
if (count($themeParts) !== 2) {
894-
return null;
895-
}
896-
897-
return $themeParts;
898-
}
899684
}

src/Console/Command/Theme/WatchCommand.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,6 @@ protected function executeCommand(InputInterface $input, OutputInterface $output
114114
}
115115

116116
$builder = $this->builderPool->getBuilder($themePath);
117-
return $builder->watch($themePath, $this->io, $output, $isVerbose) ? self::SUCCESS : self::FAILURE;
117+
return $builder->watch($themeCode, $themePath, $this->io, $output, $isVerbose) ? self::SUCCESS : self::FAILURE;
118118
}
119119
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenForgeProject\MageForge\Service;
6+
7+
use Magento\Framework\App\State;
8+
use Symfony\Component\Console\Output\OutputInterface;
9+
use Symfony\Component\Console\Style\SymfonyStyle;
10+
11+
/**
12+
* Service for automatically cleaning static content before theme builds
13+
*
14+
* In developer mode, Magento generates static files on-demand which can
15+
* conflict with theme builder outputs. This service automatically detects
16+
* and cleans such files before build/watch operations.
17+
*/
18+
class StaticContentCleaner
19+
{
20+
public function __construct(
21+
private readonly State $state,
22+
private readonly ThemeCleaner $themeCleaner
23+
) {
24+
}
25+
26+
/**
27+
* Clean static content for a theme if in developer mode and files exist
28+
*
29+
* @param string $themeCode Theme code (e.g., "Magento/luma")
30+
* @param SymfonyStyle $io Symfony style output
31+
* @param OutputInterface $output Console output interface
32+
* @param bool $isVerbose Whether to show verbose output
33+
* @return bool True if cleaning was performed or not needed, false on error
34+
*/
35+
public function cleanIfNeeded(
36+
string $themeCode,
37+
SymfonyStyle $io,
38+
OutputInterface $output,
39+
bool $isVerbose
40+
): bool {
41+
try {
42+
// Only clean in developer mode
43+
if ($this->state->getMode() !== State::MODE_DEVELOPER) {
44+
return true;
45+
}
46+
47+
// Check if static files exist for this theme
48+
if (!$this->themeCleaner->hasStaticFiles($themeCode)) {
49+
return true;
50+
}
51+
52+
// Notify user
53+
if ($isVerbose) {
54+
$io->note(sprintf(
55+
"Developer mode detected: Cleaning existing static files for theme '%s'...",
56+
$themeCode
57+
));
58+
}
59+
60+
// Clean the static files and preprocessed views using ThemeCleaner
61+
$cleanedStatic = $this->themeCleaner->cleanPubStatic($themeCode, $io, false, $isVerbose);
62+
$cleanedPreprocessed = $this->themeCleaner->cleanViewPreprocessed($themeCode, $io, false, $isVerbose);
63+
64+
return ($cleanedStatic > 0 || $cleanedPreprocessed > 0) || !$this->themeCleaner->hasStaticFiles($themeCode);
65+
} catch (\Exception $e) {
66+
$io->error('Failed to check/clean static content: ' . $e->getMessage());
67+
return false;
68+
}
69+
}
70+
}

src/Service/ThemeBuilder/BuilderInterface.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
interface BuilderInterface
1111
{
1212
public function detect(string $themePath): bool;
13-
public function build(string $themePath, SymfonyStyle $io, OutputInterface $output, bool $isVerbose): bool;
13+
public function build(string $themeCode, string $themePath, SymfonyStyle $io, OutputInterface $output, bool $isVerbose): bool;
1414
public function getName(): string;
1515
public function autoRepair(string $themePath, SymfonyStyle $io, OutputInterface $output, bool $isVerbose): bool;
16-
public function watch(string $themePath, SymfonyStyle $io, OutputInterface $output, bool $isVerbose): bool;
16+
public function watch(string $themeCode, string $themePath, SymfonyStyle $io, OutputInterface $output, bool $isVerbose): bool;
1717
}

0 commit comments

Comments
 (0)