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] Starter kit modules and other misc improvements #10559

Merged
merged 47 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
379fdb2
Docblock.
jesseleite Aug 1, 2024
24b2e12
Don’t need this.
jesseleite Aug 1, 2024
91d4614
Extract these calls to `install()`.
jesseleite Aug 1, 2024
135fd8a
Fix case.
jesseleite Aug 1, 2024
c03e5ca
Extract common filesystem helpers out to trait.
jesseleite Aug 1, 2024
251f000
Extract installation logic out to `ModuleInstaller`.
jesseleite Aug 1, 2024
a8440aa
Update method usage.
jesseleite Aug 1, 2024
8baf4d3
Merge branch '5.x' of https://github.com/statamic/cms into feature/im…
jesseleite Aug 1, 2024
d231d5d
Love a good noun.
jesseleite Aug 1, 2024
9cfe1b5
Be modern.
jesseleite Aug 2, 2024
2b02d93
Clarify what this method is doing.
jesseleite Aug 2, 2024
71585b7
Solve this problem where it happens, not deeper in child class.
jesseleite Aug 2, 2024
316b11d
Restore properties and use fluent getters/setters.
jesseleite Aug 2, 2024
cb8d068
Clean up console output.
jesseleite Aug 2, 2024
00ed4bf
Module prompts.
jesseleite Aug 2, 2024
075a499
Merge branch '5.x' of https://github.com/statamic/cms into feature/im…
jesseleite Aug 2, 2024
f07ffbd
Option `label` instead of `display`.
jesseleite Aug 3, 2024
cdc746c
Handle when running non-interactively.
jesseleite Aug 4, 2024
9d2ad4e
Fix type exceptions for tests.
jesseleite Aug 4, 2024
fe3ad25
Misc tests.
jesseleite Aug 6, 2024
fd772dc
Windows.
jesseleite Aug 6, 2024
2315fea
Misc cleanup.
jesseleite Aug 7, 2024
6eb6b79
Fixtures
jesseleite Aug 7, 2024
8050873
Add `—without-user` option to skip user prompt.
jesseleite Aug 7, 2024
4ba7836
Refactor to use built-in interactive command assertions helpers.
jesseleite Aug 7, 2024
3651692
Split `Module` logic into `InstallableModule` and `ExportableModule`.
jesseleite Aug 8, 2024
83097d6
Update `Installer` to use new `InstallableModule`.
jesseleite Aug 8, 2024
37e7106
Flesh out `InstallTest` a bit more.
jesseleite Aug 8, 2024
624c0ac
Refactor exporter to handle modules.
jesseleite Aug 8, 2024
fe4bf5e
Use new `expectsChoice()` fixes, when available.
jesseleite Aug 8, 2024
81de239
Fix a long-standing bug with how dependencies are exported.
jesseleite Aug 9, 2024
b9779a9
Less bugs, more tests.
jesseleite Aug 9, 2024
a8a33c2
Validate dependencies are actually installed in composer.json, so tha…
jesseleite Aug 9, 2024
62a6973
Merge branch '5.x' into feature/improved-starter-kits
jasonvarga Aug 12, 2024
fcd81c1
Remove square brackets from prompts.
jesseleite Aug 12, 2024
bf233e3
Rename var for clarity.
jesseleite Aug 12, 2024
d19666b
Add support for nested modules.
jesseleite Aug 12, 2024
ea3d6d7
And this is why we write tests! 😁
jesseleite Aug 12, 2024
6fec4a0
Change terminology to better match docs.
jesseleite Aug 12, 2024
976c298
Test installation of nested modules.
jesseleite Aug 12, 2024
345464a
Improve install module validation.
jesseleite Aug 12, 2024
40a8b36
Should filter after.
jesseleite Aug 12, 2024
71dff52
Add support for nested modules in exporter.
jesseleite Aug 13, 2024
533512c
Add test coverage for exporter.
jesseleite Aug 13, 2024
2481a0b
Bring back the square brackets as shown in the docs, it looks fine.
jesseleite Aug 13, 2024
b91fd06
Simplify recursion logic.
jesseleite Aug 13, 2024
814de4c
Move method up to where it’s called.
jesseleite Aug 13, 2024
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
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
Loading