zeitwerk 2.6.1 → 2.6.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +154 -7
- data/lib/zeitwerk/gem_loader.rb +1 -1
- data/lib/zeitwerk/kernel.rb +3 -0
- data/lib/zeitwerk/loader/callbacks.rb +4 -4
- data/lib/zeitwerk/loader/config.rb +24 -8
- data/lib/zeitwerk/loader/eager_load.rb +222 -0
- data/lib/zeitwerk/loader/helpers.rb +11 -1
- data/lib/zeitwerk/loader.rb +51 -76
- data/lib/zeitwerk/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a554ef9e7a26b8edb48779c400a620aa694e54313626505d231fa3c72d4f66fe
|
4
|
+
data.tar.gz: 779e40bff86dbb627aef9dfd3ed897620994f7578a3673b640fad2a449c61a61
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a9038b69db15d5800aec43f5e6d2367df5ea04f19b00f0cd9a76c36eebac1f7cc2e085442e05b4ece448ef0a717b8f10c5c75367d63dbdbfb916a3e2bf505fed
|
7
|
+
data.tar.gz: 065c3b61d2cc6fb82ae3e98ddca31c4d6842cb3d7f68187c822c7b6c97720cdbfa8de9c11b5b1ff749fc655d408cc98f72dd1107e08979a5249ba6451e2539ed
|
data/README.md
CHANGED
@@ -27,8 +27,14 @@
|
|
27
27
|
- [Autoloading](#autoloading)
|
28
28
|
- [Eager loading](#eager-loading)
|
29
29
|
- [Eager load exclusions](#eager-load-exclusions)
|
30
|
+
- [Eager load directories](#eager-load-directories)
|
31
|
+
- [Eager load namespaces](#eager-load-namespaces)
|
32
|
+
- [Eager load namespaces shared by several loaders](#eager-load-namespaces-shared-by-several-loaders)
|
30
33
|
- [Global eager load](#global-eager-load)
|
34
|
+
- [Loading individual files](#loading-individual-files)
|
31
35
|
- [Reloading](#reloading)
|
36
|
+
- [Configuration and usage](#configuration-and-usage)
|
37
|
+
- [Thread-safety](#thread-safety)
|
32
38
|
- [Inflection](#inflection)
|
33
39
|
- [Zeitwerk::Inflector](#zeitwerkinflector)
|
34
40
|
- [Zeitwerk::GemInflector](#zeitwerkgeminflector)
|
@@ -44,6 +50,7 @@
|
|
44
50
|
- [Use case: Files that do not follow the conventions](#use-case-files-that-do-not-follow-the-conventions)
|
45
51
|
- [Use case: The adapter pattern](#use-case-the-adapter-pattern)
|
46
52
|
- [Use case: Test files mixed with implementation files](#use-case-test-files-mixed-with-implementation-files)
|
53
|
+
- [Shadowed files](#shadowed-files)
|
47
54
|
- [Edge cases](#edge-cases)
|
48
55
|
- [Beware of circular dependencies](#beware-of-circular-dependencies)
|
49
56
|
- [Reopening third-party namespaces](#reopening-third-party-namespaces)
|
@@ -464,6 +471,75 @@ Which may be handy if the project eager loads in the test suite to [ensure proje
|
|
464
471
|
|
465
472
|
The `force` flag does not affect ignored files and directories, those are still ignored.
|
466
473
|
|
474
|
+
<a id="markdown-eager-load-directories" name="eager-load-directories"></a>
|
475
|
+
#### Eager load directories
|
476
|
+
|
477
|
+
The method `Zeitwerk::Loader#eager_load_dir` eager loads a given directory, recursively:
|
478
|
+
|
479
|
+
```ruby
|
480
|
+
loader.eager_load_dir("#{__dir__}/custom_web_app/routes")
|
481
|
+
```
|
482
|
+
|
483
|
+
This is useful when the loader is not eager loading the entire project, but you still need some subtree to be loaded for things to function properly.
|
484
|
+
|
485
|
+
Both strings and `Pathname` objects are supported as arguments. If the argument is not a directory managed by the receiver, the method raises `Zeitwerk::Error`.
|
486
|
+
|
487
|
+
[Eager load exclusions](#eager-load-exclusions), [ignored files and directories](#ignoring-parts-of-the-project), and [shadowed files](https://github.com/fxn/zeitwerk#shadowed-files) are not eager loaded.
|
488
|
+
|
489
|
+
`Zeitwerk::Loader#eager_load_dir` is idempotent, but compatible with reloading. If you eager load a directory and then reload, eager loading that directory will load its (current) contents again.
|
490
|
+
|
491
|
+
The method checks if a regular eager load was already executed, in which case it returns fast.
|
492
|
+
|
493
|
+
Nested root directories which are descendants of the argument are skipped. Those subtrees are considered to be conceptually apart.
|
494
|
+
|
495
|
+
<a id="markdown-eager-load-namespaces" name="eager-load-namespaces"></a>
|
496
|
+
#### Eager load namespaces
|
497
|
+
|
498
|
+
The method `Zeitwerk::Loader#eager_load_namespace` eager loads a given namespace, recursively:
|
499
|
+
|
500
|
+
```ruby
|
501
|
+
loader.eager_load_namespace(MyApp::Routes)
|
502
|
+
```
|
503
|
+
|
504
|
+
This is useful when the loader is not eager loading the entire project, but you still need some namespace to be loaded for things to function properly.
|
505
|
+
|
506
|
+
The argument has to be a class or module object and the method raises `Zeitwerk::Error` otherwise.
|
507
|
+
|
508
|
+
If the namespace is spread over multiple directories in the receiver's source tree, they are all eager loaded. For example, if you have a structure like
|
509
|
+
|
510
|
+
```
|
511
|
+
root_dir1/my_app/routes
|
512
|
+
root_dir2/my_app/routes
|
513
|
+
root_dir3/my_app/routes
|
514
|
+
```
|
515
|
+
|
516
|
+
where `root_directory{1,2,3}` are root directories, eager loading `MyApp::Routes` will eager load the contents of the three corresponding directories.
|
517
|
+
|
518
|
+
There might exist external source trees implementing part of the namespace. This happens routinely, because top-level constants are stored in the globally shared `Object`. It happens also when deliberately [reopening third-party namespaces](reopening-third-party-namespaces). Such external code is not eager loaded, the implementation is carefully scoped to what the receiver manages to avoid side-effects elsewhere.
|
519
|
+
|
520
|
+
This method is flexible about what it accepts. Its semantics have to be interpreted as: "_If_ you manage this namespace, or part of this namespace, please eager load what you got". In particular, if the receiver does not manage the namespace, it will simply do nothing, this is not an error condition.
|
521
|
+
|
522
|
+
[Eager load exclusions](#eager-load-exclusions), [ignored files and directories](#ignoring-parts-of-the-project), and [shadowed files](https://github.com/fxn/zeitwerk#shadowed-files) are not eager loaded.
|
523
|
+
|
524
|
+
`Zeitwerk::Loader#eager_load_namespace` is idempotent, but compatible with reloading. If you eager load a namespace and then reload, eager loading that namespace will load its (current) descendants again.
|
525
|
+
|
526
|
+
The method checks if a regular eager load was already executed, in which case it returns fast.
|
527
|
+
|
528
|
+
If root directories are assigned to custom namespaces, the method behaves as you'd expect, according to the namespacing relationship between the custom namespace and the argument.
|
529
|
+
|
530
|
+
<a id="markdown-eager-load-namespaces-shared-by-several-loaders" name="eager-load-namespaces-shared-by-several-loaders"></a>
|
531
|
+
#### Eager load namespaces shared by several loaders
|
532
|
+
|
533
|
+
The method `Zeitwerk::Loader.eager_load_namespace` broadcasts `eager_load_namespace` to all loaders.
|
534
|
+
|
535
|
+
```ruby
|
536
|
+
Zeitwerk::Loader.eager_load_namespace(MyFramework::Routes)
|
537
|
+
```
|
538
|
+
|
539
|
+
This may be handy, for example, if a framework supports plugins and a shared namespace needs to be eager loaded for the framework to function properly.
|
540
|
+
|
541
|
+
Please, note that loaders only eager load namespaces they manage, as documented above. Therefore, this method does not allow you to eager load namespaces not managed by Zeitwerk loaders.
|
542
|
+
|
467
543
|
<a id="markdown-global-eager-load" name="global-eager-load"></a>
|
468
544
|
#### Global eager load
|
469
545
|
|
@@ -479,9 +555,29 @@ Note that thanks to idempotence `Zeitwerk::Loader.eager_load_all` won't eager lo
|
|
479
555
|
|
480
556
|
This method does not accept the `force` flag, since in general it wouldn't be a good idea to force eager loading in 3rd party code.
|
481
557
|
|
558
|
+
<a id="markdown-loading-individual-files" name="loading-individual-files"></a>
|
559
|
+
### Loading individual files
|
560
|
+
|
561
|
+
The method `Zeitwerk::Loader#load_file` loads an individual Ruby file:
|
562
|
+
|
563
|
+
```ruby
|
564
|
+
loader.load_file("#{__dir__}/custom_web_app/routes.rb")
|
565
|
+
```
|
566
|
+
|
567
|
+
This is useful when the loader is not eager loading the entire project, but you still need an individual file to be loaded for things to function properly.
|
568
|
+
|
569
|
+
Both strings and `Pathname` objects are supported as arguments. The method raises `Zeitwerk::Error` if the argument is not a Ruby file, is [ignored](#ignoring-parts-of-the-project), is [shadowed](https://github.com/fxn/zeitwerk#shadowed-files), or is not managed by the receiver.
|
570
|
+
|
571
|
+
`Zeitwerk::Loader#load_file` is idempotent, but compatible with reloading. If you load a file and then reload, a new call will load its (current) contents again.
|
572
|
+
|
573
|
+
If you want to eager load a directory, `Zeitwerk::Loader#eager_load_dir` is more efficient than invoking `Zeitwerk::Loader#load_file` on its files.
|
574
|
+
|
482
575
|
<a id="markdown-reloading" name="reloading"></a>
|
483
576
|
### Reloading
|
484
577
|
|
578
|
+
<a id="markdown-configuration-and-usage" name="configuration-and-usage"></a>
|
579
|
+
#### Configuration and usage
|
580
|
+
|
485
581
|
Zeitwerk is able to reload code, but you need to enable this feature:
|
486
582
|
|
487
583
|
```ruby
|
@@ -503,12 +599,34 @@ Reloading removes the currently loaded classes and modules and resets the loader
|
|
503
599
|
|
504
600
|
It is important to highlight that this is an instance method. Don't worry about project dependencies managed by Zeitwerk, their loaders are independent.
|
505
601
|
|
506
|
-
|
602
|
+
<a id="markdown-thread-safety" name="thread-safety"></a>
|
603
|
+
#### Thread-safety
|
604
|
+
|
605
|
+
In order to reload safely, no other thread can be autoloading or reloading concurrently. Client code is responsible for this coordination.
|
507
606
|
|
508
|
-
|
509
|
-
|
607
|
+
For example, a web framework that serves each request in its own thread and has reloading enabled could create a read-write lock on boot like this:
|
608
|
+
|
609
|
+
```ruby
|
610
|
+
require "concurrent/atomic/read_write_lock"
|
510
611
|
|
511
|
-
|
612
|
+
MyFramework::RELOAD_RW_LOCK = Concurrent::ReadWriteLock.new
|
613
|
+
```
|
614
|
+
|
615
|
+
You acquire the lock for reading for serving each individual request:
|
616
|
+
|
617
|
+
```ruby
|
618
|
+
MyFramework::RELOAD_RW_LOCK.with_read_lock do
|
619
|
+
serve(request)
|
620
|
+
end
|
621
|
+
```
|
622
|
+
|
623
|
+
Then, when a reload is triggered, just acquire the lock for writing in order to execute the method call safely:
|
624
|
+
|
625
|
+
```ruby
|
626
|
+
MyFramework::RELOAD_RW_LOCK.with_write_lock do
|
627
|
+
loader.reload
|
628
|
+
end
|
629
|
+
```
|
512
630
|
|
513
631
|
On reloading, client code has to update anything that would otherwise be storing a stale object. For example, if the routing layer of a web framework stores reloadable controller class objects or instances in internal structures, on reload it has to refresh them somehow, possibly reevaluating routes.
|
514
632
|
|
@@ -905,10 +1023,39 @@ loader.ignore(tests)
|
|
905
1023
|
loader.setup
|
906
1024
|
```
|
907
1025
|
|
1026
|
+
<a id="markdown-shadowed-files" name="shadowed-files"></a>
|
1027
|
+
### Shadowed files
|
1028
|
+
|
1029
|
+
In Ruby, if you have several files called `foo.rb` in different directories of `$LOAD_PATH` and execute
|
1030
|
+
|
1031
|
+
```ruby
|
1032
|
+
require "foo"
|
1033
|
+
```
|
1034
|
+
|
1035
|
+
the first one found gets loaded, and the rest are ignored.
|
1036
|
+
|
1037
|
+
Zeitwerk behaves in a similar way. If `foo.rb` is present in several root directories (at the same namespace level), the constant `Foo` is autoloaded from the first one, and the rest of the files are not evaluated. If logging is enabled, you'll see something like
|
1038
|
+
|
1039
|
+
```
|
1040
|
+
file #{file} is ignored because #{previous_occurrence} has precedence
|
1041
|
+
```
|
1042
|
+
|
1043
|
+
(This message is not public interface and may change, you cannot rely on that exact wording.)
|
1044
|
+
|
1045
|
+
Even if there's only one `foo.rb`, if the constant `Foo` is already defined when Zeitwerk finds `foo.rb`, then the file is ignored too. This could happen if `Foo` was defined by a dependency, for example. If logging is enabled, you'll see something like
|
1046
|
+
|
1047
|
+
```
|
1048
|
+
file #{file} is ignored because #{constant_path} is already defined
|
1049
|
+
```
|
1050
|
+
|
1051
|
+
(This message is not public interface and may change, you cannot rely on that exact wording.)
|
1052
|
+
|
1053
|
+
Shadowing only applies to Ruby files, namespace definition can be spread over multiple directories. And you can also reopen third-party namespaces if done [orderly](#reopening-third-party-namespaces).
|
1054
|
+
|
908
1055
|
<a id="markdown-edge-cases" name="edge-cases"></a>
|
909
1056
|
### Edge cases
|
910
1057
|
|
911
|
-
|
1058
|
+
[Explicit namespaces](#explicit-namespaces) like `Trip` here:
|
912
1059
|
|
913
1060
|
```ruby
|
914
1061
|
# trip.rb
|
@@ -922,7 +1069,7 @@ module Trip::Geolocation
|
|
922
1069
|
end
|
923
1070
|
```
|
924
1071
|
|
925
|
-
|
1072
|
+
have to be defined with the `class`/`module` keywords, as in the example above.
|
926
1073
|
|
927
1074
|
For technical reasons, raw constant assignment is not supported:
|
928
1075
|
|
@@ -984,7 +1131,7 @@ require "active_job"
|
|
984
1131
|
require "active_job/queue_adapters"
|
985
1132
|
|
986
1133
|
require "zeitwerk"
|
987
|
-
# By
|
1134
|
+
# By passing the flag, we acknowledge the extra directory lib/active_job
|
988
1135
|
# has to be managed by the loader and no warning has to be issued for it.
|
989
1136
|
loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
|
990
1137
|
loader.setup
|
data/lib/zeitwerk/gem_loader.rb
CHANGED
@@ -43,7 +43,7 @@ module Zeitwerk
|
|
43
43
|
next if abspath == expected_namespace_dir
|
44
44
|
|
45
45
|
basename_without_ext = basename.delete_suffix(".rb")
|
46
|
-
cname = inflector.camelize(basename_without_ext, abspath)
|
46
|
+
cname = inflector.camelize(basename_without_ext, abspath).to_sym
|
47
47
|
ftype = dir?(abspath) ? "directory" : "file"
|
48
48
|
|
49
49
|
warn(<<~EOS)
|
data/lib/zeitwerk/kernel.rb
CHANGED
@@ -19,6 +19,9 @@ module Kernel
|
|
19
19
|
# included in Object, and changes in ancestors don't get propagated into
|
20
20
|
# already existing ancestor chains on Ruby < 3.0.
|
21
21
|
alias_method :zeitwerk_original_require, :require
|
22
|
+
class << self
|
23
|
+
alias_method :zeitwerk_original_require, :require
|
24
|
+
end
|
22
25
|
|
23
26
|
# @sig (String) -> true | false
|
24
27
|
def require(path)
|
@@ -42,7 +42,7 @@ module Zeitwerk::Loader::Callbacks
|
|
42
42
|
# Without the mutex and subsequent delete call, t2 would reset the module.
|
43
43
|
# That not only would reassign the constant (undesirable per se) but, worse,
|
44
44
|
# the module object created by t2 wouldn't have any of the autoloads for its
|
45
|
-
# children, since t1 would have correctly deleted its
|
45
|
+
# children, since t1 would have correctly deleted its namespace_dirs entry.
|
46
46
|
mutex2.synchronize do
|
47
47
|
if cref = autoloads.delete(dir)
|
48
48
|
autovivified_module = cref[0].const_set(cref[1], Module.new)
|
@@ -71,9 +71,9 @@ module Zeitwerk::Loader::Callbacks
|
|
71
71
|
# @private
|
72
72
|
# @sig (Module) -> void
|
73
73
|
def on_namespace_loaded(namespace)
|
74
|
-
if
|
75
|
-
|
76
|
-
set_autoloads_in_dir(
|
74
|
+
if dirs = namespace_dirs.delete(real_mod_name(namespace))
|
75
|
+
dirs.each do |dir|
|
76
|
+
set_autoloads_in_dir(dir, namespace)
|
77
77
|
end
|
78
78
|
end
|
79
79
|
end
|
@@ -4,13 +4,12 @@ require "set"
|
|
4
4
|
require "securerandom"
|
5
5
|
|
6
6
|
module Zeitwerk::Loader::Config
|
7
|
-
# Absolute paths of the root directories. Stored in a hash to preserve
|
8
|
-
#
|
9
|
-
#
|
7
|
+
# Absolute paths of the root directories. Stored in a hash to preserve order,
|
8
|
+
# easily handle duplicates, have a fast lookup needed for detecting nested
|
9
|
+
# paths, and store namespaces as values.
|
10
10
|
#
|
11
|
-
# "/Users/fxn/blog/app/assets" => Object,
|
12
11
|
# "/Users/fxn/blog/app/channels" => Object,
|
13
|
-
# "/Users/fxn/blog/adapters"
|
12
|
+
# "/Users/fxn/blog/app/adapters" => ActiveJob::QueueAdapters,
|
14
13
|
# ...
|
15
14
|
#
|
16
15
|
# This is a private collection maintained by the loader. The public
|
@@ -273,12 +272,21 @@ module Zeitwerk::Loader::Config
|
|
273
272
|
@logger = ->(msg) { puts msg }
|
274
273
|
end
|
275
274
|
|
275
|
+
# Returns true if the argument has been configured to be ignored, or is a
|
276
|
+
# descendant of an ignored directory.
|
277
|
+
#
|
276
278
|
# @private
|
277
279
|
# @sig (String) -> bool
|
278
280
|
def ignores?(abspath)
|
279
|
-
|
280
|
-
|
281
|
+
# Common use case.
|
282
|
+
return false if ignored_paths.empty?
|
283
|
+
|
284
|
+
walk_up(abspath) do |abspath|
|
285
|
+
return true if ignored_paths.member?(abspath)
|
286
|
+
return false if root_dirs.key?(abspath)
|
281
287
|
end
|
288
|
+
|
289
|
+
false
|
282
290
|
end
|
283
291
|
|
284
292
|
private
|
@@ -297,7 +305,15 @@ module Zeitwerk::Loader::Config
|
|
297
305
|
|
298
306
|
# @sig (String) -> bool
|
299
307
|
def excluded_from_eager_load?(abspath)
|
300
|
-
|
308
|
+
# Optimize this common use case.
|
309
|
+
return false if eager_load_exclusions.empty?
|
310
|
+
|
311
|
+
walk_up(abspath) do |abspath|
|
312
|
+
return true if eager_load_exclusions.member?(abspath)
|
313
|
+
return false if root_dirs.key?(abspath)
|
314
|
+
end
|
315
|
+
|
316
|
+
false
|
301
317
|
end
|
302
318
|
|
303
319
|
# @sig (String) -> bool
|
@@ -0,0 +1,222 @@
|
|
1
|
+
module Zeitwerk::Loader::EagerLoad
|
2
|
+
# Eager loads all files in the root directories, recursively. Files do not
|
3
|
+
# need to be in `$LOAD_PATH`, absolute file names are used. Ignored and
|
4
|
+
# shadowed files are not eager loaded. You can opt-out specifically in
|
5
|
+
# specific files and directories with `do_not_eager_load`, and that can be
|
6
|
+
# overridden passing `force: true`.
|
7
|
+
#
|
8
|
+
# @sig (true | false) -> void
|
9
|
+
def eager_load(force: false)
|
10
|
+
mutex.synchronize do
|
11
|
+
break if @eager_loaded
|
12
|
+
|
13
|
+
log("eager load start") if logger
|
14
|
+
|
15
|
+
actual_root_dirs.each do |root_dir, namespace|
|
16
|
+
actual_eager_load_dir(root_dir, namespace, force: force)
|
17
|
+
end
|
18
|
+
|
19
|
+
autoloaded_dirs.each do |autoloaded_dir|
|
20
|
+
Zeitwerk::Registry.unregister_autoload(autoloaded_dir)
|
21
|
+
end
|
22
|
+
autoloaded_dirs.clear
|
23
|
+
|
24
|
+
@eager_loaded = true
|
25
|
+
|
26
|
+
log("eager load end") if logger
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# @sig (String | Pathname) -> void
|
31
|
+
def eager_load_dir(path)
|
32
|
+
abspath = File.expand_path(path)
|
33
|
+
|
34
|
+
raise Zeitwerk::Error.new("#{abspath} is not a directory") unless dir?(abspath)
|
35
|
+
|
36
|
+
cnames = []
|
37
|
+
|
38
|
+
root_namespace = nil
|
39
|
+
walk_up(abspath) do |dir|
|
40
|
+
return if ignored_paths.member?(dir)
|
41
|
+
return if eager_load_exclusions.member?(dir)
|
42
|
+
|
43
|
+
break if root_namespace = root_dirs[dir]
|
44
|
+
|
45
|
+
unless collapse?(dir)
|
46
|
+
basename = File.basename(dir)
|
47
|
+
cnames << inflector.camelize(basename, dir).to_sym
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
raise Zeitwerk::Error.new("I do not manage #{abspath}") unless root_namespace
|
52
|
+
|
53
|
+
return if @eager_loaded
|
54
|
+
|
55
|
+
namespace = root_namespace
|
56
|
+
cnames.reverse_each do |cname|
|
57
|
+
# Can happen if there are no Ruby files. This is not an error condition,
|
58
|
+
# the directory is actually managed. Could have Ruby files later.
|
59
|
+
return unless cdef?(namespace, cname)
|
60
|
+
namespace = cget(namespace, cname)
|
61
|
+
end
|
62
|
+
|
63
|
+
# A shortcircuiting test depends on the invocation of this method. Please
|
64
|
+
# keep them in sync if refactored.
|
65
|
+
actual_eager_load_dir(abspath, namespace)
|
66
|
+
end
|
67
|
+
|
68
|
+
# @sig (Module) -> void
|
69
|
+
def eager_load_namespace(mod)
|
70
|
+
unless mod.is_a?(Module)
|
71
|
+
raise Zeitwerk::Error, "#{mod.inspect} is not a class or module object"
|
72
|
+
end
|
73
|
+
|
74
|
+
return if @eager_loaded
|
75
|
+
|
76
|
+
actual_root_dirs.each do |root_dir, root_namespace|
|
77
|
+
if mod.equal?(Object)
|
78
|
+
# A shortcircuiting test depends on the invocation of this method.
|
79
|
+
# Please keep them in sync if refactored.
|
80
|
+
actual_eager_load_dir(root_dir, root_namespace)
|
81
|
+
elsif root_namespace.equal?(Object)
|
82
|
+
eager_load_child_namespace(mod, root_dir, root_namespace)
|
83
|
+
else
|
84
|
+
mod_name = real_mod_name(mod)
|
85
|
+
root_namespace_name = real_mod_name(root_namespace)
|
86
|
+
|
87
|
+
if root_namespace_name.start_with?(mod_name + "::")
|
88
|
+
actual_eager_load_dir(root_dir, root_namespace)
|
89
|
+
elsif mod_name == root_namespace_name
|
90
|
+
actual_eager_load_dir(root_dir, root_namespace)
|
91
|
+
elsif mod_name.start_with?(root_namespace_name + "::")
|
92
|
+
eager_load_child_namespace(mod, root_dir, root_namespace)
|
93
|
+
else
|
94
|
+
# Unrelated constant hierarchies, do nothing.
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Loads the given Ruby file.
|
101
|
+
#
|
102
|
+
# Raises if the argument is ignored, shadowed, or not managed by the receiver.
|
103
|
+
#
|
104
|
+
# The method is implemented as `constantize` for files, in a sense, to be able
|
105
|
+
# to descend orderly and make sure the file is loadable.
|
106
|
+
#
|
107
|
+
# @sig (String | Pathname) -> void
|
108
|
+
def load_file(path)
|
109
|
+
abspath = File.expand_path(path)
|
110
|
+
|
111
|
+
raise Zeitwerk::Error.new("#{abspath} does not exist") unless File.exist?(abspath)
|
112
|
+
raise Zeitwerk::Error.new("#{abspath} is not a Ruby file") if dir?(abspath) || !ruby?(abspath)
|
113
|
+
raise Zeitwerk::Error.new("#{abspath} is ignored") if ignored_paths.member?(abspath)
|
114
|
+
|
115
|
+
basename = File.basename(abspath, ".rb")
|
116
|
+
base_cname = inflector.camelize(basename, abspath).to_sym
|
117
|
+
|
118
|
+
root_namespace = nil
|
119
|
+
cnames = []
|
120
|
+
|
121
|
+
walk_up(File.dirname(abspath)) do |dir|
|
122
|
+
raise Zeitwerk::Error.new("#{abspath} is ignored") if ignored_paths.member?(dir)
|
123
|
+
|
124
|
+
break if root_namespace = root_dirs[dir]
|
125
|
+
|
126
|
+
unless collapse?(dir)
|
127
|
+
basename = File.basename(dir)
|
128
|
+
cnames << inflector.camelize(basename, dir).to_sym
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
raise Zeitwerk::Error.new("I do not manage #{abspath}") unless root_namespace
|
133
|
+
|
134
|
+
namespace = root_namespace
|
135
|
+
cnames.reverse_each do |cname|
|
136
|
+
namespace = cget(namespace, cname)
|
137
|
+
end
|
138
|
+
|
139
|
+
raise Zeitwerk::Error.new("#{abspath} is shadowed") if shadowed_file?(abspath)
|
140
|
+
|
141
|
+
cget(namespace, base_cname)
|
142
|
+
end
|
143
|
+
|
144
|
+
# The caller is responsible for making sure `namespace` is the namespace that
|
145
|
+
# corresponds to `dir`.
|
146
|
+
#
|
147
|
+
# @sig (String, Module, Boolean) -> void
|
148
|
+
private def actual_eager_load_dir(dir, namespace, force: false)
|
149
|
+
honour_exclusions = !force
|
150
|
+
return if honour_exclusions && excluded_from_eager_load?(dir)
|
151
|
+
|
152
|
+
log("eager load directory #{dir} start") if logger
|
153
|
+
|
154
|
+
queue = [[dir, namespace]]
|
155
|
+
while to_eager_load = queue.shift
|
156
|
+
dir, namespace = to_eager_load
|
157
|
+
|
158
|
+
ls(dir) do |basename, abspath|
|
159
|
+
next if honour_exclusions && eager_load_exclusions.member?(abspath)
|
160
|
+
|
161
|
+
if ruby?(abspath)
|
162
|
+
if (cref = autoloads[abspath]) && !shadowed_file?(abspath)
|
163
|
+
cget(*cref)
|
164
|
+
end
|
165
|
+
else
|
166
|
+
if collapse?(abspath)
|
167
|
+
queue << [abspath, namespace]
|
168
|
+
else
|
169
|
+
cname = inflector.camelize(basename, abspath).to_sym
|
170
|
+
queue << [abspath, cget(namespace, cname)]
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
log("eager load directory #{dir} end") if logger
|
177
|
+
end
|
178
|
+
|
179
|
+
# In order to invoke this method, the caller has to ensure `child` is a
|
180
|
+
# strict namespace descendendant of `root_namespace`.
|
181
|
+
#
|
182
|
+
# @sig (Module, String, Module, Boolean) -> void
|
183
|
+
private def eager_load_child_namespace(child, root_dir, root_namespace)
|
184
|
+
suffix = real_mod_name(child)
|
185
|
+
unless root_namespace.equal?(Object)
|
186
|
+
suffix = suffix.delete_prefix(real_mod_name(root_namespace) + "::")
|
187
|
+
end
|
188
|
+
|
189
|
+
# These directories are at the same namespace level, there may be more if
|
190
|
+
# we find collapsed ones. As we scan, we look for matches for the first
|
191
|
+
# segment, and store them in `next_dirs`. If there are any, we look for
|
192
|
+
# the next segments in those matches. Repeat.
|
193
|
+
#
|
194
|
+
# If we exhaust the search locating directories that match all segments,
|
195
|
+
# we just need to eager load those ones.
|
196
|
+
dirs = [root_dir]
|
197
|
+
next_dirs = []
|
198
|
+
|
199
|
+
suffix.split("::").each do |segment|
|
200
|
+
while dir = dirs.shift
|
201
|
+
ls(dir) do |basename, abspath|
|
202
|
+
next unless dir?(abspath)
|
203
|
+
|
204
|
+
if collapse?(abspath)
|
205
|
+
current_dirs << abspath
|
206
|
+
elsif segment == inflector.camelize(basename, abspath)
|
207
|
+
next_dirs << abspath
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
return if next_dirs.empty?
|
213
|
+
|
214
|
+
dirs.replace(next_dirs)
|
215
|
+
next_dirs.clear
|
216
|
+
end
|
217
|
+
|
218
|
+
dirs.each do |dir|
|
219
|
+
actual_eager_load_dir(dir, child)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
@@ -31,7 +31,8 @@ module Zeitwerk::Loader::Helpers
|
|
31
31
|
next if ignored_paths.member?(abspath)
|
32
32
|
|
33
33
|
if dir?(abspath)
|
34
|
-
next
|
34
|
+
next if root_dirs.key?(abspath)
|
35
|
+
next if !has_at_least_one_ruby_file?(abspath)
|
35
36
|
else
|
36
37
|
next unless ruby?(abspath)
|
37
38
|
end
|
@@ -74,6 +75,15 @@ module Zeitwerk::Loader::Helpers
|
|
74
75
|
basename.start_with?(".")
|
75
76
|
end
|
76
77
|
|
78
|
+
# @sig (String) { (String) -> void } -> void
|
79
|
+
def walk_up(abspath)
|
80
|
+
loop do
|
81
|
+
yield abspath
|
82
|
+
abspath, basename = File.split(abspath)
|
83
|
+
break if basename == "/"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
77
87
|
# --- Constants ---------------------------------------------------------------------------------
|
78
88
|
|
79
89
|
# The autoload? predicate takes into account the ancestor chain of the
|
data/lib/zeitwerk/loader.rb
CHANGED
@@ -7,11 +7,13 @@ module Zeitwerk
|
|
7
7
|
require_relative "loader/helpers"
|
8
8
|
require_relative "loader/callbacks"
|
9
9
|
require_relative "loader/config"
|
10
|
+
require_relative "loader/eager_load"
|
10
11
|
|
11
12
|
include RealModName
|
12
13
|
include Callbacks
|
13
14
|
include Helpers
|
14
15
|
include Config
|
16
|
+
include EagerLoad
|
15
17
|
|
16
18
|
MUTEX = Mutex.new
|
17
19
|
private_constant :MUTEX
|
@@ -54,7 +56,7 @@ module Zeitwerk
|
|
54
56
|
# @sig Hash[String, [String, [Module, Symbol]]]
|
55
57
|
attr_reader :to_unload
|
56
58
|
|
57
|
-
# Maps constant paths
|
59
|
+
# Maps namespace constant paths to their respective directories.
|
58
60
|
#
|
59
61
|
# For example, given this mapping:
|
60
62
|
#
|
@@ -64,13 +66,24 @@ module Zeitwerk
|
|
64
66
|
# ...
|
65
67
|
# ]
|
66
68
|
#
|
67
|
-
# when `Admin` gets defined we know that it plays the role of a namespace
|
68
|
-
# that its children are spread over those directories. We'll visit them
|
69
|
-
# up the corresponding autoloads.
|
69
|
+
# when `Admin` gets defined we know that it plays the role of a namespace
|
70
|
+
# and that its children are spread over those directories. We'll visit them
|
71
|
+
# to set up the corresponding autoloads.
|
70
72
|
#
|
71
73
|
# @private
|
72
74
|
# @sig Hash[String, Array[String]]
|
73
|
-
attr_reader :
|
75
|
+
attr_reader :namespace_dirs
|
76
|
+
|
77
|
+
# A shadowed file is a file managed by this loader that is ignored when
|
78
|
+
# setting autoloads because its matching constant is already taken.
|
79
|
+
#
|
80
|
+
# This private set is populated as we descend. For example, if the loader
|
81
|
+
# has only scanned the top-level, `shadowed_files` does not have shadowed
|
82
|
+
# files that may exist deep in the project tree yet.
|
83
|
+
#
|
84
|
+
# @private
|
85
|
+
# @sig Set[String]
|
86
|
+
attr_reader :shadowed_files
|
74
87
|
|
75
88
|
# @private
|
76
89
|
# @sig Mutex
|
@@ -86,7 +99,8 @@ module Zeitwerk
|
|
86
99
|
@autoloads = {}
|
87
100
|
@autoloaded_dirs = []
|
88
101
|
@to_unload = {}
|
89
|
-
@
|
102
|
+
@namespace_dirs = Hash.new { |h, cpath| h[cpath] = [] }
|
103
|
+
@shadowed_files = Set.new
|
90
104
|
@mutex = Mutex.new
|
91
105
|
@mutex2 = Mutex.new
|
92
106
|
@setup = false
|
@@ -181,7 +195,8 @@ module Zeitwerk
|
|
181
195
|
autoloads.clear
|
182
196
|
autoloaded_dirs.clear
|
183
197
|
to_unload.clear
|
184
|
-
|
198
|
+
namespace_dirs.clear
|
199
|
+
shadowed_files.clear
|
185
200
|
|
186
201
|
Registry.on_unload(self)
|
187
202
|
ExplicitNamespace.unregister_loader(self)
|
@@ -208,58 +223,6 @@ module Zeitwerk
|
|
208
223
|
setup
|
209
224
|
end
|
210
225
|
|
211
|
-
# Eager loads all files in the root directories, recursively. Files do not
|
212
|
-
# need to be in `$LOAD_PATH`, absolute file names are used. Ignored files
|
213
|
-
# are not eager loaded. You can opt-out specifically in specific files and
|
214
|
-
# directories with `do_not_eager_load`, and that can be overridden passing
|
215
|
-
# `force: true`.
|
216
|
-
#
|
217
|
-
# @sig (true | false) -> void
|
218
|
-
def eager_load(force: false)
|
219
|
-
mutex.synchronize do
|
220
|
-
break if @eager_loaded
|
221
|
-
|
222
|
-
log("eager load start") if logger
|
223
|
-
|
224
|
-
honour_exclusions = !force
|
225
|
-
|
226
|
-
queue = []
|
227
|
-
actual_root_dirs.each do |root_dir, namespace|
|
228
|
-
queue << [namespace, root_dir] unless honour_exclusions && excluded_from_eager_load?(root_dir)
|
229
|
-
end
|
230
|
-
|
231
|
-
while to_eager_load = queue.shift
|
232
|
-
namespace, dir = to_eager_load
|
233
|
-
|
234
|
-
ls(dir) do |basename, abspath|
|
235
|
-
next if honour_exclusions && excluded_from_eager_load?(abspath)
|
236
|
-
|
237
|
-
if ruby?(abspath)
|
238
|
-
if cref = autoloads[abspath]
|
239
|
-
cget(*cref)
|
240
|
-
end
|
241
|
-
elsif !root_dirs.key?(abspath)
|
242
|
-
if collapse?(abspath)
|
243
|
-
queue << [namespace, abspath]
|
244
|
-
else
|
245
|
-
cname = inflector.camelize(basename, abspath)
|
246
|
-
queue << [cget(namespace, cname), abspath]
|
247
|
-
end
|
248
|
-
end
|
249
|
-
end
|
250
|
-
end
|
251
|
-
|
252
|
-
autoloaded_dirs.each do |autoloaded_dir|
|
253
|
-
Registry.unregister_autoload(autoloaded_dir)
|
254
|
-
end
|
255
|
-
autoloaded_dirs.clear
|
256
|
-
|
257
|
-
@eager_loaded = true
|
258
|
-
|
259
|
-
log("eager load end") if logger
|
260
|
-
end
|
261
|
-
end
|
262
|
-
|
263
226
|
# Says if the given constant path would be unloaded on reload. This
|
264
227
|
# predicate returns `false` if reloading is disabled.
|
265
228
|
#
|
@@ -285,6 +248,15 @@ module Zeitwerk
|
|
285
248
|
ExplicitNamespace.unregister_loader(self)
|
286
249
|
end
|
287
250
|
|
251
|
+
# The return value of this predicate is only meaningful if the loader has
|
252
|
+
# scanned the file. This is the case in the spots where we use it.
|
253
|
+
#
|
254
|
+
# @private
|
255
|
+
# @sig (String) -> Boolean
|
256
|
+
def shadowed_file?(file)
|
257
|
+
shadowed_files.member?(file)
|
258
|
+
end
|
259
|
+
|
288
260
|
# --- Class methods ---------------------------------------------------------------------------
|
289
261
|
|
290
262
|
class << self
|
@@ -318,6 +290,15 @@ module Zeitwerk
|
|
318
290
|
Registry.loaders.each(&:eager_load)
|
319
291
|
end
|
320
292
|
|
293
|
+
# Broadcasts `eager_load_namespace` to all loaders.
|
294
|
+
#
|
295
|
+
# @sig (Module) -> void
|
296
|
+
def eager_load_namespace(mod)
|
297
|
+
Registry.loaders.each do |loader|
|
298
|
+
loader.eager_load_namespace(mod)
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
321
302
|
# Returns an array with the absolute paths of the root directories of all
|
322
303
|
# registered loaders. This is a read-only collection.
|
323
304
|
#
|
@@ -338,19 +319,11 @@ module Zeitwerk
|
|
338
319
|
cname = inflector.camelize(basename, abspath).to_sym
|
339
320
|
autoload_file(parent, cname, abspath)
|
340
321
|
else
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
# To resolve the ambiguity file name -> constant path this introduces,
|
345
|
-
# the `app/models/concerns` directory is totally ignored as a namespace,
|
346
|
-
# it counts only as root. The guard checks that.
|
347
|
-
unless root_dir?(abspath)
|
322
|
+
if collapse?(abspath)
|
323
|
+
set_autoloads_in_dir(abspath, parent)
|
324
|
+
else
|
348
325
|
cname = inflector.camelize(basename, abspath).to_sym
|
349
|
-
|
350
|
-
set_autoloads_in_dir(abspath, parent)
|
351
|
-
else
|
352
|
-
autoload_subdir(parent, cname, abspath)
|
353
|
-
end
|
326
|
+
autoload_subdir(parent, cname, abspath)
|
354
327
|
end
|
355
328
|
end
|
356
329
|
rescue ::NameError => error
|
@@ -380,10 +353,10 @@ module Zeitwerk
|
|
380
353
|
# We do not need to issue another autoload, the existing one is enough
|
381
354
|
# no matter if it is for a file or a directory. Just remember the
|
382
355
|
# subdirectory has to be visited if the namespace is used.
|
383
|
-
|
356
|
+
namespace_dirs[cpath] << subdir
|
384
357
|
elsif !cdef?(parent, cname)
|
385
358
|
# First time we find this namespace, set an autoload for it.
|
386
|
-
|
359
|
+
namespace_dirs[cpath(parent, cname)] << subdir
|
387
360
|
set_autoload(parent, cname, subdir)
|
388
361
|
else
|
389
362
|
# For whatever reason the constant that corresponds to this namespace has
|
@@ -398,6 +371,7 @@ module Zeitwerk
|
|
398
371
|
if autoload_path = strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
|
399
372
|
# First autoload for a Ruby file wins, just ignore subsequent ones.
|
400
373
|
if ruby?(autoload_path)
|
374
|
+
shadowed_files << file
|
401
375
|
log("file #{file} is ignored because #{autoload_path} has precedence") if logger
|
402
376
|
else
|
403
377
|
promote_namespace_from_implicit_to_explicit(
|
@@ -408,6 +382,7 @@ module Zeitwerk
|
|
408
382
|
)
|
409
383
|
end
|
410
384
|
elsif cdef?(parent, cname)
|
385
|
+
shadowed_files << file
|
411
386
|
log("file #{file} is ignored because #{cpath(parent, cname)} is already defined") if logger
|
412
387
|
else
|
413
388
|
set_autoload(parent, cname, file)
|
@@ -470,15 +445,15 @@ module Zeitwerk
|
|
470
445
|
next if loader == self
|
471
446
|
next if loader.ignores?(dir)
|
472
447
|
|
473
|
-
|
448
|
+
suffixed_dir = dir + "/"
|
474
449
|
loader.root_dirs.each do |root_dir, _namespace|
|
475
450
|
next if ignores?(root_dir)
|
476
451
|
|
477
452
|
root_dir = root_dir + "/"
|
478
|
-
if
|
453
|
+
if suffixed_dir.start_with?(root_dir) || root_dir.start_with?(suffixed_dir)
|
479
454
|
require "pp" # Needed for pretty_inspect, even in Ruby 2.5.
|
480
455
|
raise Error,
|
481
|
-
"loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir
|
456
|
+
"loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir}," \
|
482
457
|
" which is already managed by\n\n#{loader.pretty_inspect}\n"
|
483
458
|
EOS
|
484
459
|
end
|
data/lib/zeitwerk/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: zeitwerk
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.6.
|
4
|
+
version: 2.6.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Xavier Noria
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-10-31 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: |2
|
14
14
|
Zeitwerk implements constant autoloading with Ruby semantics. Each gem
|
@@ -32,6 +32,7 @@ files:
|
|
32
32
|
- lib/zeitwerk/loader.rb
|
33
33
|
- lib/zeitwerk/loader/callbacks.rb
|
34
34
|
- lib/zeitwerk/loader/config.rb
|
35
|
+
- lib/zeitwerk/loader/eager_load.rb
|
35
36
|
- lib/zeitwerk/loader/helpers.rb
|
36
37
|
- lib/zeitwerk/real_mod_name.rb
|
37
38
|
- lib/zeitwerk/registry.rb
|