diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 17d468d..92930c8 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -16,10 +16,10 @@ jobs: strategy: fail-fast: true matrix: - os: [ubuntu-latest, windows-latest] - php: [8.2, 8.3] - laravel: [11.*] - stability: [prefer-lowest, prefer-stable] + os: [ ubuntu-latest, windows-latest ] + php: [ 8.2, 8.3 ] + laravel: [ 11.* ] + stability: [ prefer-lowest, prefer-stable ] include: - laravel: 11.* testbench: 9.* @@ -36,7 +36,7 @@ jobs: with: php-version: ${{ matrix.php }} extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo - coverage: none + coverage: pcov - name: Setup problem matchers run: | diff --git a/.gitignore b/.gitignore index a7f372d..a39542c 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ phpstan.neon testbench.yaml vendor node_modules +skeleton +workbench +modules \ No newline at end of file diff --git a/composer.json b/composer.json index 15d8371..d5b82ae 100644 --- a/composer.json +++ b/composer.json @@ -42,17 +42,20 @@ "autoload-dev": { "psr-4": { "Savannabits\\Modular\\Tests\\": "tests/", - "Workbench\\App\\": "workbench/app/" + "Workbench\\App\\": "workbench/app/", + "Workbench\\Database\\Factories\\": "workbench/database/factories/", + "Workbench\\Database\\Seeders\\": "workbench/database/seeders/" } }, "scripts": { - "post-autoload-dump": "@composer run prepare", - "clear": "@php vendor/bin/testbench package:purge-modular --ansi", - "prepare": "@php vendor/bin/testbench package:discover --ansi", - "build": [ - "@composer run prepare", - "@php vendor/bin/testbench workbench:build --ansi" + "post-autoload-dump": [ + "@clear", + "@prepare", + "@composer run prepare" ], + "clear": "@php vendor/bin/testbench package:purge-skeleton --ansi", + "prepare": "@php vendor/bin/testbench package:discover --ansi", + "build": "@php vendor/bin/testbench workbench:build --ansi", "start": [ "Composer\\Config::disableProcessTimeout", "@composer run build", @@ -61,7 +64,16 @@ "analyse": "vendor/bin/phpstan analyse", "test": "vendor/bin/pest", "test-coverage": "vendor/bin/pest --coverage", - "format": "vendor/bin/pint" + "format": "vendor/bin/pint", + "serve": [ + "Composer\\Config::disableProcessTimeout", + "@build", + "@php vendor/bin/testbench serve" + ], + "lint": [ + "@php vendor/bin/pint", + "@php vendor/bin/phpstan analyse" + ] }, "config": { "sort-packages": true, @@ -82,4 +94,4 @@ }, "minimum-stability": "dev", "prefer-stable": true -} +} \ No newline at end of file diff --git a/src/Commands/ModuleActivateCommand.php b/src/Commands/ModuleActivateCommand.php index 8ccfa99..0d96900 100644 --- a/src/Commands/ModuleActivateCommand.php +++ b/src/Commands/ModuleActivateCommand.php @@ -3,6 +3,7 @@ namespace Savannabits\Modular\Commands; use Illuminate\Console\Command; +use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Str; use Savannabits\Modular\Facades\Modular; @@ -27,7 +28,9 @@ private function activateModule(): void { $moduleName = $this->moduleName; $repoName = config('modular.vendor', 'modular').'/'.$moduleName; - Modular::execCommand('composer require '.$repoName.':@dev'); - Modular::execCommand("php artisan $moduleName:install"); + Modular::execCommand('composer require '.$repoName.':@dev', $this); + Modular::execCommand('composer dump-autoload'); + Artisan::call('list'); + Artisan::call("$moduleName:install"); } } diff --git a/src/Commands/ModuleMakeCommand.php b/src/Commands/ModuleMakeCommand.php index 54c4b65..e3fb355 100644 --- a/src/Commands/ModuleMakeCommand.php +++ b/src/Commands/ModuleMakeCommand.php @@ -7,9 +7,9 @@ use Illuminate\Support\Str; use Psr\Container\ContainerExceptionInterface; use Psr\Container\NotFoundExceptionInterface; -use Savannabits\Modular\Facades\Modular; use Savannabits\Modular\Support\Concerns\CanManipulateFiles; +use function Laravel\Prompts\confirm; use function Laravel\Prompts\text; class ModuleMakeCommand extends Command @@ -40,9 +40,27 @@ public function handle() $this->modulePath = config('modular.path').'/'.$this->moduleName; $this->info("Creating module: $this->moduleName in $this->modulePath"); - $this->generateModuleDirectories(); - $this->generateModuleFiles(); - $this->installModule(); + if (! $this->generateModuleDirectories()) { + $this->error('Failed to create module directories'); + + return 1; + } + if (! $this->generateModuleFiles()) { + $this->error('Failed to create module files'); + + return 1; + } + + // Ask if to install the new module + if (confirm('Do you want to activate the new module now?', false, required: true)) { + if (! $this->installModule()) { + $this->error('Failed to activate the new module'); + + return 1; + } + } + + return 0; } private function generateModuleDirectories(): bool @@ -53,6 +71,12 @@ private function generateModuleDirectories(): bool return false; } + // If the module exists and force option is not set, confirm that you want to override files + if (is_dir($this->modulePath) && ! $this->option('force')) { + if (! confirm('Module already exists. Do you want to override it?')) { + return false; + } + } foreach ($directories as $directory) { $path = $this->modulePath.'/'.ltrim($directory, '/'); if (! is_dir($path)) { @@ -65,15 +89,13 @@ private function generateModuleDirectories(): bool return true; } - private function generateModuleFiles(): void + private function generateModuleFiles(): bool { $this->generateModuleComposerFile(); - try { - $this->generateModuleServiceProvider(); - } catch (FileNotFoundException|NotFoundExceptionInterface|ContainerExceptionInterface $e) { - $this->error($e->getMessage()); - } + $this->generateModuleServiceProvider(); $this->generatePestFiles(); + + return true; } private function generateModuleComposerFile(): void @@ -124,7 +146,9 @@ private function generateModuleComposerFile(): void */ private function generateModuleServiceProvider(): void { - $path = Modular::module($this->moduleName)->appPath($this->moduleStudlyName.'ServiceProvider.php'); + $this->comment('Generating Module Service Provider'); + // get the path to the service provider + $path = $this->modulePath.DIRECTORY_SEPARATOR.'app'.DIRECTORY_SEPARATOR.$this->moduleStudlyName.'ServiceProvider.php'; $namespace = $this->moduleNamespace; $class = $this->moduleStudlyName.'ServiceProvider'; $this->copyStubToApp('module.provider', $path, [ @@ -137,27 +161,30 @@ private function generateModuleServiceProvider(): void private function generatePestFiles(): void { // phpunit.xml - $path = Modular::module($this->moduleName)->path('phpunit.xml'); + $path = $this->modulePath.DIRECTORY_SEPARATOR.'phpunit.xml'; $this->copyStubToApp('phpunit', $path, [ 'moduleName' => $this->moduleStudlyName, ]); // Pest.php - $path = Modular::module($this->moduleName)->testsPath('Pest.php'); + $path = $this->modulePath.DIRECTORY_SEPARATOR.'tests'.DIRECTORY_SEPARATOR.'Pest.php'; $this->copyStubToApp('pest.class', $path, [ 'namespace' => $this->moduleNamespace, ]); // TestCase.php - $path = Modular::module($this->moduleName)->testsPath('TestCase.php'); + $path = $this->modulePath.DIRECTORY_SEPARATOR.'tests'.DIRECTORY_SEPARATOR.'TestCase.php'; $this->copyStubToApp('test-case', $path, [ 'namespace' => $this->moduleNamespace.'\\Tests', ]); } - private function installModule(): void + + private function installModule(): bool { $this->comment('Activating the new Module'); $this->call('modular:activate', ['name' => $this->moduleName]); + + return true; } } diff --git a/src/Modular.php b/src/Modular.php index d1ab4c1..551a9ae 100755 --- a/src/Modular.php +++ b/src/Modular.php @@ -2,13 +2,21 @@ namespace Savannabits\Modular; +use Illuminate\Console\Command; +use Symfony\Component\Process\Process; + class Modular { - public function execCommand(string $command): void + public function execCommand(string $command, ?Command $artisan = null): void { - $process = proc_open($command, [STDIN, STDOUT, STDERR], $pipes, base_path()); - if (is_resource($process)) { - proc_close($process); + $process = Process::fromShellCommandline($command); + $process->start(); + foreach ($process as $type => $data) { + if (! $artisan) { + echo $data; + } else { + $artisan->info(trim($data)); + } } } diff --git a/src/ModularServiceProvider.php b/src/ModularServiceProvider.php index 4f29902..627a83f 100644 --- a/src/ModularServiceProvider.php +++ b/src/ModularServiceProvider.php @@ -39,9 +39,9 @@ public function configurePackage(Package $package): void $this->mergeConfigFrom($this->package->basePath('/../config/modular.php'), 'modular'); } - private function configureComposerMerge(InstallCommand $command): void + private function configureComposerFile(InstallCommand $command): void { - $command->comment('Configuring Composer merge plugin:'); + $command->comment('Configuring Composer File:'); $composerJson = json_decode(file_get_contents(base_path('composer.json')), true); // Add the modules repositories into compose if they don't exist if (! isset($composerJson['repositories'])) { @@ -56,29 +56,6 @@ private function configureComposerMerge(InstallCommand $command): void ], ]; } - if (! isset($composerJson['extra']['merge-plugin'])) { - $composerJson['extra']['merge-plugin'] = [ - 'include' => [ - 'modules/*/composer.json', - ], - 'replace' => true, - 'merge-extra' => true, - 'merge-extra-deep' => true, - 'merge-scripts' => true, - - ]; - - // Ensure the composer-merge-plugin is in the list of allowed plugins - if (! isset($composerJson['config']['allow-plugins'])) { - $composerJson['config']['allow-plugins'] = []; - } - // If allowed-plugins is set to true, disregard - if ($composerJson['config']['allow-plugins'] === true) { - $command->warn('Composer merge plugin already configured. skipping...'); - } else { - $composerJson['config']['allow-plugins']['wikimedia/composer-merge-plugin'] = true; - } - } file_put_contents(base_path('composer.json'), json_encode($composerJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); $command->info('Composer file configured successfully'); } @@ -86,11 +63,10 @@ private function configureComposerMerge(InstallCommand $command): void private function installationSteps(InstallCommand $command): void { $this->ensureModularPathExists($command); - $this->configureComposerMerge($command); - // Run composer dump-autoload and pipe the output realtime - $command->comment('Running composer dump-autoload:'); + $this->configureComposerFile($command); + /*$command->comment('Running composer dump-autoload:'); \Savannabits\Modular\Facades\Modular::execCommand('composer dump-autoload'); - $command->info('Composer dump-autoload completed successfully'); + $command->info('Composer dump-autoload completed successfully');*/ } private function ensureModularPathExists(InstallCommand $command): void diff --git a/tests/Installation/ModularConfigurationTest.php b/tests/Feature/ModularConfigurationTest.php similarity index 100% rename from tests/Installation/ModularConfigurationTest.php rename to tests/Feature/ModularConfigurationTest.php diff --git a/tests/Feature/ModuleCreationTest.php b/tests/Feature/ModuleCreationTest.php new file mode 100644 index 0000000..5140bc2 --- /dev/null +++ b/tests/Feature/ModuleCreationTest.php @@ -0,0 +1,32 @@ +moduleTitle = 'Access Control'; + $this->moduleName = 'access-control'; + $this->moduleStudlyName = 'AccessControl'; +}); +test('can generate a new module', function () { + // Run modular:make-module, expect a new module to be created + artisan('modular:make', ['name' => $this->moduleTitle]) + ->expectsQuestion('Module already exists. Do you want to override it?', true) + ->expectsQuestion('Do you want to activate the new module now?', false); +}); + +// can activate module +test('can activate a module whose directory exists in modules', function () { + artisan('modular:activate', ['name' => $this->moduleTitle]) + ->expectsOutput('Activating module: '.$this->moduleName) + ->expectsOutput('./composer.json has been updated') + ->expectsOutput('Running composer update modular/'.$this->moduleName) + ->doesntExpectOutputToContain('Your requirements could not be resolved to an installable set of packages') + ->expectsOutputToContain('Generating optimized autoload files') + ->assertSuccessful(); +}); + +test('should not override existing module unless the user confirms', function () { + artisan('modular:make', ['name' => $this->moduleTitle]) + ->expectsQuestion('Module already exists. Do you want to override it?', false) + ->expectsOutput('Failed to create module directories'); +}); diff --git a/tests/Installation/ModuleCreationTest.php b/tests/Installation/ModuleCreationTest.php deleted file mode 100644 index b3d9bbc..0000000 --- a/tests/Installation/ModuleCreationTest.php +++ /dev/null @@ -1 +0,0 @@ -