Skip to content

Commit

Permalink
Setup the once autoloader on bootstrap
Browse files Browse the repository at this point in the history
  • Loading branch information
fxn committed Aug 17, 2021
1 parent 6ee025a commit 2306a8e
Show file tree
Hide file tree
Showing 9 changed files with 116 additions and 122 deletions.
4 changes: 2 additions & 2 deletions actionpack/test/controller/helper_test.rb
Expand Up @@ -87,8 +87,8 @@ def test_helpers_paths_priority
class HelpersTypoControllerTest < ActiveSupport::TestCase
def test_helper_typo_error_message
e = assert_raise(NameError) { HelpersTypoController.helper "admin/users" }
# This message is better if autoloading.
assert_equal "uninitialized constant Admin::UsersHelper\nDid you mean? Admin::UsersHelpeR", e.message
assert_includes e.message, "uninitialized constant Admin::UsersHelper"
assert_includes e.message, "Did you mean? Admin::UsersHelpeR"
end
end

Expand Down
8 changes: 3 additions & 5 deletions actionview/test/actionpack/abstract/helper_test.rb
Expand Up @@ -71,10 +71,8 @@ def test_helpers_with_symbol
end

def test_declare_missing_helper
e = assert_raise NameError do
AbstractHelpers.helper :missing
end
assert_equal "uninitialized constant MissingHelper", e.message
e = assert_raise(NameError) { AbstractHelpers.helper :missing }
assert_includes e.message, "uninitialized constant MissingHelper"
end

def test_helpers_with_module_through_block
Expand Down Expand Up @@ -103,7 +101,7 @@ def test_includes_controller_default_helper
class InvalidHelpersTest < ActiveSupport::TestCase
def test_controller_raise_error_about_missing_helper
e = assert_raise(NameError) { AbstractHelpers.helper(:missing) }
assert_equal "uninitialized constant MissingHelper", e.message
assert_includes e.message, "uninitialized constant MissingHelper"
end
end
end
Expand Down
4 changes: 4 additions & 0 deletions activesupport/lib/active_support/dependencies.rb
Expand Up @@ -50,6 +50,10 @@ def self.unload_interlock

# :nodoc:

def eager_load?(path)
Dependencies._eager_load_paths.member?(path)
end

# All files ever loaded.
mattr_accessor :history, default: Set.new

Expand Down
Expand Up @@ -53,54 +53,8 @@ def self.inflect(overrides)
end
end

class << self
def take_over(enable_reloading:)
setup_autoloaders(enable_reloading)
freeze_paths
decorate_dependencies
end

private
def setup_autoloaders(enable_reloading)
Dependencies.autoload_paths.each do |autoload_path|
# Zeitwerk only accepts existing directories in `push_dir` to
# prevent misconfigurations.
next unless File.directory?(autoload_path)

autoloader = \
autoload_once?(autoload_path) ? Rails.autoloaders.once : Rails.autoloaders.main

autoloader.push_dir(autoload_path)
autoloader.do_not_eager_load(autoload_path) unless eager_load?(autoload_path)
end

if enable_reloading
Rails.autoloaders.main.enable_reloading
Rails.autoloaders.main.on_unload do |_cpath, value, _abspath|
value.before_remove_const if value.respond_to?(:before_remove_const)
end
end

Rails.autoloaders.each(&:setup)
end

def autoload_once?(autoload_path)
Dependencies.autoload_once_paths.include?(autoload_path)
end

def eager_load?(autoload_path)
Dependencies._eager_load_paths.member?(autoload_path)
end

def freeze_paths
Dependencies.autoload_paths.freeze
Dependencies.autoload_once_paths.freeze
Dependencies._eager_load_paths.freeze
end

def decorate_dependencies
Dependencies.singleton_class.prepend(Decorations)
end
def self.take_over
Dependencies.singleton_class.prepend(Decorations)
end
end
end
Expand Down
17 changes: 17 additions & 0 deletions railties/lib/rails/application/bootstrap.rb
Expand Up @@ -64,6 +64,23 @@ module Bootstrap
end
end

# We setup the once autoloader this early so that engines and applications
# are able to autoload from these paths during initialization.
initializer :setup_once_autoloader do
autoloader = Rails.autoloaders.once

ActiveSupport::Dependencies.autoload_once_paths.freeze
ActiveSupport::Dependencies.autoload_once_paths.uniq.each do |path|
# Zeitwerk only accepts existing directories in `push_dir`.
next unless File.directory?(path)

autoloader.push_dir(path)
autoloader.do_not_eager_load(path) unless ActiveSupport::Dependencies.eager_load?(path)
end

autoloader.setup
end

initializer :bootstrap_hook, group: :all do |app|
ActiveSupport.run_load_hooks(:before_initialize, app)
end
Expand Down
24 changes: 23 additions & 1 deletion railties/lib/rails/application/finisher.rb
Expand Up @@ -13,9 +13,31 @@ module Finisher
config.generators.templates.unshift(*paths["lib/templates"].existent)
end

initializer :setup_main_autoloader do
autoloader = Rails.autoloaders.main

ActiveSupport::Dependencies.autoload_paths.freeze
ActiveSupport::Dependencies.autoload_paths.uniq.each do |path|
# Zeitwerk only accepts existing directories in `push_dir`.
next unless File.directory?(path)

autoloader.push_dir(path)
autoloader.do_not_eager_load(path) unless ActiveSupport::Dependencies.eager_load?(path)
end

unless config.cache_classes
autoloader.enable_reloading
autoloader.on_unload do |_cpath, value, _abspath|
value.before_remove_const if value.respond_to?(:before_remove_const)
end
end

autoloader.setup
end

initializer :let_zeitwerk_take_over do
require "active_support/dependencies/zeitwerk_integration"
ActiveSupport::Dependencies::ZeitwerkIntegration.take_over(enable_reloading: !config.cache_classes)
ActiveSupport::Dependencies::ZeitwerkIntegration.take_over
end

# Setup default session store if not already set in config/application.rb
Expand Down
29 changes: 17 additions & 12 deletions railties/lib/rails/engine.rb
Expand Up @@ -570,17 +570,14 @@ def load_seed
$LOAD_PATH.uniq!
end

# Set the paths from which Rails will automatically load source files,
# and the load_once paths.
#
# This needs to be an initializer, since it needs to run once
# per engine and get the engine as a block parameter.
initializer :set_autoload_paths, before: :bootstrap_hook do
ActiveSupport::Dependencies.autoload_paths.unshift(*_all_autoload_paths)
initializer :set_autoload_once_paths, before: :setup_once_autoloader do
config.autoload_once_paths.freeze
ActiveSupport::Dependencies.autoload_once_paths.unshift(*_all_autoload_once_paths)
end

initializer :set_autoload_paths, before: :setup_main_autoloader do
config.autoload_paths.freeze
config.autoload_once_paths.freeze
ActiveSupport::Dependencies.autoload_paths.unshift(*_all_autoload_paths)
end

initializer :set_eager_load_paths, before: :bootstrap_hook do
Expand Down Expand Up @@ -694,17 +691,25 @@ def default_middleware_stack
end

def _all_autoload_once_paths
config.autoload_once_paths
config.autoload_once_paths.uniq
end

def _all_autoload_paths
@_all_autoload_paths ||= (config.autoload_paths + config.eager_load_paths + config.autoload_once_paths).uniq
@_all_autoload_paths ||= begin
autoload_paths = config.autoload_paths
autoload_paths += config.eager_load_paths
autoload_paths -= config.autoload_once_paths
autoload_paths.uniq
end
end

def _all_load_paths(add_autoload_paths_to_load_path)
@_all_load_paths ||= begin
load_paths = config.paths.load_paths
load_paths += _all_autoload_paths if add_autoload_paths_to_load_path
load_paths = config.paths.load_paths
if add_autoload_paths_to_load_path
load_paths += _all_autoload_paths
load_paths += _all_autoload_once_paths
end
load_paths.uniq
end
end
Expand Down
54 changes: 0 additions & 54 deletions railties/test/application/configuration_test.rb
Expand Up @@ -1874,60 +1874,6 @@ def index
assert_includes $LOAD_PATH, "#{app_path}/custom_eager_load_path"
end

test "autoloading during initialization gets deprecation message and clearing if config.cache_classes is false" do
app_file "lib/c.rb", <<~EOS
class C
extend ActiveSupport::DescendantsTracker
end
class X < C
end
EOS

app_file "app/models/d.rb", <<~EOS
require "c"
class D < C
end
EOS

app_file "config/initializers/autoload.rb", "D.class"

app "development"

# TODO: Test deprecation message, assert_deprecated { app "development" }
# does not collect it.

assert_equal [X], C.descendants
assert_empty ActiveSupport::Dependencies.autoloaded_constants
end

test "autoloading during initialization triggers nothing if config.cache_classes is true" do
app_file "lib/c.rb", <<~EOS
class C
extend ActiveSupport::DescendantsTracker
end
class X < C
end
EOS

app_file "app/models/d.rb", <<~EOS
require "c"
class D < C
end
EOS

app_file "config/initializers/autoload.rb", "D.class"

app "production"

# TODO: Test no deprecation message is issued.

assert_equal [X, D], C.descendants
end

test "load_database_yaml returns blank hash if configuration file is blank" do
app_file "config/database.yml", ""
app "development"
Expand Down
48 changes: 48 additions & 0 deletions railties/test/application/zeitwerk_integration_test.rb
Expand Up @@ -96,6 +96,54 @@ def self.name
assert_not deps.autoloaded?(invalid_constant_name)
end

test "the once autoloader can autoload from initializers" do
app_file "extras0/x.rb", "X = 0"
app_file "extras1/y.rb", "Y = 0"

# We should be able to configure autoload_once_paths in
# config/application.rb and in config/environments/*.rb.
add_to_config 'config.autoload_once_paths << "#{Rails.root}/extras0"'
add_to_env_config "development", 'config.autoload_once_paths << "#{Rails.root}/extras1"'

# Collections should br frozen after bootstrap, and you are ready to
# autoload with the once autoloader. In particular, from initializers.
$config_autoload_once_paths_is_frozen = false
$global_autoload_once_paths_is_frozen = false
add_to_config <<~RUBY
initializer :test_autoload_once_paths_is_frozen, after: :bootstrap_hook do
$config_autoload_once_paths_is_frozen = config.autoload_once_paths.frozen?
$global_autoload_once_paths_is_frozen = ActiveSupport::Dependencies.autoload_once_paths.frozen?
X
end
RUBY

app_file "config/initializers/autoload_Y.rb", "Y"

# Preconditions.
assert_not Object.const_defined?(:X)
assert_not Object.const_defined?(:Y)

boot

assert Object.const_defined?(:X)
assert Object.const_defined?(:Y)
assert $config_autoload_once_paths_is_frozen
assert $global_autoload_once_paths_is_frozen
end

test "the once autoloader can eager load" do
app_file "app/serializers/money_serializer.rb", "MoneySerializer = :dummy_value"

add_to_config 'config.autoload_once_paths << "#{Rails.root}/app/serializers"'
add_to_config 'config.eager_load_paths << "#{Rails.root}/app/serializers"'

assert_not Object.const_defined?(:MoneySerializer)

boot("production")

assert Object.const_defined?(:MoneySerializer)
end

test "unloadable constants (main)" do
app_file "app/models/user.rb", "class User; end"
app_file "app/models/post.rb", "class Post; end"
Expand Down

0 comments on commit 2306a8e

Please sign in to comment.