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/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 @@ +instantiateModules() ->clearExportPath() ->exportModules() - ->exportPackage(); + ->exportConfig() + ->exportPostInstallHook(); } /** @@ -83,67 +84,19 @@ 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() + $this->modules = (new ExportableModules($this->config(), $this->exportPath)) + ->instantiate() + ->all() + ->pipe(fn ($module) => ExportableModules::flattenModules($module)) ->each(fn ($module) => $module->validate()); 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; - } - - /** - * 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; - } - /** * Optionally clear out everything at target export path before exporting. */ @@ -159,13 +112,11 @@ protected function clearExportPath() } /** - * Export all the modules. + * Export all inline modules. */ protected function exportModules(): self { - $exportPath = $this->exportPath.'/export'; - - $this->modules->each(fn ($module) => $module->export($exportPath)); + $this->modules->each(fn ($module) => $module->export($this->exportPath.'/export')); return $this; } @@ -202,6 +153,10 @@ protected function syncConfigWithModules(): Collection $config = $this->config()->all(); $normalizedModuleKeyOrder = [ + 'prompt', + 'label', + 'skip_option', + 'options', 'export_paths', 'export_as', 'dependencies', @@ -239,15 +194,15 @@ protected function dottedModulePath(ExportableModule $module, string $key): stri return $key; } - return 'modules.'.$module->key().'.'.$key; + return $module->key().'.'.$key; } /** - * 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() @@ -257,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; + } } diff --git a/src/StarterKits/InstallableModule.php b/src/StarterKits/InstallableModule.php index c11923d76ba..863110a14f5 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; @@ -102,7 +102,7 @@ protected function installDependencies(): self /** * Get installable files. */ - protected function installableFiles(): Collection + public function installableFiles(): Collection { $installableFromExportPaths = $this ->exportPaths() @@ -125,8 +125,10 @@ protected function installableFiles(): Collection */ protected function expandExportDirectoriesToFiles(string $to, ?string $from = null): Collection { + $from = $this->relativePath($from ?? $to); + + $from = Path::tidy($this->installableFilesPath($from)); $to = Path::tidy($this->installableFilesPath($to)); - $from = Path::tidy($from ? $this->installableFilesPath($from) : $to); $paths = collect([$from => $to]); @@ -151,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; @@ -195,9 +200,8 @@ protected function installableDependencies(string $configKey): array protected function ensureInstallableFilesExist(): self { $this - ->exportPaths() - ->merge($this->exportAsPaths()) - ->reject(fn ($path) => $this->files->exists($this->installableFilesPath($path))) + ->installableFiles() + ->reject(fn ($to, $from) => $this->files->exists($from)) ->each(function ($path) { throw new StarterKitException("Starter kit path [{$path}] does not exist."); }); @@ -246,15 +250,28 @@ 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; return collect([base_path("vendor/{$package}"), $scope, $path])->filter()->implode('/'); } + /** + * Get relative module path. + */ + protected function relativePath(string $path): string + { + if (! $this->relativePath) { + return $path; + } + + return Str::ensureRight($this->relativePath, '/export/').$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 new file mode 100644 index 00000000000..5e198972af9 --- /dev/null +++ b/src/StarterKits/InstallableModules.php @@ -0,0 +1,44 @@ +installer = $installer; + + return $this; + } + + /** + * Instantiate individual InstallableModule. + */ + protected function instantiateIndividualModule(array|Collection $config, string $key): Module + { + return (new InstallableModule($config, $key))->installer($this->installer); + } + + /** + * Override so that we do not prefix option key for installable modules. + */ + protected function prefixOptionsKey(string $key): ?string + { + return $key; + } + + /** + * Override so that we do not prefix modules key for installable modules. + */ + protected function prefixModulesKey(string $key): ?string + { + return $key; + } +} diff --git a/src/StarterKits/Installer.php b/src/StarterKits/Installer.php index 8554fad327e..6eb714ceb57 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; @@ -153,6 +152,7 @@ public function install(): void ->requireStarterKit() ->ensureConfig() ->instantiateModules() + ->filterInstallableModules() ->installModules() ->copyStarterKitConfig() ->copyStarterKitHooks() @@ -277,87 +277,106 @@ 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() + $this->modules = (new InstallableModules($this->config(), $this->starterKitPath())) + ->installer($this) + ->instantiate(); + + return $this; + } + + /** + * Filter and prepare flattened collection of installable modules. + */ + protected function filterInstallableModules(): self + { + $this->modules = $this->modules->all() + ->map(fn ($module) => $this->prepareInstallableRecursively($module)) + ->pipe(fn ($module) => InstallableModules::flattenModules($module)) ->each(fn ($module) => $module->validate()); return $this; } /** - * Instantiate module and check if nested modules should be recursively instantiated. + * Recursively prepare module and its nested modules. */ - protected function instantiateModuleRecursively(array $config, string $key): InstallableModule|array + protected function prepareInstallableRecursively(InstallableModule $installable): InstallableModule|bool { - $instantiated = (new InstallableModule($config, $key))->installer($this); + $module = $this->prepareInstallableModule($installable); - 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(); + if ($module === false) { + return false; + } + + if ($modules = $module->config('modules')) { + $module->set( + key: 'modules', + value: $modules + ->map(fn ($childModule) => $this->prepareInstallableRecursively($childModule)) + ->filter() + ); } - return $instantiated; + return $module; } /** - * Instantiate individual module. + * Prepare individual module. */ - protected function instantiateModule(array $config, string $key): InstallableModule|array|bool + protected function prepareInstallableModule(InstallableModule $module): InstallableModule|bool { - $shouldPrompt = true; + if ($module->isTopLevelModule()) { + return $module; + } - if (Arr::has($config, 'options')) { - return $this->instantiateSelectModule($config, $key); + if ($module->config('options')) { + return $this->prepareSelectModule($module); } - if (Arr::get($config, 'prompt') === false) { + $shouldPrompt = true; + + if ($module->config('prompt') === false) { $shouldPrompt = false; } - $name = str_replace('_', ' ', $key); - - $default = Arr::get($config, 'default', false); + $name = $module->keyReadable(); + $default = $module->config('default', false); - if ($shouldPrompt && $this->isInteractive && ! confirm(Arr::get($config, 'prompt', "Would you like to install the [{$name}] module?"), $default)) { + if ($shouldPrompt && $this->isInteractive && ! confirm($module->config('prompt', "Would you like to install the [{$name}] module?"), $default)) { return false; } elseif ($shouldPrompt && ! $this->isInteractive && ! $default) { 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 { - $skipOptionLabel = Arr::get($config, 'skip_option', 'No'); + $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(); - $name = str_replace('_', ' ', $key); + $name = $module->keyReadable(); 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; } @@ -365,18 +384,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]; } /** diff --git a/src/StarterKits/Module.php b/src/StarterKits/Module.php index 3959b34fb2e..89223a13303 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,24 @@ 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; + } + + /** + * Check if current module is folder based module. + */ + public function isFolderBasedModule(): bool + { + return (bool) $this->relativePath; + } + /** * Get module key. */ @@ -34,6 +53,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. */ @@ -42,13 +69,23 @@ 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. */ - 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; @@ -79,12 +116,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) { diff --git a/src/StarterKits/Modules.php b/src/StarterKits/Modules.php new file mode 100644 index 00000000000..e4dc81eb693 --- /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 = Str::ensureLeft(str_replace('.', '/', $key), 'modules/'); + + 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 6fc18b8aa11..55e331c6de6 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() { @@ -1195,7 +1243,7 @@ public function it_can_disable_skip_option_in_select_module_prompts() } #[Test] - public function it_display_custom_module_prompts_and_option_labels() + public function it_displays_custom_module_prompts_and_option_labels() { $this->setConfig([ 'modules' => [ @@ -1637,6 +1685,434 @@ public function it_installs_nested_modules_confirmed_interactively_via_prompt() $this->assertComposerJsonHasPackageVersion('require', 'bobsled/speed-calculator', '^1.0.0'); } + #[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' => [ + $this->moveKitExportToModuleFolder('seo', 'resources/css/seo.css'), + ], + 'dependencies' => [ + 'statamic/seo-pro' => '^0.2.0', + ], + ], + ); + + $this->setConfig( + path: 'modules/js/module.yaml', + config: [ + 'options' => [ + 'react' => [ + 'export_paths' => [ + $this->moveKitExportToModuleFolder('js', 'resources/js/react.js'), + ], + ], + 'vue' => '@import', // import option as separate module! + 'svelte' => [ + 'export_paths' => [ + $this->moveKitExportToModuleFolder('js', 'resources/js/svelte.js'), + ], + ], + ], + ], + ); + + $this->setConfig( + path: 'modules/js/vue/module.yaml', + config: [ + 'export_paths' => [ + $this->moveKitExportToModuleFolder('js/vue', '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_can_merge_imported_module_config_with_starter_kit_config() + { + $this->setConfig([ + 'modules' => [ + 'seo' => [ + 'prompt' => 'Want some extra SEO magic?', // handle prompt flow here + // 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 + // implicitly import and merge rest of config in here + ], + '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' => [ + $this->moveKitExportToModuleFolder('js/react', 'resources/js/react.js'), // but this should still be imported and installed + ], + ], + ); + + $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_requires_imported_module_folder_config() + { + $this->setConfig([ + 'modules' => [ + 'seo' => '@import', + ], + ]); + + $this->assertFileDoesNotExist(base_path('copied.md')); + + $this + ->installCoolRunnings() + ->expectsOutputToContain('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() + ->expectsOutputToContain('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() + ->expectsOutputToContain('Starter-kit module is missing `export_paths`, `dependencies`, or nested `modules`.') + ->assertFailed(); + + $this->assertFileDoesNotExist(base_path('copied.md')); + } + + #[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' => [ + $this->moveKitExportToModuleFolder('seo', 'resources/css/seo.css'), + ], + 'dependencies' => [ + 'statamic/seo-pro' => '^0.2.0', + ], + 'modules' => [ + 'js' => [ + 'options' => [ + 'react' => [ + 'export_paths' => [ + $this->moveKitExportToModuleFolder('seo', 'resources/js/react.js'), + ], + 'modules' => [ + 'testing_tools' => [ + 'export_paths' => [ + $this->moveKitExportToModuleFolder('seo', 'resources/js/react-testing-tools.js'), + ], + ], + ], + ], + 'vue' => [ + 'export_paths' => [ + $this->moveKitExportToModuleFolder('seo', 'resources/js/vue.js'), + ], + 'dependencies_dev' => [ + 'i-love-vue/test-helpers' => '^1.5', + ], + 'modules' => [ + 'testing_tools' => '@import', // import nested module! + ], + ], + 'svelte' => [ + 'export_paths' => [ + $this->moveKitExportToModuleFolder('seo', 'resources/js/svelte.js'), + ], + ], + ], + ], + 'oldschool_js' => [ + 'options' => [ + 'jquery' => [ + 'export_paths' => [ + $this->moveKitExportToModuleFolder('seo', 'resources/js/jquery.js'), + ], + ], + 'mootools' => [ + 'export_paths' => [ + $this->moveKitExportToModuleFolder('seo', 'resources/js/mootools.js'), + ], + ], + ], + ], + ], + ], + ); + + $this->setConfig( + path: 'modules/seo/js/vue/testing_tools/module.yaml', + config: [ + 'export_paths' => [ + $this->moveKitExportToModuleFolder('seo/js/vue/testing_tools', 'resources/js/vue-testing-tools.js'), + ], + ], + ); + + $this->setConfig( + path: 'modules/jamaica/bobsled/module.yaml', + config: [ + 'export_paths' => [ + $this->moveKitExportToModuleFolder('jamaica/bobsled', '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 Path::tidy(collect([base_path('repo/cool-runnings'), $path])->filter()->implode('/')); @@ -1652,9 +2128,19 @@ 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 moveKitExportToModuleFolder($relativeModuleFolderPath, $relativeFilePath) + { + $this->files->move( + $this->kitRepoPath('export/'.$relativeFilePath), + $this->preparePath($this->kitRepoPath('modules/'.$relativeModuleFolderPath.'/export/'.$relativeFilePath)), + ); + + return $relativeFilePath; } private function preparePath($path) 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