ServeCommand   A
last analyzed

Complexity

Total Complexity 38

Size/Duplication

Total Lines 201
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 78
dl 0
loc 201
rs 9.36
c 0
b 0
f 0
wmc 38

18 Methods

Rating   Name   Duplication   Size   Complexity  
A configureOutput() 0 4 2
A getOutputHandler() 0 5 2
A checkArgvForOption() 0 9 3
A runViteProcess() 0 12 2
A getOpenCommand() 0 7 1
A getExecutablePath() 0 3 1
A getEnvironmentVariables() 0 8 1
A useBasicOutput() 0 3 2
A parseEnvironmentOption() 0 13 2
A runServerProcess() 0 3 1
A isPortAvailable() 0 10 2
A openInBrowser() 0 13 5
A printStartMessage() 0 5 2
A getPortSelection() 0 3 2
A handleViteOutput() 0 7 3
A handleRunningProcesses() 0 6 2
A getHostSelection() 0 3 2
A safeHandle() 0 22 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Hyde\Console\Commands;
6
7
use Closure;
8
use Hyde\Facades\Filesystem;
9
use Hyde\Hyde;
10
use Hyde\Facades\Config;
11
use Illuminate\Contracts\Process\InvokedProcess;
12
use Illuminate\Support\Arr;
13
use Illuminate\Support\Sleep;
14
use InvalidArgumentException;
15
use Hyde\Console\Concerns\Command;
16
use Hyde\RealtimeCompiler\ConsoleOutput;
17
use Illuminate\Support\Facades\Process;
18
19
use function rtrim;
20
use function sprintf;
21
use function in_array;
22
use function str_replace;
23
use function class_exists;
24
25
/**
26
 * Start the realtime compiler server.
27
 *
28
 * @see https://github.com/hydephp/realtime-compiler
29
 */
30
class ServeCommand extends Command
31
{
32
    /** @var string */
33
    protected $signature = 'serve
34
        {--host= : <comment>[default: "localhost"]</comment>}}
35
        {--port= : <comment>[default: 8080]</comment>}
36
        {--save-preview= : Should the served page be saved to disk? (Overrides config setting)}
37
        {--dashboard= : Enable the realtime compiler dashboard. (Overrides config setting)}
38
        {--pretty-urls= : Enable pretty URLs. (Overrides config setting)}
39
        {--play-cdn= : Enable the Tailwind Play CDN. (Overrides config setting)}
40
        {--open=false : Open the site preview in the browser.}
41
        {--vite : Enable Vite for Hot Module Replacement (HMR)}
42
    ';
43
44
    /** @var string */
45
    protected $description = 'Start the realtime compiler server';
46
47
    protected ConsoleOutput $console;
48
49
    protected InvokedProcess $server;
50
    protected ?InvokedProcess $vite = null;
51
52
    public function safeHandle(): int
53
    {
54
        $this->configureOutput();
55
        $this->printStartMessage();
56
57
        if ($this->option('open') !== 'false') {
58
            $this->openInBrowser((string) $this->option('open'));
59
        }
60
61
        if ($this->option('vite')) {
62
            $this->runViteProcess();
63
        }
64
65
        $this->runServerProcess(sprintf('php -S %s:%d %s',
66
            $this->getHostSelection(),
67
            $this->getPortSelection(),
68
            escapeshellarg($this->getExecutablePath()),
69
        ));
70
71
        $this->handleRunningProcesses();
72
73
        return Command::SUCCESS;
74
    }
75
76
    protected function getHostSelection(): string
77
    {
78
        return (string) $this->option('host') ?: Config::getString('hyde.server.host', 'localhost');
79
    }
80
81
    protected function getPortSelection(): int
82
    {
83
        return (int) ($this->option('port') ?: Config::getInt('hyde.server.port', 8080));
84
    }
85
86
    protected function getExecutablePath(): string
87
    {
88
        return Hyde::path('vendor/hyde/realtime-compiler/bin/server.php');
0 ignored issues
show
Bug introduced by
The method path() does not exist on Hyde\Hyde. Since you implemented __callStatic, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

88
        return Hyde::/** @scrutinizer ignore-call */ path('vendor/hyde/realtime-compiler/bin/server.php');
Loading history...
89
    }
90
91
    protected function runServerProcess(string $command): void
92
    {
93
        $this->server = Process::forever()->env($this->getEnvironmentVariables())->start($command, $this->getOutputHandler());
94
    }
95
96
    protected function getEnvironmentVariables(): array
97
    {
98
        return Arr::whereNotNull([
99
            'HYDE_SERVER_REQUEST_OUTPUT' => ! $this->option('no-ansi'),
100
            'HYDE_SERVER_SAVE_PREVIEW' => $this->parseEnvironmentOption('save-preview'),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->parseEnvironmentOption('save-preview') targeting Hyde\Console\Commands\Se...arseEnvironmentOption() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
101
            'HYDE_SERVER_DASHBOARD' => $this->parseEnvironmentOption('dashboard'),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->parseEnvironmentOption('dashboard') targeting Hyde\Console\Commands\Se...arseEnvironmentOption() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
102
            'HYDE_PRETTY_URLS' => $this->parseEnvironmentOption('pretty-urls'),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->parseEnvironmentOption('pretty-urls') targeting Hyde\Console\Commands\Se...arseEnvironmentOption() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
103
            'HYDE_PLAY_CDN' => $this->parseEnvironmentOption('play-cdn'),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->parseEnvironmentOption('play-cdn') targeting Hyde\Console\Commands\Se...arseEnvironmentOption() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
104
        ]);
105
    }
106
107
    protected function configureOutput(): void
108
    {
109
        if (! $this->useBasicOutput()) {
110
            $this->console = new ConsoleOutput($this->output->isVerbose());
111
        }
112
    }
113
114
    protected function printStartMessage(): void
115
    {
116
        $this->useBasicOutput()
117
            ? $this->output->writeln('<info>Starting the HydeRC server...</info> Use Ctrl+C to stop')
118
            : $this->console->printStartMessage($this->getHostSelection(), $this->getPortSelection(), $this->getEnvironmentVariables(), $this->option('vite'));
0 ignored issues
show
Bug introduced by
$this->option('vite') of type string is incompatible with the type boolean|null expected by parameter $willUseVite of Hyde\RealtimeCompiler\Co...ut::printStartMessage(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

118
            : $this->console->printStartMessage($this->getHostSelection(), $this->getPortSelection(), $this->getEnvironmentVariables(), /** @scrutinizer ignore-type */ $this->option('vite'));
Loading history...
119
    }
120
121
    protected function getOutputHandler(): Closure
122
    {
123
        return $this->useBasicOutput() ? function (string $type, string $line): void {
124
            $this->output->write($line);
125
        } : $this->console->getFormatter();
126
    }
127
128
    protected function useBasicOutput(): bool
129
    {
130
        return $this->option('no-ansi') || ! class_exists(ConsoleOutput::class);
131
    }
132
133
    protected function parseEnvironmentOption(string $name): ?string
134
    {
135
        $value = $this->option($name) ?? $this->checkArgvForOption($name);
136
137
        if ($value !== null) {
138
            return match ($value) {
139
                'true', '' => 'enabled',
140
                'false' => 'disabled',
141
                default => throw new InvalidArgumentException(sprintf('Invalid boolean value for --%s option.', $name))
142
            };
143
        }
144
145
        return null;
146
    }
147
148
    /** Fallback check so that an environment option without a value is acknowledged as true. */
149
    protected function checkArgvForOption(string $name): ?string
150
    {
151
        if (isset($_SERVER['argv'])) {
152
            if (in_array("--$name", $_SERVER['argv'], true)) {
153
                return 'true';
154
            }
155
        }
156
157
        return null;
158
    }
159
160
    protected function openInBrowser(string $path = '/'): void
161
    {
162
        $binary = $this->getOpenCommand(PHP_OS_FAMILY);
163
164
        $command = sprintf('%s http://%s:%d', $binary, $this->getHostSelection(), $this->getPortSelection());
165
        $command = rtrim("$command/$path", '/');
166
167
        $process = $binary ? Process::command($command)->run() : null;
168
169
        if (! $process || $process->failed()) {
170
            $this->warn('Unable to open the site preview in the browser on your system:');
171
            $this->line(sprintf('  %s', str_replace("\n", "\n  ", $process ? $process->errorOutput() : "Missing suitable 'open' binary.")));
172
            $this->newLine();
173
        }
174
    }
175
176
    protected function getOpenCommand(string $osFamily): ?string
177
    {
178
        return match ($osFamily) {
179
            'Windows' => 'start',
180
            'Darwin' => 'open',
181
            'Linux' => 'xdg-open',
182
            default => null
183
        };
184
    }
185
186
    protected function runViteProcess(): void
187
    {
188
        if (! $this->isPortAvailable(5173)) {
189
            throw new InvalidArgumentException(
190
                'Unable to start Vite server: Port 5173 is already in use. '.
191
                'Please stop any other Vite processes and try again.'
192
            );
193
        }
194
195
        Filesystem::touch('app/storage/framework/runtime/vite.hot');
196
197
        $this->vite = Process::forever()->start('npm run dev');
198
    }
199
200
    protected function handleRunningProcesses(): void
201
    {
202
        while ($this->server->running()) {
203
            $this->handleViteOutput();
204
205
            Sleep::for(100)->milliseconds();
206
        }
207
    }
208
209
    protected function handleViteOutput(): void
210
    {
211
        if ($this->vite?->running()) {
212
            $output = $this->vite->latestOutput();
0 ignored issues
show
Bug introduced by
The method latestOutput() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

212
            /** @scrutinizer ignore-call */ 
213
            $output = $this->vite->latestOutput();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
213
214
            if ($output) {
215
                $this->output->write($output);
216
            }
217
        }
218
    }
219
220
    /** @experimental This feature may be removed before the final release. */
221
    protected function isPortAvailable(int $port): bool
222
    {
223
        $socket = @fsockopen('localhost', $port, $errno, $errstr, 1);
224
        if ($socket !== false) {
225
            fclose($socket);
226
227
            return false;
228
        }
229
230
        return true;
231
    }
232
}
233