Skip to content

Commit 3f36d43

Browse files
dermatzCopilot
andauthored
feat: add NodeSetupValidator for validating Magento default setup files (#142)
* feat: add NodeSetupValidator for validating Magento default setup files * Update src/Service/NodeSetupValidator.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * chore: add missing import for Laravel prompt confirmation function --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 725b069 commit 3f36d43

3 files changed

Lines changed: 345 additions & 1 deletion

File tree

src/Service/NodeSetupValidator.php

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenForgeProject\MageForge\Service;
6+
7+
use Magento\Framework\Filesystem\Driver\File as FileDriver;
8+
use Symfony\Component\Console\Style\SymfonyStyle;
9+
use function Laravel\Prompts\confirm;
10+
/**
11+
* Service for validating and restoring Node.js setup files for Magento Standard themes
12+
*/
13+
class NodeSetupValidator
14+
{
15+
private const REQUIRED_FILES = [
16+
'package.json',
17+
'Gruntfile.js',
18+
'grunt-config.json',
19+
];
20+
21+
/** @var array<string> Files that will be generated by npm install */
22+
private const GENERATED_FILES = [
23+
'package-lock.json',
24+
];
25+
26+
private const REQUIRED_DIRECTORIES = [
27+
'node_modules',
28+
];
29+
30+
private const NODE_MODULES_DIR = 'node_modules/';
31+
32+
private const MAGENTO_BASE_PATH = 'vendor/magento/magento2-base';
33+
34+
/**
35+
* Constructor
36+
*
37+
* @param FileDriver $fileDriver
38+
* @param NodePackageManager $nodePackageManager
39+
*/
40+
public function __construct(
41+
private readonly FileDriver $fileDriver,
42+
private readonly NodePackageManager $nodePackageManager
43+
) {
44+
}
45+
46+
/**
47+
* Validate Node.js setup files and offer to restore missing ones
48+
*
49+
* @param string $rootPath Root directory to check (usually '.')
50+
* @param SymfonyStyle $io Console IO for output
51+
* @param bool $isVerbose Whether to show verbose output
52+
* @return bool True if validation passed or files were restored successfully
53+
*/
54+
public function validateAndRestore(string $rootPath, SymfonyStyle $io, bool $isVerbose): bool
55+
{
56+
$missing = $this->getMissingFiles($rootPath);
57+
58+
if (empty($missing)) {
59+
if ($isVerbose) {
60+
$io->success('All required Node.js setup files are present.');
61+
}
62+
return true;
63+
}
64+
65+
// Separate source files from generated files/directories
66+
$missingSourceFiles = $this->filterSourceFiles($missing);
67+
68+
// If only generated files/directories are missing, restore them directly without asking
69+
if (empty($missingSourceFiles)) {
70+
return $this->restoreGeneratedFilesAutomatically($rootPath, $missing, $io, $isVerbose);
71+
}
72+
73+
// Ask user if they want to restore missing files
74+
if (!$this->promptUserForRestoration($missing, $io)) {
75+
$io->info('Skipping file restoration.');
76+
return false;
77+
}
78+
79+
// Restore missing files
80+
return $this->restoreMissingFiles($rootPath, $missing, $io, $isVerbose);
81+
}
82+
83+
/**
84+
* Get list of missing files and directories
85+
*
86+
* @param string $rootPath Root directory to check
87+
* @return array<string> List of missing file/directory names
88+
*/
89+
private function getMissingFiles(string $rootPath): array
90+
{
91+
$missing = [];
92+
93+
// Check required files
94+
foreach (self::REQUIRED_FILES as $file) {
95+
if (!$this->fileDriver->isExists($rootPath . '/' . $file)) {
96+
$missing[] = $file;
97+
}
98+
}
99+
100+
// Check generated files
101+
foreach (self::GENERATED_FILES as $file) {
102+
if (!$this->fileDriver->isExists($rootPath . '/' . $file)) {
103+
$missing[] = $file;
104+
}
105+
}
106+
107+
// Check required directories
108+
foreach (self::REQUIRED_DIRECTORIES as $directory) {
109+
if (!$this->fileDriver->isDirectory($rootPath . '/' . $directory)) {
110+
$missing[] = $directory . '/';
111+
}
112+
}
113+
114+
return $missing;
115+
}
116+
117+
/**
118+
* Filter source files from the list of missing files
119+
*
120+
* Returns only files that need to be copied from Magento base
121+
* (excludes generated files and directories)
122+
*
123+
* @param array<string> $missing List of missing files/directories
124+
* @return array<string> List of source files that need to be restored
125+
*/
126+
private function filterSourceFiles(array $missing): array
127+
{
128+
return array_filter($missing, fn($item) => !$this->isGeneratedFileOrDirectory($item));
129+
}
130+
131+
/**
132+
* Check if a file or directory is generated (not a source file)
133+
*
134+
* @param string $item File or directory name
135+
* @return bool True if the item is generated by npm install
136+
*/
137+
private function isGeneratedFileOrDirectory(string $item): bool
138+
{
139+
return in_array($item, self::GENERATED_FILES, true) || $item === self::NODE_MODULES_DIR;
140+
}
141+
142+
/**
143+
* Restore only generated files/directories automatically without user prompt
144+
*
145+
* @param string $rootPath Root directory where files should be restored
146+
* @param array<string> $missing List of missing files/directories
147+
* @param SymfonyStyle $io Console IO for output
148+
* @param bool $isVerbose Whether to show verbose output
149+
* @return bool True if restoration was successful
150+
*/
151+
private function restoreGeneratedFilesAutomatically(
152+
string $rootPath,
153+
array $missing,
154+
SymfonyStyle $io,
155+
bool $isVerbose
156+
): bool {
157+
if ($isVerbose) {
158+
$io->note('Detected missing generated files/directories. Installing automatically...');
159+
foreach ($missing as $item) {
160+
$io->writeln(" - {$item}");
161+
}
162+
}
163+
return $this->restoreMissingFiles($rootPath, $missing, $io, $isVerbose);
164+
}
165+
166+
/**
167+
* Prompt user to confirm file restoration
168+
*
169+
* @param array<string> $missing List of missing files/directories
170+
* @param SymfonyStyle $io Console IO for output
171+
* @return bool True if user confirms restoration
172+
*/
173+
private function promptUserForRestoration(array $missing, SymfonyStyle $io): bool
174+
{
175+
// Display missing files
176+
$io->warning('The following required files/directories are missing:');
177+
foreach ($missing as $item) {
178+
$suffix = $this->isGeneratedFileOrDirectory($item)
179+
? ' (will be generated by npm install)'
180+
: '';
181+
$io->writeln(" - {$item}{$suffix}");
182+
}
183+
$io->newLine();
184+
185+
// Ask user if they want to restore
186+
return confirm(
187+
label: 'Would you like to restore missing files from Magento base?',
188+
default: true,
189+
hint: 'This will copy the standard Magento files to your project root.'
190+
);
191+
}
192+
193+
/**
194+
* Restore missing files from Magento base installation
195+
*
196+
* @param string $rootPath Root directory where files should be restored
197+
* @param array<string> $missing List of missing files/directories
198+
* @param SymfonyStyle $io Console IO for output
199+
* @param bool $isVerbose Whether to show verbose output
200+
* @return bool True if restoration was successful
201+
*/
202+
private function restoreMissingFiles(
203+
string $rootPath,
204+
array $missing,
205+
SymfonyStyle $io,
206+
bool $isVerbose
207+
): bool {
208+
if (empty($missing)) {
209+
return true;
210+
}
211+
212+
$basePath = self::MAGENTO_BASE_PATH;
213+
214+
if (!$this->fileDriver->isDirectory($basePath)) {
215+
$io->error(sprintf(
216+
'Magento base directory not found at: %s',
217+
$basePath
218+
));
219+
return false;
220+
}
221+
222+
$restored = [];
223+
$failed = [];
224+
225+
foreach ($missing as $item) {
226+
// Skip generated files/directories - they will be created by npm install
227+
if ($this->isGeneratedFileOrDirectory($item)) {
228+
if ($isVerbose) {
229+
$io->note("Skipping {$item} - will be generated by npm install");
230+
}
231+
continue;
232+
}
233+
234+
$sourcePath = $basePath . '/' . $item;
235+
$targetPath = $rootPath . '/' . $item;
236+
237+
if (!$this->fileDriver->isExists($sourcePath)) {
238+
if ($isVerbose) {
239+
$io->warning("Source file not found: {$sourcePath}");
240+
}
241+
$failed[] = $item;
242+
continue;
243+
}
244+
245+
try {
246+
$this->fileDriver->copy($sourcePath, $targetPath);
247+
$restored[] = $item;
248+
249+
if ($isVerbose) {
250+
$io->writeln("✓ Restored: {$item}");
251+
}
252+
} catch (\Exception $e) {
253+
$io->error("Failed to restore {$item}: " . $e->getMessage());
254+
$failed[] = $item;
255+
}
256+
}
257+
258+
$this->displayRestorationSummary($restored, $failed, $io);
259+
260+
// If we restored any files or node_modules is missing, run npm install
261+
if ($this->shouldRunNpmInstall($restored, $missing)) {
262+
return $this->runNpmInstall($rootPath, $io, $isVerbose) && empty($failed);
263+
}
264+
265+
return empty($failed);
266+
}
267+
268+
/**
269+
* Display summary of restoration results
270+
*
271+
* @param array<string> $restored List of successfully restored files
272+
* @param array<string> $failed List of failed files
273+
* @param SymfonyStyle $io Console IO for output
274+
* @return void
275+
*/
276+
private function displayRestorationSummary(array $restored, array $failed, SymfonyStyle $io): void
277+
{
278+
if (!empty($restored)) {
279+
$io->success(sprintf(
280+
'Restored %d file(s): %s',
281+
count($restored),
282+
implode(', ', $restored)
283+
));
284+
}
285+
286+
if (!empty($failed)) {
287+
$io->warning(sprintf(
288+
'Failed to restore %d file(s): %s',
289+
count($failed),
290+
implode(', ', $failed)
291+
));
292+
}
293+
}
294+
295+
/**
296+
* Check if npm install should be run
297+
*
298+
* @param array<string> $restored List of restored files
299+
* @param array<string> $missing List of missing files
300+
* @return bool True if npm install should be run
301+
*/
302+
private function shouldRunNpmInstall(array $restored, array $missing): bool
303+
{
304+
return !empty($restored) || in_array(self::NODE_MODULES_DIR, $missing, true);
305+
}
306+
307+
/**
308+
* Run npm install to create node_modules and generated files
309+
*
310+
* @param string $rootPath Root directory
311+
* @param SymfonyStyle $io Console IO for output
312+
* @param bool $isVerbose Whether to show verbose output
313+
* @return bool True if npm install was successful
314+
*/
315+
private function runNpmInstall(string $rootPath, SymfonyStyle $io, bool $isVerbose): bool
316+
{
317+
$io->newLine();
318+
$io->text('Installing Node.js dependencies...');
319+
320+
if (!$this->nodePackageManager->installNodeModules($rootPath, $io, $isVerbose)) {
321+
$io->error('Failed to install Node.js dependencies.');
322+
return false;
323+
}
324+
325+
return true;
326+
}
327+
}

src/Service/ThemeBuilder/MagentoStandard/Builder.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use OpenForgeProject\MageForge\Service\CacheCleaner;
1010
use OpenForgeProject\MageForge\Service\GruntTaskRunner;
1111
use OpenForgeProject\MageForge\Service\NodePackageManager;
12+
use OpenForgeProject\MageForge\Service\NodeSetupValidator;
1213
use OpenForgeProject\MageForge\Service\StaticContentCleaner;
1314
use OpenForgeProject\MageForge\Service\StaticContentDeployer;
1415
use OpenForgeProject\MageForge\Service\SymlinkCleaner;
@@ -28,7 +29,8 @@ public function __construct(
2829
private readonly CacheCleaner $cacheCleaner,
2930
private readonly SymlinkCleaner $symlinkCleaner,
3031
private readonly NodePackageManager $nodePackageManager,
31-
private readonly GruntTaskRunner $gruntTaskRunner
32+
private readonly GruntTaskRunner $gruntTaskRunner,
33+
private readonly NodeSetupValidator $nodeSetupValidator
3234
) {
3335
}
3436

@@ -90,6 +92,13 @@ private function processNodeSetup(
9092
OutputInterface $output,
9193
bool $isVerbose
9294
): bool {
95+
$rootPath = '.';
96+
97+
// Validate and restore Node.js setup files if needed
98+
if (!$this->nodeSetupValidator->validateAndRestore($rootPath, $io, $isVerbose)) {
99+
return false;
100+
}
101+
93102
// Check if Node/Grunt setup exists
94103
if (!$this->autoRepair($themePath, $io, $output, $isVerbose)) {
95104
return false;

src/etc/di.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,12 @@
7272
<argument name="fileDriver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument>
7373
</arguments>
7474
</type>
75+
76+
<!-- Configure NodeSetupValidator Service -->
77+
<type name="OpenForgeProject\MageForge\Service\NodeSetupValidator">
78+
<arguments>
79+
<argument name="fileDriver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument>
80+
<argument name="nodePackageManager" xsi:type="object">OpenForgeProject\MageForge\Service\NodePackageManager</argument>
81+
</arguments>
82+
</type>
7583
</config>

0 commit comments

Comments
 (0)