inertia_rails 3.21.1 → 3.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/README.md +117 -309
- data/app/controllers/inertia_rails/static_controller.rb +9 -1
- data/lib/generators/inertia/install/frameworks.yml +4 -2
- data/lib/generators/inertia/install/install_generator.rb +11 -4
- data/lib/generators/inertia/install/js_package_manager.rb +3 -2
- data/lib/inertia_rails/configuration.rb +15 -0
- data/lib/inertia_rails/controller.rb +18 -1
- data/lib/inertia_rails/defer_prop.rb +5 -0
- data/lib/inertia_rails/extensions/mapper.rb +19 -7
- data/lib/inertia_rails/generators/controller_template_base.rb +10 -6
- data/lib/inertia_rails/generators/helper.rb +4 -1
- data/lib/inertia_rails/generators/scaffold_template_base.rb +9 -4
- data/lib/inertia_rails/helper.rb +7 -1
- data/lib/inertia_rails/middleware.rb +9 -1
- data/lib/inertia_rails/props_resolver.rb +40 -18
- data/lib/inertia_rails/renderer.rb +3 -5
- data/lib/inertia_rails/version.rb +1 -1
- data/lib/inertia_rails/xsrf_cookie_refresh_policy.rb +64 -0
- data/lib/inertia_rails.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4e9384ee4e756ec30efcbb0afa77c7d557d02dd4d5dc60f1dc6239ec1d0b1878
|
|
4
|
+
data.tar.gz: 98e207f798ae91e888f1de274caa506bbc289ac51ea2e20e2eb5465f818575fd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6d9dd387eb80c39743d7d48268ef7540710a27194c52f165e4f75565fd4e76cc151db286b64b5e050fa387027b80a0eb3b37210908068a826cfedfe6003aea67
|
|
7
|
+
data.tar.gz: 3309e07158894db2e60b03c15e0f46b8d056e09821487412709b738a5ceeb2508c54d5df7421ad77ee04e30a2592e8a07a48de720192446ad5290e18c423eb47
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [3.22.0] - 2026-07-17
|
|
10
|
+
|
|
11
|
+
* Only set the `XSRF-TOKEN` cookie on HTML and XHR responses. Other responses no longer carry a `Set-Cookie` that keeps CDNs from caching them — notably ActiveStorage's images, since its controllers also inherit from `ActionController::Base` (@acetinick)
|
|
12
|
+
* Add `xsrf_cookie_refresh` configuration option. Set to `:lazy` to skip rewriting the `XSRF-TOKEN` cookie on safe requests when a valid cookie is already present, keeping conditionally cached (`ETag`/`304`) responses free of `Set-Cookie` churn (@darkamenosa)
|
|
13
|
+
* Fix `inertia: { errors: }` and `inertia: { clear_history: }` being discarded when `redirect_to` is given an explicit non-302 status such as `status: :see_other` (303/307/308) (@skryukov)
|
|
14
|
+
* Fix cross-variant `304 Not Modified` responses under HTTP conditional caching (`fresh_when` / `stale?`) by folding the Inertia request headers into the ETag, so HTML, JSON, and partial-reload representations of the same URL no longer share a validator (@skryukov)
|
|
15
|
+
* Generate controller views at the paths resolved by `render inertia: true` (@oiahoon)
|
|
16
|
+
* Add `@inertiajs/core` as a direct dev dependency in the TypeScript install generator, so pnpm exposes the adapter types that `globals.d.ts` augments (@heyrobertchang)
|
|
17
|
+
* Fix option handling in the `inertia` routes helper: user-supplied `defaults:` no longer replace the component (previously producing `component: null` responses), the route-to-component pair no longer leaks into `params`, and additional String-keyed pairs define routes instead of being silently dropped (@skryukov)
|
|
18
|
+
* Raise a clear error when `parent_controller` does not inherit from `ActionController::Base` instead of an opaque `NoMethodError` (@skryukov)
|
|
19
|
+
* Add static `props:` support to the `inertia` routes helper: `inertia 'about' => 'About', props: { title: 'About us' }` (@skryukov)
|
|
20
|
+
* Deduplicate `X-Inertia` in the `Vary` response header (@skryukov)
|
|
21
|
+
|
|
22
|
+
## [3.21.2] - 2026-06-09
|
|
23
|
+
|
|
24
|
+
* Add `rescue: true` option to `InertiaRails.defer` to rescue and report exceptions raised while resolving a deferred prop (@skryukov)
|
|
25
|
+
* Add CSP nonce to the initial page script when needed (@nicholaspufal)
|
|
26
|
+
* Fix Vite install to use the configured package manager in the generator (@akicho8)
|
|
27
|
+
* Fix TypeScript packages installed as dependencies instead of devDependencies (@alec-c4)
|
|
28
|
+
|
|
9
29
|
## [3.21.1] - 2026-05-19
|
|
10
30
|
|
|
11
31
|
* Specify initializer run order for middleware insertion to avoid frozen middleware stack errors on Rails 8.1+ (@julik)
|
data/README.md
CHANGED
|
@@ -1,347 +1,155 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
end
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
#### Rails Component and Instance Props
|
|
45
|
-
|
|
46
|
-
Starting in version 3.0, Inertia Rails allows you to provide your component name and props via common rails conventions.
|
|
47
|
-
|
|
48
|
-
```ruby
|
|
49
|
-
class EventsController < ApplicationController
|
|
50
|
-
use_inertia_instance_props
|
|
51
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<a href="https://inertia-rails.dev">
|
|
3
|
+
<img src="https://inertia-rails.dev/logo.svg" alt="Inertia Rails" width="150">
|
|
4
|
+
</a>
|
|
5
|
+
|
|
6
|
+
<h1>Build frontend experiences with the backend you love</h1>
|
|
7
|
+
|
|
8
|
+
<p>
|
|
9
|
+
<strong>Single-page React, Vue, and Svelte apps powered by your existing Rails
|
|
10
|
+
controllers, routes, and authentication. No API required.</strong>
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
<p>
|
|
14
|
+
<a href="https://rubygems.org/gems/inertia_rails"><img src="https://img.shields.io/gem/v/inertia_rails" alt="Gem version"></a>
|
|
15
|
+
<a href="https://rubygems.org/gems/inertia_rails"><img src="https://img.shields.io/gem/dt/inertia_rails" alt="Downloads"></a>
|
|
16
|
+
<a href="https://github.com/inertiajs/inertia-rails/actions/workflows/push.yml"><img src="https://github.com/inertiajs/inertia-rails/actions/workflows/push.yml/badge.svg" alt="Build status"></a>
|
|
17
|
+
<a href="https://github.com/inertiajs/inertia-rails/blob/master/LICENSE.txt"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT license"></a>
|
|
18
|
+
<a href="https://discord.gg/inertiajs"><img src="https://img.shields.io/badge/discord-join-5865F2?logo=discord&logoColor=white" alt="Discord"></a>
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
<p>
|
|
22
|
+
<a href="https://inertia-rails.dev"><strong>Documentation</strong></a> ·
|
|
23
|
+
<a href="https://inertia-rails.dev/guide/server-side-setup"><strong>Get started</strong></a> ·
|
|
24
|
+
<a href="https://inertia-rails.dev/guide/demo-application"><strong>Demo</strong></a> ·
|
|
25
|
+
<a href="https://discord.gg/inertiajs"><strong>Discord</strong></a>
|
|
26
|
+
</p>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Your controllers. Your routes. Modern components.
|
|
32
|
+
|
|
33
|
+
Inertia lets you build a fully client-side rendered single-page app without the
|
|
34
|
+
complexity of a separate API. Pass data from Rails directly to React, Vue, or
|
|
35
|
+
Svelte as **props** — no REST endpoints, no GraphQL, no client-side data
|
|
36
|
+
fetching, no state-management headaches.
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
# app/controllers/users_controller.rb
|
|
40
|
+
class UsersController < ApplicationController
|
|
52
41
|
def index
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
end
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
is the same as
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
```ruby
|
|
63
|
-
class EventsController < ApplicationController
|
|
64
|
-
def index
|
|
65
|
-
render inertia: 'events/index', props: {
|
|
66
|
-
events: Event.all
|
|
42
|
+
render inertia: {
|
|
43
|
+
users: User.active.map { |user| user.as_json(only: [:id, :name, :email]) }
|
|
67
44
|
}
|
|
68
45
|
end
|
|
69
46
|
end
|
|
70
47
|
```
|
|
71
48
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
are automatically disabled for that response. Instance props are only included if they are defined after the before filter is set from `use_inertia_instance_props`.
|
|
49
|
+
```jsx
|
|
50
|
+
// app/frontend/pages/users/index.jsx
|
|
51
|
+
import { Link } from '@inertiajs/react'
|
|
76
52
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
53
|
+
const Users = ({ users }) => (
|
|
54
|
+
<>
|
|
55
|
+
{users.map((user) => (
|
|
56
|
+
<div key={user.id}>
|
|
57
|
+
<Link href={`/users/${user.id}`}>{user.name}</Link>
|
|
58
|
+
<p>{user.email}</p>
|
|
59
|
+
</div>
|
|
60
|
+
))}
|
|
61
|
+
</>
|
|
86
62
|
)
|
|
87
63
|
|
|
64
|
+
export default Users
|
|
88
65
|
```
|
|
89
66
|
|
|
67
|
+
That's the whole loop: the controller returns props, the component renders them.
|
|
68
|
+
Links and form submits are intercepted and turned into XHR visits, so navigation
|
|
69
|
+
feels instant — but you're still writing plain Rails on the server.
|
|
90
70
|
|
|
71
|
+
## Get started
|
|
91
72
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
Inertia layouts use the rails layout convention and can be set or changed in the same way.
|
|
95
|
-
|
|
96
|
-
```ruby
|
|
97
|
-
class EventsController < ApplicationController
|
|
98
|
-
layout 'inertia_application'
|
|
99
|
-
end
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
### Shared Data
|
|
104
|
-
|
|
105
|
-
If you have data that you want to be provided as a prop to every component (a common use-case is information about the authenticated user) you can use the `inertia_share` controller method.
|
|
106
|
-
|
|
107
|
-
```ruby
|
|
108
|
-
class EventsController < ApplicationController
|
|
109
|
-
# share synchronously
|
|
110
|
-
inertia_share app_name: env['app.name']
|
|
111
|
-
|
|
112
|
-
# share lazily, evaluated at render time
|
|
113
|
-
inertia_share do
|
|
114
|
-
if logged_in?
|
|
115
|
-
{
|
|
116
|
-
user: logged_in_user,
|
|
117
|
-
}
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
# share lazily alternate syntax
|
|
122
|
-
inertia_share user_count: lambda { User.count }
|
|
123
|
-
|
|
124
|
-
end
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
#### Deep Merging Shared Data
|
|
128
|
-
|
|
129
|
-
By default, Inertia will shallow merge data defined in an action with the shared data. You might want a deep merge. Imagine using shared data to represent defaults you'll override sometimes.
|
|
130
|
-
|
|
131
|
-
```ruby
|
|
132
|
-
class ApplicationController
|
|
133
|
-
inertia_share do
|
|
134
|
-
{ basketball_data: { points: 50, rebounds: 100 } }
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
Let's say we want a particular action to change only part of that data structure. The renderer accepts a `deep_merge` option:
|
|
140
|
-
|
|
141
|
-
```ruby
|
|
142
|
-
class CrazyScorersController < ApplicationController
|
|
143
|
-
def index
|
|
144
|
-
render inertia: 'CrazyScorersComponent',
|
|
145
|
-
props: { basketball_data: { points: 100 } },
|
|
146
|
-
deep_merge: true
|
|
147
|
-
end
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
# The renderer will send this to the frontend:
|
|
151
|
-
{
|
|
152
|
-
basketball_data: {
|
|
153
|
-
points: 100,
|
|
154
|
-
rebounds: 100,
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
Deep merging can be configured using the [`deep_merge_shared_data`](#deep_merge_shared_data) configuration option.
|
|
160
|
-
|
|
161
|
-
If deep merging is enabled, you can still opt-out within the action:
|
|
162
|
-
|
|
163
|
-
```ruby
|
|
164
|
-
class CrazyScorersController < ApplicationController
|
|
165
|
-
inertia_config(deep_merge_shared_data: true)
|
|
166
|
-
|
|
167
|
-
inertia_share do
|
|
168
|
-
{
|
|
169
|
-
basketball_data: {
|
|
170
|
-
points: 50,
|
|
171
|
-
rebounds: 10,
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def index
|
|
177
|
-
render inertia: 'CrazyScorersComponent',
|
|
178
|
-
props: { basketball_data: { points: 100 } },
|
|
179
|
-
deep_merge: false
|
|
180
|
-
end
|
|
181
|
-
end
|
|
73
|
+
**Add to an existing Rails app** — the installer sets up Vite, your chosen framework, and example pages:
|
|
182
74
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
points: 100,
|
|
187
|
-
}
|
|
188
|
-
}
|
|
75
|
+
```bash
|
|
76
|
+
bundle add inertia_rails
|
|
77
|
+
bin/rails generate inertia:install
|
|
189
78
|
```
|
|
190
79
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
On the frontend, Inertia supports the concept of "partial reloads" where only the props requested are returned by the server. Sometimes, you may want to use this flow to avoid processing a particularly slow prop on the initial load. In this case, you can use Optional props. Optional props aren't evaluated unless they're specifically requested by name in a partial reload.
|
|
80
|
+
**Or start from a kit** with authentication, Vite, optional SSR, and Kamal
|
|
81
|
+
deployment already wired up:
|
|
194
82
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
83
|
+
- [React Starter Kit](https://github.com/inertia-rails/react-starter-kit) — React 19 · TypeScript · shadcn/ui
|
|
84
|
+
- [Vue Starter Kit](https://github.com/inertia-rails/vue-starter-kit) — Vue 3 · TypeScript · shadcn-vue
|
|
85
|
+
- [Svelte Starter Kit](https://github.com/inertia-rails/svelte-starter-kit) — Svelte 5 · TypeScript · shadcn-svelte
|
|
198
86
|
|
|
199
|
-
|
|
87
|
+
Full walkthrough: **[Server-side setup](https://inertia-rails.dev/guide/server-side-setup)** and **[Client-side setup](https://inertia-rails.dev/guide/client-side-setup)**.
|
|
200
88
|
|
|
201
|
-
|
|
89
|
+
## Built for real Rails apps
|
|
202
90
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
91
|
+
| | |
|
|
92
|
+
|---|---|
|
|
93
|
+
| **[Forms that work](https://inertia-rails.dev/guide/forms)** | Validation errors flow from Rails to your components automatically — no manual wiring. |
|
|
94
|
+
| **[Server-side rendering](https://inertia-rails.dev/guide/server-side-rendering)** | Full SSR for SEO and fast first paint. Your React/Vue/Svelte, rendered on Rails. |
|
|
95
|
+
| **[Test like Rails](https://inertia-rails.dev/guide/testing)** | RSpec and Minitest matchers that feel native. Assert on props, components, and more. |
|
|
96
|
+
| **[Partial reloads](https://inertia-rails.dev/guide/partial-reloads)** | Refresh only the data you need. Keep interactions snappy without full page loads. |
|
|
97
|
+
| **[Shared data](https://inertia-rails.dev/guide/shared-data)** | Current user, flash, permissions — available on every page automatically. |
|
|
98
|
+
| **[Deferred props](https://inertia-rails.dev/guide/deferred-props)** | Load the page fast, fetch expensive data after, with built-in loading states. |
|
|
99
|
+
| **[Rails generators](https://inertia-rails.dev/guide/server-side-setup)** | Scaffold entire CRUD interfaces — controllers with matching components. |
|
|
100
|
+
| **[History encryption](https://inertia-rails.dev/guide/history-encryption)** | Keep sensitive data private, even in browser history. Toggle per page. |
|
|
206
101
|
|
|
207
|
-
|
|
102
|
+
## Why Inertia?
|
|
208
103
|
|
|
209
|
-
|
|
104
|
+
Inertia sits between traditional server-rendered apps and full SPAs.
|
|
210
105
|
|
|
211
|
-
|
|
106
|
+
**vs. Hotwire** — Same monolith, different view layer. Both keep you in Rails;
|
|
107
|
+
Inertia gives you the full React/Vue/Svelte component model and the npm
|
|
108
|
+
ecosystem instead of HTML-over-the-wire. Choose Hotwire for minimal JS and
|
|
109
|
+
server-rendered HTML; choose Inertia for a modern component architecture.
|
|
212
110
|
|
|
213
|
-
|
|
111
|
+
**vs. API + SPA** — Same frontend, no API hassle. Both give you React/Vue/Svelte,
|
|
112
|
+
but Inertia removes the API layer entirely: one router (Rails), Rails sessions
|
|
113
|
+
instead of a JWT/OAuth dance, and props from your controller instead of fetching
|
|
114
|
+
in `useEffect`. Choose an API for public/mobile clients; choose Inertia for
|
|
115
|
+
focused web products. (You can always add an API alongside Inertia later.)
|
|
214
116
|
|
|
215
|
-
|
|
117
|
+
See the full [comparison and FAQ](https://inertia-rails.dev/#why-inertia) →
|
|
216
118
|
|
|
217
|
-
|
|
119
|
+
## Documentation
|
|
218
120
|
|
|
219
|
-
|
|
121
|
+
Everything lives at **[inertia-rails.dev](https://inertia-rails.dev)**:
|
|
220
122
|
|
|
221
|
-
|
|
222
|
-
|
|
123
|
+
- [How it works](https://inertia-rails.dev/guide/how-it-works)
|
|
124
|
+
- [Pages & layouts](https://inertia-rails.dev/guide/pages)
|
|
125
|
+
- [Forms & validation](https://inertia-rails.dev/guide/forms)
|
|
126
|
+
- [Shared data](https://inertia-rails.dev/guide/shared-data) · [Partial reloads](https://inertia-rails.dev/guide/partial-reloads) · [Deferred props](https://inertia-rails.dev/guide/deferred-props)
|
|
127
|
+
- [Server-side rendering](https://inertia-rails.dev/guide/server-side-rendering)
|
|
128
|
+
- [Testing](https://inertia-rails.dev/guide/testing)
|
|
129
|
+
- [Configuration reference](https://inertia-rails.dev/guide/configuration)
|
|
223
130
|
|
|
224
|
-
|
|
225
|
-
# Example: force a full-reload if the deployed assets change.
|
|
226
|
-
config.version = ViteRuby.digest
|
|
227
|
-
end
|
|
228
|
-
```
|
|
131
|
+
## Community
|
|
229
132
|
|
|
230
|
-
|
|
133
|
+
- [Awesome Inertia Rails](https://inertia-rails.dev/awesome) — gems, tutorials, and real-world apps
|
|
134
|
+
- [Discord](https://discord.gg/inertiajs) — ask questions, get answers fast
|
|
135
|
+
- [GitHub Discussions & Issues](https://github.com/inertiajs/inertia-rails/issues) — browse the source, report bugs
|
|
231
136
|
|
|
232
|
-
|
|
137
|
+
## Contributing
|
|
233
138
|
|
|
234
|
-
|
|
139
|
+
Bug reports and pull requests are welcome. To run the test suite:
|
|
235
140
|
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
version: "events-#{InertiaRails.configuration.version}",
|
|
240
|
-
ssr_enabled: -> { action_name == "index" },
|
|
241
|
-
)
|
|
242
|
-
end
|
|
141
|
+
```bash
|
|
142
|
+
bundle install
|
|
143
|
+
bundle exec rspec
|
|
243
144
|
```
|
|
244
145
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
#### `version` _(recommended)_
|
|
248
|
-
|
|
249
|
-
This allows Inertia to detect if the app running in the client is oudated,
|
|
250
|
-
forcing a full page visit instead of an XHR visit on the next request.
|
|
251
|
-
|
|
252
|
-
See [assets versioning](https://inertiajs.com/asset-versioning).
|
|
253
|
-
|
|
254
|
-
__Default__: `nil`
|
|
255
|
-
|
|
256
|
-
#### `deep_merge_shared_data`
|
|
257
|
-
|
|
258
|
-
When enabled, props will be deep merged with shared data, combining hashes
|
|
259
|
-
with the same keys instead of replacing them.
|
|
260
|
-
|
|
261
|
-
__Default__: `false`
|
|
146
|
+
See the [Code of Conduct](CODE_OF_CONDUCT.md). Everyone interacting with the
|
|
147
|
+
project is expected to follow it.
|
|
262
148
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
Overrides Rails default rendering behavior to render using Inertia by default.
|
|
266
|
-
|
|
267
|
-
__Default__: `false`
|
|
268
|
-
|
|
269
|
-
#### `encrypt_history`
|
|
270
|
-
|
|
271
|
-
When enabled, you instruct Inertia to encrypt your app's history, it uses
|
|
272
|
-
the browser's built-in [`crypto` api](https://developer.mozilla.org/en-US/docs/Web/API/Crypto)
|
|
273
|
-
to encrypt the current page's data before pushing it to the history state.
|
|
274
|
-
|
|
275
|
-
__Default__: `false`
|
|
276
|
-
|
|
277
|
-
#### `ssr_enabled` _(experimental)_
|
|
278
|
-
|
|
279
|
-
Whether to use a JavaScript server to pre-render your JavaScript pages,
|
|
280
|
-
allowing your visitors to receive fully rendered HTML when they first visit
|
|
281
|
-
your application.
|
|
282
|
-
|
|
283
|
-
Requires a JS server to be available at `ssr_url`. [_Example_](https://github.com/ElMassimo/inertia-rails-ssr-template)
|
|
284
|
-
|
|
285
|
-
__Default__: `false`
|
|
286
|
-
|
|
287
|
-
#### `ssr_url` _(experimental)_
|
|
288
|
-
|
|
289
|
-
The URL of the JS server that will pre-render the app using the specified
|
|
290
|
-
component and props.
|
|
291
|
-
|
|
292
|
-
__Default__: `"http://localhost:13714"`
|
|
293
|
-
|
|
294
|
-
## Testing
|
|
295
|
-
|
|
296
|
-
If you're using Rspec, Inertia Rails comes with some nice test helpers to make things simple.
|
|
297
|
-
|
|
298
|
-
To use these helpers, just add the following require statement to your `spec/rails_helper.rb`
|
|
299
|
-
|
|
300
|
-
```ruby
|
|
301
|
-
require 'inertia_rails/rspec'
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
And in any test you want to use the inertia helpers, add the inertia flag to the describe block
|
|
305
|
-
|
|
306
|
-
```ruby
|
|
307
|
-
RSpec.describe EventController, type: :request do
|
|
308
|
-
describe '#index', inertia: true do
|
|
309
|
-
# ...
|
|
310
|
-
end
|
|
311
|
-
end
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
### Assertions
|
|
315
|
-
|
|
316
|
-
```ruby
|
|
317
|
-
RSpec.describe EventController, type: :request do
|
|
318
|
-
describe '#index', inertia: true do
|
|
319
|
-
|
|
320
|
-
# check the component
|
|
321
|
-
expect_inertia.to render_component 'Event/Index'
|
|
322
|
-
|
|
323
|
-
# access the component name
|
|
324
|
-
expect(inertia.component).to eq 'TestComponent'
|
|
325
|
-
|
|
326
|
-
# props (including shared props)
|
|
327
|
-
expect_inertia.to have_exact_props({name: 'Brandon', sport: 'hockey'})
|
|
328
|
-
expect_inertia.to include_props({sport: 'hockey'})
|
|
329
|
-
|
|
330
|
-
# access props
|
|
331
|
-
expect(inertia.props[:name]).to eq 'Brandon'
|
|
332
|
-
|
|
333
|
-
# view data
|
|
334
|
-
expect_inertia.to have_exact_view_data({name: 'Brian', sport: 'basketball'})
|
|
335
|
-
expect_inertia.to include_view_data({sport: 'basketball'})
|
|
336
|
-
|
|
337
|
-
# access view data
|
|
338
|
-
expect(inertia.view_data[:name]).to eq 'Brian'
|
|
339
|
-
|
|
340
|
-
end
|
|
341
|
-
end
|
|
342
|
-
|
|
343
|
-
```
|
|
149
|
+
## Credits
|
|
344
150
|
|
|
345
|
-
|
|
151
|
+
Inertia Rails is part of the official [Inertia.js](https://inertiajs.com)
|
|
152
|
+
organization. It was originally created by the team at
|
|
153
|
+
[bellaWatt](https://bellawatt.com) and is maintained by the Inertia.js community.
|
|
346
154
|
|
|
347
|
-
[
|
|
155
|
+
Released under the [MIT License](LICENSE.txt).
|
|
@@ -3,8 +3,16 @@
|
|
|
3
3
|
module InertiaRails
|
|
4
4
|
class StaticController < InertiaRails.configuration.parent_controller.constantize
|
|
5
5
|
def static
|
|
6
|
+
# Checked at dispatch, not load: eager loading defines this class even
|
|
7
|
+
# in apps that never draw an `inertia` route, and those must still boot.
|
|
8
|
+
unless is_a?(::ActionController::Base)
|
|
9
|
+
raise ArgumentError,
|
|
10
|
+
'`config.parent_controller` must inherit from ActionController::Base to serve `inertia` routes, ' \
|
|
11
|
+
"got #{InertiaRails.configuration.parent_controller.inspect}"
|
|
12
|
+
end
|
|
13
|
+
|
|
6
14
|
respond_to do |format|
|
|
7
|
-
format.html { render inertia: params[:component] }
|
|
15
|
+
format.html { render inertia: params[:component], props: request.path_parameters[:props]&.deep_dup || {} }
|
|
8
16
|
end
|
|
9
17
|
end
|
|
10
18
|
end
|
|
@@ -31,7 +31,8 @@ vue:
|
|
|
31
31
|
- "@vitejs/plugin-vue"
|
|
32
32
|
- "vite@latest"
|
|
33
33
|
packages_ts:
|
|
34
|
-
-
|
|
34
|
+
# vue-tsc requires typescript/lib/tsc, removed in TypeScript 7
|
|
35
|
+
- "typescript@^6"
|
|
35
36
|
- "vue-tsc"
|
|
36
37
|
vite_plugin_import: "import vue from '@vitejs/plugin-vue'"
|
|
37
38
|
vite_plugin_call: "vue()"
|
|
@@ -55,7 +56,8 @@ svelte:
|
|
|
55
56
|
packages_ts:
|
|
56
57
|
- "@tsconfig/svelte@5"
|
|
57
58
|
- "svelte-check"
|
|
58
|
-
-
|
|
59
|
+
# svelte-check uses the TypeScript API, gone from TypeScript 7's package entry
|
|
60
|
+
- "typescript@^6"
|
|
59
61
|
- "tslib"
|
|
60
62
|
vite_plugin_import: "import { svelte } from '@sveltejs/vite-plugin-svelte'"
|
|
61
63
|
vite_plugin_call: "svelte()"
|
|
@@ -142,7 +142,13 @@ module Inertia
|
|
|
142
142
|
def install_typescript
|
|
143
143
|
say 'Adding TypeScript support'
|
|
144
144
|
|
|
145
|
-
|
|
145
|
+
# globals.d.ts augments @inertiajs/core and scaffolds import its types;
|
|
146
|
+
# pnpm won't expose the adapters' transitive dep, so declare it directly.
|
|
147
|
+
add_dependencies(
|
|
148
|
+
"@inertiajs/core@#{options[:inertia_version]}",
|
|
149
|
+
*FRAMEWORKS[framework]['packages_ts'],
|
|
150
|
+
dev: true
|
|
151
|
+
)
|
|
146
152
|
|
|
147
153
|
say 'Copying tsconfig and types'
|
|
148
154
|
|
|
@@ -223,7 +229,8 @@ module Inertia
|
|
|
223
229
|
say_error 'Failed to install Vite Rails gem', :red
|
|
224
230
|
exit(false)
|
|
225
231
|
end
|
|
226
|
-
|
|
232
|
+
vite_ruby_install_options = package_manager.present? ? "--package-manager=#{package_manager.name}" : ''
|
|
233
|
+
if (capture = run("bundle exec vite install #{vite_ruby_install_options}", capture: !verbose?))
|
|
227
234
|
rename_application_js_to_ts if typescript?
|
|
228
235
|
run('bundle binstub vite_ruby', capture: !verbose?) unless File.exist?(file_path('bin/vite'))
|
|
229
236
|
say 'Vite Rails successfully installed', :green
|
|
@@ -282,8 +289,8 @@ module Inertia
|
|
|
282
289
|
@package_manager ||= JSPackageManager.new(self)
|
|
283
290
|
end
|
|
284
291
|
|
|
285
|
-
def add_dependencies(*packages)
|
|
286
|
-
package_manager.add_dependencies(*packages)
|
|
292
|
+
def add_dependencies(*packages, dev: false)
|
|
293
|
+
package_manager.add_dependencies(*packages, dev: dev)
|
|
287
294
|
end
|
|
288
295
|
|
|
289
296
|
def vite_config_path
|
|
@@ -15,10 +15,11 @@ module Inertia
|
|
|
15
15
|
name.present?
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
def add_dependencies(*dependencies)
|
|
18
|
+
def add_dependencies(*dependencies, dev: false)
|
|
19
|
+
dev_flag = dev ? ' -D' : ''
|
|
19
20
|
options = @generator.options[:verbose] ? '' : ' --silent'
|
|
20
21
|
@generator.in_root do
|
|
21
|
-
@generator.run "#{name} add #{dependencies.join(' ')}#{options}"
|
|
22
|
+
@generator.run "#{name} add#{dev_flag} #{dependencies.join(' ')}#{options}"
|
|
22
23
|
end
|
|
23
24
|
end
|
|
24
25
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module InertiaRails
|
|
4
4
|
class Configuration
|
|
5
5
|
DEFAULT_SSR_URL = 'http://localhost:13714'
|
|
6
|
+
XSRF_COOKIE_REFRESH_OPTIONS = %i[always lazy].freeze
|
|
6
7
|
|
|
7
8
|
DEFAULTS = {
|
|
8
9
|
# Whether to combine hashes with the same keys instead of replacing them.
|
|
@@ -51,6 +52,9 @@ module InertiaRails
|
|
|
51
52
|
# Whether to include empty `errors` hash to the props when no errors are present.
|
|
52
53
|
always_include_errors_hash: nil,
|
|
53
54
|
|
|
55
|
+
# When to refresh the XSRF token cookie on protected requests.
|
|
56
|
+
xsrf_cookie_refresh: :always,
|
|
57
|
+
|
|
54
58
|
# Whether to use `<script>` element for initial page rendering instead of the `data-page` attribute.
|
|
55
59
|
use_script_element_for_initial_page: false,
|
|
56
60
|
|
|
@@ -149,6 +153,17 @@ module InertiaRails
|
|
|
149
153
|
@options[:cache_store] || Rails.cache
|
|
150
154
|
end
|
|
151
155
|
|
|
156
|
+
# Normalized and validated at read time — ENV values arrive as strings, and callables are only evaluated here.
|
|
157
|
+
def xsrf_cookie_refresh
|
|
158
|
+
value = evaluate_option(options[:xsrf_cookie_refresh])
|
|
159
|
+
value = value.to_sym if value.respond_to?(:to_sym)
|
|
160
|
+
return value if XSRF_COOKIE_REFRESH_OPTIONS.include?(value)
|
|
161
|
+
|
|
162
|
+
raise ArgumentError,
|
|
163
|
+
"Invalid xsrf_cookie_refresh: #{value.inspect}. " \
|
|
164
|
+
"Expected one of: #{XSRF_COOKIE_REFRESH_OPTIONS.map(&:inspect).join(', ')}"
|
|
165
|
+
end
|
|
166
|
+
|
|
152
167
|
OPTION_NAMES.each do |option|
|
|
153
168
|
unless method_defined?(option)
|
|
154
169
|
define_method(option) do
|
|
@@ -11,8 +11,18 @@ module InertiaRails
|
|
|
11
11
|
InertiaRails::Current.request = request
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
# The same URL answers as HTML, Inertia JSON, or a partial reload; fold the
|
|
15
|
+
# representation into the conditional-GET validator so they don't share an ETag.
|
|
16
|
+
etag { inertia_conditional_get_variant }
|
|
17
|
+
|
|
14
18
|
after_action do
|
|
15
|
-
|
|
19
|
+
next unless protect_against_forgery?
|
|
20
|
+
# Included into ActionController::Base, so this runs for non-Inertia
|
|
21
|
+
# responses too — ActiveStorage images, where it only breaks CDN caching.
|
|
22
|
+
next unless request.format.html? || request.xhr?
|
|
23
|
+
next if XsrfCookieRefreshPolicy.skip?(self)
|
|
24
|
+
|
|
25
|
+
cookies['XSRF-TOKEN'] = form_authenticity_token
|
|
16
26
|
end
|
|
17
27
|
|
|
18
28
|
rescue_from InertiaRails::PrecognitionResponse do |e|
|
|
@@ -112,6 +122,13 @@ module InertiaRails
|
|
|
112
122
|
view_assigns.except(*@_inertia_skip_props)
|
|
113
123
|
end
|
|
114
124
|
|
|
125
|
+
# nil for plain requests — compacted out of the ETag, so non-Inertia ETags are unchanged.
|
|
126
|
+
def inertia_conditional_get_variant
|
|
127
|
+
return unless request.inertia?
|
|
128
|
+
|
|
129
|
+
request.env.filter_map { |key, value| "#{key}=#{value}" if key.start_with?('HTTP_X_INERTIA') }.sort
|
|
130
|
+
end
|
|
131
|
+
|
|
115
132
|
# Rails < 8: _normalize_options overwrites :layout with a resolved default,
|
|
116
133
|
# making an explicit `layout: false` indistinguishable from "not provided".
|
|
117
134
|
# Stash the original value so the renderer can tell the two apart.
|
|
@@ -3,17 +3,29 @@
|
|
|
3
3
|
module InertiaRails
|
|
4
4
|
module InertiaMapper
|
|
5
5
|
def inertia(*args, **options)
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
defaults = options.delete(:defaults) || {}
|
|
7
|
+
defaults = defaults.merge(props: options.delete(:props)) if options.key?(:props)
|
|
8
|
+
|
|
9
|
+
extract_routes(args, options).each do |route, component|
|
|
10
|
+
get(route, to: StaticController.action(:static), defaults: defaults.merge(component: component), **options)
|
|
11
|
+
end
|
|
9
12
|
end
|
|
10
13
|
|
|
11
14
|
private
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
# The first hash pair is the route; any further String-keyed pairs are
|
|
17
|
+
# additional routes. Symbol-keyed leftovers are route options (`on:`, `as:`).
|
|
18
|
+
def extract_routes(args, options)
|
|
19
|
+
return [route_with_default_component(args.first)] if args.any?
|
|
20
|
+
|
|
21
|
+
route = options.keys.first
|
|
22
|
+
routes = [[route, options.delete(route)]]
|
|
23
|
+
options.keys.grep(String).each { |extra| routes << [extra, options.delete(extra)] }
|
|
24
|
+
routes
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def route_with_default_component(path)
|
|
28
|
+
if resource_scope?
|
|
17
29
|
[path,
|
|
18
30
|
InertiaRails.configuration.component_path_resolver(
|
|
19
31
|
path: [@scope[:module], @scope[:controller]].compact.join('/'), action: path
|
|
@@ -8,8 +8,8 @@ module InertiaRails
|
|
|
8
8
|
class ControllerTemplateBase < Rails::Generators::NamedBase
|
|
9
9
|
include Helper
|
|
10
10
|
|
|
11
|
-
class_option :frontend_framework,
|
|
12
|
-
|
|
11
|
+
class_option :frontend_framework,
|
|
12
|
+
desc: 'Frontend framework to generate the views for (defaults to the one detected in package.json).'
|
|
13
13
|
|
|
14
14
|
class_option :typescript, type: :boolean, desc: 'Whether to use TypeScript',
|
|
15
15
|
default: Helper.uses_typescript?
|
|
@@ -23,13 +23,17 @@ module InertiaRails
|
|
|
23
23
|
def copy_view_files
|
|
24
24
|
actions.each do |action|
|
|
25
25
|
@action = action
|
|
26
|
-
@path = File.join(base_path, "#{action
|
|
27
|
-
template "#{
|
|
26
|
+
@path = File.join(base_path, "#{action}.#{extension}")
|
|
27
|
+
template "#{frontend_framework}/#{template_filename}.#{extension}", @path
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
private
|
|
32
32
|
|
|
33
|
+
def frontend_framework
|
|
34
|
+
options[:frontend_framework] || Helper.guess_the_default_framework
|
|
35
|
+
end
|
|
36
|
+
|
|
33
37
|
def base_path
|
|
34
38
|
File.join(pages_path, inertia_base_path)
|
|
35
39
|
end
|
|
@@ -47,12 +51,12 @@ module InertiaRails
|
|
|
47
51
|
end
|
|
48
52
|
|
|
49
53
|
def extension
|
|
50
|
-
case
|
|
54
|
+
case frontend_framework
|
|
51
55
|
when 'react' then typescript? ? 'tsx' : 'jsx'
|
|
52
56
|
when 'vue' then 'vue'
|
|
53
57
|
when 'svelte' then 'svelte'
|
|
54
58
|
else
|
|
55
|
-
raise ArgumentError, "Unknown frontend framework: #{
|
|
59
|
+
raise ArgumentError, "Unknown frontend framework: #{frontend_framework}"
|
|
56
60
|
end
|
|
57
61
|
end
|
|
58
62
|
|
|
@@ -27,6 +27,8 @@ module InertiaRails
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def guess_inertia_template(package_json_path = DEFAULT_PACKAGE_PATH)
|
|
30
|
+
return 'inertia_templates' unless package_json_path.exist?
|
|
31
|
+
|
|
30
32
|
if package_json_path.read.include?('"tailwindcss"')
|
|
31
33
|
'inertia_tw_templates'
|
|
32
34
|
else
|
|
@@ -35,8 +37,9 @@ module InertiaRails
|
|
|
35
37
|
end
|
|
36
38
|
end
|
|
37
39
|
|
|
40
|
+
# Matches controller_path, which the default component path resolver expects.
|
|
38
41
|
def inertia_base_path
|
|
39
|
-
(class_path + [file_name
|
|
42
|
+
(class_path + [file_name]).join('/')
|
|
40
43
|
end
|
|
41
44
|
|
|
42
45
|
def inertia_component_name
|
|
@@ -14,21 +14,26 @@ module InertiaRails
|
|
|
14
14
|
|
|
15
15
|
def copy_view_files
|
|
16
16
|
available_views.each do |view|
|
|
17
|
-
template "#{
|
|
17
|
+
template "#{frontend_framework}/#{view}.#{template_extension}",
|
|
18
18
|
File.join(base_path, "#{view}.#{extension}")
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
template "#{
|
|
21
|
+
template "#{frontend_framework}/#{partial_name}.#{template_extension}",
|
|
22
22
|
File.join(base_path, "#{singular_name}.#{extension}")
|
|
23
23
|
|
|
24
|
-
template "#{
|
|
24
|
+
template "#{frontend_framework}/types.ts", File.join(base_path, 'types.ts') if typescript?
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
private
|
|
28
28
|
|
|
29
|
+
# Scaffold controllers are pluralized, so the views live under the pluralized path.
|
|
30
|
+
def inertia_base_path
|
|
31
|
+
(controller_class_path + [controller_file_name]).join('/')
|
|
32
|
+
end
|
|
33
|
+
|
|
29
34
|
def template_extension
|
|
30
35
|
return extension unless typescript?
|
|
31
|
-
return 'tsx' if
|
|
36
|
+
return 'tsx' if frontend_framework == 'react'
|
|
32
37
|
|
|
33
38
|
"ts.#{extension}"
|
|
34
39
|
end
|
data/lib/inertia_rails/helper.rb
CHANGED
|
@@ -36,8 +36,14 @@ module InertiaRails
|
|
|
36
36
|
id ||= config.root_dom_id
|
|
37
37
|
|
|
38
38
|
if config.use_script_element_for_initial_page
|
|
39
|
+
script_options = { 'data-page': id, type: 'application/json' }
|
|
40
|
+
if respond_to?(:content_security_policy_nonce, true)
|
|
41
|
+
nonce = content_security_policy_nonce
|
|
42
|
+
script_options[:nonce] = nonce if nonce.present?
|
|
43
|
+
end
|
|
44
|
+
|
|
39
45
|
safe_join([
|
|
40
|
-
tag.script(page.to_json.html_safe,
|
|
46
|
+
tag.script(page.to_json.html_safe, **script_options),
|
|
41
47
|
tag.div(id: id)
|
|
42
48
|
], "\n")
|
|
43
49
|
else
|
|
@@ -51,7 +51,15 @@ module InertiaRails
|
|
|
51
51
|
inertia_request? && version_stale?
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
# Matches Rack::Response::Helpers#redirect? — Inertia session options
|
|
55
|
+
# must survive every redirect until a render consumes them.
|
|
54
56
|
def redirect_status?(status)
|
|
57
|
+
[301, 302, 303, 307, 308].include? status
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Only 301/302 are rewritten to 303: a 303 already forces a GET on
|
|
61
|
+
# follow, and 307/308 preserve the request method by design.
|
|
62
|
+
def convertible_redirect_status?(status)
|
|
55
63
|
[301, 302].include? status
|
|
56
64
|
end
|
|
57
65
|
|
|
@@ -60,7 +68,7 @@ module InertiaRails
|
|
|
60
68
|
end
|
|
61
69
|
|
|
62
70
|
def inertia_non_post_redirect?(status)
|
|
63
|
-
inertia_request? &&
|
|
71
|
+
inertia_request? && convertible_redirect_status?(status) && non_get_redirectable_method?
|
|
64
72
|
end
|
|
65
73
|
|
|
66
74
|
def stale_inertia_get?
|
|
@@ -24,6 +24,7 @@ module InertiaRails
|
|
|
24
24
|
@_match_on = []
|
|
25
25
|
@_once = {}
|
|
26
26
|
@_scroll = {}
|
|
27
|
+
@_rescued = []
|
|
27
28
|
|
|
28
29
|
props = expand_dot_notation(@props)
|
|
29
30
|
resolved = deep_transform_props(props)
|
|
@@ -71,6 +72,7 @@ module InertiaRails
|
|
|
71
72
|
metadata[:deepMergeProps] = @_deep_merge unless @_deep_merge.empty?
|
|
72
73
|
metadata[:matchPropsOn] = @_match_on unless @_match_on.empty?
|
|
73
74
|
metadata[:onceProps] = @_once unless @_once.empty?
|
|
75
|
+
metadata[:rescuedProps] = @_rescued unless @_rescued.empty?
|
|
74
76
|
|
|
75
77
|
metadata
|
|
76
78
|
end
|
|
@@ -99,29 +101,39 @@ module InertiaRails
|
|
|
99
101
|
collect_metadata(prop, path)
|
|
100
102
|
next unless keep_prop?(prop, path, parent_was_resolved: parent_was_resolved)
|
|
101
103
|
|
|
102
|
-
|
|
104
|
+
rescue_enabled = prop.try(:rescue?)
|
|
103
105
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
collect_metadata(value, path)
|
|
107
|
-
next unless keep_prop?(value, path, parent_was_resolved: parent_was_resolved)
|
|
106
|
+
begin
|
|
107
|
+
value = @evaluator.call(prop)
|
|
108
108
|
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
# A closure may return a prop type — unwrap one level
|
|
110
|
+
if value.is_a?(BaseProp) && !prop.is_a?(BaseProp)
|
|
111
|
+
collect_metadata(value, path)
|
|
112
|
+
next unless keep_prop?(value, path, parent_was_resolved: parent_was_resolved)
|
|
111
113
|
|
|
112
|
-
|
|
113
|
-
if prop.is_a?(Proc)
|
|
114
|
-
if value.is_a?(Hash) && value.any?
|
|
115
|
-
nested = deep_transform_props(value, path, parent_was_resolved: true)
|
|
116
|
-
transformed_props[key] = nested unless nested.empty?
|
|
117
|
-
next
|
|
118
|
-
elsif value.is_a?(Array)
|
|
119
|
-
transformed_props[key] = transform_array(value, path, parent_was_resolved: true)
|
|
120
|
-
next
|
|
114
|
+
value = @evaluator.call(value)
|
|
121
115
|
end
|
|
122
|
-
end
|
|
123
116
|
|
|
124
|
-
|
|
117
|
+
# A closure may return a Hash or Array containing prop types — recurse into it
|
|
118
|
+
if prop.is_a?(Proc)
|
|
119
|
+
if value.is_a?(Hash) && value.any?
|
|
120
|
+
nested = deep_transform_props(value, path, parent_was_resolved: true)
|
|
121
|
+
transformed_props[key] = nested unless nested.empty?
|
|
122
|
+
next
|
|
123
|
+
elsif value.is_a?(Array)
|
|
124
|
+
transformed_props[key] = transform_array(value, path, parent_was_resolved: true)
|
|
125
|
+
next
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
transformed_props[key] = rescue_enabled ? value.as_json : value
|
|
130
|
+
rescue StandardError => e
|
|
131
|
+
raise unless rescue_enabled
|
|
132
|
+
|
|
133
|
+
report_rescued_error(e)
|
|
134
|
+
@_rescued << path
|
|
135
|
+
next
|
|
136
|
+
end
|
|
125
137
|
end
|
|
126
138
|
end
|
|
127
139
|
|
|
@@ -147,6 +159,16 @@ module InertiaRails
|
|
|
147
159
|
end
|
|
148
160
|
end
|
|
149
161
|
|
|
162
|
+
def report_rescued_error(error)
|
|
163
|
+
# `Rails.error` (the Error Reporter) was introduced in Rails 7.0. Fall back
|
|
164
|
+
# to the logger on older versions so rescued errors are never silently lost.
|
|
165
|
+
if Rails.respond_to?(:error)
|
|
166
|
+
Rails.error.report(error, handled: true)
|
|
167
|
+
else
|
|
168
|
+
Rails.logger&.error("[inertia-rails] Rescued deferred prop error: #{error.class}: #{error.message}")
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
150
172
|
def collect_metadata(prop, path)
|
|
151
173
|
return unless prop.is_a?(BaseProp)
|
|
152
174
|
|
|
@@ -44,11 +44,9 @@ module InertiaRails
|
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
def render
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"#{@response.headers['Vary']}, X-Inertia"
|
|
51
|
-
end
|
|
47
|
+
vary = @response.headers['Vary'].to_s.split(',').map(&:strip).reject(&:empty?)
|
|
48
|
+
vary << 'X-Inertia' if vary.none? { |value| value.casecmp?('X-Inertia') }
|
|
49
|
+
@response.headers['Vary'] = vary.join(', ')
|
|
52
50
|
if @request.inertia?
|
|
53
51
|
@response.set_header('X-Inertia', 'true')
|
|
54
52
|
@render_method.call json: page.to_json, status: @response.status, content_type: Mime[:json]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module InertiaRails
|
|
4
|
+
# Decides whether the XSRF-TOKEN cookie rewrite can be skipped under the `:lazy` refresh policy.
|
|
5
|
+
class XsrfCookieRefreshPolicy
|
|
6
|
+
def self.skip?(controller)
|
|
7
|
+
new(controller).skip?
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(controller)
|
|
11
|
+
@controller = controller
|
|
12
|
+
@request = controller.request
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def skip?
|
|
16
|
+
return false unless configuration.xsrf_cookie_refresh == :lazy
|
|
17
|
+
return false unless @request.get? || @request.head?
|
|
18
|
+
|
|
19
|
+
cookie = @request.cookies['XSRF-TOKEN']
|
|
20
|
+
return false if cookie.blank?
|
|
21
|
+
|
|
22
|
+
return true unless can_validate_without_loading_session?
|
|
23
|
+
|
|
24
|
+
valid_for_session?(cookie)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def configuration
|
|
30
|
+
@controller.send(:inertia_configuration)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def can_validate_without_loading_session?
|
|
34
|
+
csrf_token_loaded_in_env? ||
|
|
35
|
+
(@request.session.respond_to?(:loaded?) && @request.session.loaded?)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def valid_for_session?(cookie)
|
|
39
|
+
csrf_token_was_loaded = csrf_token_loaded_in_env?
|
|
40
|
+
@controller.send(:valid_authenticity_token?, @request.session, cookie)
|
|
41
|
+
ensure
|
|
42
|
+
# `valid_authenticity_token?` memoizes the real token into
|
|
43
|
+
# `request.env[CSRF_TOKEN]` (Rails 7.1+), and the session middleware later
|
|
44
|
+
# persists whatever sits there via `commit_csrf_token`. Drop the key when
|
|
45
|
+
# validation itself created it, so a validation-only read can't dirty the
|
|
46
|
+
# session and emit a session Set-Cookie. No-op on Rails < 7.1, where
|
|
47
|
+
# there is no env key to clean up.
|
|
48
|
+
@request.env.delete(csrf_token_env_key) if csrf_token_env_key && !csrf_token_was_loaded
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def csrf_token_loaded_in_env?
|
|
52
|
+
csrf_token_env_key && @request.env.key?(csrf_token_env_key)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def csrf_token_env_key
|
|
56
|
+
return @csrf_token_env_key if defined?(@csrf_token_env_key)
|
|
57
|
+
|
|
58
|
+
@csrf_token_env_key =
|
|
59
|
+
if ActionController::RequestForgeryProtection.const_defined?(:CSRF_TOKEN, false)
|
|
60
|
+
ActionController::RequestForgeryProtection.const_get(:CSRF_TOKEN)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
data/lib/inertia_rails.rb
CHANGED
|
@@ -43,6 +43,7 @@ require_relative 'inertia_rails/renderer'
|
|
|
43
43
|
require_relative 'inertia_rails/flash_extension'
|
|
44
44
|
require_relative 'inertia_rails/helper'
|
|
45
45
|
require_relative 'inertia_rails/precognition'
|
|
46
|
+
require_relative 'inertia_rails/xsrf_cookie_refresh_policy'
|
|
46
47
|
require_relative 'inertia_rails/controller'
|
|
47
48
|
require_relative 'inertia_rails/middleware'
|
|
48
49
|
require_relative 'inertia_rails/engine'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: inertia_rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.22.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brian Knoles
|
|
@@ -225,6 +225,7 @@ files:
|
|
|
225
225
|
- lib/inertia_rails/ssr_renderer.rb
|
|
226
226
|
- lib/inertia_rails/testing.rb
|
|
227
227
|
- lib/inertia_rails/version.rb
|
|
228
|
+
- lib/inertia_rails/xsrf_cookie_refresh_policy.rb
|
|
228
229
|
- lib/puma/plugin/inertia_ssr.rb
|
|
229
230
|
- lib/tasks/inertia_rails.rake
|
|
230
231
|
homepage: https://github.com/inertiajs/inertia-rails
|