inertia_rails 3.10.0 → 3.12.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a8109050832c842cdc65c514f09a7ab02ce79a0925517fcb1f34eceee8d54130
4
- data.tar.gz: 6ce4aaf2b8d4d2615f4e53220a23d712c97c482a2705c70a9423a1ce51b6d1ca
3
+ metadata.gz: 95b5901a42397a1ae01678a1101d5d21a6a2f2f33c6afb413a2fbca48f04e064
4
+ data.tar.gz: a4d04ff25ce45b4392e5ad5c5153530446f83256e2b00452999da0c920751416
5
5
  SHA512:
6
- metadata.gz: 32e92a9898314ff42c67450acc691513cc7e833eff2d15999bb4d4d642b62a7c1f94bcdff3093a79448f95adda2072d0d2e18d7250f9a8dfcb0a57cbcc3fa633
7
- data.tar.gz: 147352323994ef1bc475aee16990581ba6714769ac157f144c5f1bb296b6058a4bcdf9d5f00fef47cfa26a488b91878aea1e046e7bb9b70e74cda4e8a61ef31c
6
+ metadata.gz: 222ebe8277d96856c05c9d712ad0d0e33cea995e7195dada790cf6423e3ad9f16b970c555c9889a1d7eb9082ad9045ca73a00bc95116fc93912f429fc1ded50f
7
+ data.tar.gz: f859bfb3d498ae8bc2c146e5bd8d9be8cadb65a6f3a51135c89bd1f3e86c83007f2602b9e6b115179e7f294dc663e3c96ce9073890511b478689ddf99156e1b9
data/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ 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.0] - 2025-11-08
8
+
9
+ * Docs updates (@leenyburger, @skryukov, @bn-l)
10
+ * Reimplement devcontainers (@kieraneglin)
11
+ * Support for Inertia.js infinite scroll components (@skyrukov)
12
+ * New merge options (@skryukov)
13
+
14
+ ## [3.11.0] - 2025-08-29
15
+
16
+ * Fix Svelte generator (@skryukov)
17
+ * Docs updates for SSR and 2.1.2 (@skryukov)
18
+ * Devcontainers for local dev (@kieraneglin)
19
+ * Add configurable prop transformation (@kieraneglin)
20
+ * Gradual deprecation of null errors because Inertis.js expects an empty object (@skryukov)
21
+ * Allow the more helpful UnknownFormat exception to raise when a static intertia route is requested with a non-HTML format (@skryukov)
22
+
7
23
  ## [3.10.0] - 2025-07-30
8
24
 
9
25
  * llms.txt in docs (@brandonshar and @skryukov)
@@ -1,7 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module InertiaRails
2
4
  class StaticController < InertiaRails.configuration.parent_controller.constantize
3
5
  def static
4
- render inertia: params[:component]
6
+ respond_to do |format|
7
+ format.html { render inertia: params[:component] }
8
+ end
5
9
  end
6
10
  end
7
11
  end
@@ -4,6 +4,7 @@ react:
4
4
  - "@vitejs/plugin-react"
5
5
  - "react"
6
6
  - "react-dom"
7
+ - "vite@latest"
7
8
  packages_ts:
8
9
  - "@types/react"
9
10
  - "@types/react-dom"
@@ -29,6 +30,7 @@ vue:
29
30
  packages:
30
31
  - "vue"
31
32
  - "@vitejs/plugin-vue"
33
+ - "vite@latest"
32
34
  packages_ts:
33
35
  - "typescript@~5.6.2"
34
36
  - "vue-tsc"
@@ -52,6 +54,7 @@ svelte4:
52
54
  packages:
53
55
  - "svelte@4"
54
56
  - "@sveltejs/vite-plugin-svelte@3"
57
+ - "vite@5"
55
58
  packages_ts:
56
59
  - "@tsconfig/svelte@4"
57
60
  - "svelte-check"
@@ -76,7 +79,8 @@ svelte:
76
79
  inertia_package: "@inertiajs/svelte"
77
80
  packages:
78
81
  - "svelte@5"
79
- - "@sveltejs/vite-plugin-svelte@4"
82
+ - "@sveltejs/vite-plugin-svelte"
83
+ - "vite@latest"
80
84
  packages_ts:
81
85
  - "@tsconfig/svelte@5"
82
86
  - "svelte-check"
@@ -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,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  InertiaRails.configure do |config|
4
- config.ssr_enabled = ViteRuby.config.ssr_build_enabled
5
4
  config.version = ViteRuby.digest
5
+ config.encrypt_history = true
6
+ config.always_include_errors_hash = true
6
7
  end
@@ -2,11 +2,6 @@ import { createInertiaApp } from '@inertiajs/svelte'
2
2
  import { mount } from 'svelte';
3
3
 
4
4
  createInertiaApp({
5
- // Set default page title
6
- // see https://inertia-rails.dev/guide/title-and-meta
7
- //
8
- // title: title => title ? `${title} - App` : 'App',
9
-
10
5
  // Disable progress bar
11
6
  //
12
7
  // see https://inertia-rails.dev/guide/progress-indicators
@@ -2,11 +2,6 @@ import { createInertiaApp, type ResolvedComponent } from '@inertiajs/svelte'
2
2
  import { mount } from 'svelte'
3
3
 
4
4
  createInertiaApp({
5
- // Set default page title
6
- // see https://inertia-rails.dev/guide/title-and-meta
7
- //
8
- // title: title => title ? `${title} - App` : 'App',
9
-
10
5
  // Disable progress bar
11
6
  //
12
7
  // see https://inertia-rails.dev/guide/progress-indicators
@@ -25,7 +20,7 @@ createInertiaApp({
25
20
  // and use the following line.
26
21
  // see https://inertia-rails.dev/guide/pages#default-layouts
27
22
  //
28
- // return { default: page.default, layout: page.layout || Layout }
23
+ // return { default: page.default, layout: page.layout || Layout } as ResolvedComponent
29
24
 
30
25
  return page
31
26
  },
@@ -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)
@@ -12,6 +12,9 @@ module InertiaRails
12
12
  # Allows the user to hook into the default rendering behavior and change it to fit their needs
13
13
  component_path_resolver: ->(path:, action:) { "#{path}/#{action}" },
14
14
 
15
+ # A function that transforms the props before they are sent to the client.
16
+ prop_transformer: ->(props:) { props },
17
+
15
18
  # DEPRECATED: Let Rails decide which layout should be used based on the
16
19
  # controller configuration.
17
20
  layout: true,
@@ -26,8 +29,11 @@ module InertiaRails
26
29
  # Used to detect version drift between server and client.
27
30
  version: nil,
28
31
 
29
- # Allows configuring the base controller for StaticController
32
+ # Allows configuring the base controller for StaticController.
30
33
  parent_controller: '::ApplicationController',
34
+
35
+ # Whether to include empty `errors` hash to the props when no errors are present.
36
+ always_include_errors_hash: nil,
31
37
  }.freeze
32
38
 
33
39
  OPTION_NAMES = DEFAULTS.keys.freeze
@@ -89,6 +95,10 @@ module InertiaRails
89
95
  @options[:component_path_resolver].call(path: path, action: action)
90
96
  end
91
97
 
98
+ def prop_transformer(props:)
99
+ @options[:prop_transformer].call(props: props)
100
+ end
101
+
92
102
  OPTION_NAMES.each do |option|
93
103
  unless method_defined?(option)
94
104
  define_method(option) do
@@ -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
 
@@ -144,15 +149,30 @@ module InertiaRails
144
149
  end
145
150
 
146
151
  def inertia_shared_data
147
- initial_data = session[:inertia_errors].present? ? {errors: session[:inertia_errors]} : {}
152
+ initial_data =
153
+ if session[:inertia_errors].present?
154
+ { errors: session[:inertia_errors] }
155
+ elsif inertia_configuration.always_include_errors_hash
156
+ { errors: {} }
157
+ else
158
+ if inertia_configuration.always_include_errors_hash.nil?
159
+ InertiaRails.deprecator.warn(
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`.'
164
+ )
165
+ end
166
+ {}
167
+ end
148
168
 
149
- self.class._inertia_shared_data.filter_map { |shared_data|
169
+ self.class._inertia_shared_data.filter_map do |shared_data|
150
170
  if shared_data.respond_to?(:call)
151
171
  instance_exec(&shared_data)
152
172
  else
153
173
  shared_data
154
174
  end
155
- }.reduce(initial_data, &:merge)
175
+ end.reduce(initial_data, &:merge)
156
176
  end
157
177
 
158
178
  def inertia_location(url)
@@ -168,7 +188,7 @@ module InertiaRails
168
188
  session[:inertia_errors] = inertia_errors.to_hash
169
189
  else
170
190
  InertiaRails.deprecator.warn(
171
- "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.'
172
192
  )
173
193
  session[:inertia_errors] = inertia_errors
174
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
@@ -90,14 +90,27 @@ module InertiaRails
90
90
  end
91
91
 
92
92
  def computed_props
93
- merged_props = merge_props(shared_data, props)
94
- deep_transform_props(merged_props).tap do |transformed_props|
95
- transformed_props[:_inertia_meta] = meta_tags if meta_tags.present?
93
+ # rubocop:disable Style/MultilineBlockChain
94
+ merge_props(shared_data, props)
95
+ .then do |merged_props| # Always keep errors in the props
96
+ if merged_props.key?(:errors) && !merged_props[:errors].is_a?(BaseProp)
97
+ errors = merged_props[:errors]
98
+ merged_props[:errors] = InertiaRails.always { errors }
99
+ end
100
+ merged_props
101
+ end
102
+ .then { |props| deep_transform_props(props) } # Internal hydration/filtering
103
+ .then { |props| configuration.prop_transformer(props: props) } # Apply user-defined prop transformer
104
+ .tap do |props| # Add meta tags last (never transformed)
105
+ props[:_inertia_meta] = meta_tags if meta_tags.present?
96
106
  end
107
+ # rubocop:enable Style/MultilineBlockChain
97
108
  end
98
109
 
99
110
  def page
100
- default_page = {
111
+ return @page if defined?(@page)
112
+
113
+ @page = {
101
114
  component: component,
102
115
  props: computed_props,
103
116
  url: @request.original_fullpath,
@@ -107,21 +120,11 @@ module InertiaRails
107
120
  }
108
121
 
109
122
  deferred_props = deferred_props_keys
110
- default_page[:deferredProps] = deferred_props if deferred_props.present?
111
-
112
- deep_merge_props, merge_props = all_merge_props.partition do |_key, prop|
113
- prop.deep_merge?
114
- end
123
+ @page[:deferredProps] = deferred_props if deferred_props.present?
124
+ @page[:scrollProps] = scroll_props if scroll_props.present?
125
+ @page.merge!(resolve_merge_props)
115
126
 
116
- match_props_on = all_merge_props.filter_map do |key, prop|
117
- prop.match_on.map { |ms| "#{key}.#{ms}" } if prop.match_on.present?
118
- end.flatten
119
-
120
- default_page[:mergeProps] = merge_props.map(&:first) if merge_props.present?
121
- default_page[:deepMergeProps] = deep_merge_props.map(&:first) if deep_merge_props.present?
122
- default_page[:matchPropsOn] = match_props_on if match_props_on.present?
123
-
124
- default_page
127
+ @page
125
128
  end
126
129
 
127
130
  def deep_transform_props(props, parent_path = [])
@@ -153,10 +156,28 @@ module InertiaRails
153
156
  end
154
157
  end
155
158
 
156
- def all_merge_props
157
- @all_merge_props ||= @props.select do |key, prop|
159
+ def resolve_merge_props
160
+ deep_merge_props, merge_props = all_merge_props.partition do |_key, prop|
161
+ prop.deep_merge?
162
+ end
163
+
164
+ {
165
+ mergeProps: append_merge_props(merge_props),
166
+ prependProps: prepend_merge_props(merge_props),
167
+ deepMergeProps: deep_merge_props.map!(&:first),
168
+ matchPropsOn: resolve_match_on_props,
169
+ }.delete_if { |_, v| v.blank? }
170
+ end
171
+
172
+ def resolve_match_on_props
173
+ all_merge_props.filter_map do |key, prop|
174
+ prop.match_on.map! { |ms| "#{key}.#{ms}" } if prop.match_on.present?
175
+ end.flatten
176
+ end
177
+
178
+ def requested_merge_props
179
+ @requested_merge_props ||= @props.select do |key, prop|
158
180
  next unless prop.try(:merge?)
159
- next if reset_keys.include?(key)
160
181
  next if rendering_partial_component? && (
161
182
  (partial_keys.present? && partial_keys.exclude?(key.name)) ||
162
183
  (partial_except_keys.present? && partial_except_keys.include?(key.name))
@@ -166,16 +187,64 @@ module InertiaRails
166
187
  end
167
188
  end
168
189
 
190
+ def append_merge_props(props)
191
+ return props if props.empty?
192
+
193
+ root_append_props, nested_append_props = props.partition { |_key, prop| prop.appends_at_root? }
194
+
195
+ result = Set.new(root_append_props.map!(&:first))
196
+
197
+ nested_append_props.each do |key, prop|
198
+ prop.appends_at_paths.each do |path|
199
+ result.add("#{key}.#{path}")
200
+ end
201
+ end
202
+
203
+ result.to_a
204
+ end
205
+
206
+ def prepend_merge_props(props)
207
+ return props if props.empty?
208
+
209
+ root_prepend_props, nested_prepend_props = props.partition { |_key, prop| prop.prepends_at_root? }
210
+
211
+ result = Set.new(root_prepend_props.map!(&:first))
212
+
213
+ nested_prepend_props.each do |key, prop|
214
+ prop.prepends_at_paths.each do |path|
215
+ result.add("#{key}.#{path}")
216
+ end
217
+ end
218
+
219
+ result.to_a
220
+ end
221
+
222
+ def scroll_props
223
+ return @scroll_props if defined?(@scroll_props)
224
+
225
+ @scroll_props = {}
226
+ requested_merge_props.each do |key, prop|
227
+ next unless prop.is_a?(ScrollProp)
228
+
229
+ @scroll_props[key] = prop.metadata.merge!(reset: reset_keys.include?(key))
230
+ end
231
+ @scroll_props
232
+ end
233
+
234
+ def all_merge_props
235
+ @all_merge_props ||= requested_merge_props.reject { |key,| reset_keys.include?(key) }
236
+ end
237
+
169
238
  def partial_keys
170
- @partial_keys ||= (@request.headers['X-Inertia-Partial-Data'] || '').split(',').compact
239
+ @partial_keys ||= (@request.headers['X-Inertia-Partial-Data'] || '').split(',').compact_blank!
171
240
  end
172
241
 
173
242
  def reset_keys
174
- (@request.headers['X-Inertia-Reset'] || '').split(',').compact.map(&:to_sym)
243
+ @reset_keys ||= (@request.headers['X-Inertia-Reset'] || '').split(',').compact_blank!.map!(&:to_sym)
175
244
  end
176
245
 
177
246
  def partial_except_keys
178
- (@request.headers['X-Inertia-Partial-Except'] || '').split(',').compact
247
+ @partial_except_keys ||= (@request.headers['X-Inertia-Partial-Except'] || '').split(',').compact_blank!
179
248
  end
180
249
 
181
250
  def rendering_partial_component?
@@ -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.10.0"
4
+ VERSION = '3.12.0'
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,16 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: inertia_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.10.0
4
+ version: 3.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Knoles
8
8
  - Brandon Shar
9
9
  - Eugene Granovsky
10
- autorequire:
11
10
  bindir: bin
12
11
  cert_chain: []
13
- date: 2025-07-30 00:00:00.000000000 Z
12
+ date: 2025-11-08 00:00:00.000000000 Z
14
13
  dependencies:
15
14
  - !ruby/object:Gem::Dependency
16
15
  name: railties
@@ -230,13 +229,14 @@ files:
230
229
  - lib/inertia_rails/meta_tag_builder.rb
231
230
  - lib/inertia_rails/middleware.rb
232
231
  - lib/inertia_rails/optional_prop.rb
232
+ - lib/inertia_rails/prop_mergeable.rb
233
233
  - lib/inertia_rails/renderer.rb
234
234
  - lib/inertia_rails/rspec.rb
235
+ - lib/inertia_rails/scroll_metadata.rb
236
+ - lib/inertia_rails/scroll_prop.rb
235
237
  - lib/inertia_rails/version.rb
236
238
  - lib/patches/better_errors.rb
237
239
  - lib/patches/debug_exceptions.rb
238
- - lib/patches/debug_exceptions/patch-5-0.rb
239
- - lib/patches/debug_exceptions/patch-5-1.rb
240
240
  - lib/patches/mapper.rb
241
241
  - lib/patches/request.rb
242
242
  - lib/tasks/inertia_rails.rake
@@ -250,7 +250,6 @@ metadata:
250
250
  homepage_uri: https://github.com/inertiajs/inertia-rails
251
251
  source_code_uri: https://github.com/inertiajs/inertia-rails
252
252
  rubygems_mfa_required: 'true'
253
- post_install_message:
254
253
  rdoc_options: []
255
254
  require_paths:
256
255
  - lib
@@ -265,8 +264,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
265
264
  - !ruby/object:Gem::Version
266
265
  version: '0'
267
266
  requirements: []
268
- rubygems_version: 3.5.22
269
- signing_key:
267
+ rubygems_version: 3.6.2
270
268
  specification_version: 4
271
269
  summary: Inertia.js adapter for Rails
272
270
  test_files: []
@@ -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