Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[5.x] Add folder based starter kit modules #11104

Draft
wants to merge 18 commits into
base: 5.x
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions src/StarterKits/Exporter.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ public function export(): void
->validateExportPath()
->validateConfig()
->instantiateModules()
->exportModules()
->exportInlineModules()
->exportModulesFolder()
->exportConfig()
->exportHooks()
->exportComposerJson();
Expand Down Expand Up @@ -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.
*/
Expand Down
36 changes: 30 additions & 6 deletions src/StarterKits/InstallableModule.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,30 @@
final class InstallableModule extends Module
{
protected $installer;
protected $relativePath;

/**
* Set installer instance.
*
* @throws Exception|StarterKitException
*/
public function installer($installer): self
public function installer(?Installer $installer): self
{
$this->installer = $installer;

return $this;
}

/**
* Set relative module path.
*/
public function setRelativePath(string $path): self
{
$this->relativePath = $path;

return $this;
}

/**
* Validate starter kit module is installable.
*
Expand Down Expand Up @@ -102,7 +113,7 @@ protected function installDependencies(): self
/**
* Get installable files.
*/
protected function installableFiles(): Collection
public function installableFiles(): Collection
{
$installableFromExportPaths = $this
->exportPaths()
Expand All @@ -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]);

Expand Down Expand Up @@ -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.");
});
Expand Down Expand Up @@ -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.
*/
Expand Down
206 changes: 206 additions & 0 deletions src/StarterKits/InstallableModules.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
<?php

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
{
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.
*/
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, ?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.
*/
protected function moduleConfigExists(string $key): bool
{
return $this->files->exists(
$this->starterKitPath($this->relativeModulePath($key, 'module.yaml'))
);
}

/**
* Scope installable files to imported module.
*/
protected function scopeInstallableFiles(InstallableModule $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();
}
}
Loading