inertia_rails 3.9.0 → 3.10.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: 4b7d385cab4b5bdd84cc50c1c9e22d838ce8f77fc680542ba1b0a11d17c95674
4
- data.tar.gz: 472113f1732205c105f10a069070fb1c6b226ffdf176e57fb15fc66cf7a0bdd2
3
+ metadata.gz: a8109050832c842cdc65c514f09a7ab02ce79a0925517fcb1f34eceee8d54130
4
+ data.tar.gz: 6ce4aaf2b8d4d2615f4e53220a23d712c97c482a2705c70a9423a1ce51b6d1ca
5
5
  SHA512:
6
- metadata.gz: b18d7f00080810908560268ad97da13079b3c89d7fee8aee3b4edcb45d011aae7d16fdff8f9697b953fa7e4e76034eb9d9e9afa1cf79ca6f946cb7810222b83e
7
- data.tar.gz: 80805ac8753cdc1469de52a6f92fceb186e735070d1cc81b44ce93fc62218bea6fc7e515a2950a06f5fb1951aaa6dc2fc6fd40c6b958e5b453be6a2470c3103d
6
+ metadata.gz: 32e92a9898314ff42c67450acc691513cc7e833eff2d15999bb4d4d642b62a7c1f94bcdff3093a79448f95adda2072d0d2e18d7250f9a8dfcb0a57cbcc3fa633
7
+ data.tar.gz: 147352323994ef1bc475aee16990581ba6714769ac157f144c5f1bb296b6058a4bcdf9d5f00fef47cfa26a488b91878aea1e046e7bb9b70e74cda4e8a61ef31c
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.10.0] - 2025-07-30
8
+
9
+ * llms.txt in docs (@brandonshar and @skryukov)
10
+ * Add support for deep merging merge props (@skryukov)
11
+ * Server managed meta tags (@bknoles and @skryukov)
12
+
7
13
  ## [3.9.0] - 2025-06-18
8
14
 
9
15
  * Docs updates
@@ -1,6 +1,7 @@
1
1
  require_relative "inertia_rails"
2
2
  require_relative "helper"
3
3
  require_relative "action_filter"
4
+ require_relative "meta_tag_builder"
4
5
 
5
6
  module InertiaRails
6
7
  module Controller
@@ -127,6 +128,10 @@ module InertiaRails
127
128
  super
128
129
  end
129
130
 
131
+ def inertia_meta
132
+ @inertia_meta ||= InertiaRails::MetaTagBuilder.new(self)
133
+ end
134
+
130
135
  private
131
136
 
132
137
  def inertia_view_assigns
@@ -4,9 +4,9 @@ module InertiaRails
4
4
  class DeferProp < IgnoreOnFirstLoadProp
5
5
  DEFAULT_GROUP = 'default'
6
6
 
7
- attr_reader :group
7
+ attr_reader :group, :match_on
8
8
 
9
- def initialize(group: nil, merge: nil, deep_merge: nil, &block)
9
+ def initialize(group: nil, merge: nil, deep_merge: nil, match_on: nil, &block)
10
10
  raise ArgumentError, 'Cannot set both `deep_merge` and `merge` to true' if deep_merge && merge
11
11
 
12
12
  super(&block)
@@ -14,6 +14,7 @@ module InertiaRails
14
14
  @group = group || DEFAULT_GROUP
15
15
  @merge = merge || deep_merge
16
16
  @deep_merge = deep_merge
17
+ @match_on = match_on.nil? ? nil : Array(match_on)
17
18
  end
18
19
 
19
20
  def merge?
@@ -7,11 +7,12 @@ module InertiaRails
7
7
  module Generators
8
8
  class ControllerTemplateBase < Rails::Generators::NamedBase
9
9
  include Helper
10
+
10
11
  class_option :frontend_framework, required: true, desc: 'Frontend framework to generate the views for.',
11
12
  default: Helper.guess_the_default_framework
12
13
 
13
14
  class_option :typescript, type: :boolean, desc: 'Whether to use TypeScript',
14
- default: Helper.guess_typescript
15
+ default: Helper.uses_typescript?
15
16
 
16
17
  argument :actions, type: :array, default: [], banner: 'action action'
17
18
 
@@ -23,7 +23,7 @@ module InertiaRails
23
23
  end
24
24
  end
25
25
 
26
- def guess_typescript
26
+ def uses_typescript?
27
27
  Rails.root.join('tsconfig.json').exist?
28
28
  end
29
29
 
@@ -15,4 +15,18 @@ module InertiaRails::Helper
15
15
  def inertia_rendering?
16
16
  controller.instance_variable_get("@_inertia_rendering")
17
17
  end
18
+
19
+ def inertia_page
20
+ controller.instance_variable_get("@_inertia_page")
21
+ end
22
+
23
+ def inertia_meta_tags
24
+ meta_tag_data = (inertia_page || {}).dig(:props, :_inertia_meta) || []
25
+
26
+ meta_tags = meta_tag_data.map do |inertia_meta_tag|
27
+ inertia_meta_tag.to_tag(tag)
28
+ end
29
+
30
+ safe_join(meta_tags, "\n")
31
+ end
18
32
  end
@@ -8,6 +8,7 @@ require 'inertia_rails/optional_prop'
8
8
  require 'inertia_rails/defer_prop'
9
9
  require 'inertia_rails/merge_prop'
10
10
  require 'inertia_rails/configuration'
11
+ require 'inertia_rails/meta_tag'
11
12
 
12
13
  module InertiaRails
13
14
  class << self
@@ -33,16 +34,16 @@ module InertiaRails
33
34
  AlwaysProp.new(&block)
34
35
  end
35
36
 
36
- def merge(&block)
37
- MergeProp.new(&block)
37
+ def merge(match_on: nil, &block)
38
+ MergeProp.new(match_on: match_on, &block)
38
39
  end
39
40
 
40
- def deep_merge(&block)
41
- MergeProp.new(deep_merge: true, &block)
41
+ def deep_merge(match_on: nil, &block)
42
+ MergeProp.new(deep_merge: true, match_on: match_on, &block)
42
43
  end
43
44
 
44
- def defer(group: nil, merge: nil, deep_merge: nil, &block)
45
- DeferProp.new(group: group, merge: merge, deep_merge: deep_merge, &block)
45
+ def defer(group: nil, merge: nil, deep_merge: nil, match_on: nil, &block)
46
+ DeferProp.new(group: group, merge: merge, deep_merge: deep_merge, match_on: match_on, &block)
46
47
  end
47
48
  end
48
49
  end
@@ -2,9 +2,12 @@
2
2
 
3
3
  module InertiaRails
4
4
  class MergeProp < BaseProp
5
- def initialize(deep_merge: false, &block)
5
+ attr_reader :match_on
6
+
7
+ def initialize(deep_merge: false, match_on: nil, &block)
6
8
  super(&block)
7
9
  @deep_merge = deep_merge
10
+ @match_on = match_on.nil? ? nil : Array(match_on)
8
11
  end
9
12
 
10
13
  def merge?
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InertiaRails
4
+ class MetaTag
5
+ # See https://github.com/rails/rails/blob/v8.0.0/actionview/lib/action_view/helpers/tag_helper.rb#L84-L97
6
+ UNARY_TAGS = %i[
7
+ area base br col embed hr img input keygen link meta source track wbr
8
+ ].freeze
9
+
10
+ LD_JSON_TYPE = 'application/ld+json'
11
+ DEFAULT_SCRIPT_TYPE = 'text/plain'
12
+
13
+ GENERATABLE_HEAD_KEY_PROPERTIES = %i[name property http_equiv].freeze
14
+
15
+ def initialize(tag_name: nil, head_key: nil, allow_duplicates: false, type: nil, **tag_data)
16
+ if shortened_title_tag?(tag_name, tag_data)
17
+ @tag_name = :title
18
+ @tag_data = { inner_content: tag_data[:title] }
19
+ else
20
+ @tag_name = tag_name.nil? ? :meta : tag_name.to_sym
21
+ @tag_data = tag_data.symbolize_keys
22
+ end
23
+ @tag_type = determine_tag_type(type)
24
+ @allow_duplicates = allow_duplicates
25
+ @head_key = @tag_name == :title ? 'title' : (head_key || generate_head_key)
26
+ end
27
+
28
+ def as_json(_options = nil)
29
+ {
30
+ tagName: @tag_name,
31
+ headKey: @head_key,
32
+ type: @tag_type,
33
+ }.tap do |result|
34
+ result.merge!(@tag_data.transform_keys { |k| k.to_s.camelize(:lower).to_sym })
35
+ result.compact_blank!
36
+ end
37
+ end
38
+
39
+ def to_tag(tag_helper)
40
+ data = @tag_data.merge(type: @tag_type, inertia: @head_key)
41
+
42
+ inner_content =
43
+ if @tag_name == :script
44
+ tag_script_inner_content(data.delete(:inner_content))
45
+ else
46
+ data.delete(:inner_content)
47
+ end
48
+
49
+ if UNARY_TAGS.include? @tag_name
50
+ tag_helper.public_send(@tag_name, **data.transform_keys { |k| k.to_s.tr('_', '-').to_sym })
51
+ else
52
+ tag_helper.public_send(@tag_name, inner_content, **data.transform_keys { |k| k.to_s.tr('_', '-').to_sym })
53
+ end
54
+ end
55
+
56
+ def [](key)
57
+ key = key.to_sym
58
+ return @tag_name if key == :tag_name
59
+ return @head_key if key == :head_key
60
+ return @tag_type if key == :type
61
+
62
+ @tag_data[key]
63
+ end
64
+
65
+ private
66
+
67
+ def tag_script_inner_content(content)
68
+ case content
69
+ when Hash, Array
70
+ ERB::Util.json_escape(content.to_json).html_safe
71
+ else
72
+ content
73
+ end
74
+ end
75
+
76
+ def shortened_title_tag?(tag_name, tag_data)
77
+ tag_name.nil? && tag_data.keys == [:title]
78
+ end
79
+
80
+ def determine_tag_type(type)
81
+ return type unless @tag_name == :script
82
+
83
+ type == LD_JSON_TYPE ? LD_JSON_TYPE : DEFAULT_SCRIPT_TYPE
84
+ end
85
+
86
+ def generate_head_key
87
+ generate_meta_head_key || "#{@tag_name}-#{tag_digest}"
88
+ end
89
+
90
+ def tag_digest
91
+ signature = @tag_data.sort.map { |k, v| "#{k}=#{v}" }.join('&')
92
+ Digest::MD5.hexdigest(signature)[0, 8]
93
+ end
94
+
95
+ def generate_meta_head_key
96
+ return unless @tag_name == :meta
97
+ return 'meta-charset' if @tag_data.key?(:charset)
98
+
99
+ GENERATABLE_HEAD_KEY_PROPERTIES.each do |key|
100
+ next unless @tag_data.key?(key)
101
+
102
+ return [
103
+ 'meta',
104
+ key,
105
+ @tag_data[key].parameterize,
106
+ @allow_duplicates ? tag_digest : nil
107
+ ].compact.join('-')
108
+ end
109
+
110
+ nil
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InertiaRails
4
+ class MetaTagBuilder
5
+ def initialize(controller)
6
+ @controller = controller
7
+ @meta_tags = {}
8
+ end
9
+
10
+ def meta_tags
11
+ @meta_tags.values
12
+ end
13
+
14
+ def add(meta_tag)
15
+ if meta_tag.is_a?(Array)
16
+ meta_tag.each { |tag| add(tag) }
17
+ elsif meta_tag.is_a?(Hash)
18
+ add_new_tag(meta_tag)
19
+ else
20
+ raise ArgumentError, 'Meta tag must be a Hash or Array of Hashes'
21
+ end
22
+
23
+ self
24
+ end
25
+
26
+ def remove(head_key = nil, &block)
27
+ raise ArgumentError, 'Cannot provide both head_key and a block' if head_key && block_given?
28
+ raise ArgumentError, 'Must provide either head_key or a block' if head_key.nil? && !block_given?
29
+
30
+ if head_key
31
+ @meta_tags.delete(head_key)
32
+ else
33
+ @meta_tags.reject! { |_, tag| block.call(tag) }
34
+ end
35
+
36
+ self
37
+ end
38
+
39
+ def clear
40
+ @meta_tags.clear
41
+
42
+ self
43
+ end
44
+
45
+ private
46
+
47
+ def add_new_tag(new_tag_data)
48
+ new_tag = InertiaRails::MetaTag.new(**new_tag_data)
49
+ @meta_tags[new_tag[:head_key]] = new_tag
50
+ end
51
+ end
52
+ end
@@ -16,9 +16,8 @@ module InertiaRails
16
16
  :clear_history
17
17
  )
18
18
 
19
- def initialize(component, controller, request, response, render_method, props: nil, view_data: nil,
20
- deep_merge: nil, encrypt_history: nil, clear_history: nil)
21
- if component.is_a?(Hash) && !props.nil?
19
+ def initialize(component, controller, request, response, render_method, **options)
20
+ if component.is_a?(Hash) && options.key?(:props)
22
21
  raise ArgumentError,
23
22
  'Parameter `props` is not allowed when passing a Hash as the first argument'
24
23
  end
@@ -29,12 +28,13 @@ module InertiaRails
29
28
  @request = request
30
29
  @response = response
31
30
  @render_method = render_method
32
- @props = props || (component.is_a?(Hash) ? component : controller.__send__(:inertia_view_assigns))
33
- @view_data = view_data || {}
34
- @deep_merge = deep_merge.nil? ? configuration.deep_merge_shared_data : deep_merge
35
- @encrypt_history = encrypt_history.nil? ? configuration.encrypt_history : encrypt_history
36
- @clear_history = clear_history || controller.session[:inertia_clear_history] || false
31
+ @props = options.fetch(:props, component.is_a?(Hash) ? component : controller.__send__(:inertia_view_assigns))
32
+ @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)
35
+ @clear_history = options.fetch(:clear_history, controller.session[:inertia_clear_history] || false)
37
36
  @controller.instance_variable_set('@_inertia_rendering', true)
37
+ controller.inertia_meta.add(options[:meta]) if options[:meta]
38
38
  end
39
39
 
40
40
  def render
@@ -52,6 +52,7 @@ module InertiaRails
52
52
  rescue StandardError
53
53
  nil
54
54
  end
55
+ controller.instance_variable_set('@_inertia_page', page)
55
56
  @render_method.call template: 'inertia', layout: layout, locals: view_data.merge(page: page)
56
57
  end
57
58
  end
@@ -90,7 +91,9 @@ module InertiaRails
90
91
 
91
92
  def computed_props
92
93
  merged_props = merge_props(shared_data, props)
93
- deep_transform_props(merged_props)
94
+ deep_transform_props(merged_props).tap do |transformed_props|
95
+ transformed_props[:_inertia_meta] = meta_tags if meta_tags.present?
96
+ end
94
97
  end
95
98
 
96
99
  def page
@@ -106,14 +109,17 @@ module InertiaRails
106
109
  deferred_props = deferred_props_keys
107
110
  default_page[:deferredProps] = deferred_props if deferred_props.present?
108
111
 
109
- all_merge_props = merge_props_keys
110
-
111
- deep_merge_props, merge_props = all_merge_props.partition do |key|
112
- @props[key].deep_merge?
112
+ deep_merge_props, merge_props = all_merge_props.partition do |_key, prop|
113
+ prop.deep_merge?
113
114
  end
114
115
 
115
- default_page[:mergeProps] = merge_props if merge_props.present?
116
- default_page[:deepMergeProps] = deep_merge_props if deep_merge_props.present?
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?
117
123
 
118
124
  default_page
119
125
  end
@@ -147,9 +153,16 @@ module InertiaRails
147
153
  end
148
154
  end
149
155
 
150
- def merge_props_keys
151
- @props.each_with_object([]) do |(key, prop), result|
152
- result << key if prop.try(:merge?) && reset_keys.exclude?(key)
156
+ def all_merge_props
157
+ @all_merge_props ||= @props.select do |key, prop|
158
+ next unless prop.try(:merge?)
159
+ next if reset_keys.include?(key)
160
+ next if rendering_partial_component? && (
161
+ (partial_keys.present? && partial_keys.exclude?(key.name)) ||
162
+ (partial_except_keys.present? && partial_except_keys.include?(key.name))
163
+ )
164
+
165
+ true
153
166
  end
154
167
  end
155
168
 
@@ -180,7 +193,7 @@ module InertiaRails
180
193
  def keep_prop?(prop, path)
181
194
  return true if prop.is_a?(AlwaysProp)
182
195
 
183
- if rendering_partial_component?
196
+ if rendering_partial_component? && (partial_keys.present? || partial_except_keys.present?)
184
197
  path_with_prefixes = path_prefixes(path)
185
198
  return false if excluded_by_only_partial_keys?(path_with_prefixes)
186
199
  return false if excluded_by_except_partial_keys?(path_with_prefixes)
@@ -205,5 +218,9 @@ module InertiaRails
205
218
  def excluded_by_except_partial_keys?(path_with_prefixes)
206
219
  partial_except_keys.present? && (path_with_prefixes & partial_except_keys).any?
207
220
  end
221
+
222
+ def meta_tags
223
+ controller.inertia_meta.meta_tags
224
+ end
208
225
  end
209
226
  end
@@ -75,7 +75,7 @@ RSpec.configure do |config|
75
75
  config.before(:each, inertia: true) do
76
76
  new_renderer = InertiaRails::Renderer.method(:new)
77
77
  allow(InertiaRails::Renderer).to receive(:new) do |component, controller, request, response, render, named_args|
78
- new_renderer.call(component, controller, request, response, inertia_wrap_render(render), **named_args)
78
+ new_renderer.call(component, controller, request, response, inertia_wrap_render(render), **(named_args || {}))
79
79
  end
80
80
  end
81
81
  end
@@ -1,3 +1,3 @@
1
1
  module InertiaRails
2
- VERSION = "3.9.0"
2
+ VERSION = "3.10.0"
3
3
  end
data/lib/inertia_rails.rb CHANGED
@@ -15,11 +15,7 @@ ActionController::Renderers.add :inertia do |component, options|
15
15
  request,
16
16
  response,
17
17
  method(:render),
18
- props: options[:props],
19
- view_data: options[:view_data],
20
- deep_merge: options[:deep_merge],
21
- encrypt_history: options[:encrypt_history],
22
- clear_history: options[:clear_history]
18
+ **options
23
19
  ).render
24
20
  end
25
21
 
metadata CHANGED
@@ -1,15 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: inertia_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.9.0
4
+ version: 3.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Knoles
8
8
  - Brandon Shar
9
9
  - Eugene Granovsky
10
+ autorequire:
10
11
  bindir: bin
11
12
  cert_chain: []
12
- date: 2025-06-18 00:00:00.000000000 Z
13
+ date: 2025-07-30 00:00:00.000000000 Z
13
14
  dependencies:
14
15
  - !ruby/object:Gem::Dependency
15
16
  name: railties
@@ -225,6 +226,8 @@ files:
225
226
  - lib/inertia_rails/inertia_rails.rb
226
227
  - lib/inertia_rails/lazy_prop.rb
227
228
  - lib/inertia_rails/merge_prop.rb
229
+ - lib/inertia_rails/meta_tag.rb
230
+ - lib/inertia_rails/meta_tag_builder.rb
228
231
  - lib/inertia_rails/middleware.rb
229
232
  - lib/inertia_rails/optional_prop.rb
230
233
  - lib/inertia_rails/renderer.rb
@@ -247,6 +250,7 @@ metadata:
247
250
  homepage_uri: https://github.com/inertiajs/inertia-rails
248
251
  source_code_uri: https://github.com/inertiajs/inertia-rails
249
252
  rubygems_mfa_required: 'true'
253
+ post_install_message:
250
254
  rdoc_options: []
251
255
  require_paths:
252
256
  - lib
@@ -261,7 +265,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
261
265
  - !ruby/object:Gem::Version
262
266
  version: '0'
263
267
  requirements: []
264
- rubygems_version: 3.6.2
268
+ rubygems_version: 3.5.22
269
+ signing_key:
265
270
  specification_version: 4
266
271
  summary: Inertia.js adapter for Rails
267
272
  test_files: []