diff --git a/.gitignore b/.gitignore index 53b6ba82..c0a17182 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /vendor .phpunit.result.cache +/storage/framework/ diff --git a/app/Actions/CompileAssets.php b/app/Actions/CompileAssets.php new file mode 100644 index 00000000..c5c4d3aa --- /dev/null +++ b/app/Actions/CompileAssets.php @@ -0,0 +1,42 @@ +shell = $shell; + $this->silentDevScript = $silentDevScript; + } + + public function __invoke() + { + if (! config('lambo.store.mix')) { + return; + } + + $this->silentDevScript->add(); + + $this->logStep('Compiling project assets'); + + $process = $this->shell->execInProject("npm run dev{$this->extraOptions()}"); + + $this->abortIf(! $process->isSuccessful(), 'Compilation of project assets did not complete successfully', $process); + + $this->silentDevScript->remove(); + + $this->info('Project assets compiled successfully.'); + } + public function extraOptions() + { + return config('lambo.store.with_output') ? '' : ' --silent'; + } +} diff --git a/app/Actions/ConfigureFrontendFramework.php b/app/Actions/ConfigureFrontendFramework.php new file mode 100644 index 00000000..12d252c9 --- /dev/null +++ b/app/Actions/ConfigureFrontendFramework.php @@ -0,0 +1,37 @@ +shell = $shell; + $this->laravelUi = $laravelUi; + } + + public function __invoke() + { + if (! config('lambo.store.frontend')) { + return; + } + + $this->laravelUi->install(); + + $this->logStep('Configuring frontend scaffolding'); + + $process = $this->shell->execInProject('php artisan ui ' . config('lambo.store.frontend')); + + $this->abortIf(! $process->isSuccessful(), "Installation of UI scaffolding did not complete successfully.", $process); + + $this->info('UI scaffolding has been set to ' . config('lambo.store.frontend')); + } +} diff --git a/app/Actions/CreateDatabase.php b/app/Actions/CreateDatabase.php index 9b05e57b..20359c69 100644 --- a/app/Actions/CreateDatabase.php +++ b/app/Actions/CreateDatabase.php @@ -2,10 +2,54 @@ namespace App\Actions; +use App\Shell\Shell; +use Illuminate\Support\Str; +use Symfony\Component\Process\ExecutableFinder; + class CreateDatabase { + use LamboAction; + + protected $finder; + protected $shell; + + public function __construct(Shell $shell, ExecutableFinder $finder) + { + $this->finder = $finder; + $this->shell = $shell; + } + public function __invoke() { - // @todo + if (! config('lambo.store.create_database')) { + return; + } + + if (! $this->mysqlExists()) { + return "MySQL does not seem to be installed. Skipping new database creation."; + } + + $this->logStep('Creating database'); + + $process = $this->shell->execInProject($this->command()); + + $this->abortIf(! $process->isSuccessful(), "The new database was not created.", $process); + + return 'Created a new database ' . config('lambo.store.database_name'); + } + + protected function mysqlExists() + { + return $this->finder->find('mysql') !== null; + } + + protected function command() + { + return sprintf( + 'mysql --user=%s --password=%s -e "CREATE DATABASE IF NOT EXISTS %s";', + config('lambo.store.database_username'), + config('lambo.store.database_password'), + config('lambo.store.database_name') + ); } } diff --git a/app/Actions/CustomizeDotEnv.php b/app/Actions/CustomizeDotEnv.php index 510ea587..1d8e889f 100644 --- a/app/Actions/CustomizeDotEnv.php +++ b/app/Actions/CustomizeDotEnv.php @@ -2,20 +2,25 @@ namespace App\Actions; -use Facades\App\Utilities; use Illuminate\Support\Arr; use Illuminate\Support\Facades\File; class CustomizeDotEnv { + use LamboAction; + public function __invoke() { + $this->logStep('Customizing .env and .env.example'); + $filePath = config('lambo.store.project_path') . '/.env.example'; $output = $this->customize(File::get($filePath)); File::put($filePath, $output); File::put(str_replace('.env.example', '.env', $filePath), $output); + + $this->info('.env files configured.'); } public function customize($contents) @@ -41,17 +46,11 @@ public function value($key, $fallback) $replacements = [ 'APP_NAME' => config('lambo.store.project_name'), 'APP_URL' => config('lambo.store.project_url'), - 'DB_DATABASE' => $this->databaseName(), - 'DB_USERNAME' => 'root', - 'DB_PASSWORD' => null, + 'DB_DATABASE' => config('lambo.store.database_name'), + 'DB_USERNAME' => config('lambo.store.database_username'), + 'DB_PASSWORD' => config('lambo.store.database_password'), ]; return Arr::get($replacements, $key, $fallback); } - - public function databaseName() - { - // @todo allow for flag for custom database name.. TEST IT! - return Utilities::prepNameForDatabase(config('lambo.store.project_name')); - } } diff --git a/app/Actions/DisplayHelpScreen.php b/app/Actions/DisplayHelpScreen.php index 588894d2..59545c2f 100644 --- a/app/Actions/DisplayHelpScreen.php +++ b/app/Actions/DisplayHelpScreen.php @@ -3,37 +3,35 @@ namespace App\Actions; use App\Options; -use Illuminate\Support\Arr; class DisplayHelpScreen { + use LamboAction; + protected $indent = 30; protected $commands = [ 'help-screen' => 'Display this screen', - 'make-config' => 'Generate config file', 'edit-config' => 'Edit config file', - 'make-after' => 'Generate "after" file', 'edit-after' => 'Edit "after" file', ]; public function __invoke() { - $console = app('console'); - $console->line("\nUsage:"); - $console->line(" lambo new myApplication [arguments]\n"); - $console->line("Commands (lambo COMMANDNAME):"); + $this->line("\nUsage:"); + $this->line(" lambo new myApplication [arguments]\n"); + $this->line("Commands (lambo COMMANDNAME):"); foreach ($this->commands as $command => $description) { $spaces = $this->makeSpaces(strlen($command)); - $console->line(" {$command}{$spaces}{$description}"); + $this->line(" {$command}{$spaces}{$description}"); } - $console->line("\nOptions (lambo new myApplication OPTIONS):"); + $this->line("\nOptions (lambo new myApplication OPTIONS):"); foreach ((new Options)->all() as $option) { - $console->line($this->createCliStringForOption($option)); + $this->line($this->createCliStringForOption($option)); } } diff --git a/app/Actions/DisplayLamboWelcome.php b/app/Actions/DisplayLamboWelcome.php index c74b820d..2f89800a 100644 --- a/app/Actions/DisplayLamboWelcome.php +++ b/app/Actions/DisplayLamboWelcome.php @@ -4,6 +4,8 @@ class DisplayLamboWelcome { + use LamboAction; + protected $lamboLogo = " __ __ :version: / / ____ _____ ___ / /_ ____ @@ -23,12 +25,12 @@ public function __invoke() { foreach (explode("\n", $this->lamboLogo) as $line) { // Extra space on the end fixes an issue with console when it ends with backslash - app('console')->info($line . " "); + $this->info($line . " "); } foreach (explode("\n", $this->welcomeText) as $line) { // Extra space on the end fixes an issue with console when it ends with backslash - app('console')->line($line . " "); + $this->line($line . " "); } } } diff --git a/app/Actions/EditAfter.php b/app/Actions/EditAfter.php new file mode 100644 index 00000000..6c7ca9fb --- /dev/null +++ b/app/Actions/EditAfter.php @@ -0,0 +1,16 @@ +createOrEditConfigFile("after", File::get(base_path('stubs/after.stub'))); + } +} diff --git a/app/Actions/EditConfig.php b/app/Actions/EditConfig.php new file mode 100644 index 00000000..25e2c0b5 --- /dev/null +++ b/app/Actions/EditConfig.php @@ -0,0 +1,16 @@ +createOrEditConfigFile("config", File::get(base_path('stubs/config.stub'))); + } +} diff --git a/app/Actions/GenerateAppKey.php b/app/Actions/GenerateAppKey.php index 5a2dc17e..ff157768 100644 --- a/app/Actions/GenerateAppKey.php +++ b/app/Actions/GenerateAppKey.php @@ -2,10 +2,12 @@ namespace App\Actions; -use App\Shell; +use App\Shell\Shell; class GenerateAppKey { + use LamboAction; + protected $shell; public function __construct(Shell $shell) @@ -15,6 +17,12 @@ public function __construct(Shell $shell) public function __invoke() { - $this->shell->execInProject('php artisan key:generate'); + $this->logStep('Running php artisan key:generate'); + + $process = $this->shell->execInProject('php artisan key:generate'); + + $this->abortIf(! $process->isSuccessful(), 'Failed to generate application key successfully', $process); + + $this->info('Application key has been set.'); } } diff --git a/app/Actions/InitializeGitRepo.php b/app/Actions/InitializeGitRepo.php index 2ebb7a5c..b9349b82 100644 --- a/app/Actions/InitializeGitRepo.php +++ b/app/Actions/InitializeGitRepo.php @@ -2,10 +2,12 @@ namespace App\Actions; -use App\Shell; +use App\Shell\Shell; class InitializeGitRepo { + use LamboAction; + protected $shell; public function __construct(Shell $shell) @@ -15,15 +17,18 @@ public function __construct(Shell $shell) public function __invoke() { - $this->shell->execInProject('git init'); - $this->shell->execInProject('git add .'); - $this->shell->execInProject('git commit -m "' . $this->gitCommit() . '"'); + $this->logStep('Initializing git repository'); - app('console')->info('Git repository initialized.'); + $this->execAndCheck('git init'); + $this->execAndCheck('git add .'); + $this->execAndCheck('git commit -m "' . config('lambo.store.commit_message') . '"'); + $this->info('New git repository initialized.'); } - public function gitCommit() + public function execAndCheck($command) { - return app('console')->option('message') ?? 'Initial commit.'; + $process = $this->shell->execInProject($command); + + $this->abortIf(! $process->isSuccessful(), 'Initialization of git repository did not complete successfully.', $process); } } diff --git a/app/Actions/InstallNpmDependencies.php b/app/Actions/InstallNpmDependencies.php index 48b59013..b4560487 100644 --- a/app/Actions/InstallNpmDependencies.php +++ b/app/Actions/InstallNpmDependencies.php @@ -2,10 +2,12 @@ namespace App\Actions; -use App\Shell; +use App\Shell\Shell; class InstallNpmDependencies { + use LamboAction; + protected $shell; public function __construct(Shell $shell) @@ -15,8 +17,19 @@ public function __construct(Shell $shell) public function __invoke() { - app('console')->info('Installing NPM dependencies.'); + if (! config('lambo.store.node')) { + return; + } + + $process = $this->shell->execInProject("npm install{$this->extraOptions()}"); + + $this->abortIf(! $process->isSuccessful(), 'Installation of npm dependencies did not complete successfully', $process); - $this->shell->execInProject("npm install"); + $this->info('Npm dependencies installed.'); + } + + public function extraOptions() + { + return config('lambo.store.with-output') ? '' : ' --silent'; } } diff --git a/app/Actions/LamboAction.php b/app/Actions/LamboAction.php new file mode 100644 index 00000000..40e7c8a9 --- /dev/null +++ b/app/Actions/LamboAction.php @@ -0,0 +1,23 @@ +comment("\n{$step}..."); + } + + public function abortIf(bool $abort, string $message, $process) + { + if ($abort) { + throw new Exception("{$message}\n Failed to run: '{$process->getCommandLine()}'"); + } + } +} diff --git a/app/Actions/LaravelUi.php b/app/Actions/LaravelUi.php new file mode 100644 index 00000000..9b8e270b --- /dev/null +++ b/app/Actions/LaravelUi.php @@ -0,0 +1,40 @@ +shell = $shell; + } + + public function install() + { + if ($this->laravelUiInstalled()) { + return; + } + + $this->logStep('To use Laravel frontend scaffolding the composer package laravel/ui is required. Installing now...'); + + $process = $this->shell->execInProject('composer require laravel/ui --quiet'); + + $this->abortIf(! $process->isSuccessful(), "Installation of laravel/ui did not complete successfully.", $process); + + $this->info('laravel/ui installed.'); + } + + private function laravelUiInstalled(): bool + { + $composeConfig = json_decode(File::get(config('lambo.store.project_path') . '/composer.json'), true); + return Arr::has($composeConfig, 'require.laravel/ui'); + } +} diff --git a/app/Actions/OpenInBrowser.php b/app/Actions/OpenInBrowser.php index ff45a3ef..8a2cd017 100644 --- a/app/Actions/OpenInBrowser.php +++ b/app/Actions/OpenInBrowser.php @@ -2,10 +2,13 @@ namespace App\Actions; -use App\Shell; +use App\Environment; +use App\Shell\Shell; class OpenInBrowser { + use LamboAction; + protected $shell; public function __construct(Shell $shell) @@ -15,24 +18,23 @@ public function __construct(Shell $shell) public function __invoke() { - if ($this->isMac() && $this->browser()) { + $this->logStep('Opening in Browser'); + + if (Environment::isMac() && $this->browser()) { $this->shell->execInProject(sprintf( 'open -a "%s" "%s"', $this->browser(), config('lambo.store.project_url') )); - } else { - $this->shell->execInProject("valet open"); + + return; } - } - public function isMac() - { - return PHP_OS === 'Darwin'; + $this->shell->execInProject("valet open"); } public function browser() { - return app('console')->option('browser'); + return config('lambo.store.browser'); } } diff --git a/app/Actions/OpenInEditor.php b/app/Actions/OpenInEditor.php index 57afa105..ebd96b66 100644 --- a/app/Actions/OpenInEditor.php +++ b/app/Actions/OpenInEditor.php @@ -2,10 +2,12 @@ namespace App\Actions; -use App\Shell; +use App\Shell\Shell; class OpenInEditor { + use LamboAction; + protected $shell; public function __construct(Shell $shell) @@ -15,17 +17,18 @@ public function __construct(Shell $shell) public function __invoke() { - app('console')->info('Opening your editor.'); - - if ($this->editor()) { - $this->shell->execInProject($this->editor() . " ."); + if (! $this->editor()) { + return; } - // @todo: should we default to $EDITOR (environment var) + $this->logStep('Opening In Editor'); + + $this->shell->execInProject($this->editor() . " ."); + $this->info('Opening your project in ' . $this->editor()); } public function editor() { - return app('console')->option('editor'); + return config('lambo.store.editor'); } } diff --git a/app/Actions/RunAfterScript.php b/app/Actions/RunAfterScript.php new file mode 100644 index 00000000..930a824e --- /dev/null +++ b/app/Actions/RunAfterScript.php @@ -0,0 +1,33 @@ +shell = $shell; + } + + public function __invoke() + { + if (! $this->configFileExists('after')) { + return; + } + + $this->logStep('Running after script'); + + $process = $this->shell->execInProject("sh " . $this->getConfigFilePath("after")); + + $this->abortIf(! $process->isSuccessful(), 'After file did not complete successfully', $process); + + $this->info('After script has completed.'); + } +} diff --git a/app/Actions/RunLaravelInstaller.php b/app/Actions/RunLaravelInstaller.php index 12bda995..f88c69ae 100644 --- a/app/Actions/RunLaravelInstaller.php +++ b/app/Actions/RunLaravelInstaller.php @@ -2,10 +2,12 @@ namespace App\Actions; -use App\Shell; +use App\Shell\Shell; class RunLaravelInstaller { + use LamboAction; + protected $shell; public function __construct(Shell $shell) @@ -15,19 +17,34 @@ public function __construct(Shell $shell) public function __invoke() { - $projectName = config('lambo.store.project_name'); + $this->logStep('Running the Laravel installer'); + + $process = $this->shell->execInRoot('laravel new ' . config('lambo.store.project_name') . $this->extraOptions()); - app('console')->info('Creating application using the Laravel installer.'); + $this->abortIf(! $process->isSuccessful(), "The laravel installer did not complete successfully.", $process); + + if ($process->isSuccessful()) { + $this->info($this->getFeedback()); + return; + } + } - $this->shell->execInRoot("laravel new {$projectName}"); + public function extraOptions() + { + return sprintf('%s%s%s', + config('lambo.store.auth') ? ' --auth' : '', + config('lambo.store.dev') ? ' --dev' : '', + config('lambo.store.with_output') ? '' : ' --quiet' + ); + } - // @todo - // if ($isDev) { - // $this->console->info('Creating application from dev branch.'); - // $this->shell->inDirectory($directory, "laravel new {$projectName} --dev"); - // } else { - // $this->console->info('Creating application from release branch.'); - // $this->shell->inDirectory($directory, "laravel new {$projectName}"); - // } + public function getFeedback(): string + { + return sprintf("A new application '%s'%s has been created from the %s branch.", + config('lambo.store.project_name'), + config('lambo.store.auth') ? ' with auth scaffolding' : '', + config('lambo.store.dev') ? 'develop' : 'release' + ); } + } diff --git a/app/Actions/SetConfig.php b/app/Actions/SetConfig.php new file mode 100644 index 00000000..879323d4 --- /dev/null +++ b/app/Actions/SetConfig.php @@ -0,0 +1,235 @@ +savedConfig = $this->loadSavedConfig(); + } + + public function __invoke() + { + $tld = $this->getTld(); + + config()->set('lambo.store', [ + 'tld' => $tld, + 'project_name' => $this->argument('projectName'), + 'root_path' => $this->getBasePath(), + 'project_path' => $this->getBasePath() . '/' . $this->argument('projectName'), + 'project_url' => $this->getProtocol() . $this->argument('projectName') . '.' . $tld, + 'database_name' => $this->getDatabaseName(), + 'database_username' => $this->getOptionValue('dbuser', self::DB_USERNAME) ?? 'root', + 'database_password' => $this->getOptionValue('dbpassword', self::DB_PASSWORD) ?? '', + 'create_database' => $this->shouldCreateDatabase(), + 'commit_message' => $this->getOptionValue('message', self::MESSAGE) ?? 'Initial commit.', + 'valet_link' => $this->shouldLink(), + 'valet_secure' => $this->shouldSecure(), + 'quiet' => $this->getBooleanOptionValue('quiet', self::QUIET), + 'with_output' => $this->getBooleanOptionValue('with-output', self::WITH_OUTPUT), + 'editor' => $this->getOptionValue('editor', self::CODEEDITOR), + 'node' => $this->shouldInstallNpmDependencies(), + 'mix' => $this->shouldRunMix(), + 'dev' => $this->getBooleanOptionValue('dev', self::DEVELOP), + 'auth' => $this->shouldInstallAuthentication(), + 'browser' => $this->getOptionValue('browser', self::BROWSER), + 'frontend' => $this->getFrontendType(), + 'full' => $this->getBooleanOptionValue('full'), + ]); + } + + public function loadSavedConfig() + { + (Dotenv::create($this->configDir(), 'config'))->safeLoad(); + + return collect($this->keys)->reject(function ($key) { + return ! Arr::has($_ENV, $key); + })->mapWithKeys(function($value){ + return [$value => $_ENV[$value]]; + })->toArray(); + } + + public function getTld() + { + $home = config('home_dir'); + + if (File::exists($home . '/.config/valet/config.json')) { + return json_decode(File::get($home . '/.config/valet/config.json'))->tld; + } + + return json_decode(File::get($home . '/.valet/config.json'))->domain; + } + + public function getOptionValue($optionCommandLineName, $optionConfigFileName = null) + { + if (is_null($optionConfigFileName)) { + $optionConfigFileName = $optionCommandLineName; + } + + if ($this->option($optionCommandLineName)) { + return $this->option($optionCommandLineName); + } + + if (Arr::has($this->savedConfig, $optionConfigFileName)) { + return Arr::get($this->savedConfig, $optionConfigFileName); + } + } + + /* + * Cast "1", "true", "on" and "yes" to bool true. Everything else to bool false. + */ + public function getBooleanOptionValue($optionCommandLineName, $optionConfigFileName = null) + { + return filter_var($this->getOptionValue($optionCommandLineName, $optionConfigFileName), FILTER_VALIDATE_BOOLEAN); + } + + public function getFrontendType() + { + $frontEndType = $this->getOptionValue('frontend', self::FRONTEND); + + if (empty($frontEndType) || is_null($frontEndType)) { + return false; + } + + if (in_array($frontEndType, self::FRONTEND_FRAMEWORKS)) { + return $frontEndType; + } + $this->error("Oops. '{$frontEndType}' is not a valid option for -f, --frontend.\nValid options are: bootstrap, react or vue."); + app(DisplayHelpScreen::class)(); + exit(); + } + + public function getBasePath() + { + if ($value = $this->getOptionValue('path', self::PROJECTPATH)) { + return str_replace('~', config('home_dir'), $value); + } + + return getcwd(); + } + + public function getProtocol() + { + return $this->shouldSecure() ? 'https://' : 'http://'; + } + + public function getDatabaseName() + { + $configuredDatabaseName = $this->getOptionValue('dbname', self::DB_NAME) + ? $this->getOptionValue('dbname', self::DB_NAME) + : $this->argument('projectName'); + + if (! Str::contains($configuredDatabaseName, '-')) { + return $configuredDatabaseName; + } + + $newDatabaseName = str_replace('-', '_', $configuredDatabaseName); + $this->warn("Your configured database name {$configuredDatabaseName} contains hyphens which can cause problems in some instances."); + $this->warn('The hyphens have been replaced with underscores to prevent problems.'); + $this->warn("New database name: {$newDatabaseName}."); + return $newDatabaseName; + } + + public function argument($key) + { + return app('console')->argument($key); + } + + public function option($key) + { + return app('console')->option($key); + } + + public function shouldRunMix(): bool + { + return $this->getBooleanOptionValue('full') + || $this->getBooleanOptionValue('mix', self::MIX); + } + + public function shouldInstallNpmDependencies(): bool + { + return $this->shouldRunMix() + || $this->getBooleanOptionValue('node', self::NODE); + } + + public function shouldCreateDatabase(): bool + { + return $this->getBooleanOptionValue('full') + || $this->getBooleanOptionValue('create-db', self::CREATE_DATABASE); + } + + public function shouldLink(): bool + { + return $this->getBooleanOptionValue('full') + || $this->getBooleanOptionValue('link', self::LINK); + } + + public function shouldSecure(): bool + { + return $this->getBooleanOptionValue('full') + || $this->getBooleanOptionValue('secure', self::SECURE); + } + + public function shouldInstallAuthentication(): bool + { + return $this->getBooleanOptionValue('full') + || $this->getBooleanOptionValue('auth', self::AUTH); + } +} diff --git a/app/Actions/SilentDevScript.php b/app/Actions/SilentDevScript.php new file mode 100644 index 00000000..f10efe1c --- /dev/null +++ b/app/Actions/SilentDevScript.php @@ -0,0 +1,38 @@ +packageJsonPath = config('lambo.store.project_path') . '/package.json'; + $this->backupPackageJsonPath = config('lambo.store.project_path') . '/package-original.json'; + } + + public function add() + { + File::copy($this->packageJsonPath, $this->backupPackageJsonPath); + File::replace($this->packageJsonPath, $this->getSilentPackageJson($this->packageJsonPath)); + } + + public function remove() + { + File::move($this->backupPackageJsonPath, $this->packageJsonPath); + } + + private function getSilentPackageJson(string $originalPackageJson) + { + $packageJson = json_decode(File::get($originalPackageJson), true); + $silentDevelopmentCommand = str_replace('--progress', '--no-progress', Arr::get($packageJson, 'scripts.development')); + Arr::set($packageJson, 'scripts.development', $silentDevelopmentCommand); + + return json_encode($packageJson, JSON_UNESCAPED_SLASHES); + } +} diff --git a/app/Actions/ValetLink.php b/app/Actions/ValetLink.php new file mode 100644 index 00000000..caaac87b --- /dev/null +++ b/app/Actions/ValetLink.php @@ -0,0 +1,32 @@ +shell = $shell; + } + + public function __invoke() + { + if (! config('lambo.store.valet_link')) { + return; + } + + $this->logStep('Running valet link'); + + $process = $this->shell->execInProject('valet link'); + + $this->abortIf(! $process->isSuccessful(), 'valet link did not complete successfully', $process); + + $this->info('valet link successful'); + } +} diff --git a/app/Actions/ValetSecure.php b/app/Actions/ValetSecure.php index caa81e61..e4122eb0 100644 --- a/app/Actions/ValetSecure.php +++ b/app/Actions/ValetSecure.php @@ -2,10 +2,12 @@ namespace App\Actions; -use App\Shell; +use App\Shell\Shell; class ValetSecure { + use LamboAction; + protected $shell; public function __construct(Shell $shell) @@ -15,6 +17,16 @@ public function __construct(Shell $shell) public function __invoke() { - $this->shell->execInProject("valet secure"); + if (! config('lambo.store.valet_secure')) { + return; + } + + $this->logStep('Running valet secure'); + + $process = $this->shell->execInProject("valet secure"); + + $this->abortIf(! $process->isSuccessful(), 'valet secure did not complete successfully', $process); + + $this->info('valet secure successful'); } } diff --git a/app/Actions/VerifyDependencies.php b/app/Actions/VerifyDependencies.php index 20858a3b..758041d1 100644 --- a/app/Actions/VerifyDependencies.php +++ b/app/Actions/VerifyDependencies.php @@ -7,25 +7,23 @@ class VerifyDependencies { - protected $finder; + use LamboAction; - protected $dependencies = [ - 'laravel', - 'git', - 'valet', - ]; + protected $finder; public function __construct(ExecutableFinder $finder) { $this->finder = $finder; } - public function __invoke() + public function __invoke(array $dependencies) { - foreach ($this->dependencies as $dependency) { + $this->logStep('Verifying dependencies'); + foreach ($dependencies as $dependency) { if ($this->finder->find($dependency) === null) { throw new Exception($dependency . ' not installed'); } } + $this->info('Dependencies: ' . implode(', ', $dependencies) . ' are available.'); } } diff --git a/app/Actions/VerifyPathAvailable.php b/app/Actions/VerifyPathAvailable.php index a4da212b..09e2b0d6 100644 --- a/app/Actions/VerifyPathAvailable.php +++ b/app/Actions/VerifyPathAvailable.php @@ -4,28 +4,28 @@ use Exception; use Illuminate\Filesystem\Filesystem; +use Illuminate\Support\Facades\File; class VerifyPathAvailable { - protected $filesystem; - - public function __construct(Filesystem $filesystem) - { - $this->filesystem = $filesystem; - } + use LamboAction; public function __invoke() { + $this->logStep('Verifying path availability'); + $rootPath = config('lambo.store.root_path'); - if (! $this->filesystem->isDirectory($rootPath)) { + if (! File::isDirectory($rootPath)) { throw new Exception($rootPath . ' is not a directory.'); } $projectPath = config('lambo.store.project_path'); - if ($this->filesystem->isDirectory($projectPath)) { + if (File::isDirectory($projectPath)) { throw new Exception($projectPath . ' is already a directory.'); } + + $this->info('Directory ' . $projectPath . ' is available.'); } } diff --git a/app/Commands/EditAfter.php b/app/Commands/EditAfter.php index 871193df..2096ab5f 100644 --- a/app/Commands/EditAfter.php +++ b/app/Commands/EditAfter.php @@ -2,16 +2,21 @@ namespace App\Commands; +use App\Actions\EditAfter as EditAfterAction; use LaravelZero\Framework\Commands\Command; class EditAfter extends Command { - protected $signature = 'edit-after'; + protected $signature = 'edit-after {--editor= : Open the config file in the specified EDITOR or the system default if none is specified.}'; - protected $description = 'Edit After File'; + protected $description = 'Edit Config File. A new config file is created if one does not already exist.'; public function handle() { - // @todo + app()->bind('console', function () { + return $this; + }); + + app(EditAfterAction::class)(); } } diff --git a/app/Commands/EditConfig.php b/app/Commands/EditConfig.php index 56cf8667..f28705c3 100644 --- a/app/Commands/EditConfig.php +++ b/app/Commands/EditConfig.php @@ -2,16 +2,21 @@ namespace App\Commands; +use App\Actions\EditConfig as EditConfigAction; use LaravelZero\Framework\Commands\Command; class EditConfig extends Command { - protected $signature = 'edit-config'; + protected $signature = 'edit-config {--editor= : Open the config file in the specified EDITOR or the system default if none is specified.}'; - protected $description = 'Edit Config File'; + protected $description = 'Edit Config File. A new config file is created if one does not already exist.'; public function handle() { - // @todo + app()->bind('console', function () { + return $this; + }); + + app(EditConfigAction::class)(); } } diff --git a/app/Commands/MakeAfter.php b/app/Commands/MakeAfter.php deleted file mode 100644 index 7834d5e2..00000000 --- a/app/Commands/MakeAfter.php +++ /dev/null @@ -1,17 +0,0 @@ -setConfig(); + app()->bind('console', function () { + return $this; + }); app(DisplayLamboWelcome::class)(); @@ -63,93 +69,43 @@ public function handle() $this->alert('Creating a Laravel app ' . $this->argument('projectName')); + app(SetConfig::class)(); + try { - $this->logStep('Verifying Path Availability'); app(VerifyPathAvailable::class)(); - $this->logStep('Verifying Dependencies'); - app(VerifyDependencies::class)(); + app(VerifyDependencies::class)(['laravel', 'git', 'valet']); - $this->logStep('Running the Laravel Installer'); app(RunLaravelInstaller::class)(); - $this->logStep('Opening In Editor'); app(OpenInEditor::class)(); - $this->logStep('Customizing .env and .env.example'); app(CustomizeDotEnv::class)(); - $this->logStep('Creating database if selected...'); - app(CreateDatabase::class)(); + $this->info(app(CreateDatabase::class)()); - $this->logStep('Running php artisan key:generate'); app(GenerateAppKey::class)(); - $this->logStep('Initializing Git Repo'); + app(ConfigureFrontendFramework::class)(); + app(InitializeGitRepo::class)(); - $this->logStep('Installing NPM dependencies'); app(InstallNpmDependencies::class)(); - $this->logStep('Running valet secure'); - app(ValetSecure::class)(); - - $this->logStep('Opening in Browser'); - app(OpenInBrowser::class)(); - } catch (Exception $e) { - $this->error("\nFAILURE RUNNING COMMAND:"); - $this->error($e->getMessage()); - } - // @todo cd into it - } - - public function setConfig() - { - app()->bind('console', function () { - return $this; - }); + app(CompileAssets::class)(); - $tld = $this->getTld(); + app(RunAfterScript::class)(); - config()->set('lambo.store', [ - 'tld' => $tld, - 'project_name' => $this->argument('projectName'), - 'root_path' => $this->getBasePath(), - 'project_path' => $this->getBasePath() . '/' . $this->argument('projectName'), - 'project_url' => $this->getProtocol() . $this->argument('projectName') . '.' . $tld, - ]); - } + app(ValetLink::class)(); - public function getTld() - { - $home = config('home_dir'); - - if (File::exists($home . '/.config/valet/config.json')) { - return json_decode(File::get($home . '/.config/valet/config.json'))->tld; - } - - return json_decode(File::get($home . '/.valet/config.json'))->domain; - } - - public function getBasePath() - { - if ($this->option('path')) { - return str_replace('~', config('home_dir'), $this->option('path')); - } - - return getcwd(); - } + app(ValetSecure::class)(); - public function getProtocol() - { - // @todo: If securing, change to https - return 'http://'; - } + app(OpenInBrowser::class)(); - public function logStep($step) - { - if ($this->option('verbose')) { - $this->comment("$step...\n"); + $this->info("\nDone. Happy coding!"); + } catch (Exception $e) { + $this->error("\nFAILURE: " . $e->getMessage()); } + // @todo cd into it } } diff --git a/app/Environment.php b/app/Environment.php new file mode 100644 index 00000000..8b60af97 --- /dev/null +++ b/app/Environment.php @@ -0,0 +1,11 @@ +ensureConfigFileExists($fileName, $fileTemplate); + + $this->editConfigFile($this->getConfigFilePath($fileName)); + } + + protected function ensureConfigFileExists(string $fileName, string $fileTemplate) + { + $this->ensureConfigDirExists(); + + if (! $this->configFileExists($fileName)) { + app('console')->info("File: {$this->getConfigFilePath($fileName)} does not exist, creating it now."); + File::put($this->getConfigFilePath($fileName), $fileTemplate); + } + } + + public function ensureConfigDirExists() + { + if (! File::exists($this->configDir())) { + app('console')->info("Config directory: {$this->configDir()} does not exist, creating it now."); + File::makeDirectory($this->configDir()); + } + } + + public function configFileExists($fileName) + { + return File::exists($this->configDir() . '/' . $fileName); + } + + public function getConfigFilePath(string $fileName) + { + return $this->configDir() . "/" . $fileName; + } + + protected function editConfigFile(string $filePath) + { + if (! Environment::isMac()) { + exec("xdg-open {$filePath}"); + return; + } + + if ($this->editor()) { + exec(sprintf('"%s" "%s"', + $this->editor(), + $filePath + )); + return; + } + + exec("open {$filePath}"); + } + + public function editor() + { + return app('console')->option('editor'); + } +} diff --git a/app/LogsToConsole.php b/app/LogsToConsole.php new file mode 100644 index 00000000..10037d65 --- /dev/null +++ b/app/LogsToConsole.php @@ -0,0 +1,31 @@ +alert($message); + } + + public function warn(string $message) + { + app('console')->warn($message); + } + + public function error(string $message) + { + app('console')->error($message); + } + + public function line(string $message) + { + app('console')->line($message); + } + + public function info(string $message) + { + app('console')->info($message); + } +} diff --git a/app/Options.php b/app/Options.php index 0074b62d..6e8ff59a 100644 --- a/app/Options.php +++ b/app/Options.php @@ -27,13 +27,19 @@ class Options [ 'short' => 'b', 'long' => 'browser', - 'param_description' => '"browser path"', + 'param_description' => '"path"', 'cli_description' => "Open the site in the specified browser (macOS-only)", ], [ - 'long' => 'create-db', - 'param_description' => 'DBNAME', // Maybe?? @todo - 'cli_description' => "Create a new MySQL database", + 'short' => 'f', + 'long' => 'frontend', + 'param_description' => '"FRONTEND"', + 'cli_description' => "Specify the FRONTEND framework to use. Must be one of bootstrap, react or vue", + ], + [ + 'long' => 'dbname', + 'param_description' => 'DBNAME', + 'cli_description' => "Specify the database name", ], [ 'long' => 'dbuser', @@ -42,23 +48,12 @@ class Options ], [ 'long' => 'dbpassword', - 'param_description' => ' PASSWORD', + 'param_description' => 'PASSWORD', 'cli_description' => "Specify the database password", ], [ - 'short' => 'l', - 'long' => 'link', - 'cli_description' => "Create a Valet link to the project directory", - ], - [ - 'short' => 's', - 'long' => 'secure', - 'cli_description' => "Generate and use an HTTPS cert with Valet", - ], - [ - 'short' => 'q', - 'long' => 'quiet', - 'cli_description' => "Use quiet mode to hide most messages", + 'long' => 'create-db', + 'cli_description' => "Create a new MySQL database", ], [ 'short' => 'd', @@ -71,21 +66,37 @@ class Options 'cli_description' => "Scaffold the routes and views for basic Laravel auth", ], [ - 'short' => 'n', + // 'short' => 'n', 'long' => 'node', 'cli_description' => "Run 'npm install' after creating the project", ], [ - 'long' => 'vue', - 'cli_description' => "Specify Vue as the frontend", + 'short' => 'x', + 'long' => 'mix', + 'cli_description' => "Run 'npm run dev' after creating the project", + ], + [ + 'short' => 'l', + 'long' => 'link', + 'cli_description' => "Create a Valet link to the project directory", + ], + [ + 'short' => 's', + 'long' => 'secure', + 'cli_description' => "Generate and use an HTTPS cert with Valet", + ], + [ + 'long' => 'full', + 'cli_description' => "Shortcut of --create-db --link --secure --auth --node --mix", ], [ - 'long' => 'react', - 'cli_description' => "Specify React as the frontend", + 'long' => 'with-output', + 'cli_description' => "Show command line output from shell commands", ], [ - 'long' => 'bootstrap', - 'cli_description' => "Specify Bootstrap as the frontend", + 'short' => 'q', + 'long' => 'quiet', + 'cli_description' => "Use quiet mode to hide most messages from lambo", ], ]; diff --git a/app/Shell.php b/app/Shell.php deleted file mode 100644 index 7a3ee0a3..00000000 --- a/app/Shell.php +++ /dev/null @@ -1,48 +0,0 @@ -rootPath = $config->get('lambo.store.root_path'); - $this->projectPath = $config->get('lambo.store.project_path'); - } - - public function execInRoot($command) - { - return $this->exec("cd {$this->rootPath} && $command"); - } - - public function execInProject($command) - { - return $this->exec("cd {$this->projectPath} && $command"); - } - - protected function exec($command) - { - $process = app()->make(Process::class, [ - 'command' => $command, - ]); - - $process->setTimeout(null); - - // @todo resolve this - $process->run(function ($type, $buffer) /*use ($showOutput)*/ { - echo $buffer; - - // if (Process::ERR === $type) { - // echo 'ERR > ' . $buffer; - // } elseif ($showOutput) { - // echo $buffer; - // } - }); - } -} diff --git a/app/Shell/ColorOutputFormatter.php b/app/Shell/ColorOutputFormatter.php new file mode 100644 index 00000000..6554b96e --- /dev/null +++ b/app/Shell/ColorOutputFormatter.php @@ -0,0 +1,21 @@ + RUN %s"; + } + + public function getErrorMessageFormat(): string + { + return " ERR %s"; + } + + public function getMessageFormat(): string + { + return " OUT %s"; + } +} diff --git a/app/Shell/ConsoleOutputFormatter.php b/app/Shell/ConsoleOutputFormatter.php new file mode 100644 index 00000000..3e7abb11 --- /dev/null +++ b/app/Shell/ConsoleOutputFormatter.php @@ -0,0 +1,26 @@ +getStartMessageFormat(), $message); + } + + public function progress(string $buffer, bool $error) + { + if ($error) { + return rtrim(sprintf($this->getErrorMessageFormat(), $buffer)); + } + + return rtrim(sprintf($this->getMessageFormat(), $buffer)); + } + + abstract function getStartMessageFormat(): string; + + abstract function getErrorMessageFormat(): string; + + abstract function getMessageFormat(): string; +} diff --git a/app/Shell/PlainOutputFormatter.php b/app/Shell/PlainOutputFormatter.php new file mode 100644 index 00000000..8f32afaf --- /dev/null +++ b/app/Shell/PlainOutputFormatter.php @@ -0,0 +1,21 @@ +rootPath = $config->get('lambo.store.root_path'); + $this->projectPath = $config->get('lambo.store.project_path'); + } + + public function execInRoot($command) + { + return $this->exec("cd {$this->rootPath} && $command", $command); + } + + public function execInProject($command) + { + return $this->exec("cd {$this->projectPath} && $command", $command); + } + + public function getOutputFormatter() + { + return app('console')->option('no-ansi') + ? new PlainOutputFormatter + : new ColorOutputFormatter; + } + + public function buildProcess($command): Process + { + $process = app()->make(Process::class, [ + 'command' => $command, + ]); + $process->setTimeout(null); + return $process; + } + + protected function exec($command, $description) + { + $showConsoleOutput = config('lambo.store.with_output'); + $out = app(\Symfony\Component\Console\Output\ConsoleOutput::class); + + $outputFormatter = $this->getOutputFormatter(); + $out->writeln($outputFormatter->start($description)); + + $process = $this->buildProcess($command); + $process->run(function ($type, $buffer) use ($out, $outputFormatter, $showConsoleOutput) { + if (empty($buffer) || $buffer === PHP_EOL) { + return; + } + + if (Process::ERR === $type || $showConsoleOutput) { + $out->writeln( + $outputFormatter->progress( + $buffer, + Process::ERR === $type + ) + ); + } + }); + + return $process; + } +} diff --git a/app/Utilities.php b/app/Utilities.php deleted file mode 100644 index 1b5bf9fb..00000000 --- a/app/Utilities.php +++ /dev/null @@ -1,11 +0,0 @@ -make(Illuminate\Contracts\Console\Kernel::class); +$app->bind('Symfony\Component\Console\Output\ConsoleOutput', function () { + return new Symfony\Component\Console\Output\ConsoleOutput; +}); + + $status = $kernel->handle( $input = new Symfony\Component\Console\Input\ArgvInput, - new Symfony\Component\Console\Output\ConsoleOutput + $app->make('Symfony\Component\Console\Output\ConsoleOutput') ); /* diff --git a/readme.md b/readme.md index da9ca2da..837df98d 100644 --- a/readme.md +++ b/readme.md @@ -90,7 +90,7 @@ There are also a few optional behaviors based on the parameters you pass (or def lambo new superApplication --auth ``` -- `-n` or `--node` to run `yarn` if installed, otherwise runs `npm install` after creating the project +- `--node` to run `yarn` if installed, otherwise runs `npm install` after creating the project ```bash lambo new superApplication --node @@ -147,12 +147,6 @@ There are also a few optional behaviors based on the parameters you pass (or def ### Commands -- `make-config` creates a config file so you don't have to pass the parameters every time you use Lambo - - ```bash - lambo make-config - ``` - - `edit-config` edits your config file ```bash diff --git a/storage/framework/cache/facade-1a2f843361f87bd19bd57e0f1cb9c778ea2d3e08.php b/storage/framework/cache/facade-1a2f843361f87bd19bd57e0f1cb9c778ea2d3e08.php deleted file mode 100644 index 4fa34496..00000000 --- a/storage/framework/cache/facade-1a2f843361f87bd19bd57e0f1cb9c778ea2d3e08.php +++ /dev/null @@ -1,21 +0,0 @@ -silentDevScript = $this->mock(SilentDevScript::class); + $this->shell = $this->mock(Shell::class); + } + + /** @test */ + function it_compiles_project_assets_and_hides_console_output() + { + Config::set('lambo.store.mix', true); + + $this->silentDevScript->shouldReceive('add') + ->once() + ->globally() + ->ordered(); + + $this->shell->shouldReceive('execInProject') + ->with('npm run dev --silent') + ->once() + ->andReturn(FakeProcess::success()) + ->globally() + ->ordered(); + + $this->silentDevScript->shouldReceive('remove') + ->once() + ->globally() + ->ordered(); + + app(CompileAssets::class)(); + } + + /** @test */ + function it_compiles_project_assets_and_shows_console_output() + { + Config::set('lambo.store.mix', true); + Config::set('lambo.store.with_output', true); + + $this->silentDevScript->shouldReceive('add') + ->once() + ->globally() + ->ordered(); + + $this->shell->shouldReceive('execInProject') + ->with('npm run dev') + ->once() + ->andReturn(FakeProcess::success()) + ->globally() + ->ordered(); + + $this->silentDevScript->shouldReceive('remove') + ->once() + ->globally() + ->ordered(); + + app(CompileAssets::class)(); + } + + /** @test */ + function it_skips_asset_compilation_if_it_is_not_requested() + { + $this->silentDevScript = $this->spy(SilentDevScript::class); + $this->shell = $this->spy(Shell::class); + + Config::set('lambo.store.mix', false); + + app(CompileAssets::class)(); + + $this->silentDevScript->shouldNotHaveReceived('add'); + $this->shell->shouldNotHaveReceived('execInProject'); + $this->silentDevScript->shouldNotHaveReceived('remove'); + } + + /** @test */ + function it_throws_an_exception_if_asset_compilation_fails() + { + Config::set('lambo.store.mix', true); + + $this->silentDevScript->shouldReceive('add') + ->once() + ->globally() + ->ordered(); + + $command = 'npm run dev --silent'; + $this->shell->shouldReceive('execInProject') + ->with($command) + ->once() + ->andReturn(FakeProcess::fail($command)) + ->globally() + ->ordered(); + + $this->expectException(Exception::class); + + app(CompileAssets::class)(); + } +} diff --git a/tests/Feature/ConfigureFrontendFrameworkTest.php b/tests/Feature/ConfigureFrontendFrameworkTest.php new file mode 100644 index 00000000..0919c53e --- /dev/null +++ b/tests/Feature/ConfigureFrontendFrameworkTest.php @@ -0,0 +1,81 @@ +shell = $this->mock(Shell::class); + $this->laravelUi = $this->mock(LaravelUi::class); + } + + /** @test */ + function it_installs_the_specified_front_end_framework() + { + Config::set('lambo.store.frontend', 'foo-frontend'); + + $this->laravelUi->shouldReceive('install') + ->once() + ->globally() + ->ordered(); + + $this->shell->shouldReceive('execInProject') + ->with('php artisan ui foo-frontend') + ->once() + ->andReturn(FakeProcess::success()) + ->globally() + ->ordered(); + + app(ConfigureFrontendFramework::class)(); + } + + /** @test */ + function it_does_not_install_a_frontend_framework_when_none_is_specified() + { + $shell = $this->spy(Shell::class); + $laravelUi = $this->spy(LaravelUi::class); + + $this->assertEmpty(Config::get('lambo.store.frontend')); + + app(ConfigureFrontendFramework::class); + + $laravelUi->shouldNotHaveReceived('install'); + $shell->shouldNotHaveReceived('execInProject'); + } + + /** @test */ + function it_throws_and_exception_if_the_ui_framework_installation_fails() + { + Config::set('lambo.store.frontend', 'foo-frontend'); + + $this->laravelUi->shouldReceive('install') + ->once() + ->globally() + ->ordered(); + + $command = 'php artisan ui foo-frontend'; + $this->shell->shouldReceive('execInProject') + ->with($command) + ->once() + ->andReturn(FakeProcess::fail($command)) + ->globally() + ->ordered(); + + $this->expectException(Exception::class); + + app(ConfigureFrontendFramework::class)(); + } +} diff --git a/tests/Feature/CreateDatabaseTest.php b/tests/Feature/CreateDatabaseTest.php new file mode 100644 index 00000000..fcac8a94 --- /dev/null +++ b/tests/Feature/CreateDatabaseTest.php @@ -0,0 +1,83 @@ +shell = $this->mock(Shell::class); + } + + /** @test */ + function it_creates_a_mysql_database() + { + Config::set('lambo.store.create_database', true); + Config::set('lambo.store.database_username', 'user'); + Config::set('lambo.store.database_password', 'password'); + Config::set('lambo.store.database_name', 'database_name'); + + $this->shell->shouldReceive('execInProject') + ->with('mysql --user=user --password=password -e "CREATE DATABASE IF NOT EXISTS database_name";') + ->once() + ->andReturn(FakeProcess::success()); + + $this->assertStringContainsString(Config::get('lambo.store.database_name'), app(CreateDatabase::class)()); + } + + /** @test */ + function it_checks_if_mysql_is_installed() + { + $executableFinder = $this->mock(ExecutableFinder::class); + + Config::set('lambo.store.create_database', true); + Config::set('lambo.store.database_username', 'user'); + Config::set('lambo.store.database_password', 'password'); + Config::set('lambo.store.database_name', 'database_name'); + + $executableFinder->shouldReceive('find') + ->with('mysql') + ->once() + ->andReturn(null); + + $this->assertEquals('MySQL does not seem to be installed. Skipping new database creation.', app(CreateDatabase::class)()); + } + + /** + * @todo do we need to test that database creation only happens when MySQL is the configured database? + * @test + */ + function it_only_runs_when_mysql_is_the_configured_database() + { + $this->markTestSkipped('*** @TODO: Add Test: "it only runs when mysql is the configured database" ***'); + } + + /** @test */ + function it_throws_an_exception_if_database_creation_fails() + { + Config::set('lambo.store.create_database', true); + Config::set('lambo.store.database_username', 'user'); + Config::set('lambo.store.database_password', 'password'); + Config::set('lambo.store.database_name', 'database_name'); + + $this->shell->shouldReceive('execInProject') + ->with('mysql --user=user --password=password -e "CREATE DATABASE IF NOT EXISTS database_name";') + ->once() + ->andReturn(FakeProcess::fail('mysql --user=user --password=password -e "CREATE DATABASE IF NOT EXISTS database_name";')); + + $this->expectException(Exception::class); + + app(CreateDatabase::class)(); + } +} diff --git a/tests/Feature/CustomizeDotEnvTest.php b/tests/Feature/CustomizeDotEnvTest.php index 06b33d90..7b869d86 100644 --- a/tests/Feature/CustomizeDotEnvTest.php +++ b/tests/Feature/CustomizeDotEnvTest.php @@ -3,13 +3,50 @@ namespace Tests\Feature; use App\Actions\CustomizeDotEnv; +use Illuminate\Support\Facades\Config; +use Illuminate\Support\Facades\File; use Tests\TestCase; class CustomizeDotEnvTest extends TestCase { + /** @test */ + function it_saves_the_customized_dot_env_files() + { + app()->bind('console', function () { + return new class{ + public function comment(){} + public function info(){} + }; + }); + + Config::set('lambo.store.project_name', 'my-project'); + Config::set('lambo.store.database_name', 'my_project'); + Config::set('lambo.store.project_url', 'http://my-project.example.com'); + Config::set('lambo.store.database_username', 'username'); + Config::set('lambo.store.database_password', 'password'); + Config::set('lambo.store.project_path', '/some/project/path'); + + $originalDotEnv = File::get(base_path('tests/Feature/Fixtures/.env.original')); + $customizedDotEnv = File::get(base_path('tests/Feature/Fixtures/.env.customized')); + + File::shouldReceive('get') + ->once()->with('/some/project/path/.env.example') + ->andReturn($originalDotEnv); + + File::shouldReceive('put') + ->with('/some/project/path/.env.example', $customizedDotEnv); + + File::shouldReceive('put') + ->with('/some/project/path/.env', $customizedDotEnv); + + (new CustomizeDotEnv)(); + } + /** @test */ function it_replaces_static_strings() { + config()->set('lambo.store.database_username', 'root'); + $customizeDotEnv = new CustomizeDotEnv; $contents = "DB_USERNAME=previous"; $contents = $customizeDotEnv->customize($contents); @@ -19,6 +56,8 @@ function it_replaces_static_strings() /** @test */ function un_targeted_lines_are_unchanged() { + config()->set('lambo.store.database_username', 'root'); + $customizeDotEnv = new CustomizeDotEnv; $contents = "DB_USERNAME=previous\nDONT_TOUCH_ME=cant_touch_me"; $contents = $customizeDotEnv->customize($contents); @@ -42,23 +81,4 @@ function line_breaks_remain() $contents = $customizeDotEnv->customize($contents); $this->assertEquals("A=B\n\nC=D", $contents); } - - /** @test */ - function it_replaces_dashes_with_underscores_in_database_names() - { - config()->set('lambo.store.project_name', 'with-dashes'); - - $customizeDotEnv = new CustomizeDotEnv; - $contents = "DB_DATABASE=previous"; - $contents = $customizeDotEnv->customize($contents); - $this->assertEquals("DB_DATABASE=with_dashes", $contents); - } - - /** @test */ - function it_uses_passed_database_name_if_passed() - { - $this->markTestIncomplete('@todo'); - - - } } diff --git a/tests/Feature/Fakes/FakeProcess.php b/tests/Feature/Fakes/FakeProcess.php new file mode 100644 index 00000000..79e7edf8 --- /dev/null +++ b/tests/Feature/Fakes/FakeProcess.php @@ -0,0 +1,35 @@ +isSuccessful = $isSuccessful; + $this->failedCommand = $failedCommand; + } + + public function isSuccessful() + { + return $this->isSuccessful; + } + + public function getCommandLine() + { + return $this->failedCommand; + } +} diff --git a/tests/Feature/Fixtures/.env.customized b/tests/Feature/Fixtures/.env.customized new file mode 100644 index 00000000..fb499c1d --- /dev/null +++ b/tests/Feature/Fixtures/.env.customized @@ -0,0 +1,6 @@ +APP_NAME=my-project +APP_URL=http://my-project.example.com + +DB_DATABASE=my_project +DB_USERNAME=username +DB_PASSWORD=password diff --git a/tests/Feature/Fixtures/.env.original b/tests/Feature/Fixtures/.env.original new file mode 100644 index 00000000..b0a5f9a2 --- /dev/null +++ b/tests/Feature/Fixtures/.env.original @@ -0,0 +1,6 @@ +APP_NAME=Laravel +APP_URL=http://my-project.example.com + +DB_DATABASE=laravel +DB_USERNAME=root +DB_PASSWORD= diff --git a/tests/Feature/Fixtures/composer-with-laravel-ui.json b/tests/Feature/Fixtures/composer-with-laravel-ui.json new file mode 100644 index 00000000..ad373d7e --- /dev/null +++ b/tests/Feature/Fixtures/composer-with-laravel-ui.json @@ -0,0 +1,5 @@ +{ + "require": { + "laravel/ui": "^1.0" + } +} diff --git a/tests/Feature/Fixtures/composer-without-laravel-ui.json b/tests/Feature/Fixtures/composer-without-laravel-ui.json new file mode 100644 index 00000000..df8dd9cf --- /dev/null +++ b/tests/Feature/Fixtures/composer-without-laravel-ui.json @@ -0,0 +1,4 @@ +{ + "require": { + } +} diff --git a/tests/Feature/Fixtures/package-silent.json b/tests/Feature/Fixtures/package-silent.json new file mode 100644 index 00000000..84cd72d8 --- /dev/null +++ b/tests/Feature/Fixtures/package-silent.json @@ -0,0 +1 @@ +{"scripts":{"development":"cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"}} diff --git a/tests/Feature/Fixtures/package.json b/tests/Feature/Fixtures/package.json new file mode 100644 index 00000000..be5a15de --- /dev/null +++ b/tests/Feature/Fixtures/package.json @@ -0,0 +1,5 @@ +{ + "scripts": { + "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js" + } +} diff --git a/tests/Feature/GenerateAppKeyTest.php b/tests/Feature/GenerateAppKeyTest.php new file mode 100644 index 00000000..96d489e2 --- /dev/null +++ b/tests/Feature/GenerateAppKeyTest.php @@ -0,0 +1,44 @@ +shell = $this->mock(Shell::class); + } + + /** @test */ + function it_generates_a_new_app_key() + { + $this->shell->shouldReceive('execInProject') + ->with('php artisan key:generate') + ->once() + ->andReturn(FakeProcess::success()); + + app(GenerateAppKey::class)(); + } + + /** @test */ + function it_throws_an_exception_if_new_app_key_generation_fails() + { + $this->shell->shouldReceive('execInProject') + ->with('php artisan key:generate') + ->once() + ->andReturn(FakeProcess::fail('php artisan key:generate')); + + $this->expectException(Exception::class); + + app(GenerateAppKey::class)(); + } +} diff --git a/tests/Feature/InitializeGitRepoTest.php b/tests/Feature/InitializeGitRepoTest.php new file mode 100644 index 00000000..edbc897a --- /dev/null +++ b/tests/Feature/InitializeGitRepoTest.php @@ -0,0 +1,101 @@ +shell = $this->mock(Shell::class); + } + + /** @test */ + function it_initialises_the_projects_git_repository() + { + Config::set('lambo.store.commit_message', 'Initial commit'); + + $this->shell->shouldReceive('execInProject') + ->with('git init') + ->once() + ->andReturn(FakeProcess::success()); + + $this->shell->shouldReceive('execInProject') + ->with('git add .') + ->once() + ->andReturn(FakeProcess::success()); + + $this->shell->shouldReceive('execInProject') + ->with('git commit -m "' . 'Initial commit' . '"') + ->once() + ->andReturn(FakeProcess::success()); + + app(InitializeGitRepo::class)(); + } + + /** @test */ + function it_throws_an_exception_if_git_init_fails() + { + $this->shell->shouldReceive('execInProject') + ->with('git init') + ->once() + ->andReturn(FakeProcess::fail('git init')); + + $this->expectException(Exception::class); + + app(InitializeGitRepo::class)(); + } + + /** @test */ + function it_throws_an_exception_if_git_add_fails() + { + $this->shell->shouldReceive('execInProject') + ->with('git init') + ->once() + ->andReturn(FakeProcess::success()); + + $this->shell->shouldReceive('execInProject') + ->with('git add .') + ->once() + ->andReturn(FakeProcess::fail('git add .')); + + $this->expectException(Exception::class); + + app(InitializeGitRepo::class)(); + } + + /** @test */ + function it_throws_an_exception_if_git_commit_fails() + { + Config::set('lambo.store.commit_message', 'Initial commit'); + + $command = 'git init'; + $this->shell->shouldReceive('execInProject') + ->with($command) + ->once() + ->andReturn(FakeProcess::success()); + + $this->shell->shouldReceive('execInProject') + ->with('git add .') + ->once() + ->andReturn(FakeProcess::success()); + + $this->shell->shouldReceive('execInProject') + ->with('git commit -m "Initial commit"') + ->once() + ->andReturn(FakeProcess::fail('git commit -m "Initial commit"')); + + $this->expectException(Exception::class); + + app(InitializeGitRepo::class)(); + } +} diff --git a/tests/Feature/InstallNpmDependenciesTest.php b/tests/Feature/InstallNpmDependenciesTest.php new file mode 100644 index 00000000..8f297480 --- /dev/null +++ b/tests/Feature/InstallNpmDependenciesTest.php @@ -0,0 +1,65 @@ +shell = $this->mock(Shell::class); + } + + /** @test */ + function it_installs_npm_dependencies() + { + Config::set('lambo.store.node', true); + Config::set('lambo.store.with-output', false); + + $this->shell->shouldReceive('execInProject') + ->with("npm install --silent") + ->once() + ->andReturn(FakeProcess::success()); + + app(InstallNpmDependencies::class)(); + } + + /** @test */ + function it_installs_npm_dependencies_and_shows_console_output() + { + Config::set('lambo.store.node', true); + Config::set('lambo.store.with-output', true); + + $this->shell->shouldReceive('execInProject') + ->with("npm install") + ->once() + ->andReturn(FakeProcess::success()); + + app(InstallNpmDependencies::class)(); + } + + /** @test */ + function it_throws_an_exception_if_npm_install_fails() + { + Config::set('lambo.store.node', true); + Config::set('lambo.store.with-output', false); + + $this->shell->shouldReceive('execInProject') + ->with('npm install --silent') + ->once() + ->andReturn(FakeProcess::fail('npm install --silent')); + + $this->expectException(Exception::class); + + app(InstallNpmDependencies::class)(); + } +} diff --git a/tests/Feature/LaravelUiTest.php b/tests/Feature/LaravelUiTest.php new file mode 100644 index 00000000..230413d5 --- /dev/null +++ b/tests/Feature/LaravelUiTest.php @@ -0,0 +1,82 @@ +shell = $this->mock(Shell::class); + } + + /** @test */ + function it_installs_laravel_ui() + { + Config::set('lambo.store.project_path', '/some/project/path'); + + $composerJsonFixture = File::get(base_path('tests/Feature/Fixtures/composer-without-laravel-ui.json')); + + File::shouldReceive('get') + ->with('/some/project/path/composer.json') + ->once() + ->andReturn($composerJsonFixture) + ->globally() + ->ordered(); + + $this->shell->shouldReceive('execInProject') + ->with('composer require laravel/ui --quiet') + ->once() + ->andReturn(FakeProcess::success()); + + app(LaravelUi::class)->install(); + } + + /** @test */ + function it_does_not_install_laravel_ui_if_it_is_already_present() + { + $shell = $this->spy(Shell::class); + + Config::set('lambo.store.project_path', '/some/project/path'); + + $composerJsonFixture = File::get(base_path('tests/Feature/Fixtures/composer-with-laravel-ui.json')); + + File::shouldReceive('get') + ->with('/some/project/path/composer.json') + ->once() + ->andReturn($composerJsonFixture) + ->globally() + ->ordered(); + + app(LaravelUi::class)->install(); + + $shell->shouldNotHaveReceived('execInProject'); + } + + /** @test */ + function it_throws_an_exception_if_laravel_ui_fails_to_install() + { + File::shouldReceive('get'); + + Config::set('lambo.store.project_path', '/some/project/path'); + + $this->shell->shouldReceive('execInProject') + ->with('composer require laravel/ui --quiet') + ->once() + ->andReturn(FakeProcess::fail('composer require laravel/ui --quiet')); + + $this->expectException(Exception::class); + + app(LaravelUi::class)->install(); + } +} diff --git a/tests/Feature/OpenInBrowserTest.php b/tests/Feature/OpenInBrowserTest.php new file mode 100644 index 00000000..1bbce671 --- /dev/null +++ b/tests/Feature/OpenInBrowserTest.php @@ -0,0 +1,85 @@ +shell = $this->mock(Shell::class); + $this->environment = $this->mock('alias:App\Environment'); + } + + /** @test */ + function it_opens_the_project_homepage_using_the_specified_browser_on_mac() + { + Config::set('lambo.store.browser', '/Applications/my/browser.app'); + Config::set('lambo.store.project_url', 'http://my-project.test'); + + $this->environment->shouldReceive('isMac') + ->once() + ->andReturn(true); + + $this->shell->shouldReceive('execInProject') + ->once() + ->with('open -a "/Applications/my/browser.app" "http://my-project.test"'); + + app(OpenInBrowser::class)(); + } + + /** @test */ + function it_opens_the_project_homepage_using_valet_open_when_no_browser_is_specified_on_mac() + { + $this->assertEmpty(Config::get('lambo.store.browser')); + + $this->environment->shouldReceive('isMac') + ->once() + ->andReturn(true); + + $this->shell->shouldReceive('execInProject') + ->once() + ->with('valet open'); + + app(OpenInBrowser::class)(); + } + + /** @test */ + function it_uses_valet_open_when_not_running_on_mac() + { + $this->environment->shouldReceive('isMac') + ->once() + ->andReturn(false); + + $this->shell->shouldReceive('execInProject') + ->once() + ->with('valet open'); + + app(OpenInBrowser::class)(); + } + + /** @test */ + function it_ignores_the_specified_browser_when_not_running_on_mac() + { + Config::set('lambo.store.browser', '/path/to/a/browser'); + Config::set('lambo.store.project_url', 'http://my-project.test'); + + $this->environment->shouldReceive('isMac') + ->once() + ->andReturn(false); + + $this->shell->shouldReceive('execInProject') + ->once() + ->with('valet open'); + + app(OpenInBrowser::class)(); + } +} diff --git a/tests/Feature/OpenInEditorTest.php b/tests/Feature/OpenInEditorTest.php new file mode 100644 index 00000000..b2ce01b2 --- /dev/null +++ b/tests/Feature/OpenInEditorTest.php @@ -0,0 +1,37 @@ +mock(Shell::class); + + Config::set('lambo.store.editor', 'my-editor'); + + $shell->shouldReceive('execInProject') + ->with("my-editor .") + ->once(); + + app(OpenInEditor::class)(); + } + + /** @test */ + function it_does_not_open_the_project_folder_if_an_editor_is_not_specified() + { + $shell = $this->spy(Shell::class); + + $this->assertEmpty(Config::get('lambo.store.editor')); + + app(OpenInEditor::class)(); + + $shell->shouldNotHaveReceived('execInProject'); + } +} diff --git a/tests/Feature/RunAfterScriptTest.php b/tests/Feature/RunAfterScriptTest.php new file mode 100644 index 00000000..543bae95 --- /dev/null +++ b/tests/Feature/RunAfterScriptTest.php @@ -0,0 +1,66 @@ +shell = $this->mock(Shell::class); + } + + /** @test */ + function it_runs_the_after_script_if_one_exists() + { + Config::set('home_dir', '/my/home/dir'); + + File::shouldReceive('exists') + ->with('/my/home/dir/.lambo/after') + ->andReturn(true) + ->globally() + ->ordered(); + + $this->shell->shouldReceive('execInProject') + ->with('sh /my/home/dir/.lambo/after') + ->once() + ->andReturn(FakeProcess::success()) + ->globally() + ->ordered(); + + app(RunAfterScript::class)(); + } + + /** @test */ + function it_throws_an_exception_if_the_after_script_fails() + { + Config::set('home_dir', '/my/home/dir'); + + File::shouldReceive('exists') + ->with('/my/home/dir/.lambo/after') + ->andReturn(true) + ->globally() + ->ordered(); + + $this->shell->shouldReceive('execInProject') + ->with('sh /my/home/dir/.lambo/after') + ->once() + ->andReturn(FakeProcess::fail('sh /my/home/dir/.lambo/after')) + ->globally() + ->ordered(); + + $this->expectException(Exception::class); + + app(RunAfterScript::class)(); + } +} diff --git a/tests/Feature/RunLaravelInstallerTest.php b/tests/Feature/RunLaravelInstallerTest.php new file mode 100644 index 00000000..e303e92f --- /dev/null +++ b/tests/Feature/RunLaravelInstallerTest.php @@ -0,0 +1,110 @@ +shell = $this->mock(Shell::class); + } + + /** @test */ + function it_runs_the_laravel_installer() + { + collect([ + [ + 'command' => 'laravel new my-project --quiet', + 'lambo.store.auth' => false, + 'lambo.store.dev' => false, + 'lambo.store.with_output' => false, + ], + [ + 'command' => 'laravel new my-project', + 'lambo.store.auth' => false, + 'lambo.store.dev' => false, + 'lambo.store.with_output' => true, + ], + [ + 'command' => 'laravel new my-project --dev --quiet', + 'lambo.store.auth' => false, + 'lambo.store.dev' => true, + 'lambo.store.with_output' => false, + ], + [ + 'command' => 'laravel new my-project --dev', + 'lambo.store.auth' => false, + 'lambo.store.dev' => true, + 'lambo.store.with_output' => true, + ], + [ + 'command' => 'laravel new my-project --auth --quiet', + 'lambo.store.auth' => true, + 'lambo.store.dev' => false, + 'lambo.store.with_output' => false, + ], + [ + 'command' => 'laravel new my-project --auth', + 'lambo.store.auth' => true, + 'lambo.store.dev' => false, + 'lambo.store.with_output' => true, + ], + [ + 'command' => 'laravel new my-project --auth --dev --quiet', + 'lambo.store.auth' => true, + 'lambo.store.dev' => true, + 'lambo.store.with_output' => false, + ], + + [ + 'command' => 'laravel new my-project --auth --dev', + 'lambo.store.auth' => true, + 'lambo.store.dev' => true, + 'lambo.store.with_output' => true, + ], + ])->each(function ($options) { + Config::set('lambo.store.project_name', 'my-project'); + Config::set('lambo.store.auth', $options['lambo.store.auth']); + Config::set('lambo.store.dev', $options['lambo.store.dev']); + Config::set('lambo.store.with_output', $options['lambo.store.with_output']); + + $this->runLaravelInstaller($options['command']); + }); + } + + /** @test */ + function it_throws_an_exception_if_the_laravel_installer_fails() + { + Config::set('lambo.store.project_name', 'my-project'); + Config::set('lambo.store.auth', false); + Config::set('lambo.store.dev', false); + Config::set('lambo.store.with_output', false); + + $this->shell->shouldReceive('execInRoot') + ->andReturn(FakeProcess::fail('failed command')); + + $this->expectException(Exception::class); + + app(RunLaravelInstaller::class)(); + } + + function runLaravelInstaller(string $expectedCommand) + { + $this->shell->shouldReceive('execInRoot') + ->with($expectedCommand) + ->once() + ->andReturn(FakeProcess::success()); + + app(RunLaravelInstaller::class)(); + } +} diff --git a/tests/Feature/SilentDevScriptTest.php b/tests/Feature/SilentDevScriptTest.php new file mode 100644 index 00000000..5d6a4cef --- /dev/null +++ b/tests/Feature/SilentDevScriptTest.php @@ -0,0 +1,53 @@ +with('/some/project/path/package.json', '/some/project/path/package-original.json') + ->once() + ->globally() + ->ordered(); + + File::shouldReceive('get') + ->with('/some/project/path/package.json') + ->once() + ->andReturn($packageJson) + ->globally() + ->ordered(); + + File::shouldReceive('replace') + ->with('/some/project/path/package.json', trim($silentPackageJson)) + ->once() + ->globally() + ->ordered(); + + app(SilentDevScript::class)->add(); + } + + /** @test */ + function it_replaces_the_silent_compilation_script_with_the_original() + { + Config::set('lambo.store.project_path', '/some/project/path'); + + File::shouldReceive('move') + ->with('/some/project/path/package-original.json', '/some/project/path/package.json') + ->once(); + + app(SilentDevScript::class)->remove(); + } +} diff --git a/tests/Feature/ValetLinkTest.php b/tests/Feature/ValetLinkTest.php new file mode 100644 index 00000000..d94971a6 --- /dev/null +++ b/tests/Feature/ValetLinkTest.php @@ -0,0 +1,50 @@ +shell = $this->mock(Shell::class); + } + + /** @test */ + function it_runs_valet_link() + { + Config::set('lambo.store.valet_link', true); + + $this->shell->shouldReceive('execInProject') + ->with('valet link') + ->once() + ->andReturn(FakeProcess::success()); + + app(ValetLink::class)(); + } + + /** @test */ + function it_throws_an_exception_if_the_after_script_fails() + { + Config::set('lambo.store.valet_link', true); + + $command = 'valet link'; + $this->shell->shouldReceive('execInProject') + ->with($command) + ->once() + ->andReturn(FakeProcess::fail($command)); + + $this->expectException(Exception::class); + + app(ValetLink::class)(); + } +} diff --git a/tests/Feature/ValetSecureTest.php b/tests/Feature/ValetSecureTest.php new file mode 100644 index 00000000..3773f262 --- /dev/null +++ b/tests/Feature/ValetSecureTest.php @@ -0,0 +1,49 @@ +shell = $this->mock(Shell::class); + } + + /** @test */ + function it_runs_valet_link() + { + Config::set('lambo.store.valet_secure', true); + + $this->shell->shouldReceive('execInProject') + ->with('valet secure') + ->once() + ->andReturn(FakeProcess::success()); + + app(ValetSecure::class)(); + } + + /** @test */ + function it_throws_an_exception_if_the_after_script_fails() + { + Config::set('lambo.store.valet_secure', true); + + $this->shell->shouldReceive('execInProject') + ->with('valet secure') + ->once() + ->andReturn(FakeProcess::fail('valet secure')); + + $this->expectException(Exception::class); + + app(ValetSecure::class)(); + } +} diff --git a/tests/Feature/VerifyDependenciesTest.php b/tests/Feature/VerifyDependenciesTest.php new file mode 100644 index 00000000..7bebee3b --- /dev/null +++ b/tests/Feature/VerifyDependenciesTest.php @@ -0,0 +1,53 @@ +executableFinder = $this->mock(ExecutableFinder::class); + } + + /** @test */ + function it_checks_that_required_dependencies_are_available() + { + $this->executableFinder->shouldReceive('find') + ->with('dependencyA') + ->once() + ->andReturn('/path/to/dependencyA'); + + $this->executableFinder->shouldReceive('find') + ->with('dependencyB') + ->once() + ->andReturn('/path/to/dependencyB'); + + app(VerifyDependencies::class)(['dependencyA', 'dependencyB']); + } + + /** @test */ + function it_throws_and_exception_if_a_required_dependency_is_missing_missing() + { + $this->executableFinder->shouldReceive('find') + ->with('dependencyA') + ->once() + ->andReturn('/path/to/dependencyA'); + + $this->executableFinder->shouldReceive('find') + ->with('missingDependency') + ->once() + ->andReturn(null); + + $this->expectException(Exception::class); + + app(VerifyDependencies::class)(['dependencyA', 'missingDependency']); + } +} diff --git a/tests/Feature/VerifyPathAvailableTest.php b/tests/Feature/VerifyPathAvailableTest.php new file mode 100644 index 00000000..310c98e7 --- /dev/null +++ b/tests/Feature/VerifyPathAvailableTest.php @@ -0,0 +1,71 @@ +with('/some/filesystem/path') + ->once() + ->andReturn(true); + + File::shouldReceive('isDirectory') + ->with('/some/filesystem/path/my-project') + ->once() + ->andReturn(false); + + app(VerifyPathAvailable::class)(); + } + + /** @test */ + function it_throws_an_exception_if_the_root_path_is_not_available() + { + Config::set('lambo.store.root_path', '/non/existent/filesystem/path'); + + File::shouldReceive('isDirectory') + ->with('/non/existent/filesystem/path') + ->once() + ->andReturn(false); + + $this->expectException(Exception::class); + + app(VerifyPathAvailable::class)(); + } + + /** @test */ + function it_throws_an_exception_if_the_project_path_already_exists() + { + Config::set('lambo.store.root_path', '/some/filesystem/path'); + Config::set('lambo.store.project_path', '/some/filesystem/path/existing-directory'); + + File::shouldReceive('isDirectory') + ->with('/some/filesystem/path') + ->once() + ->andReturn(true) + ->globally() + ->ordered(); + + File::shouldReceive('isDirectory') + ->with('/some/filesystem/path/existing-directory') + ->once() + ->andReturn(true) + ->globally() + ->ordered();; + + $this->expectException(Exception::class); + + app( VerifyPathAvailable::class)(); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 50602b9b..71f2d05c 100755 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -7,4 +7,16 @@ abstract class TestCase extends BaseTestCase { use CreatesApplication; + + function setUp(): void + { + parent::setUp(); // TODO: Change the autogenerated stub + app()->bind('console', function () { + return new class { + public function comment($message = '') {} + public function info() {} + public function warn($message = '') {} + }; + }); + } }