diff --git a/src/Console/Commands/ServeCommand.php b/src/Console/Commands/ServeCommand.php
index bc738dac..804151fd 100644
--- a/src/Console/Commands/ServeCommand.php
+++ b/src/Console/Commands/ServeCommand.php
@@ -7,9 +7,12 @@
use Closure;
use Hyde\Hyde;
use Hyde\Facades\Config;
+use Illuminate\Support\Arr;
+use InvalidArgumentException;
use Hyde\RealtimeCompiler\ConsoleOutput;
use Illuminate\Support\Facades\Process;
use LaravelZero\Framework\Commands\Command;
+use Hyde\Publications\Commands\ValidatingCommand;
use function sprintf;
use function class_exists;
@@ -19,12 +22,16 @@
*
* @see https://github.com/hydephp/realtime-compiler
*/
-class ServeCommand extends Command
+class ServeCommand extends ValidatingCommand
{
/** @var string */
protected $signature = 'serve
{--host= : [default: "localhost"]}}
{--port= : [default: 8080]}
+ {--save-preview= : Should the served page be saved to disk? (Overrides config setting)}
+ {--dashboard= : Enable the realtime compiler dashboard. (Overrides config setting)}
+ {--pretty-urls= : Enable pretty URLs. (Overrides config setting)}
+ {--play-cdn= : Enable the Tailwind Play CDN. (Overrides config setting)}
';
/** @var string */
@@ -32,7 +39,7 @@ class ServeCommand extends Command
protected ConsoleOutput $console;
- public function handle(): int
+ public function safeHandle(): int
{
$this->configureOutput();
$this->printStartMessage();
@@ -46,14 +53,14 @@ public function handle(): int
return Command::SUCCESS;
}
- protected function getPortSelection(): int
+ protected function getHostSelection(): string
{
- return (int) ($this->option('port') ?: Config::getInt('hyde.server.port', 8080));
+ return (string) $this->option('host') ?: Config::getString('hyde.server.host', 'localhost');
}
- protected function getHostSelection(): string
+ protected function getPortSelection(): int
{
- return (string) $this->option('host') ?: Config::getString('hyde.server.host', 'localhost');
+ return (int) ($this->option('port') ?: Config::getInt('hyde.server.port', 8080));
}
protected function getExecutablePath(): string
@@ -68,9 +75,13 @@ protected function runServerProcess(string $command): void
protected function getEnvironmentVariables(): array
{
- return [
- 'HYDE_RC_REQUEST_OUTPUT' => ! $this->option('no-ansi'),
- ];
+ return Arr::whereNotNull([
+ 'HYDE_SERVER_REQUEST_OUTPUT' => ! $this->option('no-ansi'),
+ 'HYDE_SERVER_SAVE_PREVIEW' => $this->parseEnvironmentOption('save-preview'),
+ 'HYDE_SERVER_DASHBOARD' => $this->parseEnvironmentOption('dashboard'),
+ 'HYDE_PRETTY_URLS' => $this->parseEnvironmentOption('pretty-urls'),
+ 'HYDE_PLAY_CDN' => $this->parseEnvironmentOption('play-cdn'),
+ ]);
}
protected function configureOutput(): void
@@ -84,7 +95,7 @@ protected function printStartMessage(): void
{
$this->useBasicOutput()
? $this->output->writeln('Starting the HydeRC server... Press Ctrl+C to stop')
- : $this->console->printStartMessage($this->getHostSelection(), $this->getPortSelection());
+ : $this->console->printStartMessage($this->getHostSelection(), $this->getPortSelection(), $this->getEnvironmentVariables());
}
protected function getOutputHandler(): Closure
@@ -98,4 +109,31 @@ protected function useBasicOutput(): bool
{
return $this->option('no-ansi') || ! class_exists(ConsoleOutput::class);
}
+
+ protected function parseEnvironmentOption(string $name): ?string
+ {
+ $value = $this->option($name) ?? $this->checkArgvForOption($name);
+
+ if ($value !== null) {
+ return match ($value) {
+ 'true', '' => 'enabled',
+ 'false' => 'disabled',
+ default => throw new InvalidArgumentException(sprintf('Invalid boolean value for --%s option.', $name))
+ };
+ }
+
+ return null;
+ }
+
+ /** Fallback check so that an environment option without a value is acknowledged as true. */
+ protected function checkArgvForOption(string $name): ?string
+ {
+ if (isset($_SERVER['argv'])) {
+ if (in_array("--$name", $_SERVER['argv'], true)) {
+ return 'true';
+ }
+ }
+
+ return null;
+ }
}
diff --git a/src/Foundation/Internal/LoadConfiguration.php b/src/Foundation/Internal/LoadConfiguration.php
index b4b51bf4..10bf1418 100644
--- a/src/Foundation/Internal/LoadConfiguration.php
+++ b/src/Foundation/Internal/LoadConfiguration.php
@@ -6,9 +6,10 @@
use Phar;
use Illuminate\Contracts\Foundation\Application;
-use Illuminate\Contracts\Config\Repository as RepositoryContract;
+use Illuminate\Contracts\Config\Repository;
use Illuminate\Foundation\Bootstrap\LoadConfiguration as BaseLoadConfiguration;
+use function getenv;
use function array_merge;
use function dirname;
use function in_array;
@@ -30,7 +31,7 @@ protected function getConfigurationFiles(Application $app): array
}
/** Load the configuration items from all the files. */
- protected function loadConfigurationFiles(Application $app, RepositoryContract $repository): void
+ protected function loadConfigurationFiles(Application $app, Repository $repository): void
{
parent::loadConfigurationFiles($app, $repository);
@@ -39,7 +40,7 @@ protected function loadConfigurationFiles(Application $app, RepositoryContract $
$this->loadRuntimeConfiguration($app, $repository);
}
- private function mergeConfigurationFiles(RepositoryContract $repository): void
+ private function mergeConfigurationFiles(Repository $repository): void
{
// These files do commonly not need to be customized by the user, so to get them out of the way,
// we don't include them in the default project install.
@@ -49,7 +50,7 @@ private function mergeConfigurationFiles(RepositoryContract $repository): void
}
}
- private function mergeConfigurationFile(RepositoryContract $repository, string $file): void
+ private function mergeConfigurationFile(Repository $repository, string $file): void
{
// We of course want the user to be able to customize the config files,
// if they're present, so we'll merge their changes here.
@@ -78,18 +79,42 @@ private static function providePharSupportIfNeeded(array &$files): void
}
}
- private function loadRuntimeConfiguration(Application $app, RepositoryContract $repository): void
+ private function loadRuntimeConfiguration(Application $app, Repository $repository): void
{
- if ($app->runningInConsole() && isset($_SERVER['argv'])) {
- // Check if the `--pretty-urls` CLI argument is set, and if so, set the config value accordingly.
- if (in_array('--pretty-urls', $_SERVER['argv'], true)) {
- $repository->set('hyde.pretty_urls', true);
+ if ($app->runningInConsole()) {
+ if ($this->getArgv() !== null) {
+ $this->mergeCommandLineArguments($repository, '--pretty-urls', 'hyde.pretty_urls', true);
+ $this->mergeCommandLineArguments($repository, '--no-api', 'hyde.api_calls', false);
}
- // Check if the `--no-api` CLI argument is set, and if so, set the config value accordingly.
- if (in_array('--no-api', $_SERVER['argv'], true)) {
- $repository->set('hyde.api_calls', false);
- }
+ $this->mergeRealtimeCompilerEnvironment($repository, 'HYDE_SERVER_SAVE_PREVIEW', 'hyde.server.save_preview');
+ $this->mergeRealtimeCompilerEnvironment($repository, 'HYDE_SERVER_DASHBOARD', 'hyde.server.dashboard.enabled');
+ $this->mergeRealtimeCompilerEnvironment($repository, 'HYDE_PRETTY_URLS', 'hyde.pretty_urls');
+ $this->mergeRealtimeCompilerEnvironment($repository, 'HYDE_PLAY_CDN', 'hyde.use_play_cdn');
+ }
+ }
+
+ private function mergeCommandLineArguments(Repository $repository, string $argumentName, string $configKey, bool $value): void
+ {
+ if (in_array($argumentName, $this->getArgv(), true)) {
+ $repository->set($configKey, $value);
}
}
+
+ private function mergeRealtimeCompilerEnvironment(Repository $repository, string $environmentKey, string $configKey): void
+ {
+ if ($this->getEnv($environmentKey) !== false) {
+ $repository->set($configKey, $this->getEnv($environmentKey) === 'enabled');
+ }
+ }
+
+ protected function getArgv(): ?array
+ {
+ return $_SERVER['argv'] ?? null;
+ }
+
+ protected function getEnv(string $name): string|false|null
+ {
+ return getenv($name);
+ }
}
diff --git a/tests/Feature/Commands/ServeCommandTest.php b/tests/Feature/Commands/ServeCommandTest.php
index 7112b461..7aa1cd8d 100644
--- a/tests/Feature/Commands/ServeCommandTest.php
+++ b/tests/Feature/Commands/ServeCommandTest.php
@@ -13,6 +13,8 @@
/**
* @covers \Hyde\Console\Commands\ServeCommand
+ *
+ * @see \Hyde\Framework\Testing\Unit\ServeCommandOptionsUnitTest
*/
class ServeCommandTest extends TestCase
{
@@ -144,7 +146,7 @@ public function test_hyde_serve_command_passes_through_process_output()
Process::shouldReceive('env')
->once()
- ->with(['HYDE_RC_REQUEST_OUTPUT' => false])
+ ->with(['HYDE_SERVER_REQUEST_OUTPUT' => false])
->andReturnSelf();
Process::shouldReceive('run')
diff --git a/tests/Unit/LoadConfigurationTest.php b/tests/Unit/LoadConfigurationTest.php
index d1125608..4358e8bf 100644
--- a/tests/Unit/LoadConfigurationTest.php
+++ b/tests/Unit/LoadConfigurationTest.php
@@ -15,22 +15,57 @@ class LoadConfigurationTest extends UnitTestCase
{
public function testItLoadsRuntimeConfiguration()
{
- $serverBackup = $_SERVER;
+ $app = new Application(getcwd());
- $_SERVER['argv'] = ['--pretty-urls', '--no-api'];
+ $loader = new LoadConfigurationTestClass([]);
+ $loader->bootstrap($app);
- $app = new Application(getcwd());
+ $this->assertFalse(config('hyde.pretty_urls'));
+ $this->assertNull(config('hyde.api_calls'));
- $loader = new LoadConfiguration();
+ $loader = new LoadConfigurationTestClass(['--pretty-urls', '--no-api']);
$loader->bootstrap($app);
$this->assertTrue(config('hyde.pretty_urls'));
$this->assertFalse(config('hyde.api_calls'));
+ }
+
+ public function testItLoadsRealtimeCompilerEnvironmentConfiguration()
+ {
+ (new LoadConfigurationEnvironmentTestClass(['HYDE_SERVER_DASHBOARD' => 'enabled']))->bootstrap(new Application(getcwd()));
+ $this->assertTrue(config('hyde.server.dashboard.enabled'));
- $_SERVER = $serverBackup;
+ (new LoadConfigurationEnvironmentTestClass(['HYDE_SERVER_DASHBOARD' => 'disabled']))->bootstrap(new Application(getcwd()));
+ $this->assertFalse(config('hyde.server.dashboard.enabled'));
+ }
+}
- $loader->bootstrap($app);
- $this->assertFalse(config('hyde.pretty_urls'));
- $this->assertNull(config('hyde.api_calls'));
+class LoadConfigurationTestClass extends LoadConfiguration
+{
+ protected array $argv;
+
+ public function __construct(array $argv)
+ {
+ $this->argv = $argv;
+ }
+
+ protected function getArgv(): ?array
+ {
+ return $this->argv;
+ }
+}
+
+class LoadConfigurationEnvironmentTestClass extends LoadConfiguration
+{
+ protected array $env;
+
+ public function __construct(array $env)
+ {
+ $this->env = $env;
+ }
+
+ protected function getEnv(string $name): string|false|null
+ {
+ return $this->env[$name];
}
}
diff --git a/tests/Unit/ServeCommandOptionsUnitTest.php b/tests/Unit/ServeCommandOptionsUnitTest.php
new file mode 100644
index 00000000..935d9793
--- /dev/null
+++ b/tests/Unit/ServeCommandOptionsUnitTest.php
@@ -0,0 +1,245 @@
+ 'localhost',
+ 'hyde.server.port' => 8080,
+ ]);
+ }
+
+ public function test_getHostSelection()
+ {
+ $this->assertSame('localhost', $this->getMock()->getHostSelection());
+ }
+
+ public function test_getHostSelection_withHostOption()
+ {
+ $this->assertSame('foo', $this->getMock(['host' => 'foo'])->getHostSelection());
+ }
+
+ public function test_getHostSelection_withConfigOption()
+ {
+ self::mockConfig(['hyde.server.host' => 'foo']);
+ $this->assertSame('foo', $this->getMock()->getHostSelection());
+ }
+
+ public function test_getHostSelection_withHostOptionAndConfigOption()
+ {
+ self::mockConfig(['hyde.server.host' => 'foo']);
+ $this->assertSame('bar', $this->getMock(['host' => 'bar'])->getHostSelection());
+ }
+
+ public function test_getPortSelection()
+ {
+ $this->assertSame(8080, $this->getMock()->getPortSelection());
+ }
+
+ public function test_getPortSelection_withPortOption()
+ {
+ $this->assertSame(8081, $this->getMock(['port' => 8081])->getPortSelection());
+ }
+
+ public function test_getPortSelection_withConfigOption()
+ {
+ self::mockConfig(['hyde.server.port' => 8082]);
+ $this->assertSame(8082, $this->getMock()->getPortSelection());
+ }
+
+ public function test_getPortSelection_withPortOptionAndConfigOption()
+ {
+ self::mockConfig(['hyde.server.port' => 8082]);
+ $this->assertSame(8081, $this->getMock(['port' => 8081])->getPortSelection());
+ }
+
+ public function test_getEnvironmentVariables()
+ {
+ $this->assertSame([
+ 'HYDE_SERVER_REQUEST_OUTPUT' => true,
+ ], $this->getMock()->getEnvironmentVariables());
+ }
+
+ public function test_getEnvironmentVariables_withNoAnsiOption()
+ {
+ $this->assertSame([
+ 'HYDE_SERVER_REQUEST_OUTPUT' => false,
+ ], $this->getMock(['no-ansi' => true])->getEnvironmentVariables());
+ }
+
+ public function testSavePreviewOptionPropagatesToEnvironmentVariables()
+ {
+ $command = $this->getMock(['save-preview' => 'false']);
+ $this->assertSame('disabled', $command->getEnvironmentVariables()['HYDE_SERVER_SAVE_PREVIEW']);
+
+ $command = $this->getMock(['save-preview' => 'true']);
+ $this->assertSame('enabled', $command->getEnvironmentVariables()['HYDE_SERVER_SAVE_PREVIEW']);
+
+ $command = $this->getMock(['save-preview' => '']);
+ $this->assertSame('enabled', $command->getEnvironmentVariables()['HYDE_SERVER_SAVE_PREVIEW']);
+
+ $command = $this->getMock(['save-preview' => null]);
+ $this->assertFalse(isset($command->getEnvironmentVariables()['HYDE_SERVER_SAVE_PREVIEW']));
+
+ $command = $this->getMock();
+ $this->assertFalse(isset($command->getEnvironmentVariables()['HYDE_SERVER_SAVE_PREVIEW']));
+ }
+
+ public function testDashboardOptionPropagatesToEnvironmentVariables()
+ {
+ $command = $this->getMock(['dashboard' => 'false']);
+ $this->assertSame('disabled', $command->getEnvironmentVariables()['HYDE_SERVER_DASHBOARD']);
+
+ $command = $this->getMock(['dashboard' => 'true']);
+ $this->assertSame('enabled', $command->getEnvironmentVariables()['HYDE_SERVER_DASHBOARD']);
+
+ $command = $this->getMock(['dashboard' => '']);
+ $this->assertSame('enabled', $command->getEnvironmentVariables()['HYDE_SERVER_DASHBOARD']);
+
+ $command = $this->getMock(['dashboard' => null]);
+ $this->assertFalse(isset($command->getEnvironmentVariables()['HYDE_SERVER_DASHBOARD']));
+
+ $command = $this->getMock();
+ $this->assertFalse(isset($command->getEnvironmentVariables()['HYDE_SERVER_DASHBOARD']));
+ }
+
+ public function testPrettyUrlsOptionPropagatesToEnvironmentVariables()
+ {
+ $command = $this->getMock(['pretty-urls' => 'false']);
+ $this->assertSame('disabled', $command->getEnvironmentVariables()['HYDE_PRETTY_URLS']);
+
+ $command = $this->getMock(['pretty-urls' => 'true']);
+ $this->assertSame('enabled', $command->getEnvironmentVariables()['HYDE_PRETTY_URLS']);
+
+ $command = $this->getMock(['pretty-urls' => '']);
+ $this->assertSame('enabled', $command->getEnvironmentVariables()['HYDE_PRETTY_URLS']);
+
+ $command = $this->getMock(['pretty-urls' => null]);
+ $this->assertFalse(isset($command->getEnvironmentVariables()['HYDE_PRETTY_URLS']));
+
+ $command = $this->getMock();
+ $this->assertFalse(isset($command->getEnvironmentVariables()['HYDE_PRETTY_URLS']));
+ }
+
+ public function testPlayCdnOptionPropagatesToEnvironmentVariables()
+ {
+ $command = $this->getMock(['play-cdn' => 'false']);
+ $this->assertSame('disabled', $command->getEnvironmentVariables()['HYDE_PLAY_CDN']);
+
+ $command = $this->getMock(['play-cdn' => 'true']);
+ $this->assertSame('enabled', $command->getEnvironmentVariables()['HYDE_PLAY_CDN']);
+
+ $command = $this->getMock(['play-cdn' => '']);
+ $this->assertSame('enabled', $command->getEnvironmentVariables()['HYDE_PLAY_CDN']);
+
+ $command = $this->getMock(['play-cdn' => null]);
+ $this->assertFalse(isset($command->getEnvironmentVariables()['HYDE_PLAY_CDN']));
+
+ $command = $this->getMock();
+ $this->assertFalse(isset($command->getEnvironmentVariables()['HYDE_PLAY_CDN']));
+ }
+
+ public function test_parseEnvironmentOption()
+ {
+ $command = $this->getMock(['foo' => 'true']);
+ $this->assertSame('enabled', $command->parseEnvironmentOption('foo'));
+
+ $command = $this->getMock(['foo' => 'false']);
+ $this->assertSame('disabled', $command->parseEnvironmentOption('foo'));
+ }
+
+ public function test_parseEnvironmentOption_withEmptyString()
+ {
+ $command = $this->getMock(['foo' => '']);
+ $this->assertSame('enabled', $command->parseEnvironmentOption('foo'));
+ }
+
+ public function test_parseEnvironmentOption_withNull()
+ {
+ $command = $this->getMock(['foo' => null]);
+ $this->assertNull($command->parseEnvironmentOption('foo'));
+ }
+
+ public function test_parseEnvironmentOption_withInvalidValue()
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('Invalid boolean value for --foo option.');
+
+ $command = $this->getMock(['foo' => 'bar']);
+ $command->parseEnvironmentOption('foo');
+ }
+
+ public function test_checkArgvForOption()
+ {
+ $serverBackup = $_SERVER;
+
+ $_SERVER['argv'] = ['--pretty-urls'];
+
+ $command = $this->getMock();
+
+ $this->assertSame('true', $command->checkArgvForOption('pretty-urls'));
+ $this->assertSame(null, $command->checkArgvForOption('dashboard'));
+
+ $_SERVER = $serverBackup;
+ }
+
+ protected function getMock(array $options = []): ServeCommandMock
+ {
+ return new ServeCommandMock($options);
+ }
+}
+
+/**
+ * @method getHostSelection
+ * @method getPortSelection
+ * @method getEnvironmentVariables
+ * @method parseEnvironmentOption(string $name)
+ * @method checkArgvForOption(string $name)
+ */
+class ServeCommandMock extends ServeCommand
+{
+ public function __construct(array $options = [])
+ {
+ parent::__construct();
+
+ $this->input = new InputMock($options);
+ }
+
+ public function __call($method, $parameters)
+ {
+ return call_user_func_array([$this, $method], $parameters);
+ }
+
+ public function option($key = null)
+ {
+ return $this->input->getOption($key);
+ }
+}
+
+class InputMock
+{
+ protected array $options;
+
+ public function __construct(array $options = [])
+ {
+ $this->options = $options;
+ }
+
+ public function getOption(string $key)
+ {
+ return $this->options[$key] ?? null;
+ }
+}