mirror of https://github.com/rails/rails
Improve support for custom namespaces
This commit is contained in:
parent
cc0951dbb8
commit
87f3f811a7
|
@ -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
|
||||
-----------------------
|
||||
|
||||
|
|
|
@ -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*
|
||||
|
|
|
@ -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"
|
||||
|
@ -79,10 +80,15 @@ module Rails
|
|||
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)
|
||||
|
|
|
@ -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"
|
||||
|
@ -17,10 +18,15 @@ module Rails
|
|||
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)
|
||||
|
|
|
@ -56,6 +56,48 @@ class ZeitwerkIntegrationTest < ActiveSupport::TestCase
|
|||
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"
|
||||
|
|
Loading…
Reference in New Issue