diff --git a/README.md b/README.md index c0e124e..17ab342 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,7 @@ We provide a few helper commands: - `php artisan modules:clear` — clear the module cache - `php artisan modules:sync` — update project configs (like `phpunit.xml`) with module settings - `php artisan modules:list` — list all modules +- `php artisan modules:remove` — delete a module and remove it from your `composer.json` #### Laravel “`make:`” Commands diff --git a/src/Console/Commands/ModulesRemove.php b/src/Console/Commands/ModulesRemove.php new file mode 100644 index 0000000..5524089 --- /dev/null +++ b/src/Console/Commands/ModulesRemove.php @@ -0,0 +1,102 @@ +module_registry->module($this->argument('name')); + + if (null === $module) { + $this->error("Module '{$this->argument('name')}' does not exist."); + return 1; + } + + if (! $this->option('force') && ! $this->confirm("This will permanently delete {$module->base_path}. Continue?", false)) { + $this->line('Module was not removed.'); + return 1; + } + + $composer_name = $this->composerName($module); + + $this->filesystem->deleteDirectory($module->base_path); + $this->line(" - Deleted {$module->base_path}"); + + if ($composer_name) { + $this->removeFromCoreComposerConfig($composer_name); + } + + $this->call(ModulesClear::class); + + $this->module_registry->reload(); + + if ($composer_name) { + $this->newLine(); + $this->line("Please run composer update {$composer_name}"); + $this->newLine(); + } + + return 0; + } + + protected function composerName(ModuleConfig $module): ?string + { + $composer_file = $module->path('composer.json'); + + if (! $this->filesystem->isFile($composer_file)) { + return null; + } + + return json_decode($this->filesystem->get($composer_file), true)['name'] ?? null; + } + + protected function removeFromCoreComposerConfig(string $composer_name): void + { + // We're going to move into the Laravel base directory while + // we're updating the composer file so that we're sure we update + // the correct composer.json file (we'll restore CWD at the end) + $original_working_dir = getcwd(); + chdir($this->laravel->basePath()); + + $json_file = new JsonFile(Factory::getComposerFile()); + $definition = $json_file->read(); + + $has_changes = false; + + foreach (['require', 'require-dev'] as $key) { + if (isset($definition[$key][$composer_name])) { + unset($definition[$key][$composer_name]); + $this->line(" - Removed {$composer_name} from {$key}"); + $has_changes = true; + } + } + + if ($has_changes) { + $json_file->write($definition); + $this->line(" - Wrote to {$json_file->getPath()}"); + } + + chdir($original_working_dir); + } +} diff --git a/src/Support/ModularServiceProvider.php b/src/Support/ModularServiceProvider.php index 33bf966..6fee656 100644 --- a/src/Support/ModularServiceProvider.php +++ b/src/Support/ModularServiceProvider.php @@ -12,6 +12,7 @@ use InterNACHI\Modular\Console\Commands\ModulesCache; use InterNACHI\Modular\Console\Commands\ModulesClear; use InterNACHI\Modular\Console\Commands\ModulesList; +use InterNACHI\Modular\Console\Commands\ModulesRemove; use InterNACHI\Modular\Console\Commands\ModulesSync; use InterNACHI\Modular\PluginRegistry; use InterNACHI\Modular\Plugins\ArtisanPlugin; @@ -102,6 +103,7 @@ public function boot(): void ModulesClear::class, ModulesSync::class, ModulesList::class, + ModulesRemove::class, ]); } } diff --git a/tests/Commands/ModulesRemoveTest.php b/tests/Commands/ModulesRemoveTest.php new file mode 100644 index 0000000..ec0162d --- /dev/null +++ b/tests/Commands/ModulesRemoveTest.php @@ -0,0 +1,78 @@ +makeModule('test-module'); + + $this->assertTrue($this->filesystem()->isDirectory($module->base_path)); + $this->assertArrayHasKey('modules/test-module', $this->appComposerRequirements()); + + $this->artisan(ModulesRemove::class, ['name' => 'test-module', '--force' => true]) + ->assertExitCode(0); + + $this->assertFalse($this->filesystem()->isDirectory($module->base_path)); + $this->assertArrayNotHasKey('modules/test-module', $this->appComposerRequirements()); + $this->assertNull(Modules::module('test-module')); + } + + public function test_it_only_removes_the_given_module(): void + { + $this->makeModule('test-module'); + $other = $this->makeModule('other-module'); + + $this->artisan(ModulesRemove::class, ['name' => 'test-module', '--force' => true]) + ->assertExitCode(0); + + $this->assertTrue($this->filesystem()->isDirectory($other->base_path)); + $this->assertArrayHasKey('modules/other-module', $this->appComposerRequirements()); + $this->assertNotNull(Modules::module('other-module')); + } + + public function test_it_asks_for_confirmation(): void + { + $module = $this->makeModule('test-module'); + + $this->artisan(ModulesRemove::class, ['name' => 'test-module']) + ->expectsConfirmation("This will permanently delete {$module->base_path}. Continue?", 'yes') + ->assertExitCode(0); + + $this->assertFalse($this->filesystem()->isDirectory($module->base_path)); + } + + public function test_it_does_not_remove_the_module_if_confirmation_is_declined(): void + { + $module = $this->makeModule('test-module'); + + $this->artisan(ModulesRemove::class, ['name' => 'test-module']) + ->expectsConfirmation("This will permanently delete {$module->base_path}. Continue?", 'no') + ->assertExitCode(1); + + $this->assertTrue($this->filesystem()->isDirectory($module->base_path)); + $this->assertArrayHasKey('modules/test-module', $this->appComposerRequirements()); + } + + public function test_it_fails_for_unknown_modules(): void + { + $this->artisan(ModulesRemove::class, ['name' => 'does-not-exist', '--force' => true]) + ->expectsOutput("Module 'does-not-exist' does not exist.") + ->assertExitCode(1); + } + + protected function appComposerRequirements(): array + { + $composer_file = $this->getApplicationBasePath().'/composer.json'; + + return json_decode($this->filesystem()->get($composer_file), true)['require'] ?? []; + } +}