Question

I am developing a Rails app where most of the code not specific to the app has been written inside of various gems, including some Rails engines and some 3rd party gems for which I am enhancing or fixing bugs.

gem 'mygem', path: File.expath_path('../../mygem', __FILE__)

Since a lot of the code in these gems is really part of the app, it's still changing frequently. I'd like to be able to utilize the Rails feature where code is reloaded on each request when in development (i.e. when config.cache_classes is false), but this is only done within the normal application structure by default.

How can I configure Rails to reload gem code on each request, just like with the app code?

Was it helpful?

Solution

I have found through trial and error that several steps are required, with the help of ActiveSupport.

  • Add activesupport as a dependency in the .gemspec files

    spec.add_dependency 'activesupport'
    
  • Include ActiveSupport::Dependencies in the top-level module of your gem (this was the most elusive requirement)

    require 'bundler'; Bundler.setup
    require 'active_support/dependencies'
    
    module MyGem
      unloadable
      include ActiveSupport::Dependencies
    end
    
    require 'my_gem/version.rb'
    # etc...
    
  • Set up your gem to use autoloading. You an either manually use ruby autoload declarations to map symbols into filenames, or use the Rails-style folder-structure-to-module-hierarchy rules (see ActiveSupport #constantize)

  • In each module and class in your gem, add unloadable.

    module MyModule
      unloadable
    end
    
  • In each file that depends on a module or class from the gem, including in the gem itself, declare them at the top of each file using require_dependency. Look up the path of the gem as necessary to properly resolve the paths.

    require_dependency "#{Gem.loaded_specs['my_gem'].full_gem_path}/lib/my_gem/myclass"
    

If you get exceptions after modifying a file and making a request, check that you haven't missed a dependency.

For some interesting details see this comprehensive post on Rails (and ruby) autoloading.

OTHER TIPS

The solution that I used for Rails 6, with a dedicated Zeitwerk class loader and file checker :

  • Add the gem to the Rails project using the path: option in Gemfile

    gem 'mygem', path: 'TODO'  # The root directory of the local gem
    
  • In the development.rb, setup the classloader and the file watcher

    gem_path = 'TODO'  # The root directory of the local gem, the same used in Gemfile
    
    # Create a Zeitwerk class loader for each gem
    gem_lib_path = gem_path.join('lib').join(gem_path.basename)
    gem_loader = Zeitwerk::Registry.loader_for_gem(gem_lib_path)
    gem_loader.enable_reloading
    gem_loader.setup
    
    # Create a file watcher that will reload the gem classes when a file changes
    file_watcher = ActiveSupport::FileUpdateChecker.new(gem_path.glob('**/*')) do
      gem_loader.reload
    end
    
    # Plug it to Rails to be executed on each request
    Rails.application.reloaders << Class.new do 
      def initialize(file_watcher)
        @file_watcher = file_watcher
      end
    
      def updated?
        @file_watcher.execute_if_updated
      end
    end.new(file_watcher)
    

With this, on each request, the class loader will reload the gem classes if one of them has been modified.

For a detailed walkthrough, see my article Embed a gem in a Rails project and enable autoreload.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top