Skip to content

Commit ec1583a

Browse files
authored
Merge pull request #2016 from hydephp/vite-integration
[2.x] Add Vite Realtime Compiler integration for HMR
2 parents c054753 + 313e605 commit ec1583a

File tree

15 files changed

+467
-17
lines changed

15 files changed

+467
-17
lines changed

RELEASE_NOTES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ This serves two purposes:
3333
- Added method `Asset::exists()` has to check if a media file exists.
3434
- Added a `Hyde::assets()` method to get all media file instances in the site.
3535
- Added new `npm run build` command for compiling frontend assets with Vite
36+
- Added a Vite HMR support for the realtime compiler in https://github.com/hydephp/develop/pull/2016
37+
- Added Vite facade in https://github.com/hydephp/develop/pull/2016
3638

3739
### Changed
3840

@@ -131,6 +133,7 @@ This serves two purposes:
131133
#### Realtime Compiler
132134

133135
- Simplified the asset file locator to only serve files from the media source directory in https://github.com/hydephp/develop/pull/2012
136+
- Added Vite HMR support in https://github.com/hydephp/develop/pull/2016
134137

135138
### Upgrade Guide
136139

_ide_helper.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class Asset extends \Hyde\Facades\Asset {}
3737
class Author extends \Hyde\Facades\Author {}
3838
class Features extends \Hyde\Facades\Features {}
3939
class Config extends \Hyde\Facades\Config {}
40+
class Vite extends \Hyde\Facades\Vite {}
4041
/** @mixin \Illuminate\Filesystem\Filesystem */
4142
class Filesystem extends \Hyde\Facades\Filesystem {}
4243
class DataCollection extends \Hyde\Support\DataCollection {}

app/config.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
'Hyde' => Hyde\Hyde::class,
9393
'Site' => \Hyde\Facades\Site::class,
9494
'Meta' => \Hyde\Facades\Meta::class,
95+
'Vite' => \Hyde\Facades\Vite::class,
9596
'Asset' => \Hyde\Facades\Asset::class,
9697
'Author' => \Hyde\Facades\Author::class,
9798
'HydeFront' => \Hyde\Facades\HydeFront::class,

docs/extensions/realtime-compiler.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ This will start a local development server at `http://localhost:8080`
3030
- `--pretty-urls=`: Enable pretty URLs. (Overrides config setting)
3131
- `--play-cdn=`: Enable the Tailwind Play CDN. (Overrides config setting)
3232
- `--open=false`: Open the site preview in the browser.
33+
- `--vite`: Enable Vite for Hot Module Replacement (HMR).
34+
35+
### Vite Integration
36+
37+
By adding the `--vite` option, the serve command will initiate Vite's development server alongside the Hyde Realtime Compiler. This setup enables Hot Module Replacement (HMR), allowing for instant updates to your site as you make changes to your assets.
3338

3439
### Configuration
3540

docs/getting-started/console-commands.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ Run the static site builder for a single file
101101
<a name="serve" style="display: inline-block; position: absolute; margin-top: -5rem;"></a>
102102

103103
```bash
104-
php hyde serve [--host [HOST]] [--port [PORT]]
104+
php hyde serve [--host [HOST]] [--port [PORT]] [--vite]
105105
```
106106
107107
Start the realtime compiler server.

packages/framework/resources/views/layouts/scripts.blade.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
{{-- The compiled Vite scripts --}}
2-
@if(Asset::exists('app.js'))
2+
@if(Vite::running())
3+
{{ Vite::assets(['resources/assets/app.js']) }}
4+
@elseif(Asset::exists('app.js'))
35
<script defer src="{{ Asset::get('app.js') }}"></script>
46
@endif
57

packages/framework/resources/views/layouts/styles.blade.php

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,22 @@
22
<style>[x-cloak] {display: none!important}</style>
33

44
{{-- The compiled Tailwind/App styles --}}
5-
@if(config('hyde.load_app_styles_from_cdn', false))
6-
<link rel="stylesheet" href="{{ HydeFront::cdnLink('app.css') }}">
7-
@elseif(Asset::exists('app.css'))
8-
<link rel="stylesheet" href="{{ Asset::get('app.css') }}">
9-
@endif
5+
@if(Vite::running())
6+
{{ Vite::assets(['resources/assets/app.css']) }}
7+
@else
8+
@if(config('hyde.load_app_styles_from_cdn', false))
9+
<link rel="stylesheet" href="{{ HydeFront::cdnLink('app.css') }}">
10+
@elseif(Asset::exists('app.css'))
11+
<link rel="stylesheet" href="{{ Asset::get('app.css') }}">
12+
@endif
13+
1014

11-
{{-- Dynamic TailwindCSS Play CDN --}}
12-
@if(config('hyde.use_play_cdn', false))
13-
<script src="https://cdn.tailwindcss.com?plugins=typography"></script>
14-
<script>tailwind.config = { {!! HydeFront::injectTailwindConfig() !!} }</script>
15-
<script>console.warn('The HydePHP TailwindCSS Play CDN is enabled. This is for development purposes only and should not be used in production.', 'See https://hydephp.com/docs/1.x/managing-assets');</script>
15+
{{-- Dynamic TailwindCSS Play CDN --}}
16+
@if(config('hyde.use_play_cdn', false))
17+
<script src="https://cdn.tailwindcss.com?plugins=typography"></script>
18+
<script>tailwind.config = { {!! HydeFront::injectTailwindConfig() !!} }</script>
19+
<script>console.warn('The HydePHP TailwindCSS Play CDN is enabled. This is for development purposes only and should not be used in production.', 'See https://hydephp.com/docs/1.x/managing-assets');</script>
20+
@endif
1621
@endif
1722

1823
{{-- Add any extra styles to include after the others --}}

packages/framework/src/Console/Commands/ServeCommand.php

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
use Closure;
88
use Hyde\Hyde;
99
use Hyde\Facades\Config;
10+
use Illuminate\Contracts\Process\InvokedProcess;
1011
use Illuminate\Support\Arr;
12+
use Illuminate\Support\Sleep;
1113
use InvalidArgumentException;
1214
use Hyde\Console\Concerns\Command;
1315
use Hyde\RealtimeCompiler\ConsoleOutput;
@@ -35,13 +37,17 @@ class ServeCommand extends Command
3537
{--pretty-urls= : Enable pretty URLs. (Overrides config setting)}
3638
{--play-cdn= : Enable the Tailwind Play CDN. (Overrides config setting)}
3739
{--open=false : Open the site preview in the browser.}
40+
{--vite : Enable Vite for Hot Module Replacement (HMR)}
3841
';
3942

4043
/** @var string */
4144
protected $description = 'Start the realtime compiler server';
4245

4346
protected ConsoleOutput $console;
4447

48+
protected InvokedProcess $server;
49+
protected InvokedProcess $vite;
50+
4551
public function safeHandle(): int
4652
{
4753
$this->configureOutput();
@@ -51,11 +57,29 @@ public function safeHandle(): int
5157
$this->openInBrowser((string) $this->option('open'));
5258
}
5359

54-
$this->runServerProcess(sprintf('php -S %s:%d %s',
60+
$command = sprintf('php -S %s:%d %s',
5561
$this->getHostSelection(),
5662
$this->getPortSelection(),
5763
$this->getExecutablePath()
58-
));
64+
);
65+
66+
if ($this->option('vite')) {
67+
$this->runViteProcess();
68+
}
69+
70+
$this->runServerProcess($command);
71+
72+
while ($this->server->running()) {
73+
if (isset($this->vite) && $this->vite->running()) {
74+
$output = $this->vite->latestOutput();
75+
76+
if ($output) {
77+
$this->output->write($output);
78+
}
79+
}
80+
81+
Sleep::usleep(100000); // 100ms
82+
}
5983

6084
return Command::SUCCESS;
6185
}
@@ -77,7 +101,7 @@ protected function getExecutablePath(): string
77101

78102
protected function runServerProcess(string $command): void
79103
{
80-
Process::forever()->env($this->getEnvironmentVariables())->run($command, $this->getOutputHandler());
104+
$this->server = Process::forever()->env($this->getEnvironmentVariables())->start($command, $this->getOutputHandler());
81105
}
82106

83107
protected function getEnvironmentVariables(): array
@@ -88,6 +112,7 @@ protected function getEnvironmentVariables(): array
88112
'HYDE_SERVER_DASHBOARD' => $this->parseEnvironmentOption('dashboard'),
89113
'HYDE_PRETTY_URLS' => $this->parseEnvironmentOption('pretty-urls'),
90114
'HYDE_PLAY_CDN' => $this->parseEnvironmentOption('play-cdn'),
115+
'HYDE_SERVER_VITE' => $this->option('vite') ? 'enabled' : null,
91116
]);
92117
}
93118

@@ -169,4 +194,29 @@ protected function getOpenCommand(string $osFamily): ?string
169194
default => null
170195
};
171196
}
197+
198+
protected function runViteProcess(): void
199+
{
200+
if (! $this->isPortAvailable(5173)) {
201+
throw new InvalidArgumentException(
202+
'Unable to start Vite server: Port 5173 is already in use. '.
203+
'Please stop any other Vite processes and try again.'
204+
);
205+
}
206+
207+
$this->vite = Process::forever()->start('npm run dev');
208+
}
209+
210+
/** @experimental This feature may be removed before the final release. */
211+
protected function isPortAvailable(int $port): bool
212+
{
213+
$socket = @fsockopen('localhost', $port, $errno, $errstr, 1);
214+
if ($socket !== false) {
215+
fclose($socket);
216+
217+
return false;
218+
}
219+
220+
return true;
221+
}
172222
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Hyde\Facades;
6+
7+
use Illuminate\Support\HtmlString;
8+
9+
/**
10+
* Vite facade for handling Vite-related operations.
11+
*/
12+
class Vite
13+
{
14+
public static function running(): bool
15+
{
16+
// Check if Vite was enabled via the serve command
17+
if (env('HYDE_SERVER_VITE') === 'enabled') {
18+
return true;
19+
}
20+
21+
// Check if Vite dev server is running by attempting to connect to it
22+
// Todo: Improve performance on Windows (takes less than 1ms on macOS, but around 100ms on Windows)
23+
set_error_handler(fn () => false); // Todo: This warning surpressor does not work on Windows
24+
$server = fsockopen('localhost', 5173, $errno, $errstr, 0.1);
25+
restore_error_handler();
26+
27+
if ($server) {
28+
fclose($server);
29+
30+
return true;
31+
}
32+
33+
return false;
34+
}
35+
36+
public static function assets(array $paths): HtmlString
37+
{
38+
$html = sprintf('<script src="http://localhost:5173/@vite/client" type="module"></script>');
39+
40+
foreach ($paths as $path) {
41+
if (str_ends_with($path, '.css')) {
42+
$html .= sprintf('<link rel="stylesheet" href="http://localhost:5173/%s">', $path);
43+
}
44+
45+
if (str_ends_with($path, '.js')) {
46+
$html .= sprintf('<script src="http://localhost:5173/%s" type="module"></script>', $path);
47+
}
48+
}
49+
50+
return new HtmlString($html);
51+
}
52+
}

packages/framework/tests/Feature/Commands/ServeCommandTest.php

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Closure;
88
use Hyde\Hyde;
99
use Hyde\Testing\TestCase;
10+
use Illuminate\Contracts\Process\InvokedProcess;
1011
use Illuminate\Support\Facades\Process;
1112
use TypeError;
1213

@@ -138,6 +139,11 @@ public function testHydeServeCommandWithInvalidConfigValue()
138139

139140
public function testHydeServeCommandPassesThroughProcessOutput()
140141
{
142+
$mockProcess = mock(InvokedProcess::class);
143+
$mockProcess->shouldReceive('running')
144+
->once()
145+
->andReturn(false);
146+
141147
Process::shouldReceive('forever')
142148
->once()
143149
->withNoArgs()
@@ -148,14 +154,14 @@ public function testHydeServeCommandPassesThroughProcessOutput()
148154
->with(['HYDE_SERVER_REQUEST_OUTPUT' => false])
149155
->andReturnSelf();
150156

151-
Process::shouldReceive('run')
157+
Process::shouldReceive('start')
152158
->once()
153159
->withArgs(function (string $command, Closure $handle) {
154160
$handle('type', 'foo');
155161

156162
return $command === "php -S localhost:8080 {$this->binaryPath()}";
157163
})
158-
->andReturnSelf();
164+
->andReturn($mockProcess);
159165

160166
$this->artisan('serve --no-ansi')
161167
->expectsOutput('Starting the HydeRC server... Use Ctrl+C to stop')
@@ -174,6 +180,102 @@ public function testWithFancyOutput()
174180
Process::assertRan("php -S localhost:8080 {$this->binaryPath()}");
175181
}
176182

183+
public function testHydeServeCommandWithViteOption()
184+
{
185+
$mockViteProcess = mock(InvokedProcess::class);
186+
$mockViteProcess->shouldReceive('running')
187+
->once()
188+
->andReturn(true);
189+
$mockViteProcess->shouldReceive('latestOutput')
190+
->once()
191+
->andReturn('vite latest output');
192+
193+
$mockServerProcess = mock(InvokedProcess::class);
194+
$mockServerProcess->shouldReceive('running')
195+
->times(2)
196+
->andReturn(true, false);
197+
198+
Process::shouldReceive('forever')
199+
->twice()
200+
->withNoArgs()
201+
->andReturnSelf();
202+
203+
Process::shouldReceive('env')
204+
->once()
205+
->with(['HYDE_SERVER_REQUEST_OUTPUT' => false, 'HYDE_SERVER_VITE' => 'enabled'])
206+
->andReturnSelf();
207+
208+
Process::shouldReceive('start')
209+
->once()
210+
->with('npm run dev')
211+
->andReturn($mockViteProcess);
212+
213+
Process::shouldReceive('start')
214+
->once()
215+
->withArgs(function (string $command, Closure $output) {
216+
$output('stdout', 'server output');
217+
218+
return $command === "php -S localhost:8080 {$this->binaryPath()}";
219+
})
220+
->andReturn($mockServerProcess);
221+
222+
$this->artisan('serve --no-ansi --vite')
223+
->expectsOutput('Starting the HydeRC server... Use Ctrl+C to stop')
224+
->expectsOutput('server output')
225+
->expectsOutput('vite latest output')
226+
->assertExitCode(0);
227+
}
228+
229+
public function testHydeServeCommandWithViteOptionButViteNotRunning()
230+
{
231+
$mockViteProcess = mock(InvokedProcess::class);
232+
$mockViteProcess->shouldReceive('running')
233+
->once()
234+
->andReturn(false);
235+
236+
$mockServerProcess = mock(InvokedProcess::class);
237+
$mockServerProcess->shouldReceive('running')
238+
->times(2)
239+
->andReturn(true, false);
240+
241+
Process::shouldReceive('forever')
242+
->twice()
243+
->withNoArgs()
244+
->andReturnSelf();
245+
246+
Process::shouldReceive('env')
247+
->once()
248+
->with(['HYDE_SERVER_REQUEST_OUTPUT' => false, 'HYDE_SERVER_VITE' => 'enabled'])
249+
->andReturnSelf();
250+
251+
Process::shouldReceive('start')
252+
->once()
253+
->with('npm run dev')
254+
->andReturn($mockViteProcess);
255+
256+
Process::shouldReceive('start')
257+
->once()
258+
->withArgs(function (string $command, Closure $handle) {
259+
return $command === "php -S localhost:8080 {$this->binaryPath()}";
260+
})
261+
->andReturn($mockServerProcess);
262+
263+
$this->artisan('serve --no-ansi --vite')
264+
->expectsOutput('Starting the HydeRC server... Use Ctrl+C to stop')
265+
->assertExitCode(0);
266+
}
267+
268+
public function testHydeServeCommandWithViteOptionThrowsWhenPortIsInUse()
269+
{
270+
$socket = stream_socket_server('tcp://127.0.0.1:5173');
271+
272+
$this->artisan('serve --vite')
273+
->expectsOutputToContain('Unable to start Vite server: Port 5173 is already in use')
274+
->assertExitCode(1);
275+
276+
stream_socket_shutdown($socket, STREAM_SHUT_RDWR);
277+
}
278+
177279
protected function binaryPath(): string
178280
{
179281
return Hyde::path('vendor/hyde/realtime-compiler/bin/server.php');

0 commit comments

Comments
 (0)