Let's build our own code reloader for Rails, shall we?
Run this inside rails/railties
:
$ grep -rn "eager_load_paths" .
You should get the results from Rails::Engine::Configuration
and Rails::Engine
. As you know, each Rails application is actually a Rails::Engine
and Rails::Engine::Configuration
is that thing wrapped inside Rails.application.config
block.
This is the method responsible for eager loading in Rails::Engine
:
def eager_load!
config.eager_load_paths.each do |load_path|
matcher = /\A#{Regexp.escape(load_path)}\/(.*)\.rb\Z/
Dir.glob("#{load_path}/**/*.rb").sort.each do |file|
require_dependency file.sub(matcher, '\1')
end
end
end
As you can see, it uses require_dependency
and you can read more about its magic in this great answer. Long story short, Ruby instance doesn't allow you to "unrequire" a file, it does its requiring only one time. That's where require_dependency
comes in.
Try using it and it will still not work. That's because require_dependency
is depending on the context it's called from. Let me explain.
If you have a controller like this
require_dependency "monkey"
class MonkeyController < ApplicationController
def index
render :text => Monkey.new.greet
end
end
and the matching lib file looks like this
class Monkey
def greet
"ZOMG, I'm a monkey!"
end
end
then Monkey will be reloaded only when the controller is reloaded. Controller is reloaded only when you change it, so this is probably not what you want.
You want your monkey to be reloaded on each request and things get a little bit complicated because of that.
Let's see where this happens... Take a look at Rails::Application::Finisher
. This is the thing responsible for finishing the Rails initialization process.
# Set clearing dependencies after the finisher hook to ensure paths
# added in the hook are taken into account.
initializer :set_clear_dependencies_hook, group: :all do
callback = lambda do
ActiveSupport::DescendantsTracker.clear
ActiveSupport::Dependencies.clear
end
if config.reload_classes_only_on_change
reloader = config.file_watcher.new(*watchable_args, &callback)
self.reloaders << reloader
# Prepend this callback to have autoloaded constants cleared before
# any other possible reloading, in case they need to autoload fresh
# constants.
ActionDispatch::Reloader.to_prepare(prepend: true) do
# In addition to changes detected by the file watcher, if routes
# or i18n have been updated we also need to clear constants,
# that's why we run #execute rather than #execute_if_updated, this
# callback has to clear autoloaded constants after any update.
reloader.execute
end
else
ActionDispatch::Reloader.to_cleanup(&callback)
end
end
It looks like ActionDispatch::Reloader
and ActiveSupport::FileUpdateChecker
are doing all that's necessary for this. Reloader
is just a Rack middleware (hit rake middleware
to convince yourself) which calls FileUpdateChecker
on every request.
Now that we know all of this, we can build our own reloader that's thousand times simpler than this and you should probably burn it after:
class StupidReloader
def initialize(app)
@app = app
end
def call(env)
do_crazy_stuff!
@app.call env
end
private
def do_crazy_stuff!
ActiveSupport::DescendantsTracker.clear
ActiveSupport::Dependencies.clear
require_dependency "#{File.join(Rails.root, "lib", "monkey")}"
end
end
Replace the default Rails' reloader:
# somewhere inside config/environments/development.rb
config.middleware.swap ActionDispatch::Reloader,
StupidReloader # we like to live dangerously
Monkey should now get reloaded on every request.