From e2183db7f2c2c60c044f8f10a1dd8ee3e5ec861f Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Sat, 9 Nov 2024 13:31:10 -0500 Subject: [PATCH 01/30] Use dot notation in module keys so we can use `Arr::dot()` etc. --- src/StarterKits/Installer.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/StarterKits/Installer.php b/src/StarterKits/Installer.php index a17f8adfc37..4c84c6b35da 100644 --- a/src/StarterKits/Installer.php +++ b/src/StarterKits/Installer.php @@ -332,7 +332,7 @@ protected function instantiateModule(array $config, string $key): InstallableMod $shouldPrompt = false; } - $name = str_replace('_', ' ', $key); + $name = str_replace(['_', '.'], ' ', $key); if ($shouldPrompt && $this->isInteractive && ! confirm(Arr::get($config, 'prompt', "Would you like to install the [{$name}] module?"), false)) { return false; @@ -353,7 +353,7 @@ protected function instantiateSelectModule(array $config, string $key): Installa ->prepend(Arr::get($config, 'skip_option', 'No'), $skipModule = 'skip_module') ->all(); - $name = str_replace('_', ' ', $key); + $name = str_replace(['_', '.'], ' ', $key); $choice = select( label: Arr::get($config, 'prompt', "Would you like to install one of the following [{$name}] modules?"), @@ -375,7 +375,7 @@ protected function instantiateSelectModule(array $config, string $key): Installa */ protected function normalizeModuleKey(string $key, string $childKey): string { - return $key !== 'top_level' ? "{$key}_{$childKey}" : $childKey; + return $key !== 'top_level' ? "{$key}.{$childKey}" : $childKey; } /** From d69a43b841bf7b9a88272793b6442819a473503e Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Sat, 9 Nov 2024 17:57:21 -0500 Subject: [PATCH 02/30] Allow `default` param on `config()` helper. --- src/StarterKits/Module.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/StarterKits/Module.php b/src/StarterKits/Module.php index acd102d63ee..3e00ce798c0 100644 --- a/src/StarterKits/Module.php +++ b/src/StarterKits/Module.php @@ -45,10 +45,10 @@ public function isTopLevelModule(): bool /** * Get module config. */ - public function config(?string $key = null): mixed + public function config(?string $key = null, $default = null): mixed { if ($key) { - return $this->config->get($key); + return $this->config->get($key, $default); } return $this->config; From 9588e0bc9cb0d7a92c1ab415a426a8e523018919 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Sat, 9 Nov 2024 18:19:59 -0500 Subject: [PATCH 03/30] Add `setChildModules()` helper, so that we can override children after prompting. --- src/StarterKits/InstallableModule.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/StarterKits/InstallableModule.php b/src/StarterKits/InstallableModule.php index 88ef05ccd7f..13f4134de8b 100644 --- a/src/StarterKits/InstallableModule.php +++ b/src/StarterKits/InstallableModule.php @@ -26,6 +26,16 @@ public function installer($installer): self return $this; } + /** + * Set child modules onto instance, after prompting and filtering. + */ + public function setChildModules(Collection $modules): self + { + $this->config['modules'] = collect($modules); + + return $this; + } + /** * Validate starter kit module is installable. * From 8a15e7bd2bd8cd11cabba98e248633c1a3ba8851 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Sat, 9 Nov 2024 18:31:55 -0500 Subject: [PATCH 04/30] Separate out instantiation and prompting logic. --- src/StarterKits/Installer.php | 127 ++++++++++++++++++++++------------ 1 file changed, 84 insertions(+), 43 deletions(-) diff --git a/src/StarterKits/Installer.php b/src/StarterKits/Installer.php index 4c84c6b35da..6cd466e3205 100644 --- a/src/StarterKits/Installer.php +++ b/src/StarterKits/Installer.php @@ -7,6 +7,7 @@ use Facades\Statamic\StarterKits\Hook; use Illuminate\Console\Command; use Illuminate\Filesystem\Filesystem; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Http; use Statamic\Console\NullConsole; use Statamic\Console\Please\Application as PleaseApplication; @@ -162,6 +163,7 @@ public function install(): void ->requireStarterKit() ->ensureConfig() ->instantiateModules() + ->prepareInstallableModules() ->installModules() ->copyStarterKitConfig() ->copyStarterKitHooks() @@ -286,77 +288,127 @@ protected function ensureConfig(): self } /** - * Instantiate and validate modules that are to be installed. + * Instantiate modules. */ protected function instantiateModules(): self { - $this->modules = collect(['top_level' => $this->config()->all()]) - ->map(fn ($config, $key) => $this->instantiateModuleRecursively($config, $key)) - ->flatten() - ->filter() - ->each(fn ($module) => $module->validate()); + $this->modules = collect([ + 'top_level' => $this->instantiateModuleRecursively($this->config(), 'top_level'), + ]); return $this; } /** - * Instantiate module and check if nested modules should be recursively instantiated. + * Recursively instantiate module and its nested modules. */ - protected function instantiateModuleRecursively(array $config, string $key): InstallableModule|array + protected function instantiateModuleRecursively(Collection|array $config, string $key): InstallableModule { - $instantiated = (new InstallableModule($config, $key))->installer($this); + if ($options = Arr::get($config, 'options')) { + $config['options'] = collect($options) + ->map(fn ($optionConfig, $optionKey) => $this->instantiateModuleRecursively( + $optionConfig, + $this->normalizeModuleKey($key, $optionKey), + )); + } if ($modules = Arr::get($config, 'modules')) { - $instantiated = collect($modules) - ->map(fn ($config, $childKey) => $this->instantiateModule($config, $this->normalizeModuleKey($key, $childKey))) - ->prepend($instantiated, $key) - ->filter() - ->all(); + $config['modules'] = collect($modules) + ->map(fn ($childConfig, $childKey) => $this->instantiateModuleRecursively( + $childConfig, + $this->normalizeModuleKey($key, $childKey), + )); } - return $instantiated; + return (new InstallableModule($config, $key))->installer($this); } /** - * Instantiate individual module. + * Normalize module key. */ - protected function instantiateModule(array $config, string $key): InstallableModule|array|bool + protected function normalizeModuleKey(string $key, string $childKey): string { - $shouldPrompt = true; + return $key !== 'top_level' ? "{$key}.{$childKey}" : $childKey; + } + + /** + * Prompt and prepare flattened collection of installable modules. + */ + protected function prepareInstallableModules(): self + { + $this->modules = $this->modules + ->map(fn ($module) => $this->prepareInstallableRecursively($module)) + ->each(fn ($module) => $module->validate()); + + return $this; + } + + /** + * Recursively prepare module and its nested modules. + */ + protected function prepareInstallableRecursively(InstallableModule $installable): InstallableModule|bool + { + $module = $this->prepareInstallableModule($installable); + + if ($module === false) { + return false; + } - if (Arr::has($config, 'options')) { - return $this->instantiateSelectModule($config, $key); + if ($modules = $module->config('modules')) { + $module->setChildModules( + $modules + ->map(fn ($childModule) => $this->prepareInstallableRecursively($childModule)) + ->filter() + ); } - if (Arr::get($config, 'prompt') === false) { + return $module; + } + + /** + * Prepare individual module. + */ + protected function prepareInstallableModule(InstallableModule $module): InstallableModule|bool + { + if ($module->key() === 'top_level') { + return $module; + } + + if ($module->config('options')) { + return $this->prepareSelectModule($module); + } + + $shouldPrompt = true; + + if ($module->config('prompt') === false) { $shouldPrompt = false; } - $name = str_replace(['_', '.'], ' ', $key); + $name = str_replace(['_', '.'], ' ', $module->key()); - if ($shouldPrompt && $this->isInteractive && ! confirm(Arr::get($config, 'prompt', "Would you like to install the [{$name}] module?"), false)) { + if ($shouldPrompt && $this->isInteractive && ! confirm($module->config('prompt', "Would you like to install the [{$name}] module?"), false)) { return false; } elseif ($shouldPrompt && ! $this->isInteractive) { return false; } - return $this->instantiateModuleRecursively($config, $key); + return $module; } /** - * Instantiate select module. + * Prepare select module. */ - protected function instantiateSelectModule(array $config, string $key): InstallableModule|array|bool + protected function prepareSelectModule(InstallableModule $module): InstallableModule|bool { - $options = collect($config['options']) - ->map(fn ($option, $optionKey) => Arr::get($option, 'label', ucfirst($optionKey))) - ->prepend(Arr::get($config, 'skip_option', 'No'), $skipModule = 'skip_module') + $options = collect($module->config('options')) + ->map(fn ($option, $optionKey) => $option->config('label', ucfirst($optionKey))) + ->prepend($module->config('skip_option', 'No'), $skipModule = 'skip_module') ->all(); - $name = str_replace(['_', '.'], ' ', $key); + $name = str_replace(['_', '.'], ' ', $module->key()); $choice = select( - label: Arr::get($config, 'prompt', "Would you like to install one of the following [{$name}] modules?"), + label: $module->config('prompt', "Would you like to install one of the following [{$name}] modules?"), options: $options, ); @@ -364,18 +416,7 @@ protected function instantiateSelectModule(array $config, string $key): Installa return false; } - $selectedKey = "{$key}_{$choice}"; - $selectedModuleConfig = $config['options'][$choice]; - - return $this->instantiateModuleRecursively($selectedModuleConfig, $selectedKey); - } - - /** - * Normalize module key. - */ - protected function normalizeModuleKey(string $key, string $childKey): string - { - return $key !== 'top_level' ? "{$key}.{$childKey}" : $childKey; + return $module->config('options')[$choice]; } /** From 6a6d86ae77d1fa81da6a94c22eb2d6213085780f Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Sat, 9 Nov 2024 18:32:12 -0500 Subject: [PATCH 05/30] Flatten modules before installing. --- src/StarterKits/Installer.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/StarterKits/Installer.php b/src/StarterKits/Installer.php index 6cd466e3205..a6f7f631213 100644 --- a/src/StarterKits/Installer.php +++ b/src/StarterKits/Installer.php @@ -338,6 +338,7 @@ protected function prepareInstallableModules(): self { $this->modules = $this->modules ->map(fn ($module) => $this->prepareInstallableRecursively($module)) + ->pipe(fn ($module) => $this->flattenModules($module)) ->each(fn ($module) => $module->validate()); return $this; @@ -419,6 +420,22 @@ protected function prepareSelectModule(InstallableModule $module): InstallableMo return $module->config('options')[$choice]; } + /** + * Flatten modules. + */ + public function flattenModules(Collection $modules): Collection + { + return $modules + ->flatMap(function ($module) { + return [ + $module->key() => $module, + ...$this->flattenModules($module->config('options', collect())), + ...$this->flattenModules($module->config('modules', collect())), + ]; + }) + ->filter(); + } + /** * Install all the modules. */ From 833e463b4b4aea2f7b63a281bd1040d3ccfd9165 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Sat, 9 Nov 2024 20:21:37 -0500 Subject: [PATCH 06/30] Rename method. --- src/StarterKits/Installer.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/StarterKits/Installer.php b/src/StarterKits/Installer.php index a6f7f631213..c9c40b56e58 100644 --- a/src/StarterKits/Installer.php +++ b/src/StarterKits/Installer.php @@ -163,7 +163,7 @@ public function install(): void ->requireStarterKit() ->ensureConfig() ->instantiateModules() - ->prepareInstallableModules() + ->filterInstallableModules() ->installModules() ->copyStarterKitConfig() ->copyStarterKitHooks() @@ -332,9 +332,9 @@ protected function normalizeModuleKey(string $key, string $childKey): string } /** - * Prompt and prepare flattened collection of installable modules. + * Filter and prepare flattened collection of installable modules. */ - protected function prepareInstallableModules(): self + protected function filterInstallableModules(): self { $this->modules = $this->modules ->map(fn ($module) => $this->prepareInstallableRecursively($module)) From 04e80f69869abca5ecc39495c7a3209b217acfc2 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Sat, 9 Nov 2024 21:09:55 -0500 Subject: [PATCH 07/30] Test `import` syntax recursively imports from `/modules` folders. --- tests/StarterKits/InstallTest.php | 284 +++++++++++++++++++++++++++++- 1 file changed, 282 insertions(+), 2 deletions(-) diff --git a/tests/StarterKits/InstallTest.php b/tests/StarterKits/InstallTest.php index 10c85e1c2ee..1c63eaf36c8 100644 --- a/tests/StarterKits/InstallTest.php +++ b/tests/StarterKits/InstallTest.php @@ -41,6 +41,10 @@ public function tearDown(): void { $this->restoreSite(); + if ($this->files->exists($kitRepo = $this->kitRepoPath())) { + $this->files->delete($kitRepo); + } + parent::tearDown(); } @@ -925,6 +929,124 @@ public function it_installs_only_the_modules_confirmed_interactively_via_prompt( $this->assertComposerJsonHasPackageVersion('require', 'bobsled/vue-components', '^1.5'); } + #[Test] + public function it_installs_imported_modules_confirmed_interactively_via_prompt() + { + $this->setConfig([ + 'export_paths' => [ + 'copied.md', + ], + 'modules' => [ + 'seo' => 'import', // import! + 'bobsled' => [ + 'export_paths' => [ + 'resources/css/bobsled.css', + ], + 'dependencies' => [ + 'bobsled/speed-calculator' => '^1.0.0', + ], + ], + 'jamaica' => [ + 'export_as' => [ + 'resources/css/theme.css' => 'resources/css/jamaica.css', + ], + ], + 'js' => 'import', // import! + 'oldschool_js' => [ + 'options' => [ + 'jquery' => [ + 'export_paths' => [ + 'resources/js/jquery.js', + ], + ], + 'mootools' => [ + 'export_paths' => [ + 'resources/js/jquery.js', + ], + ], + ], + ], + ], + ]); + + $this->setConfig( + path: 'modules/seo/module.yaml', + config: [ + 'export_paths' => [ + 'resources/css/seo.css', + ], + 'dependencies' => [ + 'statamic/seo-pro' => '^0.2.0', + ], + ], + ); + + $this->setConfig( + path: 'modules/js/module.yaml', + config: [ + 'options' => [ + 'react' => [ + 'export_paths' => [ + 'resources/js/react.js', + ], + ], + 'vue' => 'import', // import option as separate module! + 'svelte' => [ + 'export_paths' => [ + 'resources/js/svelte.js', + ], + ], + ], + ], + ); + + $this->setConfig( + path: 'modules/js/vue/module.yaml', + config: [ + 'export_paths' => [ + 'resources/js/vue.js', + ], + 'dependencies' => [ + 'bobsled/vue-components' => '^1.5', + ], + ], + ); + + $this->assertFileDoesNotExist(base_path('copied.md')); + $this->assertFileDoesNotExist(base_path('resources/css/seo.css')); + $this->assertFileDoesNotExist(base_path('resources/css/bobsled.css')); + $this->assertFileDoesNotExist(base_path('resources/css/theme.css')); + $this->assertFileDoesNotExist(base_path('resources/js/react.js')); + $this->assertFileDoesNotExist(base_path('resources/js/vue.js')); + $this->assertFileDoesNotExist(base_path('resources/js/svelte.js')); + $this->assertFileDoesNotExist(base_path('resources/js/jquery.js')); + $this->assertFileDoesNotExist(base_path('resources/js/mootools.js')); + $this->assertComposerJsonDoesntHave('statamic/seo-pro'); + $this->assertComposerJsonDoesntHave('bobsled/speed-calculator'); + $this->assertComposerJsonDoesntHave('bobsled/vue-components'); + + $this + ->installCoolRunningsModules() + ->expectsConfirmation('Would you like to install the [seo] module?', 'yes') + ->expectsConfirmation('Would you like to install the [bobsled] module?', 'no') + ->expectsConfirmation('Would you like to install the [jamaica] module?', 'yes') + ->expectsQuestion('Would you like to install one of the following [js] modules?', 'vue') + ->expectsQuestion('Would you like to install one of the following [oldschool js] modules?', 'skip_module'); + + $this->assertFileExists(base_path('copied.md')); + $this->assertFileExists(base_path('resources/css/seo.css')); + $this->assertFileDoesNotExist(base_path('resources/css/bobsled.css')); + $this->assertFileExists(base_path('resources/css/theme.css')); + $this->assertFileDoesNotExist(base_path('resources/js/react.js')); + $this->assertFileExists(base_path('resources/js/vue.js')); + $this->assertFileDoesNotExist(base_path('resources/js/svelte.js')); + $this->assertFileDoesNotExist(base_path('resources/js/jquery.js')); + $this->assertFileDoesNotExist(base_path('resources/js/mootools.js')); + $this->assertComposerJsonHasPackageVersion('require', 'statamic/seo-pro', '^0.2.0'); + $this->assertComposerJsonDoesntHave('bobsled/speed-calculator'); + $this->assertComposerJsonHasPackageVersion('require', 'bobsled/vue-components', '^1.5'); + } + #[Test] public function it_display_custom_module_prompts() { @@ -1346,6 +1468,164 @@ public function it_installs_nested_modules_confirmed_interactively_via_prompt() $this->assertComposerJsonHasPackageVersion('require', 'bobsled/speed-calculator', '^1.0.0'); } + #[Test] + public function it_installs_nested_imported_modules_confirmed_interactively_via_prompt() + { + $this->setConfig([ + 'export_paths' => [ + 'copied.md', + ], + 'modules' => [ + 'seo' => 'import', + 'canada' => [ + 'export_paths' => [ + 'resources/css/hockey.css', + ], + 'modules' => [ + 'hockey_players' => [ + 'export_paths' => [ + 'resources/dictionaries/players.yaml', + ], + ], + ], + ], + 'jamaica' => [ + 'export_as' => [ + 'resources/css/theme.css' => 'resources/css/jamaica.css', + ], + 'modules' => [ + 'bobsled' => 'import', // import nested module! + ], + ], + ], + ]); + + $this->setConfig( + path: 'modules/seo/module.yaml', + config: [ + 'export_paths' => [ + 'resources/css/seo.css', + ], + 'dependencies' => [ + 'statamic/seo-pro' => '^0.2.0', + ], + 'modules' => [ + 'js' => [ + 'options' => [ + 'react' => [ + 'export_paths' => [ + 'resources/js/react.js', + ], + 'modules' => [ + 'testing_tools' => [ + 'export_paths' => [ + 'resources/js/react-testing-tools.js', + ], + ], + ], + ], + 'vue' => [ + 'export_paths' => [ + 'resources/js/vue.js', + ], + 'dependencies_dev' => [ + 'i-love-vue/test-helpers' => '^1.5', + ], + 'modules' => [ + 'testing_tools' => 'import', // import nested module! + ], + ], + 'svelte' => [ + 'export_paths' => [ + 'resources/js/svelte.js', + ], + ], + ], + ], + 'oldschool_js' => [ + 'options' => [ + 'jquery' => [ + 'export_paths' => [ + 'resources/js/jquery.js', + ], + ], + 'mootools' => [ + 'export_paths' => [ + 'resources/js/jquery.js', + ], + ], + ], + ], + ], + ], + ); + + $this->setConfig( + path: 'modules/seo/js/vue/testing_tools/module.yaml', + config: [ + 'export_paths' => [ + 'resources/js/vue-testing-tools.js', + ], + ], + ); + + $this->setConfig( + path: 'modules/jamaica/bobsled/module.yaml', + config: [ + 'export_paths' => [ + 'resources/css/bobsled.css', + ], + 'dependencies' => [ + 'bobsled/speed-calculator' => '^1.0.0', + ], + ], + ); + + $this->assertFileDoesNotExist(base_path('copied.md')); + $this->assertFileDoesNotExist(base_path('resources/css/seo.css')); + $this->assertComposerJsonDoesntHave('statamic/seo-pro'); + $this->assertFileDoesNotExist(base_path('resources/js/react.js')); + $this->assertFileDoesNotExist(base_path('resources/js/react-testing-tools.js')); + $this->assertFileDoesNotExist(base_path('resources/js/vue.js')); + $this->assertFileDoesNotExist(base_path('resources/js/vue-testing-tools.js')); + $this->assertComposerJsonDoesntHave('i-love-vue/test-helpers'); + $this->assertFileDoesNotExist(base_path('resources/js/svelte.js')); + $this->assertFileDoesNotExist(base_path('resources/js/jquery.js')); + $this->assertFileDoesNotExist(base_path('resources/js/mootools.js')); + $this->assertFileDoesNotExist(base_path('resources/css/hockey.css')); + $this->assertFileDoesNotExist(base_path('resources/dictionaries/players.yaml')); + $this->assertFileDoesNotExist(base_path('resources/css/theme.css')); + $this->assertFileDoesNotExist(base_path('resources/css/bobsled.css')); + $this->assertComposerJsonDoesntHave('bobsled/speed-calculator'); + + $this + ->installCoolRunningsModules() + ->expectsConfirmation('Would you like to install the [seo] module?', 'yes') + ->expectsQuestion('Would you like to install one of the following [seo js] modules?', 'vue') + ->expectsQuestion('Would you like to install the [seo js vue testing tools] module?', 'yes') + ->expectsQuestion('Would you like to install one of the following [seo oldschool js] modules?', 'skip_module') + ->expectsConfirmation('Would you like to install the [canada] module?', 'no') + ->expectsConfirmation('Would you like to install the [jamaica] module?', 'yes') + ->expectsConfirmation('Would you like to install the [jamaica bobsled] module?', 'yes'); + + $this->assertFileExists(base_path('copied.md')); + $this->assertFileExists(base_path('resources/css/seo.css')); + $this->assertComposerJsonHasPackageVersion('require', 'statamic/seo-pro', '^0.2.0'); + $this->assertFileDoesNotExist(base_path('resources/js/react.js')); + $this->assertFileDoesNotExist(base_path('resources/js/react-testing-tools.js')); + $this->assertFileExists(base_path('resources/js/vue.js')); + $this->assertFileExists(base_path('resources/js/vue-testing-tools.js')); + $this->assertComposerJsonHasPackageVersion('require-dev', 'i-love-vue/test-helpers', '^1.5'); + $this->assertFileDoesNotExist(base_path('resources/js/svelte.js')); + $this->assertFileDoesNotExist(base_path('resources/js/jquery.js')); + $this->assertFileDoesNotExist(base_path('resources/js/mootools.js')); + $this->assertFileDoesNotExist(base_path('resources/css/hockey.css')); + $this->assertFileDoesNotExist(base_path('resources/dictionaries/players.yaml')); + $this->assertFileExists(base_path('resources/css/theme.css')); + $this->assertFileExists(base_path('resources/css/bobsled.css')); + $this->assertComposerJsonHasPackageVersion('require', 'bobsled/speed-calculator', '^1.0.0'); + } + private function kitRepoPath($path = null) { return collect([base_path('repo/cool-runnings'), $path])->filter()->implode('/'); @@ -1361,9 +1641,9 @@ private function prepareRepo() $this->files->copyDirectory(__DIR__.'/__fixtures__/cool-runnings', $this->kitRepoPath()); } - private function setConfig($config) + private function setConfig($config, $path = 'starter-kit.yaml') { - $this->files->put($this->kitRepoPath('starter-kit.yaml'), YAML::dump($config)); + $this->files->put($this->preparePath($this->kitRepoPath($path)), YAML::dump($config)); } private function preparePath($path) From 3fa6bcfb6de05328c6ce62277b68a5b1bb4dfa14 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Sat, 9 Nov 2024 21:10:15 -0500 Subject: [PATCH 08/30] Module import implementation. --- src/StarterKits/Installer.php | 36 ++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/StarterKits/Installer.php b/src/StarterKits/Installer.php index c9c40b56e58..9d7e8542512 100644 --- a/src/StarterKits/Installer.php +++ b/src/StarterKits/Installer.php @@ -302,8 +302,12 @@ protected function instantiateModules(): self /** * Recursively instantiate module and its nested modules. */ - protected function instantiateModuleRecursively(Collection|array $config, string $key): InstallableModule + protected function instantiateModuleRecursively(Collection|array|string $config, string $key): InstallableModule { + if ($config === 'import') { + $config = $this->importModuleConfig($key); + } + if ($options = Arr::get($config, 'options')) { $config['options'] = collect($options) ->map(fn ($optionConfig, $optionKey) => $this->instantiateModuleRecursively( @@ -323,6 +327,36 @@ protected function instantiateModuleRecursively(Collection|array $config, string return (new InstallableModule($config, $key))->installer($this); } + /** + * Import module config from modules folder. + */ + protected function importModuleConfig(string $key): Collection + { + $moduleConfig = 'modules/'.str_replace('.', '/', $key).'/module.yaml'; + + $absolutePath = $this->starterKitPath($moduleConfig); + + if (! $this->files->exists($absolutePath)) { + throw new StarterKitException("Starter kit module config [$moduleConfig] does not exist."); + } + + return collect(YAML::parse($this->files->get($absolutePath))); + } + + /** + * Ensure starter kit has config. + * + * @throws StarterKitException + */ + protected function ensureModuleConfig(): self + { + if (! $this->files->exists($this->starterKitPath('starter-kit.yaml'))) { + throw new StarterKitException('Starter kit config [starter-kit.yaml] does not exist.'); + } + + return $this; + } + /** * Normalize module key. */ From ee5f96970e65f85c824cfd3936fe0b9df00c5716 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Sat, 9 Nov 2024 21:21:20 -0500 Subject: [PATCH 09/30] Add test coverage for error handling. --- tests/StarterKits/InstallTest.php | 78 +++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/tests/StarterKits/InstallTest.php b/tests/StarterKits/InstallTest.php index 1c63eaf36c8..02732f4202c 100644 --- a/tests/StarterKits/InstallTest.php +++ b/tests/StarterKits/InstallTest.php @@ -1217,6 +1217,84 @@ public function it_doesnt_require_anything_installable_if_module_contains_nested $this->assertFileExists(base_path('copied.md')); } + #[Test] + public function it_requires_imported_module_folder_config() + { + $this->setConfig([ + 'modules' => [ + 'seo' => 'import', + ], + ]); + + $this->assertFileDoesNotExist(base_path('copied.md')); + + $this + ->installCoolRunnings() + ->expectsOutput('Starter kit module config [modules/seo/module.yaml] does not exist.') + ->assertFailed(); + + $this->assertFileDoesNotExist(base_path('copied.md')); + } + + #[Test] + public function it_requires_nested_imported_module_folder_config() + { + $this->setConfig([ + 'modules' => [ + 'seo' => 'import', + ], + ]); + + $this->setConfig( + path: 'modules/seo/module.yaml', + config: [ + 'modules' => [ + 'js' => [ + 'options' => [ + 'vue' => 'import', + ], + ], + ], + ], + ); + + $this->assertFileDoesNotExist(base_path('copied.md')); + + $this + ->installCoolRunnings() + ->expectsOutput('Starter kit module config [modules/seo/js/vue/module.yaml] does not exist.') + ->assertFailed(); + + $this->assertFileDoesNotExist(base_path('copied.md')); + } + + #[Test] + public function it_requires_valid_imported_module_folder_config() + { + $this->setConfig([ + 'modules' => [ + 'seo' => 'import', + ], + ]); + + $this->setConfig( + path: 'modules/seo/module.yaml', + config: [ + 'prompt' => false, + // no installable config! + ] + ); + + $this->assertFileDoesNotExist(base_path('copied.md')); + + $this + ->installCoolRunnings() + ->expectsOutput('Starter-kit module is missing `export_paths`, `dependencies`, or nested `modules`!') + ->assertFailed(); + + $this->assertFileDoesNotExist(base_path('copied.md')); + } + #[Test] #[DataProvider('validModuleConfigs')] public function it_passes_validation_if_module_export_paths_or_dependencies_or_nested_modules_are_properly_configured($config) From b9d96ea18bad32bff04310f2b6f7d3cd3594941e Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Sat, 9 Nov 2024 22:16:26 -0500 Subject: [PATCH 10/30] Extract module instantiation and flattening to helper class for John. --- src/StarterKits/InstallableModule.php | 2 +- src/StarterKits/InstallableModules.php | 176 +++++++++++++++++++++++++ src/StarterKits/Installer.php | 93 +------------ 3 files changed, 182 insertions(+), 89 deletions(-) create mode 100644 src/StarterKits/InstallableModules.php diff --git a/src/StarterKits/InstallableModule.php b/src/StarterKits/InstallableModule.php index 13f4134de8b..92671954140 100644 --- a/src/StarterKits/InstallableModule.php +++ b/src/StarterKits/InstallableModule.php @@ -19,7 +19,7 @@ final class InstallableModule extends Module * * @throws Exception|StarterKitException */ - public function installer($installer): self + public function installer(?Installer $installer): self { $this->installer = $installer; diff --git a/src/StarterKits/InstallableModules.php b/src/StarterKits/InstallableModules.php new file mode 100644 index 00000000000..be77126fdfa --- /dev/null +++ b/src/StarterKits/InstallableModules.php @@ -0,0 +1,176 @@ +config = collect($config); + + $this->starterKitPath = $starterKitPath; + + $this->files = app(Filesystem::class); + } + + /** + * Set installer instance. + */ + public function installer(?Installer $installer): self + { + $this->installer = $installer; + + return $this; + } + + /** + * Get all modules. + */ + public function all(): Collection + { + return $this->modules; + } + + /** + * Flatten all modules. + */ + public function flatten(): self + { + $this->modules = self::flattenModules($this->modules); + + return $this; + } + + /** + * Instantiate all modules. + */ + public function instantiate(): self + { + $this->modules = collect([ + 'top_level' => $this->instantiateModuleRecursively($this->config, 'top_level'), + ]); + + return $this; + } + + /** + * Recursively instantiate module and its nested modules. + */ + protected function instantiateModuleRecursively(Collection|array|string $config, string $key): InstallableModule + { + if ($config === 'import') { + $config = $this->importModuleConfig($key); + } + + if ($options = Arr::get($config, 'options')) { + $config['options'] = collect($options) + ->map(fn ($optionConfig, $optionKey) => $this->instantiateModuleRecursively( + $optionConfig, + $this->normalizeModuleKey($key, $optionKey), + )); + } + + if ($modules = Arr::get($config, 'modules')) { + $config['modules'] = collect($modules) + ->map(fn ($childConfig, $childKey) => $this->instantiateModuleRecursively( + $childConfig, + $this->normalizeModuleKey($key, $childKey), + )); + } + + return (new InstallableModule($config, $key))->installer($this->installer); + } + + /** + * Import module config from modules folder. + * + * @throws StarterKitException + */ + protected function importModuleConfig(string $key): Collection + { + $moduleConfig = $this->relativeModulePath('module.yaml', $key); + + $absolutePath = $this->starterKitPath($moduleConfig); + + if (! $this->files->exists($absolutePath)) { + throw new StarterKitException("Starter kit module config [$moduleConfig] does not exist."); + } + + $config = collect(YAML::parse($this->files->get($absolutePath))); + + // TODO: prefix from in export paths + + return $config; + } + + /** + * Ensure starter kit has config. + * + * @throws StarterKitException + */ + protected function ensureModuleConfig(): self + { + if (! $this->files->exists($this->starterKitPath('starter-kit.yaml'))) { + throw new StarterKitException('Starter kit config [starter-kit.yaml] does not exist.'); + } + + return $this; + } + + /** + * Normalize module key. + */ + protected function normalizeModuleKey(string $key, string $childKey): string + { + return $key !== 'top_level' ? "{$key}.{$childKey}" : $childKey; + } + + /** + * Assemble absolute starter kit path. + */ + protected function starterKitPath(?string $path = null): string + { + return collect([$this->starterKitPath, $path])->filter()->implode('/'); + } + + /** + * Assemble relative imported module path. + */ + protected function relativeModulePath(string $path, string $key): string + { + return 'modules/'.str_replace('.', '/', $key).Str::ensureLeft($path, '/'); + } + + /** + * Flatten modules. + */ + public static function flattenModules(Collection $modules): Collection + { + return $modules + ->flatMap(function ($module) { + return [ + $module->key() => $module, + ...static::flattenModules($module->config('options', collect())), + ...static::flattenModules($module->config('modules', collect())), + ]; + }) + ->filter(); + } +} diff --git a/src/StarterKits/Installer.php b/src/StarterKits/Installer.php index 9d7e8542512..9bcfc0631dc 100644 --- a/src/StarterKits/Installer.php +++ b/src/StarterKits/Installer.php @@ -16,7 +16,6 @@ use Statamic\Facades\YAML; use Statamic\StarterKits\Concerns\InteractsWithFilesystem; use Statamic\StarterKits\Exceptions\StarterKitException; -use Statamic\Support\Arr; use Statamic\Support\Str; use Statamic\Support\Traits\FluentlyGetsAndSets; @@ -292,87 +291,21 @@ protected function ensureConfig(): self */ protected function instantiateModules(): self { - $this->modules = collect([ - 'top_level' => $this->instantiateModuleRecursively($this->config(), 'top_level'), - ]); + $this->modules = (new InstallableModules($this->config(), $this->starterKitPath())) + ->installer($this) + ->instantiate(); return $this; } - /** - * Recursively instantiate module and its nested modules. - */ - protected function instantiateModuleRecursively(Collection|array|string $config, string $key): InstallableModule - { - if ($config === 'import') { - $config = $this->importModuleConfig($key); - } - - if ($options = Arr::get($config, 'options')) { - $config['options'] = collect($options) - ->map(fn ($optionConfig, $optionKey) => $this->instantiateModuleRecursively( - $optionConfig, - $this->normalizeModuleKey($key, $optionKey), - )); - } - - if ($modules = Arr::get($config, 'modules')) { - $config['modules'] = collect($modules) - ->map(fn ($childConfig, $childKey) => $this->instantiateModuleRecursively( - $childConfig, - $this->normalizeModuleKey($key, $childKey), - )); - } - - return (new InstallableModule($config, $key))->installer($this); - } - - /** - * Import module config from modules folder. - */ - protected function importModuleConfig(string $key): Collection - { - $moduleConfig = 'modules/'.str_replace('.', '/', $key).'/module.yaml'; - - $absolutePath = $this->starterKitPath($moduleConfig); - - if (! $this->files->exists($absolutePath)) { - throw new StarterKitException("Starter kit module config [$moduleConfig] does not exist."); - } - - return collect(YAML::parse($this->files->get($absolutePath))); - } - - /** - * Ensure starter kit has config. - * - * @throws StarterKitException - */ - protected function ensureModuleConfig(): self - { - if (! $this->files->exists($this->starterKitPath('starter-kit.yaml'))) { - throw new StarterKitException('Starter kit config [starter-kit.yaml] does not exist.'); - } - - return $this; - } - - /** - * Normalize module key. - */ - protected function normalizeModuleKey(string $key, string $childKey): string - { - return $key !== 'top_level' ? "{$key}.{$childKey}" : $childKey; - } - /** * Filter and prepare flattened collection of installable modules. */ protected function filterInstallableModules(): self { - $this->modules = $this->modules + $this->modules = $this->modules->all() ->map(fn ($module) => $this->prepareInstallableRecursively($module)) - ->pipe(fn ($module) => $this->flattenModules($module)) + ->pipe(fn ($module) => InstallableModules::flattenModules($module)) ->each(fn ($module) => $module->validate()); return $this; @@ -454,22 +387,6 @@ protected function prepareSelectModule(InstallableModule $module): InstallableMo return $module->config('options')[$choice]; } - /** - * Flatten modules. - */ - public function flattenModules(Collection $modules): Collection - { - return $modules - ->flatMap(function ($module) { - return [ - $module->key() => $module, - ...$this->flattenModules($module->config('options', collect())), - ...$this->flattenModules($module->config('modules', collect())), - ]; - }) - ->filter(); - } - /** * Install all the modules. */ From bb52694bbfcbf5e153c0b80c5382e5edd551ef45 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Sat, 9 Nov 2024 22:31:02 -0500 Subject: [PATCH 11/30] Make into a more generic config setter. --- src/StarterKits/InstallableModule.php | 10 ---------- src/StarterKits/Installer.php | 5 +++-- src/StarterKits/Module.php | 10 ++++++++++ 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/StarterKits/InstallableModule.php b/src/StarterKits/InstallableModule.php index 92671954140..d30c8ffadb5 100644 --- a/src/StarterKits/InstallableModule.php +++ b/src/StarterKits/InstallableModule.php @@ -26,16 +26,6 @@ public function installer(?Installer $installer): self return $this; } - /** - * Set child modules onto instance, after prompting and filtering. - */ - public function setChildModules(Collection $modules): self - { - $this->config['modules'] = collect($modules); - - return $this; - } - /** * Validate starter kit module is installable. * diff --git a/src/StarterKits/Installer.php b/src/StarterKits/Installer.php index 9bcfc0631dc..c325165f37b 100644 --- a/src/StarterKits/Installer.php +++ b/src/StarterKits/Installer.php @@ -323,8 +323,9 @@ protected function prepareInstallableRecursively(InstallableModule $installable) } if ($modules = $module->config('modules')) { - $module->setChildModules( - $modules + $module->set( + key: 'modules', + value: $modules ->map(fn ($childModule) => $this->prepareInstallableRecursively($childModule)) ->filter() ); diff --git a/src/StarterKits/Module.php b/src/StarterKits/Module.php index 3e00ce798c0..461d6a47f0b 100644 --- a/src/StarterKits/Module.php +++ b/src/StarterKits/Module.php @@ -42,6 +42,16 @@ public function isTopLevelModule(): bool return $this->key === 'top_level'; } + /** + * Set config. + */ + public function set(string $key, mixed $value): self + { + $this->config[$key] = $value; + + return $this; + } + /** * Get module config. */ From d8641c5077e9c45966c2abef75877f9aa84f482c Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Sat, 9 Nov 2024 22:32:24 -0500 Subject: [PATCH 12/30] Use helper method. --- src/StarterKits/Installer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StarterKits/Installer.php b/src/StarterKits/Installer.php index c325165f37b..35c85039905 100644 --- a/src/StarterKits/Installer.php +++ b/src/StarterKits/Installer.php @@ -339,7 +339,7 @@ protected function prepareInstallableRecursively(InstallableModule $installable) */ protected function prepareInstallableModule(InstallableModule $module): InstallableModule|bool { - if ($module->key() === 'top_level') { + if ($module->isTopLevelModule()) { return $module; } From 3e71b181d5b1b680da7ade60724df946b9c3eccd Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Sun, 10 Nov 2024 21:11:48 -0500 Subject: [PATCH 13/30] Update to match docs PR. --- src/StarterKits/InstallableModules.php | 2 +- tests/StarterKits/InstallTest.php | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/StarterKits/InstallableModules.php b/src/StarterKits/InstallableModules.php index be77126fdfa..469c0962872 100644 --- a/src/StarterKits/InstallableModules.php +++ b/src/StarterKits/InstallableModules.php @@ -75,7 +75,7 @@ public function instantiate(): self */ protected function instantiateModuleRecursively(Collection|array|string $config, string $key): InstallableModule { - if ($config === 'import') { + if ($config === '@import') { $config = $this->importModuleConfig($key); } diff --git a/tests/StarterKits/InstallTest.php b/tests/StarterKits/InstallTest.php index 02732f4202c..d3b66af2f89 100644 --- a/tests/StarterKits/InstallTest.php +++ b/tests/StarterKits/InstallTest.php @@ -937,7 +937,7 @@ public function it_installs_imported_modules_confirmed_interactively_via_prompt( 'copied.md', ], 'modules' => [ - 'seo' => 'import', // import! + 'seo' => '@import', // import! 'bobsled' => [ 'export_paths' => [ 'resources/css/bobsled.css', @@ -951,7 +951,7 @@ public function it_installs_imported_modules_confirmed_interactively_via_prompt( 'resources/css/theme.css' => 'resources/css/jamaica.css', ], ], - 'js' => 'import', // import! + 'js' => '@import', // import! 'oldschool_js' => [ 'options' => [ 'jquery' => [ @@ -990,7 +990,7 @@ public function it_installs_imported_modules_confirmed_interactively_via_prompt( 'resources/js/react.js', ], ], - 'vue' => 'import', // import option as separate module! + 'vue' => '@import', // import option as separate module! 'svelte' => [ 'export_paths' => [ 'resources/js/svelte.js', @@ -1222,7 +1222,7 @@ public function it_requires_imported_module_folder_config() { $this->setConfig([ 'modules' => [ - 'seo' => 'import', + 'seo' => '@import', ], ]); @@ -1241,7 +1241,7 @@ public function it_requires_nested_imported_module_folder_config() { $this->setConfig([ 'modules' => [ - 'seo' => 'import', + 'seo' => '@import', ], ]); @@ -1251,7 +1251,7 @@ public function it_requires_nested_imported_module_folder_config() 'modules' => [ 'js' => [ 'options' => [ - 'vue' => 'import', + 'vue' => '@import', ], ], ], @@ -1273,7 +1273,7 @@ public function it_requires_valid_imported_module_folder_config() { $this->setConfig([ 'modules' => [ - 'seo' => 'import', + 'seo' => '@import', ], ]); @@ -1554,7 +1554,7 @@ public function it_installs_nested_imported_modules_confirmed_interactively_via_ 'copied.md', ], 'modules' => [ - 'seo' => 'import', + 'seo' => '@import', 'canada' => [ 'export_paths' => [ 'resources/css/hockey.css', @@ -1572,7 +1572,7 @@ public function it_installs_nested_imported_modules_confirmed_interactively_via_ 'resources/css/theme.css' => 'resources/css/jamaica.css', ], 'modules' => [ - 'bobsled' => 'import', // import nested module! + 'bobsled' => '@import', // import nested module! ], ], ], @@ -1610,7 +1610,7 @@ public function it_installs_nested_imported_modules_confirmed_interactively_via_ 'i-love-vue/test-helpers' => '^1.5', ], 'modules' => [ - 'testing_tools' => 'import', // import nested module! + 'testing_tools' => '@import', // import nested module! ], ], 'svelte' => [ From 889d6dbd4458d7cde0e873ea3fca7e3cbbda25fe Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Sun, 10 Nov 2024 21:23:43 -0500 Subject: [PATCH 14/30] Import merging. --- src/StarterKits/InstallableModules.php | 4 ++ tests/StarterKits/InstallTest.php | 76 +++++++++++++++++++++++++- 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/src/StarterKits/InstallableModules.php b/src/StarterKits/InstallableModules.php index 469c0962872..a05735998b1 100644 --- a/src/StarterKits/InstallableModules.php +++ b/src/StarterKits/InstallableModules.php @@ -79,6 +79,10 @@ protected function instantiateModuleRecursively(Collection|array|string $config, $config = $this->importModuleConfig($key); } + if (Arr::get($config, 'import') === '@config') { + $config = $this->importModuleConfig($key)->merge($config); + } + if ($options = Arr::get($config, 'options')) { $config['options'] = collect($options) ->map(fn ($optionConfig, $optionKey) => $this->instantiateModuleRecursively( diff --git a/tests/StarterKits/InstallTest.php b/tests/StarterKits/InstallTest.php index d3b66af2f89..938a4f4a74c 100644 --- a/tests/StarterKits/InstallTest.php +++ b/tests/StarterKits/InstallTest.php @@ -1048,7 +1048,7 @@ public function it_installs_imported_modules_confirmed_interactively_via_prompt( } #[Test] - public function it_display_custom_module_prompts() + public function it_displays_custom_module_prompts() { $this->setConfig([ 'modules' => [ @@ -1113,6 +1113,80 @@ public function it_display_custom_module_prompts() $this->assertFileExists(base_path('resources/js/svelte.js')); } + #[Test] + public function it_can_merge_imported_module_config_with_starter_kit_config() + { + $this->setConfig([ + 'modules' => [ + 'seo' => [ + 'prompt' => 'Want some extra SEO magic?', // handle prompt flow here + 'import' => '@config', // but import and merge rest of config + ], + 'js' => [ + 'prompt' => 'Want one of these fancy JS options?', + 'options' => [ + 'react' => [ + 'label' => 'React JS', // handle prompt option label here + 'import' => '@config', // but import and merge rest of config + ], + 'svelte' => [ + 'export_paths' => [ + 'resources/js/svelte.js', + ], + ], + ], + ], + ], + ]); + + $this->setConfig( + path: 'modules/seo/module.yaml', + config: [ + 'prompt' => 'This should not get used, because prompt config in starter-kit.yaml takes precedence!', + 'dependencies' => [ + 'statamic/seo-pro' => '^0.2.0', // but this should still be imported and installed + ], + ], + ); + + $this->setConfig( + path: 'modules/js/react/module.yaml', + config: [ + 'label' => 'This should not get used, because prompt config in starter-kit.yaml takes precedence!', + 'export_paths' => [ + 'resources/js/react.js', + ], + ], + ); + + $this->assertComposerJsonDoesntHave('statamic/seo-pro'); + $this->assertFileDoesNotExist(base_path('resources/js/react.js')); + $this->assertFileDoesNotExist(base_path('resources/js/vue.js')); + $this->assertFileDoesNotExist(base_path('resources/js/svelte.js')); + + $command = $this + ->installCoolRunningsModules() + ->expectsConfirmation('Want some extra SEO magic?', 'yes'); + + // Some fixes to `expectsChoice()` were merged for us, but are not available on 11.20.0 and below + // See: https://github.com/laravel/framework/pull/52408 + if (version_compare(app()->version(), '11.20.0', '>')) { + $command->expectsChoice('Want one of these fancy JS options?', 'react', [ + 'skip_module' => 'No', + 'react' => 'React JS', + 'svelte' => 'Svelte', + ]); + } else { + $command->expectsQuestion('Want one of these fancy JS options?', 'react'); + } + + $command->run(); + + $this->assertComposerJsonHasPackageVersion('require', 'statamic/seo-pro', '^0.2.0'); + $this->assertFileExists(base_path('resources/js/react.js')); + $this->assertFileDoesNotExist(base_path('resources/js/svelte.js')); + } + #[Test] public function it_installs_modules_without_dependencies() { From a0b42b1434831788524755448f421db889158196 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Sun, 10 Nov 2024 22:39:19 -0500 Subject: [PATCH 15/30] Fix and properly implement module folder scoping. --- src/StarterKits/InstallableModule.php | 32 ++++++++++++++++--- src/StarterKits/InstallableModules.php | 44 ++++++++++++++++++-------- src/StarterKits/Installer.php | 1 - tests/StarterKits/InstallTest.php | 38 ++++++++++++++-------- 4 files changed, 82 insertions(+), 33 deletions(-) diff --git a/src/StarterKits/InstallableModule.php b/src/StarterKits/InstallableModule.php index d30c8ffadb5..79c8fa367ec 100644 --- a/src/StarterKits/InstallableModule.php +++ b/src/StarterKits/InstallableModule.php @@ -13,6 +13,7 @@ final class InstallableModule extends Module { protected $installer; + protected $relativePath; /** * Set installer instance. @@ -26,6 +27,16 @@ public function installer(?Installer $installer): self return $this; } + /** + * Set relative module path. + */ + public function setRelativePath(string $path): self + { + $this->relativePath = $path; + + return $this; + } + /** * Validate starter kit module is installable. * @@ -125,8 +136,10 @@ protected function installableFiles(): Collection */ protected function expandExportDirectoriesToFiles(string $to, ?string $from = null): Collection { + $from = $this->relativePath($from ?? $to); + + $from = Path::tidy($this->starterKitPath($from)); $to = Path::tidy($this->starterKitPath($to)); - $from = Path::tidy($from ? $this->starterKitPath($from) : $to); $paths = collect([$from => $to]); @@ -184,9 +197,8 @@ protected function installableDependencies(string $configKey): array protected function ensureInstallableFilesExist(): self { $this - ->exportPaths() - ->merge($this->exportAsPaths()) - ->reject(fn ($path) => $this->files->exists($this->starterKitPath($path))) + ->installableFiles() + ->reject(fn ($to, $from) => $this->files->exists($from)) ->each(function ($path) { throw new StarterKitException("Starter kit path [{$path}] does not exist."); }); @@ -238,6 +250,18 @@ protected function starterKitPath(?string $path = null): string return collect([base_path("vendor/{$package}"), $path])->filter()->implode('/'); } + /** + * Get relative module path. + */ + protected function relativePath(string $path): string + { + if (! $this->relativePath) { + return $path; + } + + return Str::ensureRight($this->relativePath, '/').$path; + } + /** * Normalize packages array to require args, with version handling if `package => version` array structure is passed. */ diff --git a/src/StarterKits/InstallableModules.php b/src/StarterKits/InstallableModules.php index a05735998b1..ef56cb5ae7c 100644 --- a/src/StarterKits/InstallableModules.php +++ b/src/StarterKits/InstallableModules.php @@ -73,21 +73,22 @@ public function instantiate(): self /** * Recursively instantiate module and its nested modules. */ - protected function instantiateModuleRecursively(Collection|array|string $config, string $key): InstallableModule + protected function instantiateModuleRecursively(Collection|array|string $config, string $key, ?string $moduleScope = null): InstallableModule { - if ($config === '@import') { + if ($imported = $config === '@import') { $config = $this->importModuleConfig($key); - } - - if (Arr::get($config, 'import') === '@config') { + } elseif ($imported = Arr::get($config, 'import') === '@config') { $config = $this->importModuleConfig($key)->merge($config); } + $moduleScope = $imported ? $key : $moduleScope; + if ($options = Arr::get($config, 'options')) { $config['options'] = collect($options) ->map(fn ($optionConfig, $optionKey) => $this->instantiateModuleRecursively( $optionConfig, $this->normalizeModuleKey($key, $optionKey), + $moduleScope, )); } @@ -96,10 +97,17 @@ protected function instantiateModuleRecursively(Collection|array|string $config, ->map(fn ($childConfig, $childKey) => $this->instantiateModuleRecursively( $childConfig, $this->normalizeModuleKey($key, $childKey), + $moduleScope, )); } - return (new InstallableModule($config, $key))->installer($this->installer); + $module = (new InstallableModule($config, $key))->installer($this->installer); + + if ($moduleScope) { + $this->scopeInstallableFiles($module, $moduleScope); + } + + return $module; } /** @@ -109,7 +117,7 @@ protected function instantiateModuleRecursively(Collection|array|string $config, */ protected function importModuleConfig(string $key): Collection { - $moduleConfig = $this->relativeModulePath('module.yaml', $key); + $moduleConfig = $this->relativeModulePath($key, 'module.yaml'); $absolutePath = $this->starterKitPath($moduleConfig); @@ -117,11 +125,7 @@ protected function importModuleConfig(string $key): Collection throw new StarterKitException("Starter kit module config [$moduleConfig] does not exist."); } - $config = collect(YAML::parse($this->files->get($absolutePath))); - - // TODO: prefix from in export paths - - return $config; + return collect(YAML::parse($this->files->get($absolutePath))); } /** @@ -157,9 +161,21 @@ protected function starterKitPath(?string $path = null): string /** * Assemble relative imported module path. */ - protected function relativeModulePath(string $path, string $key): string + protected function relativeModulePath(string $key, ?string $path = null): string + { + $base = 'modules/'.str_replace('.', '/', $key); + + return $path + ? $base.Str::ensureLeft($path, '/') + : $base; + } + + /** + * Scope installable files to imported module. + */ + protected function scopeInstallableFiles(InstallableModule $module, string $scope): void { - return 'modules/'.str_replace('.', '/', $key).Str::ensureLeft($path, '/'); + $module->setRelativePath($this->relativeModulePath($scope)); } /** diff --git a/src/StarterKits/Installer.php b/src/StarterKits/Installer.php index 35c85039905..ff5a54211be 100644 --- a/src/StarterKits/Installer.php +++ b/src/StarterKits/Installer.php @@ -7,7 +7,6 @@ use Facades\Statamic\StarterKits\Hook; use Illuminate\Console\Command; use Illuminate\Filesystem\Filesystem; -use Illuminate\Support\Collection; use Illuminate\Support\Facades\Http; use Statamic\Console\NullConsole; use Statamic\Console\Please\Application as PleaseApplication; diff --git a/tests/StarterKits/InstallTest.php b/tests/StarterKits/InstallTest.php index 938a4f4a74c..834da8322d2 100644 --- a/tests/StarterKits/InstallTest.php +++ b/tests/StarterKits/InstallTest.php @@ -973,7 +973,7 @@ public function it_installs_imported_modules_confirmed_interactively_via_prompt( path: 'modules/seo/module.yaml', config: [ 'export_paths' => [ - 'resources/css/seo.css', + $this->moveKitRepoFile('modules/seo', 'resources/css/seo.css'), ], 'dependencies' => [ 'statamic/seo-pro' => '^0.2.0', @@ -987,13 +987,13 @@ public function it_installs_imported_modules_confirmed_interactively_via_prompt( 'options' => [ 'react' => [ 'export_paths' => [ - 'resources/js/react.js', + $this->moveKitRepoFile('modules/js', 'resources/js/react.js'), ], ], 'vue' => '@import', // import option as separate module! 'svelte' => [ 'export_paths' => [ - 'resources/js/svelte.js', + $this->moveKitRepoFile('modules/js', 'resources/js/svelte.js'), ], ], ], @@ -1004,7 +1004,7 @@ public function it_installs_imported_modules_confirmed_interactively_via_prompt( path: 'modules/js/vue/module.yaml', config: [ 'export_paths' => [ - 'resources/js/vue.js', + $this->moveKitRepoFile('modules/js/vue', 'resources/js/vue.js'), ], 'dependencies' => [ 'bobsled/vue-components' => '^1.5', @@ -1154,7 +1154,7 @@ public function it_can_merge_imported_module_config_with_starter_kit_config() config: [ 'label' => 'This should not get used, because prompt config in starter-kit.yaml takes precedence!', 'export_paths' => [ - 'resources/js/react.js', + $this->moveKitRepoFile('modules/js/react', 'resources/js/react.js'), ], ], ); @@ -1656,7 +1656,7 @@ public function it_installs_nested_imported_modules_confirmed_interactively_via_ path: 'modules/seo/module.yaml', config: [ 'export_paths' => [ - 'resources/css/seo.css', + $this->moveKitRepoFile('modules/seo', 'resources/css/seo.css'), ], 'dependencies' => [ 'statamic/seo-pro' => '^0.2.0', @@ -1666,19 +1666,19 @@ public function it_installs_nested_imported_modules_confirmed_interactively_via_ 'options' => [ 'react' => [ 'export_paths' => [ - 'resources/js/react.js', + $this->moveKitRepoFile('modules/seo', 'resources/js/react.js'), ], 'modules' => [ 'testing_tools' => [ 'export_paths' => [ - 'resources/js/react-testing-tools.js', + $this->moveKitRepoFile('modules/seo', 'resources/js/react-testing-tools.js'), ], ], ], ], 'vue' => [ 'export_paths' => [ - 'resources/js/vue.js', + $this->moveKitRepoFile('modules/seo', 'resources/js/vue.js'), ], 'dependencies_dev' => [ 'i-love-vue/test-helpers' => '^1.5', @@ -1689,7 +1689,7 @@ public function it_installs_nested_imported_modules_confirmed_interactively_via_ ], 'svelte' => [ 'export_paths' => [ - 'resources/js/svelte.js', + $this->moveKitRepoFile('modules/seo', 'resources/js/svelte.js'), ], ], ], @@ -1698,12 +1698,12 @@ public function it_installs_nested_imported_modules_confirmed_interactively_via_ 'options' => [ 'jquery' => [ 'export_paths' => [ - 'resources/js/jquery.js', + $this->moveKitRepoFile('modules/seo', 'resources/js/jquery.js'), ], ], 'mootools' => [ 'export_paths' => [ - 'resources/js/jquery.js', + $this->moveKitRepoFile('modules/seo', 'resources/js/mootools.js'), ], ], ], @@ -1716,7 +1716,7 @@ public function it_installs_nested_imported_modules_confirmed_interactively_via_ path: 'modules/seo/js/vue/testing_tools/module.yaml', config: [ 'export_paths' => [ - 'resources/js/vue-testing-tools.js', + $this->moveKitRepoFile('modules/seo/js/vue/testing_tools', 'resources/js/vue-testing-tools.js'), ], ], ); @@ -1725,7 +1725,7 @@ public function it_installs_nested_imported_modules_confirmed_interactively_via_ path: 'modules/jamaica/bobsled/module.yaml', config: [ 'export_paths' => [ - 'resources/css/bobsled.css', + $this->moveKitRepoFile('modules/jamaica/bobsled', 'resources/css/bobsled.css'), ], 'dependencies' => [ 'bobsled/speed-calculator' => '^1.0.0', @@ -1798,6 +1798,16 @@ private function setConfig($config, $path = 'starter-kit.yaml') $this->files->put($this->preparePath($this->kitRepoPath($path)), YAML::dump($config)); } + private function moveKitRepoFile($relativeModulePath, $relativeFilePath) + { + $this->files->move( + $this->kitRepoPath($relativeFilePath), + $this->preparePath($this->kitRepoPath(Str::ensureRight($relativeModulePath, '/').$relativeFilePath)), + ); + + return $relativeFilePath; + } + private function preparePath($path) { $folder = preg_replace('/(.*)\/[^\/]+\.[^\/]+/', '$1', $path); From cc89b468cccd056ecd945738ff0e4a773cdb0173 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Sun, 10 Nov 2024 23:22:36 -0500 Subject: [PATCH 16/30] Make this public for John. --- src/StarterKits/InstallableModule.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StarterKits/InstallableModule.php b/src/StarterKits/InstallableModule.php index 79c8fa367ec..5c98b319b34 100644 --- a/src/StarterKits/InstallableModule.php +++ b/src/StarterKits/InstallableModule.php @@ -113,7 +113,7 @@ protected function installDependencies(): self /** * Get installable files. */ - protected function installableFiles(): Collection + public function installableFiles(): Collection { $installableFromExportPaths = $this ->exportPaths() From 37b10c2050df6427d2b650b251373e665585672a Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Sun, 10 Nov 2024 23:32:25 -0500 Subject: [PATCH 17/30] Export whole modules folder. --- src/StarterKits/Exporter.php | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/StarterKits/Exporter.php b/src/StarterKits/Exporter.php index a62a54f996f..ddbfa123f4e 100644 --- a/src/StarterKits/Exporter.php +++ b/src/StarterKits/Exporter.php @@ -40,7 +40,8 @@ public function export(): void ->validateExportPath() ->validateConfig() ->instantiateModules() - ->exportModules() + ->exportInlineModules() + ->exportModulesFolder() ->exportConfig() ->exportHooks() ->exportComposerJson(); @@ -133,15 +134,25 @@ protected function normalizeModuleKey(string $key, string $childKey): string } /** - * Export all the modules. + * Export all inline modules. */ - protected function exportModules(): self + protected function exportInlineModules(): self { $this->modules->each(fn ($module) => $module->export($this->exportPath)); return $this; } + /** + * Export modules folder. + */ + protected function exportModulesFolder(): self + { + $this->files->copyDirectory(base_path('modules'), "{$this->exportPath}/modules"); + + return $this; + } + /** * Get starter kit config. */ From 5ded91ebbff47421cfa3485e77deed7c73b6f7e5 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Mon, 11 Nov 2024 17:19:06 -0500 Subject: [PATCH 18/30] =?UTF-8?q?Remove=20`import:=20=E2=80=98@config?= =?UTF-8?q?=E2=80=99`=20syntax,=20and=20implicitly=20import=20instead.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/StarterKits/InstallableModules.php | 12 +++++++++++- tests/StarterKits/InstallTest.php | 6 +++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/StarterKits/InstallableModules.php b/src/StarterKits/InstallableModules.php index ef56cb5ae7c..d38e1ef5bec 100644 --- a/src/StarterKits/InstallableModules.php +++ b/src/StarterKits/InstallableModules.php @@ -77,7 +77,7 @@ protected function instantiateModuleRecursively(Collection|array|string $config, { if ($imported = $config === '@import') { $config = $this->importModuleConfig($key); - } elseif ($imported = Arr::get($config, 'import') === '@config') { + } elseif ($imported = $this->moduleConfigExists($key)) { $config = $this->importModuleConfig($key)->merge($config); } @@ -170,6 +170,16 @@ protected function relativeModulePath(string $key, ?string $path = null): string : $base; } + /** + * Determine whether module config exists. + */ + protected function moduleConfigExists(string $key): bool + { + return $this->files->exists( + $this->starterKitPath($this->relativeModulePath($key, 'module.yaml')) + ); + } + /** * Scope installable files to imported module. */ diff --git a/tests/StarterKits/InstallTest.php b/tests/StarterKits/InstallTest.php index 834da8322d2..cc732b5c14a 100644 --- a/tests/StarterKits/InstallTest.php +++ b/tests/StarterKits/InstallTest.php @@ -1120,14 +1120,14 @@ public function it_can_merge_imported_module_config_with_starter_kit_config() 'modules' => [ 'seo' => [ 'prompt' => 'Want some extra SEO magic?', // handle prompt flow here - 'import' => '@config', // but import and merge rest of config + // implicitly import and merge rest of config in here ], 'js' => [ 'prompt' => 'Want one of these fancy JS options?', 'options' => [ 'react' => [ 'label' => 'React JS', // handle prompt option label here - 'import' => '@config', // but import and merge rest of config + // implicitly import and merge rest of config in here ], 'svelte' => [ 'export_paths' => [ @@ -1154,7 +1154,7 @@ public function it_can_merge_imported_module_config_with_starter_kit_config() config: [ 'label' => 'This should not get used, because prompt config in starter-kit.yaml takes precedence!', 'export_paths' => [ - $this->moveKitRepoFile('modules/js/react', 'resources/js/react.js'), + $this->moveKitRepoFile('modules/js/react', 'resources/js/react.js'), // but this should still be imported and installed ], ], ); From 92d939fe265abf8901eced764d19ad6bb5c77ad3 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Thu, 19 Dec 2024 17:04:23 -0500 Subject: [PATCH 19/30] Fix issues from last merge. --- src/StarterKits/InstallableModule.php | 2 +- src/StarterKits/Installer.php | 11 +++++------ tests/StarterKits/InstallTest.php | 6 +++--- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/StarterKits/InstallableModule.php b/src/StarterKits/InstallableModule.php index 4ee11ebe60b..4d537ad2c44 100644 --- a/src/StarterKits/InstallableModule.php +++ b/src/StarterKits/InstallableModule.php @@ -209,7 +209,7 @@ protected function ensureInstallableFilesExist(): self { $this ->installableFiles() - ->reject(fn ($path) => $this->files->exists($this->installableFilesPath($path))) + ->reject(fn ($to, $from) => $this->files->exists($from)) ->each(function ($path) { throw new StarterKitException("Starter kit path [{$path}] does not exist."); }); diff --git a/src/StarterKits/Installer.php b/src/StarterKits/Installer.php index a1310626cc7..5b5f804d251 100644 --- a/src/StarterKits/Installer.php +++ b/src/StarterKits/Installer.php @@ -16,7 +16,6 @@ use Statamic\Facades\YAML; use Statamic\StarterKits\Concerns\InteractsWithFilesystem; use Statamic\StarterKits\Exceptions\StarterKitException; -use Statamic\Support\Arr; use Statamic\Support\Str; use Statamic\Support\Traits\FluentlyGetsAndSets; @@ -365,8 +364,8 @@ protected function prepareSelectModule(InstallableModule $module): InstallableMo $skipOptionLabel = $module->config('skip_option', 'No'); $skipModuleValue = 'skip_module'; - $options = collect($config['options']) - ->map(fn ($option, $optionKey) => Arr::get($option, 'label', ucfirst($optionKey))) + $options = collect($module->config('options')) + ->map(fn ($option, $optionKey) => $option->config('label', ucfirst($optionKey))) ->when($skipOptionLabel !== false, fn ($c) => $c->prepend($skipOptionLabel, $skipModuleValue)) ->all(); @@ -374,11 +373,11 @@ protected function prepareSelectModule(InstallableModule $module): InstallableMo if ($this->isInteractive) { $choice = select( - label: Arr::get($config, 'prompt', "Would you like to install one of the following [{$name}] modules?"), + label: $module->config('prompt', "Would you like to install one of the following [{$name}] modules?"), options: $options, - default: Arr::get($config, 'default'), + default: $module->config('default'), ); - } elseif (! $this->isInteractive && ! $choice = Arr::get($config, 'default')) { + } elseif (! $this->isInteractive && ! $choice = $module->config('default')) { return false; } diff --git a/tests/StarterKits/InstallTest.php b/tests/StarterKits/InstallTest.php index 88d28e3ec1f..7c0598c7e16 100644 --- a/tests/StarterKits/InstallTest.php +++ b/tests/StarterKits/InstallTest.php @@ -1842,7 +1842,7 @@ public function it_requires_imported_module_folder_config() $this ->installCoolRunnings() - ->expectsOutput('Starter kit module config [modules/seo/module.yaml] does not exist.') + ->expectsOutputToContain('Starter kit module config [modules/seo/module.yaml] does not exist.') ->assertFailed(); $this->assertFileDoesNotExist(base_path('copied.md')); @@ -1874,7 +1874,7 @@ public function it_requires_nested_imported_module_folder_config() $this ->installCoolRunnings() - ->expectsOutput('Starter kit module config [modules/seo/js/vue/module.yaml] does not exist.') + ->expectsOutputToContain('Starter kit module config [modules/seo/js/vue/module.yaml] does not exist.') ->assertFailed(); $this->assertFileDoesNotExist(base_path('copied.md')); @@ -1901,7 +1901,7 @@ public function it_requires_valid_imported_module_folder_config() $this ->installCoolRunnings() - ->expectsOutput('Starter-kit module is missing `export_paths`, `dependencies`, or nested `modules`!') + ->expectsOutputToContain('Starter-kit module is missing `export_paths`, `dependencies`, or nested `modules`.') ->assertFailed(); $this->assertFileDoesNotExist(base_path('copied.md')); From 03741d621b01ffcc93fce0182c139045235230ab Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Thu, 19 Dec 2024 17:05:44 -0500 Subject: [PATCH 20/30] Extract to helper method. --- src/StarterKits/Installer.php | 5 ++--- src/StarterKits/Module.php | 8 ++++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/StarterKits/Installer.php b/src/StarterKits/Installer.php index 5b5f804d251..6c16d912175 100644 --- a/src/StarterKits/Installer.php +++ b/src/StarterKits/Installer.php @@ -343,8 +343,7 @@ protected function prepareInstallableModule(InstallableModule $module): Installa $shouldPrompt = false; } - $name = str_replace(['_', '.'], ' ', $module->key()); - + $name = $module->keyReadable(); $default = $module->config('default', false); if ($shouldPrompt && $this->isInteractive && ! confirm($module->config('prompt', "Would you like to install the [{$name}] module?"), $default)) { @@ -369,7 +368,7 @@ protected function prepareSelectModule(InstallableModule $module): InstallableMo ->when($skipOptionLabel !== false, fn ($c) => $c->prepend($skipOptionLabel, $skipModuleValue)) ->all(); - $name = str_replace(['_', '.'], ' ', $module->key()); + $name = $module->keyReadable(); if ($this->isInteractive) { $choice = select( diff --git a/src/StarterKits/Module.php b/src/StarterKits/Module.php index b03b61eb514..594cacb0a2c 100644 --- a/src/StarterKits/Module.php +++ b/src/StarterKits/Module.php @@ -34,6 +34,14 @@ public function key(): string return $this->key; } + /** + * Get readable module key for default prompt display text. + */ + public function keyReadable(): string + { + return str_replace(['_', '.'], ' ', $this->key); + } + /** * Check if this is a top level module. */ From 758da1b5ccd38fcb66809d43a48428aae9b0b53d Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Fri, 20 Dec 2024 15:31:36 -0500 Subject: [PATCH 21/30] Extract to parent `Modules` class for reuse. --- src/StarterKits/InstallableModules.php | 182 ++------------------- src/StarterKits/Modules.php | 217 +++++++++++++++++++++++++ tests/StarterKits/InstallTest.php | 2 +- 3 files changed, 228 insertions(+), 173 deletions(-) create mode 100644 src/StarterKits/Modules.php diff --git a/src/StarterKits/InstallableModules.php b/src/StarterKits/InstallableModules.php index d38e1ef5bec..5e198972af9 100644 --- a/src/StarterKits/InstallableModules.php +++ b/src/StarterKits/InstallableModules.php @@ -2,33 +2,11 @@ namespace Statamic\StarterKits; -use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Collection; -use Statamic\Facades\YAML; -use Statamic\StarterKits\Exceptions\StarterKitException; -use Statamic\Support\Arr; -use Statamic\Support\Str; -final class InstallableModules +final class InstallableModules extends Modules { - protected $config; - protected $starterKitPath; - protected $files; - protected $starterKit; protected $installer; - protected $modules; - - /** - * Create installable modules helper. - */ - public function __construct(Collection|array $config, string $starterKitPath) - { - $this->config = collect($config); - - $this->starterKitPath = $starterKitPath; - - $this->files = app(Filesystem::class); - } /** * Set installer instance. @@ -41,166 +19,26 @@ public function installer(?Installer $installer): self } /** - * Get all modules. - */ - public function all(): Collection - { - return $this->modules; - } - - /** - * Flatten all modules. - */ - public function flatten(): self - { - $this->modules = self::flattenModules($this->modules); - - return $this; - } - - /** - * Instantiate all modules. - */ - public function instantiate(): self - { - $this->modules = collect([ - 'top_level' => $this->instantiateModuleRecursively($this->config, 'top_level'), - ]); - - return $this; - } - - /** - * Recursively instantiate module and its nested modules. - */ - protected function instantiateModuleRecursively(Collection|array|string $config, string $key, ?string $moduleScope = null): InstallableModule - { - if ($imported = $config === '@import') { - $config = $this->importModuleConfig($key); - } elseif ($imported = $this->moduleConfigExists($key)) { - $config = $this->importModuleConfig($key)->merge($config); - } - - $moduleScope = $imported ? $key : $moduleScope; - - if ($options = Arr::get($config, 'options')) { - $config['options'] = collect($options) - ->map(fn ($optionConfig, $optionKey) => $this->instantiateModuleRecursively( - $optionConfig, - $this->normalizeModuleKey($key, $optionKey), - $moduleScope, - )); - } - - if ($modules = Arr::get($config, 'modules')) { - $config['modules'] = collect($modules) - ->map(fn ($childConfig, $childKey) => $this->instantiateModuleRecursively( - $childConfig, - $this->normalizeModuleKey($key, $childKey), - $moduleScope, - )); - } - - $module = (new InstallableModule($config, $key))->installer($this->installer); - - if ($moduleScope) { - $this->scopeInstallableFiles($module, $moduleScope); - } - - return $module; - } - - /** - * Import module config from modules folder. - * - * @throws StarterKitException - */ - protected function importModuleConfig(string $key): Collection - { - $moduleConfig = $this->relativeModulePath($key, 'module.yaml'); - - $absolutePath = $this->starterKitPath($moduleConfig); - - if (! $this->files->exists($absolutePath)) { - throw new StarterKitException("Starter kit module config [$moduleConfig] does not exist."); - } - - return collect(YAML::parse($this->files->get($absolutePath))); - } - - /** - * Ensure starter kit has config. - * - * @throws StarterKitException - */ - protected function ensureModuleConfig(): self - { - if (! $this->files->exists($this->starterKitPath('starter-kit.yaml'))) { - throw new StarterKitException('Starter kit config [starter-kit.yaml] does not exist.'); - } - - return $this; - } - - /** - * Normalize module key. - */ - protected function normalizeModuleKey(string $key, string $childKey): string - { - return $key !== 'top_level' ? "{$key}.{$childKey}" : $childKey; - } - - /** - * Assemble absolute starter kit path. - */ - protected function starterKitPath(?string $path = null): string - { - return collect([$this->starterKitPath, $path])->filter()->implode('/'); - } - - /** - * Assemble relative imported module path. - */ - protected function relativeModulePath(string $key, ?string $path = null): string - { - $base = 'modules/'.str_replace('.', '/', $key); - - return $path - ? $base.Str::ensureLeft($path, '/') - : $base; - } - - /** - * Determine whether module config exists. + * Instantiate individual InstallableModule. */ - protected function moduleConfigExists(string $key): bool + protected function instantiateIndividualModule(array|Collection $config, string $key): Module { - return $this->files->exists( - $this->starterKitPath($this->relativeModulePath($key, 'module.yaml')) - ); + return (new InstallableModule($config, $key))->installer($this->installer); } /** - * Scope installable files to imported module. + * Override so that we do not prefix option key for installable modules. */ - protected function scopeInstallableFiles(InstallableModule $module, string $scope): void + protected function prefixOptionsKey(string $key): ?string { - $module->setRelativePath($this->relativeModulePath($scope)); + return $key; } /** - * Flatten modules. + * Override so that we do not prefix modules key for installable modules. */ - public static function flattenModules(Collection $modules): Collection + protected function prefixModulesKey(string $key): ?string { - return $modules - ->flatMap(function ($module) { - return [ - $module->key() => $module, - ...static::flattenModules($module->config('options', collect())), - ...static::flattenModules($module->config('modules', collect())), - ]; - }) - ->filter(); + return $key; } } diff --git a/src/StarterKits/Modules.php b/src/StarterKits/Modules.php new file mode 100644 index 00000000000..a03ce8ce858 --- /dev/null +++ b/src/StarterKits/Modules.php @@ -0,0 +1,217 @@ +config = collect($config); + + $this->basePath = $basePath; + + $this->files = app(Filesystem::class); + } + + /** + * Get all modules. + */ + public function all(): Collection + { + return $this->modules; + } + + /** + * Flatten all modules. + */ + public function flatten(): self + { + $this->modules = self::flattenModules($this->modules); + + return $this; + } + + /** + * Instantiate all modules. + */ + public function instantiate(): self + { + $this->modules = collect([ + 'top_level' => $this->instantiateModuleRecursively($this->config, 'top_level'), + ]); + + return $this; + } + + /** + * Get the preferences. + * + * @return array + */ + abstract protected function instantiateIndividualModule(array|Collection $config, string $key): Module; + + /** + * Recursively instantiate module and its nested modules. + */ + protected function instantiateModuleRecursively(Collection|array|string $config, string $key, ?string $moduleScope = null): Module + { + if ($imported = $config === '@import') { + $config = $this->importModuleConfig($key); + } elseif ($imported = $this->moduleConfigExists($key)) { + $config = $this->importModuleConfig($key)->merge($config); + } + + $moduleScope = $imported ? $key : $moduleScope; + + if ($options = Arr::get($config, 'options')) { + $config['options'] = collect($options) + ->map(fn ($optionConfig, $optionKey) => $this->instantiateModuleRecursively( + $optionConfig, + $this->normalizeModuleKey($key, $this->prefixOptionsKey($optionKey)), + $moduleScope, + )); + } + + if ($modules = Arr::get($config, 'modules')) { + $config['modules'] = collect($modules) + ->map(fn ($childConfig, $childKey) => $this->instantiateModuleRecursively( + $childConfig, + $this->normalizeModuleKey($key, $this->prefixModulesKey($childKey)), + $moduleScope, + )); + } + + $module = $this->instantiateIndividualModule($config, $key); + + if ($moduleScope) { + $this->scopeModulesPath($module, $moduleScope); + } + + return $module; + } + + /** + * Import module config from modules folder. + * + * @throws StarterKitException + */ + protected function importModuleConfig(string $key): Collection + { + $moduleConfig = $this->relativeModulePath($key, 'module.yaml'); + + $absolutePath = $this->basePath($moduleConfig); + + if (! $this->files->exists($absolutePath)) { + throw new StarterKitException("Starter kit module config [$moduleConfig] does not exist."); + } + + return collect(YAML::parse($this->files->get($absolutePath))); + } + + /** + * Ensure starter kit has config. + * + * @throws StarterKitException + */ + protected function ensureModuleConfig(): self + { + if (! $this->files->exists($this->basePath('starter-kit.yaml'))) { + throw new StarterKitException('Starter kit config [starter-kit.yaml] does not exist.'); + } + + return $this; + } + + /** + * Normalize module key. + */ + protected function normalizeModuleKey(string $key, string $childKey): string + { + return $key !== 'top_level' ? "{$key}.{$childKey}" : $childKey; + } + + /** + * Prefix options key. + */ + protected function prefixOptionsKey(string $key): ?string + { + return 'options.'.$key; + } + + /** + * Prefix modules key. + */ + protected function prefixModulesKey(string $key): ?string + { + return 'modules.'.$key; + } + + /** + * Assemble absolute path. + */ + protected function basePath(?string $path = null): string + { + return collect([$this->basePath, $path])->filter()->implode('/'); + } + + /** + * Assemble relative imported module path. + */ + protected function relativeModulePath(string $key, ?string $path = null): string + { + $base = 'modules/'.str_replace('.', '/', $key); + + return $path + ? $base.Str::ensureLeft($path, '/') + : $base; + } + + /** + * Determine whether module config exists. + */ + protected function moduleConfigExists(string $key): bool + { + return $this->files->exists( + $this->basePath($this->relativeModulePath($key, 'module.yaml')) + ); + } + + /** + * Scope modules path. + */ + protected function scopeModulesPath(Module $module, string $scope): void + { + $module->setRelativePath($this->relativeModulePath($scope)); + } + + /** + * Flatten modules. + */ + public static function flattenModules(Collection $modules): Collection + { + return $modules + ->flatMap(function ($module) { + return [ + $module->key() => $module, + ...static::flattenModules($module->config('options', collect())), + ...static::flattenModules($module->config('modules', collect())), + ]; + }) + ->filter(); + } +} diff --git a/tests/StarterKits/InstallTest.php b/tests/StarterKits/InstallTest.php index 7c0598c7e16..62fc168481a 100644 --- a/tests/StarterKits/InstallTest.php +++ b/tests/StarterKits/InstallTest.php @@ -1894,7 +1894,7 @@ public function it_requires_valid_imported_module_folder_config() config: [ 'prompt' => false, // no installable config! - ] + ], ); $this->assertFileDoesNotExist(base_path('copied.md')); From 6752dd0f2737680cb969f11a6e4558e6271197f3 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Fri, 20 Dec 2024 15:32:20 -0500 Subject: [PATCH 22/30] Extract to parent `Module` class for reuse. --- src/StarterKits/InstallableModule.php | 11 ----------- src/StarterKits/Module.php | 11 +++++++++++ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/StarterKits/InstallableModule.php b/src/StarterKits/InstallableModule.php index 4d537ad2c44..cad88cc5d3c 100644 --- a/src/StarterKits/InstallableModule.php +++ b/src/StarterKits/InstallableModule.php @@ -13,7 +13,6 @@ final class InstallableModule extends Module { protected $installer; - protected $relativePath; /** * Set installer instance. @@ -27,16 +26,6 @@ public function installer(?Installer $installer): self return $this; } - /** - * Set relative module path. - */ - public function setRelativePath(string $path): self - { - $this->relativePath = $path; - - return $this; - } - /** * Validate starter kit module is installable. * diff --git a/src/StarterKits/Module.php b/src/StarterKits/Module.php index 594cacb0a2c..000eb91faad 100644 --- a/src/StarterKits/Module.php +++ b/src/StarterKits/Module.php @@ -13,6 +13,7 @@ abstract class Module protected $files; protected $config; protected $key; + protected $relativePath; /** * Instantiate starter kit module. @@ -26,6 +27,16 @@ public function __construct(array|Collection $config, string $key) $this->key = $key; } + /** + * Set relative module path. + */ + public function setRelativePath(string $path): self + { + $this->relativePath = $path; + + return $this; + } + /** * Get module key. */ From 032b304086959aebd7af4e8096556cabbb60465a Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Fri, 20 Dec 2024 15:32:55 -0500 Subject: [PATCH 23/30] Implement `ExportableModule` instantiation. --- src/StarterKits/ExportableModules.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/StarterKits/ExportableModules.php diff --git a/src/StarterKits/ExportableModules.php b/src/StarterKits/ExportableModules.php new file mode 100644 index 00000000000..c7f34b51b6d --- /dev/null +++ b/src/StarterKits/ExportableModules.php @@ -0,0 +1,16 @@ + Date: Fri, 20 Dec 2024 15:34:10 -0500 Subject: [PATCH 24/30] Pass tests again using new `ExportableModules` helper. --- src/StarterKits/Exporter.php | 87 +++++++----------------------------- src/StarterKits/Module.php | 3 +- 2 files changed, 19 insertions(+), 71 deletions(-) diff --git a/src/StarterKits/Exporter.php b/src/StarterKits/Exporter.php index fba6951d588..53258630cd9 100644 --- a/src/StarterKits/Exporter.php +++ b/src/StarterKits/Exporter.php @@ -50,8 +50,7 @@ public function export(): void ->validateConfig() ->instantiateModules() ->clearExportPath() - ->exportInlineModules() - ->exportModulesFolder() + ->exportModules() ->exportPackage(); } @@ -84,65 +83,21 @@ protected function validateConfig(): self } /** - * Instantiate and validate modules that are to be installed. + * Instantiate and prepare flattened modules that are to be exported. */ protected function instantiateModules(): self { - $this->modules = collect(['top_level' => $this->config()->all()]) - ->map(fn ($config, $key) => $this->instantiateModuleRecursively($config, $key)) - ->flatten() - ->filter() + ray('instantiating')->purple(); + $this->modules = (new ExportableModules($this->config(), $this->exportPath)) + ->instantiate() + ->all() + ->pipe(fn ($module) => ExportableModules::flattenModules($module)) ->each(fn ($module) => $module->validate()); + ray('instantiated')->purple(); - return $this; - } - - /** - * Instantiate module and check if nested modules should be recursively instantiated. - */ - protected function instantiateModuleRecursively(array $config, string $key): ExportableModule|array - { - $instantiated = new ExportableModule($config, $key); - - if ($modules = Arr::get($config, 'modules')) { - $instantiated = collect($modules) - ->map(fn ($config, $childKey) => $this->instantiateModule($config, $this->normalizeModuleKey($key, $childKey))) - ->prepend($instantiated, $key) - ->filter() - ->all(); - } - - return $instantiated; - } + ray($this->modules)->purple(); - /** - * Instantiate individual module. - */ - protected function instantiateModule(array $config, string $key): ExportableModule|array - { - if (Arr::has($config, 'options') && $key !== 'top_level') { - return $this->instantiateSelectModule($config, $key); - } - - return $this->instantiateModuleRecursively($config, $key); - } - - /** - * Instantiate select module. - */ - protected function instantiateSelectModule(array $config, string $key): ExportableModule|array - { - return collect($config['options']) - ->map(fn ($option, $optionKey) => $this->instantiateModuleRecursively($option, "{$key}.options.{$optionKey}")) - ->all(); - } - - /** - * Normalize module key, as dotted array key for location in starter-kit.yaml. - */ - protected function normalizeModuleKey(string $key, string $childKey): string - { - return $key !== 'top_level' ? "{$key}.modules.{$childKey}" : $childKey; + return $this; } /** @@ -162,21 +117,9 @@ protected function clearExportPath() /** * Export all inline modules. */ - protected function exportInlineModules(): self - { - $exportPath = $this->exportPath.'/export'; - - $this->modules->each(fn ($module) => $module->export($exportPath)); - - return $this; - } - - /** - * Export modules folder. - */ - protected function exportModulesFolder(): self + protected function exportModules(): self { - $this->files->copyDirectory(base_path('modules'), "{$this->exportPath}/modules"); + $this->modules->each(fn ($module) => $module->export($this->exportPath.'/export')); return $this; } @@ -213,6 +156,10 @@ protected function syncConfigWithModules(): Collection $config = $this->config()->all(); $normalizedModuleKeyOrder = [ + 'prompt', + 'label', + 'skip_option', + 'options', 'export_paths', 'export_as', 'dependencies', @@ -250,7 +197,7 @@ protected function dottedModulePath(ExportableModule $module, string $key): stri return $key; } - return 'modules.'.$module->key().'.'.$key; + return $module->key().'.'.$key; } /** diff --git a/src/StarterKits/Module.php b/src/StarterKits/Module.php index 000eb91faad..9e7b806cb49 100644 --- a/src/StarterKits/Module.php +++ b/src/StarterKits/Module.php @@ -108,12 +108,13 @@ protected function exportAsPaths(): Collection * * @throws StarterKitException */ - protected function ensureModuleConfigNotEmpty(): self + protected function ensureModuleConfigNotEmpty(): static { $hasConfig = $this->config()->has('export_paths') || $this->config()->has('export_as') || $this->config()->has('dependencies') || $this->config()->has('dependencies_dev') + || $this->config()->has('options') || $this->config()->has('modules'); if (! $hasConfig) { From 3bcd7c9b1be3e9096af5f5d4ac4ff714c6dc05eb Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Fri, 20 Dec 2024 15:35:12 -0500 Subject: [PATCH 25/30] Remove rogue `ray()` calls. --- src/StarterKits/Exporter.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/StarterKits/Exporter.php b/src/StarterKits/Exporter.php index 53258630cd9..b5447251a66 100644 --- a/src/StarterKits/Exporter.php +++ b/src/StarterKits/Exporter.php @@ -87,15 +87,11 @@ protected function validateConfig(): self */ protected function instantiateModules(): self { - ray('instantiating')->purple(); $this->modules = (new ExportableModules($this->config(), $this->exportPath)) ->instantiate() ->all() ->pipe(fn ($module) => ExportableModules::flattenModules($module)) ->each(fn ($module) => $module->validate()); - ray('instantiated')->purple(); - - ray($this->modules)->purple(); return $this; } From af377aee39dfc5eefccbe5e6228631822f07cd52 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Fri, 20 Dec 2024 16:42:56 -0500 Subject: [PATCH 26/30] Pass test around double `/modules/modules` reference. --- src/StarterKits/Modules.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StarterKits/Modules.php b/src/StarterKits/Modules.php index a03ce8ce858..e4dc81eb693 100644 --- a/src/StarterKits/Modules.php +++ b/src/StarterKits/Modules.php @@ -174,7 +174,7 @@ protected function basePath(?string $path = null): string */ protected function relativeModulePath(string $key, ?string $path = null): string { - $base = 'modules/'.str_replace('.', '/', $key); + $base = Str::ensureLeft(str_replace('.', '/', $key), 'modules/'); return $path ? $base.Str::ensureLeft($path, '/') From 8c93901ea4978eb4bad2fd273ccd302f707fd22a Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Sat, 21 Dec 2024 19:33:56 -0500 Subject: [PATCH 27/30] Use `export` folder by default, and test non-export folder for backwards compatibility. --- tests/StarterKits/InstallTest.php | 64 ++++++++++++++++--- .../{ => export}/README-for-new-site.md | 0 .../{ => export}/config/filesystems.php | 0 .../{ => export}/content/collections/.gitkeep | 0 .../content/collections/pages.yaml | 0 .../content/collections/pages/home.md | 0 .../cool-runnings/{ => export}/copied.md | 0 .../cool-runnings/{ => export}/not-copied.md | 0 .../{ => export}/renamed-dir/one.txt | 0 .../{ => export}/renamed-dir/two.txt | 0 .../resources/blueprints/.gitkeep | 0 .../resources/blueprints/bobsled.yaml | 0 .../{ => export}/resources/css/bobsled.css | 0 .../{ => export}/resources/css/hockey.css | 0 .../{ => export}/resources/css/jamaica.css | 0 .../{ => export}/resources/css/seo.css | 0 .../dictionaries/american_players.yaml | 0 .../dictionaries/canadian_players.yaml | 0 .../resources/dictionaries/players.yaml | 0 .../{ => export}/resources/js/jquery.js | 0 .../{ => export}/resources/js/mootools.js | 0 .../resources/js/react-testing-tools.js | 0 .../{ => export}/resources/js/react.js | 0 .../{ => export}/resources/js/svelte.js | 0 .../resources/js/vue-testing-tools.js | 0 .../{ => export}/resources/js/vue.js | 0 26 files changed, 56 insertions(+), 8 deletions(-) rename tests/StarterKits/__fixtures__/cool-runnings/{ => export}/README-for-new-site.md (100%) rename tests/StarterKits/__fixtures__/cool-runnings/{ => export}/config/filesystems.php (100%) rename tests/StarterKits/__fixtures__/cool-runnings/{ => export}/content/collections/.gitkeep (100%) rename tests/StarterKits/__fixtures__/cool-runnings/{ => export}/content/collections/pages.yaml (100%) rename tests/StarterKits/__fixtures__/cool-runnings/{ => export}/content/collections/pages/home.md (100%) rename tests/StarterKits/__fixtures__/cool-runnings/{ => export}/copied.md (100%) rename tests/StarterKits/__fixtures__/cool-runnings/{ => export}/not-copied.md (100%) rename tests/StarterKits/__fixtures__/cool-runnings/{ => export}/renamed-dir/one.txt (100%) rename tests/StarterKits/__fixtures__/cool-runnings/{ => export}/renamed-dir/two.txt (100%) rename tests/StarterKits/__fixtures__/cool-runnings/{ => export}/resources/blueprints/.gitkeep (100%) rename tests/StarterKits/__fixtures__/cool-runnings/{ => export}/resources/blueprints/bobsled.yaml (100%) rename tests/StarterKits/__fixtures__/cool-runnings/{ => export}/resources/css/bobsled.css (100%) rename tests/StarterKits/__fixtures__/cool-runnings/{ => export}/resources/css/hockey.css (100%) rename tests/StarterKits/__fixtures__/cool-runnings/{ => export}/resources/css/jamaica.css (100%) rename tests/StarterKits/__fixtures__/cool-runnings/{ => export}/resources/css/seo.css (100%) rename tests/StarterKits/__fixtures__/cool-runnings/{ => export}/resources/dictionaries/american_players.yaml (100%) rename tests/StarterKits/__fixtures__/cool-runnings/{ => export}/resources/dictionaries/canadian_players.yaml (100%) rename tests/StarterKits/__fixtures__/cool-runnings/{ => export}/resources/dictionaries/players.yaml (100%) rename tests/StarterKits/__fixtures__/cool-runnings/{ => export}/resources/js/jquery.js (100%) rename tests/StarterKits/__fixtures__/cool-runnings/{ => export}/resources/js/mootools.js (100%) rename tests/StarterKits/__fixtures__/cool-runnings/{ => export}/resources/js/react-testing-tools.js (100%) rename tests/StarterKits/__fixtures__/cool-runnings/{ => export}/resources/js/react.js (100%) rename tests/StarterKits/__fixtures__/cool-runnings/{ => export}/resources/js/svelte.js (100%) rename tests/StarterKits/__fixtures__/cool-runnings/{ => export}/resources/js/vue-testing-tools.js (100%) rename tests/StarterKits/__fixtures__/cool-runnings/{ => export}/resources/js/vue.js (100%) diff --git a/tests/StarterKits/InstallTest.php b/tests/StarterKits/InstallTest.php index 62fc168481a..db78bbd2123 100644 --- a/tests/StarterKits/InstallTest.php +++ b/tests/StarterKits/InstallTest.php @@ -55,6 +55,8 @@ public function it_installs_starter_kit() { $this->assertFileDoesNotExist($this->kitVendorPath()); $this->assertComposerJsonDoesntHave('repositories'); + $this->assertFileExists($this->kitRepoPath('export/copied.md')); + $this->assertFileDoesNotExist($this->kitRepoPath('copied.md')); $this->assertFileDoesNotExist(base_path('copied.md')); $this->installCoolRunnings(); @@ -68,18 +70,20 @@ public function it_installs_starter_kit() } #[Test] - public function it_installs_starter_kit_from_updatable_package_with_export_directory() + public function it_installs_export_paths_from_root_of_starter_kit_for_backwards_compatibility() { - // Move everything in the kit repo's `export` folder, except for `composer.json` and `starter-kit.yaml` - collect($this->files->allFiles($this->kitRepoPath())) - ->reject(fn ($file) => in_array($file->getRelativePathname(), ['composer.json', 'starter-kit.yaml'])) + // Move files from `export` folder to root of starter kit, like old times + collect($this->files->allFiles($this->kitRepoPath('export'))) ->each(fn ($file) => $this->files->move( - $this->kitRepoPath($file->getRelativePathname()), - $this->preparePath($this->kitRepoPath('export/'.$file->getRelativePathname())), + $this->kitRepoPath('export/'.$file->getRelativePathname()), + $this->preparePath($this->kitRepoPath($file->getRelativePathname())), )); - $this->assertFileDoesNotExist($this->kitRepoPath('copied.md')); - $this->assertFileExists($this->kitRepoPath('export/copied.md')); + // Make sure `export` folder does not exist at all + $this->files->deleteDirectory($this->kitRepoPath('export')); + + $this->assertFileDoesNotExist($this->kitRepoPath('export/copied.md')); + $this->assertFileExists($this->kitRepoPath('copied.md')); $this->assertFileExists($this->kitRepoPath('composer.json')); $this->assertFileExists($this->kitRepoPath('starter-kit.yaml')); @@ -149,6 +153,50 @@ public function it_still_installs_from_export_as_paths_for_backwards_compatibili $this->assertFileHasContent('Two.', $renamedFolder.'/two.txt'); } + #[Test] + public function it_still_installs_from_export_as_paths_from_root_of_starter_kit_for_backwards_compatibility() + { + // Move files from `export` folder to root of starter kit, like old times + collect($this->files->allFiles($this->kitRepoPath('export'))) + ->each(fn ($file) => $this->files->move( + $this->kitRepoPath('export/'.$file->getRelativePathname()), + $this->preparePath($this->kitRepoPath($file->getRelativePathname())), + )); + + // Make sure `export` folder does not exist at all + $this->files->deleteDirectory($this->kitRepoPath('export')); + + $this->setConfig([ + 'export_as' => [ + 'README.md' => 'README-for-new-site.md', + 'original-dir' => 'renamed-dir', + ], + ]); + + $this->assertFileDoesNotExist($this->kitVendorPath()); + $this->assertComposerJsonDoesntHave('repositories'); + $this->assertFileDoesNotExist($this->kitRepoPath('export/README-for-new-site.md')); + $this->assertFileExists($this->kitRepoPath('README-for-new-site.md')); + $this->assertFileDoesNotExist($renamedFile = base_path('README.md')); + $this->assertFileDoesNotExist($renamedFolder = base_path('original-dir')); + + $this->installCoolRunnings(); + + $this->assertFalse(Blink::has('starter-kit-repository-added')); + $this->assertFileDoesNotExist($this->kitVendorPath()); + $this->assertFileDoesNotExist(base_path('composer.json.bak')); + $this->assertComposerJsonDoesntHave('repositories'); + $this->assertFileExists($renamedFile); + $this->assertFileExists($renamedFolder); + + $this->assertFileDoesNotExist(base_path('README-for-new-site.md')); // This was renamed back to original path on install + $this->assertFileDoesNotExist(base_path('renamed-dir')); // This was renamed back to original path on install + + $this->assertFileHasContent('This readme should get installed to README.md.', $renamedFile); + $this->assertFileHasContent('One.', $renamedFolder.'/one.txt'); + $this->assertFileHasContent('Two.', $renamedFolder.'/two.txt'); + } + #[Test] public function it_installs_from_github() { diff --git a/tests/StarterKits/__fixtures__/cool-runnings/README-for-new-site.md b/tests/StarterKits/__fixtures__/cool-runnings/export/README-for-new-site.md similarity index 100% rename from tests/StarterKits/__fixtures__/cool-runnings/README-for-new-site.md rename to tests/StarterKits/__fixtures__/cool-runnings/export/README-for-new-site.md diff --git a/tests/StarterKits/__fixtures__/cool-runnings/config/filesystems.php b/tests/StarterKits/__fixtures__/cool-runnings/export/config/filesystems.php similarity index 100% rename from tests/StarterKits/__fixtures__/cool-runnings/config/filesystems.php rename to tests/StarterKits/__fixtures__/cool-runnings/export/config/filesystems.php diff --git a/tests/StarterKits/__fixtures__/cool-runnings/content/collections/.gitkeep b/tests/StarterKits/__fixtures__/cool-runnings/export/content/collections/.gitkeep similarity index 100% rename from tests/StarterKits/__fixtures__/cool-runnings/content/collections/.gitkeep rename to tests/StarterKits/__fixtures__/cool-runnings/export/content/collections/.gitkeep diff --git a/tests/StarterKits/__fixtures__/cool-runnings/content/collections/pages.yaml b/tests/StarterKits/__fixtures__/cool-runnings/export/content/collections/pages.yaml similarity index 100% rename from tests/StarterKits/__fixtures__/cool-runnings/content/collections/pages.yaml rename to tests/StarterKits/__fixtures__/cool-runnings/export/content/collections/pages.yaml diff --git a/tests/StarterKits/__fixtures__/cool-runnings/content/collections/pages/home.md b/tests/StarterKits/__fixtures__/cool-runnings/export/content/collections/pages/home.md similarity index 100% rename from tests/StarterKits/__fixtures__/cool-runnings/content/collections/pages/home.md rename to tests/StarterKits/__fixtures__/cool-runnings/export/content/collections/pages/home.md diff --git a/tests/StarterKits/__fixtures__/cool-runnings/copied.md b/tests/StarterKits/__fixtures__/cool-runnings/export/copied.md similarity index 100% rename from tests/StarterKits/__fixtures__/cool-runnings/copied.md rename to tests/StarterKits/__fixtures__/cool-runnings/export/copied.md diff --git a/tests/StarterKits/__fixtures__/cool-runnings/not-copied.md b/tests/StarterKits/__fixtures__/cool-runnings/export/not-copied.md similarity index 100% rename from tests/StarterKits/__fixtures__/cool-runnings/not-copied.md rename to tests/StarterKits/__fixtures__/cool-runnings/export/not-copied.md diff --git a/tests/StarterKits/__fixtures__/cool-runnings/renamed-dir/one.txt b/tests/StarterKits/__fixtures__/cool-runnings/export/renamed-dir/one.txt similarity index 100% rename from tests/StarterKits/__fixtures__/cool-runnings/renamed-dir/one.txt rename to tests/StarterKits/__fixtures__/cool-runnings/export/renamed-dir/one.txt diff --git a/tests/StarterKits/__fixtures__/cool-runnings/renamed-dir/two.txt b/tests/StarterKits/__fixtures__/cool-runnings/export/renamed-dir/two.txt similarity index 100% rename from tests/StarterKits/__fixtures__/cool-runnings/renamed-dir/two.txt rename to tests/StarterKits/__fixtures__/cool-runnings/export/renamed-dir/two.txt diff --git a/tests/StarterKits/__fixtures__/cool-runnings/resources/blueprints/.gitkeep b/tests/StarterKits/__fixtures__/cool-runnings/export/resources/blueprints/.gitkeep similarity index 100% rename from tests/StarterKits/__fixtures__/cool-runnings/resources/blueprints/.gitkeep rename to tests/StarterKits/__fixtures__/cool-runnings/export/resources/blueprints/.gitkeep diff --git a/tests/StarterKits/__fixtures__/cool-runnings/resources/blueprints/bobsled.yaml b/tests/StarterKits/__fixtures__/cool-runnings/export/resources/blueprints/bobsled.yaml similarity index 100% rename from tests/StarterKits/__fixtures__/cool-runnings/resources/blueprints/bobsled.yaml rename to tests/StarterKits/__fixtures__/cool-runnings/export/resources/blueprints/bobsled.yaml diff --git a/tests/StarterKits/__fixtures__/cool-runnings/resources/css/bobsled.css b/tests/StarterKits/__fixtures__/cool-runnings/export/resources/css/bobsled.css similarity index 100% rename from tests/StarterKits/__fixtures__/cool-runnings/resources/css/bobsled.css rename to tests/StarterKits/__fixtures__/cool-runnings/export/resources/css/bobsled.css diff --git a/tests/StarterKits/__fixtures__/cool-runnings/resources/css/hockey.css b/tests/StarterKits/__fixtures__/cool-runnings/export/resources/css/hockey.css similarity index 100% rename from tests/StarterKits/__fixtures__/cool-runnings/resources/css/hockey.css rename to tests/StarterKits/__fixtures__/cool-runnings/export/resources/css/hockey.css diff --git a/tests/StarterKits/__fixtures__/cool-runnings/resources/css/jamaica.css b/tests/StarterKits/__fixtures__/cool-runnings/export/resources/css/jamaica.css similarity index 100% rename from tests/StarterKits/__fixtures__/cool-runnings/resources/css/jamaica.css rename to tests/StarterKits/__fixtures__/cool-runnings/export/resources/css/jamaica.css diff --git a/tests/StarterKits/__fixtures__/cool-runnings/resources/css/seo.css b/tests/StarterKits/__fixtures__/cool-runnings/export/resources/css/seo.css similarity index 100% rename from tests/StarterKits/__fixtures__/cool-runnings/resources/css/seo.css rename to tests/StarterKits/__fixtures__/cool-runnings/export/resources/css/seo.css diff --git a/tests/StarterKits/__fixtures__/cool-runnings/resources/dictionaries/american_players.yaml b/tests/StarterKits/__fixtures__/cool-runnings/export/resources/dictionaries/american_players.yaml similarity index 100% rename from tests/StarterKits/__fixtures__/cool-runnings/resources/dictionaries/american_players.yaml rename to tests/StarterKits/__fixtures__/cool-runnings/export/resources/dictionaries/american_players.yaml diff --git a/tests/StarterKits/__fixtures__/cool-runnings/resources/dictionaries/canadian_players.yaml b/tests/StarterKits/__fixtures__/cool-runnings/export/resources/dictionaries/canadian_players.yaml similarity index 100% rename from tests/StarterKits/__fixtures__/cool-runnings/resources/dictionaries/canadian_players.yaml rename to tests/StarterKits/__fixtures__/cool-runnings/export/resources/dictionaries/canadian_players.yaml diff --git a/tests/StarterKits/__fixtures__/cool-runnings/resources/dictionaries/players.yaml b/tests/StarterKits/__fixtures__/cool-runnings/export/resources/dictionaries/players.yaml similarity index 100% rename from tests/StarterKits/__fixtures__/cool-runnings/resources/dictionaries/players.yaml rename to tests/StarterKits/__fixtures__/cool-runnings/export/resources/dictionaries/players.yaml diff --git a/tests/StarterKits/__fixtures__/cool-runnings/resources/js/jquery.js b/tests/StarterKits/__fixtures__/cool-runnings/export/resources/js/jquery.js similarity index 100% rename from tests/StarterKits/__fixtures__/cool-runnings/resources/js/jquery.js rename to tests/StarterKits/__fixtures__/cool-runnings/export/resources/js/jquery.js diff --git a/tests/StarterKits/__fixtures__/cool-runnings/resources/js/mootools.js b/tests/StarterKits/__fixtures__/cool-runnings/export/resources/js/mootools.js similarity index 100% rename from tests/StarterKits/__fixtures__/cool-runnings/resources/js/mootools.js rename to tests/StarterKits/__fixtures__/cool-runnings/export/resources/js/mootools.js diff --git a/tests/StarterKits/__fixtures__/cool-runnings/resources/js/react-testing-tools.js b/tests/StarterKits/__fixtures__/cool-runnings/export/resources/js/react-testing-tools.js similarity index 100% rename from tests/StarterKits/__fixtures__/cool-runnings/resources/js/react-testing-tools.js rename to tests/StarterKits/__fixtures__/cool-runnings/export/resources/js/react-testing-tools.js diff --git a/tests/StarterKits/__fixtures__/cool-runnings/resources/js/react.js b/tests/StarterKits/__fixtures__/cool-runnings/export/resources/js/react.js similarity index 100% rename from tests/StarterKits/__fixtures__/cool-runnings/resources/js/react.js rename to tests/StarterKits/__fixtures__/cool-runnings/export/resources/js/react.js diff --git a/tests/StarterKits/__fixtures__/cool-runnings/resources/js/svelte.js b/tests/StarterKits/__fixtures__/cool-runnings/export/resources/js/svelte.js similarity index 100% rename from tests/StarterKits/__fixtures__/cool-runnings/resources/js/svelte.js rename to tests/StarterKits/__fixtures__/cool-runnings/export/resources/js/svelte.js diff --git a/tests/StarterKits/__fixtures__/cool-runnings/resources/js/vue-testing-tools.js b/tests/StarterKits/__fixtures__/cool-runnings/export/resources/js/vue-testing-tools.js similarity index 100% rename from tests/StarterKits/__fixtures__/cool-runnings/resources/js/vue-testing-tools.js rename to tests/StarterKits/__fixtures__/cool-runnings/export/resources/js/vue-testing-tools.js diff --git a/tests/StarterKits/__fixtures__/cool-runnings/resources/js/vue.js b/tests/StarterKits/__fixtures__/cool-runnings/export/resources/js/vue.js similarity index 100% rename from tests/StarterKits/__fixtures__/cool-runnings/resources/js/vue.js rename to tests/StarterKits/__fixtures__/cool-runnings/export/resources/js/vue.js From f2413e15c6949ae9756bcdb618e4f62789b09336 Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Mon, 23 Dec 2024 20:14:04 -0500 Subject: [PATCH 28/30] Tweak install test for newer `modules` + `export` folder conventions. --- tests/StarterKits/InstallTest.php | 34 +++++++++++++++---------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/StarterKits/InstallTest.php b/tests/StarterKits/InstallTest.php index db78bbd2123..55e331c6de6 100644 --- a/tests/StarterKits/InstallTest.php +++ b/tests/StarterKits/InstallTest.php @@ -1729,7 +1729,7 @@ public function it_installs_imported_modules_confirmed_interactively_via_prompt( path: 'modules/seo/module.yaml', config: [ 'export_paths' => [ - $this->moveKitRepoFile('modules/seo', 'resources/css/seo.css'), + $this->moveKitExportToModuleFolder('seo', 'resources/css/seo.css'), ], 'dependencies' => [ 'statamic/seo-pro' => '^0.2.0', @@ -1743,13 +1743,13 @@ public function it_installs_imported_modules_confirmed_interactively_via_prompt( 'options' => [ 'react' => [ 'export_paths' => [ - $this->moveKitRepoFile('modules/js', 'resources/js/react.js'), + $this->moveKitExportToModuleFolder('js', 'resources/js/react.js'), ], ], 'vue' => '@import', // import option as separate module! 'svelte' => [ 'export_paths' => [ - $this->moveKitRepoFile('modules/js', 'resources/js/svelte.js'), + $this->moveKitExportToModuleFolder('js', 'resources/js/svelte.js'), ], ], ], @@ -1760,7 +1760,7 @@ public function it_installs_imported_modules_confirmed_interactively_via_prompt( path: 'modules/js/vue/module.yaml', config: [ 'export_paths' => [ - $this->moveKitRepoFile('modules/js/vue', 'resources/js/vue.js'), + $this->moveKitExportToModuleFolder('js/vue', 'resources/js/vue.js'), ], 'dependencies' => [ 'bobsled/vue-components' => '^1.5', @@ -1844,7 +1844,7 @@ public function it_can_merge_imported_module_config_with_starter_kit_config() config: [ 'label' => 'This should not get used, because prompt config in starter-kit.yaml takes precedence!', 'export_paths' => [ - $this->moveKitRepoFile('modules/js/react', 'resources/js/react.js'), // but this should still be imported and installed + $this->moveKitExportToModuleFolder('js/react', 'resources/js/react.js'), // but this should still be imported and installed ], ], ); @@ -1991,7 +1991,7 @@ public function it_installs_nested_imported_modules_confirmed_interactively_via_ path: 'modules/seo/module.yaml', config: [ 'export_paths' => [ - $this->moveKitRepoFile('modules/seo', 'resources/css/seo.css'), + $this->moveKitExportToModuleFolder('seo', 'resources/css/seo.css'), ], 'dependencies' => [ 'statamic/seo-pro' => '^0.2.0', @@ -2001,19 +2001,19 @@ public function it_installs_nested_imported_modules_confirmed_interactively_via_ 'options' => [ 'react' => [ 'export_paths' => [ - $this->moveKitRepoFile('modules/seo', 'resources/js/react.js'), + $this->moveKitExportToModuleFolder('seo', 'resources/js/react.js'), ], 'modules' => [ 'testing_tools' => [ 'export_paths' => [ - $this->moveKitRepoFile('modules/seo', 'resources/js/react-testing-tools.js'), + $this->moveKitExportToModuleFolder('seo', 'resources/js/react-testing-tools.js'), ], ], ], ], 'vue' => [ 'export_paths' => [ - $this->moveKitRepoFile('modules/seo', 'resources/js/vue.js'), + $this->moveKitExportToModuleFolder('seo', 'resources/js/vue.js'), ], 'dependencies_dev' => [ 'i-love-vue/test-helpers' => '^1.5', @@ -2024,7 +2024,7 @@ public function it_installs_nested_imported_modules_confirmed_interactively_via_ ], 'svelte' => [ 'export_paths' => [ - $this->moveKitRepoFile('modules/seo', 'resources/js/svelte.js'), + $this->moveKitExportToModuleFolder('seo', 'resources/js/svelte.js'), ], ], ], @@ -2033,12 +2033,12 @@ public function it_installs_nested_imported_modules_confirmed_interactively_via_ 'options' => [ 'jquery' => [ 'export_paths' => [ - $this->moveKitRepoFile('modules/seo', 'resources/js/jquery.js'), + $this->moveKitExportToModuleFolder('seo', 'resources/js/jquery.js'), ], ], 'mootools' => [ 'export_paths' => [ - $this->moveKitRepoFile('modules/seo', 'resources/js/mootools.js'), + $this->moveKitExportToModuleFolder('seo', 'resources/js/mootools.js'), ], ], ], @@ -2051,7 +2051,7 @@ public function it_installs_nested_imported_modules_confirmed_interactively_via_ path: 'modules/seo/js/vue/testing_tools/module.yaml', config: [ 'export_paths' => [ - $this->moveKitRepoFile('modules/seo/js/vue/testing_tools', 'resources/js/vue-testing-tools.js'), + $this->moveKitExportToModuleFolder('seo/js/vue/testing_tools', 'resources/js/vue-testing-tools.js'), ], ], ); @@ -2060,7 +2060,7 @@ public function it_installs_nested_imported_modules_confirmed_interactively_via_ path: 'modules/jamaica/bobsled/module.yaml', config: [ 'export_paths' => [ - $this->moveKitRepoFile('modules/jamaica/bobsled', 'resources/css/bobsled.css'), + $this->moveKitExportToModuleFolder('jamaica/bobsled', 'resources/css/bobsled.css'), ], 'dependencies' => [ 'bobsled/speed-calculator' => '^1.0.0', @@ -2133,11 +2133,11 @@ private function setConfig($config, $path = 'starter-kit.yaml') $this->files->put($this->preparePath($this->kitRepoPath($path)), YAML::dump($config)); } - private function moveKitRepoFile($relativeModulePath, $relativeFilePath) + private function moveKitExportToModuleFolder($relativeModuleFolderPath, $relativeFilePath) { $this->files->move( - $this->kitRepoPath($relativeFilePath), - $this->preparePath($this->kitRepoPath(Str::ensureRight($relativeModulePath, '/').$relativeFilePath)), + $this->kitRepoPath('export/'.$relativeFilePath), + $this->preparePath($this->kitRepoPath('modules/'.$relativeModuleFolderPath.'/export/'.$relativeFilePath)), ); return $relativeFilePath; From 62bd6a96e3db96dee8b833d311b8180150e9e85f Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Mon, 23 Dec 2024 20:14:51 -0500 Subject: [PATCH 29/30] Pass tests again. --- src/StarterKits/InstallableModule.php | 14 +++++++++----- src/StarterKits/Module.php | 8 ++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/StarterKits/InstallableModule.php b/src/StarterKits/InstallableModule.php index cad88cc5d3c..863110a14f5 100644 --- a/src/StarterKits/InstallableModule.php +++ b/src/StarterKits/InstallableModule.php @@ -153,7 +153,10 @@ protected function convertInstallableToDestinationPath(string $path): string { $package = $this->installer->package(); - $path = str_replace("/vendor/{$package}/export", '', $path); + $path = preg_replace("#vendor/{$package}.*/export/#", '', $path); + + // Older kits may not be using new `export` folder convention, so + // we'll convert from the kit root for backwards compatibility $path = str_replace("/vendor/{$package}", '', $path); return $path; @@ -247,9 +250,10 @@ protected function installableFilesPath(?string $path = null): string { $package = $this->installer->package(); - // Scope to new `export` folder if it exists, otherwise we'll - // look in starter kit root for backwards compatibility - $scope = $this->files->exists(base_path("vendor/{$package}/export")) + // Older kits may not be using new `export` folder convention at the top level, + // so for backwards compatibility we'll dynamically scope to `export` folder, + // but we don't need to worry about this with newer folder based modules. + $scope = $this->files->exists(base_path("vendor/{$package}/export")) && ! $this->isFolderBasedModule() ? 'export' : null; @@ -265,7 +269,7 @@ protected function relativePath(string $path): string return $path; } - return Str::ensureRight($this->relativePath, '/').$path; + return Str::ensureRight($this->relativePath, '/export/').$path; } /** diff --git a/src/StarterKits/Module.php b/src/StarterKits/Module.php index 9e7b806cb49..89223a13303 100644 --- a/src/StarterKits/Module.php +++ b/src/StarterKits/Module.php @@ -37,6 +37,14 @@ public function setRelativePath(string $path): self return $this; } + /** + * Check if current module is folder based module. + */ + public function isFolderBasedModule(): bool + { + return (bool) $this->relativePath; + } + /** * Get module key. */ From f5768045ddc7825d94fdb066115ece55b663ebbe Mon Sep 17 00:00:00 2001 From: Jesse Leite Date: Mon, 6 Jan 2025 16:10:04 -0500 Subject: [PATCH 30/30] =?UTF-8?q?Just=20export=20what=20ExportableModule?= =?UTF-8?q?=20doesn=E2=80=99t=20handle.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Concerns/InteractsWithFilesystem.php | 12 ---------- src/StarterKits/Exporter.php | 23 +++++++++++++++---- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/StarterKits/Concerns/InteractsWithFilesystem.php b/src/StarterKits/Concerns/InteractsWithFilesystem.php index d119a4ec4a7..7f39827d684 100644 --- a/src/StarterKits/Concerns/InteractsWithFilesystem.php +++ b/src/StarterKits/Concerns/InteractsWithFilesystem.php @@ -43,18 +43,6 @@ protected function exportRelativePath(string $starterKitPath, string $from, ?str : $files->copy($from, $to); } - /** - * Copy directory contents into, file by file so that it does not stomp the whole target directory. - */ - protected function copyDirectoryContentsInto(string $from, string $to): void - { - $files = app(Filesystem::class); - - collect($files->allFiles($from)) - ->mapWithKeys(fn ($file) => [$from.'/'.$file->getRelativePathname() => $to.'/'.$file->getRelativePathname()]) - ->each(fn ($to, $from) => $files->copy(Path::tidy($from), $this->preparePath($to))); - } - /** * Prepare path directory. */ diff --git a/src/StarterKits/Exporter.php b/src/StarterKits/Exporter.php index b5447251a66..65a6e138294 100644 --- a/src/StarterKits/Exporter.php +++ b/src/StarterKits/Exporter.php @@ -51,7 +51,8 @@ public function export(): void ->instantiateModules() ->clearExportPath() ->exportModules() - ->exportPackage(); + ->exportConfig() + ->exportPostInstallHook(); } /** @@ -197,11 +198,11 @@ protected function dottedModulePath(ExportableModule $module, string $key): stri } /** - * Export package config & other misc vendor files. + * Export package config. */ - protected function exportPackage(): self + protected function exportConfig(): self { - $this->copyDirectoryContentsInto(base_path('package'), $this->exportPath); + $this->files->copy(base_path('package/composer.json'), "{$this->exportPath}/composer.json"); $config = $this ->versionModuleDependencies() @@ -211,4 +212,18 @@ protected function exportPackage(): self return $this; } + + /** + * Export top level post install hook, if one exists. + */ + protected function exportPostInstallHook(): self + { + if (! $this->files->exists(base_path('package/StarterKitPostInstall.php'))) { + return $this; + } + + $this->files->copy(base_path('package/StarterKitPostInstall.php'), "{$this->exportPath}/StarterKitPostInstall.php"); + + return $this; + } }