inertia_rails 3.14.0 → 3.15.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: 8e02de8da9dda43a24dae17c59496938d35eef2caa4bade2543d4336201baa0e
4
- data.tar.gz: ae6952effd0e1144dc78f2c7f2a3ae9ccc0f1b6011000ef9a7ecfe9f8d9166aa
3
+ metadata.gz: 2460d1f62147b4360786f5caba3ed7525d0539da9b61acc65dd9c93f925d7e7b
4
+ data.tar.gz: 741b89dd90fbf8b2fe212ef48ddd1e723ef69d9d13ffa5c35a3cc61d82443743
5
5
  SHA512:
6
- metadata.gz: 5426c306db138b90256fc924f20f6ce5b68583c431f10f49f1acd5eb64c013b9ed1f4d75472dfab8a971df43a9ad4a03a2854f2a1dff5fbbb4d09748b9b92a0d
7
- data.tar.gz: f12bd8e51187679c56a6db6e545687d7ac5c795d20e9d74d5865897b61e9d0308480f4dc06a62fca9d32aa1129041977bdf4be2f69061aaccb3b571972d7d6cc
6
+ metadata.gz: 06a2bca60142ca5d450b4640fe6fe4df5000b15da57b8830c7993563d827b6d9abec3c3cf036d6ca33cf931a745e40ca81a52706598293e0e619bc4ed5bcb207
7
+ data.tar.gz: ee907ff350208d0e415aac873cc38b971d9a5fc8964b96dc502d031acd2af6ac5f6ab848163c2e56e60e3b855a1a7c95dc52fda1abbda5e1959a96cb00b068ac
data/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ 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.15.0] - 2025-12-11
8
+
9
+ * Support for rendering initial page data in a script tag (@skryukov)
10
+ * Once props support (@skryukov)
11
+ * Fix file references in TypeScript templates (@bigmasonwang)
12
+
7
13
  ## [3.14.0] - 2025-11-27
8
14
 
9
15
  Lots of quality of life improvements!
@@ -1 +1 @@
1
- <div id="app" data-page="<%= page.to_json %>"></div>
1
+ <%= inertia_root(page: page) %>
@@ -4,4 +4,5 @@ InertiaRails.configure do |config|
4
4
  config.version = ViteRuby.digest
5
5
  config.encrypt_history = true
6
6
  config.always_include_errors_hash = true
7
+ config.use_script_element_for_initial_page = true
7
8
  end
@@ -34,7 +34,7 @@ export default function InertiaExample(
34
34
  <div className={cs.footer}>
35
35
  <div className={cs.card}>
36
36
  <p>
37
- Edit <code><%= js_destination_path %>/pages/inertia_example/index.jsx</code> and save to test <abbr title="Hot Module Replacement">HMR</abbr>.
37
+ Edit <code><%= js_destination_path %>/pages/inertia_example/index.tsx</code> and save to test <abbr title="Hot Module Replacement">HMR</abbr>.
38
38
  </p>
39
39
  </div>
40
40
 
@@ -44,6 +44,7 @@ createInertiaApp({
44
44
  forceIndicesArrayFormatInFormData: false,
45
45
  },
46
46
  future: {
47
+ useScriptElementForInitialPage: true,
47
48
  useDataInertiaHeadAttribute: true,
48
49
  useDialogForErrorModal: true,
49
50
  preserveEqualProps: true,
@@ -44,6 +44,7 @@ void createInertiaApp({
44
44
  forceIndicesArrayFormatInFormData: false,
45
45
  },
46
46
  future: {
47
+ useScriptElementForInitialPage: true,
47
48
  useDataInertiaHeadAttribute: true,
48
49
  useDialogForErrorModal: true,
49
50
  preserveEqualProps: true,
@@ -42,6 +42,7 @@ createInertiaApp({
42
42
  forceIndicesArrayFormatInFormData: false,
43
43
  },
44
44
  future: {
45
+ useScriptElementForInitialPage: true,
45
46
  useDataInertiaHeadAttribute: true,
46
47
  useDialogForErrorModal: true,
47
48
  preserveEqualProps: true,
@@ -17,7 +17,7 @@
17
17
  <div class="footer">
18
18
  <div class="card">
19
19
  <p>
20
- Edit <code><%= js_destination_path %>/pages/inertia_example/index.ts.vue</code> and save to test <abbr title="Hot Module Replacement">HMR</abbr>.
20
+ Edit <code><%= js_destination_path %>/pages/inertia_example/index.vue</code> and save to test <abbr title="Hot Module Replacement">HMR</abbr>.
21
21
  </p>
22
22
  </div>
23
23
 
@@ -41,6 +41,7 @@ createInertiaApp({
41
41
  forceIndicesArrayFormatInFormData: false,
42
42
  },
43
43
  future: {
44
+ useScriptElementForInitialPage: true,
44
45
  useDataInertiaHeadAttribute: true,
45
46
  useDialogForErrorModal: true,
46
47
  preserveEqualProps: true,
@@ -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,12 @@ 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',
37
43
  }.freeze
38
44
 
39
45
  OPTION_NAMES = DEFAULTS.keys.freeze
@@ -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'
@@ -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'
@@ -36,6 +38,10 @@ module InertiaRails
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)
@@ -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
@@ -128,6 +130,9 @@ module InertiaRails
128
130
  @page[:scrollProps] = scroll_props if scroll_props.present?
129
131
  @page.merge!(resolve_merge_props)
130
132
 
133
+ once_props = resolve_once_props
134
+ @page[:onceProps] = once_props if once_props.present?
135
+
131
136
  @page
132
137
  end
133
138
 
@@ -173,6 +178,17 @@ module InertiaRails
173
178
  }.delete_if { |_, v| v.blank? }
174
179
  end
175
180
 
181
+ def resolve_once_props
182
+ @props.each_with_object({}) do |(key, prop), result|
183
+ next unless prop.try(:once?)
184
+ next if excluded_by_partial_request?([key.to_s])
185
+
186
+ once_key = (prop.once_key || key).to_s
187
+
188
+ result[once_key] = { prop: key.to_s, expiresAt: prop.expires_at }.compact
189
+ end
190
+ end
191
+
176
192
  def resolve_match_on_props
177
193
  all_merge_props.filter_map do |key, prop|
178
194
  prop.match_on.map! { |ms| "#{key}.#{ms}" } if prop.match_on.present?
@@ -251,6 +267,10 @@ module InertiaRails
251
267
  @partial_except_keys ||= (@request.headers['X-Inertia-Partial-Except'] || '').split(',').compact_blank!
252
268
  end
253
269
 
270
+ def except_once_keys
271
+ @except_once_keys ||= (@request.headers['X-Inertia-Except-Once-Props'] || '').split(',').compact_blank!
272
+ end
273
+
254
274
  def rendering_partial_component?
255
275
  @request.headers['X-Inertia-Partial-Component'] == @component
256
276
  end
@@ -265,12 +285,8 @@ module InertiaRails
265
285
 
266
286
  def keep_prop?(prop, path)
267
287
  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
288
+ return false if excluded_by_once_cache?(prop, path)
289
+ return false if excluded_by_partial_request?(path)
274
290
 
275
291
  # Precedence: Evaluate IgnoreOnFirstLoadProp only after partial keys have been checked
276
292
  return false if prop.is_a?(IgnoreOnFirstLoadProp) && !rendering_partial_component?
@@ -278,6 +294,29 @@ module InertiaRails
278
294
  true
279
295
  end
280
296
 
297
+ def excluded_by_once_cache?(prop, path)
298
+ return false unless prop.try(:once?)
299
+ return false if prop.try(:fresh?)
300
+ return false if explicitly_requested?(path)
301
+
302
+ once_key = (prop.once_key || path.join('.')).to_s
303
+ except_once_keys.include?(once_key)
304
+ end
305
+
306
+ def explicitly_requested?(path)
307
+ return false unless rendering_partial_component? && partial_keys.present?
308
+
309
+ path_with_prefixes = path_prefixes(path)
310
+ (path_with_prefixes & partial_keys).any?
311
+ end
312
+
313
+ def excluded_by_partial_request?(path)
314
+ return false unless rendering_partial_component? && (partial_keys.present? || partial_except_keys.present?)
315
+
316
+ path_with_prefixes = path_prefixes(path)
317
+ excluded_by_only_partial_keys?(path_with_prefixes) || excluded_by_except_partial_keys?(path_with_prefixes)
318
+ end
319
+
281
320
  def path_prefixes(parts)
282
321
  (0...parts.length).map do |i|
283
322
  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.15.0'
5
5
  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.14.0
4
+ version: 3.15.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-12 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: railties
@@ -200,8 +200,10 @@ files:
200
200
  - lib/inertia_rails/meta_tag.rb
201
201
  - lib/inertia_rails/meta_tag_builder.rb
202
202
  - lib/inertia_rails/middleware.rb
203
+ - lib/inertia_rails/once_prop.rb
203
204
  - lib/inertia_rails/optional_prop.rb
204
205
  - lib/inertia_rails/prop_mergeable.rb
206
+ - lib/inertia_rails/prop_onceable.rb
205
207
  - lib/inertia_rails/renderer.rb
206
208
  - lib/inertia_rails/rspec.rb
207
209
  - lib/inertia_rails/scroll_metadata.rb