view_component 4.20.0 → 4.21.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/docs/CHANGELOG.md +94 -0
- data/lib/generators/view_component/abstract_generator.rb +24 -4
- data/lib/generators/view_component/component/component_generator.rb +8 -6
- data/lib/generators/view_component/component/templates/component.rb.tt +1 -1
- data/lib/generators/view_component/erb/erb_generator.rb +2 -2
- data/lib/generators/view_component/haml/haml_generator.rb +1 -1
- data/lib/generators/view_component/locale/locale_generator.rb +2 -6
- data/lib/generators/view_component/preview/preview_generator.rb +9 -6
- data/lib/generators/view_component/preview/templates/component_preview.rb.tt +2 -2
- data/lib/generators/view_component/rspec/rspec_generator.rb +1 -1
- data/lib/generators/view_component/rspec/templates/component_spec.rb.tt +1 -1
- data/lib/generators/view_component/slim/slim_generator.rb +1 -1
- data/lib/generators/view_component/stimulus/stimulus_generator.rb +3 -7
- data/lib/generators/view_component/test_unit/templates/component_test.rb.tt +2 -2
- data/lib/generators/view_component/test_unit/test_unit_generator.rb +8 -7
- data/lib/view_component/base.rb +106 -98
- data/lib/view_component/cache_digest/dependency_tracking.rb +6 -6
- data/lib/view_component/cache_digest/digestor_lookup.rb +3 -1
- data/lib/view_component/cache_digest/resolver.rb +59 -38
- data/lib/view_component/cache_digest.rb +161 -53
- data/lib/view_component/cacheable.rb +16 -18
- data/lib/view_component/collection.rb +4 -9
- data/lib/view_component/compile_cache.rb +44 -5
- data/lib/view_component/compiler.rb +50 -2
- data/lib/view_component/config.rb +114 -8
- data/lib/view_component/engine.rb +1 -2
- data/lib/view_component/errors.rb +16 -0
- data/lib/view_component/slot.rb +12 -0
- data/lib/view_component/slotable.rb +28 -4
- data/lib/view_component/template.rb +16 -3
- data/lib/view_component/test_helpers.rb +38 -16
- data/lib/view_component/translatable.rb +34 -0
- data/lib/view_component/version.rb +1 -1
- metadata +1 -1
data/lib/view_component/base.rb
CHANGED
|
@@ -32,6 +32,9 @@ end
|
|
|
32
32
|
|
|
33
33
|
module ViewComponent
|
|
34
34
|
class Base
|
|
35
|
+
EMPTY_OUTPUT = "".html_safe.freeze
|
|
36
|
+
private_constant :EMPTY_OUTPUT
|
|
37
|
+
|
|
35
38
|
class << self
|
|
36
39
|
# A digest of the component's source, ancestors, and rendered dependencies.
|
|
37
40
|
#
|
|
@@ -42,7 +45,7 @@ module ViewComponent
|
|
|
42
45
|
def cache_digest(finder: nil, format: :html)
|
|
43
46
|
ViewComponent::CacheDigest.digest(
|
|
44
47
|
self,
|
|
45
|
-
finder: finder
|
|
48
|
+
finder: finder,
|
|
46
49
|
format: format
|
|
47
50
|
)
|
|
48
51
|
end
|
|
@@ -121,86 +124,14 @@ module ViewComponent
|
|
|
121
124
|
# @return [String]
|
|
122
125
|
def render_in(view_context, **, &block)
|
|
123
126
|
self.class.__vc_compile(raise_errors: true) unless self.class.__vc_compiled?
|
|
124
|
-
|
|
125
|
-
if self.class.__vc_cache_output && !@__vc_cache_bypassed
|
|
126
|
-
return __vc_render_cached(view_context, block)
|
|
127
|
-
end
|
|
128
|
-
|
|
129
127
|
__vc_check_reused_instance!
|
|
130
|
-
__vc_reset_render_state!
|
|
131
|
-
|
|
132
|
-
@view_context = view_context
|
|
133
|
-
@old_virtual_path = view_context.instance_variable_get(:@virtual_path)
|
|
134
|
-
self.__vc_original_view_context = view_context
|
|
135
|
-
|
|
136
|
-
@output_buffer = view_context.output_buffer
|
|
137
|
-
|
|
138
|
-
@lookup_context = view_context.lookup_context
|
|
139
|
-
|
|
140
|
-
# For content_for
|
|
141
|
-
@view_flow = view_context.view_flow
|
|
142
|
-
|
|
143
|
-
# For i18n
|
|
144
|
-
@virtual_path ||= virtual_path
|
|
145
|
-
|
|
146
|
-
# Describes the inferred request constraints (locales, formats, variants)
|
|
147
|
-
@__vc_requested_details = @lookup_context.vc_requested_details
|
|
148
|
-
|
|
149
|
-
# For caching, such as #cache_if
|
|
150
|
-
@current_template = nil unless defined?(@current_template)
|
|
151
|
-
old_current_template = @current_template
|
|
152
128
|
|
|
153
|
-
if
|
|
154
|
-
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
@__vc_content_evaluated = false
|
|
158
|
-
remove_instance_variable(:@__vc_content) if defined?(@__vc_content)
|
|
159
|
-
@__vc_render_in_block = block
|
|
160
|
-
@view_context.instance_variable_set(:@virtual_path, virtual_path)
|
|
161
|
-
|
|
162
|
-
before_render
|
|
163
|
-
|
|
164
|
-
if render?
|
|
165
|
-
value = nil
|
|
166
|
-
|
|
167
|
-
@output_buffer.with_buffer do
|
|
168
|
-
inner_rendered_template = nil
|
|
169
|
-
around_rendered_template = around_render do
|
|
170
|
-
inner_rendered_template = render_template_for(@__vc_requested_details).to_s
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
# If `around_render` returned the same object the block yielded, the inner
|
|
174
|
-
# template's escaping is authoritative and we can trust the result. If the
|
|
175
|
-
# user replaced/wrapped the value, re-check HTML safety to prevent
|
|
176
|
-
# bypassing the escaping applied to normal `#call` return values
|
|
177
|
-
# (GHSA-97jw-64cj-jc58).
|
|
178
|
-
rendered_template = if around_rendered_template.equal?(inner_rendered_template)
|
|
179
|
-
around_rendered_template
|
|
180
|
-
else
|
|
181
|
-
__vc_safe_around_render_output(around_rendered_template)
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
# Avoid allocating new string when output_preamble and output_postamble are blank
|
|
185
|
-
value = if output_preamble.blank? && output_postamble.blank?
|
|
186
|
-
rendered_template
|
|
187
|
-
else
|
|
188
|
-
__vc_safe_output_preamble + rendered_template + __vc_safe_output_postamble
|
|
189
|
-
end
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
if ActionView::Base.annotate_rendered_view_with_filenames && current_template.inline_call? && request&.format == :html
|
|
193
|
-
identifier = defined?(Rails.root) ? self.class.identifier.sub("#{Rails.root}/", "") : self.class.identifier
|
|
194
|
-
value = "<!-- BEGIN #{identifier} -->".html_safe + value + "<!-- END #{identifier} -->".html_safe
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
value
|
|
129
|
+
if self.class.__vc_cache_output
|
|
130
|
+
__vc_render_cached(view_context, block)
|
|
198
131
|
else
|
|
199
|
-
|
|
132
|
+
__vc_render_uncached(view_context, &block)
|
|
200
133
|
end
|
|
201
134
|
ensure
|
|
202
|
-
view_context.instance_variable_set(:@virtual_path, @old_virtual_path)
|
|
203
|
-
@current_template = old_current_template
|
|
204
135
|
@__vc_rendered = true
|
|
205
136
|
end
|
|
206
137
|
|
|
@@ -232,14 +163,21 @@ module ViewComponent
|
|
|
232
163
|
# When rendering the parent inside an .erb template, use `#render_parent` instead.
|
|
233
164
|
def render_parent_to_string
|
|
234
165
|
@__vc_parent_render_level ||= 0 # ensure a good starting value
|
|
166
|
+
old_current_template = @current_template
|
|
235
167
|
|
|
236
168
|
begin
|
|
237
169
|
target_render = self.class.instance_variable_get(:@__vc_ancestor_calls)[@__vc_parent_render_level]
|
|
238
170
|
@__vc_parent_render_level += 1
|
|
239
171
|
|
|
172
|
+
unless target_render.owner.__vc_compiled?
|
|
173
|
+
target_render.owner.__vc_compile(raise_errors: true)
|
|
174
|
+
target_render = target_render.owner.instance_method(:render_template_for)
|
|
175
|
+
end
|
|
176
|
+
|
|
240
177
|
target_render.bind_call(self, @__vc_requested_details)
|
|
241
178
|
ensure
|
|
242
179
|
@__vc_parent_render_level -= 1
|
|
180
|
+
@current_template = old_current_template
|
|
243
181
|
end
|
|
244
182
|
end
|
|
245
183
|
|
|
@@ -247,14 +185,14 @@ module ViewComponent
|
|
|
247
185
|
#
|
|
248
186
|
# @return [String]
|
|
249
187
|
def output_preamble
|
|
250
|
-
|
|
188
|
+
EMPTY_OUTPUT
|
|
251
189
|
end
|
|
252
190
|
|
|
253
191
|
# Optional content to be returned after the rendered template.
|
|
254
192
|
#
|
|
255
193
|
# @return [String]
|
|
256
194
|
def output_postamble
|
|
257
|
-
|
|
195
|
+
EMPTY_OUTPUT
|
|
258
196
|
end
|
|
259
197
|
|
|
260
198
|
# Called before rendering the component. Override to perform operations that
|
|
@@ -289,7 +227,7 @@ module ViewComponent
|
|
|
289
227
|
#
|
|
290
228
|
# @private
|
|
291
229
|
def render(options = {}, args = {}, &block)
|
|
292
|
-
if options.respond_to?(:set_original_view_context)
|
|
230
|
+
if options.is_a?(ViewComponent::Base) || options.respond_to?(:set_original_view_context)
|
|
293
231
|
options.set_original_view_context(__vc_original_view_context)
|
|
294
232
|
|
|
295
233
|
# We assume options is a component, so there's no need to evaluate the
|
|
@@ -448,20 +386,99 @@ module ViewComponent
|
|
|
448
386
|
# @private
|
|
449
387
|
def digest_path_from_template(template)
|
|
450
388
|
component_path = self.class.virtual_path
|
|
451
|
-
return super unless component_path && template.
|
|
389
|
+
return super unless component_path && template.is_a?(ViewComponent::Template)
|
|
452
390
|
|
|
453
391
|
digest = self.class.cache_digest(
|
|
454
392
|
finder: lookup_context,
|
|
455
393
|
format: template.format || lookup_context.formats.first || :html
|
|
456
394
|
)
|
|
457
395
|
|
|
458
|
-
digest.present? ? "#{
|
|
396
|
+
digest.present? ? "#{template.virtual_path}:#{digest}" : template.virtual_path
|
|
459
397
|
end
|
|
460
398
|
|
|
461
399
|
private
|
|
462
400
|
|
|
463
401
|
attr_reader :view_context
|
|
464
402
|
|
|
403
|
+
def __vc_render_uncached(view_context, &block)
|
|
404
|
+
old_virtual_path = view_context.instance_variable_get(:@virtual_path)
|
|
405
|
+
__vc_reset_render_state!
|
|
406
|
+
|
|
407
|
+
@view_context = view_context
|
|
408
|
+
@old_virtual_path = old_virtual_path
|
|
409
|
+
self.__vc_original_view_context = view_context
|
|
410
|
+
|
|
411
|
+
@output_buffer = view_context.output_buffer
|
|
412
|
+
|
|
413
|
+
@lookup_context = view_context.lookup_context
|
|
414
|
+
|
|
415
|
+
# For content_for
|
|
416
|
+
@view_flow = view_context.view_flow
|
|
417
|
+
|
|
418
|
+
# For i18n
|
|
419
|
+
component_virtual_path = virtual_path
|
|
420
|
+
@virtual_path ||= component_virtual_path
|
|
421
|
+
|
|
422
|
+
# Describes the inferred request constraints (locales, formats, variants)
|
|
423
|
+
@__vc_requested_details = @lookup_context.vc_requested_details
|
|
424
|
+
|
|
425
|
+
# For caching, such as #cache_if
|
|
426
|
+
@current_template = nil unless defined?(@current_template)
|
|
427
|
+
old_current_template = @current_template
|
|
428
|
+
|
|
429
|
+
if block && defined?(@__vc_content_set_by_with_content)
|
|
430
|
+
raise DuplicateContentError.new(self.class.name)
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
@__vc_content_evaluated = false
|
|
434
|
+
remove_instance_variable(:@__vc_content) if defined?(@__vc_content)
|
|
435
|
+
@__vc_render_in_block = block
|
|
436
|
+
@view_context.instance_variable_set(:@virtual_path, component_virtual_path)
|
|
437
|
+
|
|
438
|
+
before_render
|
|
439
|
+
|
|
440
|
+
if render?
|
|
441
|
+
value = nil
|
|
442
|
+
|
|
443
|
+
@output_buffer.with_buffer do
|
|
444
|
+
inner_rendered_template = nil
|
|
445
|
+
around_rendered_template = around_render do
|
|
446
|
+
inner_rendered_template = render_template_for(@__vc_requested_details).to_s
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
# If `around_render` returned the same object the block yielded, the inner
|
|
450
|
+
# template's escaping is authoritative and we can trust the result. If the
|
|
451
|
+
# user replaced/wrapped the value, re-check HTML safety to prevent
|
|
452
|
+
# bypassing the escaping applied to normal `#call` return values
|
|
453
|
+
# (GHSA-97jw-64cj-jc58).
|
|
454
|
+
rendered_template = if around_rendered_template.equal?(inner_rendered_template)
|
|
455
|
+
around_rendered_template
|
|
456
|
+
else
|
|
457
|
+
__vc_safe_around_render_output(around_rendered_template)
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# Avoid allocating new string when output_preamble and output_postamble are blank
|
|
461
|
+
value = if output_preamble.blank? && output_postamble.blank?
|
|
462
|
+
rendered_template
|
|
463
|
+
else
|
|
464
|
+
__vc_safe_output_preamble + rendered_template + __vc_safe_output_postamble
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
if ActionView::Base.annotate_rendered_view_with_filenames && current_template.inline_call? && request&.format == :html
|
|
469
|
+
identifier = defined?(Rails.root) ? self.class.identifier.sub("#{Rails.root}/", "") : self.class.identifier
|
|
470
|
+
value = "<!-- BEGIN #{identifier} -->".html_safe + value + "<!-- END #{identifier} -->".html_safe
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
value
|
|
474
|
+
else
|
|
475
|
+
"".html_safe
|
|
476
|
+
end
|
|
477
|
+
ensure
|
|
478
|
+
view_context.instance_variable_set(:@virtual_path, old_virtual_path)
|
|
479
|
+
@current_template = old_current_template
|
|
480
|
+
end
|
|
481
|
+
|
|
465
482
|
def __vc_render_in_block_provided?
|
|
466
483
|
defined?(@view_context) && @view_context && @__vc_render_in_block
|
|
467
484
|
end
|
|
@@ -471,15 +488,12 @@ module ViewComponent
|
|
|
471
488
|
end
|
|
472
489
|
|
|
473
490
|
def __vc_maybe_escape_html(text)
|
|
491
|
+
return text if text.html_safe?
|
|
474
492
|
return text if @current_template && !@current_template.html?
|
|
475
493
|
return text if text.blank?
|
|
476
494
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
else
|
|
480
|
-
yield
|
|
481
|
-
html_escape(text)
|
|
482
|
-
end
|
|
495
|
+
yield
|
|
496
|
+
html_escape(text)
|
|
483
497
|
end
|
|
484
498
|
|
|
485
499
|
def __vc_safe_output_preamble
|
|
@@ -508,16 +522,10 @@ module ViewComponent
|
|
|
508
522
|
# slot setters or `with_content`); reuse of an instance that has slot or
|
|
509
523
|
# `with_content` state is guarded against separately by
|
|
510
524
|
# `__vc_check_reused_instance!`.
|
|
511
|
-
RENDER_STATE_IVARS = %i[
|
|
512
|
-
@__vc_controller
|
|
513
|
-
@__vc_helpers
|
|
514
|
-
@__vc_request
|
|
515
|
-
].freeze
|
|
516
|
-
|
|
517
525
|
def __vc_reset_render_state!
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
526
|
+
remove_instance_variable(:@__vc_controller) if defined?(@__vc_controller)
|
|
527
|
+
remove_instance_variable(:@__vc_helpers) if defined?(@__vc_helpers)
|
|
528
|
+
remove_instance_variable(:@__vc_request) if defined?(@__vc_request)
|
|
521
529
|
end
|
|
522
530
|
|
|
523
531
|
# Raises when a component instance is rendered more than once.
|
|
@@ -733,7 +741,7 @@ module ViewComponent
|
|
|
733
741
|
|
|
734
742
|
# @private
|
|
735
743
|
def __vc_compiled?
|
|
736
|
-
|
|
744
|
+
CompileCache.compiled?(self)
|
|
737
745
|
end
|
|
738
746
|
|
|
739
747
|
# Hook called by the compiler after a component is compiled.
|
|
@@ -915,7 +923,7 @@ module ViewComponent
|
|
|
915
923
|
end
|
|
916
924
|
|
|
917
925
|
def __vc_files_ending_with(extensions, pattern)
|
|
918
|
-
paths =
|
|
926
|
+
paths = CompileCache.sidecar_files_matching(pattern)
|
|
919
927
|
extensions.flat_map { |extension| paths.select { |path| path.end_with?(extension) } }
|
|
920
928
|
end
|
|
921
929
|
|
|
@@ -26,15 +26,15 @@ module ViewComponent
|
|
|
26
26
|
dependencies = dependencies - [declared] + [virtual_path]
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return dependencies unless CacheDigest::CONSTANT_RENDER.match?(source)
|
|
29
|
+
# Synthetic templates already declare every dependency found in their sources.
|
|
30
|
+
return dependencies if name.start_with?("#{CacheDigest::VIRTUAL_PATH_PREFIX}/")
|
|
31
|
+
return dependencies unless CacheDigest.template_dependencies?(source, template.handler)
|
|
33
32
|
|
|
34
33
|
component_paths, inferred_partials =
|
|
35
|
-
CacheDigest.
|
|
34
|
+
CacheDigest.template_renders_in(CacheDigest.compiled_source(template), name, view_paths: view_paths)
|
|
35
|
+
declared = source.scan(CacheDigest::EXPLICIT_DEPENDENCY).flatten
|
|
36
36
|
|
|
37
|
-
(dependencies - inferred_partials + component_paths).uniq
|
|
37
|
+
(dependencies - (inferred_partials - declared) + component_paths).uniq
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
# @private
|
|
@@ -24,7 +24,9 @@ module ViewComponent
|
|
|
24
24
|
private
|
|
25
25
|
|
|
26
26
|
def find_template(finder, name, prefixes, partial, keys)
|
|
27
|
-
Resolver.instance.find_templates(
|
|
27
|
+
Resolver.instance.find_templates(
|
|
28
|
+
name, prefixes.first, partial, {formats: finder.formats}, view_paths: finder.view_paths
|
|
29
|
+
).first || super
|
|
28
30
|
end
|
|
29
31
|
end
|
|
30
32
|
end
|
|
@@ -18,6 +18,10 @@ module ViewComponent
|
|
|
18
18
|
# Extensions whose contents are hashed into the synthetic source.
|
|
19
19
|
SIDECAR_EXTENSIONS = %w[yml yaml].freeze
|
|
20
20
|
|
|
21
|
+
RELATIVE_PARTIAL_PREFIX = "view_component/relative_partial/"
|
|
22
|
+
RELATIVE_DEPENDENCY = /<%# Relative Template Dependency: (\S+) %>/
|
|
23
|
+
private_constant :RELATIVE_PARTIAL_PREFIX, :RELATIVE_DEPENDENCY
|
|
24
|
+
|
|
21
25
|
# Where ViewComponent's own modules, such as the ones it includes into
|
|
22
26
|
# every component, are defined.
|
|
23
27
|
VIEW_COMPONENT_LIB = "#{::File.expand_path("..", __dir__)}/".freeze
|
|
@@ -28,12 +32,12 @@ module ViewComponent
|
|
|
28
32
|
end
|
|
29
33
|
end
|
|
30
34
|
|
|
31
|
-
def find_templates(name, prefix, partial, details, locals = [])
|
|
35
|
+
def find_templates(name, prefix, partial, details, locals = [], view_paths: ActionController::Base.view_paths)
|
|
32
36
|
virtual_path = [prefix.presence, name].compact.join("/")
|
|
33
37
|
component = CacheDigest.component_for(virtual_path)
|
|
34
38
|
return [] unless component
|
|
35
39
|
|
|
36
|
-
[build_template(component, virtual_path, details)]
|
|
40
|
+
[build_template(component, virtual_path, details, view_paths)]
|
|
37
41
|
end
|
|
38
42
|
|
|
39
43
|
def to_s
|
|
@@ -74,7 +78,11 @@ module ViewComponent
|
|
|
74
78
|
# the template sources, where they'd also infer partials from component
|
|
75
79
|
# renders.
|
|
76
80
|
dependencies(component, templates).each do |dependency|
|
|
77
|
-
parts <<
|
|
81
|
+
parts << if dependency.start_with?(RELATIVE_PARTIAL_PREFIX)
|
|
82
|
+
"<%# Relative Template Dependency: #{dependency.delete_prefix(RELATIVE_PARTIAL_PREFIX)} %>"
|
|
83
|
+
else
|
|
84
|
+
"<%# Template Dependency: #{dependency} %>"
|
|
85
|
+
end
|
|
78
86
|
end
|
|
79
87
|
|
|
80
88
|
# Content hashes of the templates, including inline ones.
|
|
@@ -87,9 +95,10 @@ module ViewComponent
|
|
|
87
95
|
|
|
88
96
|
private
|
|
89
97
|
|
|
90
|
-
def build_template(component, virtual_path, details)
|
|
98
|
+
def build_template(component, virtual_path, details, view_paths)
|
|
99
|
+
source = component.__vc_cache_digest_source || source_for(component)
|
|
91
100
|
ActionView::Template.new(
|
|
92
|
-
|
|
101
|
+
resolve_relative_dependencies(source, view_paths),
|
|
93
102
|
"view_component cache digest for #{component.name}",
|
|
94
103
|
ActionView::Template.handler_for_extension(:erb),
|
|
95
104
|
locals: [],
|
|
@@ -98,15 +107,32 @@ module ViewComponent
|
|
|
98
107
|
)
|
|
99
108
|
end
|
|
100
109
|
|
|
110
|
+
# A relative render uses the calling controller's prefixes. Track all
|
|
111
|
+
# matching partials so precomputed sources work for every controller.
|
|
112
|
+
def resolve_relative_dependencies(source, view_paths)
|
|
113
|
+
return source unless source.include?("Relative Template Dependency:")
|
|
114
|
+
|
|
115
|
+
paths = view_paths.flat_map(&:all_template_paths).uniq
|
|
116
|
+
source.gsub(RELATIVE_DEPENDENCY) do
|
|
117
|
+
name = Regexp.last_match(1)
|
|
118
|
+
matches = paths.filter_map do |path|
|
|
119
|
+
path.to_s if path.partial? && ::File.fnmatch?(name.delete_prefix("_"), path.name)
|
|
120
|
+
end.sort
|
|
121
|
+
matches = [name] if matches.empty?
|
|
122
|
+
matches.map { |path| "<%# Template Dependency: #{path} %>" }.join("\n")
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
101
126
|
# The component and any component superclasses, nearest first. Including
|
|
102
127
|
# ancestors means editing `ApplicationComponent` invalidates every
|
|
103
128
|
# component that inherits from it.
|
|
104
129
|
def component_ancestors(component)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
130
|
+
ancestors = []
|
|
131
|
+
until component == ViewComponent::Base
|
|
132
|
+
ancestors << component
|
|
133
|
+
component = component.superclass
|
|
109
134
|
end
|
|
135
|
+
ancestors
|
|
110
136
|
end
|
|
111
137
|
|
|
112
138
|
# Modules the component and its component superclasses include, such as
|
|
@@ -119,7 +145,11 @@ module ViewComponent
|
|
|
119
145
|
component.ancestors.take_while { |ancestor| ancestor != ViewComponent::Base }.filter_map { |ancestor|
|
|
120
146
|
next if ancestor.is_a?(Class) || ancestor.name.nil?
|
|
121
147
|
|
|
122
|
-
|
|
148
|
+
begin
|
|
149
|
+
path, _line = Object.const_source_location(ancestor.name)
|
|
150
|
+
rescue NameError
|
|
151
|
+
next
|
|
152
|
+
end
|
|
123
153
|
path unless path.nil? || path.start_with?(VIEW_COMPONENT_LIB)
|
|
124
154
|
}.uniq
|
|
125
155
|
end
|
|
@@ -134,26 +164,20 @@ module ViewComponent
|
|
|
134
164
|
.compact.uniq.select { |path| ::File.exist?(path) }
|
|
135
165
|
end
|
|
136
166
|
|
|
137
|
-
def template_files(component)
|
|
138
|
-
component_ancestors(component)
|
|
139
|
-
.flat_map { |ancestor| ancestor.__vc_sidecar_files(ActionView::Template::Handlers.extensions) }
|
|
140
|
-
.uniq
|
|
141
|
-
.select { |path| ::File.exist?(path) }
|
|
142
|
-
end
|
|
143
|
-
|
|
144
167
|
# Sidecar template files plus inline templates, which live in the Ruby
|
|
145
168
|
# file and so are invisible to Action View's trackers.
|
|
146
169
|
#
|
|
147
|
-
# @return [Array<Array(String, String)>]
|
|
170
|
+
# @return [Array<Array(String, String, Class)>] source, extension, and defining component
|
|
148
171
|
def template_sources(component)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
172
|
+
component_ancestors(component).flat_map do |ancestor|
|
|
173
|
+
sources = ancestor.__vc_sidecar_files(ActionView::Template::Handlers.extensions).filter_map do |path|
|
|
174
|
+
[CompileCache.read_file(path), ::File.extname(path).delete_prefix("."), ancestor] if ::File.exist?(path)
|
|
175
|
+
end
|
|
152
176
|
inline_template = ancestor.__vc_inline_template
|
|
153
|
-
sources << [inline_template.source, inline_template.language.to_s] if inline_template
|
|
154
|
-
end
|
|
177
|
+
sources << [inline_template.source, inline_template.language.to_s, ancestor] if inline_template
|
|
155
178
|
|
|
156
|
-
|
|
179
|
+
sources
|
|
180
|
+
end.uniq
|
|
157
181
|
end
|
|
158
182
|
|
|
159
183
|
def explicit_dependencies(sources)
|
|
@@ -173,42 +197,39 @@ module ViewComponent
|
|
|
173
197
|
# Everything but the component and its superclasses, which a component
|
|
174
198
|
# references in its own source but are digested as part of it already.
|
|
175
199
|
def dependencies(component, templates)
|
|
176
|
-
virtual_path =
|
|
200
|
+
virtual_path = "#{RELATIVE_PARTIAL_PREFIX}template"
|
|
177
201
|
ruby = ruby_sources(component)
|
|
178
202
|
own_paths = component_ancestors(component).filter_map { |ancestor| CacheDigest.virtual_path_for(ancestor) }
|
|
179
203
|
|
|
180
204
|
dependencies = explicit_dependencies(ruby + templates.map(&:first)) |
|
|
181
205
|
ruby.flat_map { |source| ruby_dependencies(source, virtual_path) } |
|
|
182
206
|
CacheDigest.slot_renders_of(component) |
|
|
183
|
-
templates.flat_map { |source, extension| template_dependencies(
|
|
207
|
+
templates.flat_map { |source, extension, owner| template_dependencies(owner, source, extension, virtual_path) }
|
|
184
208
|
|
|
185
209
|
dependencies - own_paths
|
|
186
210
|
end
|
|
187
211
|
|
|
188
212
|
# Components and partials rendered from Ruby, such as a `#call` method.
|
|
189
|
-
# Partials are narrowed to paths that appear in the source, discarding
|
|
190
|
-
# the speculative `things/thing` Action View infers from dynamic renders
|
|
191
|
-
# like `render @thing`, which would resolve to nothing.
|
|
192
213
|
def ruby_dependencies(source, virtual_path)
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
components | partials.select { |path|
|
|
196
|
-
source.include?(path) || source.include?(path.sub(%r{[^/]+\z}, '_\0'))
|
|
197
|
-
}
|
|
214
|
+
CacheDigest.renders_in(source, virtual_path, view_paths: ActionController::Base.view_paths).flatten
|
|
198
215
|
end
|
|
199
216
|
|
|
200
217
|
def template_dependencies(component, source, extension, virtual_path)
|
|
201
|
-
|
|
218
|
+
handler = ActionView::Template.handler_for_extension(extension)
|
|
219
|
+
return [] unless CacheDigest.template_dependencies?(source, handler)
|
|
202
220
|
|
|
203
221
|
template = ActionView::Template.new(
|
|
204
222
|
source,
|
|
205
223
|
"view_component cache digest for #{component.name}",
|
|
206
|
-
|
|
224
|
+
handler,
|
|
207
225
|
locals: [],
|
|
208
226
|
format: nil,
|
|
209
227
|
virtual_path: virtual_path
|
|
210
228
|
)
|
|
211
|
-
CacheDigest.renders_in(
|
|
229
|
+
CacheDigest.renders_in(
|
|
230
|
+
CacheDigest.compiled_source(template), virtual_path,
|
|
231
|
+
view_paths: ActionController::Base.view_paths, nesting: [component]
|
|
232
|
+
).flatten
|
|
212
233
|
end
|
|
213
234
|
|
|
214
235
|
# Components loaded from encrypted files, as some commercial gems ship
|
|
@@ -216,7 +237,7 @@ module ViewComponent
|
|
|
216
237
|
# hashed by `file_digest`.
|
|
217
238
|
def ruby_sources(component)
|
|
218
239
|
ruby_files(component).filter_map { |path|
|
|
219
|
-
source =
|
|
240
|
+
source = CompileCache.read_file(path)
|
|
220
241
|
source if source.valid_encoding?
|
|
221
242
|
}
|
|
222
243
|
end
|