├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github ├── FUNDING.yml └── workflows │ ├── commitlint.yml │ ├── main.yml │ └── release.yml ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg └── pre-commit ├── .huskyrc ├── .lintstagedrc ├── .nova └── Configuration.json ├── .nvmrc ├── .nycrc.json ├── .prettierrc ├── README.md ├── adonis-typings ├── context.ts ├── index.ts ├── inertia-middleware.ts ├── inertia.ts ├── request.ts └── route.ts ├── commands ├── Base.ts ├── Build.ts ├── Watch.ts └── index.ts ├── commitlint.config.js ├── instructions.ts ├── invoke.gif ├── japaFile.js ├── middleware └── Inertia.ts ├── package-lock.json ├── package.json ├── providers └── InertiaProvider │ ├── InertiaProvider.ts │ └── index.ts ├── release.config.js ├── src ├── Inertia.ts ├── LazyProp.ts ├── inertiaHelper.ts └── utils.ts ├── templates ├── inertia.txt ├── start.txt ├── view.txt └── webpack.ssr.config.txt ├── test ├── data.spec.ts ├── inertia-middleware.spec.ts ├── location.spec.ts ├── redirect.spec.ts ├── rendering.spec.ts ├── ssr.spec.ts ├── utils.ts ├── validation.spec.ts └── versioning.spec.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | [*] 3 | indent_style = space 4 | indent_size = 2 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.json] 11 | insert_final_newline = ignore 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | test/app/config/** 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:adonis/typescriptApp", "prettier"], 3 | "rules": { 4 | "no-console": "error" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: eidellev 2 | -------------------------------------------------------------------------------- /.github/workflows/commitlint.yml: -------------------------------------------------------------------------------- 1 | name: Lint Commit Messages 2 | on: [pull_request] 3 | 4 | jobs: 5 | commitlint: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | with: 10 | fetch-depth: 0 11 | - uses: wagoid/commitlint-github-action@v2.0.3 12 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | jobs: 8 | lint-and-test: 9 | name: Test 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v2 14 | - run: npm ci 15 | - run: npm run lint 16 | - run: npm test 17 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | branches: ['main'] 6 | jobs: 7 | release: 8 | name: Release 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-node@v3 13 | - run: npm ci 14 | - run: npm run build 15 | - name: Semantic Release 16 | uses: cycjimmy/semantic-release-action@v3 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | coverage 4 | .vscode 5 | .DS_STORE 6 | .env 7 | tmp 8 | .nyc_output 9 | yarn-error.log 10 | test/app 11 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx commitlint --edit 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": ["lint-staged"] 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{ts,d.ts}": ["eslint --fix"], 3 | "*.{json,md}": ["prettier --write"] 4 | } 5 | -------------------------------------------------------------------------------- /.nova/Configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "index.use_scm_ignored_files": false, 3 | "workspace.color": 2, 4 | "workspace.name": "Inertia.js Adonis" 5 | } 6 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/* 2 | -------------------------------------------------------------------------------- /.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "check-coverage": true, 3 | "reporter": ["text", "lcov"], 4 | "exclude": ["build/**", "japaFile.js", "test/", "providers/InertiaProvider/index.ts", "middleware/Inertia.ts"], 5 | "branches": 65, 6 | "lines": 65, 7 | "functions": 65, 8 | "statements": 65 9 | } 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | endOfLine: lf 2 | printWidth: 120 3 | singleQuote: true 4 | trailingComma: all 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Inertia.js AdonisJS Provider 2 | 3 | ![Typescript](https://img.shields.io/npm/types/typescript?style=for-the-badge) 4 | 5 | 6 | 7 | 8 | code style: prettier 9 | 10 | 11 | 12 | 13 | 14 | 15 | ## What is this all about? 16 | 17 | [Inertia.js](https://inertiajs.com/) lets you quickly build modern single-page 18 | React, Vue and Svelte apps using classic server-side routing and controllers. 19 | 20 | [AdonisJS](https://adonisjs.com/) is a fully featured web framework focused on 21 | productivity and developer ergonomics. 22 | 23 | ### Project goals 24 | 25 | - Feature parity with the official Inertia backend adapters 26 | - Full compatibility with all official client-side adapters 27 | - Easy setup 28 | - Quality documentation 29 | 30 | ## Installation 31 | 32 | ```shell 33 | # NPM 34 | npm i @eidellev/inertia-adonisjs 35 | 36 | # or Yarn 37 | yarn add @eidellev/inertia-adonisjs 38 | ``` 39 | 40 | ## Required AdonisJS libraries 41 | 42 | This library depends on two `AdonisJS` core libraries: `@adonisjs/view` and `@adonisjs/session`. 43 | If you started off with the `api` or `slim` project structure you will need to 44 | install these separately: 45 | 46 | ```shell 47 | # NPM 48 | npm i @adonisjs/view 49 | npm i @adonisjs/session 50 | 51 | # or Yarn 52 | yarn add @adonisjs/view 53 | yarn add @adonisjs/session 54 | 55 | # Additionally, you will need to configure the packages: 56 | node ace configure @adonisjs/view 57 | node ace configure @adonisjs/session 58 | ``` 59 | 60 | ## Setup 61 | 62 | You can register the package, generate additional files and install additional 63 | dependencies by running: 64 | 65 | ```shell 66 | node ace configure @eidellev/inertia-adonisjs 67 | ``` 68 | 69 | Inertia will query you on your preferences (e.g. which front-end framework you 70 | prefer and if you want server side rendering) and generate additional files. 71 | 72 | ![Invoke example](invoke.gif 'node ace invoke @eidellev/inertia-adonisjs') 73 | 74 | ### Configuration 75 | 76 | The configuration for `inertia-adonisjs` is set in `/config/inertia.ts`: 77 | 78 | ```typescript 79 | import { InertiaConfig } from '@ioc:EidelLev/Inertia'; 80 | 81 | export const inertia: InertiaConfig = { 82 | view: 'app', 83 | }; 84 | ``` 85 | 86 | ### Register inertia middleware 87 | 88 | Add Inertia middleware to `start/kernel.ts`: 89 | 90 | ```typescript 91 | Server.middleware.register([ 92 | () => import('@ioc:Adonis/Core/BodyParser'), 93 | () => import('@ioc:EidelLev/Inertia/Middleware'), 94 | ]); 95 | ``` 96 | 97 | ## Making an Inertia Response 98 | 99 | ```typescript 100 | export default class UsersController { 101 | public async index({ inertia, request }: HttpContextContract) { 102 | const users = await User.all(); 103 | 104 | return inertia.render('Users/IndexPage', { users }); 105 | } 106 | } 107 | ``` 108 | 109 | ## Making lazy Inertia Response 110 | 111 | Lazy responses are useful when you want to render a page without some data that should be loaded initially. 112 | 113 | ```typescript 114 | import Inertia from '@ioc:EidelLev/Inertia'; 115 | 116 | export default class UsersController { 117 | public async index({ inertia, request }: HttpContextContract) { 118 | const users = await User.all(); 119 | 120 | return inertia.render('Users/IndexPage', { 121 | users, 122 | lazyProp: Inertia.lazy(() => { 123 | return { lazy: 'too lazy' }; 124 | }), 125 | }); 126 | } 127 | } 128 | ``` 129 | 130 | The data will be loaded on demand by the explicit Inertia visit with option 131 | 132 | ```typescript 133 | { 134 | only: ['lazyProp']; 135 | } 136 | ``` 137 | 138 | ## Root template data 139 | 140 | There are situations where you may want to access your prop data in your root 141 | Edge template. For example, you may want to add a meta description tag, 142 | Twitter card meta tags, or Facebook Open Graph meta tags. 143 | 144 | ```blade 145 | 146 | ``` 147 | 148 | Sometimes you may even want to provide data that will not be sent to your 149 | JavaScript component. 150 | 151 | ```typescript 152 | return inertia.render('Users/IndexPage', { users }, { metadata: '...' : '...' }); 153 | ``` 154 | 155 | ## Shared data 156 | 157 | Sometimes you need to access certain data on numerous pages within your 158 | application. For example, a common use-case for this is showing the current user 159 | in the site header. Passing this data manually in each response isn't practical. 160 | In these situations shared data can be useful. 161 | 162 | In order to add shared props, edit `start/inertia.ts`: 163 | 164 | ```typescript 165 | import Inertia from '@ioc:EidelLev/Inertia'; 166 | 167 | Inertia.share({ 168 | errors: (ctx) => { 169 | return ctx.session.flashMessages.get('errors'); 170 | }, 171 | // Add more shared props here 172 | }); 173 | ``` 174 | 175 | ### Sharing route params 176 | 177 | Traditionally in Adonis, we have access to the context instance eg. params 178 | inside view (.edge) that we can use to help build our dynamic routes. 179 | But with inertia, we lose access to the context instance entirely. 180 | 181 | We can overcome this limitation by passing the context 182 | instance as a shared data prop: 183 | 184 | ```typescript 185 | // start/inertia.ts 186 | import Inertia from '@ioc:EidelLev/Inertia'; 187 | 188 | Inertia.share({ 189 | params: ({ params }) => params, 190 | }); 191 | ``` 192 | 193 | Then we can access the params in our component like so: 194 | 195 | ```typescript 196 | import { usePage } from '@inertiajs/inertia-react'; 197 | 198 | const { params } = usePage().props; 199 | stardust.route('users.show', { id: params.id }); 200 | ``` 201 | 202 | ## Route Helper 203 | 204 | If you have a page that doesn't need a corresponding controller method, like an 205 | FAQ or about page, you can route directly to a component. 206 | 207 | ```typescript 208 | // /start/routes.ts 209 | import Route from '@ioc:Adonis/Core/Route'; 210 | 211 | Route.inertia('about', 'About'); 212 | 213 | // You can also pass root template data as the third parameter: 214 | Route.inertia('about', 'About', { metadata: '...' }); 215 | ``` 216 | 217 | ## Redirects 218 | 219 | ### External redirects 220 | 221 | Sometimes it's necessary to redirect to an external website, or even another 222 | non-Inertia endpoint in your app, within an Inertia request. 223 | This is possible using a server-side initiated window.location visit. 224 | 225 | ```typescript 226 | Route.get('redirect', async ({ inertia }) => { 227 | inertia.location('https://inertiajs.com/redirects'); 228 | }); 229 | ``` 230 | 231 | ## Advanced 232 | 233 | ### Server-side rendering 234 | 235 | When Inertia detects that it's running in a Node.js environment, 236 | it will automatically render the provided page object to HTML and return it. 237 | 238 | #### Setting up server side rendering 239 | 240 | After configuring the the package using `ace configure` and enabling SSR, 241 | you will need to edit `webpack.ssr.config.js`. 242 | Set it up as you have your regular encore config to 243 | support your client-side framework of choice. 244 | 245 | #### Adding an additional entrypoint 246 | 247 | Create a new entrypoint `resources/js/ssr.js` (or `ssr.ts`/`ssr.tsx` 248 | if you prefer to use Typescript). 249 | 250 | Your entrypoint code will depend on your client-side framework of choice: 251 | 252 | ##### React 253 | 254 | ```jsx 255 | import React from 'react'; 256 | import ReactDOMServer from 'react-dom/server'; 257 | import { createInertiaApp } from '@inertiajs/react'; 258 | 259 | export default function render(page) { 260 | return createInertiaApp({ 261 | page, 262 | render: ReactDOMServer.renderToString, 263 | resolve: (name) => require(`./Pages/${name}`), 264 | setup: ({ App, props }) => , 265 | }); 266 | } 267 | ``` 268 | 269 | ##### Vue3 270 | 271 | ```javascript 272 | import { createSSRApp, h } from 'vue'; 273 | import { renderToString } from '@vue/server-renderer'; 274 | import { createInertiaApp } from '@inertiajs/vue3'; 275 | 276 | export default function render(page) { 277 | return createInertiaApp({ 278 | page, 279 | render: renderToString, 280 | resolve: (name) => require(`./Pages/${name}`), 281 | setup({ app, props, plugin }) { 282 | return createSSRApp({ 283 | render: () => h(app, props), 284 | }).use(plugin); 285 | }, 286 | }); 287 | } 288 | ``` 289 | 290 | ##### Vue2 291 | 292 | ```javascript 293 | import Vue from 'vue'; 294 | import { createRenderer } from 'vue-server-renderer'; 295 | import { createInertiaApp } from '@inertiajs/vue2'; 296 | 297 | export default function render(page) { 298 | return createInertiaApp({ 299 | page, 300 | render: createRenderer().renderToString, 301 | resolve: (name) => require(`./Pages/${name}`), 302 | setup({ app, props, plugin }) { 303 | Vue.use(plugin); 304 | return new Vue({ 305 | render: (h) => h(app, props), 306 | }); 307 | }, 308 | }); 309 | } 310 | ``` 311 | 312 | ##### Svelte 313 | 314 | ```javascript 315 | import { createInertiaApp } from '@inertiajs/svelte'; 316 | import createServer from '@inertiajs/svelte/server'; 317 | 318 | createServer((page) => 319 | createInertiaApp({ 320 | page, 321 | resolve: (name) => require(`./Pages/${name}.svelte`), 322 | }), 323 | ); 324 | ``` 325 | 326 | #### Starting the SSR dev server 327 | 328 | In a separate terminal run encore for SSR in watch mode: 329 | 330 | ```shell 331 | node ace ssr:watch 332 | ``` 333 | 334 | #### Building SSR for production 335 | 336 | ```shell 337 | node ace ssr:build 338 | ``` 339 | 340 | > ❗In most cases you do not want the compiled javascript for ssr committed 341 | > to source control. 342 | > To avoid it, please add the `inertia` directory to `.gitignore`. 343 | 344 | #### Customizing SSR output directory 345 | 346 | By default, SSR assets will be emitted to `inertia/ssr` directory. If you 347 | prefer to use a different directory, you can change it by setting the 348 | `buildDirectory` parameter: 349 | 350 | ```typescript 351 | // /config/inertia.ts 352 | { 353 | ssr: { 354 | enabled:true, 355 | buildDirectory: 'custom_path/ssr' 356 | } 357 | } 358 | ``` 359 | 360 | **You will also need to configure your SSR webpack config to output files to 361 | the same path.** 362 | 363 | #### Opting Out of SSR 364 | 365 | Building isomorphic apps often comes with additional complexity. 366 | In some cases you may prefer to render only certain public routes on the 367 | server while letting the rest be rendered on the client. 368 | Luckily you can easily opt out of SSR by configuring a list of components that 369 | will rendered on the server, excluding all other components. 370 | 371 | ```typescript 372 | { 373 | ssr: { 374 | enabled:true, 375 | allowList: ['HomePage', 'Login'] 376 | } 377 | } 378 | ``` 379 | 380 | ### Authentication 381 | 382 | AdonisJS provides us with powerful authentication and authorization APIs through 383 | `@adonisjs/auth`. After installing and setting up `@adonisjs/auth` you will need 384 | to set up exception handling to make it work with Inertia. 385 | 386 | First, let's use `@adonisjs/auth` in our controller to authenticate the user: 387 | 388 | ```typescript 389 | // app/Controllers/Http/AuthController.ts 390 | public async login({ auth, request, response }: HttpContextContract) { 391 | const loginSchema = schema.create({ 392 | email: schema.string({ trim: true }, [rules.email()]), 393 | password: schema.string(), 394 | }); 395 | 396 | const { email, password } = await request.validate({ 397 | schema: loginSchema, 398 | messages: { 399 | required: 'This field is required', 400 | email: 'Please enter a valid email', 401 | }, 402 | }); 403 | 404 | await auth.use('web').attempt(email, password); 405 | 406 | response.redirect('/'); 407 | } 408 | 409 | ``` 410 | 411 | By default, AdonisJS will send an HTTP 400 response, which inertia does not know 412 | how to handle. Therefore, we will intercept this exception and redirect back to 413 | our login page (we can also optionally preserve the error message with flash messages). 414 | 415 | ```typescript 416 | // app/Exceptions/Handler.ts 417 | 418 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; 419 | import HttpExceptionHandler from '@ioc:Adonis/Core/HttpExceptionHandler'; 420 | import Logger from '@ioc:Adonis/Core/Logger'; 421 | 422 | export default class ExceptionHandler extends HttpExceptionHandler { 423 | protected statusPages = { 424 | '403': 'errors/unauthorized', 425 | '404': 'errors/not-found', 426 | '500..599': 'errors/server-error', 427 | }; 428 | 429 | constructor() { 430 | super(Logger); 431 | } 432 | 433 | public async handle(error: any, ctx: HttpContextContract) { 434 | const { session, response } = ctx; 435 | 436 | /** 437 | * Handle failed authentication attempt 438 | */ 439 | if (['E_INVALID_AUTH_PASSWORD', 'E_INVALID_AUTH_UID'].includes(error.code)) { 440 | session.flash('errors', { login: error.message }); 441 | return response.redirect('/login'); 442 | } 443 | 444 | /** 445 | * Forward rest of the exceptions to the parent class 446 | */ 447 | return super.handle(error, ctx); 448 | } 449 | } 450 | ``` 451 | 452 | ### Asset Versioning 453 | 454 | To enable automatic asset refreshing, you simply need to tell Inertia what the 455 | current version of your assets is. This can be any string 456 | (letters, numbers, or a file hash), as long as it changes 457 | when your assets have been updated. 458 | 459 | To configure the current asset version, edit `start/inertia.ts`: 460 | 461 | ```typescript 462 | import Inertia from '@ioc:EidelLev/Inertia'; 463 | 464 | Inertia.version('v1'); 465 | 466 | // You can also pass a function that will be lazily evaluated: 467 | Inertia.version(() => 'v2'); 468 | ``` 469 | 470 | If you are using Adonis's built-in assets manager [webpack encore](https://docs.adonisjs.com/guides/assets-manager) 471 | you can also pass the path to the manifest file to Inertia and the current 472 | version will be set automatically: 473 | 474 | ```typescript 475 | Inertia.version(() => Inertia.manifestFile('public/assets/manifest.json')); 476 | ``` 477 | 478 | ## Setting Up View 479 | 480 | You can set up the inertia root div in your view using the @inertia tag: 481 | 482 | ```blade 483 | 484 | @inertia 485 | 486 | ``` 487 | 488 | ## Contributing 489 | 490 | This project happily accepts contributions. 491 | 492 | ### Getting Started 493 | 494 | After cloning the project run 495 | 496 | ```shell 497 | npm ci 498 | npx husky install # This sets up the project's git hooks 499 | ``` 500 | 501 | ### Before Making a Commit 502 | 503 | This project adheres to the [semantic versioning](https://semver.org/) convention, 504 | therefore all commits must be [conventional](https://github.com/conventional-changelog/commitlint). 505 | 506 | After staging your changes using `git add`, you can use the `commitlint CLI` 507 | to write your commit message: 508 | 509 | ```shell 510 | npx commit 511 | ``` 512 | 513 | ### Before Opening a Pull Request 514 | 515 | - Make sure you add tests that cover your changes 516 | - Make sure all tests pass: 517 | 518 | ```shell 519 | npm test 520 | ``` 521 | 522 | - Make sure eslint passes: 523 | 524 | ```shell 525 | npm run lint 526 | ``` 527 | 528 | - Make sure your commit message is valid: 529 | 530 | ```shell 531 | npx commitlint --edit 532 | ``` 533 | 534 | **Thank you to all the people who already contributed to this project!** 535 | 536 | ## Issues 537 | 538 | If you have a question or found a bug, feel free to [open an issue](https://github.com/eidellev/inertiajs-adonisjs/issues). 539 | -------------------------------------------------------------------------------- /adonis-typings/context.ts: -------------------------------------------------------------------------------- 1 | declare module '@ioc:Adonis/Core/HttpContext' { 2 | import { InertiaContract } from '@ioc:EidelLev/Inertia'; 3 | 4 | interface HttpContextContract { 5 | /** 6 | * InertiaJs 7 | */ 8 | inertia: InertiaContract; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /adonis-typings/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | /// 6 | -------------------------------------------------------------------------------- /adonis-typings/inertia-middleware.ts: -------------------------------------------------------------------------------- 1 | declare module '@ioc:EidelLev/Inertia/Middleware' { 2 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; 3 | 4 | export default class InertiaMiddleware { 5 | public handle(ctx: HttpContextContract, next: () => Promise); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /adonis-typings/inertia.ts: -------------------------------------------------------------------------------- 1 | declare module '@ioc:EidelLev/Inertia' { 2 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; 3 | import { ResponseContract } from '@ioc:Adonis/Core/Response'; 4 | 5 | export type ResponseProps = Record; 6 | export type RenderResponse = Promise | string | ResponseContract>; 7 | 8 | /** 9 | * Shared data types 10 | */ 11 | export type Data = string | number | object | boolean; 12 | export type LazyShare = (ctx: HttpContextContract) => LazyShareResponse | Promise; 13 | export type SharedData = Record; 14 | export type LazyShareResponse = Record; 15 | 16 | /** 17 | * Version data types 18 | */ 19 | export type VersionValue = string | number | undefined; 20 | export type LazyVersion = () => VersionValue | Promise; 21 | export type Version = VersionValue | LazyVersion | undefined; 22 | 23 | export interface InertiaLazyProp { 24 | lazyValue: ResponseProps | Promise; 25 | } 26 | 27 | export interface InertiaContract { 28 | /** 29 | * Render inertia response 30 | * 31 | * @param {string} component Page component 32 | * @param {ResponseProps} responseProps Props 33 | */ 34 | render(component: string, responseProps?: ResponseProps, pageOnlyProps?: ResponseProps): RenderResponse; 35 | 36 | /** 37 | * Redirect back with the correct HTTP status code 38 | */ 39 | redirectBack(): void; 40 | 41 | /** 42 | * Initiate a server-side redirect to an external resource 43 | * 44 | * See https://inertiajs.com/redirects 45 | */ 46 | location(url: string): void; 47 | } 48 | 49 | export interface InertiaConfig { 50 | /** 51 | * Which view template to render 52 | */ 53 | view: string; 54 | /** 55 | * SSR config 56 | */ 57 | ssr?: { 58 | enabled: boolean; 59 | /** 60 | * Programaticaly control which page components are rendered server-side
61 | * All other components will only be rendered on the client
62 | * This can be useful if you wish to avoid some of the complexities of building an isomorphic app
63 | * 64 | * @example 65 | * ```typescript 66 | * { 67 | * ssr: { 68 | * enabled:true, 69 | * allowList: ['HomePage', 'Login'], 70 | * autoreload: process.env.NODE_ENV === 'development' 71 | * } 72 | * } 73 | * ``` 74 | * 75 | */ 76 | allowList?: string[]; 77 | /** 78 | * Controls SSR build output directory 79 | * 80 | * **If you change this you will also need to change the output directory in your webpack encore config!** 81 | * @default './inertia/ssr' 82 | */ 83 | buildDirectory?: string; 84 | 85 | /** 86 | * Controls SSR autoreloading when content is changed. 87 | * 88 | * This should be set to true only during development. In production, you should set this to false for 89 | * performance reasons. 90 | * @default false 91 | */ 92 | autoreload?: boolean; 93 | }; 94 | } 95 | 96 | interface InertiaGlobal { 97 | /** 98 | * Shared props 99 | */ 100 | share: (data: SharedData) => InertiaGlobal; 101 | /** 102 | * Asset tracking 103 | */ 104 | version: (currentVersion: string | number | LazyVersion) => InertiaGlobal; 105 | 106 | /** 107 | * Returns md5 hash for manifest file at path 108 | * Can be used to automatically determine asset version 109 | * @param path manifest file path 110 | */ 111 | manifestFile: (path: string) => string; 112 | 113 | /** 114 | * Lazy prop (not loaded until explicitly requested) 115 | */ 116 | lazy(lazyPropCallback: () => ResponseProps | Promise): InertiaLazyProp; 117 | } 118 | 119 | export interface SsrRenderResult { 120 | head: string[]; 121 | body: string; 122 | } 123 | 124 | const Inertia: InertiaGlobal; 125 | 126 | export default Inertia; 127 | } 128 | -------------------------------------------------------------------------------- /adonis-typings/request.ts: -------------------------------------------------------------------------------- 1 | declare module '@ioc:Adonis/Core/Request' { 2 | interface RequestContract { 3 | /** 4 | * Returns `true` if this reuqest was made by an inertia app 5 | */ 6 | inertia: () => boolean; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /adonis-typings/route.ts: -------------------------------------------------------------------------------- 1 | import { ResponseProps } from '@ioc:EidelLev/Inertia'; 2 | 3 | declare module '@ioc:Adonis/Core/Route' { 4 | interface RouterContract { 5 | /** 6 | * Inertia route helper 7 | * 8 | * @param {string} pattern Path 9 | * @param {string} component The component you'd like inertia to render 10 | * @param {ResponseProps} pageOnlyProps View metadata that will be passed only to the edge view 11 | * @return {RouteContract} 12 | */ 13 | inertia: (pattern: string, component: string, pageOnlyProps?: ResponseProps) => RouterContract; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /commands/Base.ts: -------------------------------------------------------------------------------- 1 | import { BaseCommand } from '@adonisjs/ace'; 2 | import { spawn } from 'child_process'; 3 | 4 | /** 5 | * Base class to provide helpers for Mix commands 6 | */ 7 | export abstract class BaseSsrCommand extends BaseCommand { 8 | protected webpackConfig = 'webpack.ssr.config.js'; 9 | 10 | protected runScript(script: string, scriptEnv: NodeJS.ProcessEnv) { 11 | const child = spawn(script, { 12 | stdio: 'inherit', 13 | shell: true, 14 | env: { ...process.env, ...scriptEnv }, 15 | }); 16 | 17 | child.on('exit', (code, signal) => { 18 | if (code === null) { 19 | code = signal === 'SIGINT' ? 130 : 1; 20 | } 21 | 22 | process.exitCode = code; 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /commands/Build.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'fs'; 2 | import { BaseSsrCommand } from './Base'; 3 | 4 | /** 5 | * Command to watch assets 6 | */ 7 | export default class Watch extends BaseSsrCommand { 8 | public static commandName = 'ssr:build'; 9 | public static description = 'Build and watch files for changes'; 10 | public static settings = { 11 | stayAlive: true, 12 | }; 13 | 14 | public async run() { 15 | const mixConfigPath = this.application.makePath(this.webpackConfig); 16 | 17 | if (!existsSync(mixConfigPath)) { 18 | this.logger.error(`Webpack configuration file '${this.webpackConfig}' could not be found`); 19 | return; 20 | } 21 | 22 | const script: string = 'npx encore dev -c webpack.ssr.config.js'; 23 | 24 | const scriptEnv = { 25 | NODE_ENV: 'production', 26 | }; 27 | 28 | this.runScript(script, scriptEnv); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /commands/Watch.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'fs'; 2 | import { BaseSsrCommand } from './Base'; 3 | 4 | /** 5 | * Command to watch assets 6 | */ 7 | export default class Watch extends BaseSsrCommand { 8 | public static commandName = 'ssr:watch'; 9 | public static description = 'Build and watch files for changes'; 10 | public static settings = { 11 | stayAlive: true, 12 | }; 13 | 14 | public async run() { 15 | const mixConfigPath = this.application.makePath(this.webpackConfig); 16 | 17 | if (!existsSync(mixConfigPath)) { 18 | this.logger.error(`Webpack configuration file '${this.webpackConfig}' could not be found`); 19 | return; 20 | } 21 | 22 | const script: string = 'npx encore dev -c webpack.ssr.config.js -w'; 23 | 24 | const scriptEnv = { 25 | NODE_ENV: 'development', 26 | }; 27 | 28 | this.runScript(script, scriptEnv); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /commands/index.ts: -------------------------------------------------------------------------------- 1 | export default ['@eidellev/inertia-adonisjs/build/commands/Build', '@eidellev/inertia-adonisjs/build/commands/Watch']; 2 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {extends: ['@commitlint/config-conventional']} 2 | -------------------------------------------------------------------------------- /instructions.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import * as sinkStatic from '@adonisjs/sink'; 3 | import { ApplicationContract } from '@ioc:Adonis/Core/Application'; 4 | 5 | const ADAPTER_PROMPT_CHOICES = [ 6 | { 7 | name: '@inertiajs/vue2' as const, 8 | message: 'Vue 2', 9 | }, 10 | { 11 | name: '@inertiajs/vue3' as const, 12 | message: 'Vue 3', 13 | }, 14 | { 15 | name: '@inertiajs/react' as const, 16 | message: 'React', 17 | }, 18 | { 19 | name: '@inertiajs/svelte' as const, 20 | message: 'Svelte', 21 | }, 22 | ]; 23 | 24 | /** 25 | * Returns absolute path to the stub relative from the templates 26 | * directory 27 | */ 28 | function getStub(...relativePaths: string[]) { 29 | return join(__dirname, 'templates', ...relativePaths); 30 | } 31 | 32 | /** 33 | * Prompts user for view file they wish to use 34 | */ 35 | function getView(sink: typeof sinkStatic) { 36 | return sink.getPrompt().ask('Enter the `.edge` view file you would like to use as your root template', { 37 | default: 'app', 38 | validate(view) { 39 | return !!view.length || 'This cannot be left empty'; 40 | }, 41 | }); 42 | } 43 | 44 | /** 45 | * Asks user if they wish to enable SSR 46 | */ 47 | function getSsrUserPref(sink: typeof sinkStatic) { 48 | return sink.getPrompt().confirm('Would you like to use SSR?', { 49 | default: false, 50 | }); 51 | } 52 | 53 | /** 54 | * Prompts user for their preferred inertia client-side adapter 55 | */ 56 | function getInertiaAdapterPref(sink: typeof sinkStatic) { 57 | return sink.getPrompt().choice('Which client-side adapter would you like to set up?', ADAPTER_PROMPT_CHOICES, { 58 | validate(choices) { 59 | return choices && choices.length ? true : 'Please select an adapter to continue'; 60 | }, 61 | }); 62 | } 63 | 64 | /** 65 | * Instructions to be executed when setting up the package. 66 | */ 67 | export default async function instructions(projectRoot: string, app: ApplicationContract, sink: typeof sinkStatic) { 68 | const configPath = app.configPath('inertia.ts'); 69 | const inertiaConfig = new sink.files.MustacheFile(projectRoot, configPath, getStub('inertia.txt')); 70 | const view = await getView(sink); 71 | const shouldEnableSsr = await getSsrUserPref(sink); 72 | const pkg = new sink.files.PackageJsonFile(projectRoot); 73 | const adapter = await getInertiaAdapterPref(sink); 74 | 75 | let packagesToInstall; 76 | 77 | if (adapter === '@inertiajs/vue2') { 78 | packagesToInstall = [ 79 | adapter, 80 | 'vue@2', 81 | shouldEnableSsr ? 'vue-server-renderer' : false, 82 | shouldEnableSsr ? 'webpack-node-externals' : false, 83 | ]; 84 | } else if (adapter === '@inertiajs/vue3') { 85 | packagesToInstall = [ 86 | adapter, 87 | 'vue', 88 | shouldEnableSsr ? '@vue/server-renderer' : false, 89 | shouldEnableSsr ? 'webpack-node-externals' : false, 90 | ]; 91 | } else if (adapter === '@inertiajs/react') { 92 | packagesToInstall = [ 93 | adapter, 94 | 'react', 95 | 'react-dom', 96 | '@types/react', 97 | '@types/react-dom', 98 | shouldEnableSsr ? 'webpack-node-externals' : false, 99 | ]; 100 | } else { 101 | packagesToInstall = [adapter, 'svelte', shouldEnableSsr ? 'webpack-node-externals' : false]; 102 | } 103 | 104 | packagesToInstall = packagesToInstall.filter(Boolean); 105 | 106 | /** 107 | * Install required dependencies 108 | */ 109 | for (const packageToInstall of packagesToInstall) { 110 | pkg.install(packageToInstall, undefined, false); 111 | } 112 | 113 | /** 114 | * Find the list of packages we have to remove 115 | */ 116 | const packageList = packagesToInstall.map((packageName) => sink.logger.colors.green(packageName)).join(', '); 117 | const spinner = sink.logger.await(`Installing dependencies: ${packageList}`); 118 | 119 | try { 120 | await pkg.commitAsync(); 121 | spinner.update('Dependencies installed'); 122 | } catch (error) { 123 | spinner.update('Unable to install some or all dependencies'); 124 | sink.logger.fatal(error); 125 | } 126 | 127 | spinner.stop(); 128 | 129 | /** 130 | * Generate inertia config 131 | */ 132 | inertiaConfig.overwrite = true; 133 | inertiaConfig.apply({ view, shouldEnableSsr }).commit(); 134 | 135 | const configDir = app.directoriesMap.get('config') || 'config'; 136 | sink.logger.action('create').succeeded(`${configDir}/inertia.ts`); 137 | 138 | /** 139 | * Generate inertia view 140 | */ 141 | const viewPath = app.viewsPath(`${view}.edge`); 142 | const inertiaView = new sink.files.MustacheFile(projectRoot, viewPath, getStub('view.txt')); 143 | 144 | inertiaView.overwrite = true; 145 | inertiaView.apply({ name: app.appName, inertiaHead: shouldEnableSsr ? '@inertiaHead' : undefined }).commit(); 146 | const viewsDir = app.directoriesMap.get('views'); 147 | sink.logger.action('create').succeeded(`${viewsDir}/${view}.edge`); 148 | 149 | /** 150 | * Generate inertia preload file 151 | */ 152 | const preloadedPath = app.startPath(`inertia.ts`); 153 | const inertiaPreload = new sink.files.MustacheFile(projectRoot, preloadedPath, getStub('start.txt')); 154 | 155 | inertiaPreload.overwrite = true; 156 | inertiaPreload.apply().commit(); 157 | const preloadsDir = app.directoriesMap.get('start'); 158 | sink.logger.action('create').succeeded(`${preloadsDir}/inertia.ts`); 159 | 160 | /** 161 | * Generate SSR webpack config 162 | */ 163 | if (shouldEnableSsr) { 164 | const webpackSsrConfig = new sink.files.MustacheFile( 165 | projectRoot, 166 | 'webpack.ssr.config.js', 167 | getStub('webpack.ssr.config.txt'), 168 | ); 169 | 170 | webpackSsrConfig.overwrite = true; 171 | webpackSsrConfig.apply().commit(); 172 | sink.logger.action('create').succeeded('webpack.ssr.config.js'); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /invoke.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eidellev/inertiajs-adonisjs/d42b2195b0666d22bfd091688077f57ab85a26d0/invoke.gif -------------------------------------------------------------------------------- /japaFile.js: -------------------------------------------------------------------------------- 1 | require('@adonisjs/require-ts/build/register'); 2 | 3 | const { configure } = require('japa'); 4 | 5 | configure({ 6 | files: ['test/**/*.spec.ts'], 7 | }); 8 | -------------------------------------------------------------------------------- /middleware/Inertia.ts: -------------------------------------------------------------------------------- 1 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; 2 | import { HEADERS } from '../src/utils'; 3 | 4 | export default class InertiaMiddleware { 5 | public async handle({ request, response }: HttpContextContract, next: () => Promise) { 6 | if (request.inertia()) { 7 | response.header(HEADERS.INERTIA_HEADER, true); 8 | response.header(HEADERS.VARY, 'Accept'); 9 | } 10 | 11 | await next(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@eidellev/inertia-adonisjs", 3 | "version": "2.2.2", 4 | "private": false, 5 | "description": "InertiaJS provider for AdonisJS", 6 | "repository": "https://github.com/eidellev/inertiajs-adonisjs", 7 | "bugs": "https://github.com/eidellev/inertiajs-adonisjs/issues", 8 | "main": "build/providers/InertiaProvider/index.js", 9 | "typings": "./build/adonis-typings/index.d.ts", 10 | "files": [ 11 | "build/adonis-typings", 12 | "build/providers", 13 | "build/middleware", 14 | "build/src", 15 | "build/instructions.js", 16 | "build/templates", 17 | "build/commands" 18 | ], 19 | "adonisjs": { 20 | "types": "@eidellev/inertia-adonisjs", 21 | "instructions": "./build/instructions.js", 22 | "preloads": [ 23 | { 24 | "file": "./start/inertia", 25 | "environment": [ 26 | "web" 27 | ] 28 | } 29 | ], 30 | "providers": [ 31 | "@eidellev/inertia-adonisjs" 32 | ], 33 | "commands": [ 34 | "@eidellev/inertia-adonisjs/build/commands" 35 | ] 36 | }, 37 | "license": "MIT", 38 | "scripts": { 39 | "lint": "tsc --noEmit && eslint . --ext=ts", 40 | "lint:fix": "eslint . --ext=ts --fix", 41 | "clean": "rimraf build", 42 | "copyfiles": "copyfiles \"templates/**/*.txt\" build", 43 | "build": "cross-env npm run clean && npm run copyfiles && tsc", 44 | "watch": "cross-env npm run clean && npm run copyfiles && tsc -w", 45 | "test": "nyc node japaFile.js", 46 | "test:debug": "node --inspect-brk japaFile.js", 47 | "prepare": "npm run build", 48 | "check-dependencies": "npx npm-check -u" 49 | }, 50 | "dependencies": { 51 | "@types/md5": "^2.3.2", 52 | "html-entities": "^2.3.3", 53 | "md5": "^2.3.0", 54 | "qs": "^6.11.2" 55 | }, 56 | "peerDependencies": { 57 | "@adonisjs/core": ">=5" 58 | }, 59 | "devDependencies": { 60 | "@adonisjs/config": "^3.0.9", 61 | "@adonisjs/core": "5.9.0", 62 | "@adonisjs/mrm-preset": "^5.0.3", 63 | "@adonisjs/require-ts": "^2.0.13", 64 | "@adonisjs/session": "^6.4.0", 65 | "@adonisjs/sink": "5.4.2", 66 | "@adonisjs/validator": "12.4.2", 67 | "@adonisjs/view": "^6.2.0", 68 | "@commitlint/cli": "17.4.4", 69 | "@commitlint/config-conventional": "17.4.4", 70 | "@commitlint/prompt-cli": "17.4.4", 71 | "@poppinss/dev-utils": "^2.0.3", 72 | "@types/common-tags": "^1.8.1", 73 | "@types/supertest": "^2.0.12", 74 | "adonis-preset-ts": "^2.1.0", 75 | "common-tags": "^1.8.2", 76 | "copyfiles": "^2.4.1", 77 | "cross-env": "^7.0.3", 78 | "eslint": "8.35.0", 79 | "eslint-config-prettier": "8.7.0", 80 | "eslint-plugin-adonis": "^2.1.1", 81 | "eslint-plugin-prettier": "^4.2.1", 82 | "husky": "8.0.3", 83 | "japa": "^4.0.0", 84 | "lint-staged": "13.1.2", 85 | "nyc": "^15.1.0", 86 | "prettier": "2.8.4", 87 | "rimraf": "4.3.1", 88 | "semantic-release": "20.1.1", 89 | "supertest": "6.3.3", 90 | "typescript": "4.9.5" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /providers/InertiaProvider/InertiaProvider.ts: -------------------------------------------------------------------------------- 1 | import { Redirect } from '@adonisjs/http-server/build/src/Redirect'; 2 | import { ApplicationContract } from '@ioc:Adonis/Core/Application'; 3 | import { ConfigContract } from '@ioc:Adonis/Core/Config'; 4 | import { HttpContextConstructorContract } from '@ioc:Adonis/Core/HttpContext'; 5 | import { RequestConstructorContract, RequestContract } from '@ioc:Adonis/Core/Request'; 6 | import { RedirectContract, ResponseConstructorContract } from '@ioc:Adonis/Core/Response'; 7 | import { RouterContract } from '@ioc:Adonis/Core/Route'; 8 | import Validator, { ErrorReporterConstructorContract } from '@ioc:Adonis/Core/Validator'; 9 | import { ViewContract } from '@ioc:Adonis/Core/View'; 10 | import { ResponseProps } from '@ioc:EidelLev/Inertia'; 11 | import { encode } from 'html-entities'; 12 | import { Inertia } from '../../src/Inertia'; 13 | import { inertiaHelper } from '../../src/inertiaHelper'; 14 | import { HEADERS } from '../../src/utils'; 15 | import InertiaMiddleware from '../../middleware/Inertia'; 16 | 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | Inertia Provider 20 | |-------------------------------------------------------------------------- 21 | */ 22 | export default class InertiaProvider { 23 | constructor(protected app: ApplicationContract) {} 24 | public static needsApplication = true; 25 | 26 | /** 27 | * Register the `inertia` view global 28 | */ 29 | private registerInertiaViewGlobal(View: ViewContract) { 30 | View.global('inertia', (page: Record = {}) => { 31 | if (page.ssrBody) { 32 | return page.ssrBody; 33 | } 34 | 35 | return `
`; 36 | }); 37 | } 38 | 39 | /** 40 | * Register the `inertiaHead` view global 41 | */ 42 | private registerInertiaHeadViewGlobal(View: ViewContract) { 43 | View.global('inertiaHead', (page: Record) => { 44 | const { ssrHead = [] }: { ssrHead?: string[] } = page || {}; 45 | 46 | return ssrHead.join('\n'); 47 | }); 48 | } 49 | 50 | private registerInertiaTag(View: ViewContract) { 51 | View.registerTag({ 52 | block: false, 53 | tagName: 'inertia', 54 | seekable: false, 55 | compile(_, buffer, token) { 56 | buffer.writeExpression( 57 | `\n 58 | out += template.sharedState.inertia(state.page) 59 | `, 60 | token.filename, 61 | token.loc.start.line, 62 | ); 63 | }, 64 | }); 65 | } 66 | 67 | private registerInertiaHeadTag(View: ViewContract) { 68 | View.registerTag({ 69 | block: false, 70 | tagName: 'inertiaHead', 71 | seekable: false, 72 | compile(_, buffer, token) { 73 | buffer.writeExpression( 74 | `\n 75 | out += template.sharedState.inertiaHead(state.page) 76 | `, 77 | token.filename, 78 | token.loc.start.line, 79 | ); 80 | }, 81 | }); 82 | } 83 | 84 | /* 85 | * Hook inertia into ctx during request cycle 86 | */ 87 | private registerInertia( 88 | Application: ApplicationContract, 89 | HttpContext: HttpContextConstructorContract, 90 | Config: ConfigContract, 91 | ) { 92 | const config = Config.get('inertia.inertia', { view: 'app' }); 93 | 94 | HttpContext.getter( 95 | 'inertia', 96 | function inertia() { 97 | return new Inertia(Application, this, config); 98 | }, 99 | false, 100 | ); 101 | } 102 | 103 | /* 104 | * Register `inertia` helper on request object 105 | */ 106 | private registerInertiaHelper(request: RequestConstructorContract) { 107 | request.getter( 108 | 'inertia', 109 | function inertia() { 110 | return () => inertiaHelper(this); 111 | }, 112 | false, 113 | ); 114 | } 115 | 116 | /** 117 | * Registers inertia binding 118 | */ 119 | public registerBinding() { 120 | this.app.container.bind('EidelLev/Inertia/Middleware', () => InertiaMiddleware); 121 | 122 | this.app.container.singleton('EidelLev/Inertia', () => ({ 123 | share: Inertia.share, 124 | version: Inertia.version, 125 | manifestFile: Inertia.manifestFile, 126 | lazy: Inertia.lazy, 127 | })); 128 | } 129 | 130 | /** 131 | * Registers custom validation negotiator 132 | * https://preview.adonisjs.com/releases/core/preview-rc-2#validator 133 | */ 134 | public registerNegotiator({ validator }: typeof Validator) { 135 | validator.negotiator((request: RequestContract): ErrorReporterConstructorContract => { 136 | if (request.inertia()) { 137 | return validator.reporters.vanilla; 138 | } 139 | 140 | if (request.ajax()) { 141 | return validator.reporters.api; 142 | } 143 | 144 | switch (request.accepts(['html', 'application/vnd.api+json', 'json'])) { 145 | case 'html': 146 | case null: 147 | return validator.reporters.vanilla; 148 | case 'json': 149 | return validator.reporters.api; 150 | case 'application/vnd.api+json': 151 | return validator.reporters.jsonapi; 152 | } 153 | }); 154 | } 155 | 156 | /** 157 | * Registers the Inertia route helper 158 | */ 159 | public registerRouteHelper(Route: RouterContract): void { 160 | Route.inertia = (pattern: string, component: string, pageOnlyProps: ResponseProps = {}) => { 161 | Route.get(pattern, ({ inertia }) => { 162 | return inertia.render(component, {}, pageOnlyProps); 163 | }); 164 | 165 | return Route; 166 | }; 167 | } 168 | 169 | /** 170 | * Set HTTP code 303 after a PUT, PATCH or POST request so the redirect is treated as GET request 171 | * https://inertiajs.com/redirects#303-response-code 172 | */ 173 | public registerRedirect(Response: ResponseConstructorContract) { 174 | Response.macro( 175 | 'redirect', 176 | function (path?: string, forwardQueryString: boolean = false, statusCode = 302): RedirectContract | void { 177 | const isInertia = this.request.headers[HEADERS.INERTIA_HEADER]; 178 | const method = this.request.method; 179 | let finalStatusCode = statusCode; 180 | 181 | if (isInertia && statusCode === 302 && method && method && ['PUT', 'PATCH', 'DELETE'].includes(method)) { 182 | finalStatusCode = 303; 183 | } 184 | 185 | // @ts-ignore 186 | const handler = new Redirect(this.request, this, this.router); 187 | 188 | if (forwardQueryString) { 189 | handler.withQs(); 190 | } 191 | 192 | if (path === 'back') { 193 | return handler.status(finalStatusCode).back(); 194 | } 195 | 196 | if (path) { 197 | return handler.status(finalStatusCode).toPath(path); 198 | } 199 | 200 | handler.status(finalStatusCode); 201 | 202 | return handler; 203 | }, 204 | ); 205 | } 206 | 207 | public boot(): void { 208 | this.app.container.withBindings( 209 | [ 210 | 'Adonis/Core/HttpContext', 211 | 'Adonis/Core/View', 212 | 'Adonis/Core/Config', 213 | 'Adonis/Core/Request', 214 | 'Adonis/Core/Response', 215 | 'Adonis/Core/Validator', 216 | 'Adonis/Core/Route', 217 | 'Adonis/Core/Application', 218 | ], 219 | (HttpContext, View, Config, Request, Response, Validator, Route, Application) => { 220 | this.registerInertia(Application, HttpContext, Config); 221 | this.registerInertiaViewGlobal(View); 222 | this.registerInertiaHeadViewGlobal(View); 223 | this.registerInertiaTag(View); 224 | this.registerInertiaHeadTag(View); 225 | this.registerInertiaHelper(Request); 226 | this.registerRedirect(Response); 227 | this.registerNegotiator(Validator); 228 | this.registerBinding(); 229 | this.registerRouteHelper(Route); 230 | }, 231 | ); 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /providers/InertiaProvider/index.ts: -------------------------------------------------------------------------------- 1 | import InertiaProvider from './InertiaProvider'; 2 | export { Inertia } from '../../src/Inertia'; 3 | 4 | export default InertiaProvider; 5 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | branches: ['main'], 3 | }; 4 | -------------------------------------------------------------------------------- /src/Inertia.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationContract } from '@ioc:Adonis/Core/Application'; 2 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; 3 | import { 4 | InertiaConfig, 5 | InertiaContract, 6 | RenderResponse, 7 | ResponseProps, 8 | SharedData, 9 | Version, 10 | VersionValue, 11 | SsrRenderResult, 12 | InertiaLazyProp, 13 | } from '@ioc:EidelLev/Inertia'; 14 | import { readFile } from 'fs/promises'; 15 | import md5 from 'md5'; 16 | import qs from 'qs'; 17 | import { HEADERS } from './utils'; 18 | import LazyProp from './LazyProp'; 19 | 20 | export class Inertia implements InertiaContract { 21 | private static sharedData: SharedData = {}; 22 | private static currentVersion: Version; 23 | 24 | constructor(private app: ApplicationContract, private ctx: HttpContextContract, private config: InertiaConfig) {} 25 | 26 | public static share(data: SharedData) { 27 | Inertia.sharedData = { ...Inertia.sharedData, ...data }; 28 | return Inertia; 29 | } 30 | 31 | public static async manifestFile(path: string) { 32 | try { 33 | const buffer = await readFile(path); 34 | 35 | return md5(buffer); 36 | } catch (error) { 37 | // eslint-disable-next-line no-console 38 | console.warn('Manifest file could not be read'); 39 | return ''; 40 | } 41 | } 42 | 43 | public static version(version: Version) { 44 | Inertia.currentVersion = version; 45 | return Inertia; 46 | } 47 | 48 | public static lazy(callback: () => ResponseProps | Promise): InertiaLazyProp { 49 | return new LazyProp(callback); 50 | } 51 | 52 | public async render( 53 | component: string, 54 | responseProps: ResponseProps = {}, 55 | pageOnlyProps: ResponseProps = {}, 56 | ): RenderResponse { 57 | const { view: inertiaView, ssr = { enabled: false } } = this.config; 58 | const { request, response, view, session } = this.ctx; 59 | const isInertia = request.inertia(); 60 | const partialData = this.resolvePartialData(request.header(HEADERS.INERTIA_PARTIAL_DATA)); 61 | const partialDataComponentHeader = request.header(HEADERS.INERTIA_PARTIAL_DATA_COMPONENT); 62 | const requestAssetVersion = request.header(HEADERS.INERTIA_VERSION); 63 | const props: ResponseProps = await this.resolveProps( 64 | { ...Inertia.sharedData, ...responseProps }, 65 | partialData, 66 | component, 67 | partialDataComponentHeader, 68 | ); 69 | const disallowSsr = ssr?.allowList && !ssr.allowList.includes(component); 70 | 71 | // Get asset version 72 | const version = await this.resolveVersion(); 73 | const isGet = request.method() === 'GET'; 74 | const queryParams = request.all(); 75 | let url = request.url(); 76 | 77 | if (isGet && Object.keys(queryParams).length) { 78 | // Keep original request query params 79 | url += `?${qs.stringify(queryParams)}`; 80 | } 81 | 82 | const page = { 83 | component, 84 | version, 85 | props, 86 | url, 87 | }; 88 | 89 | const assetsChanged = requestAssetVersion && requestAssetVersion !== version; 90 | 91 | // Handle asset version update 92 | if (isInertia && isGet && assetsChanged) { 93 | session.responseFlashMessages = session.flashMessages; 94 | await session.commit(); 95 | return response.status(409).header(HEADERS.INERTIA_LOCATION, url); 96 | } 97 | 98 | // JSON response 99 | if (isInertia) { 100 | return page; 101 | } 102 | 103 | // Initial page render in SSR mode 104 | if (ssr.enabled && !disallowSsr) { 105 | const { head, body } = await this.renderSsrPage(page); 106 | 107 | return view.render(inertiaView, { 108 | page: { 109 | ssrHead: head, 110 | ssrBody: body, 111 | }, 112 | ...pageOnlyProps, 113 | }); 114 | } 115 | 116 | // Initial page render in CSR mode 117 | return view.render(inertiaView, { page, ...pageOnlyProps }); 118 | } 119 | 120 | private renderSsrPage(page: any): Promise { 121 | const { ssr = { buildDirectory: undefined, autoreload: false } } = this.config; 122 | const { buildDirectory, autoreload } = ssr; 123 | const path = buildDirectory || 'inertia/ssr'; 124 | const ssrModulePath = this.app.makePath(path, 'ssr.js'); 125 | 126 | if (autoreload) { 127 | delete require.cache[ssrModulePath]; 128 | } 129 | 130 | const render = require(ssrModulePath).default; 131 | 132 | return render(page); 133 | } 134 | 135 | /** 136 | * Converts partial data header to an array of values 137 | */ 138 | private resolvePartialData(partialDataHeader?: string): string[] { 139 | return (partialDataHeader || '').split(',').filter(Boolean); 140 | } 141 | 142 | /** 143 | * Get current asset version 144 | */ 145 | private async resolveVersion(): Promise { 146 | const { currentVersion } = Inertia; 147 | 148 | if (!currentVersion) { 149 | return undefined; 150 | } 151 | 152 | if (typeof currentVersion !== 'function') { 153 | return currentVersion; 154 | } 155 | 156 | return await currentVersion(); 157 | } 158 | 159 | /** 160 | * Resolves all response prop values 161 | */ 162 | private async resolveProps( 163 | props: ResponseProps, 164 | partialData: string[], 165 | component: string, 166 | partialDataComponentHeader?: string, 167 | ) { 168 | // Keep only partial data 169 | if (partialData.length && component === partialDataComponentHeader) { 170 | const filteredProps = Object.entries(props).filter(([key]) => { 171 | return partialData.includes(key); 172 | }); 173 | 174 | props = Object.fromEntries(filteredProps); 175 | } else { 176 | const filteredLazyProps = Object.entries(props).filter(([, value]) => { 177 | return !(value instanceof LazyProp); 178 | }); 179 | 180 | props = Object.fromEntries(filteredLazyProps); 181 | } 182 | 183 | // Resolve lazy props 184 | Object.entries(props).forEach(([key, value]) => { 185 | if (value instanceof LazyProp) { 186 | const resolvedValue = value.lazyValue; 187 | props[key] = resolvedValue; 188 | } else if (typeof value === 'function') { 189 | const resolvedValue = value(this.ctx); 190 | props[key] = resolvedValue; 191 | } 192 | }); 193 | 194 | // Resolve promises 195 | const result = await Promise.all( 196 | Object.entries(props).map(async ([key, value]) => { 197 | return [key, await value]; 198 | }), 199 | ); 200 | 201 | // Marshall back into an object 202 | return Object.fromEntries(result); 203 | } 204 | 205 | /** 206 | * Simply replace with Adonis' `response.redirect().withQs().back()` 207 | */ 208 | public redirectBack() { 209 | const { response } = this.ctx; 210 | 211 | response.status(303).redirect().withQs().back(); 212 | } 213 | 214 | /** 215 | * Server initiated external redirect 216 | * 217 | * @param {string} url The external URL 218 | */ 219 | public location(url: string) { 220 | const { response } = this.ctx; 221 | 222 | response.removeHeader(HEADERS.INERTIA_HEADER).header(HEADERS.INERTIA_LOCATION, url).conflict(); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/LazyProp.ts: -------------------------------------------------------------------------------- 1 | import { InertiaLazyProp, ResponseProps } from '@ioc:EidelLev/Inertia'; 2 | 3 | export default class LazyProp implements InertiaLazyProp { 4 | constructor(protected lazyPropCallback: () => ResponseProps | Promise) {} 5 | 6 | public get lazyValue(): ResponseProps | Promise { 7 | return this.lazyPropCallback() 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/inertiaHelper.ts: -------------------------------------------------------------------------------- 1 | import { RequestContract } from '@ioc:Adonis/Core/Request'; 2 | import { HEADERS } from './utils'; 3 | 4 | export function inertiaHelper(request: RequestContract) { 5 | return !!request.header(HEADERS.INERTIA_HEADER); 6 | } 7 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export enum HEADERS { 2 | INERTIA_HEADER = 'x-inertia', 3 | INERTIA_PARTIAL_DATA = 'x-inertia-partial-data', 4 | INERTIA_PARTIAL_DATA_COMPONENT = 'x-inertia-partial-component', 5 | INERTIA_VERSION = 'x-inertia-version', 6 | INERTIA_LOCATION = 'x-inertia-location', 7 | VARY = 'vary', 8 | } 9 | -------------------------------------------------------------------------------- /templates/inertia.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * Feel free to let me know via PR, 3 | * if you find something broken in this config file. 4 | */ 5 | 6 | import { InertiaConfig } from '@ioc:EidelLev/Inertia'; 7 | 8 | /* 9 | |-------------------------------------------------------------------------- 10 | | Inertia-AdonisJS config 11 | |-------------------------------------------------------------------------- 12 | | 13 | */ 14 | 15 | export const inertia: InertiaConfig = { 16 | view: '{{view}}', 17 | ssr: { 18 | enabled: {{shouldEnableSsr}}, 19 | {{#shouldEnableSsr}} 20 | autoreload: process.env.NODE_ENV === 'development', 21 | {{/shouldEnableSsr}} 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /templates/start.txt: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | Inertia Preloaded File 4 | |-------------------------------------------------------------------------- 5 | | 6 | | Any code written inside this file will be executed during the application 7 | | boot. 8 | | 9 | */ 10 | 11 | import Inertia from '@ioc:EidelLev/Inertia'; 12 | 13 | Inertia.share({ 14 | errors: (ctx) => { 15 | return ctx.session.flashMessages.get('errors'); 16 | }, 17 | }).version(() => Inertia.manifestFile('public/assets/manifest.json')); 18 | -------------------------------------------------------------------------------- /templates/view.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | @entryPointStyles('app') 9 | @entryPointScripts('app') 10 | 11 | {{name}} 12 | {{inertiaHead}} 13 | 14 | 15 | @inertia 16 | 17 | 18 | -------------------------------------------------------------------------------- /templates/webpack.ssr.config.txt: -------------------------------------------------------------------------------- 1 | const { join } = require('path') 2 | const Encore = require('@symfony/webpack-encore') 3 | 4 | /* 5 | |-------------------------------------------------------------------------- 6 | | Encore runtime environment 7 | |-------------------------------------------------------------------------- 8 | */ 9 | if (!Encore.isRuntimeEnvironmentConfigured()) { 10 | Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev') 11 | } 12 | 13 | /* 14 | |-------------------------------------------------------------------------- 15 | | Output path 16 | |-------------------------------------------------------------------------- 17 | | 18 | | The output path for writing the compiled files. It should always 19 | | be inside the public directory, so that AdonisJS can serve it. 20 | | 21 | */ 22 | Encore.setOutputPath('./inertia/ssr') 23 | 24 | /* 25 | |-------------------------------------------------------------------------- 26 | | Public URI 27 | |-------------------------------------------------------------------------- 28 | | 29 | | The public URI to access the static files. It should always be 30 | | relative from the "public" directory. 31 | | 32 | */ 33 | Encore.setPublicPath('/ssr') 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Entrypoints 38 | |-------------------------------------------------------------------------- 39 | | 40 | | Entrypoints are script files that boots your frontend application. Ideally 41 | | a single entrypoint is used by majority of applications. However, feel 42 | | free to add more (if required). 43 | | 44 | | Also, make sure to read the docs on "Assets bundler" to learn more about 45 | | entrypoints. 46 | | 47 | */ 48 | Encore.addEntry('ssr', './resources/js/ssr.js') 49 | 50 | /* 51 | |-------------------------------------------------------------------------- 52 | | Isolated entrypoints 53 | |-------------------------------------------------------------------------- 54 | | 55 | | Treat each entry point and its dependencies as its own isolated module. 56 | | 57 | */ 58 | Encore.disableSingleRuntimeChunk() 59 | 60 | /* 61 | |-------------------------------------------------------------------------- 62 | | Cleanup output folder 63 | |-------------------------------------------------------------------------- 64 | | 65 | | It is always nice to cleanup the build output before creating a build. It 66 | | will ensure that all unused files from the previous build are removed. 67 | | 68 | */ 69 | Encore.cleanupOutputBeforeBuild() 70 | 71 | /* 72 | |-------------------------------------------------------------------------- 73 | | Assets versioning 74 | |-------------------------------------------------------------------------- 75 | | 76 | | Enable assets versioning to leverage lifetime browser and CDN cache 77 | | 78 | */ 79 | Encore.enableVersioning(Encore.isProduction()) 80 | 81 | /* 82 | |-------------------------------------------------------------------------- 83 | | Configure dev server 84 | |-------------------------------------------------------------------------- 85 | | 86 | | Here we configure the dev server to enable live reloading for edge templates. 87 | | Remember edge templates are not processed by Webpack and hence we need 88 | | to watch them explicitly and livereload the browser. 89 | | 90 | */ 91 | Encore.configureDevServerOptions((options) => { 92 | /** 93 | * Normalize "options.static" property to an array 94 | */ 95 | if (!options.static) { 96 | options.static = [] 97 | } else if (!Array.isArray(options.static)) { 98 | options.static = [options.static] 99 | } 100 | 101 | /** 102 | * Enable live reload and add views directory 103 | */ 104 | options.liveReload = true 105 | options.static.push({ 106 | directory: join(__dirname, './resources/views'), 107 | watch: true, 108 | }) 109 | }) 110 | 111 | /* 112 | |-------------------------------------------------------------------------- 113 | | CSS precompilers support 114 | |-------------------------------------------------------------------------- 115 | | 116 | | Uncomment one of the following lines of code to enable support for your 117 | | favorite CSS precompiler 118 | | 119 | */ 120 | // Encore.enableSassLoader() 121 | // Encore.enableLessLoader() 122 | // Encore.enableStylusLoader() 123 | 124 | /* 125 | |-------------------------------------------------------------------------- 126 | | CSS loaders 127 | |-------------------------------------------------------------------------- 128 | | 129 | | Uncomment one of the following line of code to enable support for 130 | | PostCSS or CSS. 131 | | 132 | */ 133 | // Encore.enablePostCssLoader() 134 | // Encore.configureCssLoader(() => {}) 135 | 136 | /* 137 | |-------------------------------------------------------------------------- 138 | | Enable Vue loader 139 | |-------------------------------------------------------------------------- 140 | | 141 | | Uncomment the following lines of code to enable support for vue. Also make 142 | | sure to install the required dependencies. 143 | | 144 | */ 145 | // Encore.enableVueLoader(() => {}, { 146 | // version: 3, 147 | // runtimeCompilerBuild: false, 148 | // useJsx: false, 149 | // }) 150 | 151 | /* 152 | |-------------------------------------------------------------------------- 153 | | Configure logging 154 | |-------------------------------------------------------------------------- 155 | | 156 | | To keep the terminal clean from unnecessary info statements , we only 157 | | log warnings and errors. If you want all the logs, you can change 158 | | the level to "info". 159 | | 160 | */ 161 | const config = Encore.getWebpackConfig() 162 | config.infrastructureLogging = { 163 | level: 'warn', 164 | } 165 | config.stats = 'errors-warnings' 166 | 167 | /* 168 | |-------------------------------------------------------------------------- 169 | | SSR Config 170 | |-------------------------------------------------------------------------- 171 | | 172 | */ 173 | config.externals = [require('webpack-node-externals')()] 174 | config.externalsPresets = { node: true } 175 | config.output = { 176 | libraryTarget: 'commonjs2', 177 | filename: 'ssr.js', 178 | path: join(__dirname, './inertia/ssr'), 179 | } 180 | config.experiments = { outputModule: true } 181 | 182 | /* 183 | |-------------------------------------------------------------------------- 184 | | Export config 185 | |-------------------------------------------------------------------------- 186 | | 187 | | Export config for webpack to do its job 188 | | 189 | */ 190 | 191 | module.exports = config 192 | -------------------------------------------------------------------------------- /test/data.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'japa'; 2 | import supertest from 'supertest'; 3 | import { createServer } from 'http'; 4 | import { Inertia } from '../src/Inertia'; 5 | import { HEADERS } from '../src/utils'; 6 | import { setup, teardown } from './utils'; 7 | 8 | test.group('Data', (group) => { 9 | group.afterEach(async () => { 10 | await teardown(); 11 | // @ts-ignore 12 | Inertia.sharedData = {}; 13 | }); 14 | 15 | test('Should return shared data', async (assert) => { 16 | const props = { 17 | some: { 18 | props: { 19 | for: ['your', 'page'], 20 | }, 21 | }, 22 | }; 23 | const app = await setup(); 24 | const server = createServer(async (req, res) => { 25 | const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res); 26 | Inertia.share({ 27 | shared: 'data', 28 | }); 29 | const response = await ctx.inertia.render('Some/Page', props); 30 | 31 | res.setHeader('Content-Type', 'application/json'); 32 | res.write(JSON.stringify(response)); 33 | res.end(); 34 | }); 35 | 36 | const response = await supertest(server).get('/').set(HEADERS.INERTIA_HEADER, 'true').expect(200); 37 | assert.deepEqual(response.body, { 38 | component: 'Some/Page', 39 | props: { ...props, shared: 'data' }, 40 | url: '/', 41 | }); 42 | }); 43 | 44 | test('Should combine shared data(multiple calls)', async (assert) => { 45 | const props = { 46 | some: { 47 | props: { 48 | for: ['your', 'page'], 49 | }, 50 | }, 51 | }; 52 | const app = await setup(); 53 | const server = createServer(async (req, res) => { 54 | const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res); 55 | Inertia.share({ 56 | shared: 'data', 57 | }).share({ additional: 'shared data' }); 58 | const response = await ctx.inertia.render('Some/Page', props); 59 | 60 | res.setHeader('Content-Type', 'application/json'); 61 | res.write(JSON.stringify(response)); 62 | res.end(); 63 | }); 64 | 65 | const response = await supertest(server).get('/').set(HEADERS.INERTIA_HEADER, 'true').expect(200); 66 | assert.deepEqual(response.body, { 67 | component: 'Some/Page', 68 | props: { ...props, shared: 'data', additional: 'shared data' }, 69 | url: '/', 70 | }); 71 | }); 72 | 73 | test('Should resolve lazy props', async (assert) => { 74 | const props = { 75 | some() { 76 | return { 77 | props: { 78 | for: ['your', 'page'], 79 | }, 80 | }; 81 | }, 82 | another: 'prop', 83 | }; 84 | const app = await setup(); 85 | const server = createServer(async (req, res) => { 86 | const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res); 87 | const response = await ctx.inertia.render('Some/Page', props); 88 | 89 | res.setHeader('Content-Type', 'application/json'); 90 | res.write(JSON.stringify(response)); 91 | res.end(); 92 | }); 93 | 94 | const response = await supertest(server).get('/').set(HEADERS.INERTIA_HEADER, 'true').expect(200); 95 | 96 | assert.deepEqual(response.body, { 97 | component: 'Some/Page', 98 | props: { 99 | some: { 100 | props: { 101 | for: ['your', 'page'], 102 | }, 103 | }, 104 | another: 'prop', 105 | }, 106 | url: '/', 107 | }); 108 | }); 109 | 110 | test('Should return partial data', async (assert) => { 111 | const props = { 112 | some() { 113 | return { 114 | props: { 115 | for: ['your', 'page'], 116 | }, 117 | }; 118 | }, 119 | another: 'prop', 120 | partial: 1234, 121 | }; 122 | const app = await setup(); 123 | const server = createServer(async (req, res) => { 124 | const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res); 125 | const response = await ctx.inertia.render('Some/Page', props); 126 | 127 | res.setHeader('Content-Type', 'application/json'); 128 | res.write(JSON.stringify(response)); 129 | res.end(); 130 | }); 131 | 132 | const response = await supertest(server) 133 | .get('/') 134 | .set(HEADERS.INERTIA_HEADER, 'true') 135 | .set(HEADERS.INERTIA_PARTIAL_DATA, 'partial,another') 136 | .set(HEADERS.INERTIA_PARTIAL_DATA_COMPONENT, 'Some/Page') 137 | .expect(200); 138 | 139 | assert.deepEqual(response.body, { 140 | component: 'Some/Page', 141 | props: { 142 | another: 'prop', 143 | partial: 1234, 144 | }, 145 | url: '/', 146 | }); 147 | }); 148 | 149 | test('Should return full data', async (assert) => { 150 | const props = { 151 | some() { 152 | return { 153 | props: { 154 | for: ['your', 'page'], 155 | }, 156 | }; 157 | }, 158 | another: 'prop', 159 | partial: 1234, 160 | }; 161 | const app = await setup(); 162 | const server = createServer(async (req, res) => { 163 | const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res); 164 | const response = await ctx.inertia.render('Some/Page', props); 165 | 166 | res.setHeader('Content-Type', 'application/json'); 167 | res.write(JSON.stringify(response)); 168 | res.end(); 169 | }); 170 | 171 | const response = await supertest(server) 172 | .get('/') 173 | .set(HEADERS.INERTIA_HEADER, 'true') 174 | .set(HEADERS.INERTIA_PARTIAL_DATA, 'partial,another') 175 | .set(HEADERS.INERTIA_PARTIAL_DATA_COMPONENT, 'Some/Other/Page') 176 | .expect(200); 177 | 178 | assert.deepEqual(response.body, { 179 | component: 'Some/Page', 180 | props: { 181 | another: 'prop', 182 | partial: 1234, 183 | some: { 184 | props: { 185 | for: ['your', 'page'], 186 | }, 187 | }, 188 | }, 189 | url: '/', 190 | }); 191 | }); 192 | 193 | test('Should not return lazy response data', async (assert) => { 194 | const props = { 195 | some() { 196 | return { 197 | props: { 198 | for: ['your', 'page'], 199 | }, 200 | }; 201 | }, 202 | another: 'prop', 203 | lazyProp: Inertia.lazy(() => { return { lazy: 'too lazy'}}), 204 | }; 205 | const app = await setup(); 206 | const server = createServer(async (req, res) => { 207 | const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res); 208 | const response = await ctx.inertia.render('Some/Page', props); 209 | 210 | res.setHeader('Content-Type', 'application/json'); 211 | res.write(JSON.stringify(response)); 212 | res.end(); 213 | }); 214 | 215 | const response = await supertest(server).get('/').set(HEADERS.INERTIA_HEADER, 'true').expect(200); 216 | 217 | assert.deepEqual(response.body, { 218 | component: 'Some/Page', 219 | props: { 220 | some: { 221 | props: { 222 | for: ['your', 'page'], 223 | }, 224 | }, 225 | another: 'prop', 226 | }, 227 | url: '/', 228 | }); 229 | }); 230 | 231 | test('Should return lazy response data', async (assert) => { 232 | const props = { 233 | some() { 234 | return { 235 | props: { 236 | for: ['your', 'page'], 237 | }, 238 | }; 239 | }, 240 | another: 'prop', 241 | lazyProp: Inertia.lazy(() => { return { lazy: 'too lazy'}}), 242 | lazyAsyncProp: Inertia.lazy(() => new Promise((res) => 243 | res({ 244 | lazyAsync: 'too lazy to be async' 245 | }) 246 | )), 247 | }; 248 | const app = await setup(); 249 | const server = createServer(async (req, res) => { 250 | const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res); 251 | const response = await ctx.inertia.render('Some/Page', props); 252 | 253 | res.setHeader('Content-Type', 'application/json'); 254 | res.write(JSON.stringify(response)); 255 | res.end(); 256 | }); 257 | 258 | const response = await supertest(server) 259 | .get('/') 260 | .set(HEADERS.INERTIA_HEADER, 'true') 261 | .set(HEADERS.INERTIA_PARTIAL_DATA, 'another,lazyProp,lazyAsyncProp') 262 | .set(HEADERS.INERTIA_PARTIAL_DATA_COMPONENT, 'Some/Page') 263 | .expect(200); 264 | 265 | assert.deepEqual(response.body, { 266 | component: 'Some/Page', 267 | props: { 268 | another: 'prop', 269 | lazyProp: { lazy: 'too lazy' }, 270 | lazyAsyncProp: { lazyAsync: 'too lazy to be async' } 271 | }, 272 | url: '/', 273 | }); 274 | }); 275 | }); 276 | -------------------------------------------------------------------------------- /test/inertia-middleware.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'japa'; 2 | import InertiaMiddleware from '../middleware/Inertia'; 3 | import { fs, setup } from './utils'; 4 | 5 | test.group('Inertia middleware', (group) => { 6 | group.afterEach(async () => { 7 | await fs.cleanup(); 8 | }); 9 | 10 | test('register inertia middleware', async (assert) => { 11 | const app = await setup(); 12 | 13 | assert.deepEqual(app.container.use('EidelLev/Inertia/Middleware'), InertiaMiddleware); 14 | }); 15 | 16 | test('Make inertia middleware instance via container', async (assert) => { 17 | const app = await setup(); 18 | 19 | assert.instanceOf(app.container.make(app.container.use('EidelLev/Inertia/Middleware')), InertiaMiddleware); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/location.spec.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import test from 'japa'; 3 | import supertest from 'supertest'; 4 | import { HEADERS } from '../src/utils'; 5 | import { setup, teardown } from './utils'; 6 | 7 | test.group('Location', (group) => { 8 | group.afterEach(async () => { 9 | await teardown(); 10 | }); 11 | 12 | test('Should set HTTP status code to 409 external redirect', async () => { 13 | const app = await setup(); 14 | const server = createServer(async (req, res) => { 15 | const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res); 16 | ctx.inertia.location('https://adonisjs.com'); 17 | 18 | res.end(); 19 | }); 20 | 21 | await supertest(server).put('/').set(HEADERS.INERTIA_HEADER, 'true').expect(409); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/redirect.spec.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import test from 'japa'; 3 | import supertest from 'supertest'; 4 | import { HEADERS } from '../src/utils'; 5 | import { setup, teardown } from './utils'; 6 | 7 | test.group('Redirect', (group) => { 8 | group.afterEach(async () => { 9 | await teardown(); 10 | }); 11 | 12 | test('Should set HTTP status code to 303 on PUT', async (assert) => { 13 | const app = await setup(); 14 | const server = createServer(async (req, res) => { 15 | const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res); 16 | ctx.response.redirect('/some/other/route'); 17 | assert.equal(ctx.response.getStatus(), 303); 18 | res.end(); 19 | }); 20 | 21 | await supertest(server).put('/').set(HEADERS.INERTIA_HEADER, 'true'); 22 | }); 23 | 24 | test('Should set HTTP status code to 303 on PATCH', async (assert) => { 25 | const app = await setup(); 26 | const server = createServer(async (req, res) => { 27 | const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res); 28 | ctx.response.redirect('/some/other/route'); 29 | assert.equal(ctx.response.getStatus(), 303); 30 | res.end(); 31 | }); 32 | 33 | await supertest(server).patch('/').set(HEADERS.INERTIA_HEADER, 'true'); 34 | }); 35 | 36 | test('Should set HTTP status code to 303 on DELETE', async (assert) => { 37 | const app = await setup(); 38 | const server = createServer(async (req, res) => { 39 | const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res); 40 | ctx.response.redirect('/some/other/route'); 41 | assert.equal(ctx.response.getStatus(), 303); 42 | res.end(); 43 | }); 44 | 45 | await supertest(server).delete('/').set(HEADERS.INERTIA_HEADER, 'true'); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/rendering.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'japa'; 2 | import supertest from 'supertest'; 3 | import { codeBlock } from 'common-tags'; 4 | import { createServer } from 'http'; 5 | import { Inertia } from '../src/Inertia'; 6 | import { HEADERS } from '../src/utils'; 7 | import { setup, teardown } from './utils'; 8 | 9 | test.group('Rendering', (group) => { 10 | group.afterEach(async () => { 11 | await teardown(); 12 | Inertia.share({}); 13 | }); 14 | 15 | test('Should return HTML', async (assert) => { 16 | const props = { 17 | some: { 18 | props: { 19 | for: ['your', 'page'], 20 | }, 21 | }, 22 | }; 23 | const app = await setup(); 24 | const server = createServer(async (req, res) => { 25 | const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res); 26 | const respose = await ctx.inertia.render('Some/Page', props); 27 | 28 | res.write(respose); 29 | res.end(); 30 | }); 31 | 32 | const response = await supertest(server).get('/').expect(200); 33 | 34 | assert.equal( 35 | response.text, 36 | codeBlock` 37 | 38 | 39 | 40 | 41 | Journeyman 42 | 43 |
44 | 45 | `, 46 | ); 47 | }); 48 | 49 | test('Should render HTML with page-only props', async (assert) => { 50 | const props = { 51 | some: { 52 | props: { 53 | for: ['your', 'page'], 54 | }, 55 | }, 56 | }; 57 | const pageOnlyProps = { 58 | test: 'page only prop', 59 | }; 60 | const app = await setup(); 61 | const server = createServer(async (req, res) => { 62 | const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res); 63 | const respose = await ctx.inertia.render('Some/Page', props, pageOnlyProps); 64 | 65 | res.write(respose); 66 | res.end(); 67 | }); 68 | 69 | const response = await supertest(server).get('/').expect(200); 70 | 71 | assert.equal( 72 | response.text, 73 | codeBlock` 74 | 75 | 76 | 77 | 78 | Journeyman 79 | 80 | 81 | page only prop
82 | 83 | `, 84 | ); 85 | }); 86 | 87 | test('Should return JSON', async (assert) => { 88 | const props = { 89 | some: { 90 | props: { 91 | for: ['your', 'page'], 92 | }, 93 | }, 94 | }; 95 | const app = await setup(); 96 | const server = createServer(async (req, res) => { 97 | const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res); 98 | const response = await ctx.inertia.render('Some/Page', props); 99 | 100 | res.setHeader('Content-Type', 'application/json'); 101 | res.write(JSON.stringify(response)); 102 | res.end(); 103 | }); 104 | 105 | const response = await supertest(server).get('/').set(HEADERS.INERTIA_HEADER, 'true').expect(200); 106 | assert.deepEqual(response.body, { 107 | component: 'Some/Page', 108 | props, 109 | url: '/', 110 | }); 111 | }); 112 | 113 | test('Should preserve Query paramaters', async (assert) => { 114 | const props = { 115 | some: { 116 | props: { 117 | for: ['your', 'page'], 118 | }, 119 | }, 120 | }; 121 | const app = await setup(); 122 | const server = createServer(async (req, res) => { 123 | const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res); 124 | const response = await ctx.inertia.render('Some/Page', props); 125 | 126 | res.setHeader('Content-Type', 'application/json'); 127 | res.write(JSON.stringify(response)); 128 | res.end(); 129 | }); 130 | 131 | const response = await supertest(server) 132 | .get('/') 133 | .query({ search: 'query' }) 134 | .set(HEADERS.INERTIA_HEADER, 'true') 135 | .expect(200); 136 | 137 | assert.deepEqual(response.body, { 138 | component: 'Some/Page', 139 | props, 140 | url: '/?search=query', 141 | }); 142 | }); 143 | }); 144 | -------------------------------------------------------------------------------- /test/ssr.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'japa'; 2 | import supertest from 'supertest'; 3 | import { codeBlock } from 'common-tags'; 4 | import { createServer } from 'http'; 5 | import { Inertia } from '../src/Inertia'; 6 | import { setupSSR, teardown } from './utils'; 7 | 8 | test.group('SSR', (group) => { 9 | group.afterEach(async () => { 10 | await teardown(); 11 | Inertia.share({}); 12 | }); 13 | 14 | test('Should return pre-rendered react component HTML', async (assert) => { 15 | const props = { 16 | some: { 17 | props: { 18 | for: ['your', 'page'], 19 | }, 20 | }, 21 | }; 22 | const { app } = await setupSSR(); 23 | const server = createServer(async (req, res) => { 24 | const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res); 25 | const respose = await ctx.inertia.render('SomePage', props); 26 | 27 | res.write(respose); 28 | res.end(); 29 | }); 30 | 31 | const response = await supertest(server).get('/').expect(200); 32 | 33 | assert.equal( 34 | response.text, 35 | codeBlock` 36 | 37 | 38 | 39 | 40 | Journeyman 41 | 42 |

Mock SSR

43 | 44 | `, 45 | ); 46 | }); 47 | 48 | test('Should return updated pre-rendered react component HTML', async (assert) => { 49 | const props = { 50 | some: { 51 | props: { 52 | for: ['your', 'page'], 53 | }, 54 | }, 55 | }; 56 | const { app, changeContent } = await setupSSR(); 57 | const server = createServer(async (req, res) => { 58 | const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res); 59 | const respose = await ctx.inertia.render('SomePage', props); 60 | 61 | res.write(respose); 62 | res.end(); 63 | }); 64 | 65 | const response = await supertest(server).get('/').expect(200); 66 | 67 | assert.equal( 68 | response.text, 69 | codeBlock` 70 | 71 | 72 | 73 | 74 | Journeyman 75 | 76 |

Mock SSR

77 | 78 | `, 79 | ); 80 | 81 | await changeContent('Updated Mock SSR'); 82 | 83 | const updatedResponse = await supertest(server).get('/').expect(200); 84 | 85 | assert.equal( 86 | updatedResponse.text, 87 | codeBlock` 88 | 89 | 90 | 91 | 92 | Journeyman 93 | 94 |

Updated Mock SSR

95 | 96 | `, 97 | ); 98 | }); 99 | 100 | test("Should not prerender a component that's not in the allow list", async (assert) => { 101 | const props = { 102 | some: { 103 | props: { 104 | for: ['your', 'page'], 105 | }, 106 | }, 107 | }; 108 | const { app } = await setupSSR(); 109 | const server = createServer(async (req, res) => { 110 | const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res); 111 | const respose = await ctx.inertia.render('ClientSideOnlyPage', props); 112 | 113 | res.write(respose); 114 | res.end(); 115 | }); 116 | 117 | const response = await supertest(server).get('/').expect(200); 118 | 119 | assert.equal( 120 | response.text, 121 | codeBlock` 122 | 123 | 124 | 125 | 126 | Journeyman 127 | 128 |
129 | 130 | `, 131 | ); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /test/utils.ts: -------------------------------------------------------------------------------- 1 | import { Filesystem } from '@poppinss/dev-utils'; 2 | import { join } from 'path'; 3 | import { codeBlock } from 'common-tags'; 4 | import { Application } from '@adonisjs/core/build/standalone'; 5 | 6 | export const fs = new Filesystem(join(__dirname, 'app')); 7 | 8 | export async function setup() { 9 | await fs.add('.env', ''); 10 | 11 | await fs.add( 12 | 'config/app.ts', 13 | codeBlock`export const appKey = '${Math.random().toFixed(36).substring(2, 38)}', 14 | export const http = { 15 | cookie: {}, 16 | trustProxy: () => true, 17 | }`, 18 | ); 19 | 20 | await fs.add( 21 | 'config/inertia.ts', 22 | codeBlock`import { InertiaConfig } from '@ioc:EidelLev/Inertia'; 23 | 24 | export const inertia: InertiaConfig = { 25 | view: 'app', 26 | };`, 27 | ); 28 | 29 | await fs.add( 30 | 'config/session.ts', 31 | codeBlock`import Env from '@ioc:Adonis/Core/Env'; 32 | import { SessionConfig } from '@ioc:Adonis/Addons/Session'; 33 | 34 | const sessionConfig: SessionConfig = { 35 | driver: 'cookie', 36 | cookieName: 'test', 37 | age: '2h', 38 | cookie: { 39 | path: '/', 40 | httpOnly: true, 41 | sameSite: false, 42 | }, 43 | } 44 | 45 | export default sessionConfig;`, 46 | ); 47 | 48 | await fs.add( 49 | 'resources/views/app.edge', 50 | codeBlock` 51 | 52 | 53 | 54 | 55 | Journeyman 56 | 57 | 58 | @if(test) 59 | {{ test }} 60 | @endif 61 | @inertia() 62 | 63 | 64 | `, 65 | ); 66 | 67 | const app = new Application(fs.basePath, 'web', { 68 | providers: ['@adonisjs/core', '@adonisjs/view', '@adonisjs/session', '../../providers/InertiaProvider'], 69 | }); 70 | 71 | await app.setup(); 72 | await app.registerProviders(); 73 | await app.bootProviders(); 74 | 75 | return app; 76 | } 77 | 78 | export async function setupSSR() { 79 | await fs.add('.env', ''); 80 | 81 | await fs.add( 82 | 'config/app.ts', 83 | codeBlock`export const appKey = '${Math.random().toFixed(36).substring(2, 38)}', 84 | export const http = { 85 | cookie: {}, 86 | trustProxy: () => true, 87 | }`, 88 | ); 89 | 90 | await fs.add( 91 | 'config/inertia.ts', 92 | codeBlock`import { InertiaConfig } from '@ioc:EidelLev/Inertia'; 93 | 94 | export const inertia: InertiaConfig = { 95 | view: 'app', 96 | ssr: { 97 | enabled: true, 98 | allowList: ['SomePage'], 99 | autoreload: true, 100 | } 101 | };`, 102 | ); 103 | 104 | await fs.add( 105 | 'config/session.ts', 106 | codeBlock`import Env from '@ioc:Adonis/Core/Env'; 107 | import { SessionConfig } from '@ioc:Adonis/Addons/Session'; 108 | 109 | const sessionConfig: SessionConfig = { 110 | driver: 'cookie', 111 | cookieName: 'test', 112 | age: '2h', 113 | cookie: { 114 | path: '/', 115 | httpOnly: true, 116 | sameSite: false, 117 | }, 118 | } 119 | 120 | export default sessionConfig;`, 121 | ); 122 | 123 | await fs.add( 124 | 'resources/views/app.edge', 125 | codeBlock` 126 | 127 | 128 | 129 | 130 | Journeyman 131 | 132 | 133 | @if(test) 134 | {{ test }} 135 | @endif 136 | @inertia() 137 | 138 | 139 | `, 140 | ); 141 | 142 | await fs.add( 143 | 'inertia/ssr/ssr.js', 144 | codeBlock` 145 | module.exports.default = function render(page) { 146 | return {body:'

Mock SSR

', head: ''}; 147 | } 148 | `, 149 | ); 150 | 151 | const changeContent = async (content) => 152 | fs.add( 153 | 'inertia/ssr/ssr.js', 154 | codeBlock` 155 | module.exports.default = function render(page) { 156 | return {body:'

${content}

', head: ''}; 157 | } 158 | `, 159 | ); 160 | 161 | const app = new Application(fs.basePath, 'web', { 162 | providers: ['@adonisjs/core', '@adonisjs/view', '@adonisjs/session', '../../providers/InertiaProvider'], 163 | }); 164 | 165 | await app.setup(); 166 | await app.registerProviders(); 167 | await app.bootProviders(); 168 | 169 | return { app, changeContent }; 170 | } 171 | 172 | export async function teardown() { 173 | await fs.cleanup(); 174 | } 175 | -------------------------------------------------------------------------------- /test/validation.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'japa'; 2 | import { HEADERS } from '../src/utils'; 3 | import { setup, teardown } from './utils'; 4 | 5 | test.group('Validation negotiator', (group) => { 6 | group.afterEach(async () => { 7 | await teardown(); 8 | }); 9 | 10 | test('Should use vanilla validator for Inertia requests', async (assert) => { 11 | const app = await setup(); 12 | const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}); 13 | const { schema } = app.container.use('Adonis/Core/Validator'); 14 | 15 | ctx.request.request.headers.accept = 'application/json'; 16 | ctx.request.request.headers[HEADERS.INERTIA_HEADER] = 'true'; 17 | 18 | class Validator { 19 | public schema = schema.create({ 20 | username: schema.string(), 21 | }); 22 | } 23 | 24 | try { 25 | ctx.request.headers(); 26 | await ctx.request.validate(Validator); 27 | } catch (error) { 28 | assert.deepEqual(error.messages, { 29 | username: ['required validation failed'], 30 | }); 31 | } 32 | }); 33 | 34 | test('Should use vanilla validator for HTML requests', async (assert) => { 35 | const app = await setup(); 36 | const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}); 37 | const { schema } = app.container.use('Adonis/Core/Validator'); 38 | 39 | ctx.request.request.headers.accept = 'text/html'; 40 | 41 | class Validator { 42 | public schema = schema.create({ 43 | username: schema.string(), 44 | }); 45 | } 46 | 47 | try { 48 | ctx.request.headers(); 49 | await ctx.request.validate(Validator); 50 | } catch (error) { 51 | assert.deepEqual(error.messages, { 52 | username: ['required validation failed'], 53 | }); 54 | } 55 | }); 56 | 57 | test('Should use JSON API validator for JSON API requests', async (assert) => { 58 | const app = await setup(); 59 | const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}); 60 | const { schema } = app.container.use('Adonis/Core/Validator'); 61 | 62 | ctx.request.request.headers.accept = 'application/vnd.api+json'; 63 | 64 | class Validator { 65 | public schema = schema.create({ 66 | username: schema.string(), 67 | }); 68 | } 69 | 70 | try { 71 | ctx.request.headers(); 72 | await ctx.request.validate(Validator); 73 | } catch (error) { 74 | assert.deepEqual(error.messages, { 75 | errors: [ 76 | { 77 | code: 'required', 78 | source: { 79 | pointer: 'username', 80 | }, 81 | title: 'required validation failed', 82 | }, 83 | ], 84 | }); 85 | } 86 | }); 87 | 88 | test('Should use Ajax validator for API requests', async (assert) => { 89 | const app = await setup(); 90 | const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}); 91 | const { schema } = app.container.use('Adonis/Core/Validator'); 92 | 93 | ctx.request.request.headers.accept = 'application/json'; 94 | 95 | class Validator { 96 | public schema = schema.create({ 97 | username: schema.string(), 98 | }); 99 | } 100 | 101 | try { 102 | ctx.request.headers(); 103 | await ctx.request.validate(Validator); 104 | } catch (error) { 105 | assert.deepEqual(error.messages, { 106 | errors: [ 107 | { 108 | rule: 'required', 109 | message: 'required validation failed', 110 | field: 'username', 111 | }, 112 | ], 113 | }); 114 | } 115 | }); 116 | 117 | test('Should use Ajax validator for API requests made from client', async (assert) => { 118 | const app = await setup(); 119 | const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}); 120 | const { schema } = app.container.use('Adonis/Core/Validator'); 121 | 122 | ctx.request.request.headers['x-requested-with'] = 'xmlhttprequest'; 123 | 124 | class Validator { 125 | public schema = schema.create({ 126 | username: schema.string(), 127 | }); 128 | } 129 | 130 | try { 131 | ctx.request.headers(); 132 | await ctx.request.validate(Validator); 133 | } catch (error) { 134 | assert.deepEqual(error.messages, { 135 | errors: [ 136 | { 137 | rule: 'required', 138 | message: 'required validation failed', 139 | field: 'username', 140 | }, 141 | ], 142 | }); 143 | } 144 | }); 145 | }); 146 | -------------------------------------------------------------------------------- /test/versioning.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'japa'; 2 | import supertest from 'supertest'; 3 | import { Inertia } from '../src/Inertia'; 4 | import { HEADERS } from '../src/utils'; 5 | import { createServer } from 'http'; 6 | import { setup, teardown } from './utils'; 7 | 8 | test.group('Asset versioning', (group) => { 9 | group.afterEach(async () => { 10 | await teardown(); 11 | }); 12 | 13 | test('Should return 409 CONFLICT', async () => { 14 | const props = { 15 | some: { 16 | props: { 17 | for: ['your', 'page'], 18 | }, 19 | }, 20 | }; 21 | const app = await setup(); 22 | const server = createServer(async (req, res) => { 23 | const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res); 24 | Inertia.version('new'); 25 | 26 | await ctx.inertia.render('Some/Page', props); 27 | res.end(); 28 | }); 29 | 30 | await supertest(server) 31 | .get('/') 32 | .set(HEADERS.INERTIA_HEADER, 'true') 33 | .set(HEADERS.INERTIA_VERSION, 'old') 34 | .expect(409); 35 | }); 36 | 37 | test('Should lazily evaluate version', async () => { 38 | const props = { 39 | some: { 40 | props: { 41 | for: ['your', 'page'], 42 | }, 43 | }, 44 | }; 45 | const app = await setup(); 46 | const server = createServer(async (req, res) => { 47 | const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res); 48 | Inertia.version(() => 'new'); 49 | 50 | await ctx.inertia.render('Some/Page', props); 51 | res.end(); 52 | }); 53 | 54 | await supertest(server) 55 | .get('/') 56 | .set(HEADERS.INERTIA_HEADER, 'true') 57 | .set(HEADERS.INERTIA_VERSION, 'old') 58 | .expect(409); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@adonisjs/mrm-preset/_tsconfig", 3 | "compilerOptions": { 4 | "lib": ["dom", "es2019", "es2020.bigint", "es2020.string", "es2020.symbol.wellknown"], 5 | "skipLibCheck": true 6 | }, 7 | "files": [ 8 | "./node_modules/@adonisjs/core/build/adonis-typings/index.d.ts", 9 | "./node_modules/@adonisjs/view/build/adonis-typings/index.d.ts", 10 | "./node_modules/@adonisjs/config/build/adonis-typings/config.d.ts", 11 | "./node_modules/@adonisjs/session/build/adonis-typings/index.d.ts" 12 | ], 13 | "exclude": ["build/*", "node_modules/*"] 14 | } 15 | --------------------------------------------------------------------------------