view_component 4.21.0 → 4.22.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 +26 -0
- data/lib/view_component/base.rb +4 -4
- data/lib/view_component/cache_digest/dependency_tracking.rb +25 -19
- data/lib/view_component/cache_digest/digestor_lookup.rb +47 -3
- data/lib/view_component/cache_digest/resolver.rb +14 -3
- data/lib/view_component/cache_digest.rb +74 -22
- data/lib/view_component/compile_cache.rb +17 -14
- data/lib/view_component/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 347185365d72f04ae47e10751bdaafc79ca1b92a5cc60659464cd91ea3fe5b1d
|
|
4
|
+
data.tar.gz: 8cc35ca70babcf15504c6cf3329357045b92cf5bce0b7059a376b465b9d82b3a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1f65ef1db7168f46ff4c0e41bcd7d329e30f5da9cfb05b06500c67686eaef0705b9b26433f644b39138c79ecf5d904176451c511f8dc48438cae9d9d6cbe81f8
|
|
7
|
+
data.tar.gz: 5b8df5a1f324a0fb481da6c822333e08e0bd424535310d851634da151702cb50a6a92d56be23f49f573c66aa5b9f9b8137a288969d85f4d5cec47ea59c114cca
|
data/docs/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,32 @@ nav_order: 6
|
|
|
10
10
|
|
|
11
11
|
## main
|
|
12
12
|
|
|
13
|
+
## 4.22.0
|
|
14
|
+
|
|
15
|
+
* Comment on release pull requests with benchmark results compared to the previous release.
|
|
16
|
+
|
|
17
|
+
*Joel Hawksley*
|
|
18
|
+
|
|
19
|
+
* Reduce source-file reads and dependency-scanning allocations during cold cache digests and boot-time precomputation.
|
|
20
|
+
|
|
21
|
+
*Joel Hawksley*
|
|
22
|
+
|
|
23
|
+
* Share sidecar directory listings and source reads during cold cache digests, and hash shared Ruby and sidecar files once per digest computation or boot-time precomputation.
|
|
24
|
+
|
|
25
|
+
*Joel Hawksley*
|
|
26
|
+
|
|
27
|
+
* Avoid missing-template digest errors for unresolvable interpolated render paths in components and included helpers.
|
|
28
|
+
|
|
29
|
+
*Joel Hawksley*
|
|
30
|
+
|
|
31
|
+
* Cover shared helper dependencies in the boot benchmark and add a cold cache digest benchmark to the CI performance check.
|
|
32
|
+
|
|
33
|
+
*Joel Hawksley*
|
|
34
|
+
|
|
35
|
+
* Speed up cache digests of components that render shared dependencies, such as a helper module included by most components. The Digestor looks a template up once per edge of the tree rather than once per component, so each synthetic template was rebuilt, and its sources re-parsed, for every place it was rendered from, and every component digested at boot rebuilt the tree of everything it renders. Templates, dependencies and parsed sources are now shared within one digest computation and across boot-time precomputation, and dependencies whose digest is already known are no longer walked again. On an app with 1,547 components, one cold digest drops from 70 s to 0.5 s and boot-time precomputation from 144 s to 13 s, with identical digests.
|
|
36
|
+
|
|
37
|
+
*Erik Axel Nielsen*
|
|
38
|
+
|
|
13
39
|
## 4.21.0
|
|
14
40
|
|
|
15
41
|
* Avoid `NameError` during eager loading when components include modules with temporary names.
|
data/lib/view_component/base.rb
CHANGED
|
@@ -667,10 +667,10 @@ module ViewComponent
|
|
|
667
667
|
(sidecar_files - [identifier] + sidecar_directory_files + nested_component_files).uniq
|
|
668
668
|
end
|
|
669
669
|
|
|
670
|
-
# `sidecar_files`, looked up once per component
|
|
671
|
-
# every component looks up its superclasses' templates
|
|
672
|
-
# too, when it compiles, loads its translations, and
|
|
673
|
-
# digest.
|
|
670
|
+
# `sidecar_files`, looked up once per component during eager loading or a
|
|
671
|
+
# digest computation: every component looks up its superclasses' templates
|
|
672
|
+
# and translations too, when it compiles, loads its translations, and
|
|
673
|
+
# computes its cache digest.
|
|
674
674
|
#
|
|
675
675
|
# @private
|
|
676
676
|
def __vc_sidecar_files(extensions)
|
|
@@ -14,27 +14,33 @@ module ViewComponent
|
|
|
14
14
|
#
|
|
15
15
|
# @private
|
|
16
16
|
module DependencyTracking
|
|
17
|
+
# Templates are memoized during a digest, so a template reached along
|
|
18
|
+
# many edges of the tree is scanned once.
|
|
17
19
|
def find_dependencies(name, template, view_paths = nil)
|
|
18
|
-
dependencies
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
CacheDigest.memoize([:dependencies, name, template, view_paths]) do
|
|
21
|
+
dependencies = super
|
|
22
|
+
source = template.source
|
|
23
|
+
|
|
24
|
+
# `# Template Dependency: SomeComponent` names a class, which Rails
|
|
25
|
+
# would resolve as a template path and never find. Swap it for the path
|
|
26
|
+
# the component is digested under, so the declaration resolves instead
|
|
27
|
+
# of becoming a missing node.
|
|
28
|
+
CacheDigest.explicit_component_dependencies(source).each do |declared, virtual_path|
|
|
29
|
+
dependencies = dependencies - [declared] + [virtual_path]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Synthetic templates already declare every dependency found in their sources.
|
|
33
|
+
next dependencies if name.start_with?("#{CacheDigest::VIRTUAL_PATH_PREFIX}/")
|
|
34
|
+
next dependencies unless CacheDigest.template_dependencies?(source, template.handler)
|
|
35
|
+
|
|
36
|
+
component_paths, inferred_partials =
|
|
37
|
+
CacheDigest.template_renders_in(CacheDigest.compiled_source(template), name, view_paths: view_paths)
|
|
38
|
+
# Older ERB trackers omit the underscore on unqualified partial paths.
|
|
39
|
+
inferred_partials |= inferred_partials.map { |path| path.delete_prefix("_") }
|
|
40
|
+
declared = source.scan(CacheDigest::EXPLICIT_DEPENDENCY).flatten
|
|
41
|
+
|
|
42
|
+
(dependencies - (inferred_partials - declared) + component_paths).uniq
|
|
27
43
|
end
|
|
28
|
-
|
|
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)
|
|
32
|
-
|
|
33
|
-
component_paths, inferred_partials =
|
|
34
|
-
CacheDigest.template_renders_in(CacheDigest.compiled_source(template), name, view_paths: view_paths)
|
|
35
|
-
declared = source.scan(CacheDigest::EXPLICIT_DEPENDENCY).flatten
|
|
36
|
-
|
|
37
|
-
(dependencies - (inferred_partials - declared) + component_paths).uniq
|
|
38
44
|
end
|
|
39
45
|
|
|
40
46
|
# @private
|
|
@@ -21,12 +21,56 @@ module ViewComponent
|
|
|
21
21
|
digestor.prepend(self)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
+
def digest(finder:, **)
|
|
25
|
+
CacheDigest.memoizing do
|
|
26
|
+
previous = CacheDigest.memo[:pruned]
|
|
27
|
+
loop do
|
|
28
|
+
pruned = CacheDigest.memo[:pruned] = {}
|
|
29
|
+
digest = super
|
|
30
|
+
|
|
31
|
+
# A component that registered while the tree was built expired the
|
|
32
|
+
# digests the pruned nodes stood in for, so this one, and the
|
|
33
|
+
# digests computed for them since, are missing their dependencies.
|
|
34
|
+
break digest if pruned.all? { |name, cached| finder.digest_cache[name].equal?(cached) }
|
|
35
|
+
|
|
36
|
+
finder.digest_cache.clear
|
|
37
|
+
end
|
|
38
|
+
ensure
|
|
39
|
+
CacheDigest.memo[:pruned] = previous
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# A dependency whose digest is cached already becomes a leaf:
|
|
44
|
+
# `Node#dependency_digest` reads the cache and never walks its children.
|
|
45
|
+
# Without this, every component digested at boot rebuilds the tree of
|
|
46
|
+
# everything it renders, which for components rendering a shared helper
|
|
47
|
+
# module is most of the application.
|
|
48
|
+
def tree(name, finder, partial = false, seen = {})
|
|
49
|
+
CompileCache.with_sidecar_files_cache do
|
|
50
|
+
pruned = CacheDigest.memo&.fetch(:pruned, nil)
|
|
51
|
+
cached = finder.digest_cache[name] if pruned && partial
|
|
52
|
+
next super unless cached
|
|
53
|
+
|
|
54
|
+
path = ActionView::TemplatePath.parse(name)
|
|
55
|
+
template = find_template(finder, path.name, [path.prefix], partial, [])
|
|
56
|
+
next super unless template
|
|
57
|
+
|
|
58
|
+
pruned[name] = cached
|
|
59
|
+
seen[template.identifier] ||= ActionView::Digestor::Node.create(name, name.gsub(%r{/_}, "/"), template, partial)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
24
63
|
private
|
|
25
64
|
|
|
26
65
|
def find_template(finder, name, prefixes, partial, keys)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
66
|
+
key = [:template, name, prefixes.first, partial, finder.formats, finder.view_paths]
|
|
67
|
+
template = CacheDigest.memoize(key) do
|
|
68
|
+
Resolver.instance.find_templates(
|
|
69
|
+
name, prefixes.first, partial, {formats: finder.formats}, view_paths: finder.view_paths
|
|
70
|
+
).first
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
template || super
|
|
30
74
|
end
|
|
31
75
|
end
|
|
32
76
|
end
|
|
@@ -160,8 +160,10 @@ module ViewComponent
|
|
|
160
160
|
end
|
|
161
161
|
|
|
162
162
|
def ruby_files(component)
|
|
163
|
-
|
|
164
|
-
.
|
|
163
|
+
CacheDigest.memoize([:ruby_files, component]) do
|
|
164
|
+
component_ancestors(component).map(&:identifier).concat(module_files(component))
|
|
165
|
+
.compact.uniq.select { |path| ::File.exist?(path) }
|
|
166
|
+
end
|
|
165
167
|
end
|
|
166
168
|
|
|
167
169
|
# Sidecar template files plus inline templates, which live in the Ruby
|
|
@@ -243,7 +245,16 @@ module ViewComponent
|
|
|
243
245
|
end
|
|
244
246
|
|
|
245
247
|
def file_digest(path)
|
|
246
|
-
|
|
248
|
+
CacheDigest.memoize([:file_digest, path]) do
|
|
249
|
+
# Reuse source reads unless text mode can transcode or translate newlines.
|
|
250
|
+
source = if ::File::ALT_SEPARATOR.nil? &&
|
|
251
|
+
(Encoding.default_internal.nil? || Encoding.default_internal == Encoding.default_external)
|
|
252
|
+
CompileCache.read_file(path)
|
|
253
|
+
else
|
|
254
|
+
::File.binread(path)
|
|
255
|
+
end
|
|
256
|
+
ActiveSupport::Digest.hexdigest(source)
|
|
257
|
+
end
|
|
247
258
|
end
|
|
248
259
|
|
|
249
260
|
# Built once at load time rather than memoized, so no class-level state
|
|
@@ -43,7 +43,8 @@ module ViewComponent
|
|
|
43
43
|
RENDER_METHODS = %i[render render_to_string].freeze
|
|
44
44
|
|
|
45
45
|
COMPILED_SOURCES = :view_component_cache_digest_compiled_sources
|
|
46
|
-
|
|
46
|
+
MEMO = :view_component_cache_digest_memo
|
|
47
|
+
private_constant :COMPILED_SOURCES, :MEMO
|
|
47
48
|
|
|
48
49
|
# Rails' escape hatch for dependencies static analysis can't see.
|
|
49
50
|
EXPLICIT_DEPENDENCY = /#\s*Template Dependency:\s*(\S+)/
|
|
@@ -157,9 +158,13 @@ module ViewComponent
|
|
|
157
158
|
# @return [Array(Array<String>, Array<String>)] synthetic virtual paths of
|
|
158
159
|
# components and partial virtual paths
|
|
159
160
|
def renders_in(source, name, view_paths: nil, nesting: [])
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
[
|
|
161
|
+
# Shared by every component that includes a module or inherits from
|
|
162
|
+
# the same superclass, so it's scanned once per digest computation.
|
|
163
|
+
memoize([:renders_in, source, name, view_paths, nesting]) do
|
|
164
|
+
components, _component_calls, other_calls = partition_render_calls(Prism.parse(source).value, nesting)
|
|
165
|
+
partials = partials_rendered_by(other_calls, name) - unresolved_partials_rendered_by(other_calls, name, view_paths)
|
|
166
|
+
[component_paths(components), partials]
|
|
167
|
+
end
|
|
163
168
|
end
|
|
164
169
|
|
|
165
170
|
# Find the components Ruby source renders, along with partial guesses
|
|
@@ -280,6 +285,40 @@ module ViewComponent
|
|
|
280
285
|
Thread.current[COMPILED_SOURCES] = nil
|
|
281
286
|
end
|
|
282
287
|
|
|
288
|
+
# Share synthetic templates between every lookup in the block.
|
|
289
|
+
#
|
|
290
|
+
# The Digestor looks a template up once per edge of the tree, before it
|
|
291
|
+
# checks whether it has seen the template already. Action View's own
|
|
292
|
+
# resolvers cache their templates, but building a synthetic one reads and
|
|
293
|
+
# parses the component's sources, so a component rendered from many
|
|
294
|
+
# places was rebuilt once per place. Kept for one digest computation
|
|
295
|
+
# only, so a source edited between requests is still seen.
|
|
296
|
+
#
|
|
297
|
+
# @private
|
|
298
|
+
def memoizing
|
|
299
|
+
return yield if Thread.current[MEMO]
|
|
300
|
+
|
|
301
|
+
begin
|
|
302
|
+
Thread.current[MEMO] = {}
|
|
303
|
+
yield
|
|
304
|
+
ensure
|
|
305
|
+
Thread.current[MEMO] = nil
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# @private
|
|
310
|
+
def memo
|
|
311
|
+
Thread.current[MEMO]
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# @private
|
|
315
|
+
def memoize(key)
|
|
316
|
+
memo = Thread.current[MEMO]
|
|
317
|
+
return yield unless memo
|
|
318
|
+
|
|
319
|
+
memo.fetch(key) { memo[key] = yield }
|
|
320
|
+
end
|
|
321
|
+
|
|
283
322
|
# @private
|
|
284
323
|
def compiled(handler, source, compiled_source)
|
|
285
324
|
sources = Thread.current[COMPILED_SOURCES]
|
|
@@ -300,16 +339,18 @@ module ViewComponent
|
|
|
300
339
|
# digest, so registering midway would discard digests already warmed.
|
|
301
340
|
components.each { |component| register(component) }
|
|
302
341
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
342
|
+
memoizing do
|
|
343
|
+
components.each do |component|
|
|
344
|
+
component.__vc_cache_digest_source = Resolver.instance.source_for(component).freeze
|
|
345
|
+
end
|
|
306
346
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
347
|
+
# Warms the same digest cache requests read, since a lookup context's
|
|
348
|
+
# cache is shared by every context with the same details.
|
|
349
|
+
components.each do |component|
|
|
350
|
+
component.__vc_template_formats.each do |format|
|
|
351
|
+
finder = ActionView::LookupContext.new(ActionController::Base.view_paths, formats: [format])
|
|
352
|
+
component.cache_digest(finder: finder, format: format)
|
|
353
|
+
end
|
|
313
354
|
end
|
|
314
355
|
end
|
|
315
356
|
end
|
|
@@ -327,7 +368,8 @@ module ViewComponent
|
|
|
327
368
|
|
|
328
369
|
each_node(program, nesting) do |node, scope|
|
|
329
370
|
if node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
|
|
330
|
-
|
|
371
|
+
name = node.slice
|
|
372
|
+
names << [name, scope] if CONSTANT_NAME.match?(name)
|
|
331
373
|
elsif render_call?(node)
|
|
332
374
|
if rendered_component(node, scope)
|
|
333
375
|
component_calls << node
|
|
@@ -348,9 +390,10 @@ module ViewComponent
|
|
|
348
390
|
# Class/module bodies add one lexical scope, even for qualified names:
|
|
349
391
|
# `class UI::Card` adds Card, not UI, to Module.nesting.
|
|
350
392
|
def each_node(program, nesting = [])
|
|
351
|
-
nodes
|
|
352
|
-
|
|
353
|
-
|
|
393
|
+
# Keep scopes and nodes in one stack rather than allocating a pair per node.
|
|
394
|
+
nodes = [nesting, program]
|
|
395
|
+
while (node = nodes.pop)
|
|
396
|
+
scope = nodes.pop
|
|
354
397
|
yield node, scope
|
|
355
398
|
|
|
356
399
|
body = nil
|
|
@@ -362,7 +405,7 @@ module ViewComponent
|
|
|
362
405
|
end
|
|
363
406
|
|
|
364
407
|
node.compact_child_nodes.reverse_each do |child|
|
|
365
|
-
nodes
|
|
408
|
+
nodes.push(child.equal?(body) ? body_scope : scope, child)
|
|
366
409
|
end
|
|
367
410
|
end
|
|
368
411
|
end
|
|
@@ -416,20 +459,29 @@ module ViewComponent
|
|
|
416
459
|
# Dynamic renders can return components without naming their classes,
|
|
417
460
|
# such as `render UI.button`. Keep Action View's guess only if it resolves,
|
|
418
461
|
# without executing application methods to discover what they return.
|
|
462
|
+
# Interpolated paths are discarded if Rails cannot expand their wildcards.
|
|
419
463
|
def unresolved_partials_rendered_by(calls, name, view_paths)
|
|
420
|
-
return [] unless view_paths
|
|
421
|
-
|
|
422
464
|
targets = calls.flat_map { |call| render_targets(call) }
|
|
423
465
|
literal, dynamic = targets.partition do |target|
|
|
424
466
|
target.is_a?(Prism::StringNode) || target.is_a?(Prism::InterpolatedStringNode)
|
|
425
467
|
end
|
|
426
|
-
|
|
468
|
+
interpolated = literal.grep(Prism::InterpolatedStringNode)
|
|
469
|
+
unresolved = []
|
|
470
|
+
if interpolated.any?
|
|
471
|
+
paths = partial_paths_in(interpolated.map { |target| "render(#{target.slice})" }.join("\n"), name)
|
|
472
|
+
# Rails expands only directory wildcards with a literal prefix.
|
|
473
|
+
# Older parsers can return truncated paths without any wildcard.
|
|
474
|
+
unresolved = paths.reject { |path| path.match?(%r{\A[^*]+/\*\z}) }
|
|
475
|
+
static = literal.grep(Prism::StringNode)
|
|
476
|
+
unresolved -= partial_paths_in(static.map { |target| "render(#{target.slice})" }.join("\n"), name)
|
|
477
|
+
end
|
|
478
|
+
return unresolved unless view_paths && dynamic.any?
|
|
427
479
|
|
|
428
480
|
inferred = partial_paths_in(dynamic.map { |target| "render(#{target.slice})" }.join("\n"), name)
|
|
429
481
|
declared = partial_paths_in(literal.map { |target| "render(#{target.slice})" }.join("\n"), name)
|
|
430
482
|
finder = ActionView::LookupContext.new(view_paths, variants: :any, locale: I18n.available_locales)
|
|
431
483
|
|
|
432
|
-
inferred.reject do |path|
|
|
484
|
+
unresolved | inferred.reject do |path|
|
|
433
485
|
declared.include?(path) || finder.exists?(path, [], true)
|
|
434
486
|
end
|
|
435
487
|
end
|
|
@@ -26,25 +26,28 @@ module ViewComponent
|
|
|
26
26
|
cache.clear
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
# Fiber storage keeps the caches from surviving boot.
|
|
29
|
+
# Share directory listings, sidecar lookups, and file contents within one
|
|
30
|
+
# eager load or digest computation, but never across reloads.
|
|
32
31
|
def with_sidecar_files_cache
|
|
33
|
-
Fiber[:__vc_sidecar_files]
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
32
|
+
return yield if Fiber[:__vc_sidecar_files]
|
|
33
|
+
|
|
34
|
+
begin
|
|
35
|
+
Fiber[:__vc_sidecar_files] = {}
|
|
36
|
+
Fiber[:__vc_sidecar_directories] = {}
|
|
37
|
+
Fiber[:__vc_source_files] = {}
|
|
38
|
+
Fiber[:__vc_path_parser] = ActionView::Resolver::PathParser.new
|
|
39
|
+
yield
|
|
40
|
+
ensure
|
|
41
|
+
Fiber[:__vc_sidecar_files] = nil
|
|
42
|
+
Fiber[:__vc_sidecar_directories] = nil
|
|
43
|
+
Fiber[:__vc_source_files] = nil
|
|
44
|
+
Fiber[:__vc_path_parser] = nil
|
|
45
|
+
end
|
|
43
46
|
end
|
|
44
47
|
|
|
45
48
|
def path_parser
|
|
46
49
|
# Its regular expression depends on registered handlers, formats and locales.
|
|
47
|
-
# Reuse it during eager loading, but not across reloads.
|
|
50
|
+
# Reuse it during eager loading or a digest computation, but not across reloads.
|
|
48
51
|
Fiber[:__vc_path_parser] || ActionView::Resolver::PathParser.new
|
|
49
52
|
end
|
|
50
53
|
|