inertia_rails 3.11.0 → 3.12.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 416af24e523dd32daacb768be83fc88bf5465a87adbf49e8105cb630409e45f9
4
- data.tar.gz: 4c1d7c883d6ffc3e3239777bd1b1f400f5a0f69c22093a307225d696551b2da5
3
+ metadata.gz: a3bde9e557a4d5b76450d4f019ae8461b40a0eb2fda2984d8d5cb32fa58d769c
4
+ data.tar.gz: d662136e29e4f72957a6677a74b1c6c29086e8e32056023e5b51fb4b5e70b976
5
5
  SHA512:
6
- metadata.gz: 23f7eeab79b5e54cc12408f6196f946f5cb5ea78086d55ac56d06827ea0243ba96c90e432e7c69583f7531ae7cd2611a026fdce15039219c36f7ef9034888fa6
7
- data.tar.gz: c29f0e2d3c88ebc7a4cc6a405219152a3ae5f4428b23b8ec9a82de8e38e98a52c035d15389c51858b1a6534aae47adef1a26ebd179c4892c7a7b0b76427b88b0
6
+ metadata.gz: 86753985c32f435b2967144c8b88e22984d1999fd0e528ac862d9b2dcc081545d52841d54b984d8184cb7774681576aa03ce244b09016a79b0110067b1d69d7e
7
+ data.tar.gz: 22197c8fc07a0f2d25402f04ab9988b3a572bd7c713ef8a391bf95bdb680bfac5a03c65c19796671d7172f921d1fdc900cc9f4f3d41420702e556a1a820a3942
data/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@ 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.12.1] - 2025-11-09
8
+
9
+ * Fix scroll props and deferred props for shared data (@bknoles)
10
+ * Deprecate the probably-no-actually-used-anywhere public readers on InertiaRails::Renderer (@bknoles)
11
+
12
+ ## [3.12.0] - 2025-11-08
13
+
14
+ * Docs updates (@leenyburger, @skryukov, @bn-l)
15
+ * Reimplement devcontainers (@kieraneglin)
16
+ * Support for Inertia.js infinite scroll components (@skyrukov)
17
+ * New merge options (@skryukov)
18
+
7
19
  ## [3.11.0] - 2025-08-29
8
20
 
9
21
  * Fix Svelte generator (@skryukov)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module InertiaRails
2
4
  class StaticController < InertiaRails.configuration.parent_controller.constantize
3
5
  def static
@@ -128,8 +128,12 @@ module Inertia
128
128
  add_dependencies(*FRAMEWORKS[framework]['packages_ts'])
129
129
 
130
130
  say 'Copying adding scripts to package.json'
131
- run 'npm pkg set scripts.check="svelte-check --tsconfig ./tsconfig.json && tsc -p tsconfig.node.json"' if svelte?
132
- run 'npm pkg set scripts.check="vue-tsc -p tsconfig.app.json && tsc -p tsconfig.node.json"' if framework == 'vue'
131
+ if svelte?
132
+ run 'npm pkg set scripts.check="svelte-check --tsconfig ./tsconfig.json && tsc -p tsconfig.node.json"'
133
+ end
134
+ if framework == 'vue'
135
+ run 'npm pkg set scripts.check="vue-tsc -p tsconfig.app.json && tsc -p tsconfig.node.json"'
136
+ end
133
137
  run 'npm pkg set scripts.check="tsc -p tsconfig.app.json && tsc -p tsconfig.node.json"' if framework == 'react'
134
138
  end
135
139
 
@@ -262,8 +266,9 @@ module Inertia
262
266
  end
263
267
 
264
268
  def inertia_resolved_version
269
+ package = "@inertiajs/core@#{options[:inertia_version]}"
265
270
  @inertia_resolved_version ||= Gem::Version.new(
266
- `npm show @inertiajs/core@#{options[:inertia_version]} version --json | tail -n2 | head -n1 | tr -d '", '`.strip
271
+ `npm show #{package} version --json | tail -n2 | head -n1 | tr -d '", '`.strip
267
272
  )
268
273
  end
269
274
 
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  #
3
4
  # Based on AbstractController::Callbacks::ActionFilter
4
5
  # https://github.com/rails/rails/blob/v7.2.0/actionpack/lib/abstract_controller/callbacks.rb#L39
@@ -6,7 +7,7 @@ module InertiaRails
6
7
  class ActionFilter
7
8
  def initialize(conditional_key, actions)
8
9
  @conditional_key = conditional_key
9
- @actions = Array(actions).map(&:to_s).to_set
10
+ @actions = Array(actions).to_set(&:to_s)
10
11
  end
11
12
 
12
13
  def match?(controller)
@@ -1,7 +1,9 @@
1
- require_relative "inertia_rails"
2
- require_relative "helper"
3
- require_relative "action_filter"
4
- require_relative "meta_tag_builder"
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'inertia_rails'
4
+ require_relative 'helper'
5
+ require_relative 'action_filter'
6
+ require_relative 'meta_tag_builder'
5
7
 
6
8
  module InertiaRails
7
9
  module Controller
@@ -21,8 +23,8 @@ module InertiaRails
21
23
  return push_to_inertia_share(**(hash || props), &block) if options.empty?
22
24
 
23
25
  push_to_inertia_share do
24
- next unless options[:if].all? { |filter| instance_exec(&filter) } if options[:if]
25
- next unless options[:unless].none? { |filter| instance_exec(&filter) } if options[:unless]
26
+ next if options[:if] && !options[:if].all? { |filter| instance_exec(&filter) }
27
+ next if options[:unless]&.any? { |filter| instance_exec(&filter) }
26
28
 
27
29
  next hash unless block
28
30
 
@@ -81,7 +83,9 @@ module InertiaRails
81
83
  return options if options.empty?
82
84
 
83
85
  if props.except(:if, :unless, :only, :except).any?
84
- raise ArgumentError, "You must not mix shared data and [:if, :unless, :only, :except] options, pass data as a hash or a block."
86
+ raise ArgumentError,
87
+ 'You must not mix shared data and [:if, :unless, :only, :except] options, ' \
88
+ 'pass data as a hash or a block.'
85
89
  end
86
90
 
87
91
  transform_inertia_share_option(options, :only, :if)
@@ -110,7 +114,7 @@ module InertiaRails
110
114
  when InertiaRails::ActionFilter
111
115
  -> { filter.match?(self) }
112
116
  else
113
- raise ArgumentError, "You must pass a symbol or a proc as a filter."
117
+ raise ArgumentError, 'You must pass a symbol or a proc as a filter.'
114
118
  end
115
119
  end
116
120
  end
@@ -136,6 +140,7 @@ module InertiaRails
136
140
 
137
141
  def inertia_view_assigns
138
142
  return {} unless @_inertia_instance_props
143
+
139
144
  view_assigns.except(*@_inertia_skip_props)
140
145
  end
141
146
 
@@ -152,22 +157,22 @@ module InertiaRails
152
157
  else
153
158
  if inertia_configuration.always_include_errors_hash.nil?
154
159
  InertiaRails.deprecator.warn(
155
- "To comply with the Inertia protocol, an empty errors hash `{errors: {}}` " \
156
- "will be included to all responses by default starting with InertiaRails 4.0. " \
157
- "To opt-in now, set `config.always_include_errors_hash = true`. " \
158
- "To disable this warning, set it to `false`."
160
+ 'To comply with the Inertia protocol, an empty errors hash `{errors: {}}` ' \
161
+ 'will be included to all responses by default starting with InertiaRails 4.0. ' \
162
+ 'To opt-in now, set `config.always_include_errors_hash = true`. ' \
163
+ 'To disable this warning, set it to `false`.'
159
164
  )
160
165
  end
161
166
  {}
162
167
  end
163
168
 
164
- self.class._inertia_shared_data.filter_map { |shared_data|
169
+ self.class._inertia_shared_data.filter_map do |shared_data|
165
170
  if shared_data.respond_to?(:call)
166
171
  instance_exec(&shared_data)
167
172
  else
168
173
  shared_data
169
174
  end
170
- }.reduce(initial_data, &:merge)
175
+ end.reduce(initial_data, &:merge)
171
176
  end
172
177
 
173
178
  def inertia_location(url)
@@ -183,7 +188,7 @@ module InertiaRails
183
188
  session[:inertia_errors] = inertia_errors.to_hash
184
189
  else
185
190
  InertiaRails.deprecator.warn(
186
- "Object passed to `inertia: { errors: ... }` must respond to `to_hash`. Pass a hash-like object instead."
191
+ 'Object passed to `inertia: { errors: ... }` must respond to `to_hash`. Pass a hash-like object instead.'
187
192
  )
188
193
  session[:inertia_errors] = inertia_errors
189
194
  end
@@ -2,27 +2,16 @@
2
2
 
3
3
  module InertiaRails
4
4
  class DeferProp < IgnoreOnFirstLoadProp
5
- DEFAULT_GROUP = 'default'
5
+ prepend PropMergeable
6
6
 
7
- attr_reader :group, :match_on
7
+ DEFAULT_GROUP = 'default'
8
8
 
9
- def initialize(group: nil, merge: nil, deep_merge: nil, match_on: nil, &block)
10
- raise ArgumentError, 'Cannot set both `deep_merge` and `merge` to true' if deep_merge && merge
9
+ attr_reader :group
11
10
 
11
+ def initialize(**props, &block)
12
12
  super(&block)
13
13
 
14
- @group = group || DEFAULT_GROUP
15
- @merge = merge || deep_merge
16
- @deep_merge = deep_merge
17
- @match_on = match_on.nil? ? nil : Array(match_on)
18
- end
19
-
20
- def merge?
21
- @merge
22
- end
23
-
24
- def deep_merge?
25
- @deep_merge
14
+ @group = props[:group] || DEFAULT_GROUP
26
15
  end
27
16
  end
28
17
  end
@@ -1,13 +1,15 @@
1
- require_relative "middleware"
2
- require_relative "controller"
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'middleware'
4
+ require_relative 'controller'
3
5
 
4
6
  module InertiaRails
5
7
  class Engine < ::Rails::Engine
6
- initializer "inertia_rails.configure_rails_initialization" do |app|
8
+ initializer 'inertia_rails.configure_rails_initialization' do |app|
7
9
  app.middleware.use ::InertiaRails::Middleware
8
10
  end
9
11
 
10
- initializer "inertia_rails.action_controller" do
12
+ initializer 'inertia_rails.action_controller' do
11
13
  ActiveSupport.on_load(:action_controller_base) do
12
14
  include ::InertiaRails::Controller
13
15
  end
@@ -1,32 +1,36 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'inertia_rails'
2
4
 
3
- module InertiaRails::Helper
4
- def inertia_ssr_head
5
- controller.instance_variable_get("@_inertia_ssr_head")
6
- end
5
+ module InertiaRails
6
+ module Helper
7
+ def inertia_ssr_head
8
+ controller.instance_variable_get('@_inertia_ssr_head')
9
+ end
7
10
 
8
- def inertia_headers
9
- InertiaRails.deprecator.warn(
10
- "`inertia_headers` is deprecated and will be removed in InertiaRails 4.0, use `inertia_ssr_head` instead."
11
- )
12
- inertia_ssr_head
13
- end
11
+ def inertia_headers
12
+ InertiaRails.deprecator.warn(
13
+ '`inertia_headers` is deprecated and will be removed in InertiaRails 4.0, use `inertia_ssr_head` instead.'
14
+ )
15
+ inertia_ssr_head
16
+ end
14
17
 
15
- def inertia_rendering?
16
- controller.instance_variable_get("@_inertia_rendering")
17
- end
18
+ def inertia_rendering?
19
+ controller.instance_variable_get('@_inertia_rendering')
20
+ end
18
21
 
19
- def inertia_page
20
- controller.instance_variable_get("@_inertia_page")
21
- end
22
+ def inertia_page
23
+ controller.instance_variable_get('@_inertia_page')
24
+ end
22
25
 
23
- def inertia_meta_tags
24
- meta_tag_data = (inertia_page || {}).dig(:props, :_inertia_meta) || []
26
+ def inertia_meta_tags
27
+ meta_tag_data = (inertia_page || {}).dig(:props, :_inertia_meta) || []
25
28
 
26
- meta_tags = meta_tag_data.map do |inertia_meta_tag|
27
- inertia_meta_tag.to_tag(tag)
28
- end
29
+ meta_tags = meta_tag_data.map do |inertia_meta_tag|
30
+ inertia_meta_tag.to_tag(tag)
31
+ end
29
32
 
30
- safe_join(meta_tags, "\n")
33
+ safe_join(meta_tags, "\n")
34
+ end
31
35
  end
32
36
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'inertia_rails/prop_mergeable'
3
4
  require 'inertia_rails/base_prop'
4
5
  require 'inertia_rails/ignore_on_first_load_prop'
5
6
  require 'inertia_rails/always_prop'
@@ -7,6 +8,7 @@ require 'inertia_rails/lazy_prop'
7
8
  require 'inertia_rails/optional_prop'
8
9
  require 'inertia_rails/defer_prop'
9
10
  require 'inertia_rails/merge_prop'
11
+ require 'inertia_rails/scroll_prop'
10
12
  require 'inertia_rails/configuration'
11
13
  require 'inertia_rails/meta_tag'
12
14
 
@@ -34,8 +36,8 @@ module InertiaRails
34
36
  AlwaysProp.new(&block)
35
37
  end
36
38
 
37
- def merge(match_on: nil, &block)
38
- MergeProp.new(match_on: match_on, &block)
39
+ def merge(...)
40
+ MergeProp.new(...)
39
41
  end
40
42
 
41
43
  def deep_merge(match_on: nil, &block)
@@ -45,5 +47,9 @@ module InertiaRails
45
47
  def defer(group: nil, merge: nil, deep_merge: nil, match_on: nil, &block)
46
48
  DeferProp.new(group: group, merge: merge, deep_merge: deep_merge, match_on: match_on, &block)
47
49
  end
50
+
51
+ def scroll(metadata = nil, **options, &block)
52
+ ScrollProp.new(metadata: metadata, **options, &block)
53
+ end
48
54
  end
49
55
  end
@@ -10,7 +10,7 @@ module InertiaRails
10
10
  )
11
11
 
12
12
  @value = value
13
- @block = block
13
+ super(&block)
14
14
  end
15
15
 
16
16
  def call(controller)
@@ -2,20 +2,11 @@
2
2
 
3
3
  module InertiaRails
4
4
  class MergeProp < BaseProp
5
- attr_reader :match_on
5
+ prepend PropMergeable
6
6
 
7
- def initialize(deep_merge: false, match_on: nil, &block)
7
+ def initialize(**_props, &block)
8
8
  super(&block)
9
- @deep_merge = deep_merge
10
- @match_on = match_on.nil? ? nil : Array(match_on)
11
- end
12
-
13
- def merge?
14
- true
15
- end
16
-
17
- def deep_merge?
18
- @deep_merge
9
+ @merge = true
19
10
  end
20
11
  end
21
12
  end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InertiaRails
4
+ module PropMergeable
5
+ attr_reader :match_on, :appends_at_paths, :prepends_at_paths
6
+
7
+ def initialize(**props, &block)
8
+ raise ArgumentError, 'Cannot set both `deep_merge` and `merge` to true' if props[:deep_merge] && props[:merge]
9
+
10
+ @deep_merge = props.fetch(:deep_merge, false)
11
+ @merge = props[:merge] || @deep_merge
12
+ @match_on = props[:match_on].nil? ? nil : Array(props[:match_on])
13
+ @appends_at_paths = []
14
+ @prepends_at_paths = []
15
+ @append = true
16
+
17
+ append(props[:append]) if props.key?(:append)
18
+ prepend(props[:prepend]) if props.key?(:prepend)
19
+
20
+ super
21
+ end
22
+
23
+ def appends_at_root?
24
+ @append && merges_at_root?
25
+ end
26
+
27
+ def prepends_at_root?
28
+ !@append && merges_at_root?
29
+ end
30
+
31
+ def merges_at_root?
32
+ merge? && appends_at_paths.none? && prepends_at_paths.none?
33
+ end
34
+
35
+ def merge?
36
+ @merge
37
+ end
38
+
39
+ def deep_merge?
40
+ @deep_merge
41
+ end
42
+
43
+ private
44
+
45
+ def append(path, match_on: nil)
46
+ case path
47
+ when TrueClass, FalseClass
48
+ @append = path
49
+ when String
50
+ @appends_at_paths << path
51
+ when Array
52
+ @appends_at_paths += path
53
+ when Hash
54
+ @match_on ||= []
55
+ path.each do |key, value|
56
+ @appends_at_paths << key.to_s
57
+ @match_on << "#{key}.#{value}" if value
58
+ end
59
+ end
60
+
61
+ (@match_on ||= []) << "#{path}.#{match_on}" if match_on && path.is_a?(String)
62
+ end
63
+
64
+ def prepend(path, match_on: nil)
65
+ case path
66
+ when TrueClass, FalseClass
67
+ @append = !path
68
+ when String
69
+ @prepends_at_paths << path
70
+ when Array
71
+ @prepends_at_paths += path
72
+ when Hash
73
+ @match_on ||= []
74
+ path.each do |key, value|
75
+ @prepends_at_paths << key.to_s
76
+ @match_on << "#{key}.#{value}" if value
77
+ end
78
+ end
79
+
80
+ (@match_on ||= []) << "#{path}.#{match_on}" if match_on && path.is_a?(String)
81
+ end
82
+ end
83
+ end
@@ -6,15 +6,15 @@ require_relative 'inertia_rails'
6
6
 
7
7
  module InertiaRails
8
8
  class Renderer
9
- attr_reader(
10
- :component,
11
- :configuration,
12
- :controller,
13
- :props,
14
- :view_data,
15
- :encrypt_history,
16
- :clear_history
17
- )
9
+ %i[component configuration controller props view_data encrypt_history
10
+ clear_history].each do |method_name|
11
+ define_method(method_name) do
12
+ InertiaRails.deprecator.warn(
13
+ "[DEPRECATION] Accessing `InertiaRails::Renderer##{method_name}` is deprecated and will be removed in v4.0"
14
+ )
15
+ instance_variable_get("@#{method_name}")
16
+ end
17
+ end
18
18
 
19
19
  def initialize(component, controller, request, response, render_method, **options)
20
20
  if component.is_a?(Hash) && options.key?(:props)
@@ -24,15 +24,20 @@ module InertiaRails
24
24
 
25
25
  @controller = controller
26
26
  @configuration = controller.__send__(:inertia_configuration)
27
- @component = resolve_component(component)
28
27
  @request = request
29
28
  @response = response
30
29
  @render_method = render_method
31
- @props = options.fetch(:props, component.is_a?(Hash) ? component : controller.__send__(:inertia_view_assigns))
32
30
  @view_data = options.fetch(:view_data, {})
33
- @deep_merge = options.fetch(:deep_merge, configuration.deep_merge_shared_data)
34
- @encrypt_history = options.fetch(:encrypt_history, configuration.encrypt_history)
31
+ @encrypt_history = options.fetch(:encrypt_history, @configuration.encrypt_history)
35
32
  @clear_history = options.fetch(:clear_history, controller.session[:inertia_clear_history] || false)
33
+
34
+ deep_merge = options.fetch(:deep_merge, @configuration.deep_merge_shared_data)
35
+ passed_props = options.fetch(:props,
36
+ component.is_a?(Hash) ? component : @controller.__send__(:inertia_view_assigns))
37
+ @props = merge_props(shared_data, passed_props, deep_merge)
38
+
39
+ @component = resolve_component(component)
40
+
36
41
  @controller.instance_variable_set('@_inertia_rendering', true)
37
42
  controller.inertia_meta.add(options[:meta]) if options[:meta]
38
43
  end
@@ -48,32 +53,32 @@ module InertiaRails
48
53
  @render_method.call json: page.to_json, status: @response.status, content_type: Mime[:json]
49
54
  else
50
55
  begin
51
- return render_ssr if configuration.ssr_enabled
56
+ return render_ssr if @configuration.ssr_enabled
52
57
  rescue StandardError
53
58
  nil
54
59
  end
55
- controller.instance_variable_set('@_inertia_page', page)
56
- @render_method.call template: 'inertia', layout: layout, locals: view_data.merge(page: page)
60
+ @controller.instance_variable_set('@_inertia_page', page)
61
+ @render_method.call template: 'inertia', layout: layout, locals: @view_data.merge(page: page)
57
62
  end
58
63
  end
59
64
 
60
65
  private
61
66
 
62
67
  def render_ssr
63
- uri = URI("#{configuration.ssr_url}/render")
68
+ uri = URI("#{@configuration.ssr_url}/render")
64
69
  res = JSON.parse(Net::HTTP.post(uri, page.to_json, 'Content-Type' => 'application/json').body)
65
70
 
66
- controller.instance_variable_set('@_inertia_ssr_head', res['head'].join.html_safe)
67
- @render_method.call html: res['body'].html_safe, layout: layout, locals: view_data.merge(page: page)
71
+ @controller.instance_variable_set('@_inertia_ssr_head', res['head'].join.html_safe)
72
+ @render_method.call html: res['body'].html_safe, layout: layout, locals: @view_data.merge(page: page)
68
73
  end
69
74
 
70
75
  def layout
71
- layout = configuration.layout
76
+ layout = @configuration.layout
72
77
  layout.nil? || layout
73
78
  end
74
79
 
75
80
  def shared_data
76
- controller.__send__(:inertia_shared_data)
81
+ @controller.__send__(:inertia_shared_data)
77
82
  end
78
83
 
79
84
  # Cast props to symbol keyed hash before merging so that we have a consistent data structure and
@@ -81,8 +86,8 @@ module InertiaRails
81
86
  #
82
87
  # Functionally, this permits using either string or symbol keys in the controller. Since the results
83
88
  # is cast to json, we should treat string/symbol keys as identical.
84
- def merge_props(shared_props, props)
85
- if @deep_merge
89
+ def merge_props(shared_props, props, deep_merge)
90
+ if deep_merge
86
91
  shared_props.deep_symbolize_keys.deep_merge!(props.deep_symbolize_keys)
87
92
  else
88
93
  shared_props.symbolize_keys.merge(props.symbolize_keys)
@@ -91,47 +96,39 @@ module InertiaRails
91
96
 
92
97
  def computed_props
93
98
  # rubocop:disable Style/MultilineBlockChain
94
- merge_props(shared_data, props)
95
- .then do |merged_props| # Always keep errors in the props
99
+ @props
100
+ .tap do |merged_props| # Always keep errors in the props
96
101
  if merged_props.key?(:errors) && !merged_props[:errors].is_a?(BaseProp)
97
102
  errors = merged_props[:errors]
98
103
  merged_props[:errors] = InertiaRails.always { errors }
99
104
  end
100
- merged_props
101
105
  end
102
106
  .then { |props| deep_transform_props(props) } # Internal hydration/filtering
103
- .then { |props| configuration.prop_transformer(props: props) } # Apply user-defined prop transformer
104
- .tap { |props| props[:_inertia_meta] = meta_tags if meta_tags.present? } # Add meta tags last (never transformed)
105
-
107
+ .then { |props| @configuration.prop_transformer(props: props) } # Apply user-defined prop transformer
108
+ .tap do |props| # Add meta tags last (never transformed)
109
+ props[:_inertia_meta] = meta_tags if meta_tags.present?
110
+ end
106
111
  # rubocop:enable Style/MultilineBlockChain
107
112
  end
108
113
 
109
114
  def page
110
- default_page = {
111
- component: component,
115
+ return @page if defined?(@page)
116
+
117
+ @page = {
118
+ component: @component,
112
119
  props: computed_props,
113
120
  url: @request.original_fullpath,
114
- version: configuration.version,
115
- encryptHistory: encrypt_history,
116
- clearHistory: clear_history,
121
+ version: @configuration.version,
122
+ encryptHistory: @encrypt_history,
123
+ clearHistory: @clear_history,
117
124
  }
118
125
 
119
126
  deferred_props = deferred_props_keys
120
- default_page[:deferredProps] = deferred_props if deferred_props.present?
121
-
122
- deep_merge_props, merge_props = all_merge_props.partition do |_key, prop|
123
- prop.deep_merge?
124
- end
125
-
126
- match_props_on = all_merge_props.filter_map do |key, prop|
127
- prop.match_on.map { |ms| "#{key}.#{ms}" } if prop.match_on.present?
128
- end.flatten
127
+ @page[:deferredProps] = deferred_props if deferred_props.present?
128
+ @page[:scrollProps] = scroll_props if scroll_props.present?
129
+ @page.merge!(resolve_merge_props)
129
130
 
130
- default_page[:mergeProps] = merge_props.map(&:first) if merge_props.present?
131
- default_page[:deepMergeProps] = deep_merge_props.map(&:first) if deep_merge_props.present?
132
- default_page[:matchPropsOn] = match_props_on if match_props_on.present?
133
-
134
- default_page
131
+ @page
135
132
  end
136
133
 
137
134
  def deep_transform_props(props, parent_path = [])
@@ -145,9 +142,9 @@ module InertiaRails
145
142
  transformed_props[key] =
146
143
  case prop
147
144
  when BaseProp
148
- prop.call(controller)
145
+ prop.call(@controller)
149
146
  when Proc
150
- controller.instance_exec(&prop)
147
+ @controller.instance_exec(&prop)
151
148
  else
152
149
  prop
153
150
  end
@@ -163,10 +160,28 @@ module InertiaRails
163
160
  end
164
161
  end
165
162
 
166
- def all_merge_props
167
- @all_merge_props ||= @props.select do |key, prop|
163
+ def resolve_merge_props
164
+ deep_merge_props, merge_props = all_merge_props.partition do |_key, prop|
165
+ prop.deep_merge?
166
+ end
167
+
168
+ {
169
+ mergeProps: append_merge_props(merge_props),
170
+ prependProps: prepend_merge_props(merge_props),
171
+ deepMergeProps: deep_merge_props.map!(&:first),
172
+ matchPropsOn: resolve_match_on_props,
173
+ }.delete_if { |_, v| v.blank? }
174
+ end
175
+
176
+ def resolve_match_on_props
177
+ all_merge_props.filter_map do |key, prop|
178
+ prop.match_on.map! { |ms| "#{key}.#{ms}" } if prop.match_on.present?
179
+ end.flatten
180
+ end
181
+
182
+ def requested_merge_props
183
+ @requested_merge_props ||= @props.select do |key, prop|
168
184
  next unless prop.try(:merge?)
169
- next if reset_keys.include?(key)
170
185
  next if rendering_partial_component? && (
171
186
  (partial_keys.present? && partial_keys.exclude?(key.name)) ||
172
187
  (partial_except_keys.present? && partial_except_keys.include?(key.name))
@@ -176,25 +191,73 @@ module InertiaRails
176
191
  end
177
192
  end
178
193
 
194
+ def append_merge_props(props)
195
+ return props if props.empty?
196
+
197
+ root_append_props, nested_append_props = props.partition { |_key, prop| prop.appends_at_root? }
198
+
199
+ result = Set.new(root_append_props.map!(&:first))
200
+
201
+ nested_append_props.each do |key, prop|
202
+ prop.appends_at_paths.each do |path|
203
+ result.add("#{key}.#{path}")
204
+ end
205
+ end
206
+
207
+ result.to_a
208
+ end
209
+
210
+ def prepend_merge_props(props)
211
+ return props if props.empty?
212
+
213
+ root_prepend_props, nested_prepend_props = props.partition { |_key, prop| prop.prepends_at_root? }
214
+
215
+ result = Set.new(root_prepend_props.map!(&:first))
216
+
217
+ nested_prepend_props.each do |key, prop|
218
+ prop.prepends_at_paths.each do |path|
219
+ result.add("#{key}.#{path}")
220
+ end
221
+ end
222
+
223
+ result.to_a
224
+ end
225
+
226
+ def scroll_props
227
+ return @scroll_props if defined?(@scroll_props)
228
+
229
+ @scroll_props = {}
230
+ requested_merge_props.each do |key, prop|
231
+ next unless prop.is_a?(ScrollProp)
232
+
233
+ @scroll_props[key] = prop.metadata.merge!(reset: reset_keys.include?(key))
234
+ end
235
+ @scroll_props
236
+ end
237
+
238
+ def all_merge_props
239
+ @all_merge_props ||= requested_merge_props.reject { |key,| reset_keys.include?(key) }
240
+ end
241
+
179
242
  def partial_keys
180
- @partial_keys ||= (@request.headers['X-Inertia-Partial-Data'] || '').split(',').compact
243
+ @partial_keys ||= (@request.headers['X-Inertia-Partial-Data'] || '').split(',').compact_blank!
181
244
  end
182
245
 
183
246
  def reset_keys
184
- (@request.headers['X-Inertia-Reset'] || '').split(',').compact.map(&:to_sym)
247
+ @reset_keys ||= (@request.headers['X-Inertia-Reset'] || '').split(',').compact_blank!.map!(&:to_sym)
185
248
  end
186
249
 
187
250
  def partial_except_keys
188
- (@request.headers['X-Inertia-Partial-Except'] || '').split(',').compact
251
+ @partial_except_keys ||= (@request.headers['X-Inertia-Partial-Except'] || '').split(',').compact_blank!
189
252
  end
190
253
 
191
254
  def rendering_partial_component?
192
- @request.headers['X-Inertia-Partial-Component'] == component
255
+ @request.headers['X-Inertia-Partial-Component'] == @component
193
256
  end
194
257
 
195
258
  def resolve_component(component)
196
259
  if component == true || component.is_a?(Hash)
197
- configuration.component_path_resolver(path: controller.controller_path, action: controller.action_name)
260
+ @configuration.component_path_resolver(path: @controller.controller_path, action: @controller.action_name)
198
261
  else
199
262
  component
200
263
  end
@@ -230,7 +293,7 @@ module InertiaRails
230
293
  end
231
294
 
232
295
  def meta_tags
233
- controller.inertia_meta.meta_tags
296
+ @controller.inertia_meta.meta_tags
234
297
  end
235
298
  end
236
299
  end
@@ -1,30 +1,32 @@
1
- require "rspec/core"
2
- require "rspec/matchers"
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/core'
4
+ require 'rspec/matchers'
3
5
 
4
6
  module InertiaRails
5
7
  module RSpec
6
8
  class InertiaRenderWrapper
7
9
  attr_reader :view_data, :props, :component
8
-
10
+
9
11
  def initialize
10
12
  @view_data = nil
11
13
  @props = nil
12
14
  @component = nil
13
15
  end
14
-
16
+
15
17
  def call(params)
16
- set_values(params)
18
+ assign_locals(params)
17
19
  @render_method&.call(params)
18
20
  end
19
-
21
+
20
22
  def wrap_render(render_method)
21
23
  @render_method = render_method
22
24
  self
23
25
  end
24
-
26
+
25
27
  protected
26
-
27
- def set_values(params)
28
+
29
+ def assign_locals(params)
28
30
  if params[:locals].present?
29
31
  @view_data = params[:locals].except(:page)
30
32
  @props = params[:locals][:page][:props]
@@ -33,18 +35,24 @@ module InertiaRails
33
35
  # Sequential Inertia request
34
36
  @view_data = {}
35
37
  json = JSON.parse(params[:json])
36
- @props = json["props"]
37
- @component = json["component"]
38
+ @props = json['props']
39
+ @component = json['component']
38
40
  end
39
41
  end
40
42
  end
41
43
 
42
44
  module Helpers
43
45
  def inertia
44
- raise 'Inertia test helpers aren\'t set up! Make sure you add inertia: true to describe blocks using inertia tests.' unless inertia_tests_setup?
46
+ unless inertia_tests_setup?
47
+ raise "Inertia test helpers aren't set up! " \
48
+ 'Make sure you add `inertia: true` to describe blocks using inertia tests.'
49
+ end
45
50
 
46
51
  if @_inertia_render_wrapper.nil? && !::RSpec.configuration.inertia[:skip_missing_renderer_warnings]
47
- warn 'WARNING: the test never created an Inertia renderer. Maybe the code wasn\'t able to reach a `render inertia:` call? If this was intended, or you don\'t want to see this message, set ::RSpec.configuration.inertia[:skip_missing_renderer_warnings] = true'
52
+ warn 'WARNING: the test never created an Inertia renderer. ' \
53
+ "Maybe the code wasn't able to reach a `render inertia:` call? If this was intended, " \
54
+ "or you don't want to see this message, " \
55
+ 'set ::RSpec.configuration.inertia[:skip_missing_renderer_warnings] = true'
48
56
  end
49
57
  @_inertia_render_wrapper
50
58
  end
@@ -57,8 +65,8 @@ module InertiaRails
57
65
  @_inertia_render_wrapper = InertiaRenderWrapper.new.wrap_render(render)
58
66
  end
59
67
 
60
- protected
61
-
68
+ protected
69
+
62
70
  def inertia_tests_setup?
63
71
  ::RSpec.current_example.metadata.fetch(:inertia, false)
64
72
  end
@@ -67,9 +75,9 @@ module InertiaRails
67
75
  end
68
76
 
69
77
  RSpec.configure do |config|
70
- config.include ::InertiaRails::RSpec::Helpers
78
+ config.include InertiaRails::RSpec::Helpers
71
79
  config.add_setting :inertia, default: {
72
- skip_missing_renderer_warnings: false
80
+ skip_missing_renderer_warnings: false,
73
81
  }
74
82
 
75
83
  config.before(:each, inertia: true) do
@@ -106,13 +114,14 @@ RSpec::Matchers.define :render_component do |expected_component|
106
114
  end
107
115
 
108
116
  failure_message do |inertia|
109
- "expected rendered inertia component to be #{expected_component}, instead received #{inertia.component || 'nothing'}"
117
+ "expected rendered inertia component to be #{expected_component}, " \
118
+ "instead received #{inertia.component || 'nothing'}"
110
119
  end
111
120
  end
112
121
 
113
122
  RSpec::Matchers.define :have_exact_view_data do |expected_view_data|
114
123
  match do |inertia|
115
- expect(inertia.view_data).to eq expected_view_data
124
+ expect(inertia.view_data).to eq expected_view_data
116
125
  end
117
126
 
118
127
  failure_message do |inertia|
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InertiaRails
4
+ module ScrollMetadata
5
+ class MissingMetadataAdapterError < StandardError; end
6
+
7
+ class Props
8
+ def initialize(page_name:, previous_page:, next_page:, current_page:)
9
+ @page_name = page_name
10
+ @previous_page = previous_page
11
+ @next_page = next_page
12
+ @current_page = current_page
13
+ end
14
+
15
+ def as_json(_options = nil)
16
+ {
17
+ pageName: @page_name,
18
+ previousPage: @previous_page,
19
+ nextPage: @next_page,
20
+ currentPage: @current_page,
21
+ }
22
+ end
23
+ end
24
+
25
+ class KaminariAdapter
26
+ def match?(metadata)
27
+ defined?(Kaminari) && metadata.is_a?(Kaminari::PageScopeMethods)
28
+ end
29
+
30
+ def call(metadata, **_options)
31
+ {
32
+ page_name: (Kaminari.config.param_name || 'page').to_s,
33
+ previous_page: metadata.prev_page,
34
+ next_page: metadata.next_page,
35
+ current_page: metadata.current_page,
36
+ }
37
+ end
38
+ end
39
+
40
+ class PagyAdapter
41
+ def match?(metadata)
42
+ defined?(Pagy) && metadata.is_a?(Pagy)
43
+ end
44
+
45
+ def call(metadata, **_options)
46
+ page_name = metadata.respond_to?(:vars) ? metadata.vars.fetch(:page_param) : metadata.options[:page_key]
47
+ {
48
+ page_name: page_name.to_s,
49
+ previous_page: metadata.try(:prev) || metadata.try(:previous),
50
+ next_page: metadata.next,
51
+ current_page: metadata.page,
52
+ }
53
+ end
54
+ end
55
+
56
+ class HashAdapter
57
+ def match?(metadata)
58
+ metadata.is_a?(Hash)
59
+ end
60
+
61
+ def call(metadata, **_options)
62
+ {
63
+ page_name: metadata.fetch(:page_name),
64
+ previous_page: metadata.fetch(:previous_page),
65
+ next_page: metadata.fetch(:next_page),
66
+ current_page: metadata.fetch(:current_page),
67
+ }
68
+ end
69
+ end
70
+
71
+ class << self
72
+ attr_accessor :adapters
73
+
74
+ def extract(metadata, **options)
75
+ overrides = options.slice(:page_name, :previous_page, :next_page, :current_page)
76
+
77
+ adapters.each do |adapter|
78
+ next unless adapter.match?(metadata)
79
+
80
+ return Props.new(**adapter.call(metadata, **options).merge!(overrides)).as_json
81
+ end
82
+
83
+ begin
84
+ Props.new(**overrides).as_json
85
+ rescue ArgumentError
86
+ raise MissingMetadataAdapterError, "No ScrollMetadata adapter found for #{metadata}"
87
+ end
88
+ end
89
+
90
+ def register_adapter(adapter)
91
+ adapters.unshift(adapter.new)
92
+ end
93
+ end
94
+
95
+ self.adapters = [KaminariAdapter, PagyAdapter, HashAdapter].map(&:new)
96
+ end
97
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'scroll_metadata'
4
+
5
+ module InertiaRails
6
+ class ScrollProp < BaseProp
7
+ prepend PropMergeable
8
+
9
+ def initialize(**options, &block)
10
+ super(&block)
11
+
12
+ @merge = true
13
+ @metadata = options.delete(:metadata)
14
+ @wrapper = options.delete(:wrapper)
15
+
16
+ @options = options
17
+ end
18
+
19
+ def call(controller)
20
+ @value = super
21
+ configure_merge_intent(controller.request.headers['X-Inertia-Infinite-Scroll-Merge-Intent'])
22
+ @value
23
+ end
24
+
25
+ def metadata
26
+ ScrollMetadata.extract(@metadata, **@options)
27
+ end
28
+
29
+ private
30
+
31
+ def configure_merge_intent(scroll_intent)
32
+ scroll_intent == 'prepend' ? prepend(@wrapper || true) : append(@wrapper || true)
33
+ end
34
+ end
35
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module InertiaRails
2
- VERSION = "3.11.0"
4
+ VERSION = '3.12.1'
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Patch BetterErrors::Middleware to render HTML for Inertia requests
2
4
  #
3
5
  # Original source:
@@ -7,13 +9,11 @@
7
9
  module InertiaRails
8
10
  module InertiaBetterErrors
9
11
  def text?(env)
10
- return false if env["HTTP_X_INERTIA"]
12
+ return false if env['HTTP_X_INERTIA']
11
13
 
12
14
  super
13
15
  end
14
16
  end
15
17
  end
16
18
 
17
- if defined?(BetterErrors)
18
- BetterErrors::Middleware.include InertiaRails::InertiaBetterErrors
19
- end
19
+ BetterErrors::Middleware.include InertiaRails::InertiaBetterErrors if defined?(BetterErrors)
@@ -1,17 +1,34 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Patch ActionDispatch::DebugExceptions to render HTML for Inertia requests
2
4
  #
3
- # Rails has introduced text rendering for XHR requests with Rails 4.1 and
4
- # changed the implementation in 4.2, 5.0 and 5.1 (unchanged since then).
5
- #
6
5
  # The original source needs to be patched, so that Inertia requests are
7
6
  # NOT responded with plain text, but with HTML.
7
+ #
8
+ # Original source (unchanged since Rails 5.1):
9
+ # https://github.com/rails/rails/blob/5-1-stable/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
10
+ # https://github.com/rails/rails/blob/8-0-stable/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
11
+ #
8
12
 
9
- if defined?(ActionDispatch::DebugExceptions)
10
- if ActionPack.version.to_s >= '5.1'
11
- require 'patches/debug_exceptions/patch-5-1'
12
- elsif ActionPack.version.to_s >= '5.0'
13
- require 'patches/debug_exceptions/patch-5-0'
14
- else
15
- # This gem supports Rails 5 or later
13
+ module InertiaRails
14
+ module InertiaDebugExceptions
15
+ def render_for_browser_request(request, wrapper)
16
+ template = create_template(request, wrapper)
17
+ file = "rescues/#{wrapper.rescue_template}"
18
+
19
+ if request.xhr? && !request.headers['X-Inertia'] # <<<< this line is changed only
20
+ body = template.render(template: file, layout: false, formats: [:text])
21
+ format = 'text/plain'
22
+ else
23
+ body = template.render(template: file, layout: 'rescues/layout')
24
+ format = 'text/html'
25
+ end
26
+
27
+ render(wrapper.status_code, body, format)
28
+ end
16
29
  end
17
30
  end
31
+
32
+ if defined?(ActionDispatch::DebugExceptions)
33
+ ActionDispatch::DebugExceptions.prepend InertiaRails::InertiaDebugExceptions
34
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module InertiaRails
2
4
  module InertiaMapper
3
5
  def inertia(*args, **options)
@@ -12,7 +14,10 @@ module InertiaRails
12
14
  if path.is_a?(Hash)
13
15
  path.first
14
16
  elsif resource_scope?
15
- [path, InertiaRails.configuration.component_path_resolver(path: [@scope[:module], @scope[:controller]].compact.join('/'), action: path)]
17
+ [path,
18
+ InertiaRails.configuration.component_path_resolver(
19
+ path: [@scope[:module], @scope[:controller]].compact.join('/'), action: path
20
+ )]
16
21
  elsif @scope[:module].blank?
17
22
  [path, path]
18
23
  else
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module InertiaRails
2
4
  module InertiaRequest
3
5
  def inertia?
@@ -1,16 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  namespace :inertia_rails do
2
4
  namespace :install do
3
- desc "Installs inertia_rails packages and configurations for a React based app"
4
- task :react => :environment do
5
+ desc 'Installs inertia_rails packages and configurations for a React based app'
6
+ task react: :environment do
5
7
  system 'rails g inertia_rails:install --front_end react'
6
8
  end
7
- desc "Installs inertia_rails packages and configurations for a Vue based app"
9
+ desc 'Installs inertia_rails packages and configurations for a Vue based app'
8
10
  task vue: :environment do
9
11
  system 'rails g inertia_rails:install --front_end vue'
10
12
  end
11
- desc "Installs inertia_rails packages and configurations for a Svelte based app"
13
+ desc 'Installs inertia_rails packages and configurations for a Svelte based app'
12
14
  task svelte: :environment do
13
15
  system 'rails g inertia_rails:install --front_end svelte'
14
16
  end
15
17
  end
16
- end
18
+ end
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.11.0
4
+ version: 3.12.1
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: 2025-08-29 00:00:00.000000000 Z
12
+ date: 2025-11-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: railties
@@ -229,13 +229,14 @@ files:
229
229
  - lib/inertia_rails/meta_tag_builder.rb
230
230
  - lib/inertia_rails/middleware.rb
231
231
  - lib/inertia_rails/optional_prop.rb
232
+ - lib/inertia_rails/prop_mergeable.rb
232
233
  - lib/inertia_rails/renderer.rb
233
234
  - lib/inertia_rails/rspec.rb
235
+ - lib/inertia_rails/scroll_metadata.rb
236
+ - lib/inertia_rails/scroll_prop.rb
234
237
  - lib/inertia_rails/version.rb
235
238
  - lib/patches/better_errors.rb
236
239
  - lib/patches/debug_exceptions.rb
237
- - lib/patches/debug_exceptions/patch-5-0.rb
238
- - lib/patches/debug_exceptions/patch-5-1.rb
239
240
  - lib/patches/mapper.rb
240
241
  - lib/patches/request.rb
241
242
  - lib/tasks/inertia_rails.rake
@@ -1,27 +0,0 @@
1
- # Patch ActionDispatch::DebugExceptions to render HTML for Inertia requests
2
- #
3
- # Original source:
4
- # https://github.com/rails/rails/blob/5-0-stable/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
5
- #
6
-
7
- module InertiaRails
8
- module InertiaDebugExceptions
9
- def render_for_default_application(request, wrapper)
10
- template = create_template(request, wrapper)
11
- file = "rescues/#{wrapper.rescue_template}"
12
-
13
- if request.xhr? && !request.headers['X-Inertia'] # <<<< this line is changed only
14
- body = template.render(template: file, layout: false, formats: [:text])
15
- format = "text/plain"
16
- else
17
- body = template.render(template: file, layout: 'rescues/layout')
18
- format = "text/html"
19
- end
20
- render(wrapper.status_code, body, format)
21
- end
22
- end
23
- end
24
-
25
- if defined?(ActionDispatch::DebugExceptions)
26
- ActionDispatch::DebugExceptions.prepend InertiaRails::InertiaDebugExceptions
27
- end
@@ -1,30 +0,0 @@
1
- # Patch ActionDispatch::DebugExceptions to render HTML for Inertia requests
2
- #
3
- # Original source (unchanged since Rails 5.1):
4
- # https://github.com/rails/rails/blob/5-1-stable/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
5
- # https://github.com/rails/rails/blob/5-2-stable/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
6
- # https://github.com/rails/rails/blob/6-0-stable/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
7
- #
8
-
9
- module InertiaRails
10
- module InertiaDebugExceptions
11
- def render_for_browser_request(request, wrapper)
12
- template = create_template(request, wrapper)
13
- file = "rescues/#{wrapper.rescue_template}"
14
-
15
- if request.xhr? && !request.headers['X-Inertia'] # <<<< this line is changed only
16
- body = template.render(template: file, layout: false, formats: [:text])
17
- format = "text/plain"
18
- else
19
- body = template.render(template: file, layout: "rescues/layout")
20
- format = "text/html"
21
- end
22
-
23
- render(wrapper.status_code, body, format)
24
- end
25
- end
26
- end
27
-
28
- if defined?(ActionDispatch::DebugExceptions)
29
- ActionDispatch::DebugExceptions.prepend InertiaRails::InertiaDebugExceptions
30
- end