Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lazily boot the rails app during tapioca gem #2116

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading