Skip to content

Commit 8f969ff

Browse files
Add Inertia SSR directives (#339)
Co-authored-by: Claudio Dekker <[email protected]>
1 parent d42eb01 commit 8f969ff

12 files changed

+347
-14
lines changed

composer.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@
3030
"laravel/framework": "^6.0|^7.0|^8.74"
3131
},
3232
"require-dev": {
33-
"roave/security-advisories": "dev-master",
33+
"mockery/mockery": "^1.3.3",
3434
"orchestra/testbench": "^4.0|^5.0|^6.4",
35-
"phpunit/phpunit": "^8.0|^9.5.8"
35+
"phpunit/phpunit": "^8.0|^9.5.8",
36+
"roave/security-advisories": "dev-master"
3637
},
3738
"extra": {
3839
"laravel": {

config/inertia.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,28 @@
22

33
return [
44

5+
/*
6+
|--------------------------------------------------------------------------
7+
| Server Side Rendering
8+
|--------------------------------------------------------------------------
9+
|
10+
| These options configures if and how Inertia uses Server Side Rendering
11+
| to pre-render the initial visits made to your application's pages.
12+
|
13+
| Do note that enabling these options will NOT automatically make SSR work,
14+
| as a separate rendering service needs to be available. To learn more,
15+
| please visit https://inertiajs.com/server-side-rendering
16+
|
17+
*/
18+
19+
'ssr' => [
20+
21+
'enabled' => false,
22+
23+
'url' => 'http://127.0.0.1:8080/render',
24+
25+
],
26+
527
/*
628
|--------------------------------------------------------------------------
729
| Testing

src/Directive.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
namespace Inertia;
4+
5+
class Directive
6+
{
7+
/**
8+
* Compiles the "@inertia" directive.
9+
*
10+
* @param string $expression
11+
* @return string
12+
*/
13+
public static function compile($expression = ''): string
14+
{
15+
$id = trim(trim($expression), "\'\"") ?: 'app';
16+
17+
$template = '<?php
18+
if (!isset($__inertiaSsr)) {
19+
$__inertiaSsr = app(\Inertia\Ssr\Gateway::class)->dispatch($page);
20+
}
21+
22+
if ($__inertiaSsr instanceof \Inertia\Ssr\Response) {
23+
echo $__inertiaSsr->body;
24+
} else {
25+
?><div id="'.$id.'" data-page="{{ json_encode($page) }}"></div><?php
26+
}
27+
?>';
28+
29+
return implode(' ', array_map('trim', explode("\n", $template)));
30+
}
31+
32+
/**
33+
* Compiles the "@inertiaHead" directive.
34+
*
35+
* @param string $expression
36+
* @return string
37+
*/
38+
public static function compileHead($expression = ''): string
39+
{
40+
$template = '<?php
41+
if (!isset($__inertiaSsr)) {
42+
$__inertiaSsr = app(\Inertia\Ssr\Gateway::class)->dispatch($page);
43+
}
44+
45+
if ($__inertiaSsr instanceof \Inertia\Ssr\Response) {
46+
echo $__inertiaSsr->head;
47+
}
48+
?>';
49+
50+
return implode(' ', array_map('trim', explode("\n", $template)));
51+
}
52+
}

src/ServiceProvider.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
use Illuminate\Support\ServiceProvider as BaseServiceProvider;
1010
use Illuminate\Testing\TestResponse;
1111
use Illuminate\View\FileViewFinder;
12+
use Inertia\Ssr\Gateway;
13+
use Inertia\Ssr\HttpGateway;
1214
use Inertia\Testing\TestResponseMacros;
1315
use LogicException;
1416
use ReflectionException;
@@ -18,6 +20,7 @@ class ServiceProvider extends BaseServiceProvider
1820
public function register(): void
1921
{
2022
$this->app->singleton(ResponseFactory::class);
23+
$this->app->bind(Gateway::class, HttpGateway::class);
2124

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

4043
public function boot(): void
4144
{
42-
$this->registerBladeDirective();
45+
$this->registerBladeDirectives();
4346
$this->registerConsoleCommands();
4447

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

50-
protected function registerBladeDirective(): void
53+
protected function registerBladeDirectives(): void
5154
{
52-
Blade::directive('inertia', function () {
53-
return '<div id="app" data-page="{{ json_encode($page) }}"></div>';
54-
});
55+
Blade::directive('inertia', [Directive::class, 'compile']);
56+
Blade::directive('inertiaHead', [Directive::class, 'compileHead']);
5557
}
5658

5759
protected function registerConsoleCommands(): void

src/Ssr/Gateway.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace Inertia\Ssr;
4+
5+
interface Gateway
6+
{
7+
/**
8+
* Dispatch the Inertia page to the Server Side Rendering engine.
9+
*
10+
* @param array $page
11+
* @return Response|null
12+
*/
13+
public function dispatch(array $page): ?Response;
14+
}

src/Ssr/HttpGateway.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace Inertia\Ssr;
4+
5+
use Exception;
6+
use Illuminate\Support\Facades\Config;
7+
use Illuminate\Support\Facades\Http;
8+
9+
class HttpGateway implements Gateway
10+
{
11+
/**
12+
* Dispatch the Inertia page to the Server Side Rendering engine.
13+
*
14+
* @param array $page
15+
* @return Response|null
16+
*/
17+
public function dispatch(array $page): ?Response
18+
{
19+
if (! Config::get('inertia.ssr.enabled', false)) {
20+
return null;
21+
}
22+
23+
$url = Config::get('inertia.ssr.url', 'http://127.0.0.1:8080/render');
24+
25+
try {
26+
$response = Http::post($url, $page)->throw()->json();
27+
} catch (Exception $e) {
28+
return null;
29+
}
30+
31+
return new Response(
32+
implode("\n", $response['head']),
33+
$response['body']
34+
);
35+
}
36+
}

src/Ssr/Response.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace Inertia\Ssr;
4+
5+
class Response
6+
{
7+
/**
8+
* @var string
9+
*/
10+
public $head;
11+
12+
/**
13+
* @var string
14+
*/
15+
public $body;
16+
17+
/**
18+
* Prepare the Inertia Server Side Rendering (SSR) response.
19+
*
20+
* @param string $head
21+
* @param string $body
22+
*/
23+
public function __construct(string $head, string $body)
24+
{
25+
$this->head = $head;
26+
$this->body = $body;
27+
}
28+
}

tests/ControllerTest.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ public function test_controller_returns_an_inertia_response(): void
2020

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

23-
$page = $response->viewData('page');
24-
$this->assertEquals($page, [
23+
$this->assertEquals($response->viewData('page'), [
2524
'component' => 'User/Edit',
2625
'props' => [
2726
'user' => ['name' => 'Jonathan'],

tests/DirectiveTest.php

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
<?php
2+
3+
namespace Inertia\Tests;
4+
5+
use Illuminate\Filesystem\Filesystem;
6+
use Illuminate\Support\Facades\Config;
7+
use Illuminate\View\Compilers\BladeCompiler;
8+
use Illuminate\View\Engines\PhpEngine;
9+
use Illuminate\View\Factory;
10+
use Illuminate\View\View;
11+
use Inertia\Directive;
12+
use Inertia\Ssr\Gateway;
13+
use Inertia\Tests\Stubs\FakeGateway;
14+
use Mockery as m;
15+
16+
class DirectiveTest extends TestCase
17+
{
18+
/**
19+
* @var Filesystem|m\MockInterface
20+
*/
21+
private $filesystem;
22+
23+
/**
24+
* @var BladeCompiler
25+
*/
26+
protected $compiler;
27+
28+
/**
29+
* Example Page Objects.
30+
*/
31+
protected const EXAMPLE_PAGE_OBJECT = ['component' => 'Foo/Bar', 'props' => ['foo' => 'bar'], 'url' => '/test', 'version' => ''];
32+
33+
public function setUp(): void
34+
{
35+
parent::setUp();
36+
37+
$this->app->bind(Gateway::class, FakeGateway::class);
38+
$this->filesystem = m::mock(Filesystem::class);
39+
40+
$this->compiler = new BladeCompiler($this->filesystem, __DIR__.'/cache/views');
41+
$this->compiler->directive('inertia', [Directive::class, 'compile']);
42+
$this->compiler->directive('inertiaHead', [Directive::class, 'compileHead']);
43+
}
44+
45+
protected function tearDown(): void
46+
{
47+
m::close();
48+
parent::tearDown();
49+
}
50+
51+
protected function renderView($contents, $data = [])
52+
{
53+
// First, we'll create a temporary file, and use compileString to 'emulate' compilation of our view.
54+
// This skips caching, and a bunch of other logic that's not relevant for what we need here.
55+
$path = tempnam(sys_get_temp_dir(), 'inertia_tests_render_');
56+
file_put_contents($path, $this->compiler->compileString($contents));
57+
58+
// Next, we'll 'render' out compiled view.
59+
$view = new View(
60+
m::mock(Factory::class),
61+
new PhpEngine(new Filesystem()),
62+
'fake-view',
63+
$path,
64+
$data
65+
);
66+
67+
// Then, we'll just hack and slash our way to success..
68+
$view->getFactory()->allows('incrementRender')->once();
69+
$view->getFactory()->allows('callComposer')->once();
70+
$view->getFactory()->allows('getShared')->once()->andReturn([]);
71+
$view->getFactory()->allows('decrementRender')->once();
72+
$view->getFactory()->allows('flushStateIfDoneRendering')->once();
73+
$view->getFactory()->allows('flushState');
74+
75+
try {
76+
$output = $view->render();
77+
@unlink($path);
78+
} catch (\Throwable $e) {
79+
@unlink($path);
80+
throw $e;
81+
}
82+
83+
return $output;
84+
}
85+
86+
public function test_inertia_directive_renders_the_root_element(): void
87+
{
88+
$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>';
89+
90+
$this->assertSame($html, $this->renderView('@inertia', ['page' => self::EXAMPLE_PAGE_OBJECT]));
91+
$this->assertSame($html, $this->renderView('@inertia()', ['page' => self::EXAMPLE_PAGE_OBJECT]));
92+
$this->assertSame($html, $this->renderView('@inertia("")', ['page' => self::EXAMPLE_PAGE_OBJECT]));
93+
$this->assertSame($html, $this->renderView("@inertia('')", ['page' => self::EXAMPLE_PAGE_OBJECT]));
94+
}
95+
96+
public function test_inertia_directive_renders_server_side_rendered_content_when_enabled(): void
97+
{
98+
Config::set(['inertia.ssr.enabled' => true]);
99+
100+
$this->assertSame(
101+
'<p>This is some example SSR content</p>',
102+
$this->renderView('@inertia', ['page' => self::EXAMPLE_PAGE_OBJECT])
103+
);
104+
}
105+
106+
public function test_inertia_directive_can_use_a_different_root_element_id(): void
107+
{
108+
$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>';
109+
110+
$this->assertSame($html, $this->renderView('@inertia(foo)', ['page' => self::EXAMPLE_PAGE_OBJECT]));
111+
$this->assertSame($html, $this->renderView("@inertia('foo')", ['page' => self::EXAMPLE_PAGE_OBJECT]));
112+
$this->assertSame($html, $this->renderView('@inertia("foo")', ['page' => self::EXAMPLE_PAGE_OBJECT]));
113+
}
114+
115+
public function test_inertia_head_directive_renders_nothing(): void
116+
{
117+
$this->assertEmpty($this->renderView('@inertiaHead', ['page' => self::EXAMPLE_PAGE_OBJECT]));
118+
}
119+
120+
public function test_inertia_head_directive_renders_server_side_rendered_head_elements_when_enabled(): void
121+
{
122+
Config::set(['inertia.ssr.enabled' => true]);
123+
124+
$this->assertSame(
125+
"<meta charset=\"UTF-8\" />\n<title inertia>Example SSR Title</title>\n",
126+
$this->renderView('@inertiaHead', ['page' => self::EXAMPLE_PAGE_OBJECT])
127+
);
128+
}
129+
130+
public function test_the_server_side_rendering_request_is_dispatched_only_once_per_request(): void
131+
{
132+
Config::set(['inertia.ssr.enabled' => true]);
133+
$this->app->instance(Gateway::class, $gateway = new FakeGateway());
134+
135+
$view = "<!DOCTYPE html>\n<html>\n<head>\n@inertiaHead\n</head>\n<body>\n@inertia\n</body>\n</html>";
136+
$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>";
137+
138+
$this->assertSame(
139+
$expected,
140+
$this->renderView($view, ['page' => self::EXAMPLE_PAGE_OBJECT])
141+
);
142+
143+
$this->assertSame(1, $gateway->times);
144+
}
145+
}

tests/ResponseTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public function test_server_response(): void
4545
$this->assertSame('Jonathan', $page['props']['user']['name']);
4646
$this->assertSame('/user/123', $page['url']);
4747
$this->assertSame('123', $page['version']);
48-
$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());
48+
$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());
4949
}
5050

5151
public function test_xhr_response(): void

0 commit comments

Comments
 (0)