Skip to content

Commit

Permalink
[5.x] Starter kit modules and other misc improvements (#10559)
Browse files Browse the repository at this point in the history
Co-authored-by: Jason Varga <[email protected]>
  • Loading branch information
jesseleite and jasonvarga authored Aug 13, 2024
1 parent 52ee6a0 commit c6180f6
Show file tree
Hide file tree
Showing 27 changed files with 2,598 additions and 664 deletions.
16 changes: 7 additions & 9 deletions src/Console/Commands/StarterKitExport.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

namespace Statamic\Console\Commands;

use Facades\Statamic\StarterKits\Exporter as StarterKitExporter;
use Illuminate\Console\Command;
use Statamic\Console\RunsInPlease;
use Statamic\Facades\File;
use Statamic\Facades\Path;
use Statamic\StarterKits\Exceptions\StarterKitException;
use Statamic\StarterKits\Exporter as StarterKitExporter;

use function Laravel\Prompts\confirm;

Expand Down Expand Up @@ -42,8 +42,10 @@ public function handle()
$this->askToCreateExportPath($path);
}

$exporter = new StarterKitExporter($path);

try {
StarterKitExporter::export($path);
$exporter->export();
} catch (StarterKitException $exception) {
$this->components->error($exception->getMessage());

Expand All @@ -56,7 +58,7 @@ public function handle()
/**
* Ask to stub out starter kit config.
*/
protected function askToStubStarterKitConfig()
protected function askToStubStarterKitConfig(): void
{
$stubPath = __DIR__.'/stubs/starter-kits/starter-kit.yaml.stub';
$newPath = base_path($config = 'starter-kit.yaml');
Expand All @@ -75,10 +77,8 @@ protected function askToStubStarterKitConfig()

/**
* Get absolute path.
*
* @return string
*/
protected function getAbsolutePath()
protected function getAbsolutePath(): string
{
$path = $this->argument('path');

Expand All @@ -89,10 +89,8 @@ protected function getAbsolutePath()

/**
* Ask to create export path.
*
* @param string $path
*/
protected function askToCreateExportPath($path)
protected function askToCreateExportPath(string $path): void
{
if ($this->input->isInteractive()) {
if (! confirm("Path [{$path}] does not exist. Would you like to create it now?", true)) {
Expand Down
37 changes: 25 additions & 12 deletions src/Console/Commands/StarterKitInstall.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Statamic\Console\Commands;

use Illuminate\Console\Command;
use Laravel\Prompts\Prompt;
use Statamic\Console\RunsInPlease;
use Statamic\Console\ValidatesInput;
use Statamic\Rules\ComposerPackage;
Expand All @@ -28,6 +29,7 @@ class StarterKitInstall extends Command
{ --local : Install from local repo configured in composer config.json }
{ --with-config : Copy starter-kit.yaml config for local development }
{ --without-dependencies : Install without dependencies }
{ --without-user : Install without creating user }
{ --force : Force install and allow dependency errors }
{ --cli-install : Installing from CLI Tool }
{ --clear-site : Clear site before installing }';
Expand Down Expand Up @@ -56,17 +58,17 @@ public function handle()
return;
}

if ($cleared = $this->shouldClear()) {
$this->call('statamic:site:clear', ['--no-interaction' => true]);
if ($cleared = $this->shouldClearSite()) {
$this->clearSite();
}

$installer = StarterKitInstaller::package($package, $this, $licenseManager)
$installer = (new StarterKitInstaller($package, $this, $licenseManager))
->branch($branch)
->fromLocalRepo($this->option('local'))
->withConfig($this->option('with-config'))
->withoutDependencies($this->option('without-dependencies'))
->isInteractive($isInteractive = $this->input->isInteractive())
->withUser($cleared && $isInteractive && ! $this->option('cli-install'))
->withUserPrompt($cleared && $this->input->isInteractive() && ! $this->option('without-user') && ! $this->option('cli-install'))
->isInteractive($this->input->isInteractive())
->usingSubProcess($this->option('cli-install'))
->force($this->option('force'));

Expand All @@ -93,10 +95,8 @@ public function handle()

/**
* Get composer package (and optional branch).
*
* @return string
*/
protected function getPackageAndBranch()
protected function getPackageAndBranch(): array
{
$package = $this->argument('package') ?: text('Package');

Expand All @@ -111,10 +111,8 @@ protected function getPackageAndBranch()

/**
* Check if should clear site first.
*
* @return bool
*/
protected function shouldClear()
protected function shouldClearSite(): bool
{
if ($this->option('clear-site')) {
return true;
Expand All @@ -125,7 +123,22 @@ protected function shouldClear()
return false;
}

private function oldCliToolInstallationDetected()
/**
* Clear site, and re-set prompt interactivity for future prompts.
*
* See: https://github.com/statamic/cli/issues/62
*/
protected function clearSite(): void
{
$this->call('statamic:site:clear', ['--no-interaction' => true]);

Prompt::interactive($this->input->isInteractive());
}

/**
* Detect older Statamic CLI installation.
*/
private function oldCliToolInstallationDetected(): bool
{
return (! $this->input->isInteractive()) // CLI tool never runs interactively.
&& (! $this->option('cli-install')) // Updated CLI tool passes this option.
Expand Down
4 changes: 2 additions & 2 deletions src/Console/Commands/StarterKitRunPostInstall.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ public function handle()
return 1;
}

$installer = StarterKitInstaller::package($package, $this);
$installer = new StarterKitInstaller($package, $this);

try {
$installer->runPostInstallHook(true)->removeStarterKit();
$installer->runPostInstallHooks(true)->removeStarterKit();
} catch (StarterKitException $exception) {
$this->components->error($exception->getMessage());

Expand Down
63 changes: 63 additions & 0 deletions src/StarterKits/Concerns/InteractsWithFilesystem.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

namespace Statamic\StarterKits\Concerns;

use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use Statamic\Console\NullConsole;
use Statamic\Facades\Path;

trait InteractsWithFilesystem
{
/**
* Install starter kit file.
*/
protected function installFile(string $fromPath, string $toPath, Command|NullConsole $console): self
{
$displayPath = str_replace(Path::tidy(base_path().'/'), '', $toPath);

$console->line("Installing file [{$displayPath}]");

app(Filesystem::class)->copy($fromPath, $this->preparePath($toPath));

return $this;
}

/**
* Export starter kit path.
*/
protected function exportPath(string $starterKitPath, string $from, ?string $to = null): void
{
$to = $to
? "{$starterKitPath}/{$to}"
: "{$starterKitPath}/{$from}";

$from = base_path($from);

$this->preparePath($to);

$files = app(Filesystem::class);

$files->isDirectory($from)
? $files->copyDirectory($from, $to)
: $files->copy($from, $to);
}

/**
* Prepare path directory.
*/
protected function preparePath(string $path): string
{
$files = app(Filesystem::class);

$directory = $files->isDirectory($path)
? $path
: preg_replace('/(.*)\/[^\/]*/', '$1', Path::tidy($path));

if (! $files->exists($directory)) {
$files->makeDirectory($directory, 0755, true);
}

return Path::tidy($path);
}
}
161 changes: 161 additions & 0 deletions src/StarterKits/ExportableModule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
<?php

namespace Statamic\StarterKits;

use Exception;
use Illuminate\Support\Collection;
use Statamic\StarterKits\Exceptions\StarterKitException;
use Statamic\Support\Str;

class ExportableModule extends Module
{
/**
* Validate starter kit module is exportable.
*
* @throws Exception|StarterKitException
*/
public function validate(): void
{
$this
->ensureModuleConfigNotEmpty()
->ensureNotExportingComposerJson()
->ensureExportablePathsExist()
->ensureExportableDependenciesExist();
}

/**
* Export starter kit module.
*
* @throws Exception|StarterKitException
*/
public function export(string $starterKitPath): void
{
$this
->exportPaths()
->each(fn ($path) => $this->exportPath(
from: $path,
starterKitPath: $starterKitPath,
));

$this
->exportAsPaths()
->each(fn ($to, $from) => $this->exportPath(
from: $from,
to: $to,
starterKitPath: $starterKitPath,
));
}

public function versionDependencies(): self
{
$exportableDependencies = $this->exportableDependencies();

$this->config->forget('dependencies');
$this->config->forget('dependencies_dev');

if ($dependencies = $this->exportDependenciesFromComposerRequire('require', $exportableDependencies)) {
$this->config->put('dependencies', $dependencies->all());
}

if ($devDependencies = $this->exportDependenciesFromComposerRequire('require-dev', $exportableDependencies)) {
$this->config->put('dependencies_dev', $devDependencies->all());
}

return $this;
}

/**
* Get exportable dependencies without versions from module config.
*/
protected function exportableDependencies(): Collection
{
$config = $this->config();

return collect()
->merge($config->get('dependencies') ?? [])
->merge($config->get('dependencies_dev') ?? [])
->map(function ($value, $key) {
return Str::contains($key, '/')
? $key
: $value;
});
}

/**
* Export dependencies from composer.json using specific require key.
*/
protected function exportDependenciesFromComposerRequire(string $requireKey, Collection $exportableDependencies): mixed
{
$composerJson = json_decode($this->files->get(base_path('composer.json')), true);

$dependencies = collect($composerJson[$requireKey] ?? [])
->filter(function ($version, $dependency) use ($exportableDependencies) {
return $exportableDependencies->contains($dependency);
});

return $dependencies->isNotEmpty()
? $dependencies
: false;
}

/**
* Ensure composer.json is not one of the export paths.
*
* @throws StarterKitException
*/
protected function ensureNotExportingComposerJson(): self
{
// Here we'll ensure both `export_as` values and keys are included,
// because we want to make sure `composer.json` is referenced on either end.
$flattenedExportPaths = $this
->exportPaths()
->merge($this->exportAsPaths())
->merge($this->exportAsPaths()->keys());

if ($flattenedExportPaths->contains('composer.json')) {
throw new StarterKitException('Cannot export [composer.json]. Please use `dependencies` array!');
}

return $this;
}

/**
* Ensure export paths exist.
*
* @throws StarterKitException
*/
protected function ensureExportablePathsExist(): self
{
$this
->exportPaths()
->merge($this->exportAsPaths()->keys())
->reject(fn ($path) => $this->files->exists(base_path($path)))
->each(function ($path) {
throw new StarterKitException("Cannot export [{$path}], because it does not exist in your app!");
});

return $this;
}

/**
* Ensure export dependencies exist in app's composer.json.
*
* @throws StarterKitException
*/
protected function ensureExportableDependenciesExist(): self
{
$installedDependencies = collect(json_decode($this->files->get(base_path('composer.json')), true))
->only(['require', 'require-dev'])
->map(fn ($dependencies) => array_keys($dependencies))
->flatten();

$this
->exportableDependencies()
->reject(fn ($dependency) => $installedDependencies->contains($dependency))
->each(function ($dependency) {
throw new StarterKitException("Cannot export [{$dependency}], because it does not exist in your composer.json!");
});

return $this;
}
}
Loading

0 comments on commit c6180f6

Please sign in to comment.