inertia_rails 3.17.0 → 3.18.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 +9 -0
- data/lib/generators/inertia/install/templates/react/inertia.jsx +1 -0
- data/lib/generators/inertia/install/templates/react/inertia.tsx +1 -0
- data/lib/generators/inertia/install/templates/svelte/inertia.ts +1 -0
- data/lib/generators/inertia/install/templates/vue/inertia.ts +1 -0
- data/lib/inertia_rails/configuration.rb +5 -0
- data/lib/inertia_rails/controller.rb +32 -0
- data/lib/inertia_rails/current.rb +7 -0
- data/lib/inertia_rails/defer_prop.rb +4 -0
- data/lib/inertia_rails/errors.rb +11 -0
- data/lib/inertia_rails/middleware.rb +18 -2
- data/lib/inertia_rails/precognition.rb +68 -0
- data/lib/inertia_rails/renderer.rb +10 -9
- data/lib/inertia_rails/scroll_prop.rb +8 -0
- data/lib/inertia_rails/testing.rb +17 -3
- data/lib/inertia_rails/version.rb +1 -1
- data/lib/patches/request.rb +8 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9e041109440a74fee87c6aa2ec7a338db68e01e1c5160dee3e9e00364a55f504
|
|
4
|
+
data.tar.gz: e4e8fc4f115e2f7d84f8f39bfe90099ca5b17a2490cd97aa6c21282b112ef15c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f399be828b7f2e01c5cb9dedb86d67d3a0fe278370846f2c70a37576246e96d8ea00395239152bd908aad238b9389e06c5349f6bbb51b68b1b131367fe51b317
|
|
7
|
+
data.tar.gz: 833c82fff42dce56d47e20dbfb32e9103a76dd889b921902f73f9f020369409bfcd81f8ddfecabe122b95983034d0c3eb2e24cf5a0e46e018a78b2aeb94f7b1e
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [3.18.0] - 2026-03-11
|
|
8
|
+
|
|
9
|
+
* New (super awesome) landing page design (seriously, go look at the animated graphic) (@skryukov)
|
|
10
|
+
* Convert `_inertia_meta` to Hash in props for testing (@greendrop)
|
|
11
|
+
* Make InertaRails.scroll props deferable (@skyrukov)
|
|
12
|
+
* Add `withAllErrors` as default (@skyrukov)
|
|
13
|
+
* Improved testing with `evaluate_optional_props` (@skyrukov)
|
|
14
|
+
* Add Railsified Precognition support (@skyrukov)
|
|
15
|
+
|
|
7
16
|
## [3.17.0] - 2026-02-04
|
|
8
17
|
|
|
9
18
|
* Add support for use_data_inertia_head_attribute configuration option (@greendrop)
|
|
@@ -47,6 +47,11 @@ module InertiaRails
|
|
|
47
47
|
# Flash keys from Rails flash to expose to frontend.
|
|
48
48
|
# Set to nil to disable Rails flash integration (use only flash.inertia).
|
|
49
49
|
flash_keys: %i[notice alert].freeze,
|
|
50
|
+
|
|
51
|
+
# Whether to prevent database writes during precognition requests.
|
|
52
|
+
# When enabled, any ActiveRecord write during a precognition request
|
|
53
|
+
# will raise ActiveRecord::ReadOnlyError.
|
|
54
|
+
precognition_prevent_writes: false,
|
|
50
55
|
}.freeze
|
|
51
56
|
|
|
52
57
|
OPTION_NAMES = DEFAULTS.keys.freeze
|
|
@@ -5,6 +5,7 @@ require_relative 'flash_extension'
|
|
|
5
5
|
require_relative 'helper'
|
|
6
6
|
require_relative 'action_filter'
|
|
7
7
|
require_relative 'meta_tag_builder'
|
|
8
|
+
require_relative 'precognition'
|
|
8
9
|
|
|
9
10
|
module InertiaRails
|
|
10
11
|
module Controller
|
|
@@ -13,9 +14,17 @@ module InertiaRails
|
|
|
13
14
|
included do
|
|
14
15
|
helper ::InertiaRails::Helper
|
|
15
16
|
|
|
17
|
+
before_action do
|
|
18
|
+
InertiaRails::Current.request = request
|
|
19
|
+
end
|
|
20
|
+
|
|
16
21
|
after_action do
|
|
17
22
|
cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
|
|
18
23
|
end
|
|
24
|
+
|
|
25
|
+
rescue_from InertiaRails::PrecognitionResponse do |e|
|
|
26
|
+
render_precognition(e.errors)
|
|
27
|
+
end
|
|
19
28
|
end
|
|
20
29
|
|
|
21
30
|
module ClassMethods
|
|
@@ -139,6 +148,29 @@ module InertiaRails
|
|
|
139
148
|
|
|
140
149
|
private
|
|
141
150
|
|
|
151
|
+
def precognition!(model_or_errors)
|
|
152
|
+
InertiaRails.precognition!(model_or_errors)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def precognition(model_or_errors)
|
|
156
|
+
errors = InertiaRails::Precognition.validate(model_or_errors)
|
|
157
|
+
return if errors.nil?
|
|
158
|
+
|
|
159
|
+
render_precognition(errors)
|
|
160
|
+
true
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def render_precognition(errors)
|
|
164
|
+
response.headers['Precognition'] = 'true'
|
|
165
|
+
|
|
166
|
+
if errors.empty?
|
|
167
|
+
response.headers['Precognition-Success'] = 'true'
|
|
168
|
+
head :no_content
|
|
169
|
+
else
|
|
170
|
+
render json: { errors: errors }, status: :unprocessable_entity
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
142
174
|
def inertia_view_assigns
|
|
143
175
|
return {} unless @_inertia_instance_props
|
|
144
176
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module InertiaRails
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class DoublePrecognitionError < StandardError
|
|
7
|
+
def initialize
|
|
8
|
+
super('You can only call precognition once per action, use a form object to validate multiple models.')
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -18,7 +18,11 @@ module InertiaRails
|
|
|
18
18
|
|
|
19
19
|
def response
|
|
20
20
|
copy_xsrf_to_csrf!
|
|
21
|
-
status, headers, body =
|
|
21
|
+
status, headers, body = if prevent_precognition_writes?
|
|
22
|
+
ActiveRecord::Base.while_preventing_writes { @app.call(@env) }
|
|
23
|
+
else
|
|
24
|
+
@app.call(@env)
|
|
25
|
+
end
|
|
22
26
|
request = ActionDispatch::Request.new(@env)
|
|
23
27
|
|
|
24
28
|
# Inertia session data is added via redirect_to
|
|
@@ -96,7 +100,19 @@ module InertiaRails
|
|
|
96
100
|
end
|
|
97
101
|
|
|
98
102
|
def copy_xsrf_to_csrf!
|
|
99
|
-
|
|
103
|
+
return unless @env['HTTP_X_XSRF_TOKEN'] && (inertia_request? || precognition_request?)
|
|
104
|
+
|
|
105
|
+
@env['HTTP_X_CSRF_TOKEN'] = @env['HTTP_X_XSRF_TOKEN']
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def precognition_request?
|
|
109
|
+
@env['HTTP_PRECOGNITION'] == 'true'
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def prevent_precognition_writes?
|
|
113
|
+
precognition_request? &&
|
|
114
|
+
InertiaRails.configuration.precognition_prevent_writes &&
|
|
115
|
+
defined?(ActiveRecord::Base)
|
|
100
116
|
end
|
|
101
117
|
end
|
|
102
118
|
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'current'
|
|
4
|
+
require_relative 'errors'
|
|
5
|
+
|
|
6
|
+
module InertiaRails
|
|
7
|
+
class PrecognitionResponse < StandardError
|
|
8
|
+
attr_reader :errors
|
|
9
|
+
|
|
10
|
+
def initialize(errors)
|
|
11
|
+
@errors = errors
|
|
12
|
+
super('Precognition response')
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
module Precognition
|
|
17
|
+
class << self
|
|
18
|
+
# Returns filtered errors hash if precognition request, nil otherwise
|
|
19
|
+
def validate(model_or_errors)
|
|
20
|
+
# Check before the precognitive? guard to catch errors early
|
|
21
|
+
# without waiting for precognition requests.
|
|
22
|
+
ensure_single_precognition_call!
|
|
23
|
+
request = Current.request
|
|
24
|
+
return unless request&.inertia_precognitive?
|
|
25
|
+
|
|
26
|
+
errors = normalize_errors(model_or_errors)
|
|
27
|
+
filter_errors(errors, request)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def normalize_errors(errors)
|
|
33
|
+
return errors if errors.is_a?(Hash)
|
|
34
|
+
|
|
35
|
+
if errors.respond_to?(:valid?) && errors.respond_to?(:errors)
|
|
36
|
+
errors.valid?
|
|
37
|
+
return errors.errors.to_hash
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
return errors.to_hash if errors.respond_to?(:to_hash)
|
|
41
|
+
return errors.to_h if errors.respond_to?(:to_h)
|
|
42
|
+
|
|
43
|
+
raise ArgumentError,
|
|
44
|
+
"Expected a Hash or an object responding to :valid? and :errors, :to_hash, or :to_h, got #{errors.class}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def filter_errors(errors, request)
|
|
48
|
+
only_keys = request.inertia_precognitive_validate_only
|
|
49
|
+
return errors unless only_keys&.any?
|
|
50
|
+
|
|
51
|
+
errors.slice(*only_keys, *only_keys.map(&:to_sym))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def ensure_single_precognition_call!
|
|
55
|
+
raise DoublePrecognitionError if Current.precognition_called
|
|
56
|
+
|
|
57
|
+
Current.precognition_called = true
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.precognition!(model_or_errors)
|
|
63
|
+
errors = Precognition.validate(model_or_errors)
|
|
64
|
+
return false if errors.nil?
|
|
65
|
+
|
|
66
|
+
raise PrecognitionResponse, errors, []
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -98,17 +98,17 @@ module InertiaRails
|
|
|
98
98
|
# rubocop:disable Style/MultilineBlockChain
|
|
99
99
|
@props
|
|
100
100
|
.tap do |merged_props|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
# Always keep errors in the props
|
|
102
|
+
if merged_props.key?(:errors) && !merged_props[:errors].is_a?(BaseProp)
|
|
103
|
+
errors = merged_props[:errors]
|
|
104
|
+
merged_props[:errors] = InertiaRails.always { errors }
|
|
105
|
+
end
|
|
106
106
|
end
|
|
107
107
|
.then { |props| deep_transform_props(props) } # Internal hydration/filtering
|
|
108
108
|
.then { |props| @configuration.prop_transformer(props: props) } # Apply user-defined prop transformer
|
|
109
109
|
.tap do |props|
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
# Add meta tags last (never transformed)
|
|
111
|
+
props[:_inertia_meta] = meta_tags if meta_tags.present?
|
|
112
112
|
end
|
|
113
113
|
# rubocop:enable Style/MultilineBlockChain
|
|
114
114
|
end
|
|
@@ -164,7 +164,7 @@ module InertiaRails
|
|
|
164
164
|
return if rendering_partial_component?
|
|
165
165
|
|
|
166
166
|
@props.each_with_object({}) do |(key, prop), result|
|
|
167
|
-
(result[prop.group] ||= []) << key if prop.
|
|
167
|
+
(result[prop.group] ||= []) << key if prop.try(:deferred?)
|
|
168
168
|
end
|
|
169
169
|
end
|
|
170
170
|
|
|
@@ -248,6 +248,7 @@ module InertiaRails
|
|
|
248
248
|
@scroll_props = {}
|
|
249
249
|
requested_merge_props.each do |key, prop|
|
|
250
250
|
next unless prop.is_a?(ScrollProp)
|
|
251
|
+
next if prop.deferred? && !rendering_partial_component?
|
|
251
252
|
|
|
252
253
|
@scroll_props[key] = prop.metadata.merge!(reset: reset_keys.include?(key))
|
|
253
254
|
end
|
|
@@ -292,7 +293,7 @@ module InertiaRails
|
|
|
292
293
|
return false if excluded_by_partial_request?(path)
|
|
293
294
|
|
|
294
295
|
# Precedence: Evaluate IgnoreOnFirstLoadProp only after partial keys have been checked
|
|
295
|
-
return false if prop.is_a?(IgnoreOnFirstLoadProp) && !rendering_partial_component?
|
|
296
|
+
return false if (prop.is_a?(IgnoreOnFirstLoadProp) || prop.try(:deferred?)) && !rendering_partial_component?
|
|
296
297
|
|
|
297
298
|
true
|
|
298
299
|
end
|
|
@@ -6,16 +6,24 @@ module InertiaRails
|
|
|
6
6
|
class ScrollProp < BaseProp
|
|
7
7
|
prepend PropMergeable
|
|
8
8
|
|
|
9
|
+
attr_reader :group
|
|
10
|
+
|
|
9
11
|
def initialize(**options, &block)
|
|
10
12
|
super(&block)
|
|
11
13
|
|
|
12
14
|
@merge = true
|
|
15
|
+
@deferred = options.delete(:defer) || false
|
|
16
|
+
@group = options.delete(:group) || DeferProp::DEFAULT_GROUP
|
|
13
17
|
@metadata = options.delete(:metadata)
|
|
14
18
|
@wrapper = options.delete(:wrapper)
|
|
15
19
|
|
|
16
20
|
@options = options
|
|
17
21
|
end
|
|
18
22
|
|
|
23
|
+
def deferred?
|
|
24
|
+
@deferred
|
|
25
|
+
end
|
|
26
|
+
|
|
19
27
|
def call(controller)
|
|
20
28
|
@value = super
|
|
21
29
|
configure_merge_intent(controller.request.headers['X-Inertia-Infinite-Scroll-Merge-Intent'])
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module InertiaRails
|
|
4
4
|
module Testing
|
|
5
5
|
thread_mattr_accessor :current_response
|
|
6
|
+
mattr_accessor :evaluate_optional_props, default: false
|
|
6
7
|
|
|
7
8
|
module RendererTestingPatch
|
|
8
9
|
def new(component, controller, request, response, render, **options)
|
|
@@ -12,10 +13,23 @@ module InertiaRails
|
|
|
12
13
|
end
|
|
13
14
|
end
|
|
14
15
|
|
|
16
|
+
module RendererOptionalInTests
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def keep_prop?(prop, path)
|
|
20
|
+
return true if InertiaRails::Testing.evaluate_optional_props &&
|
|
21
|
+
(prop.is_a?(IgnoreOnFirstLoadProp) || prop.try(:deferred?)) &&
|
|
22
|
+
!rendering_partial_component?
|
|
23
|
+
|
|
24
|
+
super
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
15
28
|
def self.install!
|
|
16
29
|
return if @installed
|
|
17
30
|
|
|
18
31
|
InertiaRails::Renderer.singleton_class.prepend(RendererTestingPatch)
|
|
32
|
+
InertiaRails::Renderer.prepend(RendererOptionalInTests)
|
|
19
33
|
@installed = true
|
|
20
34
|
end
|
|
21
35
|
|
|
@@ -37,7 +51,7 @@ module InertiaRails
|
|
|
37
51
|
def assign_locals(params)
|
|
38
52
|
if params[:locals].present?
|
|
39
53
|
@view_data = params[:locals].except(:page).with_indifferent_access
|
|
40
|
-
page = params[:locals][:page] || {}
|
|
54
|
+
page = JSON.parse((params[:locals][:page] || {}).to_json)
|
|
41
55
|
else
|
|
42
56
|
# Sequential Inertia request
|
|
43
57
|
@view_data = {}
|
|
@@ -189,7 +203,7 @@ module InertiaRails
|
|
|
189
203
|
def inertia_reload_only(*props)
|
|
190
204
|
partial_headers = {
|
|
191
205
|
'X-Inertia' => 'true',
|
|
192
|
-
'X-Inertia-Partial-Data' => props.
|
|
206
|
+
'X-Inertia-Partial-Data' => props.join(','),
|
|
193
207
|
'X-Inertia-Partial-Component' => inertia.component,
|
|
194
208
|
}
|
|
195
209
|
get request.fullpath, headers: partial_headers
|
|
@@ -198,7 +212,7 @@ module InertiaRails
|
|
|
198
212
|
def inertia_reload_except(*props)
|
|
199
213
|
partial_headers = {
|
|
200
214
|
'X-Inertia' => 'true',
|
|
201
|
-
'X-Inertia-Partial-Except' => props.
|
|
215
|
+
'X-Inertia-Partial-Except' => props.join(','),
|
|
202
216
|
'X-Inertia-Partial-Component' => inertia.component,
|
|
203
217
|
}
|
|
204
218
|
get request.fullpath, headers: partial_headers
|
data/lib/patches/request.rb
CHANGED
|
@@ -9,6 +9,14 @@ module InertiaRails
|
|
|
9
9
|
def inertia_partial?
|
|
10
10
|
key?('HTTP_X_INERTIA_PARTIAL_COMPONENT')
|
|
11
11
|
end
|
|
12
|
+
|
|
13
|
+
def inertia_precognitive?
|
|
14
|
+
headers['Precognition'] == 'true'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def inertia_precognitive_validate_only
|
|
18
|
+
headers['Precognition-Validate-Only']&.split(',')&.map(&:strip)
|
|
19
|
+
end
|
|
12
20
|
end
|
|
13
21
|
end
|
|
14
22
|
|
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.18.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brian Knoles
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
- Eugene Granovsky
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2026-
|
|
12
|
+
date: 2026-03-11 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: railties
|
|
@@ -187,8 +187,10 @@ files:
|
|
|
187
187
|
- lib/inertia_rails/base_prop.rb
|
|
188
188
|
- lib/inertia_rails/configuration.rb
|
|
189
189
|
- lib/inertia_rails/controller.rb
|
|
190
|
+
- lib/inertia_rails/current.rb
|
|
190
191
|
- lib/inertia_rails/defer_prop.rb
|
|
191
192
|
- lib/inertia_rails/engine.rb
|
|
193
|
+
- lib/inertia_rails/errors.rb
|
|
192
194
|
- lib/inertia_rails/flash_extension.rb
|
|
193
195
|
- lib/inertia_rails/generators/controller_template_base.rb
|
|
194
196
|
- lib/inertia_rails/generators/helper.rb
|
|
@@ -204,6 +206,7 @@ files:
|
|
|
204
206
|
- lib/inertia_rails/minitest.rb
|
|
205
207
|
- lib/inertia_rails/once_prop.rb
|
|
206
208
|
- lib/inertia_rails/optional_prop.rb
|
|
209
|
+
- lib/inertia_rails/precognition.rb
|
|
207
210
|
- lib/inertia_rails/prop_mergeable.rb
|
|
208
211
|
- lib/inertia_rails/prop_onceable.rb
|
|
209
212
|
- lib/inertia_rails/renderer.rb
|