Skip to content

Commit a0eeb0f

Browse files
committed
Add ActiveSupport::Notifications instrumentation to compiler
Emit 'compile.view_component' notification when components are compiled, allowing gems to extend ViewComponent without monkey-patching. The notification payload includes: - component: The component class being compiled - compiler: The compiler instance
1 parent 6a33362 commit a0eeb0f

File tree

3 files changed

+75
-19
lines changed

3 files changed

+75
-19
lines changed

docs/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ nav_order: 6
1010

1111
## main
1212

13+
## 4.0.0.rc6
14+
15+
* Add `compile.view_component` ActiveSupport::Notifications instrumentation, enabling gems to safely extend the compiler without monkey-patching.
16+
17+
*Jose Solás*
18+
1319
* Setup Trusted Publishing to RubyGems to improve software supply chain safety.
1420

1521
*Hans Lemuet*

lib/view_component/compiler.rb

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,34 +24,40 @@ def compile(raise_errors: false, force: false)
2424
return if @component == ViewComponent::Base
2525

2626
@lock.synchronize do
27-
# this check is duplicated so that concurrent compile calls can still
28-
# early exit
27+
# This check is duplicated so that concurrent compile calls can still
28+
# early exit before we instrument.
2929
return if compiled? && !force
3030

31-
gather_templates
31+
ActiveSupport::Notifications.instrument(
32+
"compile.view_component",
33+
component: @component,
34+
compiler: self
35+
) do |payload|
36+
gather_templates
3237

33-
if self.class.__vc_development_mode && @templates.any?(&:requires_compiled_superclass?)
34-
@component.superclass.__vc_compile(raise_errors: raise_errors)
35-
end
38+
if self.class.__vc_development_mode && @templates.any?(&:requires_compiled_superclass?)
39+
@component.superclass.__vc_compile(raise_errors: raise_errors)
40+
end
3641

37-
if template_errors.present?
38-
raise TemplateError.new(template_errors) if raise_errors
42+
if template_errors.present?
43+
raise TemplateError.new(template_errors) if raise_errors
3944

40-
# this return is load bearing, and prevents the component from being considered "compiled?"
41-
return false
42-
end
45+
# this return is load bearing, and prevents the component from being considered "compiled?"
46+
return false
47+
end
4348

44-
if raise_errors
45-
@component.__vc_validate_initialization_parameters!
46-
@component.__vc_validate_collection_parameter!
47-
end
49+
if raise_errors
50+
@component.__vc_validate_initialization_parameters!
51+
@component.__vc_validate_collection_parameter!
52+
end
4853

49-
define_render_template_for
54+
define_render_template_for
5055

51-
@component.__vc_register_default_slots
52-
@component.__vc_build_i18n_backend
56+
@component.__vc_register_default_slots
57+
@component.__vc_build_i18n_backend
5358

54-
CompileCache.register(@component)
59+
CompileCache.register(@component)
60+
end
5561
end
5662
end
5763

test/sandbox/test/base_test.rb

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,4 +195,48 @@ def test_uses_module_configuration
195195
assert_equal false, TestAlreadyConfigurableModule::SomeComponent.instrumentation_enabled
196196
assert_equal false, TestAlreadyConfiguredModule::SomeComponent.instrumentation_enabled
197197
end
198+
199+
def test_compiler_emits_notification_on_compile
200+
notification_received = false
201+
component_received = nil
202+
compiler_received = nil
203+
204+
# Subscribe to the notification
205+
subscription = ActiveSupport::Notifications.subscribe("compile.view_component") do |name, start, finish, id, payload|
206+
notification_received = true
207+
component_received = payload[:component]
208+
compiler_received = payload[:compiler]
209+
end
210+
211+
begin
212+
# Compile a component to trigger the notification (force to ensure it runs)
213+
ViewComponent::Compiler.new(EmptyComponent).compile(force: true)
214+
215+
# Verify the notification was emitted
216+
assert notification_received, "Expected compile.view_component notification to be emitted"
217+
assert_equal EmptyComponent, component_received
218+
assert_instance_of ViewComponent::Compiler, compiler_received
219+
ensure
220+
# Clean up the subscription
221+
ActiveSupport::Notifications.unsubscribe(subscription)
222+
end
223+
end
224+
225+
def test_compiler_does_not_emit_notification_when_already_compiled
226+
# Ensure the component is compiled once
227+
ViewComponent::Compiler.new(EmptyComponent).compile
228+
229+
notification_count = 0
230+
subscription = ActiveSupport::Notifications.subscribe("compile.view_component") do
231+
notification_count += 1
232+
end
233+
234+
begin
235+
# This should not emit a notification as it's already compiled (no `force`)
236+
ViewComponent::Compiler.new(EmptyComponent).compile
237+
assert_equal 0, notification_count, "Expected no notification for an already compiled component"
238+
ensure
239+
ActiveSupport::Notifications.unsubscribe(subscription)
240+
end
241+
end
198242
end

0 commit comments

Comments
 (0)