Skip to content

Commit

Permalink
Lazily boot the rails app during tapioca gem
Browse files Browse the repository at this point in the history
  • Loading branch information
KaanOzkan committed Dec 9, 2024
1 parent cc54939 commit 2b58eae
Show file tree
Hide file tree
Showing 2 changed files with 25 additions and 114 deletions.
116 changes: 2 additions & 114 deletions lib/tapioca/loaders/loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,11 @@ def load; end
def load_bundle(gemfile, initialize_file, require_file, halt_upon_load_error)
require_helper(initialize_file)

load_rails_application(halt_upon_load_error: halt_upon_load_error)

gemfile.require_bundle

require_helper(require_file)
load_rails_application(environment_load: true, halt_upon_load_error: halt_upon_load_error)

load_rails_engines
require_helper(require_file)
end

sig do
Expand Down Expand Up @@ -85,116 +83,6 @@ def load_rails_application(environment_load: false, eager_load: false, app_root:
say("Continuing RBI generation without loading the Rails application.")
end

sig { void }
def load_rails_engines
return if engines.empty?

with_rails_application do
run_initializers

if zeitwerk_mode?
load_engines_in_zeitwerk_mode
else
load_engines_in_classic_mode
end
end
end

def run_initializers
engines.each do |engine|
engine.instance.initializers.tsort_each do |initializer|
initializer.run(Rails.application)
rescue ScriptError, StandardError
nil
end
end
end

sig { void }
def load_engines_in_zeitwerk_mode
# Collect all the directories that are already managed by all existing Zeitwerk loaders.
managed_dirs = Zeitwerk::Registry.loaders.flat_map(&:dirs).to_set
# We use a fresh loader to load the engine directories, so that we don't interfere with
# any of the existing loaders.
autoloader = Zeitwerk::Loader.new

engines.each do |engine|
eager_load_paths(engine).each do |path|
# Zeitwerk only accepts existing directories in `push_dir`.
next unless File.directory?(path)
# We should not add directories that are already managed by a Zeitwerk loader.
next if managed_dirs.member?(path)

autoloader.push_dir(path)
end
end

autoloader.setup
end

sig { void }
def load_engines_in_classic_mode
# This is code adapted from `Rails::Engine#eager_load!` in
# https://github.com/rails/rails/blob/d9e188dbab81b412f73dfb7763318d52f360af49/railties/lib/rails/engine.rb#L489-L495
#
# We can't use `Rails::Engine#eager_load!` directly because it will raise as soon as it encounters
# an error, which is not what we want. We want to try to load as much as we can.
engines.each do |engine|
eager_load_paths(engine).each do |load_path|
Dir.glob("#{load_path}/**/*.rb").sort.each do |file|
require_dependency file
end
rescue ScriptError, StandardError
nil
end
end
end

sig { returns(T::Boolean) }
def zeitwerk_mode?
Rails.respond_to?(:autoloaders) &&
Rails.autoloaders.respond_to?(:zeitwerk_enabled?) &&
Rails.autoloaders.zeitwerk_enabled?
end

sig { params(blk: T.proc.void).void }
def with_rails_application(&blk)
# Store the current Rails.application object so that we can restore it
rails_application = T.unsafe(Rails.application)

# Create a new Rails::Application object, so that we can load the engines.
# Some engines and the `Rails.autoloaders` call might expect `Rails.application`
# to be set, so we need to create one here.
unless rails_application
Rails.application = Class.new(Rails::Application)
end

blk.call
ensure
Rails.app_class = Rails.application = rails_application
end

T::Sig::WithoutRuntime.sig { returns(T::Array[T.class_of(Rails::Engine)]) }
def engines
return [] unless defined?(Rails::Engine)

safe_require("active_support/core_ext/class/subclasses")

project_path = Bundler.default_gemfile.parent.expand_path
# We can use `Class#descendants` here, since we know Rails is loaded
Rails::Engine
.descendants
.reject(&:abstract_railtie?)
.reject { |engine| gem_in_app_dir?(project_path, engine.config.root.to_path) }
end

sig { params(path: String).void }
def safe_require(path)
require path
rescue LoadError
nil
end

sig { void }
def eager_load_rails_app
application = Rails.application
Expand Down
23 changes: 23 additions & 0 deletions spec/tapioca/cli/gem_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1272,6 +1272,19 @@ class Post
RB
end

@project.write!("config/application.rb", <<~RB)
module Tapioca
class Application < Rails::Application
config.load_defaults(#{ActiveSupport.gem_version.to_s[0..2]})
end
end
RB

@project.write!("config/environment.rb", <<~RB)
require_relative "application"
Rails.application.initialize!
RB

@project.require_real_gem("rails", ActiveSupport.gem_version.to_s)
@project.require_mock_gem(foo)
@project.bundle_install!
Expand Down Expand Up @@ -1329,6 +1342,11 @@ class Application < Rails::Application
end
RB

@project.write!("config/environment.rb", <<~RB)
require_relative "application"
Rails.application.initialize!
RB

response = @project.tapioca("gem turbo-rails")

assert_includes(response.out, "Compiled turbo-rails")
Expand Down Expand Up @@ -1360,6 +1378,11 @@ class Application < Rails::Application
end
RB

@project.write!("config/environment.rb", <<~RB)
require_relative "application"
Rails.application.initialize!
RB

response = @project.tapioca("gem turbo-rails")

assert_includes(response.out, "Compiled turbo-rails")
Expand Down

0 comments on commit 2b58eae

Please sign in to comment.