inertia_rails 3.14.0 → 3.16.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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/app/views/inertia.html.erb +1 -1
  4. data/lib/generators/inertia/install/helpers.rb +9 -0
  5. data/lib/generators/inertia/install/install_generator.rb +11 -26
  6. data/lib/generators/inertia/install/templates/inertia_controller.rb +3 -1
  7. data/lib/generators/inertia/install/templates/initializer.rb +1 -0
  8. data/lib/generators/inertia/install/templates/react/InertiaExample.tsx +1 -1
  9. data/lib/generators/inertia/install/templates/react/inertia.jsx +1 -0
  10. data/lib/generators/inertia/install/templates/react/inertia.tsx +1 -0
  11. data/lib/generators/inertia/install/templates/react/types/globals.d.ts +2 -1
  12. data/lib/generators/inertia/install/templates/react/types/index.ts +2 -4
  13. data/lib/generators/inertia/install/templates/svelte/inertia.ts +1 -0
  14. data/lib/generators/inertia/install/templates/svelte/types/globals.d.ts +2 -1
  15. data/lib/generators/inertia/install/templates/svelte/types/index.ts +2 -4
  16. data/lib/generators/inertia/install/templates/vue/InertiaExample.ts.vue +1 -1
  17. data/lib/generators/inertia/install/templates/vue/inertia.ts +1 -0
  18. data/lib/generators/inertia/install/templates/vue/types/globals.d.ts +2 -1
  19. data/lib/generators/inertia/install/templates/vue/types/index.ts +2 -4
  20. data/lib/generators/inertia/scaffold_controller/templates/controller.rb.tt +0 -4
  21. data/lib/generators/inertia_templates/scaffold/templates/react/index.jsx.tt +4 -2
  22. data/lib/generators/inertia_templates/scaffold/templates/react/index.tsx.tt +4 -3
  23. data/lib/generators/inertia_templates/scaffold/templates/react/show.jsx.tt +4 -2
  24. data/lib/generators/inertia_templates/scaffold/templates/react/show.tsx.tt +4 -3
  25. data/lib/generators/inertia_templates/scaffold/templates/svelte/index.svelte.tt +4 -4
  26. data/lib/generators/inertia_templates/scaffold/templates/svelte/index.ts.svelte.tt +4 -5
  27. data/lib/generators/inertia_templates/scaffold/templates/svelte/show.svelte.tt +4 -4
  28. data/lib/generators/inertia_templates/scaffold/templates/svelte/show.ts.svelte.tt +4 -5
  29. data/lib/generators/inertia_templates/scaffold/templates/vue/index.ts.vue.tt +3 -3
  30. data/lib/generators/inertia_templates/scaffold/templates/vue/index.vue.tt +3 -2
  31. data/lib/generators/inertia_templates/scaffold/templates/vue/show.ts.vue.tt +3 -3
  32. data/lib/generators/inertia_templates/scaffold/templates/vue/show.vue.tt +3 -2
  33. data/lib/generators/inertia_tw_templates/scaffold/templates/react/edit.jsx.tt +4 -4
  34. data/lib/generators/inertia_tw_templates/scaffold/templates/react/edit.tsx.tt +4 -4
  35. data/lib/generators/inertia_tw_templates/scaffold/templates/react/index.jsx.tt +4 -2
  36. data/lib/generators/inertia_tw_templates/scaffold/templates/react/index.tsx.tt +4 -3
  37. data/lib/generators/inertia_tw_templates/scaffold/templates/react/show.jsx.tt +4 -2
  38. data/lib/generators/inertia_tw_templates/scaffold/templates/react/show.tsx.tt +4 -3
  39. data/lib/generators/inertia_tw_templates/scaffold/templates/svelte/index.svelte.tt +4 -4
  40. data/lib/generators/inertia_tw_templates/scaffold/templates/svelte/index.ts.svelte.tt +4 -5
  41. data/lib/generators/inertia_tw_templates/scaffold/templates/svelte/show.svelte.tt +4 -4
  42. data/lib/generators/inertia_tw_templates/scaffold/templates/svelte/show.ts.svelte.tt +4 -5
  43. data/lib/generators/inertia_tw_templates/scaffold/templates/vue/index.ts.vue.tt +3 -3
  44. data/lib/generators/inertia_tw_templates/scaffold/templates/vue/index.vue.tt +3 -2
  45. data/lib/generators/inertia_tw_templates/scaffold/templates/vue/show.ts.vue.tt +3 -3
  46. data/lib/generators/inertia_tw_templates/scaffold/templates/vue/show.vue.tt +3 -2
  47. data/lib/inertia_rails/base_prop.rb +1 -1
  48. data/lib/inertia_rails/configuration.rb +10 -0
  49. data/lib/inertia_rails/controller.rb +12 -1
  50. data/lib/inertia_rails/defer_prop.rb +1 -0
  51. data/lib/inertia_rails/engine.rb +6 -0
  52. data/lib/inertia_rails/flash_extension.rb +63 -0
  53. data/lib/inertia_rails/generators/helper.rb +0 -8
  54. data/lib/inertia_rails/helper.rb +14 -0
  55. data/lib/inertia_rails/inertia_rails.rb +10 -4
  56. data/lib/inertia_rails/merge_prop.rb +1 -0
  57. data/lib/inertia_rails/middleware.rb +1 -1
  58. data/lib/inertia_rails/once_prop.rb +12 -0
  59. data/lib/inertia_rails/optional_prop.rb +1 -0
  60. data/lib/inertia_rails/prop_onceable.rb +39 -0
  61. data/lib/inertia_rails/renderer.rb +55 -13
  62. data/lib/inertia_rails/version.rb +1 -1
  63. data/lib/patches/debug_exceptions.rb +8 -4
  64. metadata +5 -2
@@ -1,11 +1,10 @@
1
1
  <script lang="ts">
2
- import { Link } from '@inertiajs/svelte'
2
+ import { Link, page } from '@inertiajs/svelte'
3
3
  import <%= inertia_component_name %> from './<%= singular_name %>.svelte'
4
4
  import type { <%= inertia_model_type %> } from './types'
5
5
 
6
- let { <%= plural_table_name %>, flash } = $props<{
6
+ let { <%= plural_table_name %> } = $props<{
7
7
  <%= plural_table_name %>: <%= inertia_model_type %>[]
8
- flash: { notice?: string }
9
8
  }>()
10
9
  </script>
11
10
 
@@ -14,9 +13,9 @@
14
13
  </svelte:head>
15
14
 
16
15
  <div class="mx-auto md:w-2/3 w-full px-8 pt-8">
17
- {#if flash.notice}
16
+ {#if $page.flash.notice}
18
17
  <p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block">
19
- {flash.notice}
18
+ {$page.flash.notice}
20
19
  </p>
21
20
  {/if}
22
21
 
@@ -1,8 +1,8 @@
1
1
  <script>
2
- import { Link } from '@inertiajs/svelte'
2
+ import { Link, page } from '@inertiajs/svelte'
3
3
  import <%= inertia_component_name %> from './<%= singular_name %>.svelte'
4
4
 
5
- let { <%= singular_table_name %>, flash } = $props()
5
+ let { <%= singular_table_name %> } = $props()
6
6
  </script>
7
7
 
8
8
  <svelte:head>
@@ -11,9 +11,9 @@
11
11
 
12
12
  <div class="mx-auto md:w-2/3 w-full px-8 pt-8">
13
13
  <div class="mx-auto">
14
- {#if flash.notice}
14
+ {#if $page.flash.notice}
15
15
  <p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block">
16
- {flash.notice}
16
+ {$page.flash.notice}
17
17
  </p>
18
18
  {/if}
19
19
 
@@ -1,11 +1,10 @@
1
1
  <script lang="ts">
2
- import { Link } from '@inertiajs/svelte'
2
+ import { Link, page } from '@inertiajs/svelte'
3
3
  import <%= inertia_component_name %> from './<%= singular_name %>.svelte'
4
4
  import type { <%= inertia_model_type %> } from './types'
5
5
 
6
- let { <%= singular_table_name %>, flash } = $props<{
6
+ let { <%= singular_table_name %> } = $props<{
7
7
  <%= singular_table_name %>: <%= inertia_model_type %>
8
- flash: { notice?: string }
9
8
  }>()
10
9
  </script>
11
10
 
@@ -15,9 +14,9 @@
15
14
 
16
15
  <div class="mx-auto md:w-2/3 w-full px-8 pt-8">
17
16
  <div class="mx-auto">
18
- {#if flash.notice}
17
+ {#if $page.flash.notice}
19
18
  <p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block">
20
- {flash.notice}
19
+ {$page.flash.notice}
21
20
  </p>
22
21
  {/if}
23
22
 
@@ -36,12 +36,12 @@
36
36
  </template>
37
37
 
38
38
  <script setup lang="ts">
39
- import { Head, Link } from '@inertiajs/vue3'
39
+ import { Head, Link, usePage } from '@inertiajs/vue3'
40
40
  import <%= inertia_component_name %> from './<%= singular_name %>.vue'
41
41
  import { <%= inertia_model_type %> } from './types'
42
42
 
43
- const { <%= plural_table_name %>, flash } = defineProps<{
43
+ defineProps<{
44
44
  <%= plural_table_name %>: <%= inertia_model_type %>[]
45
- flash: { notice?: string }
46
45
  }>()
46
+ const { flash } = usePage()
47
47
  </script>
@@ -36,8 +36,9 @@
36
36
  </template>
37
37
 
38
38
  <script setup>
39
- import { Head, Link } from '@inertiajs/vue3'
39
+ import { Head, Link, usePage } from '@inertiajs/vue3'
40
40
  import <%= inertia_component_name %> from './<%= singular_name %>.vue'
41
41
 
42
- const { <%= plural_table_name %>, flash } = defineProps(['<%= plural_table_name %>', 'flash'])
42
+ defineProps(['<%= plural_table_name %>'])
43
+ const { flash } = usePage()
43
44
  </script>
@@ -42,12 +42,12 @@
42
42
  </template>
43
43
 
44
44
  <script setup lang="ts">
45
- import { Head, Link } from '@inertiajs/vue3'
45
+ import { Head, Link, usePage } from '@inertiajs/vue3'
46
46
  import <%= inertia_component_name %> from './<%= singular_name %>.vue'
47
47
  import { <%= inertia_model_type %> } from './types'
48
48
 
49
- const { <%= singular_table_name %>, flash } = defineProps<{
49
+ defineProps<{
50
50
  <%= singular_table_name %>: <%= inertia_model_type %>
51
- flash: { notice?: string }
52
51
  }>()
52
+ const { flash } = usePage()
53
53
  </script>
@@ -42,8 +42,9 @@
42
42
  </template>
43
43
 
44
44
  <script setup>
45
- import { Head, Link } from '@inertiajs/vue3'
45
+ import { Head, Link, usePage } from '@inertiajs/vue3'
46
46
  import <%= inertia_component_name %> from './<%= singular_name %>.vue'
47
47
 
48
- const { <%= singular_table_name %>, flash } = defineProps(['<%= singular_table_name %>', 'flash'])
48
+ defineProps(['<%= singular_table_name %>'])
49
+ const { flash } = usePage()
49
50
  </script>
@@ -3,7 +3,7 @@
3
3
  module InertiaRails
4
4
  # Base class for all props.
5
5
  class BaseProp
6
- def initialize(&block)
6
+ def initialize(**, &block)
7
7
  @block = block
8
8
  end
9
9
 
@@ -34,6 +34,16 @@ module InertiaRails
34
34
 
35
35
  # Whether to include empty `errors` hash to the props when no errors are present.
36
36
  always_include_errors_hash: nil,
37
+
38
+ # Whether to use `<script>` element for initial page rendering instead of the `data-page` attribute.
39
+ use_script_element_for_initial_page: false,
40
+
41
+ # DOM id to use for the root Inertia.js element.
42
+ root_dom_id: 'app',
43
+
44
+ # Flash keys from Rails flash to expose to frontend.
45
+ # Set to nil to disable Rails flash integration (use only flash.inertia).
46
+ flash_keys: %i[notice alert].freeze,
37
47
  }.freeze
38
48
 
39
49
  OPTION_NAMES = DEFAULTS.keys.freeze
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'inertia_rails'
4
+ require_relative 'flash_extension'
4
5
  require_relative 'helper'
5
6
  require_relative 'action_filter'
6
7
  require_relative 'meta_tag_builder'
@@ -180,6 +181,17 @@ module InertiaRails
180
181
  head :conflict
181
182
  end
182
183
 
184
+ def inertia_collect_flash_data
185
+ flash_data = flash.to_hash
186
+
187
+ allowed_keys = inertia_configuration.flash_keys
188
+ result = allowed_keys ? flash_data.slice(*allowed_keys.map(&:to_s)) : {}
189
+
190
+ result.merge!(flash_data['inertia'].transform_keys(&:to_s)) if flash_data['inertia'].is_a?(Hash)
191
+
192
+ result.symbolize_keys
193
+ end
194
+
183
195
  def capture_inertia_session_options(options)
184
196
  return unless (inertia = options[:inertia])
185
197
 
@@ -192,7 +204,6 @@ module InertiaRails
192
204
  )
193
205
  session[:inertia_errors] = inertia_errors
194
206
  end
195
-
196
207
  end
197
208
 
198
209
  session[:inertia_clear_history] = inertia[:clear_history] if inertia[:clear_history]
@@ -2,6 +2,7 @@
2
2
 
3
3
  module InertiaRails
4
4
  class DeferProp < IgnoreOnFirstLoadProp
5
+ prepend PropOnceable
5
6
  prepend PropMergeable
6
7
 
7
8
  DEFAULT_GROUP = 'default'
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'middleware'
4
4
  require_relative 'controller'
5
+ require_relative 'flash_extension'
5
6
 
6
7
  module InertiaRails
7
8
  class Engine < ::Rails::Engine
@@ -14,5 +15,10 @@ module InertiaRails
14
15
  include ::InertiaRails::Controller
15
16
  end
16
17
  end
18
+
19
+ initializer 'inertia_rails.flash_extension' do
20
+ ActionDispatch::Flash::FlashHash.prepend ::InertiaRails::FlashExtension
21
+ ActionDispatch::Flash::FlashNow.prepend ::InertiaRails::FlashExtension
22
+ end
17
23
  end
18
24
  end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InertiaRails
4
+ # Provides a scoped interface for Inertia flash data within Rails' flash.
5
+ # Uses native hash storage: flash[:inertia] = { key: value }
6
+ # Tracks .now keys separately in @inertia_now_keys for session filtering.
7
+ module FlashExtension
8
+ INERTIA_KEY = 'inertia'
9
+
10
+ def inertia
11
+ @inertia ||= InertiaFlashScope.new(self)
12
+ end
13
+
14
+ # Keys set via flash.now.inertia that should not persist to session
15
+ def inertia_now_keys
16
+ @inertia_now_keys ||= Set.new
17
+ end
18
+
19
+ # Clear .now tracking when user explicitly keeps :inertia or all flash
20
+ def keep(key = nil)
21
+ @inertia_now_keys&.clear if key.nil? || key.to_s == INERTIA_KEY
22
+ super
23
+ end
24
+
25
+ # Override to filter .now keys from nested inertia hash before session persistence
26
+ def to_session_value
27
+ inertia_hash = self[INERTIA_KEY]
28
+ if inertia_hash.is_a?(Hash) && @inertia_now_keys&.any?
29
+ @inertia_now_keys.each { |k| inertia_hash.delete(k.to_s) }
30
+ delete(INERTIA_KEY) if inertia_hash.empty?
31
+ end
32
+
33
+ super
34
+ end
35
+
36
+ class InertiaFlashScope
37
+ def initialize(flash_or_now)
38
+ if flash_or_now.respond_to?(:flash)
39
+ @flash = flash_or_now.flash
40
+ @now = true
41
+ else
42
+ @flash = flash_or_now
43
+ @now = false
44
+ end
45
+ end
46
+
47
+ def []=(key, value)
48
+ @flash[INERTIA_KEY] ||= {}
49
+ @flash[INERTIA_KEY][key.to_s] = value
50
+ @flash.inertia_now_keys.add(key.to_s) if @now
51
+ end
52
+
53
+ def [](key)
54
+ @flash[INERTIA_KEY]&.[](key.to_s)
55
+ end
56
+
57
+ def to_hash
58
+ @flash[INERTIA_KEY]&.dup || {}
59
+ end
60
+ alias to_h to_hash
61
+ end
62
+ end
63
+ end
@@ -87,14 +87,6 @@ module InertiaRails
87
87
  route_url
88
88
  end
89
89
 
90
- def inertia_js_version
91
- @inertia_js_version ||= Gem::Version.new(
92
- JSON.parse(`npm ls @inertiajs/core --json`).then do |json|
93
- json['dependencies'].values.first['version']
94
- end
95
- )
96
- end
97
-
98
90
  def ts_type(attribute)
99
91
  case attribute.type
100
92
  when :float, :decimal, :integer
@@ -32,5 +32,19 @@ module InertiaRails
32
32
 
33
33
  safe_join(meta_tags, "\n")
34
34
  end
35
+
36
+ def inertia_root(id: nil, page: inertia_page)
37
+ config = controller.send(:inertia_configuration)
38
+ id ||= config.root_dom_id
39
+
40
+ if config.use_script_element_for_initial_page
41
+ safe_join([
42
+ tag.script(page.to_json.html_safe, 'data-page': id, type: 'application/json'),
43
+ tag.div(id: id)
44
+ ], "\n")
45
+ else
46
+ tag.div(id: id, 'data-page': page.to_json)
47
+ end
48
+ end
35
49
  end
36
50
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'inertia_rails/prop_onceable'
3
4
  require 'inertia_rails/prop_mergeable'
4
5
  require 'inertia_rails/base_prop'
5
6
  require 'inertia_rails/ignore_on_first_load_prop'
@@ -8,6 +9,7 @@ require 'inertia_rails/lazy_prop'
8
9
  require 'inertia_rails/optional_prop'
9
10
  require 'inertia_rails/defer_prop'
10
11
  require 'inertia_rails/merge_prop'
12
+ require 'inertia_rails/once_prop'
11
13
  require 'inertia_rails/scroll_prop'
12
14
  require 'inertia_rails/configuration'
13
15
  require 'inertia_rails/meta_tag'
@@ -28,14 +30,18 @@ module InertiaRails
28
30
  LazyProp.new(value, &block)
29
31
  end
30
32
 
31
- def optional(&block)
32
- OptionalProp.new(&block)
33
+ def optional(...)
34
+ OptionalProp.new(...)
33
35
  end
34
36
 
35
37
  def always(&block)
36
38
  AlwaysProp.new(&block)
37
39
  end
38
40
 
41
+ def once(...)
42
+ OnceProp.new(...)
43
+ end
44
+
39
45
  def merge(...)
40
46
  MergeProp.new(...)
41
47
  end
@@ -44,8 +50,8 @@ module InertiaRails
44
50
  MergeProp.new(deep_merge: true, match_on: match_on, &block)
45
51
  end
46
52
 
47
- def defer(group: nil, merge: nil, deep_merge: nil, match_on: nil, &block)
48
- DeferProp.new(group: group, merge: merge, deep_merge: deep_merge, match_on: match_on, &block)
53
+ def defer(...)
54
+ DeferProp.new(...)
49
55
  end
50
56
 
51
57
  def scroll(metadata = nil, **options, &block)
@@ -2,6 +2,7 @@
2
2
 
3
3
  module InertiaRails
4
4
  class MergeProp < BaseProp
5
+ prepend PropOnceable
5
6
  prepend PropMergeable
6
7
 
7
8
  def initialize(**_props, &block)
@@ -21,7 +21,7 @@ module InertiaRails
21
21
  status, headers, body = @app.call(@env)
22
22
  request = ActionDispatch::Request.new(@env)
23
23
 
24
- # Inertia errors are added to the session via redirect_to
24
+ # Inertia session data is added via redirect_to
25
25
  unless keep_inertia_session_options?(status)
26
26
  request.session.delete(:inertia_errors)
27
27
  request.session.delete(:inertia_clear_history)
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InertiaRails
4
+ class OnceProp < BaseProp
5
+ prepend PropOnceable
6
+
7
+ def initialize(**, &block)
8
+ @once = true
9
+ super(&block)
10
+ end
11
+ end
12
+ end
@@ -2,5 +2,6 @@
2
2
 
3
3
  module InertiaRails
4
4
  class OptionalProp < IgnoreOnFirstLoadProp
5
+ prepend PropOnceable
5
6
  end
6
7
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InertiaRails
4
+ module PropOnceable
5
+ attr_reader :once_key, :once_expires_in
6
+
7
+ def initialize(**props, &block)
8
+ @once = props.fetch(:once, false)
9
+ @once_key = props[:key]
10
+ @once_expires_in = props[:expires_in]
11
+ @fresh = props.fetch(:fresh, false)
12
+
13
+ super
14
+ end
15
+
16
+ def once?
17
+ @once
18
+ end
19
+
20
+ def fresh?
21
+ @fresh
22
+ end
23
+
24
+ def expires_at
25
+ return nil unless @once_expires_in
26
+
27
+ timestamp = case @once_expires_in
28
+ when ActiveSupport::Duration
29
+ (Time.current + @once_expires_in).to_f
30
+ when Numeric
31
+ Time.current.to_f + @once_expires_in
32
+ else
33
+ raise ArgumentError, "Invalid `expires_in` value: #{@once_expires_in.inspect}"
34
+ end
35
+
36
+ (timestamp * 1000).to_i
37
+ end
38
+ end
39
+ end
@@ -48,7 +48,7 @@ module InertiaRails
48
48
  else
49
49
  "#{@response.headers['Vary']}, X-Inertia"
50
50
  end
51
- if @request.headers['X-Inertia']
51
+ if @request.inertia?
52
52
  @response.set_header('X-Inertia', 'true')
53
53
  @render_method.call json: page.to_json, status: @response.status, content_type: Mime[:json]
54
54
  else
@@ -97,15 +97,17 @@ module InertiaRails
97
97
  def computed_props
98
98
  # rubocop:disable Style/MultilineBlockChain
99
99
  @props
100
- .tap do |merged_props| # Always keep errors in the props
101
- if merged_props.key?(:errors) && !merged_props[:errors].is_a?(BaseProp)
102
- errors = merged_props[:errors]
103
- merged_props[:errors] = InertiaRails.always { errors }
104
- end
100
+ .tap do |merged_props|
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
105
  end
106
+ end
106
107
  .then { |props| deep_transform_props(props) } # Internal hydration/filtering
107
108
  .then { |props| @configuration.prop_transformer(props: props) } # Apply user-defined prop transformer
108
- .tap do |props| # Add meta tags last (never transformed)
109
+ .tap do |props|
110
+ # Add meta tags last (never transformed)
109
111
  props[:_inertia_meta] = meta_tags if meta_tags.present?
110
112
  end
111
113
  # rubocop:enable Style/MultilineBlockChain
@@ -123,11 +125,17 @@ module InertiaRails
123
125
  clearHistory: @clear_history,
124
126
  }
125
127
 
128
+ flash_data = @controller.__send__(:inertia_collect_flash_data)
129
+ @page[:flash] = flash_data if flash_data.present?
130
+
126
131
  deferred_props = deferred_props_keys
127
132
  @page[:deferredProps] = deferred_props if deferred_props.present?
128
133
  @page[:scrollProps] = scroll_props if scroll_props.present?
129
134
  @page.merge!(resolve_merge_props)
130
135
 
136
+ once_props = resolve_once_props
137
+ @page[:onceProps] = once_props if once_props.present?
138
+
131
139
  @page
132
140
  end
133
141
 
@@ -173,6 +181,17 @@ module InertiaRails
173
181
  }.delete_if { |_, v| v.blank? }
174
182
  end
175
183
 
184
+ def resolve_once_props
185
+ @props.each_with_object({}) do |(key, prop), result|
186
+ next unless prop.try(:once?)
187
+ next if excluded_by_partial_request?([key.to_s])
188
+
189
+ once_key = (prop.once_key || key).to_s
190
+
191
+ result[once_key] = { prop: key.to_s, expiresAt: prop.expires_at }.compact
192
+ end
193
+ end
194
+
176
195
  def resolve_match_on_props
177
196
  all_merge_props.filter_map do |key, prop|
178
197
  prop.match_on.map! { |ms| "#{key}.#{ms}" } if prop.match_on.present?
@@ -251,6 +270,10 @@ module InertiaRails
251
270
  @partial_except_keys ||= (@request.headers['X-Inertia-Partial-Except'] || '').split(',').compact_blank!
252
271
  end
253
272
 
273
+ def except_once_keys
274
+ @except_once_keys ||= (@request.headers['X-Inertia-Except-Once-Props'] || '').split(',').compact_blank!
275
+ end
276
+
254
277
  def rendering_partial_component?
255
278
  @request.headers['X-Inertia-Partial-Component'] == @component
256
279
  end
@@ -265,12 +288,8 @@ module InertiaRails
265
288
 
266
289
  def keep_prop?(prop, path)
267
290
  return true if prop.is_a?(AlwaysProp)
268
-
269
- if rendering_partial_component? && (partial_keys.present? || partial_except_keys.present?)
270
- path_with_prefixes = path_prefixes(path)
271
- return false if excluded_by_only_partial_keys?(path_with_prefixes)
272
- return false if excluded_by_except_partial_keys?(path_with_prefixes)
273
- end
291
+ return false if excluded_by_once_cache?(prop, path)
292
+ return false if excluded_by_partial_request?(path)
274
293
 
275
294
  # Precedence: Evaluate IgnoreOnFirstLoadProp only after partial keys have been checked
276
295
  return false if prop.is_a?(IgnoreOnFirstLoadProp) && !rendering_partial_component?
@@ -278,6 +297,29 @@ module InertiaRails
278
297
  true
279
298
  end
280
299
 
300
+ def excluded_by_once_cache?(prop, path)
301
+ return false unless prop.try(:once?)
302
+ return false if prop.try(:fresh?)
303
+ return false if explicitly_requested?(path)
304
+
305
+ once_key = (prop.once_key || path.join('.')).to_s
306
+ except_once_keys.include?(once_key)
307
+ end
308
+
309
+ def explicitly_requested?(path)
310
+ return false unless rendering_partial_component? && partial_keys.present?
311
+
312
+ path_with_prefixes = path_prefixes(path)
313
+ (path_with_prefixes & partial_keys).any?
314
+ end
315
+
316
+ def excluded_by_partial_request?(path)
317
+ return false unless rendering_partial_component? && (partial_keys.present? || partial_except_keys.present?)
318
+
319
+ path_with_prefixes = path_prefixes(path)
320
+ excluded_by_only_partial_keys?(path_with_prefixes) || excluded_by_except_partial_keys?(path_with_prefixes)
321
+ end
322
+
281
323
  def path_prefixes(parts)
282
324
  (0...parts.length).map do |i|
283
325
  parts[0..i].join('.')
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module InertiaRails
4
- VERSION = '3.14.0'
4
+ VERSION = '3.16.0'
5
5
  end
@@ -5,18 +5,22 @@
5
5
  # The original source needs to be patched, so that Inertia requests are
6
6
  # NOT responded with plain text, but with HTML.
7
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
8
+ # Original source:
10
9
  # https://github.com/rails/rails/blob/8-0-stable/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
10
+ # https://github.com/rails/rails/blob/main/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
11
11
  #
12
12
 
13
13
  module InertiaRails
14
14
  module InertiaDebugExceptions
15
- def render_for_browser_request(request, wrapper)
15
+ # Rails 8.2+ passes content_type as third argument
16
+ def render_for_browser_request(request, wrapper, content_type = nil)
16
17
  template = create_template(request, wrapper)
17
18
  file = "rescues/#{wrapper.rescue_template}"
18
19
 
19
- if request.xhr? && !request.headers['X-Inertia'] # <<<< this line is changed only
20
+ if content_type == Mime[:md]
21
+ body = template.render(template: file, layout: false, formats: [:text])
22
+ format = 'text/markdown'
23
+ elsif request.xhr? && !request.headers['X-Inertia']
20
24
  body = template.render(template: file, layout: false, formats: [:text])
21
25
  format = 'text/plain'
22
26
  else
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.14.0
4
+ version: 3.16.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: 2025-11-27 00:00:00.000000000 Z
12
+ date: 2025-12-30 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: railties
@@ -189,6 +189,7 @@ files:
189
189
  - lib/inertia_rails/controller.rb
190
190
  - lib/inertia_rails/defer_prop.rb
191
191
  - lib/inertia_rails/engine.rb
192
+ - lib/inertia_rails/flash_extension.rb
192
193
  - lib/inertia_rails/generators/controller_template_base.rb
193
194
  - lib/inertia_rails/generators/helper.rb
194
195
  - lib/inertia_rails/generators/scaffold_template_base.rb
@@ -200,8 +201,10 @@ files:
200
201
  - lib/inertia_rails/meta_tag.rb
201
202
  - lib/inertia_rails/meta_tag_builder.rb
202
203
  - lib/inertia_rails/middleware.rb
204
+ - lib/inertia_rails/once_prop.rb
203
205
  - lib/inertia_rails/optional_prop.rb
204
206
  - lib/inertia_rails/prop_mergeable.rb
207
+ - lib/inertia_rails/prop_onceable.rb
205
208
  - lib/inertia_rails/renderer.rb
206
209
  - lib/inertia_rails/rspec.rb
207
210
  - lib/inertia_rails/scroll_metadata.rb