view_component 4.17.0 → 4.18.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 +6 -0
- data/lib/view_component/base.rb +49 -0
- data/lib/view_component/cache_digest/digestor_lookup.rb +31 -0
- data/lib/view_component/cache_digest/resolver.rb +19 -15
- data/lib/view_component/cache_digest.rb +49 -31
- data/lib/view_component/{experimentally_cacheable.rb → cacheable.rb} +38 -113
- data/lib/view_component/compiler.rb +10 -0
- data/lib/view_component/engine.rb +9 -3
- data/lib/view_component/version.rb +1 -1
- data/lib/view_component.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2f3ee513a8505728d9295aa0a7c6ce8e228f256058c13f67591ac7697ab4eec1
|
|
4
|
+
data.tar.gz: 7b4a193c71baab6711b130dde0dbe2e84297db77d1e3ae1ac26805fe7272b796
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b196d2a623e878c83325fe8ed684e0d8c938e9daa51668d1fd0665628162d29d6856748d76b65917fe3c6a3849486d77f1a8f50929d1207501fb5d66a2c9d37f
|
|
7
|
+
data.tar.gz: aecd6d15e2e16dac5557f9e9a082b191a9c36a604dbc9e4cd067aedeee0e478e7fd0d380267d2a623977c3b58f6666e9ead18ac8e9119a2c6216f350d14bb834
|
data/docs/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,12 @@ nav_order: 6
|
|
|
10
10
|
|
|
11
11
|
## main
|
|
12
12
|
|
|
13
|
+
## 4.18.0
|
|
14
|
+
|
|
15
|
+
* Enable caching support for all components by default, and rename `ViewComponent::ExperimentallyCacheable` to `ViewComponent::Cacheable`. Including the module is no longer necessary. Breaking: remove existing `include ViewComponent::ExperimentallyCacheable` lines. Caching is no longer experimental.
|
|
16
|
+
|
|
17
|
+
*Erik Axel Nielsen, Joel Hawksley*
|
|
18
|
+
|
|
13
19
|
## 4.17.0
|
|
14
20
|
|
|
15
21
|
* Only register the executor hook that watches component templates when Action View doesn't cache templates at boot. With template caching on, as in production, the hook did nothing but still read `ActionView::Base.cache_template_loading` on every request. That value lives in a class variable, which non-main Ractors can't read on Ruby 4.0, so under Rails' experimental `Rails.application.ractorize!` every request failed.
|
data/lib/view_component/base.rb
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
require "action_view"
|
|
4
4
|
require "action_view/base"
|
|
5
5
|
require "view_component/collection"
|
|
6
|
+
require "view_component/cache_digest"
|
|
6
7
|
require "view_component/compile_cache"
|
|
7
8
|
require "view_component/compiler"
|
|
8
9
|
require "view_component/config"
|
|
9
10
|
require "view_component/errors"
|
|
11
|
+
require "view_component/cacheable"
|
|
10
12
|
require "view_component/inline_template"
|
|
11
13
|
require "view_component/preview"
|
|
12
14
|
require "view_component/request_details"
|
|
@@ -31,6 +33,20 @@ end
|
|
|
31
33
|
module ViewComponent
|
|
32
34
|
class Base
|
|
33
35
|
class << self
|
|
36
|
+
# A digest of the component's source, ancestors, and rendered dependencies.
|
|
37
|
+
#
|
|
38
|
+
# @param finder [ActionView::LookupContext, nil]
|
|
39
|
+
# @param format [Symbol]
|
|
40
|
+
#
|
|
41
|
+
# @return [String]
|
|
42
|
+
def cache_digest(finder: nil, format: :html)
|
|
43
|
+
ViewComponent::CacheDigest.digest(
|
|
44
|
+
self,
|
|
45
|
+
finder: finder || ViewComponent::CacheDigest.default_finder,
|
|
46
|
+
format: format
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
34
50
|
delegate(*ViewComponent::Config.defaults.keys, to: :config)
|
|
35
51
|
|
|
36
52
|
# Returns the current config.
|
|
@@ -106,6 +122,10 @@ module ViewComponent
|
|
|
106
122
|
def render_in(view_context, **, &block)
|
|
107
123
|
self.class.__vc_compile(raise_errors: true) unless self.class.__vc_compiled?
|
|
108
124
|
|
|
125
|
+
if self.class.__vc_cache_output && !@__vc_cache_bypassed
|
|
126
|
+
return __vc_render_cached(view_context, block)
|
|
127
|
+
end
|
|
128
|
+
|
|
109
129
|
__vc_check_reused_instance!
|
|
110
130
|
__vc_reset_render_state!
|
|
111
131
|
|
|
@@ -423,6 +443,21 @@ module ViewComponent
|
|
|
423
443
|
@view_context.instance_variable_set(:@virtual_path, old_virtual_path)
|
|
424
444
|
end
|
|
425
445
|
|
|
446
|
+
# Use the component's source digest for fragments inside its own template.
|
|
447
|
+
#
|
|
448
|
+
# @private
|
|
449
|
+
def digest_path_from_template(template)
|
|
450
|
+
component_path = self.class.virtual_path
|
|
451
|
+
return super unless component_path && template.virtual_path == component_path
|
|
452
|
+
|
|
453
|
+
digest = self.class.cache_digest(
|
|
454
|
+
finder: lookup_context,
|
|
455
|
+
format: template.format || lookup_context.formats.first || :html
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
digest.present? ? "#{component_path}:#{digest}" : component_path
|
|
459
|
+
end
|
|
460
|
+
|
|
426
461
|
private
|
|
427
462
|
|
|
428
463
|
attr_reader :view_context
|
|
@@ -668,6 +703,7 @@ module ViewComponent
|
|
|
668
703
|
# owner is included in a prefix like `ApplicationComponent.inherited`.
|
|
669
704
|
child.identifier = caller_locations(1, 10).reject { |l| l.base_label == "inherited" }[0].path
|
|
670
705
|
child.virtual_path = child.name&.underscore
|
|
706
|
+
ViewComponent::CacheDigest.register(child)
|
|
671
707
|
|
|
672
708
|
# Set collection parameter to the extended component
|
|
673
709
|
child.with_collection_parameter(__vc_provided_collection_parameter)
|
|
@@ -707,6 +743,17 @@ module ViewComponent
|
|
|
707
743
|
@__vc_compiler ||= Compiler.new(self)
|
|
708
744
|
end
|
|
709
745
|
|
|
746
|
+
# Synthetic digest source computed at boot. Not inherited, since each
|
|
747
|
+
# component's source differs.
|
|
748
|
+
#
|
|
749
|
+
# @private
|
|
750
|
+
attr_accessor :__vc_cache_digest_source
|
|
751
|
+
|
|
752
|
+
# @private
|
|
753
|
+
def __vc_template_formats
|
|
754
|
+
__vc_compiler.template_formats
|
|
755
|
+
end
|
|
756
|
+
|
|
710
757
|
# Set the parameter name used when rendering elements of a collection ([documentation](/guide/collections)):
|
|
711
758
|
#
|
|
712
759
|
# ```ruby
|
|
@@ -857,6 +904,8 @@ module ViewComponent
|
|
|
857
904
|
attr_reader :__vc_provided_collection_parameter
|
|
858
905
|
end
|
|
859
906
|
|
|
907
|
+
include ViewComponent::Cacheable
|
|
908
|
+
|
|
860
909
|
ActiveSupport.run_load_hooks(:view_component, self)
|
|
861
910
|
end
|
|
862
911
|
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_view/digestor"
|
|
4
|
+
|
|
5
|
+
module ViewComponent
|
|
6
|
+
module CacheDigest
|
|
7
|
+
# Lets `ActionView::Digestor` find the synthetic templates `Resolver` builds.
|
|
8
|
+
#
|
|
9
|
+
# Prepended to the Digestor's singleton class rather than appending
|
|
10
|
+
# `Resolver` to the application's view paths, so the resolver is only ever
|
|
11
|
+
# consulted while digesting. Code that walks `view_paths` and expects every
|
|
12
|
+
# entry to be a file system resolver is unaffected.
|
|
13
|
+
#
|
|
14
|
+
# @private
|
|
15
|
+
module DigestorLookup
|
|
16
|
+
# @private
|
|
17
|
+
def self.install!
|
|
18
|
+
digestor = ActionView::Digestor.singleton_class
|
|
19
|
+
return if digestor.include?(self)
|
|
20
|
+
|
|
21
|
+
digestor.prepend(self)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def find_template(finder, name, prefixes, partial, keys)
|
|
27
|
+
Resolver.instance.find_templates(name, prefixes.first, partial, {formats: finder.formats}).first || super
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -42,21 +42,11 @@ module ViewComponent
|
|
|
42
42
|
end
|
|
43
43
|
alias_method :==, :eql?
|
|
44
44
|
|
|
45
|
-
private
|
|
46
|
-
|
|
47
|
-
def build_template(component, virtual_path, details)
|
|
48
|
-
ActionView::Template.new(
|
|
49
|
-
source_for(component),
|
|
50
|
-
"view_component cache digest for #{component.name}",
|
|
51
|
-
ActionView::Template.handler_for_extension(:erb),
|
|
52
|
-
locals: [],
|
|
53
|
-
format: Array(details[:formats]).first || :html,
|
|
54
|
-
virtual_path: virtual_path
|
|
55
|
-
)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
45
|
# The synthetic source. Every section exists to change this string when
|
|
59
46
|
# something the component renders from changes.
|
|
47
|
+
#
|
|
48
|
+
# @param component [Class]
|
|
49
|
+
# @return [String]
|
|
60
50
|
def source_for(component)
|
|
61
51
|
parts = []
|
|
62
52
|
|
|
@@ -88,6 +78,19 @@ module ViewComponent
|
|
|
88
78
|
parts.join("\n")
|
|
89
79
|
end
|
|
90
80
|
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def build_template(component, virtual_path, details)
|
|
84
|
+
ActionView::Template.new(
|
|
85
|
+
component.__vc_cache_digest_source || source_for(component),
|
|
86
|
+
"view_component cache digest for #{component.name}",
|
|
87
|
+
ActionView::Template.handler_for_extension(:erb),
|
|
88
|
+
locals: [],
|
|
89
|
+
format: Array(details[:formats]).first || :html,
|
|
90
|
+
virtual_path: virtual_path
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
91
94
|
# The component and any component superclasses, nearest first. Including
|
|
92
95
|
# ancestors means editing `ApplicationComponent` invalidates every
|
|
93
96
|
# component that inherits from it.
|
|
@@ -165,8 +168,9 @@ module ViewComponent
|
|
|
165
168
|
end
|
|
166
169
|
|
|
167
170
|
# Built once at load time rather than memoized, so no class-level state
|
|
168
|
-
# is written after boot.
|
|
169
|
-
# every call so it can't go stale when a
|
|
171
|
+
# is written after boot. Unless sources were precomputed at boot, the
|
|
172
|
+
# resolver reads from disk on every call so it can't go stale when a
|
|
173
|
+
# component changes.
|
|
170
174
|
INSTANCE = new
|
|
171
175
|
end
|
|
172
176
|
end
|
|
@@ -21,15 +21,15 @@ module ViewComponent
|
|
|
21
21
|
# not just its template.
|
|
22
22
|
#
|
|
23
23
|
# This module fixes both, reusing Rails' own `ActionView::Digestor` rather than
|
|
24
|
-
# reimplementing static analysis.
|
|
25
|
-
#
|
|
26
|
-
# every hook here short-circuits.
|
|
24
|
+
# reimplementing static analysis. Every discovered component is tracked
|
|
25
|
+
# automatically, independently of output caching.
|
|
27
26
|
#
|
|
28
27
|
# @private
|
|
29
28
|
module CacheDigest
|
|
30
29
|
extend ActiveSupport::Autoload
|
|
31
30
|
|
|
32
31
|
autoload :DependencyTracking
|
|
32
|
+
autoload :DigestorLookup
|
|
33
33
|
autoload :Resolver
|
|
34
34
|
|
|
35
35
|
# Prefix for the synthetic virtual paths components are digested under.
|
|
@@ -56,8 +56,7 @@ module ViewComponent
|
|
|
56
56
|
# Rails' escape hatch for dependencies static analysis can't see.
|
|
57
57
|
EXPLICIT_DEPENDENCY = /#\s*Template Dependency:\s*(\S+)/
|
|
58
58
|
class << self
|
|
59
|
-
# Virtual paths of components
|
|
60
|
-
# their class names.
|
|
59
|
+
# Virtual paths of components, mapped to their class names.
|
|
61
60
|
#
|
|
62
61
|
# Class *names* rather than class objects so the registry survives
|
|
63
62
|
# autoloader reloads without pinning stale constants in memory.
|
|
@@ -67,11 +66,6 @@ module ViewComponent
|
|
|
67
66
|
@registry ||= {}
|
|
68
67
|
end
|
|
69
68
|
|
|
70
|
-
# @return [Boolean] whether any component has opted in.
|
|
71
|
-
def enabled?
|
|
72
|
-
!registry.empty?
|
|
73
|
-
end
|
|
74
|
-
|
|
75
69
|
# @private
|
|
76
70
|
def register(component)
|
|
77
71
|
return unless component.virtual_path && component.name
|
|
@@ -108,28 +102,22 @@ module ViewComponent
|
|
|
108
102
|
constantize_component(name)
|
|
109
103
|
end
|
|
110
104
|
|
|
111
|
-
# Scan a template's source for renders of
|
|
112
|
-
#
|
|
113
|
-
# Called for every template Rails digests, so it exits early when the
|
|
114
|
-
# feature is unused.
|
|
105
|
+
# Scan a template's source for renders of components.
|
|
115
106
|
#
|
|
116
107
|
# @return [Array<String>] synthetic virtual paths
|
|
117
108
|
def dependencies_in(template)
|
|
118
|
-
return [] unless enabled?
|
|
119
|
-
|
|
120
109
|
component_paths_in(template.source)
|
|
121
110
|
end
|
|
122
111
|
|
|
123
112
|
# Scan arbitrary source (a template or a component's Ruby file) for
|
|
124
|
-
# renders of
|
|
113
|
+
# renders of components.
|
|
125
114
|
#
|
|
126
115
|
# @return [Array<String>] synthetic virtual paths
|
|
127
116
|
def component_paths_in(source)
|
|
128
117
|
return [] unless source.is_a?(String) && source.include?("render")
|
|
129
118
|
|
|
130
119
|
source.scan(RENDER_CALL).flatten.uniq.filter_map do |constant_name|
|
|
131
|
-
|
|
132
|
-
virtual_path_for(component) if component
|
|
120
|
+
registered_path_for(constantize_component(constant_name))
|
|
133
121
|
end
|
|
134
122
|
end
|
|
135
123
|
|
|
@@ -182,17 +170,19 @@ module ViewComponent
|
|
|
182
170
|
next unless /\A(?:::)?[A-Z]/.match?(declared)
|
|
183
171
|
|
|
184
172
|
component = constantize_component(declared)
|
|
185
|
-
|
|
173
|
+
virtual_path = registered_path_for(component)
|
|
174
|
+
[declared, virtual_path] if virtual_path
|
|
186
175
|
end
|
|
187
176
|
end
|
|
188
177
|
|
|
189
178
|
# Compute the digest of a component using Rails' digest tree.
|
|
190
179
|
#
|
|
191
|
-
# @param component [Class] a component
|
|
180
|
+
# @param component [Class] a component class
|
|
192
181
|
# @param finder [ActionView::LookupContext]
|
|
193
182
|
# @param format [Symbol]
|
|
194
183
|
# @return [String]
|
|
195
184
|
def digest(component, finder: default_finder, format: :html)
|
|
185
|
+
register(component)
|
|
196
186
|
virtual_path = virtual_path_for(component)
|
|
197
187
|
return "" unless virtual_path
|
|
198
188
|
|
|
@@ -210,24 +200,53 @@ module ViewComponent
|
|
|
210
200
|
|
|
211
201
|
# Wire the tracker and resolver into Action View.
|
|
212
202
|
#
|
|
213
|
-
#
|
|
214
|
-
# steps below are individually idempotent, so no "already installed" flag
|
|
215
|
-
# is kept. Both hooks short-circuit while the registry is empty, so
|
|
216
|
-
# applications that never opt in are unaffected.
|
|
203
|
+
# Installed during engine initialization. Both steps are idempotent.
|
|
217
204
|
#
|
|
218
205
|
# @private
|
|
219
206
|
def install!
|
|
220
207
|
DependencyTracking.install!
|
|
208
|
+
DigestorLookup.install!
|
|
209
|
+
end
|
|
221
210
|
|
|
222
|
-
|
|
223
|
-
|
|
211
|
+
# Compute component digests at boot, so requests read them from memory
|
|
212
|
+
# instead of reading and scanning source files.
|
|
213
|
+
#
|
|
214
|
+
# Only safe when templates are cached at boot: the precomputed sources are
|
|
215
|
+
# never refreshed.
|
|
216
|
+
#
|
|
217
|
+
# @param components [Array<Class>]
|
|
218
|
+
# @return [void]
|
|
219
|
+
# @private
|
|
220
|
+
def precompute!(components)
|
|
221
|
+
# Register everything first: a registration expires every memoized
|
|
222
|
+
# digest, so registering midway would discard digests already warmed.
|
|
223
|
+
components.each { |component| register(component) }
|
|
224
|
+
|
|
225
|
+
components.each do |component|
|
|
226
|
+
component.__vc_cache_digest_source = Resolver.instance.source_for(component).freeze
|
|
227
|
+
end
|
|
224
228
|
|
|
225
|
-
|
|
229
|
+
# Warms the same digest cache requests read, since a lookup context's
|
|
230
|
+
# cache is shared by every context with the same details.
|
|
231
|
+
components.each do |component|
|
|
232
|
+
component.__vc_template_formats.each do |format|
|
|
233
|
+
finder = ActionView::LookupContext.new(ActionController::Base.view_paths, formats: [format])
|
|
234
|
+
component.cache_digest(finder: finder, format: format)
|
|
235
|
+
end
|
|
226
236
|
end
|
|
227
237
|
end
|
|
228
238
|
|
|
229
239
|
private
|
|
230
240
|
|
|
241
|
+
# Record the actual class name rather than trying to reverse a virtual
|
|
242
|
+
# path, which may contain acronyms or be overridden by the component.
|
|
243
|
+
def registered_path_for(component)
|
|
244
|
+
return unless component
|
|
245
|
+
|
|
246
|
+
register(component)
|
|
247
|
+
virtual_path_for(component)
|
|
248
|
+
end
|
|
249
|
+
|
|
231
250
|
# Drop Action View's memoized template digests, leaving its resolver
|
|
232
251
|
# caches alone: no template changed, only the set of dependencies the
|
|
233
252
|
# Digestor can see.
|
|
@@ -235,15 +254,14 @@ module ViewComponent
|
|
|
235
254
|
ActionView::LookupContext::DetailsKey.digest_caches.each(&:clear)
|
|
236
255
|
end
|
|
237
256
|
|
|
238
|
-
# Resolve a constant name to
|
|
257
|
+
# Resolve a constant name to any ViewComponent.
|
|
239
258
|
#
|
|
240
259
|
# Returns nil for anything else, including constants that don't exist.
|
|
241
260
|
# Autoloading here is safe: the template is about to render this constant
|
|
242
261
|
# anyway.
|
|
243
262
|
def constantize_component(constant_name)
|
|
244
263
|
component = constant_name.safe_constantize
|
|
245
|
-
return unless component.is_a?(Class)
|
|
246
|
-
return unless component.respond_to?(:__vc_cacheable?) && component.__vc_cacheable?
|
|
264
|
+
return unless component.is_a?(Class) && component < ViewComponent::Base
|
|
247
265
|
|
|
248
266
|
component
|
|
249
267
|
end
|
|
@@ -3,26 +3,13 @@
|
|
|
3
3
|
require "view_component/cache_digest"
|
|
4
4
|
|
|
5
5
|
module ViewComponent
|
|
6
|
-
#
|
|
6
|
+
# Caching support for ViewComponents.
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# https://github.com/ViewComponent/view_component/issues/234.
|
|
11
|
-
#
|
|
12
|
-
# Including this module does two things:
|
|
13
|
-
#
|
|
14
|
-
# 1. Registers the component with Rails' template digest tree, so a
|
|
15
|
-
# `<% cache %>` block is invalidated when the component's template, Ruby
|
|
16
|
-
# class, sidecar files, or child components change. This covers blocks
|
|
17
|
-
# wrapping the component in a view and blocks inside the component's own
|
|
18
|
-
# template.
|
|
19
|
-
# 2. Enables the `cache_on` macro, which caches the component's own rendered
|
|
20
|
-
# output.
|
|
8
|
+
# Provides the `cache_on` macro, which caches the component's own rendered
|
|
9
|
+
# output. Components don't cache their output until they call `cache_on`.
|
|
21
10
|
#
|
|
22
11
|
# ```ruby
|
|
23
12
|
# class MessageComponent < ViewComponent::Base
|
|
24
|
-
# include ViewComponent::ExperimentallyCacheable
|
|
25
|
-
#
|
|
26
13
|
# cache_on :message
|
|
27
14
|
#
|
|
28
15
|
# def initialize(message:)
|
|
@@ -30,7 +17,7 @@ module ViewComponent
|
|
|
30
17
|
# end
|
|
31
18
|
# end
|
|
32
19
|
# ```
|
|
33
|
-
module
|
|
20
|
+
module Cacheable
|
|
34
21
|
extend ActiveSupport::Concern
|
|
35
22
|
|
|
36
23
|
# Stands in for `nil` in the cache key. Without it, `expand_cache_key`
|
|
@@ -38,11 +25,6 @@ module ViewComponent
|
|
|
38
25
|
# that respect would share an entry.
|
|
39
26
|
NIL_CACHE_VALUE = :__vc_nil
|
|
40
27
|
|
|
41
|
-
included do
|
|
42
|
-
ViewComponent::CacheDigest.install!
|
|
43
|
-
ViewComponent::CacheDigest.register(self)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
28
|
class_methods do
|
|
47
29
|
# Declare the values that identify a rendering of this component.
|
|
48
30
|
#
|
|
@@ -55,8 +37,7 @@ module ViewComponent
|
|
|
55
37
|
# ```
|
|
56
38
|
#
|
|
57
39
|
# Calling `cache_on` opts the component into caching its own output.
|
|
58
|
-
#
|
|
59
|
-
# Rails' digest tree.
|
|
40
|
+
# Components that don't call it aren't cached.
|
|
60
41
|
#
|
|
61
42
|
# Pass `if:` or `unless:` to cache only some renders. Both accept a method
|
|
62
43
|
# name or a proc evaluated on the component:
|
|
@@ -119,11 +100,6 @@ module ViewComponent
|
|
|
119
100
|
superclass.__vc_cache_unless if superclass.respond_to?(:__vc_cache_unless)
|
|
120
101
|
end
|
|
121
102
|
|
|
122
|
-
# @private
|
|
123
|
-
def __vc_cacheable?
|
|
124
|
-
true
|
|
125
|
-
end
|
|
126
|
-
|
|
127
103
|
# Whether this component caches its own rendered output.
|
|
128
104
|
#
|
|
129
105
|
# @return [Boolean]
|
|
@@ -131,67 +107,11 @@ module ViewComponent
|
|
|
131
107
|
__vc_cache_on.any?
|
|
132
108
|
end
|
|
133
109
|
|
|
134
|
-
#
|
|
135
|
-
#
|
|
136
|
-
# partials it renders.
|
|
137
|
-
#
|
|
138
|
-
# Computed with Rails' own `ActionView::Digestor`, so it's the same digest
|
|
139
|
-
# used to invalidate `<% cache %>` blocks.
|
|
110
|
+
# `__vc_caches_output?`, stored when the component compiles so rendering
|
|
111
|
+
# reads a flag instead of computing it.
|
|
140
112
|
#
|
|
141
|
-
# Usable outside a request, where no view context exists:
|
|
142
|
-
#
|
|
143
|
-
# ```ruby
|
|
144
|
-
# MessageComponent.cache_digest
|
|
145
|
-
# ```
|
|
146
|
-
#
|
|
147
|
-
# @param finder [ActionView::LookupContext] Defaults to a lookup context
|
|
148
|
-
# built from `ActionController::Base.view_paths`.
|
|
149
|
-
# @param format [Symbol]
|
|
150
|
-
# @return [String]
|
|
151
|
-
def cache_digest(finder: nil, format: :html)
|
|
152
|
-
ViewComponent::CacheDigest.digest(
|
|
153
|
-
self,
|
|
154
|
-
finder: finder || ViewComponent::CacheDigest.default_finder,
|
|
155
|
-
format: format
|
|
156
|
-
)
|
|
157
|
-
end
|
|
158
|
-
|
|
159
113
|
# @private
|
|
160
|
-
|
|
161
|
-
super
|
|
162
|
-
ViewComponent::CacheDigest.register(child)
|
|
163
|
-
end
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
# Renders the component, reading from and writing to the Rails cache when
|
|
167
|
-
# `cache_on` has been declared.
|
|
168
|
-
#
|
|
169
|
-
# @private
|
|
170
|
-
def render_in(view_context, **, &block)
|
|
171
|
-
return super unless self.class.__vc_caches_output?
|
|
172
|
-
|
|
173
|
-
# Content provided by the caller isn't part of the cache key, so caching
|
|
174
|
-
# it would serve one caller's content to another. Raised whether or not
|
|
175
|
-
# caching is currently enabled, so the conflict surfaces in development
|
|
176
|
-
# and test rather than only in production.
|
|
177
|
-
if block || __vc_content_set_by_with_content_defined? || __vc_slots_set_by_caller?
|
|
178
|
-
raise ContentPassedToCachedComponentError.new(self.class.name)
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
return super unless __vc_cache_enabled?(view_context)
|
|
182
|
-
|
|
183
|
-
store = Rails.cache
|
|
184
|
-
key = cache_key(view_context)
|
|
185
|
-
|
|
186
|
-
if (cached = store.read(key))
|
|
187
|
-
# Safe to mark as HTML-safe: the cached string was produced by this same
|
|
188
|
-
# rendering pipeline, which escapes output before it's written.
|
|
189
|
-
return cached.html_safe
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
super.tap do |output|
|
|
193
|
-
store.write(key, output.to_s)
|
|
194
|
-
end
|
|
114
|
+
attr_accessor :__vc_cache_output
|
|
195
115
|
end
|
|
196
116
|
|
|
197
117
|
# The cache key for this rendering of the component.
|
|
@@ -226,34 +146,39 @@ module ViewComponent
|
|
|
226
146
|
)
|
|
227
147
|
end
|
|
228
148
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
#
|
|
232
|
-
#
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
# Renders through the Rails cache. Called by `Base#render_in` for
|
|
152
|
+
# components that declare `cache_on`.
|
|
153
|
+
def __vc_render_cached(view_context, content_block)
|
|
154
|
+
# Content provided by the caller isn't part of the cache key, so caching
|
|
155
|
+
# it would serve one caller's content to another. Raised whether or not
|
|
156
|
+
# caching is currently enabled, so the conflict surfaces in development
|
|
157
|
+
# and test rather than only in production.
|
|
158
|
+
if content_block || __vc_content_set_by_with_content_defined? || __vc_slots_set_by_caller?
|
|
159
|
+
raise ContentPassedToCachedComponentError.new(self.class.name)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
return __vc_render_uncached(view_context) unless __vc_cache_enabled?(view_context)
|
|
163
|
+
|
|
164
|
+
store = Rails.cache
|
|
165
|
+
key = cache_key(view_context)
|
|
166
|
+
|
|
167
|
+
if (cached = store.read(key))
|
|
168
|
+
# Safe to mark as HTML-safe: the cached string was produced by this same
|
|
169
|
+
# rendering pipeline, which escapes output before it's written.
|
|
170
|
+
return cached.html_safe
|
|
171
|
+
end
|
|
250
172
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
173
|
+
__vc_render_uncached(view_context).tap do |output|
|
|
174
|
+
store.write(key, output.to_s)
|
|
175
|
+
end
|
|
254
176
|
end
|
|
255
177
|
|
|
256
|
-
|
|
178
|
+
def __vc_render_uncached(view_context)
|
|
179
|
+
@__vc_cache_bypassed = true
|
|
180
|
+
render_in(view_context)
|
|
181
|
+
end
|
|
257
182
|
|
|
258
183
|
# Slots set by the caller via `with_*`. Checked before rendering, so slots
|
|
259
184
|
# a component fills in for itself with a `default_*` method — which resolve
|
|
@@ -50,6 +50,8 @@ module ViewComponent
|
|
|
50
50
|
|
|
51
51
|
define_render_template_for
|
|
52
52
|
|
|
53
|
+
@component.__vc_cache_output = @component.__vc_caches_output?
|
|
54
|
+
|
|
53
55
|
@component.__vc_register_default_slots
|
|
54
56
|
@component.__vc_build_i18n_backend
|
|
55
57
|
|
|
@@ -76,6 +78,14 @@ module ViewComponent
|
|
|
76
78
|
filtered_templates
|
|
77
79
|
end
|
|
78
80
|
|
|
81
|
+
# Formats the component has templates for. Inline templates and `#call`
|
|
82
|
+
# methods have no format of their own and render as HTML.
|
|
83
|
+
#
|
|
84
|
+
# @return [Array<Symbol>]
|
|
85
|
+
def template_formats
|
|
86
|
+
gather_templates.map { |template| template.format || :html }.uniq
|
|
87
|
+
end
|
|
88
|
+
|
|
79
89
|
private
|
|
80
90
|
|
|
81
91
|
attr_reader :templates
|
|
@@ -3,11 +3,16 @@
|
|
|
3
3
|
require "rails"
|
|
4
4
|
require "view_component/config"
|
|
5
5
|
require "view_component/deprecation"
|
|
6
|
+
require "view_component/cache_digest"
|
|
6
7
|
|
|
7
8
|
module ViewComponent
|
|
8
9
|
class Engine < Rails::Engine # :nodoc:
|
|
9
10
|
config.view_component = ViewComponent::Config.current
|
|
10
11
|
|
|
12
|
+
initializer "view_component.cache_digest" do
|
|
13
|
+
ViewComponent::CacheDigest.install!
|
|
14
|
+
end
|
|
15
|
+
|
|
11
16
|
initializer "view_component.set_configs" do |app|
|
|
12
17
|
options = app.config.view_component
|
|
13
18
|
|
|
@@ -89,10 +94,11 @@ module ViewComponent
|
|
|
89
94
|
ActiveSupport.on_load(:after_initialize) do
|
|
90
95
|
if Rails.application.config.eager_load
|
|
91
96
|
ActiveSupport::Notifications.instrument("compile.view_component") do
|
|
92
|
-
ViewComponent::Base.descendants.
|
|
93
|
-
|
|
97
|
+
components = ViewComponent::Base.descendants.reject(&:anonymous?)
|
|
98
|
+
components.each(&:__vc_compile)
|
|
94
99
|
|
|
95
|
-
|
|
100
|
+
if ActionView::Base.cache_template_loading
|
|
101
|
+
ViewComponent::CacheDigest.precompute!(components.select(&:__vc_compiled?))
|
|
96
102
|
end
|
|
97
103
|
end
|
|
98
104
|
end
|
data/lib/view_component.rb
CHANGED
|
@@ -9,11 +9,11 @@ module ViewComponent
|
|
|
9
9
|
|
|
10
10
|
autoload :Base
|
|
11
11
|
autoload :CacheDigest
|
|
12
|
+
autoload :Cacheable
|
|
12
13
|
autoload :Compiler
|
|
13
14
|
autoload :CompileCache
|
|
14
15
|
autoload :Config
|
|
15
16
|
autoload :Deprecation
|
|
16
|
-
autoload :ExperimentallyCacheable
|
|
17
17
|
autoload :InlineTemplate
|
|
18
18
|
autoload :Instrumentation
|
|
19
19
|
autoload :Preview
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: view_component
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.
|
|
4
|
+
version: 4.18.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ViewComponent Team
|
|
@@ -93,7 +93,9 @@ files:
|
|
|
93
93
|
- lib/view_component/base.rb
|
|
94
94
|
- lib/view_component/cache_digest.rb
|
|
95
95
|
- lib/view_component/cache_digest/dependency_tracking.rb
|
|
96
|
+
- lib/view_component/cache_digest/digestor_lookup.rb
|
|
96
97
|
- lib/view_component/cache_digest/resolver.rb
|
|
98
|
+
- lib/view_component/cacheable.rb
|
|
97
99
|
- lib/view_component/collection.rb
|
|
98
100
|
- lib/view_component/compile_cache.rb
|
|
99
101
|
- lib/view_component/compiler.rb
|
|
@@ -102,7 +104,6 @@ files:
|
|
|
102
104
|
- lib/view_component/deprecation.rb
|
|
103
105
|
- lib/view_component/engine.rb
|
|
104
106
|
- lib/view_component/errors.rb
|
|
105
|
-
- lib/view_component/experimentally_cacheable.rb
|
|
106
107
|
- lib/view_component/inline_template.rb
|
|
107
108
|
- lib/view_component/instrumentation.rb
|
|
108
109
|
- lib/view_component/preview.rb
|