Skip to content

Commit c06e61f

Browse files
authored
Merge pull request rails#47583 from rails/zeitwerk-namespaces
Improve support for custom namespaces
2 parents cc0951d + 87f3f81 commit c06e61f

File tree

5 files changed

+115
-0
lines changed

5 files changed

+115
-0
lines changed

guides/source/autoloading_and_reloading_constants.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,42 @@ If an application does not use the `once` autoloader, the snippets above can go
487487

488488
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.
489489

490+
Custom Namespaces
491+
-----------------
492+
493+
As we saw above, autoload paths represent the top-level namespace: `Object`.
494+
495+
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.
496+
497+
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`.
498+
499+
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`.
500+
501+
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:
502+
503+
```ruby
504+
# config/initializers/autoloading.rb
505+
506+
# The namespace has to exist.
507+
#
508+
# In this example we define the module on the spot. Could also be created
509+
# elsewhere and its definition loaded here with an ordinary `require`. In
510+
# any case, `push_dir` expects a class or module object as second argument.
511+
module Services; end
512+
513+
Rails.autoloaders.main.push_dir("#{Rails.root}/app/services", Services)
514+
```
515+
516+
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:
517+
518+
```ruby
519+
# For applications running on Rails < 7.1.
520+
# The argument has to be a string.
521+
ActiveSupport::Dependencies.autoload_paths("#{Rails.root}/app/services")
522+
```
523+
524+
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.
525+
490526
Autoloading and Engines
491527
-----------------------
492528

railties/CHANGELOG.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,28 @@
1+
* Autoloading setup honors root directories manually set by the user.
2+
3+
This is relevant for custom namespaces. For example, if you'd like classes
4+
and modules under `app/services` to be defined in the `Services` namespace
5+
without an extra `app/services/services` directory, this is now enough:
6+
7+
```ruby
8+
# config/initializers/autoloading.rb
9+
10+
# The namespace has to exist.
11+
#
12+
# In this example we define the module on the spot. Could also be created
13+
# elsewhere and its definition loaded here with an ordinary `require`. In
14+
# any case, `push_dir` expects a class or module object as second argument.
15+
module Services; end
16+
17+
Rails.autoloaders.main.push_dir("#{Rails.root}/app/services", Services)
18+
```
19+
20+
Before this change, Rails would later override the configuration. You had to
21+
delete `app/services` from `ActiveSupport::Dependencies.autoload_paths` as
22+
well.
23+
24+
*Xavier Noria*
25+
126
* Use infinitive form for all rails command descriptions verbs.
227

328
*Petrik de Heus*

railties/lib/rails/application/bootstrap.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require "fileutils"
4+
require "set"
45
require "active_support/notifications"
56
require "active_support/dependencies"
67
require "active_support/descendants_tracker"
@@ -79,10 +80,15 @@ module Bootstrap
7980
initializer :setup_once_autoloader, after: :set_eager_load_paths, before: :bootstrap_hook do
8081
autoloader = Rails.autoloaders.once
8182

83+
# Normally empty, but if the user already defined some, we won't
84+
# override them. Important if there are custom namespaces associated.
85+
already_configured_dirs = Set.new(autoloader.dirs)
86+
8287
ActiveSupport::Dependencies.autoload_once_paths.freeze
8388
ActiveSupport::Dependencies.autoload_once_paths.uniq.each do |path|
8489
# Zeitwerk only accepts existing directories in `push_dir`.
8590
next unless File.directory?(path)
91+
next if already_configured_dirs.member?(path.to_s)
8692

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

railties/lib/rails/application/finisher.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# frozen_string_literal: true
22

3+
require "set"
34
require "active_support/core_ext/string/inflections"
45
require "active_support/core_ext/array/conversions"
56
require "active_support/descendants_tracker"
@@ -17,10 +18,15 @@ module Finisher
1718
initializer :setup_main_autoloader do
1819
autoloader = Rails.autoloaders.main
1920

21+
# Normally empty, but if the user already defined some, we won't
22+
# override them. Important if there are custom namespaces associated.
23+
already_configured_dirs = Set.new(autoloader.dirs)
24+
2025
ActiveSupport::Dependencies.autoload_paths.freeze
2126
ActiveSupport::Dependencies.autoload_paths.uniq.each do |path|
2227
# Zeitwerk only accepts existing directories in `push_dir`.
2328
next unless File.directory?(path)
29+
next if already_configured_dirs.member?(path.to_s)
2430

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

railties/test/application/zeitwerk_integration_test.rb

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,48 @@ class RESTfulController < ApplicationController
5656
assert RESTfulController
5757
end
5858

59+
test "root directories manually set by the user are honored (once)" do
60+
app_file "extras1/x.rb", "ZeitwerkIntegrationTestExtras::X = true"
61+
app_file "extras2/y.rb", "ZeitwerkIntegrationTestExtras::Y = true"
62+
63+
add_to_env_config "development", <<~'RUBY'
64+
config.autoload_once_paths << "#{Rails.root}/extras1"
65+
config.autoload_once_paths << Rails.root.join("extras2")
66+
67+
module ZeitwerkIntegrationTestExtras; end
68+
69+
autoloader = Rails.autoloaders.once
70+
autoloader.push_dir("#{Rails.root}/extras1", namespace: ZeitwerkIntegrationTestExtras)
71+
autoloader.push_dir("#{Rails.root}/extras2", namespace: ZeitwerkIntegrationTestExtras)
72+
RUBY
73+
74+
boot
75+
76+
assert ZeitwerkIntegrationTestExtras::X
77+
assert ZeitwerkIntegrationTestExtras::Y
78+
end
79+
80+
test "root directories manually set by the user are honored (main)" do
81+
app_file "app/services/x.rb", "ZeitwerkIntegrationTestServices::X = true"
82+
app_file "extras/x.rb", "ZeitwerkIntegrationTestExtras::X = true"
83+
84+
app_file "config/initializers/namespaces.rb", <<~'RUBY'
85+
module ZeitwerkIntegrationTestServices; end
86+
module ZeitwerkIntegrationTestExtras; end
87+
88+
ActiveSupport::Dependencies.autoload_paths << Rails.root.join("extras")
89+
90+
Rails.autoloaders.main.tap do |main|
91+
main.push_dir("#{Rails.root}/app/services", namespace: ZeitwerkIntegrationTestServices)
92+
main.push_dir("#{Rails.root}/extras", namespace: ZeitwerkIntegrationTestExtras)
93+
end
94+
RUBY
95+
96+
boot
97+
98+
assert ZeitwerkIntegrationTestServices::X
99+
assert ZeitwerkIntegrationTestExtras::X
100+
end
59101

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

0 commit comments

Comments
 (0)