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

Add Inertia SSR directives #339

Merged
merged 8 commits into from
Dec 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 3 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@
"laravel/framework": "^6.0|^7.0|^8.74"
},
"require-dev": {
"roave/security-advisories": "dev-master",
"mockery/mockery": "^1.3.3",
"orchestra/testbench": "^4.0|^5.0|^6.4",
"phpunit/phpunit": "^8.0|^9.5.8"
"phpunit/phpunit": "^8.0|^9.5.8",
"roave/security-advisories": "dev-master"
},
"extra": {
"laravel": {
Expand Down
22 changes: 22 additions & 0 deletions config/inertia.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,28 @@

return [

/*
|--------------------------------------------------------------------------
| Server Side Rendering
|--------------------------------------------------------------------------
|
| These options configures if and how Inertia uses Server Side Rendering
| to pre-render the initial visits made to your application's pages.
|
| Do note that enabling these options will NOT automatically make SSR work,
| as a separate rendering service needs to be available. To learn more,
| please visit https://inertiajs.com/server-side-rendering
|
*/

'ssr' => [

'enabled' => false,

'url' => 'http://127.0.0.1:8080/render',

],

/*
|--------------------------------------------------------------------------
| Testing
Expand Down
52 changes: 52 additions & 0 deletions src/Directive.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

namespace Inertia;

class Directive
{
/**
* Compiles the "@inertia" directive.
*
* @param string $expression
* @return string
*/
public static function compile($expression = ''): string
{
$id = trim(trim($expression), "\'\"") ?: 'app';

$template = '<?php
if (!isset($__inertiaSsr)) {
$__inertiaSsr = app(\Inertia\Ssr\Gateway::class)->dispatch($page);
}

if ($__inertiaSsr instanceof \Inertia\Ssr\Response) {
echo $__inertiaSsr->body;
} else {
?><div id="'.$id.'" data-page="{{ json_encode($page) }}"></div><?php
}
?>';

return implode(' ', array_map('trim', explode("\n", $template)));
}

/**
* Compiles the "@inertiaHead" directive.
*
* @param string $expression
* @return string
*/
public static function compileHead($expression = ''): string
{
$template = '<?php
if (!isset($__inertiaSsr)) {
$__inertiaSsr = app(\Inertia\Ssr\Gateway::class)->dispatch($page);
}

if ($__inertiaSsr instanceof \Inertia\Ssr\Response) {
echo $__inertiaSsr->head;
}
?>';

return implode(' ', array_map('trim', explode("\n", $template)));
}
}
12 changes: 7 additions & 5 deletions src/ServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
use Illuminate\Support\ServiceProvider as BaseServiceProvider;
use Illuminate\Testing\TestResponse;
use Illuminate\View\FileViewFinder;
use Inertia\Ssr\Gateway;
use Inertia\Ssr\HttpGateway;
use Inertia\Testing\TestResponseMacros;
use LogicException;
use ReflectionException;
Expand All @@ -18,6 +20,7 @@ class ServiceProvider extends BaseServiceProvider
public function register(): void
{
$this->app->singleton(ResponseFactory::class);
$this->app->bind(Gateway::class, HttpGateway::class);

$this->mergeConfigFrom(
__DIR__.'/../config/inertia.php',
Expand All @@ -39,19 +42,18 @@ public function register(): void

public function boot(): void
{
$this->registerBladeDirective();
$this->registerBladeDirectives();
$this->registerConsoleCommands();

$this->publishes([
__DIR__.'/../config/inertia.php' => config_path('inertia.php'),
]);
}

protected function registerBladeDirective(): void
protected function registerBladeDirectives(): void
{
Blade::directive('inertia', function () {
return '<div id="app" data-page="{{ json_encode($page) }}"></div>';
});
Blade::directive('inertia', [Directive::class, 'compile']);
Blade::directive('inertiaHead', [Directive::class, 'compileHead']);
}

protected function registerConsoleCommands(): void
Expand Down
14 changes: 14 additions & 0 deletions src/Ssr/Gateway.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Inertia\Ssr;

interface Gateway
{
/**
* Dispatch the Inertia page to the Server Side Rendering engine.
*
* @param array $page
* @return Response|null
*/
public function dispatch(array $page): ?Response;
}
36 changes: 36 additions & 0 deletions src/Ssr/HttpGateway.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace Inertia\Ssr;

use Exception;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Http;

class HttpGateway implements Gateway
{
/**
* Dispatch the Inertia page to the Server Side Rendering engine.
*
* @param array $page
* @return Response|null
*/
public function dispatch(array $page): ?Response
{
if (! Config::get('inertia.ssr.enabled', false)) {
return null;
}

$url = Config::get('inertia.ssr.url', 'http://127.0.0.1:8080/render');

try {
$response = Http::post($url, $page)->throw()->json();
} catch (Exception $e) {
return null;
}

return new Response(
implode("\n", $response['head']),
$response['body']
);
}
}
28 changes: 28 additions & 0 deletions src/Ssr/Response.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Inertia\Ssr;

class Response
{
/**
* @var string
*/
public $head;

/**
* @var string
*/
public $body;

/**
* Prepare the Inertia Server Side Rendering (SSR) response.
*
* @param string $head
* @param string $body
*/
public function __construct(string $head, string $body)
{
$this->head = $head;
$this->body = $body;
}
}
3 changes: 1 addition & 2 deletions tests/ControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ public function test_controller_returns_an_inertia_response(): void

$response = $this->get('/');

$page = $response->viewData('page');
$this->assertEquals($page, [
$this->assertEquals($response->viewData('page'), [
'component' => 'User/Edit',
'props' => [
'user' => ['name' => 'Jonathan'],
Expand Down
145 changes: 145 additions & 0 deletions tests/DirectiveTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php

namespace Inertia\Tests;

use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Facades\Config;
use Illuminate\View\Compilers\BladeCompiler;
use Illuminate\View\Engines\PhpEngine;
use Illuminate\View\Factory;
use Illuminate\View\View;
use Inertia\Directive;
use Inertia\Ssr\Gateway;
use Inertia\Tests\Stubs\FakeGateway;
use Mockery as m;

class DirectiveTest extends TestCase
{
/**
* @var Filesystem|m\MockInterface
*/
private $filesystem;

/**
* @var BladeCompiler
*/
protected $compiler;

/**
* Example Page Objects.
*/
protected const EXAMPLE_PAGE_OBJECT = ['component' => 'Foo/Bar', 'props' => ['foo' => 'bar'], 'url' => '/test', 'version' => ''];

public function setUp(): void
{
parent::setUp();

$this->app->bind(Gateway::class, FakeGateway::class);
$this->filesystem = m::mock(Filesystem::class);

$this->compiler = new BladeCompiler($this->filesystem, __DIR__.'/cache/views');
$this->compiler->directive('inertia', [Directive::class, 'compile']);
$this->compiler->directive('inertiaHead', [Directive::class, 'compileHead']);
}

protected function tearDown(): void
{
m::close();
parent::tearDown();
}

protected function renderView($contents, $data = [])
{
// First, we'll create a temporary file, and use compileString to 'emulate' compilation of our view.
// This skips caching, and a bunch of other logic that's not relevant for what we need here.
$path = tempnam(sys_get_temp_dir(), 'inertia_tests_render_');
file_put_contents($path, $this->compiler->compileString($contents));

// Next, we'll 'render' out compiled view.
$view = new View(
m::mock(Factory::class),
new PhpEngine(new Filesystem()),
'fake-view',
$path,
$data
);

// Then, we'll just hack and slash our way to success..
$view->getFactory()->allows('incrementRender')->once();
$view->getFactory()->allows('callComposer')->once();
$view->getFactory()->allows('getShared')->once()->andReturn([]);
$view->getFactory()->allows('decrementRender')->once();
$view->getFactory()->allows('flushStateIfDoneRendering')->once();
$view->getFactory()->allows('flushState');

try {
$output = $view->render();
@unlink($path);
} catch (\Throwable $e) {
@unlink($path);
throw $e;
}

return $output;
}

public function test_inertia_directive_renders_the_root_element(): void
{
$html = '<div id="app" data-page="{&quot;component&quot;:&quot;Foo\/Bar&quot;,&quot;props&quot;:{&quot;foo&quot;:&quot;bar&quot;},&quot;url&quot;:&quot;\/test&quot;,&quot;version&quot;:&quot;&quot;}"></div>';

$this->assertSame($html, $this->renderView('@inertia', ['page' => self::EXAMPLE_PAGE_OBJECT]));
$this->assertSame($html, $this->renderView('@inertia()', ['page' => self::EXAMPLE_PAGE_OBJECT]));
$this->assertSame($html, $this->renderView('@inertia("")', ['page' => self::EXAMPLE_PAGE_OBJECT]));
$this->assertSame($html, $this->renderView("@inertia('')", ['page' => self::EXAMPLE_PAGE_OBJECT]));
}

public function test_inertia_directive_renders_server_side_rendered_content_when_enabled(): void
{
Config::set(['inertia.ssr.enabled' => true]);

$this->assertSame(
'<p>This is some example SSR content</p>',
$this->renderView('@inertia', ['page' => self::EXAMPLE_PAGE_OBJECT])
);
}

public function test_inertia_directive_can_use_a_different_root_element_id(): void
{
$html = '<div id="foo" data-page="{&quot;component&quot;:&quot;Foo\/Bar&quot;,&quot;props&quot;:{&quot;foo&quot;:&quot;bar&quot;},&quot;url&quot;:&quot;\/test&quot;,&quot;version&quot;:&quot;&quot;}"></div>';

$this->assertSame($html, $this->renderView('@inertia(foo)', ['page' => self::EXAMPLE_PAGE_OBJECT]));
$this->assertSame($html, $this->renderView("@inertia('foo')", ['page' => self::EXAMPLE_PAGE_OBJECT]));
$this->assertSame($html, $this->renderView('@inertia("foo")', ['page' => self::EXAMPLE_PAGE_OBJECT]));
}

public function test_inertia_head_directive_renders_nothing(): void
{
$this->assertEmpty($this->renderView('@inertiaHead', ['page' => self::EXAMPLE_PAGE_OBJECT]));
}

public function test_inertia_head_directive_renders_server_side_rendered_head_elements_when_enabled(): void
{
Config::set(['inertia.ssr.enabled' => true]);

$this->assertSame(
"<meta charset=\"UTF-8\" />\n<title inertia>Example SSR Title</title>\n",
$this->renderView('@inertiaHead', ['page' => self::EXAMPLE_PAGE_OBJECT])
);
}

public function test_the_server_side_rendering_request_is_dispatched_only_once_per_request(): void
{
Config::set(['inertia.ssr.enabled' => true]);
$this->app->instance(Gateway::class, $gateway = new FakeGateway());

$view = "<!DOCTYPE html>\n<html>\n<head>\n@inertiaHead\n</head>\n<body>\n@inertia\n</body>\n</html>";
$expected = "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"UTF-8\" />\n<title inertia>Example SSR Title</title>\n</head>\n<body>\n<p>This is some example SSR content</p></body>\n</html>";

$this->assertSame(
$expected,
$this->renderView($view, ['page' => self::EXAMPLE_PAGE_OBJECT])
);

$this->assertSame(1, $gateway->times);
}
}
2 changes: 1 addition & 1 deletion tests/ResponseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public function test_server_response(): void
$this->assertSame('Jonathan', $page['props']['user']['name']);
$this->assertSame('/user/123', $page['url']);
$this->assertSame('123', $page['version']);
$this->assertSame('<div id="app" data-page="{&quot;component&quot;:&quot;User\/Edit&quot;,&quot;props&quot;:{&quot;user&quot;:{&quot;name&quot;:&quot;Jonathan&quot;}},&quot;url&quot;:&quot;\/user\/123&quot;,&quot;version&quot;:&quot;123&quot;}"></div>'."\n", $view->render());
$this->assertSame('<div id="app" data-page="{&quot;component&quot;:&quot;User\/Edit&quot;,&quot;props&quot;:{&quot;user&quot;:{&quot;name&quot;:&quot;Jonathan&quot;}},&quot;url&quot;:&quot;\/user\/123&quot;,&quot;version&quot;:&quot;123&quot;}"></div>', $view->render());
}

public function test_xhr_response(): void
Expand Down
5 changes: 1 addition & 4 deletions tests/ServiceProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@ class ServiceProviderTest extends TestCase
{
public function test_blade_directive_is_registered(): void
{
$directives = Blade::getCustomDirectives();

$this->assertArrayHasKey('inertia', $directives);
$this->assertEquals('<div id="app" data-page="{{ json_encode($page) }}"></div>', $directives['inertia']());
$this->assertArrayHasKey('inertia', Blade::getCustomDirectives());
}

public function test_request_macro_is_registered(): void
Expand Down
Loading