Christopher Oezbek
Christopher Oezbek

Reputation: 26363

Rails 6: Automatically reload local gem on change

I am developing a Rails 6 app and a Gem in parallel.

In the past, I used the require_reloader Gem so that Rails would reload the Gem when any files changed in the Gem's local directory.

With Zeitwerk becoming the new loader in Rails 6, this Gem doesn't seem to work anymore.

So my question: What is the canonical way to develop a Gem and a Rails 6 app in parallel so that changes made to Gem files are automatically visible in Rails?

Upvotes: 5

Views: 1160

Answers (1)

fiedl
fiedl

Reputation: 6137

I also have not found the canonical solution to this problem, but, in the context of Rails: Auto-reload gem files used in dummy app, have found a workaround:

Suppose, the gem folder is

~/rails/foo_gem

and the rails-6-app folder is:

~/rails/bar_app

To reload the gem code in the app on file-system changes, I needed to do three steps:

  1. Unregister the zeitwerk loader defined in foo_gem.rb that handles loading the different gem files.
  2. Define a new zeitwerk loader in the development.rb of the app that is configured with enable_reloading.
  3. Setup a file-system watcher and trigger a reload when a gem file changes.

Zeitwerk::Loader in foo_gem.rb

# ~/rails/foo_gem/lib/foo_gem.rb

# require 'foo_gem/bar`  # Did not work. Instead:

# (a) use zeitwerk:
require "zeitwerk"
loader = Zeitwerk::Loader.new
loader.push_dir File.join(__dir__)
loader.tag = "foo_gem"
loader.setup

# or (b) use autoload:
module FooGem
  autoload :Bar, "foo_gem/bar"
end

Note:

  • In the past, I've just loaded all ruby files of the gem with require from a kind of index file called just like the gem, here: foo_gem.rb. This does not work here, because zeitwerk appears to ignore files that have previously been loaded with require. Instead I needed to create a separate zeitwerk loader for the gem.
  • This loader has no enable_reloading because otherwise, reloading would be enabled for this gem whenever using the gem, not just while developing the gem.
  • I have given the loader a tag, which allows to find this loader later in the Zeitwerk::Registry in order to un-register it.
  • Instead of using zeitwerk in foo_gem.rb, one could also use autoload there like the devise gem does. This is the best way if you want to support rails versions earlier than 6 because zeitwerk requires rails 6+. Using autoload here also makes step 1 in the next section unnecessary.

Zeitwerk::Loader in development.rb of the app

# ~/rails/bar_app/config/environments/development.rb

# 1. Unregister the zeitwerk loader defined in foo_gem.rb that handles loading
#    the different gem files.
#
Zeitwerk::Registry.loaders.detect { |l| l.tag == "foo_gem" }.unregister

# 2. Define a new zeitwerk loader in the development.rb of the app
#    that is configured with enable_reloading.
#
gem_root_path = Pathname.new(Gem.loaded_specs["foo_gem"].full_gem_path)
gem_loader = Zeitwerk::Loader.new
gem_loader.push_dir gem_root_path.join("lib")
gem_loader.enable_reloading
gem_loader.setup

# 3. Setup a file-system watcher and trigger a reload when a gem file changes.
#
Listen.to gem_root_path.join("lib"), only: /\.rb$/ do
  gem_loader.reload
end.start

Note:

  • Zeitwerk does not allow two loaders managing the same files. Therefore, I need to unregister the previously defined loader tagged "foo_gem".
  • The new loader used in the app has enable_reloading. Therefore, when using the app with rails server, rails console, or when running the specs, the gem files can be reloaded.
  • The gem files are not automatically reloaded by zeitwerk. One needs a file-system watcher to trigger the reload on file-system changes. I did not manage to get the ActiveSupport::FileUpdateChecker working. Instead, I've used the listen gem as file-system watcher.

With this setup, when using the rails server, the rails console, or the specs of the bar_app, gem files of the foo_gem are reloaded after being edited, which means that one does no longer need to restart the rails server to pick up the changes.

However, I do not find this workaround very convenient.

Upvotes: 2

Related Questions