From 821d6c694cd305b7792b96d6ebc1c15ca235cf3e Mon Sep 17 00:00:00 2001 From: Xavier Noria Date: Mon, 11 Feb 2019 12:44:25 -0800 Subject: Zeitwerk integration --- railties/lib/rails.rb | 20 +++ railties/lib/rails/application/configuration.rb | 13 +- railties/lib/rails/application/finisher.rb | 8 + railties/lib/rails/engine.rb | 26 +++- .../generators/rails/app/templates/Gemfile.tt | 6 +- railties/test/application/configuration_test.rb | 21 +++ railties/test/application/mailer_previews_test.rb | 3 + .../test/application/multiple_applications_test.rb | 24 --- railties/test/application/rake_test.rb | 4 +- .../test/application/zeitwerk_integration_test.rb | 164 +++++++++++++++++++++ railties/test/generators/app_generator_test.rb | 9 ++ railties/test/isolation/abstract_unit.rb | 10 +- railties/test/railties/engine_test.rb | 32 ++-- 13 files changed, 289 insertions(+), 51 deletions(-) create mode 100644 railties/test/application/zeitwerk_integration_test.rb (limited to 'railties') diff --git a/railties/lib/rails.rb b/railties/lib/rails.rb index 092105d502..bca2cf34e1 100644 --- a/railties/lib/rails.rb +++ b/railties/lib/rails.rb @@ -110,5 +110,25 @@ module Rails def public_path application && Pathname.new(application.paths["public"].first) end + + def autoloader + if configuration.autoloader == :zeitwerk + @autoloader ||= Zeitwerk::Loader.new + end + end + + def once_autoloader + if configuration.autoloader == :zeitwerk + @once_autoloader ||= Zeitwerk::Loader.new + end + end + + def autoloaders + if configuration.autoloader == :zeitwerk + [autoloader, once_autoloader] + else + [] + end + end end end diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index b7838f7e32..16fbc99e7a 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -20,7 +20,7 @@ module Rails :read_encrypted_secrets, :log_level, :content_security_policy_report_only, :content_security_policy_nonce_generator, :require_master_key, :credentials - attr_reader :encoding, :api_only, :loaded_config_version + attr_reader :encoding, :api_only, :loaded_config_version, :autoloader def initialize(*) super @@ -64,6 +64,7 @@ module Rails @credentials = ActiveSupport::OrderedOptions.new @credentials.content_path = default_credentials_content_path @credentials.key_path = default_credentials_key_path + @autoloader = :classic end def load_defaults(target_version) @@ -117,6 +118,8 @@ module Rails when "6.0" load_defaults "5.2" + self.autoloader = :zeitwerk if RUBY_ENGINE == "ruby" + if respond_to?(:action_view) action_view.default_enforce_utf8 = false end @@ -267,6 +270,14 @@ module Rails end end + def autoloader=(autoloader) + if %i(classic zeitwerk).include?(autoloader) + @autoloader = autoloader + else + raise ArgumentError, "config.autoloader may be :classic or :zeitwerk, got #{autoloader.inspect} instead" + end + end + class Custom #:nodoc: def initialize @configurations = Hash.new diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb index 04aaf6dd9a..39e8ef6631 100644 --- a/railties/lib/rails/application/finisher.rb +++ b/railties/lib/rails/application/finisher.rb @@ -21,6 +21,13 @@ module Rails end end + initializer :let_zeitwerk_take_over do + if config.autoloader == :zeitwerk + require "active_support/dependencies/zeitwerk_integration" + ActiveSupport::Dependencies::ZeitwerkIntegration.take_over + end + end + initializer :add_builtin_route do |app| if Rails.env.development? app.routes.prepend do @@ -66,6 +73,7 @@ module Rails initializer :eager_load! do if config.eager_load ActiveSupport.run_load_hooks(:before_eager_load, self) + Zeitwerk::Loader.eager_load_all if defined?(Zeitwerk) config.eager_load_namespaces.each(&:eager_load!) end end diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb index d6c329b581..2485158a7b 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -472,12 +472,10 @@ module Rails # Eager load the application by loading all ruby # files inside eager_load paths. def eager_load! - config.eager_load_paths.each do |load_path| - # Starts after load_path plus a slash, ends before ".rb". - relname_range = (load_path.to_s.length + 1)...-3 - Dir.glob("#{load_path}/**/*.rb").sort.each do |file| - require_dependency file[relname_range] - end + if Rails.autoloader + eager_load_with_zeitwerk! + else + eager_load_with_dependencies! end end @@ -653,6 +651,22 @@ module Rails private + def eager_load_with_zeitwerk! + (config.eager_load_paths - Zeitwerk::Loader.all_dirs).each do |path| + Dir.glob("#{path}/**/*.rb").sort.each { |file| require file } + end + end + + def eager_load_with_dependencies! + config.eager_load_paths.each do |load_path| + # Starts after load_path plus a slash, ends before ".rb". + relname_range = (load_path.to_s.length + 1)...-3 + Dir.glob("#{load_path}/**/*.rb").sort.each do |file| + require_dependency file[relname_range] + end + end + end + def load_config_initializer(initializer) # :doc: ActiveSupport::Notifications.instrument("load_config_initializer.railties", initializer: initializer) do load(initializer) diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile.tt b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt index d39b5d311f..a1f1224a45 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile.tt +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt @@ -28,7 +28,7 @@ ruby <%= "'#{RUBY_VERSION}'" -%> <% if depend_on_bootsnap? -%> # Reduces boot times through caching; required in config/boot.rb -gem 'bootsnap', '>= 1.1.0', require: false +gem 'bootsnap', '>= 1.4.0', require: false <%- end -%> <%- if options.api? -%> @@ -36,7 +36,9 @@ gem 'bootsnap', '>= 1.1.0', require: false # gem 'rack-cors' <%- end -%> -<% if RUBY_ENGINE == 'ruby' -%> +<% if RUBY_ENGINE == "ruby" -%> +gem "zeitwerk", ">= 1.0.0" + group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index 9ea4d6dc5f..960f708bdf 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -1157,6 +1157,27 @@ module ApplicationTests end end + test "autoloader & autoloader=" do + app "development" + + config = Rails.application.config + assert_instance_of Zeitwerk::Loader, Rails.autoloader + assert_instance_of Zeitwerk::Loader, Rails.once_autoloader + assert_equal [Rails.autoloader, Rails.once_autoloader], Rails.autoloaders + + config.autoloader = :classic + assert_nil Rails.autoloader + assert_nil Rails.once_autoloader + assert_empty Rails.autoloaders + + config.autoloader = :zeitwerk + assert_instance_of Zeitwerk::Loader, Rails.autoloader + assert_instance_of Zeitwerk::Loader, Rails.once_autoloader + assert_equal [Rails.autoloader, Rails.once_autoloader], Rails.autoloaders + + assert_raises(ArgumentError) { config.autoloader = :unknown } + end + test "config.action_view.cache_template_loading with cache_classes default" do add_to_config "config.cache_classes = true" diff --git a/railties/test/application/mailer_previews_test.rb b/railties/test/application/mailer_previews_test.rb index ba186bda44..fb84276b8a 100644 --- a/railties/test/application/mailer_previews_test.rb +++ b/railties/test/application/mailer_previews_test.rb @@ -85,6 +85,7 @@ module ApplicationTests end test "mailer previews are loaded from a custom preview_path" do + app_dir "lib/mailer_previews" add_to_config "config.action_mailer.preview_path = '#{app_path}/lib/mailer_previews'" mailer "notifier", <<-RUBY @@ -254,6 +255,7 @@ module ApplicationTests end test "mailer previews are reloaded from a custom preview_path" do + app_dir "lib/mailer_previews" add_to_config "config.action_mailer.preview_path = '#{app_path}/lib/mailer_previews'" app("development") @@ -818,6 +820,7 @@ module ApplicationTests def build_app super app_file "config/routes.rb", "Rails.application.routes.draw do; end" + app_dir "test/mailers/previews" end def mailer(name, contents) diff --git a/railties/test/application/multiple_applications_test.rb b/railties/test/application/multiple_applications_test.rb index 432344bccc..f0f1112f6b 100644 --- a/railties/test/application/multiple_applications_test.rb +++ b/railties/test/application/multiple_applications_test.rb @@ -100,30 +100,6 @@ module ApplicationTests assert_nothing_raised { AppTemplate::Application.new } end - def test_initializers_run_on_different_applications_go_to_the_same_class - application1 = AppTemplate::Application.new - run_count = 0 - - AppTemplate::Application.initializer :init0 do - run_count += 1 - end - - application1.initializer :init1 do - run_count += 1 - end - - AppTemplate::Application.new.initializer :init2 do - run_count += 1 - end - - assert_equal 0, run_count, "Without loading the initializers, the count should be 0" - - # Set config.eager_load to false so that an eager_load warning doesn't pop up - AppTemplate::Application.create { config.eager_load = false }.initialize! - - assert_equal 3, run_count, "There should have been three initializers that incremented the count" - end - def test_consoles_run_on_different_applications_go_to_the_same_class run_count = 0 AppTemplate::Application.console { run_count += 1 } diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index 44e3b0f66b..fe56e3d076 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -145,8 +145,8 @@ module ApplicationTests # loading a specific fixture rails "db:fixtures:load", "FIXTURES=products" - assert_equal 2, ::AppTemplate::Application::Product.count - assert_equal 0, ::AppTemplate::Application::User.count + assert_equal 2, Product.count + assert_equal 0, User.count end def test_loading_only_yml_fixtures diff --git a/railties/test/application/zeitwerk_integration_test.rb b/railties/test/application/zeitwerk_integration_test.rb new file mode 100644 index 0000000000..d44b7f7e4d --- /dev/null +++ b/railties/test/application/zeitwerk_integration_test.rb @@ -0,0 +1,164 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "active_support/dependencies/zeitwerk_integration" + +class ZeitwerkIntegrationTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + end + + def boot(env = "development") + app(env) + end + + def teardown + teardown_app + end + + def deps + ActiveSupport::Dependencies + end + + def decorated? + deps.singleton_class < deps::ZeitwerkIntegration::Decorations + end + + test "ActiveSupport::Dependencies is decorated by default" do + boot + + assert decorated? + assert_instance_of Zeitwerk::Loader, Rails.autoloader + assert_instance_of Zeitwerk::Loader, Rails.once_autoloader + assert_equal [Rails.autoloader, Rails.once_autoloader], Rails.autoloaders + end + + test "ActiveSupport::Dependencies is not decorated in classic mode" do + add_to_config 'config.autoloader = :classic' + boot + + assert_not decorated? + assert_nil Rails.autoloader + assert_nil Rails.once_autoloader + assert_empty Rails.autoloaders + end + + test "constantize returns the value stored in the constant" do + app_file "app/models/admin/user.rb", "class Admin::User; end" + boot + + assert_same Admin::User, deps.constantize("Admin::User") + end + + test "constantize raises if the constant is unknown" do + boot + + assert_raises(NameError) { deps.constantize("Admin") } + end + + test "safe_constantize returns the value stored in the constant" do + app_file "app/models/admin/user.rb", "class Admin::User; end" + boot + + assert_same Admin::User, deps.safe_constantize("Admin::User") + end + + test "safe_constantize returns nil for unknown constants" do + boot + + assert_nil deps.safe_constantize("Admin") + end + + test "autoloaded_constants returns autoloaded constant paths" do + app_file "app/models/admin/user.rb", "class Admin::User; end" + app_file "app/models/post.rb", "class Post; end" + boot + + assert Admin::User + assert_equal ["Admin", "Admin::User"], deps.autoloaded_constants + end + + test "autoloaded? says if a constant has been autoloaded" do + app_file "app/models/user.rb", "class User; end" + app_file "app/models/post.rb", "class Post; end" + boot + + assert Post + assert deps.autoloaded?("Post") + assert deps.autoloaded?(Post) + assert_not deps.autoloaded?("User") + end + + test "eager loading loads the application code" do + $zeitwerk_integration_test_user = false + $zeitwerk_integration_test_post = false + + app_file "app/models/user.rb", "class User; end; $zeitwerk_integration_test_user = true" + app_file "app/models/post.rb", "class Post; end; $zeitwerk_integration_test_post = true" + boot("production") + + assert $zeitwerk_integration_test_user + assert $zeitwerk_integration_test_post + end + + test "eager loading loads anything managed by Zeitwerk" do + $zeitwerk_integration_test_user = false + app_file "app/models/user.rb", "class User; end; $zeitwerk_integration_test_user = true" + + $zeitwerk_integration_test_extras = false + app_dir "extras" + app_file "extras/webhook_hacks.rb", "WebhookHacks = 1; $zeitwerk_integration_test_extras = true" + + require "zeitwerk" + autoloader = Zeitwerk::Loader.new + autoloader.push_dir("#{app_path}/extras") + autoloader.setup + + boot("production") + + assert $zeitwerk_integration_test_user + assert $zeitwerk_integration_test_extras + end + + test "autoload paths that are below Gem.path go to the once autoloader" do + app_dir "extras" + add_to_config 'config.autoload_paths << "#{Rails.root}/extras"' + + # Mocks Gem.path to include the extras directory. + Gem.singleton_class.prepend( + Module.new do + def path + super + ["#{Rails.root}/extras"] + end + end + ) + boot + + refute_includes Rails.autoloader.dirs, "#{app_path}/extras" + assert_includes Rails.once_autoloader.dirs, "#{app_path}/extras" + end + + test "clear reloads the main autoloader, and does not reload the once one" do + boot + + $zeitwerk_integration_reload_test = [] + + autoloader = Rails.autoloader + def autoloader.reload + $zeitwerk_integration_reload_test << :autoloader + super + end + + once_autoloader = Rails.once_autoloader + def once_autoloader.reload + $zeitwerk_integration_reload_test << :once_autoloader + super + end + + ActiveSupport::Dependencies.clear + + assert_equal %i(autoloader), $zeitwerk_integration_reload_test + end +end diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 1ee9e43e89..937b8eb427 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -660,6 +660,15 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_gem "jbuilder" end + def test_inclusion_of_zeitwerk + run_generator + if RUBY_ENGINE == "ruby" + assert_gem "zeitwerk" + else + assert_no_gem "zeitwerk" + end + end + def test_inclusion_of_a_debugger run_generator if defined?(JRUBY_VERSION) || RUBY_ENGINE == "rbx" diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 0e8e0e86ee..47d42645c6 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -421,6 +421,10 @@ module TestHelpers file_name end + def app_dir(path) + FileUtils.mkdir_p("#{app_path}/#{path}") + end + def remove_file(path) FileUtils.rm_rf "#{app_path}/#{path}" end @@ -487,7 +491,11 @@ Module.new do # Fake 'Bundler.require' -- we run using the repo's Gemfile, not an # app-specific one: we don't want to require every gem that lists. contents = File.read("#{app_template_path}/config/application.rb") - contents.sub!(/^Bundler\.require.*/, "%w(turbolinks webpacker).each { |r| require r }") + if RUBY_ENGINE == "ruby" + contents.sub!(/^Bundler\.require.*/, "%w(turbolinks webpacker zeitwerk).each { |r| require r }") + else + contents.sub!(/^Bundler\.require.*/, "%w(turbolinks webpacker).each { |r| require r }") + end File.write("#{app_template_path}/config/application.rb", contents) require "rails" diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb index 851407dede..69f6e34d58 100644 --- a/railties/test/railties/engine_test.rb +++ b/railties/test/railties/engine_test.rb @@ -704,25 +704,27 @@ YAML RUBY @plugin.write "app/controllers/bukkits/foo_controller.rb", <<-RUBY - class Bukkits::FooController < ActionController::Base - def index - render inline: "<%= help_the_engine %>" - end + module Bukkits + class FooController < ActionController::Base + def index + render inline: "<%= help_the_engine %>" + end - def show - render plain: foo_path - end + def show + render plain: foo_path + end - def from_app - render inline: "<%= (self.respond_to?(:bar_path) || self.respond_to?(:something)) %>" - end + def from_app + render inline: "<%= (self.respond_to?(:bar_path) || self.respond_to?(:something)) %>" + end - def routes_helpers_in_view - render inline: "<%= foo_path %>, <%= main_app.bar_path %>" - end + def routes_helpers_in_view + render inline: "<%= foo_path %>, <%= main_app.bar_path %>" + end - def polymorphic_path_without_namespace - render plain: polymorphic_path(Post.new) + def polymorphic_path_without_namespace + render plain: polymorphic_path(Post.new) + end end end RUBY -- cgit v1.2.3