Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
56 / 56
100.00% covered (success)
100.00%
14 / 14
CRAP
100.00% covered (success)
100.00%
1 / 1
ServeCommand
100.00% covered (success)
100.00%
56 / 56
100.00% covered (success)
100.00%
14 / 14
28
100.00% covered (success)
100.00%
1 / 1
 safeHandle
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 getHostSelection
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getPortSelection
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getExecutablePath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 runServerProcess
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getEnvironmentVariables
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 configureOutput
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 printStartMessage
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getOutputHandler
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 useBasicOutput
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 parseEnvironmentOption
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 checkArgvForOption
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 openInBrowser
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 getOpenCommand
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Hyde\Console\Commands;
6
7use Closure;
8use Hyde\Hyde;
9use Hyde\Facades\Config;
10use Illuminate\Support\Arr;
11use InvalidArgumentException;
12use Hyde\Console\Concerns\Command;
13use Hyde\RealtimeCompiler\ConsoleOutput;
14use Illuminate\Support\Facades\Process;
15
16use function rtrim;
17use function sprintf;
18use function in_array;
19use function str_replace;
20use function class_exists;
21
22/**
23 * Start the realtime compiler server.
24 *
25 * @see https://github.com/hydephp/realtime-compiler
26 */
27class ServeCommand extends Command
28{
29    /** @var string */
30    protected $signature = 'serve
31        {--host= : <comment>[default: "localhost"]</comment>}}
32        {--port= : <comment>[default: 8080]</comment>}
33        {--save-preview= : Should the served page be saved to disk? (Overrides config setting)}
34        {--dashboard= : Enable the realtime compiler dashboard. (Overrides config setting)}
35        {--pretty-urls= : Enable pretty URLs. (Overrides config setting)}
36        {--play-cdn= : Enable the Tailwind Play CDN. (Overrides config setting)}
37        {--open=false : Open the site preview in the browser.}
38    ';
39
40    /** @var string */
41    protected $description = 'Start the realtime compiler server';
42
43    protected ConsoleOutput $console;
44
45    public function safeHandle(): int
46    {
47        $this->configureOutput();
48        $this->printStartMessage();
49
50        if ($this->option('open') !== 'false') {
51            $this->openInBrowser((string) $this->option('open'));
52        }
53
54        $this->runServerProcess(sprintf('php -S %s:%d %s',
55            $this->getHostSelection(),
56            $this->getPortSelection(),
57            $this->getExecutablePath()
58        ));
59
60        return Command::SUCCESS;
61    }
62
63    protected function getHostSelection(): string
64    {
65        return (string) $this->option('host') ?: Config::getString('hyde.server.host', 'localhost');
66    }
67
68    protected function getPortSelection(): int
69    {
70        return (int) ($this->option('port') ?: Config::getInt('hyde.server.port', 8080));
71    }
72
73    protected function getExecutablePath(): string
74    {
75        return Hyde::path('vendor/hyde/realtime-compiler/bin/server.php');
76    }
77
78    protected function runServerProcess(string $command): void
79    {
80        Process::forever()->env($this->getEnvironmentVariables())->run($command, $this->getOutputHandler());
81    }
82
83    protected function getEnvironmentVariables(): array
84    {
85        return Arr::whereNotNull([
86            'HYDE_SERVER_REQUEST_OUTPUT' => ! $this->option('no-ansi'),
87            'HYDE_SERVER_SAVE_PREVIEW' => $this->parseEnvironmentOption('save-preview'),
88            'HYDE_SERVER_DASHBOARD' => $this->parseEnvironmentOption('dashboard'),
89            'HYDE_PRETTY_URLS' => $this->parseEnvironmentOption('pretty-urls'),
90            'HYDE_PLAY_CDN' => $this->parseEnvironmentOption('play-cdn'),
91        ]);
92    }
93
94    protected function configureOutput(): void
95    {
96        if (! $this->useBasicOutput()) {
97            $this->console = new ConsoleOutput($this->output->isVerbose());
98        }
99    }
100
101    protected function printStartMessage(): void
102    {
103        $this->useBasicOutput()
104            ? $this->output->writeln('<info>Starting the HydeRC server...</info> Use Ctrl+C to stop')
105            : $this->console->printStartMessage($this->getHostSelection(), $this->getPortSelection(), $this->getEnvironmentVariables());
106    }
107
108    protected function getOutputHandler(): Closure
109    {
110        return $this->useBasicOutput() ? function (string $type, string $line): void {
111            $this->output->write($line);
112        } : $this->console->getFormatter();
113    }
114
115    protected function useBasicOutput(): bool
116    {
117        return $this->option('no-ansi') || ! class_exists(ConsoleOutput::class);
118    }
119
120    protected function parseEnvironmentOption(string $name): ?string
121    {
122        $value = $this->option($name) ?? $this->checkArgvForOption($name);
123
124        if ($value !== null) {
125            return match ($value) {
126                'true', '' => 'enabled',
127                'false' => 'disabled',
128                default => throw new InvalidArgumentException(sprintf('Invalid boolean value for --%s option.', $name))
129            };
130        }
131
132        return null;
133    }
134
135    /** Fallback check so that an environment option without a value is acknowledged as true. */
136    protected function checkArgvForOption(string $name): ?string
137    {
138        if (isset($_SERVER['argv'])) {
139            if (in_array("--$name", $_SERVER['argv'], true)) {
140                return 'true';
141            }
142        }
143
144        return null;
145    }
146
147    protected function openInBrowser(string $path = '/'): void
148    {
149        $binary = $this->getOpenCommand(PHP_OS_FAMILY);
150
151        $command = sprintf('%s http://%s:%d', $binary, $this->getHostSelection(), $this->getPortSelection());
152        $command = rtrim("$command/$path", '/');
153
154        $process = $binary ? Process::command($command)->run() : null;
155
156        if (! $process || $process->failed()) {
157            $this->warn('Unable to open the site preview in the browser on your system:');
158            $this->line(sprintf('  %s', str_replace("\n", "\n  ", $process ? $process->errorOutput() : "Missing suitable 'open' binary.")));
159            $this->newLine();
160        }
161    }
162
163    protected function getOpenCommand(string $osFamily): ?string
164    {
165        return match ($osFamily) {
166            'Windows' => 'start',
167            'Darwin' => 'open',
168            'Linux' => 'xdg-open',
169            default => null
170        };
171    }
172}