├── .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 | 
4 |
5 |
6 |
7 |
8 |
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 | 
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 |
--------------------------------------------------------------------------------