Skip to content

Commit 062bd02

Browse files
committed
feat: add NodeSetupValidator for validating Magento default setup files
1 parent 725b069 commit 062bd02

3 files changed

Lines changed: 346 additions & 1 deletion

File tree

src/Service/NodeSetupValidator.php

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

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)