Rails 6: Automatically reload local gem on change - ruby-on-rails

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?

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:
Unregister the zeitwerk loader defined in foo_gem.rb that handles loading the different gem files.
Define a new zeitwerk loader in the development.rb of the app that is configured with enable_reloading.
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.

Related

Why do I need to explicitly include a gem when it's already a dependency of another gem I'm using?

I have created a gem/Rails engine (let's call it my_rails_gem) that depends on another gem; specifically, the composite_primary_keys gem (some of the models need it). So in the .gemspec file, I have
Gem::Specification.new do |s|
#...
s.add_dependency 'composite_primary_keys'
The problem is that when I include the my_rails_gem in another Rails project's Gemfile and try to use the models, I get errors about composite_primary_keys' functionality. I must also explicitly add gem 'composite_primary_keys' to the app's Gemfile for it to function correctly.
Why is this? Isn't this the whole point of Bundle and gem dependencies? I want to take the burden off the developer using my_rails_gem to remember to have to include composite_primary_keys, but this is specifically preventing that. Am I doing something wrong, or are my expectations/understanding wrong?
I believe the reason that the dependent gem is not automatically required is so that the user of your gem (you) have the option of requiring the dependent gem. There are reasons for that, but it's a bit of a longer conversation.
If you want the dependent gem to be required automatically when you include your gem, then in your my_rails_gem.rb file (in your lib directory) you can do:
require 'composite_primary_keys'
I believe that ought to do the trick for you. At least, that's how I do it.
Also, if the dependent gem has stylesheet and javascript assets that you want to include (I suspect composite_primary_keys does not), you'll need to add the appropriate directives to your my_rails_gem.js and my_rails_gem.sass (or whatever templating engine you use) files.

When to load Gem code when depending on rails

I'm developing a Gem that is to be used exclusively in Rails projects. It has been developed inside of a Rails application's lib directory and is now to be extracted in a separate Gem.
Some of the classes depend on the Rails framework to be loaded though. Two examples:
class OurGem::Hookup
CONFIG_PATH = 'config/hookup.rb'.freeze
[...]
end
class OurGem::RoutingContainer
include Rails.application.routes.url_helpers
[...]
end
Normally, I load the Gem code in the Gem's main module file using require. But as this is loaded by bundler, Rails is not ready and things like Rails.application and Rails.root cannot be used. The first example could be worked around by not evaluating the path at load time, but the second one seems kind of tricky to me.
What is the proper way of doing this? Register an initializer using a railtie and require the "delicate" files there?
This is a strange setup, because your gem depends on your rails app and your rails app depends on your gem.
This looks far too coupled to me. Have you considered creating a self-contained rails engine that your main app mounts, instead?
You might be able to get away with doing this, though:
# Gemfile
gem 'our_gem', require: false
# config/initializers/our_gem.rb
require 'our_gem'
OurGem::Hookup.config_path = '...'
This ensures that your gem is only being loaded after the rails application initialises - so things like Rails.application.routes.url_helpers will be defined.

Best practices when including Rails models in another application

I'm developing a ruby application that uses the models and data from another Ruby on Rails web application as its main data source.
The Rails models were included in this application by including the environment.rb file in the main file like this:
# Require Rails
require_relative "../../RailsApp/config/environment.rb"
This works but there are uninitialized dependencies when loading models that use gems that are defined in the Rails Gemfile. (For example, acts_as_taggable_on, rack-pjax, devise, etc)
This ruby application dependencies are also managed through Bundler, so at the moment the only way to get the application working is to copy and paste the contents from the Rails' Gemfile into the ruby app's Gemfile.
Obviously this approach is not optimal as the gem requirements are duplicated.
Is there a better way to include Rails and the dependencies that its models require in another application? Is there a way to include a Gemfile into another?
Here are some options, in order of simplicity
Just keep everything in one app, a lot of stuff is easier this way
Use plugins to share common code
Use web services to share data
You could extract the models and code out from RailsAppA into a Gem. RailsAppA then includes that Gem and uses it.
The gem can remain in a private repository and does not need published.
Both apps would then do something like:
gem "yourapp-modelage", git: "http://github.com/you/yourapp-modelage.git"
Then, App2 would also use that Gem... How much goes into the Gem will depends on how much you need to re-use.

Undefined method 'devise' when including User model outside Rails

I am developing an application that consists in two parts: A ruby command line application and a Rails application for the front-end.
I've been using ActiveRecord and the Rails models in the Ruby application by including them individually the following way:
Dir[File.dirname(__FILE__) + '/../../RailsApp/app/models/*.rb'].each do |file|
filename = File.basename(file, File.extname(file))
require_relative "../../RailsApp/app/models/" + filename
end
And by manually mantaining two nearly identical Gemfile files for dependencies (an approach that I think is not the best)
Yesterday I added Devise to the Rails application, and now when the ruby app tries to include the user model, the message undefined method devise for class <xxxxx> appears.
I added the devise gem in my ruby app's Gemfile, but the error continues. If I understand correctly, Devise is a Rails Engine, which is why it's not being loaded just by requiring the gem.
I read that a better approach to include Rails models inside another application is by requiring the environment.rb file, but when I tried that, I got an error with ActionMailer:
undefined method `action_mailer' for #<Rails::Application::Configuration:0xbbb54bc>
How can I solve the issue with devise? Is there a better way to include Rails models inside another application than the one I'm currently using to avoid mantaining two gemfiles ? Thanks
Even though this architecture must change in the near future, we managed to work around this error by including devise and its initializer in the ruby application.
# FIXME: Dependency needed in Rails but not in the engine.
require 'devise'
require_relative "../../RailsApp/config/initializers/devise.rb"
#Load all the models
You could try putting require 'devise' before you call the models, but I'm not sure if that would work?
I agree that this general architecture feels wrong.
I would extract the code that is common to both applications to a separate library and then include that library in both applications. Your easiest bet to do this is to make your own library into a Gem and then include it both Gemfiles. The common gems will be specified in the shared library. Making Gems is really easy. Good instructions can be found at http://asciicasts.com/episodes/245-new-gem-with-bundler (or the associated Railscast).
Your command-line app probably shouldn't need to care about Devise. If you need a User model in both, you can just include it in the shared library and then add any extra functionality (such as Devise) in the Rails app.
Does that help?
You could move the time of loading your models into the parent app initialization step, rather than at the time your gem is loading itself.
Build a shared_code gem, as your other answer suggests.
Move your shared_code loading loop into an initializer file (such as require 'user')
List the gem dependencies (like devise) of your models in the shared_code gemspec.
In your shared_code gem, include a generator to install the initializer file into apps that use the shared_code gem.
Then to use your shared code in the parent app,
Gemfile:
gem 'shared_code'
console:
rails generate shared_code:install
This is the same pattern devise and other gems use to configure themselves at the correct time in the initialization sequence of the parent application.
If you use require_relative on the devise initializer in the user.rb file itself, within the shared_code gem, you are removing the possibility that two different parent applications that are using your shared_code might need to initialize devise (or another gem that both the app and your gem depend on) differently.
Details on the generator:
You can create the shared_code:install command by creating the generator like this:
lib/generators/shared_code/install_generator.rb
module SharedCode
class InstallGenerator < Rails::Generators::Base
source_root File.expand_path('../templates', __FILE__)
def copy_initializer_file
copy_file "shared_code.rb", "config/initializers/shared_code.rb"
end
end
end
Put your model load code into lib/generators/shared_code/templates/shared_code.rb

Updating to Rails 3.2.2: How to properly move my plugin from the '/vendor' to '/lib' directory?

I am upgrading Ruby on Rails from 3.1 to 3.2.2 and I would like to know what I should make and at what I should be care in order to properly move my vendor plugin (note: it is not a gem and at this time I am not planning to make that a gem) from the directory /vendor to /lib as well as wrote in the official documentation:
Rails 3.2 deprecates vendor/plugins and Rails 4.0 will remove them completely. You can start replacing these plugins by extracting them as gems and adding them in your Gemfile. If you choose not to make them gems, you can move them into, say, lib/my_plugin/* and add an appropriate initializer in config/initializers/my_plugin.rb.
I refer mostly to the "an appropriate initializer in config/initializers/my_plugin.rb": What code should I put in that file?
More: Do you have some advice or alert on making the above process?
The initializer should contain the appropriate requires and other startup related tasks that are necessary for your plugin to work correctly. It's difficult to help you without real code examples from your app but this link should help you get started.
http://code.coneybeare.net/how-to-convert-simple-rails-23-style-plugins
The example in the link requires the plugin (now in the lib directory) and adds a module to ActiveRecord::Base.

Resources