Module: ViewComponent::CacheDigest
- Extended by:
- ActiveSupport::Autoload
- Defined in:
- lib/view_component/cache_digest.rb,
lib/view_component/cache_digest/resolver.rb,
lib/view_component/cache_digest/dependency_tracking.rb
Overview
Integrates ViewComponents into Rails' template digest tree.
Rails computes a digest for every template from its source and the templates
it renders. That digest is mixed into the key of every <% cache %> block in
the template, so editing a partial busts the caches of everything that
renders it.
Components are invisible to that mechanism for two reasons:
- Discovery —
ActionView::DependencyTrackerdoesn't recognizerender SomeComponent.new(...)as a dependency. - Resolution — component templates live outside the view paths, and a component's rendered output depends on its Ruby class and sidecar files, not just its template.
This module fixes both, reusing Rails' own ActionView::Digestor rather than
reimplementing static analysis. Components opt in individually by including
ViewComponent::ExperimentallyCacheable; until at least one component does,
every hook here short-circuits.
Defined Under Namespace
Modules: DependencyTracking Classes: Resolver
Constant Summary collapse
- VIRTUAL_PATH_PREFIX =
Prefix for the synthetic virtual paths components are digested under.
Namespaced under
view_component/so it can't collide with an application partial. "view_component/cache_digest"- RENDER_CALL =
Matches
render FooComponent,render(Foo::BarComponent.new(...)),render FooComponent.with_collection(...), etc.Deliberately a plain source scan rather than a tracker-specific hook: it behaves identically for the ERB tracker, the Prism-based Ruby tracker, and third-party Haml/Slim trackers.
/ \brender(?:_to_string)?\b # render or render_to_string \s*\(?\s* # optional opening paren (?<const> (?:::)?[A-Z]\w* # a constant (?:::[A-Z]\w*)* # optionally namespaced ) /x- EXPLICIT_DEPENDENCY =
Rails' escape hatch for dependencies static analysis can't see.
/#\s*Template Dependency:\s*(\S+)/- RENDER_PARSER =
Resolved once at load time rather than memoized, so no class-level state is written after boot.
resolve_render_parser(ActionView::RenderParser)
Class Method Summary collapse
-
.component_for(virtual_path) ⇒ Class?
Resolve a synthetic virtual path back to the component that owns it.
-
.component_paths_in(source) ⇒ Array<String>
Scan arbitrary source (a template or a component's Ruby file) for renders of cacheable components.
-
.default_finder ⇒ ActionView::LookupContext
A lookup context for digesting components outside a request, where no view context (and therefore no finder) exists.
-
.dependencies_in(template) ⇒ Array<String>
Scan a template's source for renders of cacheable components.
-
.digest(component, finder: default_finder, format: :html) ⇒ String
Compute the digest of a component using Rails' digest tree.
-
.enabled? ⇒ Boolean
Whether any component has opted in.
-
.explicit_component_dependencies(source) ⇒ Array<Array(String, String)>
Resolve
# Template Dependency: SomeComponentdeclarations. -
.install! ⇒ Object
Wire the tracker and resolver into Action View.
-
.partial_paths_in(source, name) ⇒ Array<String>
Scan a component's Ruby source for partials referenced by string path, such as
render "posts/byline"inside a#callmethod. - .register(component) ⇒ Object
-
.registry ⇒ Hash{String => String}
Virtual paths of components that have opted into caching, mapped to their class names.
-
.resolve_render_parser(parser) ⇒ Class
Action View has shipped its render parser as a class (Rails 7.1, and again on main) and as a module holding a
Defaultimplementation chosen from Prism or Ripper (Rails 7.2 through 8.1). -
.virtual_path_for(component) ⇒ String?
The synthetic virtual path a component is digested under.
Class Method Details
.component_for(virtual_path) ⇒ Class?
Resolve a synthetic virtual path back to the component that owns it.
94 95 96 97 98 99 100 101 |
# File 'lib/view_component/cache_digest.rb', line 94 def component_for(virtual_path) return unless virtual_path.start_with?("#{VIRTUAL_PATH_PREFIX}/") name = registry[virtual_path.delete_prefix("#{VIRTUAL_PATH_PREFIX}/")] return unless name constantize_component(name) end |
.component_paths_in(source) ⇒ Array<String>
Scan arbitrary source (a template or a component's Ruby file) for renders of cacheable components.
119 120 121 122 123 124 125 126 |
# File 'lib/view_component/cache_digest.rb', line 119 def component_paths_in(source) return [] unless source.is_a?(String) && source.include?("render") source.scan(RENDER_CALL).flatten.uniq.filter_map do |constant_name| component = constantize_component(constant_name) virtual_path_for(component) if component end end |
.default_finder ⇒ ActionView::LookupContext
A lookup context for digesting components outside a request, where no view context (and therefore no finder) exists.
201 202 203 204 |
# File 'lib/view_component/cache_digest.rb', line 201 def default_finder # Not memoized across reloads: view paths change when the app reloads. ActionView::LookupContext.new(ActionController::Base.view_paths) end |
.dependencies_in(template) ⇒ Array<String>
Scan a template's source for renders of cacheable components.
Called for every template Rails digests, so it exits early when the feature is unused.
109 110 111 112 113 |
# File 'lib/view_component/cache_digest.rb', line 109 def dependencies_in(template) return [] unless enabled? component_paths_in(template.source) end |
.digest(component, finder: default_finder, format: :html) ⇒ String
Compute the digest of a component using Rails' digest tree.
190 191 192 193 194 195 |
# File 'lib/view_component/cache_digest.rb', line 190 def digest(component, finder: default_finder, format: :html) virtual_path = virtual_path_for(component) return "" unless virtual_path ActionView::Digestor.digest(name: virtual_path, format: format, finder: finder) end |
.enabled? ⇒ Boolean
Returns whether any component has opted in.
71 72 73 |
# File 'lib/view_component/cache_digest.rb', line 71 def enabled? !registry.empty? end |
.explicit_component_dependencies(source) ⇒ Array<Array(String, String)>
Resolve # Template Dependency: SomeComponent declarations.
Rails' escape hatch takes a template path, but the path a component is
digested under is an internal detail. Naming the class instead keeps
that detail out of application code, so SomeComponent is translated
to the path the Digestor can resolve.
173 174 175 176 177 178 179 180 181 182 |
# File 'lib/view_component/cache_digest.rb', line 173 def explicit_component_dependencies(source) return [] unless source.is_a?(String) && source.include?("Template Dependency:") source.scan(EXPLICIT_DEPENDENCY).flatten.uniq.filter_map do |declared| next unless /\A(?:::)?[A-Z]/.match?(declared) component = constantize_component(declared) [declared, virtual_path_for(component)] if component end end |
.install! ⇒ Object
Wire the tracker and resolver into Action View.
Called each time a component includes ExperimentallyCacheable. Both
steps below are individually idempotent, so no "already installed" flag
is kept. Both hooks short-circuit while the registry is empty, so
applications that never opt in are unaffected.
214 215 216 217 218 219 220 221 222 |
# File 'lib/view_component/cache_digest.rb', line 214 def install! DependencyTracking.install! ActiveSupport.on_load(:action_controller_base) do resolver = ViewComponent::CacheDigest::Resolver.instance append_view_path(resolver) unless view_paths.include?(resolver) end end |
.partial_paths_in(source, name) ⇒ Array<String>
Scan a component's Ruby source for partials referenced by string path,
such as render "posts/byline" inside a #call method.
Uses Rails' own render parser — the same one RubyTracker runs over
compiled templates — rather than a second implementation of the same
analysis. Its results are then narrowed to paths that appear verbatim in
the source, which keeps string literals and discards the speculative
things/_thing entries the parser infers from dynamic renders like
render @thing or render FooComponent.new. Those would resolve to
nothing and only add log noise; components rendered from Ruby are
already found precisely by component_paths_in.
143 144 145 146 147 148 149 150 151 152 |
# File 'lib/view_component/cache_digest.rb', line 143 def partial_paths_in(source, name) return [] unless source.is_a?(String) && source.include?("render") RENDER_PARSER.new(name, source).render_calls.uniq.select do |path| source.include?(path) || source.include?(path.sub(%r{(\A|/)_}, '\1')) end rescue # Never let digest computation break rendering. [] end |
.register(component) ⇒ Object
76 77 78 79 80 |
# File 'lib/view_component/cache_digest.rb', line 76 def register(component) return unless component.virtual_path && component.name registry[component.virtual_path] = component.name end |
.registry ⇒ Hash{String => String}
Virtual paths of components that have opted into caching, mapped to their class names.
Class names rather than class objects so the registry survives autoloader reloads without pinning stale constants in memory.
66 67 68 |
# File 'lib/view_component/cache_digest.rb', line 66 def registry @registry ||= {} end |
.resolve_render_parser(parser) ⇒ Class
Action View has shipped its render parser as a class (Rails 7.1, and
again on main) and as a module holding a Default implementation
chosen from Prism or Ripper (Rails 7.2 through 8.1).
160 161 162 |
# File 'lib/view_component/cache_digest.rb', line 160 def resolve_render_parser(parser) parser.is_a?(Class) ? parser : parser::Default end |
.virtual_path_for(component) ⇒ String?
The synthetic virtual path a component is digested under.
85 86 87 88 89 |
# File 'lib/view_component/cache_digest.rb', line 85 def virtual_path_for(component) return unless component.respond_to?(:virtual_path) && component.virtual_path "#{VIRTUAL_PATH_PREFIX}/#{component.virtual_path}" end |