diff --git a/src/StarterKits/Installer.php b/src/StarterKits/Installer.php index 61edfb1ee6..448b07d2c8 100644 --- a/src/StarterKits/Installer.php +++ b/src/StarterKits/Installer.php @@ -251,7 +251,7 @@ function () { : $this->package; try { - Composer::withoutQueue()->throwOnFailure()->requireDev($package); + Composer::withoutQueue()->throwOnFailure()->require($package); } catch (ProcessException $exception) { $this->rollbackWithError("Error installing starter kit [{$package}].", $exception->getMessage()); } @@ -549,14 +549,14 @@ function () { */ public function removeStarterKit(): self { - if ($this->disableCleanup) { + if ($this->isUpdatable() || $this->disableCleanup) { return $this; } spin( function () { if (Composer::isInstalled($this->package)) { - Composer::withoutQueue()->throwOnFailure(false)->removeDev($this->package); + Composer::withoutQueue()->throwOnFailure(false)->remove($this->package); } }, 'Cleaning up temporary files...' @@ -590,7 +590,7 @@ protected function completeInstall(): self */ protected function removeRepository(): self { - if ($this->fromLocalRepo || ! $this->url) { + if ($this->isUpdatable() || $this->fromLocalRepo || ! $this->url) { return $this; } @@ -673,7 +673,9 @@ protected function starterKitPath(?string $path = null): string */ protected function config(?string $key = null): mixed { - $config = collect(YAML::parse($this->files->get($this->starterKitPath('starter-kit.yaml')))); + $config = Blink::once('starter-kit-config', function () { + return collect(YAML::parse($this->files->get($this->starterKitPath('starter-kit.yaml')))); + }); if ($key) { return $config->get($key); @@ -681,4 +683,12 @@ protected function config(?string $key = null): mixed return $config; } + + /** + * Should starter kit be treated as an updatable package, and live on for future composer updates, etc? + */ + protected function isUpdatable(): bool + { + return (bool) $this->config('updatable'); + } } diff --git a/tests/StarterKits/InstallTest.php b/tests/StarterKits/InstallTest.php index 3533e513c1..6fc18b8aa1 100644 --- a/tests/StarterKits/InstallTest.php +++ b/tests/StarterKits/InstallTest.php @@ -15,6 +15,7 @@ use Statamic\Facades\Config; use Statamic\Facades\Path; use Statamic\Facades\YAML; +use Statamic\Support\Arr; use Statamic\Support\Str; use Tests\Fakes\Composer\FakeComposer; use Tests\TestCase; @@ -61,6 +62,7 @@ public function it_installs_starter_kit() $this->assertFalse(Blink::has('starter-kit-repository-added')); $this->assertFileDoesNotExist($this->kitVendorPath()); $this->assertFileDoesNotExist(base_path('composer.json.bak')); + $this->assertComposerJsonDoesntHavePackage('statamic/cool-runnings'); $this->assertComposerJsonDoesntHave('repositories'); $this->assertFileExists(base_path('copied.md')); } @@ -258,6 +260,70 @@ public function it_restores_existing_repositories_after_successful_install() $this->assertEquals($expectedRepositories, $composerJson['repositories']); } + #[Test] + public function it_installs_as_living_package_with_custom_config() + { + $this->setConfig([ + 'updatable' => true, // With `updatable: true`, kit should live on as composer updatable package + 'export_paths' => [ + 'copied.md', + ], + ]); + + $this->assertFileDoesNotExist(base_path('copied.md')); + $this->assertFileDoesNotExist($this->kitVendorPath()); + + $this->installCoolRunnings(); + + $this->assertFileExists(base_path('copied.md')); + $this->assertComposerJsonDoesntHave('repositories'); + + // Keep package around + $this->assertFileExists($this->kitVendorPath()); + $this->assertComposerJsonHasPackage('require', 'statamic/cool-runnings'); + + // But ensure we still delete backup composer.json, which is only used for error handling purposes + $this->assertFileDoesNotExist(base_path('composer.json.bak')); + } + + #[Test] + public function it_leaves_custom_repository_for_living_packages_that_need_it() + { + $this->setConfig([ + 'updatable' => true, // With `updatable: true`, kit should live on as composer updatable package + 'export_paths' => [ + 'copied.md', + ], + ]); + + $this->assertFileDoesNotExist(base_path('copied.md')); + $this->assertFileDoesNotExist($this->kitVendorPath()); + $this->assertComposerJsonDoesntHave('repositories'); + + $this->installCoolRunnings([], [ + 'outpost.*' => Http::response(['data' => ['price' => null]], 200), + 'github.com/*' => Http::response('', 200), + '*' => Http::response('', 404), + ]); + + $this->assertFileExists(base_path('copied.md')); + + // Keep package around + $this->assertFileExists($this->kitVendorPath()); + $this->assertComposerJsonHasPackage('require', 'statamic/cool-runnings'); + + // As well as custom repository, which will be needed for composer updates, if it was needed for install + $composerJson = json_decode($this->files->get(base_path('composer.json')), true); + $this->assertCount(1, $composerJson['repositories']); + $this->assertEquals([[ + 'type' => 'vcs', + 'url' => 'https://github.com/statamic/cool-runnings', + ]], $composerJson['repositories']); + + // But delete backup composer.json, which is only used for error handling purposes + $this->assertFileDoesNotExist(base_path('composer.json.bak')); + } + #[Test] public function it_fails_if_starter_kit_config_does_not_exist() { @@ -737,8 +803,8 @@ public function it_parses_branch_from_package_param_when_installing() ]); // Ensure `Composer::requireDev()` gets called with `package:branch` - $this->assertEquals(Blink::get('composer-require-dev-package'), 'statamic/cool-runnings'); - $this->assertEquals(Blink::get('composer-require-dev-branch'), 'dev-custom-branch'); + $this->assertEquals(Blink::get('composer-require-package'), 'statamic/cool-runnings'); + $this->assertEquals(Blink::get('composer-require-branch'), 'dev-custom-branch'); // But ensure the rest of the installer handles parsed `package` without branch messing things up $this->assertFalse(Blink::has('starter-kit-repository-added')); @@ -760,8 +826,8 @@ public function it_installs_branch_with_slash_without_failing_package_validation ]); // Ensure `Composer::requireDev()` gets called with `package:branch` - $this->assertEquals(Blink::get('composer-require-dev-package'), 'statamic/cool-runnings'); - $this->assertEquals(Blink::get('composer-require-dev-branch'), 'dev-feature/custom-branch'); + $this->assertEquals(Blink::get('composer-require-package'), 'statamic/cool-runnings'); + $this->assertEquals(Blink::get('composer-require-branch'), 'dev-feature/custom-branch'); // But ensure the rest of the installer handles parsed `package` without branch messing things up $this->assertFalse(Blink::has('starter-kit-repository-added')); @@ -1652,6 +1718,21 @@ private function assertFileDoesntHaveContent($expected, $path) $this->assertStringNotContainsString($expected, $this->files->get($path)); } + private function assertComposerJsonHasPackage($requireKey, $package) + { + $composerJson = json_decode($this->files->get(base_path('composer.json')), true); + + $this->assertTrue(Arr::has($composerJson, "{$requireKey}.{$package}")); + } + + private function assertComposerJsonDoesntHavePackage($package) + { + $composerJson = json_decode($this->files->get(base_path('composer.json')), true); + + $this->assertFalse(Arr::has($composerJson, "require.{$package}")); + $this->assertFalse(Arr::has($composerJson, "require-dev.{$package}")); + } + private function assertComposerJsonHasPackageVersion($requireKey, $package, $version) { $composerJson = json_decode($this->files->get(base_path('composer.json')), true);