From 8ed08c8c94339afa5b8bec8b4010dfb56efa7c98 Mon Sep 17 00:00:00 2001 From: Jose Solas Moreno Date: Fri, 18 Jul 2025 19:04:59 -0400 Subject: [PATCH] Add after_compile hook --- docs/CHANGELOG.md | 6 ++++ lib/view_component/base.rb | 12 ++++++++ lib/view_component/compiler.rb | 7 +++++ test/sandbox/test/base_test.rb | 55 ++++++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index e6bb654e2..e55dc6d6b 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -10,6 +10,12 @@ nav_order: 6 ## main +## 4.0.3 + +* Add `after_compile` hook to enable extensions to run logic after component compilation. Extensions can override this instance method to add custom post-compilation behavior. + + *Jose Solás* + ## 4.0.2 * Share the view context in tests to prevent out-of-order rendering issues for certain advanced use-cases, eg. testing instances of Rails' `FormBuilder`. diff --git a/lib/view_component/base.rb b/lib/view_component/base.rb index 97ba41815..28668fcd2 100644 --- a/lib/view_component/base.rb +++ b/lib/view_component/base.rb @@ -611,6 +611,18 @@ def __vc_compiled? __vc_compiler.compiled? end + # Called after a component class has been compiled. + # + # Extensions can override this instance method to run logic after + # compilation (e.g., generate helpers, register metadata, etc.). + # + # By default, this is a no-op. The compiler will invoke this method on an + # uninitialized instance using `allocate` to avoid requiring initializer + # arguments. + def after_compile + # no-op by default + end + # @private def __vc_ensure_compiled __vc_compile unless __vc_compiled? diff --git a/lib/view_component/compiler.rb b/lib/view_component/compiler.rb index 130ecbde3..613290c66 100644 --- a/lib/view_component/compiler.rb +++ b/lib/view_component/compiler.rb @@ -52,6 +52,13 @@ def compile(raise_errors: false, force: false) @component.__vc_build_i18n_backend CompileCache.register(@component) + + # Invoke instance-level after_compile hook without calling initialize. + begin + @component.allocate.after_compile + rescue NoMethodError + # no-op + end end end diff --git a/test/sandbox/test/base_test.rb b/test/sandbox/test/base_test.rb index a9c0e2552..2d5aad28f 100644 --- a/test/sandbox/test/base_test.rb +++ b/test/sandbox/test/base_test.rb @@ -195,4 +195,59 @@ def test_uses_module_configuration assert_equal false, TestAlreadyConfigurableModule::SomeComponent.instrumentation_enabled assert_equal false, TestAlreadyConfiguredModule::SomeComponent.instrumentation_enabled end + + def test_after_compile_hook_called_on_compile + klass = Class.new(ViewComponent::Base) do + @@calls = 0 + + def self.calls + @@calls + end + + def after_compile + @@calls += 1 + end + + erb_template "" + end + + self.class.const_set(:TempHookComponent, klass) + ViewComponent::CompileCache.invalidate_class!(klass) + + ViewComponent::Compiler.new(klass).compile(force: true) + assert_equal 1, klass.calls + ensure + self.class.send(:remove_const, :TempHookComponent) if self.class.const_defined?(:TempHookComponent) + ViewComponent::CompileCache.invalidate_class!(klass) if defined?(klass) + end + + def test_after_compile_not_called_on_cached_compile + klass = Class.new(ViewComponent::Base) do + @@calls = 0 + + def self.calls + @@calls + end + + def after_compile + @@calls += 1 + end + + erb_template "" + end + + self.class.const_set(:TempHookCachedComponent, klass) + ViewComponent::CompileCache.invalidate_class!(klass) + + compiler = ViewComponent::Compiler.new(klass) + compiler.compile(force: true) + assert_equal 1, klass.calls + + # compile again without force -> should not call hook again + compiler.compile + assert_equal 1, klass.calls + ensure + self.class.send(:remove_const, :TempHookCachedComponent) if self.class.const_defined?(:TempHookCachedComponent) + ViewComponent::CompileCache.invalidate_class!(klass) if defined?(klass) + end end