Skip to content
Merged
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
36 changes: 36 additions & 0 deletions guides/source/autoloading_and_reloading_constants.md
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,42 @@ If an application does not use the `once` autoloader, the snippets above can go

Applications using the `once` autoloader have to move or load this configuration from the body of the application class in `config/application.rb`, because the `once` autoloader uses the inflector early in the boot process.

Custom Namespaces
-----------------

As we saw above, autoload paths represent the top-level namespace: `Object`.

Let's consider `app/services`, for example. This directory is not generated by default, but if it exists, Rails automatically adds it to the autoload paths.

By default, the file `app/services/users/signup.rb` is expected to define `Users::Signup`, but what if you prefer that entire subtree to be under a `Services` namespace? Well, with default settings, that can be accomplished by creating a subdirectory: `app/services/services`.

However, depending on your taste, that just might not feel right to you. You might prefer that `app/services/users/signup.rb` simply defines `Services::Users::Signup`.

Zeitwerk supports [custom root namespaces](https://github.com/fxn/zeitwerk#custom-root-namespaces) to address this use case, and you can customize the `main` autoloader to accomplish that:

```ruby
# config/initializers/autoloading.rb

# The namespace has to exist.
#
# In this example we define the module on the spot. Could also be created
# elsewhere and its definition loaded here with an ordinary `require`. In
# any case, `push_dir` expects a class or module object as second argument.
module Services; end

Rails.autoloaders.main.push_dir("#{Rails.root}/app/services", Services)
```

Applications running on Rails < 7.1 have to additionally delete the directory from `ActiveSupport::Dependencies.autoload_paths`. Just add this line to the same file:

```ruby
# For applications running on Rails < 7.1.
# The argument has to be a string.
ActiveSupport::Dependencies.autoload_paths("#{Rails.root}/app/services")
```

Custom namespaces are also supported for the `once` autoloader. However, since that one is set up earlier in the boot process, the configuration cannot be done in an application initializer. Instead, please put it in `config/application.rb`, for example.

Autoloading and Engines
-----------------------

Expand Down
25 changes: 25 additions & 0 deletions railties/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
* Autoloading setup honors root directories manually set by the user.

This is relevant for custom namespaces. For example, if you'd like classes
and modules under `app/services` to be defined in the `Services` namespace
without an extra `app/services/services` directory, this is now enough:

```ruby
# config/initializers/autoloading.rb

# The namespace has to exist.
#
# In this example we define the module on the spot. Could also be created
# elsewhere and its definition loaded here with an ordinary `require`. In
# any case, `push_dir` expects a class or module object as second argument.
module Services; end

Rails.autoloaders.main.push_dir("#{Rails.root}/app/services", Services)
```

Before this change, Rails would later override the configuration. You had to
delete `app/services` from `ActiveSupport::Dependencies.autoload_paths` as
well.

*Xavier Noria*

* Use infinitive form for all rails command descriptions verbs.

*Petrik de Heus*
Expand Down
6 changes: 6 additions & 0 deletions railties/lib/rails/application/bootstrap.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

require "fileutils"
require "set"
require "active_support/notifications"
require "active_support/dependencies"
require "active_support/descendants_tracker"
Expand Down Expand Up @@ -79,10 +80,15 @@ module Bootstrap
initializer :setup_once_autoloader, after: :set_eager_load_paths, before: :bootstrap_hook do
autoloader = Rails.autoloaders.once

# Normally empty, but if the user already defined some, we won't
# override them. Important if there are custom namespaces associated.
already_configured_dirs = Set.new(autoloader.dirs)

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)
next if already_configured_dirs.member?(path.to_s)

autoloader.push_dir(path)
autoloader.do_not_eager_load(path) unless ActiveSupport::Dependencies.eager_load?(path)
Expand Down
6 changes: 6 additions & 0 deletions railties/lib/rails/application/finisher.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# frozen_string_literal: true

require "set"
require "active_support/core_ext/string/inflections"
require "active_support/core_ext/array/conversions"
require "active_support/descendants_tracker"
Expand All @@ -17,10 +18,15 @@ module Finisher
initializer :setup_main_autoloader do
autoloader = Rails.autoloaders.main

# Normally empty, but if the user already defined some, we won't
# override them. Important if there are custom namespaces associated.
already_configured_dirs = Set.new(autoloader.dirs)

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)
next if already_configured_dirs.member?(path.to_s)

autoloader.push_dir(path)
autoloader.do_not_eager_load(path) unless ActiveSupport::Dependencies.eager_load?(path)
Expand Down
42 changes: 42 additions & 0 deletions railties/test/application/zeitwerk_integration_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,48 @@ class RESTfulController < ApplicationController
assert RESTfulController
end

test "root directories manually set by the user are honored (once)" do
app_file "extras1/x.rb", "ZeitwerkIntegrationTestExtras::X = true"
app_file "extras2/y.rb", "ZeitwerkIntegrationTestExtras::Y = true"

add_to_env_config "development", <<~'RUBY'
config.autoload_once_paths << "#{Rails.root}/extras1"
config.autoload_once_paths << Rails.root.join("extras2")

module ZeitwerkIntegrationTestExtras; end

autoloader = Rails.autoloaders.once
autoloader.push_dir("#{Rails.root}/extras1", namespace: ZeitwerkIntegrationTestExtras)
autoloader.push_dir("#{Rails.root}/extras2", namespace: ZeitwerkIntegrationTestExtras)
RUBY

boot

assert ZeitwerkIntegrationTestExtras::X
assert ZeitwerkIntegrationTestExtras::Y
end

test "root directories manually set by the user are honored (main)" do
app_file "app/services/x.rb", "ZeitwerkIntegrationTestServices::X = true"
app_file "extras/x.rb", "ZeitwerkIntegrationTestExtras::X = true"

app_file "config/initializers/namespaces.rb", <<~'RUBY'
module ZeitwerkIntegrationTestServices; end
module ZeitwerkIntegrationTestExtras; end

ActiveSupport::Dependencies.autoload_paths << Rails.root.join("extras")

Rails.autoloaders.main.tap do |main|
main.push_dir("#{Rails.root}/app/services", namespace: ZeitwerkIntegrationTestServices)
main.push_dir("#{Rails.root}/extras", namespace: ZeitwerkIntegrationTestExtras)
end
RUBY

boot

assert ZeitwerkIntegrationTestServices::X
assert ZeitwerkIntegrationTestExtras::X
end

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