6

While trying to build a Ruby gem (using Bundler), I tend to test the code using the REPL provided by Bundler - accessible via bundle console.

Is there any way to reload the entire project in it? I end up loading individual (changed) files again to test the new change.

Jikku Jose
  • 18,306
  • 11
  • 41
  • 61

1 Answers1

1

The following hack works for a relatively simple gem of mine and Ruby 2.2.2. I'll be curious to see if it works for you. It makes the following assumptions:

  1. You have the conventional folder structure: a file called lib/my_gem_name.rb and a folder lib/my_gem_name/ with any files / folder structure underneath.
  2. All the classes you want to reload are nested within your top module MyGemName.

It will probably not work if in one of the files under lib/my_gem_name/ you monkey-patch classes outside of your MyGemName namespace.

If you're good with the assumptions above, put the following code inside the module definition in lib/my_gem_name.rb and give it a try:

module MyGemName

  def self.reload!
    Reloader.new(self).reload
  end

  class Reloader
    def initialize(top)
      @top = top
    end

    def reload
      cleanup
      load_all
    end

    private

    # @return [Array<String>] array of all files that were loaded to memory
    # under the lib/my_gem_name folder. 
    # This code makes assumption #1 above.  
    def loaded_files
      $LOADED_FEATURES.select{|x| x.starts_with?(__FILE__.chomp('.rb'))}
    end

    # @return [Array<Module>] Recursively find all modules and classes 
    # under the MyGemName namespace.
    # This code makes assumption number #2 above.
    def all_project_objects(current = @top)
      return [] unless current.is_a?(Module) and current.to_s.split('::').first == @top.to_s
      [current] + current.constants.flat_map{|x| all_project_objects(current.const_get(x))}
    end

    # @return [Hash] of the format {Module => true} containing all modules 
    #   and classes under the MyGemName namespace
    def all_project_objects_lookup
      @_all_project_objects_lookup ||= Hash[all_project_objects.map{|x| [x, true]}]
    end

    # Recursively removes all constant entries of modules and classes under
    # the MyGemName namespace
    def cleanup(parent = Object, current = @top)
      return unless all_project_objects_lookup[current]
      current.constants.each {|const| cleanup current, current.const_get(const)}
      parent.send(:remove_const, current.to_s.split('::').last.to_sym)
    end

    # Re-load all files that were previously reloaded
    def load_all
      loaded_files.each{|x| load x}
      true
    end
  end
end

If you don't want this functionality to be available in production, consider monkey-patching this in the bin/console script, but make sure to change the line $LOADED_FEATURES.select{|x| x.starts_with?(__FILE__.chomp('.rb'))} to something that will return a list of relevant loaded files given the new location of the code.

If you have a standard gem structure, this should work: $LOADED_FEATURES.select{|x| x.starts_with?(File.expand_path('../../lib/my_gem_name'))} (make sure to put your monkey patching code before the IRB.start or Pry.start)

AmitA
  • 3,239
  • 1
  • 22
  • 31