lib/tapioca/loaders/loader.rb in tapioca-0.10.5 vs lib/tapioca/loaders/loader.rb in tapioca-0.11.0

- old
+ new

@@ -56,40 +56,106 @@ say("Continuing RBI generation without loading the Rails application.") end sig { void } def load_rails_engines - rails_engines.each do |engine| - errored_files = [] + 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| + engine.config.eager_load_paths.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| engine.config.eager_load_paths.each do |load_path| Dir.glob("#{load_path}/**/*.rb").sort.each do |file| - require(file) - rescue LoadError, StandardError - errored_files << file + require_dependency file end - end - - # Try files that have errored one more time - # It might have been a load order problem - errored_files.each do |file| - require(file) - rescue LoadError, StandardError + rescue ScriptError, StandardError nil end end end - sig { returns(T::Array[T.untyped]) } - def rails_engines - return [] unless Object.const_defined?("Rails::Engine") + 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 - Object.const_get("Rails::Engine") + Rails::Engine .descendants .reject(&:abstract_railtie?) .reject { |engine| gem_in_app_dir?(project_path, engine.config.root.to_path) } end @@ -101,33 +167,28 @@ end sig { void } def silence_deprecations # Stop any ActiveSupport Deprecations from being reported - Object.const_get("ActiveSupport::Deprecation").silenced = true - rescue NameError - nil + if defined?(ActiveSupport::Deprecation) + ActiveSupport::Deprecation.silenced = true + end end sig { void } def eager_load_rails_app - rails = Object.const_get("Rails") - application = rails.application + application = Rails.application - if Object.const_defined?("ActiveSupport") - Object.const_get("ActiveSupport").run_load_hooks( - :before_eager_load, - application, - ) + if defined?(ActiveSupport) + ActiveSupport.run_load_hooks(:before_eager_load, application) end - if Object.const_defined?("Zeitwerk::Loader") - zeitwerk_loader = Object.const_get("Zeitwerk::Loader") - zeitwerk_loader.eager_load_all + if defined?(Zeitwerk::Loader) + Zeitwerk::Loader.eager_load_all end - if rails.respond_to?(:autoloaders) && rails.autoloaders.zeitwerk_enabled? - rails.autoloaders.each(&:eager_load) + if Rails.respond_to?(:autoloaders) + Rails.autoloaders.each(&:eager_load) end if application.config.respond_to?(:eager_load_namespaces) application.config.eager_load_namespaces.each(&:eager_load!) end