diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ec3bcd..ab28ab5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added +* Add initial JavaScript/TypeScript/Fractal support to `versa install` and `versa run`. * Add a Symfony project type. * Automatically use PHPUnit or ParaTest based on `require-dev` dependencies. * Automatically find the PHP project type (i.e. Drupal or Sculpin) based on its `composer.json` dependencies. diff --git a/src/Console/Command/BuildCommand.php b/src/Console/Command/BuildCommand.php index 3b29b8c..04363d7 100644 --- a/src/Console/Command/BuildCommand.php +++ b/src/Console/Command/BuildCommand.php @@ -2,6 +2,7 @@ namespace App\Console\Command; +use App\Enum\ProjectLanguage; use App\Enum\ProjectType; use App\Process\Process; use RuntimeException; @@ -14,6 +15,7 @@ final class BuildCommand extends AbstractCommand { public function execute(InputInterface $input, OutputInterface $output): int { + $projectLanguage = null; $projectType = null; $extraArgs = $input->getOption('extra-args'); @@ -25,6 +27,8 @@ final class BuildCommand extends AbstractCommand // based on its dependencies. // TODO: move this logic to a service so it can be tested. if ($filesystem->exists($workingDir.'/composer.json')) { + $projectLanguage = ProjectLanguage::PHP->value; + $json = json_decode( json: strval(file_get_contents($workingDir.'/composer.json')), associative: true, @@ -39,40 +43,61 @@ final class BuildCommand extends AbstractCommand } elseif (in_array(needle: 'symfony/framework-bundle', haystack: $dependencies, strict: true)) { $projectType = ProjectType::Symfony->value; } + } elseif ($filesystem->exists($workingDir.'/fractal.config.js')) { + $projectLanguage = ProjectLanguage::JavaScript->value; + $projectType = ProjectType::Fractal->value; } - // Even if the project type is found automatically, still override it with - // the option value if there is one. + // Even if the project language or type is found automatically, still + // override it with the option value if there is one. + $projectLanguage = $input->getOption('language') ?? $projectLanguage; $projectType = $input->getOption('type') ?? $projectType; $isDockerCompose = $filesystem->exists($workingDir . '/docker-compose.yaml'); - switch ($projectType) { - case ProjectType::Drupal->value: - if ($isDockerCompose) { - $process = Process::create( - command: ['docker', 'compose', 'build'], - extraArgs: $extraArgs, - workingDir: $workingDir, - ); + switch ($projectLanguage) { + case ProjectLanguage::PHP->value: + switch ($projectType) { + case ProjectType::Drupal->value: + if ($isDockerCompose) { + $process = Process::create( + command: ['docker', 'compose', 'build'], + extraArgs: $extraArgs, + workingDir: $workingDir, + ); - $process->run(); + $process->run(); + } + break; + + case ProjectType::Symfony->value: + // TODO: run humbug/box if added to generate a phar? + throw new RuntimeException('No build command set for Symfony projects.'); + + case ProjectType::Sculpin->value: + $process = Process::create( + command: ['./vendor/bin/sculpin', 'generate'], + extraArgs: $extraArgs, + workingDir: $workingDir, + ); + + $process->run(); + break; + } + + case ProjectLanguage::JavaScript->value: + switch ($projectType) { + case ProjectType::Fractal->value: + $process = Process::create( + command: ['npx', 'fractal', 'build'], + extraArgs: $extraArgs, + workingDir: $workingDir, + ); + + $process->run(); + break; } break; - - case ProjectType::Symfony->value: - // TODO: run humbug/box if added to generate a phar? - throw new RuntimeException('No build command set for Symfony projects.'); - - case ProjectType::Sculpin->value: - $process = Process::create( - command: ['./vendor/bin/sculpin', 'generate'], - extraArgs: $extraArgs, - workingDir: $workingDir, - ); - - $process->run(); - break; } return Command::SUCCESS; diff --git a/src/Console/Command/InstallCommand.php b/src/Console/Command/InstallCommand.php index 862c03d..5f5e923 100644 --- a/src/Console/Command/InstallCommand.php +++ b/src/Console/Command/InstallCommand.php @@ -21,10 +21,13 @@ final class InstallCommand extends AbstractCommand // TODO: validate the language is an allowed value. + $filesystem = new Filesystem(); + // TODO: Composer in Docker Compose? $process = Process::create( command: $this->getCommand( - language: $input->getOption('language'), + filesystem: $filesystem, + language: $this->getProjectLanguage($filesystem, $workingDir, $input), workingDir: $workingDir, ), extraArgs: $extraArgs, @@ -38,14 +41,13 @@ final class InstallCommand extends AbstractCommand } /** + * @param Filesystem $filesystem * @param non-empty-string $language * @param non-empty-string $workingDir * @return non-empty-array */ - private function getCommand(string $language, string $workingDir): array + private function getCommand(Filesystem $filesystem, string $language, string $workingDir): array { - $filesystem = new Filesystem(); - if ($language === ProjectLanguage::JavaScript->value) { if ($filesystem->exists($workingDir.'/yarn.lock')) { return ['yarn']; @@ -59,4 +61,22 @@ final class InstallCommand extends AbstractCommand return ['composer', 'install']; } + /** + * @param Filesystem $filesystem + * @param non-empty-string $workingDir + * @param InputInterface $input + * @return non-empty-string + */ + private function getProjectLanguage(Filesystem $filesystem, string $workingDir, InputInterface $input): string { + $projectLanguage = null; + + // Determine the language based on the files. + if ($filesystem->exists($workingDir.'/composer.json')) { + $projectLanguage = ProjectLanguage::PHP->value; + } elseif ($filesystem->exists($workingDir.'/package.json')) { + $projectLanguage = ProjectLanguage::JavaScript->value; + } + + return $input->getOption('language') ?? $projectLanguage; + } } diff --git a/src/Console/Command/RunCommand.php b/src/Console/Command/RunCommand.php index 637e6bd..6ed117c 100644 --- a/src/Console/Command/RunCommand.php +++ b/src/Console/Command/RunCommand.php @@ -2,6 +2,7 @@ namespace App\Console\Command; +use App\Enum\ProjectLanguage; use App\Enum\ProjectType; use App\Process\Process; use Symfony\Component\Console\Command\Command; @@ -13,31 +14,45 @@ final class RunCommand extends AbstractCommand { public function execute(InputInterface $input, OutputInterface $output): int { + $projectLanguage = null; $projectType = null; $extraArgs = $input->getOption('extra-args'); $workingDir = $input->getOption('working-dir'); + $filesystem = new Filesystem(); + // Attempt to prepopulate some of the options, such as the project type // based on its dependencies. // TODO: move this logic to a service so it can be tested. - $json = json_decode( - json: strval(file_get_contents($workingDir.'/composer.json')), - associative: true, - ); + if ($filesystem->exists($workingDir.'/composer.json')) { + $projectLanguage = ProjectLanguage::PHP->value; - $dependencies = array_keys($json['require']); + $json = json_decode( + json: strval(file_get_contents($workingDir.'/composer.json')), + associative: true, + ); - if (in_array(needle: 'drupal/core', haystack: $dependencies, strict: true) || in_array(needle: 'drupal/core-recommended', haystack: $dependencies, strict: true)) { - $projectType = ProjectType::Drupal->value; - } elseif (in_array(needle: 'sculpin/sculpin', haystack: $dependencies, strict: true)) { - $projectType = ProjectType::Sculpin->value; - } elseif (in_array(needle: 'symfony/framework-bundle', haystack: $dependencies, strict: true)) { - $projectType = ProjectType::Symfony->value; + $dependencies = array_keys($json['require']); + + if (in_array(needle: 'drupal/core', haystack: $dependencies, strict: true) || in_array(needle: 'drupal/core-recommended', haystack: $dependencies, strict: true)) { + $projectType = ProjectType::Drupal->value; + } elseif (in_array(needle: 'sculpin/sculpin', haystack: $dependencies, strict: true)) { + $projectType = ProjectType::Sculpin->value; + } elseif (in_array(needle: 'symfony/framework-bundle', haystack: $dependencies, strict: true)) { + $projectType = ProjectType::Symfony->value; + } + } elseif ($filesystem->exists($workingDir.'/package.json')) { + $projectLanguage = ProjectLanguage::JavaScript->value; + + if ($filesystem->exists($workingDir.'/fractal.config.js')) { + $projectType = ProjectType::Fractal->value; + } } // Even if the project type is found automatically, still override it with // the option value if there is one. + $projectLanguage = $input->getOption('language') ?? $projectLanguage; $projectType = $input->getOption('type') ?? $projectType; $filesystem = new Filesystem(); @@ -49,19 +64,30 @@ final class RunCommand extends AbstractCommand extraArgs: $extraArgs, workingDir: $workingDir, ); - $process->setTimeout(null); + $process->setTimeout(null); $process->run(); } else { switch ($projectType) { + case ProjectType::Fractal->value: + $process = Process::create( + command: ['npx', 'fractal', 'start', '--sync'], + extraArgs: $extraArgs, + workingDir: $workingDir, + ); + + $process->setTimeout(null); + $process->run(); + break; + case ProjectType::Sculpin->value: $process = Process::create( command: ['./vendor/bin/sculpin', 'generate', '--server', '--watch'], extraArgs: $extraArgs, workingDir: $workingDir, ); - $process->setTimeout(null); + $process->setTimeout(null); $process->run(); break; } diff --git a/src/Enum/ProjectType.php b/src/Enum/ProjectType.php index c036761..f5f77b2 100644 --- a/src/Enum/ProjectType.php +++ b/src/Enum/ProjectType.php @@ -6,6 +6,9 @@ namespace App\Enum; enum ProjectType: string { + // JavaScript. + case Fractal = 'fractal'; + // PHP. case Drupal = 'drupal'; case Sculpin = 'sculpin';