diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c25c64b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +# Ignore all test and documentation with "export-ignore". +/.gitattributes export-ignore +/.gitignore export-ignore +/.github export-ignore +/phpunit.xml.dist export-ignore +/tests export-ignore +/docs export-ignore \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b62a76b --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +/vendor +/storage/framework/testing +.env +.phpunit.result.cache +npm-debug.log +yarn-error.log +composer.lock +bootstrap/cache/packages.php +bootstrap/cache/services.php + +docs/*.blade.php +docs/**/*.blade.php \ No newline at end of file diff --git a/.styleci.yml b/.styleci.yml new file mode 100644 index 0000000..9a4d524 --- /dev/null +++ b/.styleci.yml @@ -0,0 +1,11 @@ +preset: laravel + +disabled: + - concat_without_spaces + - not_operator_with_successor_space + - cast_spaces + - trailing_comma_in_multiline_array + - heredoc_to_nowdoc + - phpdoc_summary + +risky: false \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b9a294e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog +## Unreleased + +## v0.0.1 - January 7th, 2022 + +First release. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..64f2dc8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Hammerstone + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b51a36c --- /dev/null +++ b/README.md @@ -0,0 +1,208 @@ +# Sidecar SSR for InertiaJS + +> 🚨 This is currently very much in beta! + +## Overview + +This package provides a Sidecar function to run [Inertia server-side rendering](https://inertiajs.com/server-side-rendering) on AWS Lambda. + +Sidecar packages, deploys, and executes AWS Lambda functions from your Laravel application. + +It works with any Laravel 7 or 8 application, hosted anywhere including your local machine, Vapor, Heroku, a shared virtual server, or any other kind of hosting environment. + +- [Sidecar docs](https://hammerstone.dev/sidecar/docs/main/overview) +- [Sidecar GitHub](https://github.com/hammerstonedev/sidecar) + +You can see a fully working Jetstream + Inertia + Sidecar demo app at hammerstone/TODO + +## Enabling SSR + +Following the [official Inertia docs](https://inertiajs.com/server-side-rendering#enabling-ssr) on enabling SSR is a good place to start, but there are a few things you can skip: + +- You do not need to `npm install @inertiajs/server` +- You do not need to `npm install webpack-node-externals` +- Come back here when you get to the "Building your application" section + +### Updating Inertia + +Make sure that `inertia/laravel-inertia` is at least version `0.5.1`. + +### Installation + +To require this package, run the following: + +```shell +composer require hammerstone/sidecar-inertia +``` + +This will install Sidecar as well. + +### Use the Sidecar Gateway + +Update your `AppServiceProvider` to use the `SidecarGateway` as the default Inertia SSR Gateway + +```php +namespace App\Providers; + +use Hammerstone\Sidecar\Inertia\SidecarGateway; +use Illuminate\Support\ServiceProvider; +use Inertia\Ssr\Gateway; + +class AppServiceProvider extends ServiceProvider +{ + public function register() + { + // Use Sidecar to run Inertia SSR. + $this->app->instance(Gateway::class, new SidecarGateway); + } +} +``` + +### Update Configuration + +Update your `config/inertia.php` to include the Sidecar settings + +```php + [ + 'enabled' => true, + + 'sidecar' => [ + // The Sidecar function that handles the SSR. + 'handler' => \Hammerstone\Sidecar\Inertia\SSR::class, + + // Log some stats on how long each Lambda request takes. + 'timings' => false, + + // Throw exceptions, should they occur. + 'debug' => env('APP_DEBUG', false), + ], + ], + + // ... +]; +``` + +### Configure Sidecar + +If you haven't already, you'll need to configure Sidecar. + +Publish the `sidecar.php` configuration file by running + +```shell +php artisan sidecar:install +``` + +To configure your Sidecar AWS credentials interactively, you can run + +```shell +php artisan sidecar:configure +``` + +The [official Sidecar docs](https://hammerstone.dev/sidecar/docs/main/configuration) go into much further detail. + +Now update your `config/sidecar.php` to include the function shipped with this package. + +```php + [ + \Hammerstone\Sidecar\Inertia\SSR::class + ], + + // ... +]; +``` + +### Updating Your JavaScript + +> This only covers Vue3, please follow the Inertia docs for Vue2 or React, and please open any issues. + +You'll need to update your `webpack.ssr.mix.js` file. This _should_ work for most cases, but please open any issues for errors you run into. (This is based on the Inertia docs, with slight modifications.) + +```js +const path = require('path') +const mix = require('laravel-mix') + +mix + .js('resources/js/ssr.js', 'public/js') + .options({ + manifest: false + }) + .vue({ + version: 3, + useVueStyleLoader: true, + options: { + optimizeSSR: true + } + }) + .alias({ + '@': path.resolve('resources/js') + }) + .webpackConfig({ + target: 'node', + externals: { + node: true, + }, + resolve: { + alias: { + // Uncomment if you're using Ziggy. + // ziggy: path.resolve('vendor/tightenco/ziggy/src/js'), + }, + }, + }) +``` + +And update your `resources/js/ssr.js` to look something like this. The specifics may vary based on your application. If you're using [Ziggy](https://github.com/tighten/ziggy), you'll want to uncomment the Ziggy stuff. (This is based on the Inertia docs, with slight modifications.) + +```js +import {createSSRApp, h} from 'vue' +import {renderToString} from '@vue/server-renderer' +import {createInertiaApp} from '@inertiajs/inertia-vue3' +// import route from 'ziggy'; + +// This is the Lambda handler that will respond to the Sidecar invocation. +exports.handler = async function (event) { + return await createInertiaApp({ + page: event, + render: renderToString, + resolve: (name) => require(`./Pages/${name}`), + setup({app, props, plugin}) { + // const Ziggy = { + // ...event.props.ziggy, + // location: new URL(event.props.ziggy.url) + // } + + return createSSRApp({ + render: () => h(app, props), + }).use(plugin).mixin({ + methods: { + // Pull the ziggy config off of the event. + // route: (name, params, absolute, config = Ziggy) => route(name, params, absolute, config), + }, + }) + }, + }); +} +``` + +### Ziggy (Optional) + +If you are using Ziggy, you'll need to pass the Ziggy information along to your Lambda. You can do that by adding the following to your +`HandleInertiaRequests` middleware. + +```php +class HandleInertiaRequests extends Middleware +{ + public function share(Request $request) + { + return array_merge(parent::share($request), [ + 'ziggy' => (new Ziggy)->toArray() + ]); + } +} +``` + diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..1a1ab38 --- /dev/null +++ b/composer.json @@ -0,0 +1,39 @@ +{ + "name": "hammerstone/sidecar-inertia", + "description": "A Laravel package to render Inertia apps on AWS Lambda.", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Aaron Francis", + "email": "aaron@hammerstone.dev" + } + ], + "require": { + "php": "^7.2|^8.0", + "hammerstone/sidecar": "v0.3.4", + "inertiajs/inertia-laravel": "^0.5.1", + "illuminate/support": "^8.0" + }, + "require-dev": { + "orchestra/testbench": "^5.0|^6.0", + "mockery/mockery": "^1.3.3", + "phpunit/phpunit": "^8.4" + }, + "autoload": { + "psr-4": { + "Hammerstone\\Sidecar\\Inertia\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Hammerstone\\Sidecar\\Inertia\\Tests\\": "tests/" + } + }, + "extra": { + "laravel": { + "providers": [ + ] + } + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..ab5011a --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,27 @@ + + + + + tests/Unit + + + tests/Integration + + + + + src/ + + + + + \ No newline at end of file diff --git a/src/SSR.php b/src/SSR.php new file mode 100644 index 0000000..4cb70d0 --- /dev/null +++ b/src/SSR.php @@ -0,0 +1,72 @@ + + */ + +namespace Hammerstone\Sidecar\Inertia; + +use Hammerstone\Sidecar\LambdaFunction; +use Hammerstone\Sidecar\Sidecar; +use Symfony\Component\Process\Process; + +class SSR extends LambdaFunction +{ + public function name() + { + return 'Inertia-SSR'; + } + + public function memory() + { + // The more memory you give your function, the faster it will + // run, but the more expensive it will be. You will need to + // find the settings that are right for your application. + return 1024; + } + + public function handler() + { + // This format meets AWS requirements. The compiled SSR file + // is "ssr.js", built by Laravel Mix. The function that + // handles the incoming event is called "handler." + return 'public/js/ssr.handler'; + } + + public function package() + { + + // Since webpack compiles everything down to a single + // file, it's the only thing we need to ship. + return [ + 'public/js/ssr.js', + ]; + } + + public function beforeDeployment() + { + Sidecar::log('Executing beforeDeployment hooks'); + + // Compile the SSR bundle before deploying. + $this->compileJavascript(); + } + + protected function compileJavascript() + { + Sidecar::log('Compiling Inertia SSR bundle.'); + + $command = ['npx', 'mix', '--mix-config=webpack.ssr.mix.js']; + + if (Sidecar::getEnvironment() === 'production') { + $command[] = '--production'; + } + + Sidecar::log('Running ' . implode($command, ' ')); + + $process = new Process($command, $cwd = base_path(), $env = []); + + // mustRun will throw an exception if it fails, which is what we want. + $process->setTimeout(60)->disableOutput()->mustRun(); + + Sidecar::log('JavaScript SSR bundle compiled!'); + } +} diff --git a/src/SidecarGateway.php b/src/SidecarGateway.php new file mode 100644 index 0000000..2714d74 --- /dev/null +++ b/src/SidecarGateway.php @@ -0,0 +1,61 @@ + + */ + +namespace Hammerstone\Sidecar\Inertia; + +use Exception; +use Hammerstone\Sidecar\LambdaFunction; +use Illuminate\Support\Facades\Config; +use Illuminate\Support\Facades\Log; +use Inertia\Ssr\Gateway; +use Inertia\Ssr\Response; +use Throwable; + +class SidecarGateway implements Gateway +{ + + public function dispatch(array $page): ?Response + { + if (!Config::get('inertia.ssr.enabled', false)) { + return null; + } + + if (!$handler = Config::get('inertia.ssr.sidecar.handler')) { + return null; + } + + try { + return $this->execute($handler, $page); + } catch (Throwable $e) { + if (Config::get('inertia.ssr.sidecar.debug')) { + throw $e; + } + + return null; + } + } + + protected function execute($handler, array $page): ?Response + { + $handler = app($handler); + + if (!$handler instanceof LambdaFunction) { + throw new Exception('The configured Sidecar SSR Handler is not a Sidecar function.'); + } + + $result = $handler::execute($page)->throw(); + + if (Config::get('inertia.ssr.sidecar.timings')) { + Log::info('Sending SSR request to Lambda', $result->report()); + } + + $response = $result->body(); + + return new Response( + implode("\n", $response['head']), + $response['body'] + ); + } +} diff --git a/tests/Unit/BaseTest.php b/tests/Unit/BaseTest.php new file mode 100644 index 0000000..5596bf2 --- /dev/null +++ b/tests/Unit/BaseTest.php @@ -0,0 +1,19 @@ + + */ + +namespace Hammerstone\Sidecar\Tests\Unit; + +use Hammerstone\Sidecar\Providers\SidecarServiceProvider; +use Orchestra\Testbench\TestCase; + +abstract class BaseTest extends TestCase +{ + protected function getPackageProviders($app) + { + return [ + // @TODO. + ]; + } +}