Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[2.x] Add Vite Realtime Compiler integration for HMR #2016

Merged
merged 50 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from 49 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
2f9ad79
Create Vite.php
caendesilva Nov 12, 2024
e3dc0a2
Create ViteFacadeTest.php
caendesilva Nov 12, 2024
8aba46b
Create Vite server configuration
caendesilva Nov 12, 2024
220fceb
Draft Vite option for the serve command
caendesilva Nov 12, 2024
d67c699
Improve realtime compiler documentation
caendesilva Nov 12, 2024
851421d
Method to print the Vite message
caendesilva Nov 12, 2024
d2afcea
Run processes in parallel
caendesilva Nov 12, 2024
6ad2b04
Stream both process outputs
caendesilva Nov 12, 2024
b6361ab
Create vite-index-page.html
caendesilva Nov 12, 2024
c87203a
Render Vite index page
caendesilva Nov 12, 2024
ed2404d
Draft facade methods
caendesilva Nov 12, 2024
f5d6c84
Alias Vite facade
caendesilva Nov 12, 2024
e59cecf
Add strict types declaration
caendesilva Nov 12, 2024
93bfbbc
Implement the Vite asset method
caendesilva Nov 12, 2024
2683f86
Serve Vite assets when running
caendesilva Nov 12, 2024
58a7f7a
Implement the Vite connection checker
caendesilva Nov 12, 2024
86e0180
Use much faster environment variable for Vite status
caendesilva Nov 12, 2024
b083300
Flush output
caendesilva Nov 12, 2024
da82fa6
Start over with Vite server
caendesilva Nov 12, 2024
310407e
Simpler Vite process handler
caendesilva Nov 12, 2024
d0fc208
Run server asynchronously
caendesilva Nov 12, 2024
b6d96c4
Sleep while server is running
caendesilva Nov 12, 2024
6d40de6
Handle output from sleep loop
caendesilva Nov 12, 2024
6ea538f
Declare properties
caendesilva Nov 12, 2024
6a19d1f
Type against the contract
caendesilva Nov 12, 2024
f170366
Sleep for 100ms instead of 1 second
caendesilva Nov 12, 2024
d935511
Remove unused Vite start message helper
caendesilva Nov 12, 2024
406e8ae
Add Vite information to output start message
caendesilva Nov 12, 2024
8419a68
Support standalone Vite servers
caendesilva Nov 12, 2024
57a22d9
Use default Vite port
caendesilva Nov 12, 2024
f1c421e
Add widths for layout to work without network
caendesilva Nov 12, 2024
c0d67ff
Update tests for asynchronous server process
caendesilva Nov 12, 2024
704f67c
Use the sleep facade
caendesilva Nov 12, 2024
b03ec7f
Add tests to cover introduced code
caendesilva Nov 12, 2024
2208e17
Suppress failed connection warnings
caendesilva Nov 12, 2024
761d92a
Update Vite server to check if the port is free
caendesilva Nov 12, 2024
deb202c
Conditionally write output
caendesilva Nov 14, 2024
41a79fe
Document Windows performance
caendesilva Nov 14, 2024
0daac5a
Document Windows issue
caendesilva Nov 14, 2024
91297e8
Update RELEASE_NOTES.md
caendesilva Nov 14, 2024
3687cea
Merge branch 'new-asset-system' into vite-integration
caendesilva Nov 14, 2024
fa4a631
Merge branch 'new-asset-system' into vite-integration
caendesilva Nov 14, 2024
2e7f526
Merge branch 'new-asset-system' into vite-integration
caendesilva Nov 14, 2024
ff1f6dc
Fix unintentional change
caendesilva Nov 14, 2024
0da73bf
Fix formatting
caendesilva Nov 14, 2024
9dc44d1
Normalize formatting
caendesilva Nov 14, 2024
5ed9f6b
Update RELEASE_NOTES.md
caendesilva Nov 14, 2024
e26941d
Mark feature as experimental
caendesilva Nov 14, 2024
2a480c2
Add Vite HMR support for the app scripts
caendesilva Nov 14, 2024
313e605
Update _ide_helper.php
caendesilva Nov 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ This serves two purposes:
- Added method `Asset::exists()` has to check if a media file exists.
- Added a `Hyde::assets()` method to get all media file instances in the site.
- Added new `npm run build` command for compiling frontend assets with Vite
- Added a Vite HMR support for the realtime compiler in https://github.com/hydephp/develop/pull/2016
- Added Vite facade in https://github.com/hydephp/develop/pull/2016

### Changed

Expand Down Expand Up @@ -131,6 +133,7 @@ This serves two purposes:
#### Realtime Compiler

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

### Upgrade Guide

Expand Down
1 change: 1 addition & 0 deletions app/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
'Hyde' => Hyde\Hyde::class,
'Site' => \Hyde\Facades\Site::class,
'Meta' => \Hyde\Facades\Meta::class,
'Vite' => \Hyde\Facades\Vite::class,
'Asset' => \Hyde\Facades\Asset::class,
'Author' => \Hyde\Facades\Author::class,
'HydeFront' => \Hyde\Facades\HydeFront::class,
Expand Down
5 changes: 5 additions & 0 deletions docs/extensions/realtime-compiler.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ This will start a local development server at `http://localhost:8080`
- `--pretty-urls=`: Enable pretty URLs. (Overrides config setting)
- `--play-cdn=`: Enable the Tailwind Play CDN. (Overrides config setting)
- `--open=false`: Open the site preview in the browser.
- `--vite`: Enable Vite for Hot Module Replacement (HMR).

### Vite Integration

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.

### Configuration

Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started/console-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ Run the static site builder for a single file
<a name="serve" style="display: inline-block; position: absolute; margin-top: -5rem;"></a>

```bash
php hyde serve [--host [HOST]] [--port [PORT]]
php hyde serve [--host [HOST]] [--port [PORT]] [--vite]
```

Start the realtime compiler server.
Expand Down
4 changes: 3 additions & 1 deletion packages/framework/resources/views/layouts/scripts.blade.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{{-- The compiled Vite scripts --}}
@if(Asset::exists('app.js'))
@if(Vite::running())
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May make more sense to have this alongside the CSS include since the script is deferred anyways, but not sure how to handle that best with the partials since it would fit better in the head component, and don't want to make such a big change here.

{{ Vite::assets(['resources/assets/app.js']) }}
@elseif(Asset::exists('app.js'))
<script defer src="{{ Asset::get('app.js') }}"></script>
@endif

Expand Down
25 changes: 15 additions & 10 deletions packages/framework/resources/views/layouts/styles.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,22 @@
<style>[x-cloak] {display: none!important}</style>

{{-- The compiled Tailwind/App styles --}}
@if(config('hyde.load_app_styles_from_cdn', false))
<link rel="stylesheet" href="{{ HydeFront::cdnLink('app.css') }}">
@elseif(Asset::exists('app.css'))
<link rel="stylesheet" href="{{ Asset::get('app.css') }}">
@endif
@if(Vite::running())
{{ Vite::assets(['resources/assets/app.css']) }}
caendesilva marked this conversation as resolved.
Show resolved Hide resolved
@else
@if(config('hyde.load_app_styles_from_cdn', false))
<link rel="stylesheet" href="{{ HydeFront::cdnLink('app.css') }}">
@elseif(Asset::exists('app.css'))
<link rel="stylesheet" href="{{ Asset::get('app.css') }}">
@endif

Comment on lines +7 to +13
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make more sense if we just automatically enabled Vite when using Asset::get and Vite is running?


{{-- Dynamic TailwindCSS Play CDN --}}
@if(config('hyde.use_play_cdn', false))
<script src="https://cdn.tailwindcss.com?plugins=typography"></script>
<script>tailwind.config = { {!! HydeFront::injectTailwindConfig() !!} }</script>
<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>
{{-- Dynamic TailwindCSS Play CDN --}}
@if(config('hyde.use_play_cdn', false))
<script src="https://cdn.tailwindcss.com?plugins=typography"></script>
<script>tailwind.config = { {!! HydeFront::injectTailwindConfig() !!} }</script>
<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>
@endif
@endif

{{-- Add any extra styles to include after the others --}}
Expand Down
56 changes: 53 additions & 3 deletions packages/framework/src/Console/Commands/ServeCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
use Closure;
use Hyde\Hyde;
use Hyde\Facades\Config;
use Illuminate\Contracts\Process\InvokedProcess;
use Illuminate\Support\Arr;
use Illuminate\Support\Sleep;
use InvalidArgumentException;
use Hyde\Console\Concerns\Command;
use Hyde\RealtimeCompiler\ConsoleOutput;
Expand Down Expand Up @@ -35,13 +37,17 @@ class ServeCommand extends Command
{--pretty-urls= : Enable pretty URLs. (Overrides config setting)}
{--play-cdn= : Enable the Tailwind Play CDN. (Overrides config setting)}
{--open=false : Open the site preview in the browser.}
{--vite : Enable Vite for Hot Module Replacement (HMR)}
';

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

protected ConsoleOutput $console;

protected InvokedProcess $server;
protected InvokedProcess $vite;

public function safeHandle(): int
{
$this->configureOutput();
Expand All @@ -51,11 +57,29 @@ public function safeHandle(): int
$this->openInBrowser((string) $this->option('open'));
}

$this->runServerProcess(sprintf('php -S %s:%d %s',
$command = sprintf('php -S %s:%d %s',
$this->getHostSelection(),
$this->getPortSelection(),
$this->getExecutablePath()
));
);

if ($this->option('vite')) {
$this->runViteProcess();
}

$this->runServerProcess($command);

while ($this->server->running()) {
if (isset($this->vite) && $this->vite->running()) {
$output = $this->vite->latestOutput();

if ($output) {
$this->output->write($output);
}
}

Sleep::usleep(100000); // 100ms
}

return Command::SUCCESS;
}
Expand All @@ -77,7 +101,7 @@ protected function getExecutablePath(): string

protected function runServerProcess(string $command): void
{
Process::forever()->env($this->getEnvironmentVariables())->run($command, $this->getOutputHandler());
$this->server = Process::forever()->env($this->getEnvironmentVariables())->start($command, $this->getOutputHandler());
}

protected function getEnvironmentVariables(): array
Expand All @@ -88,6 +112,7 @@ protected function getEnvironmentVariables(): array
'HYDE_SERVER_DASHBOARD' => $this->parseEnvironmentOption('dashboard'),
'HYDE_PRETTY_URLS' => $this->parseEnvironmentOption('pretty-urls'),
'HYDE_PLAY_CDN' => $this->parseEnvironmentOption('play-cdn'),
'HYDE_SERVER_VITE' => $this->option('vite') ? 'enabled' : null,
]);
}

Expand Down Expand Up @@ -169,4 +194,29 @@ protected function getOpenCommand(string $osFamily): ?string
default => null
};
}

protected function runViteProcess(): void
{
if (! $this->isPortAvailable(5173)) {
throw new InvalidArgumentException(
'Unable to start Vite server: Port 5173 is already in use. '.
'Please stop any other Vite processes and try again.'
);
}

$this->vite = Process::forever()->start('npm run dev');
}

/** @experimental This feature may be removed before the final release. */
protected function isPortAvailable(int $port): bool
{
$socket = @fsockopen('localhost', $port, $errno, $errstr, 1);
if ($socket !== false) {
fclose($socket);

return false;
}

return true;
}
caendesilva marked this conversation as resolved.
Show resolved Hide resolved
}
52 changes: 52 additions & 0 deletions packages/framework/src/Facades/Vite.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

declare(strict_types=1);

namespace Hyde\Facades;

use Illuminate\Support\HtmlString;

/**
* Vite facade for handling Vite-related operations.
*/
class Vite
{
public static function running(): bool
{
// Check if Vite was enabled via the serve command
if (env('HYDE_SERVER_VITE') === 'enabled') {
return true;
}

// Check if Vite dev server is running by attempting to connect to it
// Todo: Improve performance on Windows (takes less than 1ms on macOS, but around 100ms on Windows)
set_error_handler(fn () => false); // Todo: This warning surpressor does not work on Windows
$server = fsockopen('localhost', 5173, $errno, $errstr, 0.1);
restore_error_handler();

if ($server) {
fclose($server);

return true;
}

return false;
}

public static function assets(array $paths): HtmlString
{
$html = sprintf('<script src="http://localhost:5173/@vite/client" type="module"></script>');

foreach ($paths as $path) {
if (str_ends_with($path, '.css')) {
$html .= sprintf('<link rel="stylesheet" href="http://localhost:5173/%s">', $path);
}

if (str_ends_with($path, '.js')) {
$html .= sprintf('<script src="http://localhost:5173/%s" type="module"></script>', $path);
}
}

return new HtmlString($html);
}
}
106 changes: 104 additions & 2 deletions packages/framework/tests/Feature/Commands/ServeCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Closure;
use Hyde\Hyde;
use Hyde\Testing\TestCase;
use Illuminate\Contracts\Process\InvokedProcess;
use Illuminate\Support\Facades\Process;
use TypeError;

Expand Down Expand Up @@ -138,6 +139,11 @@ public function testHydeServeCommandWithInvalidConfigValue()

public function testHydeServeCommandPassesThroughProcessOutput()
{
$mockProcess = mock(InvokedProcess::class);
$mockProcess->shouldReceive('running')
->once()
->andReturn(false);

Process::shouldReceive('forever')
->once()
->withNoArgs()
Expand All @@ -148,14 +154,14 @@ public function testHydeServeCommandPassesThroughProcessOutput()
->with(['HYDE_SERVER_REQUEST_OUTPUT' => false])
->andReturnSelf();

Process::shouldReceive('run')
Process::shouldReceive('start')
->once()
->withArgs(function (string $command, Closure $handle) {
$handle('type', 'foo');

return $command === "php -S localhost:8080 {$this->binaryPath()}";
})
->andReturnSelf();
->andReturn($mockProcess);

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

public function testHydeServeCommandWithViteOption()
{
$mockViteProcess = mock(InvokedProcess::class);
$mockViteProcess->shouldReceive('running')
->once()
->andReturn(true);
$mockViteProcess->shouldReceive('latestOutput')
->once()
->andReturn('vite latest output');

$mockServerProcess = mock(InvokedProcess::class);
$mockServerProcess->shouldReceive('running')
->times(2)
->andReturn(true, false);

Process::shouldReceive('forever')
->twice()
->withNoArgs()
->andReturnSelf();

Process::shouldReceive('env')
->once()
->with(['HYDE_SERVER_REQUEST_OUTPUT' => false, 'HYDE_SERVER_VITE' => 'enabled'])
->andReturnSelf();

Process::shouldReceive('start')
->once()
->with('npm run dev')
->andReturn($mockViteProcess);

Process::shouldReceive('start')
->once()
->withArgs(function (string $command, Closure $output) {
$output('stdout', 'server output');

return $command === "php -S localhost:8080 {$this->binaryPath()}";
})
->andReturn($mockServerProcess);

$this->artisan('serve --no-ansi --vite')
->expectsOutput('Starting the HydeRC server... Use Ctrl+C to stop')
->expectsOutput('server output')
->expectsOutput('vite latest output')
->assertExitCode(0);
}

public function testHydeServeCommandWithViteOptionButViteNotRunning()
{
$mockViteProcess = mock(InvokedProcess::class);
$mockViteProcess->shouldReceive('running')
->once()
->andReturn(false);

$mockServerProcess = mock(InvokedProcess::class);
$mockServerProcess->shouldReceive('running')
->times(2)
->andReturn(true, false);

Process::shouldReceive('forever')
->twice()
->withNoArgs()
->andReturnSelf();

Process::shouldReceive('env')
->once()
->with(['HYDE_SERVER_REQUEST_OUTPUT' => false, 'HYDE_SERVER_VITE' => 'enabled'])
->andReturnSelf();

Process::shouldReceive('start')
->once()
->with('npm run dev')
->andReturn($mockViteProcess);

Process::shouldReceive('start')
->once()
->withArgs(function (string $command, Closure $handle) {
return $command === "php -S localhost:8080 {$this->binaryPath()}";
})
->andReturn($mockServerProcess);

$this->artisan('serve --no-ansi --vite')
->expectsOutput('Starting the HydeRC server... Use Ctrl+C to stop')
->assertExitCode(0);
}

public function testHydeServeCommandWithViteOptionThrowsWhenPortIsInUse()
{
$socket = stream_socket_server('tcp://127.0.0.1:5173');

$this->artisan('serve --vite')
->expectsOutputToContain('Unable to start Vite server: Port 5173 is already in use')
->assertExitCode(1);

stream_socket_shutdown($socket, STREAM_SHUT_RDWR);
}

protected function binaryPath(): string
{
return Hyde::path('vendor/hyde/realtime-compiler/bin/server.php');
Expand Down
Loading