From 5fcd870da79446acc366a1cdef1c2b4065716cc3 Mon Sep 17 00:00:00 2001 From: Brian Knoles Date: Thu, 21 Nov 2024 23:24:31 -0500 Subject: [PATCH 1/7] Improve readability of render method --- lib/inertia_rails/renderer.rb | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/lib/inertia_rails/renderer.rb b/lib/inertia_rails/renderer.rb index 4e19b2e2..433a8590 100644 --- a/lib/inertia_rails/renderer.rb +++ b/lib/inertia_rails/renderer.rb @@ -30,21 +30,28 @@ def initialize(component, controller, request, response, render_method, props: n end def render + set_vary_header + + return render_inertia_response if @request.headers['X-Inertia'] + return render_ssr if configuration.ssr_enabled rescue nil + + render_full_page + end + + private + + def set_vary_header if @response.headers["Vary"].blank? @response.headers["Vary"] = 'X-Inertia' else @response.headers["Vary"] = "#{@response.headers["Vary"]}, X-Inertia" end - if @request.headers['X-Inertia'] - @response.set_header('X-Inertia', 'true') - @render_method.call json: page, status: @response.status, content_type: Mime[:json] - else - return render_ssr if configuration.ssr_enabled rescue nil - @render_method.call template: 'inertia', layout: layout, locals: view_data.merge(page: page) - end end - private + def render_inertia_response + @response.set_header('X-Inertia', 'true') + @render_method.call json: page, status: @response.status, content_type: Mime[:json] + end def render_ssr uri = URI("#{configuration.ssr_url}/render") @@ -54,6 +61,10 @@ def render_ssr @render_method.call html: res['body'].html_safe, layout: layout, locals: view_data.merge(page: page) end + def render_full_page + @render_method.call template: 'inertia', layout: layout, locals: view_data.merge(page: page) + end + def layout layout = configuration.layout layout.nil? ? true : layout From b8ed6a0d00d1abc5bee48223366a54e30d3bf2bd Mon Sep 17 00:00:00 2001 From: Brian Knoles Date: Fri, 22 Nov 2024 00:34:36 -0500 Subject: [PATCH 2/7] Refactor prop transformation into a nested subclass --- lib/inertia_rails/renderer.rb | 85 +++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 39 deletions(-) diff --git a/lib/inertia_rails/renderer.rb b/lib/inertia_rails/renderer.rb index 433a8590..bc9b3bdf 100644 --- a/lib/inertia_rails/renderer.rb +++ b/lib/inertia_rails/renderer.rb @@ -6,9 +6,6 @@ module InertiaRails class Renderer - KEEP_PROP = :keep - DONT_KEEP_PROP = :dont_keep - attr_reader( :component, :configuration, @@ -79,58 +76,28 @@ def shared_data # # Functionally, this permits using either string or symbol keys in the controller. Since the results # is cast to json, we should treat string/symbol keys as identical. - def merge_props(shared_data, props) - if @deep_merge + def merged_props + @merged_props ||= if @deep_merge shared_data.deep_symbolize_keys.deep_merge!(props.deep_symbolize_keys) else shared_data.symbolize_keys.merge(props.symbolize_keys) end end - def computed_props - _props = merge_props(shared_data, props) - - deep_transform_props _props do |prop, path| - next [DONT_KEEP_PROP] unless keep_prop?(prop, path) - - transformed_prop = case prop - when BaseProp - prop.call(controller) - when Proc - controller.instance_exec(&prop) - else - prop - end - - [KEEP_PROP, transformed_prop] - end + def prop_transformer + @prop_transformer ||= PropTransformer.new(merged_props, controller) + .select_transformed { |prop, path| keep_prop?(prop, path) } end def page { component: component, - props: computed_props, + props: prop_transformer.props, url: @request.original_fullpath, version: configuration.version, } end - def deep_transform_props(props, parent_path = [], &block) - props.reduce({}) do |transformed_props, (key, prop)| - current_path = parent_path + [key] - - if prop.is_a?(Hash) && prop.any? - nested = deep_transform_props(prop, current_path, &block) - transformed_props.merge!(key => nested) unless nested.empty? - else - action, transformed_prop = block.call(prop, current_path) - transformed_props.merge!(key => transformed_prop) if action == KEEP_PROP - end - - transformed_props - end - end - def partial_keys (@request.headers['X-Inertia-Partial-Data'] || '').split(',').compact end @@ -177,5 +144,45 @@ def excluded_by_only_partial_keys?(path_with_prefixes) def excluded_by_except_partial_keys?(path_with_prefixes) partial_except_keys.present? && (path_with_prefixes & partial_except_keys).any? end + + class PropTransformer + attr_reader :props + + def initialize(initial_props, controller) + @initial_props = initial_props + @controller = controller + end + + def select_transformed(&block) + @props = deep_transform_and_select(@initial_props, &block) + self + end + + def deep_transform_and_select(original_props, parent_path = [], &block) + original_props.reduce({}) do |transformed_props, (key, prop)| + current_path = parent_path + [key] + + if prop.is_a?(Hash) && prop.any? + nested = deep_transform_and_select(prop, current_path, &block) + transformed_props.merge!(key => nested) unless nested.empty? + elsif block.call(prop, current_path) + transformed_props.merge!(key => transform_prop(prop)) + end + + transformed_props + end + end + + def transform_prop(prop) + case prop + when BaseProp + prop.call(@controller) + when Proc + @controller.instance_exec(&prop) + else + prop + end + end + end end end From c82daf4d70a74b956ca7df2fafc6f4c69ea170ce Mon Sep 17 00:00:00 2001 From: Brian Knoles Date: Fri, 22 Nov 2024 01:06:52 -0500 Subject: [PATCH 3/7] Basic implementation of unoptimized partial reloads with a hardcoded logger warning --- lib/inertia_rails/renderer.rb | 22 +++++++++++++++++++--- spec/inertia/rendering_spec.rb | 25 +++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/lib/inertia_rails/renderer.rb b/lib/inertia_rails/renderer.rb index bc9b3bdf..2b9fc184 100644 --- a/lib/inertia_rails/renderer.rb +++ b/lib/inertia_rails/renderer.rb @@ -29,6 +29,8 @@ def initialize(component, controller, request, response, render_method, props: n def render set_vary_header + validate_partial_reload_optimization if rendering_partial_component? + return render_inertia_response if @request.headers['X-Inertia'] return render_ssr if configuration.ssr_enabled rescue nil @@ -78,9 +80,9 @@ def shared_data # is cast to json, we should treat string/symbol keys as identical. def merged_props @merged_props ||= if @deep_merge - shared_data.deep_symbolize_keys.deep_merge!(props.deep_symbolize_keys) + shared_data.deep_symbolize_keys.deep_merge!(@props.deep_symbolize_keys) else - shared_data.symbolize_keys.merge(props.symbolize_keys) + shared_data.symbolize_keys.merge(@props.symbolize_keys) end end @@ -145,12 +147,24 @@ def excluded_by_except_partial_keys?(path_with_prefixes) partial_except_keys.present? && (path_with_prefixes & partial_except_keys).any? end + def validate_partial_reload_optimization + if prop_transformer.unoptimized_prop_paths.any? + message = + "InertiaRails: The \"#{prop_transformer.unoptimized_prop_paths.join(', ')}\" " \ + "prop(s) were excluded in a partial reload but still evaluated because they are defined as values. " \ + "Consider wrapping them in something callable like a lambda." + + controller.logger.warn(message) + end + end + class PropTransformer - attr_reader :props + attr_reader :props, :unoptimized_prop_paths def initialize(initial_props, controller) @initial_props = initial_props @controller = controller + @unoptimized_prop_paths = [] end def select_transformed(&block) @@ -167,6 +181,8 @@ def deep_transform_and_select(original_props, parent_path = [], &block) transformed_props.merge!(key => nested) unless nested.empty? elsif block.call(prop, current_path) transformed_props.merge!(key => transform_prop(prop)) + elsif !prop.respond_to?(:call) + @unoptimized_prop_paths << current_path.join(".") end transformed_props diff --git a/spec/inertia/rendering_spec.rb b/spec/inertia/rendering_spec.rb index af527147..53060cb1 100644 --- a/spec/inertia/rendering_spec.rb +++ b/spec/inertia/rendering_spec.rb @@ -393,6 +393,31 @@ end end end + + context 'when there are unoptimized props' do + let(:headers) {{ + 'X-Inertia' => true, + 'X-Inertia-Partial-Data' => 'nested.first', + 'X-Inertia-Partial-Component' => 'TestComponent', + }} + + it 'logs a warning' do + expect(Rails.logger).to receive(:warn).with(/flat, nested\.second/) + get except_props_path, headers: headers + end + #flat: 'flat param', + #lazy: InertiaRails.lazy('lazy param'), + #nested_lazy: InertiaRails.lazy do + #{ + #first: 'first nested lazy param', + #} + #end, + #nested: { + #first: 'first nested param', + #second: 'second nested param' + #}, + #always: InertiaRails.always { 'always prop' } + end end def inertia_div(page) From 32b829eb6e073d1b4f779d93015553a1bcb959d4 Mon Sep 17 00:00:00 2001 From: Brian Knoles Date: Fri, 22 Nov 2024 01:46:29 -0500 Subject: [PATCH 4/7] Initialize was doing too much work --- lib/inertia_rails/renderer.rb | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/inertia_rails/renderer.rb b/lib/inertia_rails/renderer.rb index 2b9fc184..5b742c98 100644 --- a/lib/inertia_rails/renderer.rb +++ b/lib/inertia_rails/renderer.rb @@ -87,8 +87,8 @@ def merged_props end def prop_transformer - @prop_transformer ||= PropTransformer.new(merged_props, controller) - .select_transformed { |prop, path| keep_prop?(prop, path) } + @prop_transformer ||= PropTransformer.new(controller) + .select_transformed(merged_props){ |prop, path| keep_prop?(prop, path) } end def page @@ -161,17 +161,21 @@ def validate_partial_reload_optimization class PropTransformer attr_reader :props, :unoptimized_prop_paths - def initialize(initial_props, controller) - @initial_props = initial_props + def initialize(controller) @controller = controller @unoptimized_prop_paths = [] + @props = {} end - def select_transformed(&block) - @props = deep_transform_and_select(@initial_props, &block) + def select_transformed(initial_props, &block) + @unoptimized_prop_paths = [] + @props = deep_transform_and_select(initial_props, &block) + self end + private + def deep_transform_and_select(original_props, parent_path = [], &block) original_props.reduce({}) do |transformed_props, (key, prop)| current_path = parent_path + [key] From 4e16d06f36af9358e8accb65e792c927a4a40cc6 Mon Sep 17 00:00:00 2001 From: Brian Knoles Date: Fri, 22 Nov 2024 01:51:34 -0500 Subject: [PATCH 5/7] Create a method to better reveal intent --- lib/inertia_rails/renderer.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/inertia_rails/renderer.rb b/lib/inertia_rails/renderer.rb index 5b742c98..19f4b585 100644 --- a/lib/inertia_rails/renderer.rb +++ b/lib/inertia_rails/renderer.rb @@ -186,7 +186,7 @@ def deep_transform_and_select(original_props, parent_path = [], &block) elsif block.call(prop, current_path) transformed_props.merge!(key => transform_prop(prop)) elsif !prop.respond_to?(:call) - @unoptimized_prop_paths << current_path.join(".") + track_unoptimized_prop(current_path) end transformed_props @@ -203,6 +203,10 @@ def transform_prop(prop) prop end end + + def track_unoptimized_prop(path) + @unoptimized_prop_paths << path.join(".") + end end end end From e590eb920cfe7d4bdc3550f320b3818b4d28673f Mon Sep 17 00:00:00 2001 From: Brian Knoles Date: Mon, 25 Nov 2024 22:53:13 -0500 Subject: [PATCH 6/7] Use ActiveSupport::Notifications for warning on unoptimized partial reloads --- lib/inertia_rails/engine.rb | 15 +++++++++++++++ lib/inertia_rails/renderer.rb | 12 ++++++------ spec/dummy/config/environments/test.rb | 2 +- spec/inertia/rendering_spec.rb | 2 +- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/lib/inertia_rails/engine.rb b/lib/inertia_rails/engine.rb index 0e39d545..edcf5808 100644 --- a/lib/inertia_rails/engine.rb +++ b/lib/inertia_rails/engine.rb @@ -12,5 +12,20 @@ class Engine < ::Rails::Engine include ::InertiaRails::Controller end end + + initializer "inertia_rails.subscribe_to_notifications" do + if Rails.env.development? || Rails.env.test? + ActiveSupport::Notifications.subscribe('inertia_rails.unoptimized_partial_render') do |*args| + event = ActiveSupport::Notifications::Event.new(*args) + + message = + "InertiaRails: The \"#{event.payload[:paths].join(', ')}\" " \ + "prop(s) were excluded in a partial reload but still evaluated because they are defined as values. " \ + "Consider wrapping them in something callable like a lambda." + + Rails.logger.debug(message) + end + end + end end end diff --git a/lib/inertia_rails/renderer.rb b/lib/inertia_rails/renderer.rb index 19f4b585..03867459 100644 --- a/lib/inertia_rails/renderer.rb +++ b/lib/inertia_rails/renderer.rb @@ -149,12 +149,12 @@ def excluded_by_except_partial_keys?(path_with_prefixes) def validate_partial_reload_optimization if prop_transformer.unoptimized_prop_paths.any? - message = - "InertiaRails: The \"#{prop_transformer.unoptimized_prop_paths.join(', ')}\" " \ - "prop(s) were excluded in a partial reload but still evaluated because they are defined as values. " \ - "Consider wrapping them in something callable like a lambda." - - controller.logger.warn(message) + ActiveSupport::Notifications.instrument( + 'inertia_rails.unoptimized_partial_render', + paths: prop_transformer.unoptimized_prop_paths, + controller: controller, + action: controller.action_name, + ) end end diff --git a/spec/dummy/config/environments/test.rb b/spec/dummy/config/environments/test.rb index 0e16e721..e7bad992 100644 --- a/spec/dummy/config/environments/test.rb +++ b/spec/dummy/config/environments/test.rb @@ -9,7 +9,7 @@ Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. - + config.cache_classes = false # Do not eager load code on boot. This avoids loading your whole application diff --git a/spec/inertia/rendering_spec.rb b/spec/inertia/rendering_spec.rb index 53060cb1..3058f760 100644 --- a/spec/inertia/rendering_spec.rb +++ b/spec/inertia/rendering_spec.rb @@ -402,7 +402,7 @@ }} it 'logs a warning' do - expect(Rails.logger).to receive(:warn).with(/flat, nested\.second/) + expect(Rails.logger).to receive(:debug).with(/flat, nested\.second/) get except_props_path, headers: headers end #flat: 'flat param', From 8ee2cd4614eeadc573f367a92fe7783d31f2d380 Mon Sep 17 00:00:00 2001 From: Brian Knoles Date: Mon, 25 Nov 2024 23:21:36 -0500 Subject: [PATCH 7/7] Make partial reload warnings configurable --- lib/inertia_rails.rb | 8 +++++ lib/inertia_rails/configuration.rb | 2 ++ lib/inertia_rails/renderer.rb | 17 +++++++---- spec/inertia/configuration_spec.rb | 49 ++++++++++++++++++++++++++++++ spec/inertia/rendering_spec.rb | 25 --------------- 5 files changed, 70 insertions(+), 31 deletions(-) diff --git a/lib/inertia_rails.rb b/lib/inertia_rails.rb index ef6b0885..66e74e73 100644 --- a/lib/inertia_rails.rb +++ b/lib/inertia_rails.rb @@ -21,6 +21,14 @@ module InertiaRails class Error < StandardError; end + class UnoptimizedPartialReload < StandardError + attr_reader :paths + + def initialize(paths) + @paths = paths + super("The #{paths.join(', ')} prop(s) were excluded in a partial reload but still evaluated because they are defined as values.") + end + end def self.deprecator # :nodoc: @deprecator ||= ActiveSupport::Deprecation.new diff --git a/lib/inertia_rails/configuration.rb b/lib/inertia_rails/configuration.rb index 02f73f05..40864298 100644 --- a/lib/inertia_rails/configuration.rb +++ b/lib/inertia_rails/configuration.rb @@ -22,6 +22,8 @@ class Configuration # Used to detect version drift between server and client. version: nil, + + action_on_unoptimized_partial_reload: :log, }.freeze OPTION_NAMES = DEFAULTS.keys.freeze diff --git a/lib/inertia_rails/renderer.rb b/lib/inertia_rails/renderer.rb index 03867459..e9dc4d96 100644 --- a/lib/inertia_rails/renderer.rb +++ b/lib/inertia_rails/renderer.rb @@ -149,12 +149,17 @@ def excluded_by_except_partial_keys?(path_with_prefixes) def validate_partial_reload_optimization if prop_transformer.unoptimized_prop_paths.any? - ActiveSupport::Notifications.instrument( - 'inertia_rails.unoptimized_partial_render', - paths: prop_transformer.unoptimized_prop_paths, - controller: controller, - action: controller.action_name, - ) + case configuration.action_on_unoptimized_partial_reload + when :log + ActiveSupport::Notifications.instrument( + 'inertia_rails.unoptimized_partial_render', + paths: prop_transformer.unoptimized_prop_paths, + controller: controller, + action: controller.action_name, + ) + when :raise + raise InertiaRails::UnoptimizedPartialReload.new(prop_transformer.unoptimized_prop_paths) + end end end diff --git a/spec/inertia/configuration_spec.rb b/spec/inertia/configuration_spec.rb index 42135356..b4f3eb30 100644 --- a/spec/inertia/configuration_spec.rb +++ b/spec/inertia/configuration_spec.rb @@ -153,11 +153,60 @@ end end end + + describe '.action_on_unoptimized_partial_reload' do + let(:headers) {{ + 'X-Inertia' => true, + 'X-Inertia-Partial-Data' => 'nested.first', + 'X-Inertia-Partial-Component' => 'TestComponent', + }} + + context 'default case' do + let(:headers) {{ + 'X-Inertia' => true, + 'X-Inertia-Partial-Data' => 'nested.first', + 'X-Inertia-Partial-Component' => 'TestComponent', + }} + + it 'logs a warning' do + expect(Rails.logger).to receive(:debug).with(/flat, nested\.second/) + get except_props_path, headers: headers + end + end + + context 'when set to :raise' do + before do + InertiaRails.configure {|c| c.action_on_unoptimized_partial_reload = :raise} + end + + let(:headers) {{ + 'X-Inertia' => true, + 'X-Inertia-Partial-Data' => 'nested.first', + 'X-Inertia-Partial-Component' => 'TestComponent', + }} + + it 'raises an exception' do + expect { get except_props_path, headers: headers }.to raise_error(InertiaRails::UnoptimizedPartialReload).with_message(/flat, nested\.second/) + end + end + + context 'with an unknown value' do + before do + InertiaRails.configure {|c| c.action_on_unoptimized_partial_reload = :unknown} + end + + it 'does nothing' do + expect(Rails.logger).not_to receive(:debug) + get except_props_path, headers: headers + end + end + end end def reset_config! InertiaRails.configure do |config| config.version = nil config.layout = 'application' + config.action_on_unoptimized_partial_reload = :log end end diff --git a/spec/inertia/rendering_spec.rb b/spec/inertia/rendering_spec.rb index 3058f760..af527147 100644 --- a/spec/inertia/rendering_spec.rb +++ b/spec/inertia/rendering_spec.rb @@ -393,31 +393,6 @@ end end end - - context 'when there are unoptimized props' do - let(:headers) {{ - 'X-Inertia' => true, - 'X-Inertia-Partial-Data' => 'nested.first', - 'X-Inertia-Partial-Component' => 'TestComponent', - }} - - it 'logs a warning' do - expect(Rails.logger).to receive(:debug).with(/flat, nested\.second/) - get except_props_path, headers: headers - end - #flat: 'flat param', - #lazy: InertiaRails.lazy('lazy param'), - #nested_lazy: InertiaRails.lazy do - #{ - #first: 'first nested lazy param', - #} - #end, - #nested: { - #first: 'first nested param', - #second: 'second nested param' - #}, - #always: InertiaRails.always { 'always prop' } - end end def inertia_div(page)