reactionview 0.4.1 → 0.6.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.
@@ -32,20 +32,55 @@ module ReActionView
32
32
  # How to handle templates that come from gems (:fallback, :skip, or :compile), defaults to :fallback
33
33
  # config.external_template_mode = :skip
34
34
 
35
- # Add custom transform visitors to process templates before compilation
36
- # config.transform_visitors = [
37
- # Herb::Visitor::new
38
- # ]
35
+ # Measure what a page does while it renders, and show it in the dev tools.
36
+ # Follows development unless you say otherwise, and each measurement can be turned off.
37
+ # config.instrumentation.enabled = Rails.env.development?
38
+ # config.instrumentation.sql_queries = false
39
+ # config.instrumentation.render_times = false
40
+ # config.instrumentation.translations = false
41
+
42
+ # Add visitors to the compile. Place them with `insert_before` and `insert_after`.
43
+ # config.engine.visitors.use(Herb::Visitor.new)
44
+
45
+ # Parser options for every compile, merged over the ones in .herb.yml
46
+ # config.engine.parser_options = { strict_locals: true }
39
47
  end
40
48
  RUBY
41
49
  end
42
50
 
51
+ def add_javascript
52
+ if File.exist?("config/importmap.rb")
53
+ say "The gem pins the reactionview client runtime in your importmap, nothing to add there.", :green
54
+ elsif File.exist?("package.json")
55
+ say "Add the client runtime to your bundle: #{javascript_install_command}", :yellow
56
+ end
57
+
58
+ return unless File.exist?("app/javascript/application.js")
59
+
60
+ say "Importing reactionview in app/javascript/application.js...", :green
61
+
62
+ append_to_file "app/javascript/application.js", %(import "reactionview"\n)
63
+ end
64
+
65
+ def javascript_install_command
66
+ if File.exist?("bun.lock") || File.exist?("bun.lockb")
67
+ "bun add reactionview"
68
+ elsif File.exist?("pnpm-lock.yaml")
69
+ "pnpm add reactionview"
70
+ elsif File.exist?("package-lock.json")
71
+ "npm install reactionview"
72
+ else
73
+ "yarn add reactionview"
74
+ end
75
+ end
76
+
43
77
  def show_installation_complete
44
78
  say "\nReActionView has been successfully installed! 🎉", :green
45
79
  say "\nNext steps:", :blue
46
80
  say " 1. Review config/initializers/reactionview.rb"
47
81
  say " 2. Enable `config.intercept_erb = true` to process all `*.html.erb` templates using `Herb::Engine`."
48
82
  say " 3. Create `*.html.herb` templates for explicit Herb usage."
83
+ say " 4. Make sure `import \"reactionview\"` runs in your JavaScript entry point, see https://reactionview.dev/javascript"
49
84
 
50
85
  say "\nLearn more:", :yellow
51
86
  say " GitHub: https://github.com/marcoroth/reactionview"
@@ -46,7 +46,7 @@ module ReActionView
46
46
 
47
47
  def explanation
48
48
  <<~MESSAGE
49
- ReActionView's dev tools assets are missing from your precompiled assets.
49
+ ReActionView's JavaScript assets are missing from your precompiled assets.
50
50
  #{status}
51
51
 
52
52
  To fix this, delete the precompiled assets:
@@ -1,13 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "herb"
4
+ require "herb/visitor/stack"
5
+
3
6
  module ReActionView
4
7
  class Config
8
+ EXTERNAL_TEMPLATE_MODES = %i[fallback skip compile].freeze
9
+ SLOT_MODES = %i[server client].freeze
10
+
5
11
  attr_accessor :intercept_erb
6
12
  attr_accessor :debug_mode
7
- attr_accessor :transform_visitors
8
13
 
9
- EXTERNAL_TEMPLATE_MODES = %i[fallback skip compile].freeze
14
+ attr_reader :slots
10
15
 
16
+ attr_writer :dev_server
11
17
  attr_writer :dev_server_port
12
18
  attr_writer :project_path
13
19
  attr_writer :validation_mode
@@ -15,10 +21,72 @@ module ReActionView
15
21
  def initialize
16
22
  @intercept_erb = false
17
23
  @debug_mode = nil
24
+ @dev_server = nil
18
25
  @dev_server_port = nil
19
26
  @external_template_mode = nil
20
- @transform_visitors = []
27
+ @engine = nil
21
28
  @project_path = nil
29
+ @slots = false
30
+ @instrumentation = nil
31
+ end
32
+
33
+ def engine
34
+ @engine ||= EngineOptions.new
35
+ end
36
+
37
+ def transform_visitors
38
+ ReActionView.deprecator.warn("`config.transform_visitors` is deprecated. Read the visitors from `config.engine.visitors` instead.")
39
+
40
+ engine.visitors
41
+ end
42
+
43
+ def transform_visitors=(visitors)
44
+ ReActionView.deprecator.warn(
45
+ "`config.transform_visitors=` is deprecated. Add visitors with `config.engine.visitors.use(visitor)` instead, " \
46
+ "and place them with `insert_before` and `insert_after`."
47
+ )
48
+
49
+ engine.visitors.replace(Array(visitors))
50
+ end
51
+
52
+ class EngineOptions
53
+ attr_reader :visitors
54
+ attr_reader :parser_options
55
+
56
+ def initialize
57
+ @visitors = ::Herb::Visitor::Stack.new
58
+ @parser_options = {}
59
+ end
60
+
61
+ def parser_options=(options)
62
+ raise ArgumentError, "parser_options must be a Hash of parser options, got #{options.inspect}" unless options.is_a?(Hash)
63
+
64
+ @parser_options = options.transform_keys(&:to_sym)
65
+ end
66
+ end
67
+
68
+ def dev_server_enabled?
69
+ return @dev_server unless @dev_server.nil?
70
+
71
+ true
72
+ end
73
+
74
+ def slots=(value)
75
+ unless [nil, true, false].include?(value) || SLOT_MODES.include?(value)
76
+ raise ArgumentError, "slots must be true, false, or one of #{SLOT_MODES.inspect}, got #{value.inspect}"
77
+ end
78
+
79
+ @slots = value
80
+ end
81
+
82
+ def slot_mode_for(source)
83
+ ::Herb::Engine::Slots::Visitor.directive_mode(source) || default_slot_mode
84
+ end
85
+
86
+ def default_slot_mode
87
+ return nil unless slots
88
+
89
+ slots == true ? :server : slots
22
90
  end
23
91
 
24
92
  def external_template_mode
@@ -61,11 +129,38 @@ module ReActionView
61
129
  development?
62
130
  end
63
131
 
132
+ def instrumentation
133
+ @instrumentation ||= InstrumentationOptions.new(enabled: development?)
134
+ end
135
+
136
+ class InstrumentationOptions < ::ActiveSupport::OrderedOptions
137
+ BUILT_INS = [:sql_queries, :render_times, :translations].freeze
138
+ KEYS = ([:enabled] + BUILT_INS).freeze
139
+
140
+ def initialize(enabled:)
141
+ super()
142
+
143
+ merge!(enabled: enabled, **BUILT_INS.to_h { |key| [key, true] })
144
+ end
145
+
146
+ def []=(key, value)
147
+ raise ArgumentError, "unknown instrumentation option #{key.inspect}, expected one of #{KEYS.inspect}" unless KEYS.include?(key.to_sym)
148
+
149
+ super
150
+ end
151
+
152
+ def measuring?(built_in)
153
+ !!self[:enabled] && !!self[built_in]
154
+ end
155
+ end
156
+
64
157
  def dev_server_port
65
158
  return @dev_server_port if @dev_server_port
66
159
  return nil unless development?
67
160
 
68
- ::Herb.dev_server_port(Rails.root.to_s)
161
+ require "herb/dev/server_entry"
162
+
163
+ ::Herb::Dev::ServerEntry.port_for(Rails.root.to_s)
69
164
  end
70
165
  end
71
166
 
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ pin "reactionview", to: "reactionview.esm.js", preload: true
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReActionView
4
+ # What a page did while it rendered, filed against the tags that did it.
5
+ #
6
+ # `Herb::Engine::InstrumentationVisitor` says which tag is rendering at any moment. It does not
7
+ # say what is worth noticing while one is, because that is not the engine's business: an
8
+ # application knows what it cares about, and Rails already announces most of it through
9
+ # `ActiveSupport::Notifications`.
10
+ #
11
+ # These are the three worth having in every application, so they are here rather than in each one:
12
+ # the queries a tag ran, what a render cost, and what a translation rendered. Each is a subscriber
13
+ # that observes, plus a measurement that says how to read what was observed. Either half can be
14
+ # turned off on its own.
15
+ #
16
+ # ReActionView.configure do |config|
17
+ # config.instrumentation = true
18
+ # config.instrumentation.sql_queries = false
19
+ # end
20
+ #
21
+ module Instrumentation
22
+ TRANSLATION_TAGS = [/\At\(/, /\Atranslate\(/].freeze #: Array[Regexp]
23
+ IGNORED_QUERIES = ["SCHEMA", "TRANSACTION"].freeze #: Array[String]
24
+ RENDER_EVENTS = %w[
25
+ render_partial.action_view
26
+ render_collection.action_view
27
+ render_template.action_view
28
+ ].freeze #: Array[String]
29
+
30
+ def self.available?
31
+ require "herb/engine/runtime/session"
32
+
33
+ ::Herb::Engine::Runtime::Session.respond_to?(:measurement)
34
+ rescue LoadError
35
+ false
36
+ end
37
+
38
+ def self.install!(config = ReActionView.config)
39
+ return unless config.instrumentation.enabled
40
+ return unless available?
41
+ return if installed?
42
+
43
+ @installed = true
44
+
45
+ install_sql_queries if config.instrumentation.measuring?(:sql_queries)
46
+ install_render_times if config.instrumentation.measuring?(:render_times)
47
+ install_translations if config.instrumentation.measuring?(:translations)
48
+
49
+ config.engine.visitors.use(visitor(config))
50
+
51
+ nil
52
+ end
53
+
54
+ def self.installed?
55
+ @installed == true
56
+ end
57
+
58
+ def self.reset!
59
+ subscriptions.each { |subscription| ::ActiveSupport::Notifications.unsubscribe(subscription) }
60
+ subscriptions.clear
61
+
62
+ @installed = false
63
+ end
64
+
65
+ def self.subscriptions
66
+ @subscriptions ||= []
67
+ end
68
+
69
+ def self.visitor(config = ReActionView.config)
70
+ require "herb/engine/visitors/instrumentation_visitor"
71
+
72
+ ::Herb::Engine::InstrumentationVisitor.new(capture_output: config.instrumentation.measuring?(:translations) ? TRANSLATION_TAGS : nil)
73
+ end
74
+
75
+ def self.install_sql_queries
76
+ subscriptions << ::ActiveSupport::Notifications.subscribe("sql.active_record") do |*, payload|
77
+ next if payload[:cached]
78
+ next if IGNORED_QUERIES.include?(payload[:name])
79
+
80
+ session.observe(:queries, payload[:sql])
81
+ end
82
+
83
+ session.measurement(
84
+ :queries,
85
+ origin: "Herb Engine (Runtime)",
86
+ code: "sql-queries",
87
+ description: ->(queries) { "This ERB tag ran #{queries.size} SQL #{"query".pluralize(queries.size)} while the page rendered." }
88
+ ) { |queries| "#{queries.size} SQL #{"query".pluralize(queries.size)}" }
89
+ end
90
+
91
+ def self.install_render_times
92
+ RENDER_EVENTS.each do |event|
93
+ subscriptions << ::ActiveSupport::Notifications.subscribe(event) do |notification|
94
+ session.observe(:render, {
95
+ duration: notification.duration.round(2),
96
+ gc: notification.respond_to?(:gc_time) ? notification.gc_time.round(2) : 0.0,
97
+ allocations: notification.respond_to?(:allocations) ? notification.allocations : 0,
98
+ cached: notification.payload[:cache_hit] ? true : nil,
99
+ }.compact)
100
+ end
101
+ end
102
+
103
+ session.measurement(:render, origin: "Herb Engine (Runtime)", code: "render-time", description: method(:render_description)) do |renders|
104
+ duration = renders.sum { |render| render[:duration] }.round(1)
105
+
106
+ renders.size > 1 ? "#{duration} ms over #{renders.size} renders" : "#{duration} ms"
107
+ end
108
+ end
109
+
110
+ def self.install_translations
111
+ session.measurement(
112
+ :output,
113
+ origin: "Herb Engine (Runtime)",
114
+ code: "rendered-output",
115
+ kind: :value,
116
+ per: :position,
117
+ description: ->(values) { "This ERB tag rendered #{values.last.to_s.strip.inspect} when the page was last built." }
118
+ ) { |values| values.last.to_s }
119
+ end
120
+
121
+ def self.render_description(renders)
122
+ duration = renders.sum { |render| render[:duration] }
123
+ gc = renders.sum { |render| render[:gc] }
124
+ allocations = renders.sum { |render| render[:allocations] }
125
+ cached = renders.count { |render| render[:cached] }
126
+
127
+ parts = ["taking #{duration.round(1)} ms"]
128
+ parts << "#{gc.round(1)} ms of it in GC" if gc.positive?
129
+ parts << "allocating #{allocations.to_fs(:delimited)} objects" if allocations.positive?
130
+ parts << "#{cached} served from cache" if cached.positive?
131
+
132
+ if renders.size > 1
133
+ "This tag rendered #{renders.size} times, #{parts.to_sentence}. Slowest was #{renders.map { |render| render[:duration] }.max.round(1)} ms."
134
+ else
135
+ "This tag rendered once, #{parts.to_sentence}."
136
+ end
137
+ end
138
+
139
+ def self.session
140
+ ::Herb::Engine::Runtime::Session
141
+ end
142
+ end
143
+ end
@@ -10,17 +10,31 @@ module ReActionView
10
10
  # end
11
11
  #
12
12
  PRECOMPILE_ASSETS = %w[
13
+ reactionview.esm.js
13
14
  reactionview-dev-tools.esm.js
14
15
  reactionview-dev-tools.umd.js
15
16
  ].freeze
16
17
 
18
+ def self.gem_root
19
+ Gem::Specification.find_by_name("reactionview").gem_dir
20
+ end
21
+
17
22
  initializer "reactionview.assets", after: :load_config_initializers do |app|
18
- if ReActionView.config.debug_mode_enabled? && app.config.respond_to?(:assets)
19
- gem_root = Gem::Specification.find_by_name("reactionview").gem_dir
23
+ next unless app.config.respond_to?(:assets)
20
24
 
21
- app.config.assets.paths << File.join(gem_root, "app", "assets", "javascripts")
22
- app.config.assets.precompile += PRECOMPILE_ASSETS
23
- end
25
+ app.config.assets.paths << File.join(ReActionView::Railtie.gem_root, "app", "assets", "javascripts")
26
+ app.config.assets.precompile += PRECOMPILE_ASSETS
27
+ end
28
+
29
+ initializer "reactionview.importmap", before: "importmap" do |app|
30
+ next unless app.config.respond_to?(:importmap)
31
+
32
+ app.config.importmap.paths << File.join(ReActionView::Railtie.gem_root, "lib", "reactionview", "importmap.rb")
33
+ app.config.importmap.cache_sweepers << File.join(ReActionView::Railtie.gem_root, "app", "assets", "javascripts")
34
+ end
35
+
36
+ initializer "reactionview.deprecator" do |app|
37
+ app.deprecators[:reactionview] = ReActionView.deprecator if app.respond_to?(:deprecators)
24
38
  end
25
39
 
26
40
  initializer "reactionview.asset_manifest_check" do |app|
@@ -31,6 +45,104 @@ module ReActionView
31
45
  app.middleware.use ReActionView::Middleware::AssetManifestCheck
32
46
  end
33
47
 
48
+ initializer "reactionview.diagnostics" do |app|
49
+ next unless ReActionView.config.debug_mode_enabled? || ReActionView.config.validation_mode == :overlay || ReActionView.config.slots
50
+
51
+ require "herb/engine/runtime/middleware"
52
+
53
+ app.middleware.use ::Herb::Engine::Runtime::Middleware
54
+ end
55
+
56
+ initializer "reactionview.instrumentation", after: :load_config_initializers do |_app|
57
+ next unless ReActionView.config.instrumentation.enabled
58
+
59
+ require_relative "instrumentation"
60
+
61
+ ReActionView::Instrumentation.install!
62
+ end
63
+
64
+ initializer "reactionview.error_page", after: :load_config_initializers do |app|
65
+ next unless ReActionView.config.development?
66
+
67
+ require "herb/engine/runtime/error_page"
68
+
69
+ app.middleware.use(
70
+ ::Herb::Engine::Runtime::ErrorPage,
71
+ dev_tools: -> { ReActionView::Railtie.dev_tools_module_path },
72
+ dev_server_port: -> { ReActionView.config.dev_server_port }
73
+ )
74
+ end
75
+
76
+ def self.dev_tools_module_path
77
+ ActionController::Base.helpers.asset_path("reactionview-dev-tools.esm.js")
78
+ end
79
+
80
+ initializer "reactionview.slots" do |app|
81
+ next unless ReActionView.config.slots
82
+
83
+ Mime::Type.register ReActionView::Slots::MIME_TYPE, ReActionView::Slots::FORMAT unless Mime[ReActionView::Slots::FORMAT]
84
+
85
+ ActiveSupport.on_load(:action_view) do
86
+ include ReActionView::Slots::StateOverridesHelper
87
+ end
88
+
89
+ ActiveSupport.on_load(:action_controller_base) do
90
+ include ReActionView::Slots::StateOverridesHelper
91
+ prepend ReActionView::Slots::Rendering
92
+
93
+ app.config.paths["app/views"].existent.each do |path|
94
+ prepend_view_path ReActionView::Slots::Resolver.new(path)
95
+ end
96
+ end
97
+ end
98
+
99
+ initializer "reactionview.slots.reloading" do |app|
100
+ next unless ReActionView.config.slots
101
+
102
+ views = app.config.paths["app/views"].existent
103
+
104
+ next if views.empty?
105
+
106
+ watcher = app.config.file_watcher.new([], views.index_with { %w[erb] }) do
107
+ ReActionView::Slots.reset_dependencies!
108
+ end
109
+
110
+ app.reloaders << watcher
111
+ app.reloader.to_run { watcher.execute_if_updated }
112
+ end
113
+
114
+ initializer "reactionview.slots.dev_server", after: :load_config_initializers do |app|
115
+ next unless ReActionView.config.slots
116
+ next unless ReActionView.config.development?
117
+ next unless ReActionView.config.dev_server_enabled?
118
+
119
+ begin
120
+ require "herb/dev"
121
+ rescue LoadError
122
+ next
123
+ end
124
+
125
+ ::Herb::Dev.compiler = ReActionView::Slots::DevCompiler.new
126
+
127
+ view_paths = app.config.paths["app/views"].existent
128
+
129
+ app.server { Railtie.boot_dev_server(view_paths: view_paths) }
130
+ end
131
+
132
+ def self.boot_dev_server(view_paths: nil)
133
+ embedded = ::Herb::Dev.boot(
134
+ ReActionView.config.project_path,
135
+ watch_paths: view_paths,
136
+ logger: ->(message) { Rails.logger.info("[Herb Dev Server] #{message}") }
137
+ )
138
+
139
+ if embedded
140
+ $stdout.puts "* Herb Dev Server: ws://localhost:#{embedded.server.port} (embedded)"
141
+ else
142
+ $stdout.puts "* Herb Dev Server: not started, see the log"
143
+ end
144
+ end
145
+
34
146
  initializer "reactionview.register_herb_handler" do
35
147
  ActiveSupport.on_load(:action_view) do
36
148
  ActionView::Template.register_template_handler :herb, ReActionView::Template::Handlers::Herb
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReActionView
4
+ module Slots
5
+ # Compiles one template for the Herb dev server, the way the application would.
6
+ #
7
+ # The dev server's watcher calls this from its own thread whenever a template changes, so
8
+ # the compile runs inside the Rails executor for autoload and reloader safety. A raise is
9
+ # left to the caller, which turns it into diagnostics instead of crashing the thread.
10
+ #
11
+ class DevCompiler
12
+ def call(source, relative_path)
13
+ absolute = File.expand_path(relative_path, ReActionView.config.project_path)
14
+
15
+ result = Rails.application.executor.wrap do
16
+ ReActionView::Template::Handlers::Herb.compile_for_schema(source, absolute)
17
+ end
18
+
19
+ ReActionView::Slots.reset_dependencies!
20
+
21
+ result
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module ReActionView
6
+ module Slots
7
+ module Rendering
8
+ BODY_END_TAG = "</body>"
9
+ BLOCK_HEADER = "Herb-Block"
10
+
11
+ def _normalize_options(options)
12
+ super
13
+
14
+ options[:layout] = false if slots_request?
15
+
16
+ options
17
+ end
18
+
19
+ def render_to_body(options = {})
20
+ protect_steered_response
21
+
22
+ if (block = scoped_block_index)
23
+ begin
24
+ return ::JSON.generate(render_scoped_block(block, options))
25
+ rescue ::StandardError => e
26
+ raise unless ReActionView.config.development?
27
+
28
+ return render_slots_error(e)
29
+ end
30
+ end
31
+
32
+ begin
33
+ rendered = super
34
+ rescue ::StandardError => e
35
+ raise unless slots_request? && ReActionView.config.development?
36
+
37
+ return render_slots_error(e)
38
+ end
39
+
40
+ return ::JSON.generate(merge_schema(rendered, options)) if rendered.is_a?(::Hash)
41
+
42
+ deliver_slot_dependencies(rendered, options)
43
+
44
+ rendered
45
+ end
46
+
47
+ private
48
+
49
+ def protect_steered_response
50
+ return unless slots_request?
51
+ return unless request.respond_to?(:headers) && respond_to?(:response) && response
52
+ return if request.headers[StateOverrides::HEADER].nil?
53
+
54
+ response.headers["Cache-Control"] = "no-store"
55
+ end
56
+
57
+ def render_slots_error(error)
58
+ self.status = 500
59
+
60
+ cause = error.cause || error
61
+ template = error.respond_to?(:template) && error.template
62
+
63
+ entry = {
64
+ class: cause.class.name,
65
+ message: cause.message,
66
+ template: template.respond_to?(:short_identifier) ? template.short_identifier : nil,
67
+ backtrace: cleaned_backtrace(cause),
68
+ }
69
+
70
+ ::JSON.generate({ error: entry })
71
+ end
72
+
73
+ def cleaned_backtrace(cause)
74
+ raw = cause.backtrace || []
75
+ cleaned = ::Rails.backtrace_cleaner.clean(raw)
76
+
77
+ (cleaned.empty? ? raw : cleaned).first(5)
78
+ end
79
+
80
+ def merge_schema(rendered, options)
81
+ return rendered unless request&.headers&.[]("Herb-Schema").present?
82
+
83
+ entry = entry_point_for(options)
84
+
85
+ return rendered unless entry
86
+
87
+ schema = ReActionView::Template::Handlers::Herb.compile_for_schema(::File.read(entry), entry)
88
+
89
+ rendered.merge(schema: {
90
+ mode: schema.mode,
91
+ version: schema.version,
92
+ manifest: schema.manifest,
93
+ static_markup: schema.static_markup,
94
+ statics: schema.statics,
95
+ })
96
+ rescue ::StandardError => e
97
+ logger = defined?(::Rails) && ::Rails.logger
98
+ logger&.debug { "ReActionView could not build the schema envelope: #{e.class}: #{e.message}" }
99
+
100
+ rendered
101
+ end
102
+
103
+ def scoped_block_index
104
+ return nil unless slots_request?
105
+
106
+ value = request.headers[BLOCK_HEADER]
107
+
108
+ value&.match?(/\A\d+\z/) ? Integer(value, 10) : nil
109
+ end
110
+
111
+ def render_scoped_block(index, options)
112
+ entry = entry_point_for(options)
113
+
114
+ raise ::ArgumentError, "a scoped block request found no entry template" unless entry
115
+
116
+ program = ReActionView::Slots.block_program(entry, index)
117
+
118
+ raise ::ArgumentError, "the entry template compiles no slots, so it holds no block \#{index}" unless program
119
+
120
+ view_context.instance_eval(program, entry)
121
+ end
122
+
123
+ def slots_request?
124
+ respond_to?(:request) && request&.format&.symbol == Slots::FORMAT
125
+ end
126
+
127
+ def deliver_slot_dependencies(body, options)
128
+ return if slots_request?
129
+ return unless body.is_a?(::String)
130
+ return unless body.include?(Slots::REGION_MARKER)
131
+
132
+ entry = entry_point_for(options)
133
+
134
+ return unless entry
135
+
136
+ Slots.dependencies.deliver(entry)
137
+ rescue ::StandardError => e
138
+ logger = defined?(::Rails) && ::Rails.logger
139
+ logger&.debug { "ReActionView could not build the slot dependency map: #{e.class}: #{e.message}" }
140
+
141
+ nil
142
+ end
143
+
144
+ def entry_point_for(options)
145
+ name = options[:template] || (respond_to?(:action_name) ? action_name : nil)
146
+
147
+ return nil unless name
148
+
149
+ template = lookup_context.find(name.to_s, options[:prefixes] || _prefixes, false)
150
+
151
+ template&.identifier
152
+ rescue ::ActionView::MissingTemplate
153
+ nil
154
+ end
155
+ end
156
+ end
157
+ end