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.
- checksums.yaml +4 -4
- data/README.md +22 -39
- data/app/assets/javascripts/reactionview-dev-tools.esm.js +59495 -2633
- data/app/assets/javascripts/reactionview-dev-tools.esm.js.map +1 -1
- data/app/assets/javascripts/reactionview-dev-tools.umd.js +59476 -2617
- data/app/assets/javascripts/reactionview-dev-tools.umd.js.map +1 -1
- data/app/assets/javascripts/reactionview.esm.js +26 -0
- data/app/assets/javascripts/reactionview.esm.js.map +1 -0
- data/lib/generators/reactionview/install_generator.rb +39 -4
- data/lib/reactionview/asset_manifest.rb +1 -1
- data/lib/reactionview/config.rb +99 -4
- data/lib/reactionview/importmap.rb +3 -0
- data/lib/reactionview/instrumentation.rb +143 -0
- data/lib/reactionview/railtie.rb +117 -5
- data/lib/reactionview/slots/dev_compiler.rb +25 -0
- data/lib/reactionview/slots/rendering.rb +157 -0
- data/lib/reactionview/slots/resolver.rb +27 -0
- data/lib/reactionview/slots/state_overrides.rb +68 -0
- data/lib/reactionview/slots.rb +38 -0
- data/lib/reactionview/template/dependencies.rb +90 -0
- data/lib/reactionview/template/handlers/erb.rb +25 -1
- data/lib/reactionview/template/handlers/herb/herb.rb +69 -57
- data/lib/reactionview/template/handlers/herb.rb +188 -13
- data/lib/reactionview/template/local_template.rb +37 -6
- data/lib/reactionview/template/partial_resolver.rb +90 -0
- data/lib/reactionview/version.rb +1 -1
- data/lib/reactionview.rb +13 -0
- data/reactionview.gemspec +3 -2
- metadata +30 -11
|
@@ -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
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
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
|
|
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:
|
data/lib/reactionview/config.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
@
|
|
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
|
-
|
|
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,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
|
data/lib/reactionview/railtie.rb
CHANGED
|
@@ -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
|
-
|
|
19
|
-
gem_root = Gem::Specification.find_by_name("reactionview").gem_dir
|
|
23
|
+
next unless app.config.respond_to?(:assets)
|
|
20
24
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|