zeitwerk 2.6.1 → 2.6.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +166 -9
- data/lib/zeitwerk/error.rb +6 -0
- data/lib/zeitwerk/explicit_namespace.rb +8 -11
- data/lib/zeitwerk/gem_loader.rb +1 -1
- data/lib/zeitwerk/internal.rb +12 -0
- data/lib/zeitwerk/kernel.rb +3 -0
- data/lib/zeitwerk/loader/callbacks.rb +4 -4
- data/lib/zeitwerk/loader/config.rb +71 -45
- data/lib/zeitwerk/loader/eager_load.rb +228 -0
- data/lib/zeitwerk/loader/helpers.rb +24 -16
- data/lib/zeitwerk/loader.rb +78 -87
- data/lib/zeitwerk/version.rb +1 -1
- data/lib/zeitwerk.rb +1 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 220865ab50f0336d05c8b907a08a94a8bf92843ba688c1a9686163a190b0754e
|
4
|
+
data.tar.gz: a9d62662351c60a3b264a68726a84c41fc4b6f92eaae855fb4887e3eb4cf14a1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b802eaabb27e6268eafa8d35d4402819551203c5fd2a1692d9a9bae65075bf668f5aecfaf3fa6c763987796b60fd5bde4704aebda4a8bfa1c74a526c27264ab5
|
7
|
+
data.tar.gz: c4a53a60b49e5cb83aee148271b26fee8de15fd072327e52363438b0531d9953961305375ec727cc3a0ff2c021254eb081056350165922bd9f6f8b1e1b3e96f3
|
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)
|
@@ -206,7 +213,7 @@ serializers/user_serializer.rb -> UserSerializer
|
|
206
213
|
<a id="markdown-custom-root-namespaces" name="custom-root-namespaces"></a>
|
207
214
|
#### Custom root namespaces
|
208
215
|
|
209
|
-
While `Object` is by far the most common root namespace, you can associate a different one to a particular root directory. The method `push_dir` accepts a class or module object in the optional `namespace` keyword argument.
|
216
|
+
While `Object` is by far the most common root namespace, you can associate a different one to a particular root directory. The method `push_dir` accepts a non-anonymous class or module object in the optional `namespace` keyword argument.
|
210
217
|
|
211
218
|
For example, given:
|
212
219
|
|
@@ -442,6 +449,8 @@ In gems, the method needs to be invoked after the main namespace has been define
|
|
442
449
|
|
443
450
|
Eager loading is synchronized and idempotent.
|
444
451
|
|
452
|
+
Attempting to eager load without previously calling `setup` raises `Zeitwerk::SetupRequired`.
|
453
|
+
|
445
454
|
<a id="markdown-eager-load-exclusions" name="eager-load-exclusions"></a>
|
446
455
|
#### Eager load exclusions
|
447
456
|
|
@@ -464,6 +473,81 @@ Which may be handy if the project eager loads in the test suite to [ensure proje
|
|
464
473
|
|
465
474
|
The `force` flag does not affect ignored files and directories, those are still ignored.
|
466
475
|
|
476
|
+
<a id="markdown-eager-load-directories" name="eager-load-directories"></a>
|
477
|
+
#### Eager load directories
|
478
|
+
|
479
|
+
The method `Zeitwerk::Loader#eager_load_dir` eager loads a given directory, recursively:
|
480
|
+
|
481
|
+
```ruby
|
482
|
+
loader.eager_load_dir("#{__dir__}/custom_web_app/routes")
|
483
|
+
```
|
484
|
+
|
485
|
+
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.
|
486
|
+
|
487
|
+
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`.
|
488
|
+
|
489
|
+
[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.
|
490
|
+
|
491
|
+
`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.
|
492
|
+
|
493
|
+
The method checks if a regular eager load was already executed, in which case it returns fast.
|
494
|
+
|
495
|
+
Nested root directories which are descendants of the argument are skipped. Those subtrees are considered to be conceptually apart.
|
496
|
+
|
497
|
+
Attempting to eager load a directory without previously calling `setup` raises `Zeitwerk::SetupRequired`.
|
498
|
+
|
499
|
+
<a id="markdown-eager-load-namespaces" name="eager-load-namespaces"></a>
|
500
|
+
#### Eager load namespaces
|
501
|
+
|
502
|
+
The method `Zeitwerk::Loader#eager_load_namespace` eager loads a given namespace, recursively:
|
503
|
+
|
504
|
+
```ruby
|
505
|
+
loader.eager_load_namespace(MyApp::Routes)
|
506
|
+
```
|
507
|
+
|
508
|
+
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.
|
509
|
+
|
510
|
+
The argument has to be a class or module object and the method raises `Zeitwerk::Error` otherwise.
|
511
|
+
|
512
|
+
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
|
513
|
+
|
514
|
+
```
|
515
|
+
root_dir1/my_app/routes
|
516
|
+
root_dir2/my_app/routes
|
517
|
+
root_dir3/my_app/routes
|
518
|
+
```
|
519
|
+
|
520
|
+
where `root_directory{1,2,3}` are root directories, eager loading `MyApp::Routes` will eager load the contents of the three corresponding directories.
|
521
|
+
|
522
|
+
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.
|
523
|
+
|
524
|
+
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.
|
525
|
+
|
526
|
+
[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.
|
527
|
+
|
528
|
+
`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.
|
529
|
+
|
530
|
+
The method checks if a regular eager load was already executed, in which case it returns fast.
|
531
|
+
|
532
|
+
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.
|
533
|
+
|
534
|
+
Attempting to eager load a namespace without previously calling `setup` raises `Zeitwerk::SetupRequired`.
|
535
|
+
|
536
|
+
<a id="markdown-eager-load-namespaces-shared-by-several-loaders" name="eager-load-namespaces-shared-by-several-loaders"></a>
|
537
|
+
#### Eager load namespaces shared by several loaders
|
538
|
+
|
539
|
+
The method `Zeitwerk::Loader.eager_load_namespace` broadcasts `eager_load_namespace` to all loaders.
|
540
|
+
|
541
|
+
```ruby
|
542
|
+
Zeitwerk::Loader.eager_load_namespace(MyFramework::Routes)
|
543
|
+
```
|
544
|
+
|
545
|
+
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.
|
546
|
+
|
547
|
+
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.
|
548
|
+
|
549
|
+
This method does not require that all registered loaders have `setup` already invoked, since that is out of your control. If there's any in that state, it is simply skipped.
|
550
|
+
|
467
551
|
<a id="markdown-global-eager-load" name="global-eager-load"></a>
|
468
552
|
#### Global eager load
|
469
553
|
|
@@ -479,9 +563,31 @@ Note that thanks to idempotence `Zeitwerk::Loader.eager_load_all` won't eager lo
|
|
479
563
|
|
480
564
|
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
565
|
|
566
|
+
This method does not require that all registered loaders have `setup` already invoked, since that is out of your control. If there's any in that state, it is simply skipped.
|
567
|
+
|
568
|
+
<a id="markdown-loading-individual-files" name="loading-individual-files"></a>
|
569
|
+
### Loading individual files
|
570
|
+
|
571
|
+
The method `Zeitwerk::Loader#load_file` loads an individual Ruby file:
|
572
|
+
|
573
|
+
```ruby
|
574
|
+
loader.load_file("#{__dir__}/custom_web_app/routes.rb")
|
575
|
+
```
|
576
|
+
|
577
|
+
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.
|
578
|
+
|
579
|
+
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.
|
580
|
+
|
581
|
+
`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.
|
582
|
+
|
583
|
+
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.
|
584
|
+
|
482
585
|
<a id="markdown-reloading" name="reloading"></a>
|
483
586
|
### Reloading
|
484
587
|
|
588
|
+
<a id="markdown-configuration-and-usage" name="configuration-and-usage"></a>
|
589
|
+
#### Configuration and usage
|
590
|
+
|
485
591
|
Zeitwerk is able to reload code, but you need to enable this feature:
|
486
592
|
|
487
593
|
```ruby
|
@@ -495,7 +601,7 @@ loader.reload
|
|
495
601
|
|
496
602
|
There is no way to undo this, either you want to reload or you don't.
|
497
603
|
|
498
|
-
Enabling reloading after setup raises `Zeitwerk::Error`. Attempting to reload without having it enabled raises `Zeitwerk::ReloadingDisabledError`.
|
604
|
+
Enabling reloading after setup raises `Zeitwerk::Error`. Attempting to reload without having it enabled raises `Zeitwerk::ReloadingDisabledError`. Attempting to reload without previously calling `setup` raises `Zeitwerk::SetupRequired`.
|
499
605
|
|
500
606
|
Generally speaking, reloading is useful while developing running services like web applications. Gems that implement regular libraries, so to speak, or services running in testing or production environments, won't normally have a use case for reloading. If reloading is not enabled, Zeitwerk is able to use less memory.
|
501
607
|
|
@@ -503,12 +609,34 @@ Reloading removes the currently loaded classes and modules and resets the loader
|
|
503
609
|
|
504
610
|
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
611
|
|
506
|
-
|
612
|
+
<a id="markdown-thread-safety" name="thread-safety"></a>
|
613
|
+
#### Thread-safety
|
507
614
|
|
508
|
-
|
509
|
-
* You should not autoload while another thread is reloading.
|
615
|
+
In order to reload safely, no other thread can be autoloading or reloading concurrently. Client code is responsible for this coordination.
|
510
616
|
|
511
|
-
|
617
|
+
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:
|
618
|
+
|
619
|
+
```ruby
|
620
|
+
require "concurrent/atomic/read_write_lock"
|
621
|
+
|
622
|
+
MyFramework::RELOAD_RW_LOCK = Concurrent::ReadWriteLock.new
|
623
|
+
```
|
624
|
+
|
625
|
+
You acquire the lock for reading for serving each individual request:
|
626
|
+
|
627
|
+
```ruby
|
628
|
+
MyFramework::RELOAD_RW_LOCK.with_read_lock do
|
629
|
+
serve(request)
|
630
|
+
end
|
631
|
+
```
|
632
|
+
|
633
|
+
Then, when a reload is triggered, just acquire the lock for writing in order to execute the method call safely:
|
634
|
+
|
635
|
+
```ruby
|
636
|
+
MyFramework::RELOAD_RW_LOCK.with_write_lock do
|
637
|
+
loader.reload
|
638
|
+
end
|
639
|
+
```
|
512
640
|
|
513
641
|
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
642
|
|
@@ -905,10 +1033,39 @@ loader.ignore(tests)
|
|
905
1033
|
loader.setup
|
906
1034
|
```
|
907
1035
|
|
1036
|
+
<a id="markdown-shadowed-files" name="shadowed-files"></a>
|
1037
|
+
### Shadowed files
|
1038
|
+
|
1039
|
+
In Ruby, if you have several files called `foo.rb` in different directories of `$LOAD_PATH` and execute
|
1040
|
+
|
1041
|
+
```ruby
|
1042
|
+
require "foo"
|
1043
|
+
```
|
1044
|
+
|
1045
|
+
the first one found gets loaded, and the rest are ignored.
|
1046
|
+
|
1047
|
+
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
|
1048
|
+
|
1049
|
+
```
|
1050
|
+
file #{file} is ignored because #{previous_occurrence} has precedence
|
1051
|
+
```
|
1052
|
+
|
1053
|
+
(This message is not public interface and may change, you cannot rely on that exact wording.)
|
1054
|
+
|
1055
|
+
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
|
1056
|
+
|
1057
|
+
```
|
1058
|
+
file #{file} is ignored because #{constant_path} is already defined
|
1059
|
+
```
|
1060
|
+
|
1061
|
+
(This message is not public interface and may change, you cannot rely on that exact wording.)
|
1062
|
+
|
1063
|
+
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).
|
1064
|
+
|
908
1065
|
<a id="markdown-edge-cases" name="edge-cases"></a>
|
909
1066
|
### Edge cases
|
910
1067
|
|
911
|
-
|
1068
|
+
[Explicit namespaces](#explicit-namespaces) like `Trip` here:
|
912
1069
|
|
913
1070
|
```ruby
|
914
1071
|
# trip.rb
|
@@ -922,7 +1079,7 @@ module Trip::Geolocation
|
|
922
1079
|
end
|
923
1080
|
```
|
924
1081
|
|
925
|
-
|
1082
|
+
have to be defined with the `class`/`module` keywords, as in the example above.
|
926
1083
|
|
927
1084
|
For technical reasons, raw constant assignment is not supported:
|
928
1085
|
|
@@ -984,7 +1141,7 @@ require "active_job"
|
|
984
1141
|
require "active_job/queue_adapters"
|
985
1142
|
|
986
1143
|
require "zeitwerk"
|
987
|
-
# By
|
1144
|
+
# By passing the flag, we acknowledge the extra directory lib/active_job
|
988
1145
|
# has to be managed by the loader and no warning has to be issued for it.
|
989
1146
|
loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
|
990
1147
|
loader.setup
|
data/lib/zeitwerk/error.rb
CHANGED
@@ -11,28 +11,28 @@ module Zeitwerk
|
|
11
11
|
module ExplicitNamespace # :nodoc: all
|
12
12
|
class << self
|
13
13
|
include RealModName
|
14
|
+
extend Internal
|
14
15
|
|
15
16
|
# Maps constant paths that correspond to explicit namespaces according to
|
16
17
|
# the file system, to the loader responsible for them.
|
17
18
|
#
|
18
|
-
# @private
|
19
19
|
# @sig Hash[String, Zeitwerk::Loader]
|
20
20
|
attr_reader :cpaths
|
21
|
+
private :cpaths
|
21
22
|
|
22
|
-
# @private
|
23
23
|
# @sig Mutex
|
24
24
|
attr_reader :mutex
|
25
|
+
private :mutex
|
25
26
|
|
26
|
-
# @private
|
27
27
|
# @sig TracePoint
|
28
28
|
attr_reader :tracer
|
29
|
+
private :tracer
|
29
30
|
|
30
31
|
# Asserts `cpath` corresponds to an explicit namespace for which `loader`
|
31
32
|
# is responsible.
|
32
33
|
#
|
33
|
-
# @private
|
34
34
|
# @sig (String, Zeitwerk::Loader) -> void
|
35
|
-
def register(cpath, loader)
|
35
|
+
internal def register(cpath, loader)
|
36
36
|
mutex.synchronize do
|
37
37
|
cpaths[cpath] = loader
|
38
38
|
# We check enabled? because, looking at the C source code, enabling an
|
@@ -41,24 +41,21 @@ module Zeitwerk
|
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
44
|
-
# @private
|
45
44
|
# @sig (Zeitwerk::Loader) -> void
|
46
|
-
def unregister_loader(loader)
|
45
|
+
internal def unregister_loader(loader)
|
47
46
|
cpaths.delete_if { |_cpath, l| l == loader }
|
48
47
|
disable_tracer_if_unneeded
|
49
48
|
end
|
50
49
|
|
51
|
-
private
|
52
|
-
|
53
50
|
# @sig () -> void
|
54
|
-
def disable_tracer_if_unneeded
|
51
|
+
private def disable_tracer_if_unneeded
|
55
52
|
mutex.synchronize do
|
56
53
|
tracer.disable if cpaths.empty?
|
57
54
|
end
|
58
55
|
end
|
59
56
|
|
60
57
|
# @sig (TracePoint) -> void
|
61
|
-
def tracepoint_class_callback(event)
|
58
|
+
private def tracepoint_class_callback(event)
|
62
59
|
# If the class is a singleton class, we won't do anything with it so we
|
63
60
|
# can bail out immediately. This is several orders of magnitude faster
|
64
61
|
# than accessing its name.
|
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,86 +4,91 @@ require "set"
|
|
4
4
|
require "securerandom"
|
5
5
|
|
6
6
|
module Zeitwerk::Loader::Config
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
extend Zeitwerk::Internal
|
8
|
+
include Zeitwerk::RealModName
|
9
|
+
|
10
|
+
# @sig #camelize
|
11
|
+
attr_accessor :inflector
|
12
|
+
|
13
|
+
# @sig #call | #debug | nil
|
14
|
+
attr_accessor :logger
|
15
|
+
|
16
|
+
# Absolute paths of the root directories, mapped to their respective root namespaces:
|
10
17
|
#
|
11
|
-
# "/Users/fxn/blog/app/assets" => Object,
|
12
18
|
# "/Users/fxn/blog/app/channels" => Object,
|
13
|
-
# "/Users/fxn/blog/adapters"
|
19
|
+
# "/Users/fxn/blog/app/adapters" => ActiveJob::QueueAdapters,
|
14
20
|
# ...
|
15
21
|
#
|
22
|
+
# Stored in a hash to preserve order, easily handle duplicates, and have a
|
23
|
+
# fast lookup by directory.
|
24
|
+
#
|
16
25
|
# This is a private collection maintained by the loader. The public
|
17
26
|
# interface for it is `push_dir` and `dirs`.
|
18
27
|
#
|
19
|
-
# @private
|
20
28
|
# @sig Hash[String, Module]
|
21
|
-
attr_reader :
|
22
|
-
|
23
|
-
# @sig #camelize
|
24
|
-
attr_accessor :inflector
|
29
|
+
attr_reader :roots
|
30
|
+
internal :roots
|
25
31
|
|
26
32
|
# Absolute paths of files, directories, or glob patterns to be totally
|
27
33
|
# ignored.
|
28
34
|
#
|
29
|
-
# @private
|
30
35
|
# @sig Set[String]
|
31
36
|
attr_reader :ignored_glob_patterns
|
37
|
+
private :ignored_glob_patterns
|
32
38
|
|
33
39
|
# The actual collection of absolute file and directory names at the time the
|
34
40
|
# ignored glob patterns were expanded. Computed on setup, and recomputed on
|
35
41
|
# reload.
|
36
42
|
#
|
37
|
-
# @private
|
38
43
|
# @sig Set[String]
|
39
44
|
attr_reader :ignored_paths
|
45
|
+
private :ignored_paths
|
40
46
|
|
41
47
|
# Absolute paths of directories or glob patterns to be collapsed.
|
42
48
|
#
|
43
|
-
# @private
|
44
49
|
# @sig Set[String]
|
45
50
|
attr_reader :collapse_glob_patterns
|
51
|
+
private :collapse_glob_patterns
|
46
52
|
|
47
53
|
# The actual collection of absolute directory names at the time the collapse
|
48
54
|
# glob patterns were expanded. Computed on setup, and recomputed on reload.
|
49
55
|
#
|
50
|
-
# @private
|
51
56
|
# @sig Set[String]
|
52
57
|
attr_reader :collapse_dirs
|
58
|
+
private :collapse_dirs
|
53
59
|
|
54
60
|
# Absolute paths of files or directories not to be eager loaded.
|
55
61
|
#
|
56
|
-
# @private
|
57
62
|
# @sig Set[String]
|
58
63
|
attr_reader :eager_load_exclusions
|
64
|
+
private :eager_load_exclusions
|
59
65
|
|
60
66
|
# User-oriented callbacks to be fired on setup and on reload.
|
61
67
|
#
|
62
|
-
# @private
|
63
68
|
# @sig Array[{ () -> void }]
|
64
69
|
attr_reader :on_setup_callbacks
|
70
|
+
private :on_setup_callbacks
|
65
71
|
|
66
72
|
# User-oriented callbacks to be fired when a constant is loaded.
|
67
73
|
#
|
68
|
-
# @private
|
69
74
|
# @sig Hash[String, Array[{ (Object, String) -> void }]]
|
70
75
|
# Hash[Symbol, Array[{ (String, Object, String) -> void }]]
|
71
76
|
attr_reader :on_load_callbacks
|
77
|
+
private :on_load_callbacks
|
72
78
|
|
73
79
|
# User-oriented callbacks to be fired before constants are removed.
|
74
80
|
#
|
75
|
-
# @private
|
76
81
|
# @sig Hash[String, Array[{ (Object, String) -> void }]]
|
77
82
|
# Hash[Symbol, Array[{ (String, Object, String) -> void }]]
|
78
83
|
attr_reader :on_unload_callbacks
|
79
|
-
|
80
|
-
# @sig #call | #debug | nil
|
81
|
-
attr_accessor :logger
|
84
|
+
private :on_unload_callbacks
|
82
85
|
|
83
86
|
def initialize
|
84
|
-
@initialized_at = Time.now
|
85
|
-
@root_dirs = {}
|
86
87
|
@inflector = Zeitwerk::Inflector.new
|
88
|
+
@logger = self.class.default_logger
|
89
|
+
@tag = SecureRandom.hex(3)
|
90
|
+
@initialized_at = Time.now
|
91
|
+
@roots = {}
|
87
92
|
@ignored_glob_patterns = Set.new
|
88
93
|
@ignored_paths = Set.new
|
89
94
|
@collapse_glob_patterns = Set.new
|
@@ -93,8 +98,6 @@ module Zeitwerk::Loader::Config
|
|
93
98
|
@on_setup_callbacks = []
|
94
99
|
@on_load_callbacks = {}
|
95
100
|
@on_unload_callbacks = {}
|
96
|
-
@logger = self.class.default_logger
|
97
|
-
@tag = SecureRandom.hex(3)
|
98
101
|
end
|
99
102
|
|
100
103
|
# Pushes `path` to the list of root directories.
|
@@ -111,10 +114,14 @@ module Zeitwerk::Loader::Config
|
|
111
114
|
raise Zeitwerk::Error, "#{namespace.inspect} is not a class or module object, should be"
|
112
115
|
end
|
113
116
|
|
117
|
+
unless real_mod_name(namespace)
|
118
|
+
raise Zeitwerk::Error, "root namespaces cannot be anonymous"
|
119
|
+
end
|
120
|
+
|
114
121
|
abspath = File.expand_path(path)
|
115
122
|
if dir?(abspath)
|
116
123
|
raise_if_conflicting_directory(abspath)
|
117
|
-
|
124
|
+
roots[abspath] = namespace
|
118
125
|
else
|
119
126
|
raise Zeitwerk::Error, "the root directory #{abspath} does not exist"
|
120
127
|
end
|
@@ -147,9 +154,9 @@ module Zeitwerk::Loader::Config
|
|
147
154
|
# @sig () -> Array[String] | Hash[String, Module]
|
148
155
|
def dirs(namespaces: false)
|
149
156
|
if namespaces
|
150
|
-
|
157
|
+
roots.clone
|
151
158
|
else
|
152
|
-
|
159
|
+
roots.keys
|
153
160
|
end.freeze
|
154
161
|
end
|
155
162
|
|
@@ -273,57 +280,76 @@ module Zeitwerk::Loader::Config
|
|
273
280
|
@logger = ->(msg) { puts msg }
|
274
281
|
end
|
275
282
|
|
276
|
-
#
|
283
|
+
# Returns true if the argument has been configured to be ignored, or is a
|
284
|
+
# descendant of an ignored directory.
|
285
|
+
#
|
277
286
|
# @sig (String) -> bool
|
278
|
-
def ignores?(abspath)
|
279
|
-
|
280
|
-
|
287
|
+
internal def ignores?(abspath)
|
288
|
+
# Common use case.
|
289
|
+
return false if ignored_paths.empty?
|
290
|
+
|
291
|
+
walk_up(abspath) do |abspath|
|
292
|
+
return true if ignored_path?(abspath)
|
293
|
+
return false if roots.key?(abspath)
|
281
294
|
end
|
295
|
+
|
296
|
+
false
|
282
297
|
end
|
283
298
|
|
284
|
-
|
299
|
+
# @sig (String) -> bool
|
300
|
+
private def ignored_path?(abspath)
|
301
|
+
ignored_paths.member?(abspath)
|
302
|
+
end
|
285
303
|
|
286
304
|
# @sig () -> Array[String]
|
287
|
-
def
|
288
|
-
|
289
|
-
!dir?(root_dir) ||
|
305
|
+
private def actual_roots
|
306
|
+
roots.reject do |root_dir, _root_namespace|
|
307
|
+
!dir?(root_dir) || ignored_path?(root_dir)
|
290
308
|
end
|
291
309
|
end
|
292
310
|
|
293
311
|
# @sig (String) -> bool
|
294
|
-
def root_dir?(dir)
|
295
|
-
|
312
|
+
private def root_dir?(dir)
|
313
|
+
roots.key?(dir)
|
296
314
|
end
|
297
315
|
|
298
316
|
# @sig (String) -> bool
|
299
|
-
def excluded_from_eager_load?(abspath)
|
300
|
-
|
317
|
+
private def excluded_from_eager_load?(abspath)
|
318
|
+
# Optimize this common use case.
|
319
|
+
return false if eager_load_exclusions.empty?
|
320
|
+
|
321
|
+
walk_up(abspath) do |abspath|
|
322
|
+
return true if eager_load_exclusions.member?(abspath)
|
323
|
+
return false if roots.key?(abspath)
|
324
|
+
end
|
325
|
+
|
326
|
+
false
|
301
327
|
end
|
302
328
|
|
303
329
|
# @sig (String) -> bool
|
304
|
-
def collapse?(dir)
|
330
|
+
private def collapse?(dir)
|
305
331
|
collapse_dirs.member?(dir)
|
306
332
|
end
|
307
333
|
|
308
334
|
# @sig (String | Pathname | Array[String | Pathname]) -> Array[String]
|
309
|
-
def expand_paths(paths)
|
335
|
+
private def expand_paths(paths)
|
310
336
|
paths.flatten.map! { |path| File.expand_path(path) }
|
311
337
|
end
|
312
338
|
|
313
339
|
# @sig (Array[String]) -> Array[String]
|
314
|
-
def expand_glob_patterns(glob_patterns)
|
340
|
+
private def expand_glob_patterns(glob_patterns)
|
315
341
|
# Note that Dir.glob works with regular file names just fine. That is,
|
316
342
|
# glob patterns technically need no wildcards.
|
317
343
|
glob_patterns.flat_map { |glob_pattern| Dir.glob(glob_pattern) }
|
318
344
|
end
|
319
345
|
|
320
346
|
# @sig () -> void
|
321
|
-
def recompute_ignored_paths
|
347
|
+
private def recompute_ignored_paths
|
322
348
|
ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns))
|
323
349
|
end
|
324
350
|
|
325
351
|
# @sig () -> void
|
326
|
-
def recompute_collapse_dirs
|
352
|
+
private def recompute_collapse_dirs
|
327
353
|
collapse_dirs.replace(expand_glob_patterns(collapse_glob_patterns))
|
328
354
|
end
|
329
355
|
end
|
@@ -0,0 +1,228 @@
|
|
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
|
+
raise Zeitwerk::SetupRequired unless @setup
|
13
|
+
|
14
|
+
log("eager load start") if logger
|
15
|
+
|
16
|
+
actual_roots.each do |root_dir, root_namespace|
|
17
|
+
actual_eager_load_dir(root_dir, root_namespace, force: force)
|
18
|
+
end
|
19
|
+
|
20
|
+
autoloaded_dirs.each do |autoloaded_dir|
|
21
|
+
Zeitwerk::Registry.unregister_autoload(autoloaded_dir)
|
22
|
+
end
|
23
|
+
autoloaded_dirs.clear
|
24
|
+
|
25
|
+
@eager_loaded = true
|
26
|
+
|
27
|
+
log("eager load end") if logger
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# @sig (String | Pathname) -> void
|
32
|
+
def eager_load_dir(path)
|
33
|
+
raise Zeitwerk::SetupRequired unless @setup
|
34
|
+
|
35
|
+
abspath = File.expand_path(path)
|
36
|
+
|
37
|
+
raise Zeitwerk::Error.new("#{abspath} is not a directory") unless dir?(abspath)
|
38
|
+
|
39
|
+
cnames = []
|
40
|
+
|
41
|
+
root_namespace = nil
|
42
|
+
walk_up(abspath) do |dir|
|
43
|
+
return if ignored_path?(dir)
|
44
|
+
return if eager_load_exclusions.member?(dir)
|
45
|
+
|
46
|
+
break if root_namespace = roots[dir]
|
47
|
+
|
48
|
+
unless collapse?(dir)
|
49
|
+
basename = File.basename(dir)
|
50
|
+
cnames << inflector.camelize(basename, dir).to_sym
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
raise Zeitwerk::Error.new("I do not manage #{abspath}") unless root_namespace
|
55
|
+
|
56
|
+
return if @eager_loaded
|
57
|
+
|
58
|
+
namespace = root_namespace
|
59
|
+
cnames.reverse_each do |cname|
|
60
|
+
# Can happen if there are no Ruby files. This is not an error condition,
|
61
|
+
# the directory is actually managed. Could have Ruby files later.
|
62
|
+
return unless cdef?(namespace, cname)
|
63
|
+
namespace = cget(namespace, cname)
|
64
|
+
end
|
65
|
+
|
66
|
+
# A shortcircuiting test depends on the invocation of this method. Please
|
67
|
+
# keep them in sync if refactored.
|
68
|
+
actual_eager_load_dir(abspath, namespace)
|
69
|
+
end
|
70
|
+
|
71
|
+
# @sig (Module) -> void
|
72
|
+
def eager_load_namespace(mod)
|
73
|
+
raise Zeitwerk::SetupRequired unless @setup
|
74
|
+
|
75
|
+
unless mod.is_a?(Module)
|
76
|
+
raise Zeitwerk::Error, "#{mod.inspect} is not a class or module object"
|
77
|
+
end
|
78
|
+
|
79
|
+
return if @eager_loaded
|
80
|
+
|
81
|
+
mod_name = real_mod_name(mod)
|
82
|
+
return unless mod_name
|
83
|
+
|
84
|
+
actual_roots.each do |root_dir, root_namespace|
|
85
|
+
if mod.equal?(Object)
|
86
|
+
# A shortcircuiting test depends on the invocation of this method.
|
87
|
+
# Please keep them in sync if refactored.
|
88
|
+
actual_eager_load_dir(root_dir, root_namespace)
|
89
|
+
elsif root_namespace.equal?(Object)
|
90
|
+
eager_load_child_namespace(mod, mod_name, root_dir, root_namespace)
|
91
|
+
else
|
92
|
+
root_namespace_name = real_mod_name(root_namespace)
|
93
|
+
if root_namespace_name.start_with?(mod_name + "::")
|
94
|
+
actual_eager_load_dir(root_dir, root_namespace)
|
95
|
+
elsif mod_name == root_namespace_name
|
96
|
+
actual_eager_load_dir(root_dir, root_namespace)
|
97
|
+
elsif mod_name.start_with?(root_namespace_name + "::")
|
98
|
+
eager_load_child_namespace(mod, mod_name, root_dir, root_namespace)
|
99
|
+
else
|
100
|
+
# Unrelated constant hierarchies, do nothing.
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Loads the given Ruby file.
|
107
|
+
#
|
108
|
+
# Raises if the argument is ignored, shadowed, or not managed by the receiver.
|
109
|
+
#
|
110
|
+
# The method is implemented as `constantize` for files, in a sense, to be able
|
111
|
+
# to descend orderly and make sure the file is loadable.
|
112
|
+
#
|
113
|
+
# @sig (String | Pathname) -> void
|
114
|
+
def load_file(path)
|
115
|
+
abspath = File.expand_path(path)
|
116
|
+
|
117
|
+
raise Zeitwerk::Error.new("#{abspath} does not exist") unless File.exist?(abspath)
|
118
|
+
raise Zeitwerk::Error.new("#{abspath} is not a Ruby file") if dir?(abspath) || !ruby?(abspath)
|
119
|
+
raise Zeitwerk::Error.new("#{abspath} is ignored") if ignored_path?(abspath)
|
120
|
+
|
121
|
+
basename = File.basename(abspath, ".rb")
|
122
|
+
base_cname = inflector.camelize(basename, abspath).to_sym
|
123
|
+
|
124
|
+
root_namespace = nil
|
125
|
+
cnames = []
|
126
|
+
|
127
|
+
walk_up(File.dirname(abspath)) do |dir|
|
128
|
+
raise Zeitwerk::Error.new("#{abspath} is ignored") if ignored_path?(dir)
|
129
|
+
|
130
|
+
break if root_namespace = roots[dir]
|
131
|
+
|
132
|
+
unless collapse?(dir)
|
133
|
+
basename = File.basename(dir)
|
134
|
+
cnames << inflector.camelize(basename, dir).to_sym
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
raise Zeitwerk::Error.new("I do not manage #{abspath}") unless root_namespace
|
139
|
+
|
140
|
+
namespace = root_namespace
|
141
|
+
cnames.reverse_each do |cname|
|
142
|
+
namespace = cget(namespace, cname)
|
143
|
+
end
|
144
|
+
|
145
|
+
raise Zeitwerk::Error.new("#{abspath} is shadowed") if shadowed_file?(abspath)
|
146
|
+
|
147
|
+
cget(namespace, base_cname)
|
148
|
+
end
|
149
|
+
|
150
|
+
# The caller is responsible for making sure `namespace` is the namespace that
|
151
|
+
# corresponds to `dir`.
|
152
|
+
#
|
153
|
+
# @sig (String, Module, Boolean) -> void
|
154
|
+
private def actual_eager_load_dir(dir, namespace, force: false)
|
155
|
+
honour_exclusions = !force
|
156
|
+
return if honour_exclusions && excluded_from_eager_load?(dir)
|
157
|
+
|
158
|
+
log("eager load directory #{dir} start") if logger
|
159
|
+
|
160
|
+
queue = [[dir, namespace]]
|
161
|
+
while to_eager_load = queue.shift
|
162
|
+
dir, namespace = to_eager_load
|
163
|
+
|
164
|
+
ls(dir) do |basename, abspath|
|
165
|
+
next if honour_exclusions && eager_load_exclusions.member?(abspath)
|
166
|
+
|
167
|
+
if ruby?(abspath)
|
168
|
+
if (cref = autoloads[abspath]) && !shadowed_file?(abspath)
|
169
|
+
cget(*cref)
|
170
|
+
end
|
171
|
+
else
|
172
|
+
if collapse?(abspath)
|
173
|
+
queue << [abspath, namespace]
|
174
|
+
else
|
175
|
+
cname = inflector.camelize(basename, abspath).to_sym
|
176
|
+
queue << [abspath, cget(namespace, cname)]
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
log("eager load directory #{dir} end") if logger
|
183
|
+
end
|
184
|
+
|
185
|
+
# In order to invoke this method, the caller has to ensure `child` is a
|
186
|
+
# strict namespace descendendant of `root_namespace`.
|
187
|
+
#
|
188
|
+
# @sig (Module, String, Module, Boolean) -> void
|
189
|
+
private def eager_load_child_namespace(child, child_name, root_dir, root_namespace)
|
190
|
+
suffix = child_name
|
191
|
+
unless root_namespace.equal?(Object)
|
192
|
+
suffix = suffix.delete_prefix(real_mod_name(root_namespace) + "::")
|
193
|
+
end
|
194
|
+
|
195
|
+
# These directories are at the same namespace level, there may be more if
|
196
|
+
# we find collapsed ones. As we scan, we look for matches for the first
|
197
|
+
# segment, and store them in `next_dirs`. If there are any, we look for
|
198
|
+
# the next segments in those matches. Repeat.
|
199
|
+
#
|
200
|
+
# If we exhaust the search locating directories that match all segments,
|
201
|
+
# we just need to eager load those ones.
|
202
|
+
dirs = [root_dir]
|
203
|
+
next_dirs = []
|
204
|
+
|
205
|
+
suffix.split("::").each do |segment|
|
206
|
+
while dir = dirs.shift
|
207
|
+
ls(dir) do |basename, abspath|
|
208
|
+
next unless dir?(abspath)
|
209
|
+
|
210
|
+
if collapse?(abspath)
|
211
|
+
dirs << abspath
|
212
|
+
elsif segment == inflector.camelize(basename, abspath)
|
213
|
+
next_dirs << abspath
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
return if next_dirs.empty?
|
219
|
+
|
220
|
+
dirs.replace(next_dirs)
|
221
|
+
next_dirs.clear
|
222
|
+
end
|
223
|
+
|
224
|
+
dirs.each do |dir|
|
225
|
+
actual_eager_load_dir(dir, child)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
@@ -1,12 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Zeitwerk::Loader::Helpers
|
4
|
-
private
|
5
|
-
|
6
4
|
# --- Logging -----------------------------------------------------------------------------------
|
7
5
|
|
8
6
|
# @sig (String) -> void
|
9
|
-
def log(message)
|
7
|
+
private def log(message)
|
10
8
|
method_name = logger.respond_to?(:debug) ? :debug : :call
|
11
9
|
logger.send(method_name, "Zeitwerk@#{tag}: #{message}")
|
12
10
|
end
|
@@ -14,7 +12,7 @@ module Zeitwerk::Loader::Helpers
|
|
14
12
|
# --- Files and directories ---------------------------------------------------------------------
|
15
13
|
|
16
14
|
# @sig (String) { (String, String) -> void } -> void
|
17
|
-
def ls(dir)
|
15
|
+
private def ls(dir)
|
18
16
|
children = Dir.children(dir)
|
19
17
|
|
20
18
|
# The order in which a directory is listed depends on the file system.
|
@@ -28,10 +26,11 @@ module Zeitwerk::Loader::Helpers
|
|
28
26
|
next if hidden?(basename)
|
29
27
|
|
30
28
|
abspath = File.join(dir, basename)
|
31
|
-
next if
|
29
|
+
next if ignored_path?(abspath)
|
32
30
|
|
33
31
|
if dir?(abspath)
|
34
|
-
next
|
32
|
+
next if roots.key?(abspath)
|
33
|
+
next if !has_at_least_one_ruby_file?(abspath)
|
35
34
|
else
|
36
35
|
next unless ruby?(abspath)
|
37
36
|
end
|
@@ -43,7 +42,7 @@ module Zeitwerk::Loader::Helpers
|
|
43
42
|
end
|
44
43
|
|
45
44
|
# @sig (String) -> bool
|
46
|
-
def has_at_least_one_ruby_file?(dir)
|
45
|
+
private def has_at_least_one_ruby_file?(dir)
|
47
46
|
to_visit = [dir]
|
48
47
|
|
49
48
|
while dir = to_visit.shift
|
@@ -60,20 +59,29 @@ module Zeitwerk::Loader::Helpers
|
|
60
59
|
end
|
61
60
|
|
62
61
|
# @sig (String) -> bool
|
63
|
-
def ruby?(path)
|
62
|
+
private def ruby?(path)
|
64
63
|
path.end_with?(".rb")
|
65
64
|
end
|
66
65
|
|
67
66
|
# @sig (String) -> bool
|
68
|
-
def dir?(path)
|
67
|
+
private def dir?(path)
|
69
68
|
File.directory?(path)
|
70
69
|
end
|
71
70
|
|
72
71
|
# @sig (String) -> bool
|
73
|
-
def hidden?(basename)
|
72
|
+
private def hidden?(basename)
|
74
73
|
basename.start_with?(".")
|
75
74
|
end
|
76
75
|
|
76
|
+
# @sig (String) { (String) -> void } -> void
|
77
|
+
private def walk_up(abspath)
|
78
|
+
loop do
|
79
|
+
yield abspath
|
80
|
+
abspath, basename = File.split(abspath)
|
81
|
+
break if basename == "/"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
77
85
|
# --- Constants ---------------------------------------------------------------------------------
|
78
86
|
|
79
87
|
# The autoload? predicate takes into account the ancestor chain of the
|
@@ -94,11 +102,11 @@ module Zeitwerk::Loader::Helpers
|
|
94
102
|
#
|
95
103
|
# @sig (Module, Symbol) -> String?
|
96
104
|
if method(:autoload?).arity == 1
|
97
|
-
def strict_autoload_path(parent, cname)
|
105
|
+
private def strict_autoload_path(parent, cname)
|
98
106
|
parent.autoload?(cname) if cdef?(parent, cname)
|
99
107
|
end
|
100
108
|
else
|
101
|
-
def strict_autoload_path(parent, cname)
|
109
|
+
private def strict_autoload_path(parent, cname)
|
102
110
|
parent.autoload?(cname, false)
|
103
111
|
end
|
104
112
|
end
|
@@ -107,23 +115,23 @@ module Zeitwerk::Loader::Helpers
|
|
107
115
|
if Symbol.method_defined?(:name)
|
108
116
|
# Symbol#name was introduced in Ruby 3.0. It returns always the same
|
109
117
|
# frozen object, so we may save a few string allocations.
|
110
|
-
def cpath(parent, cname)
|
118
|
+
private def cpath(parent, cname)
|
111
119
|
Object == parent ? cname.name : "#{real_mod_name(parent)}::#{cname.name}"
|
112
120
|
end
|
113
121
|
else
|
114
|
-
def cpath(parent, cname)
|
122
|
+
private def cpath(parent, cname)
|
115
123
|
Object == parent ? cname.to_s : "#{real_mod_name(parent)}::#{cname}"
|
116
124
|
end
|
117
125
|
end
|
118
126
|
|
119
127
|
# @sig (Module, Symbol) -> bool
|
120
|
-
def cdef?(parent, cname)
|
128
|
+
private def cdef?(parent, cname)
|
121
129
|
parent.const_defined?(cname, false)
|
122
130
|
end
|
123
131
|
|
124
132
|
# @raise [NameError]
|
125
133
|
# @sig (Module, Symbol) -> Object
|
126
|
-
def cget(parent, cname)
|
134
|
+
private def cget(parent, cname)
|
127
135
|
parent.const_get(cname, false)
|
128
136
|
end
|
129
137
|
end
|
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
|
@@ -95,15 +109,15 @@ module Zeitwerk
|
|
95
109
|
Registry.register_loader(self)
|
96
110
|
end
|
97
111
|
|
98
|
-
# Sets autoloads in the root
|
112
|
+
# Sets autoloads in the root namespaces.
|
99
113
|
#
|
100
114
|
# @sig () -> void
|
101
115
|
def setup
|
102
116
|
mutex.synchronize do
|
103
117
|
break if @setup
|
104
118
|
|
105
|
-
|
106
|
-
set_autoloads_in_dir(root_dir,
|
119
|
+
actual_roots.each do |root_dir, root_namespace|
|
120
|
+
set_autoloads_in_dir(root_dir, root_namespace)
|
107
121
|
end
|
108
122
|
|
109
123
|
on_setup_callbacks.each(&:call)
|
@@ -126,6 +140,8 @@ module Zeitwerk
|
|
126
140
|
# @sig () -> void
|
127
141
|
def unload
|
128
142
|
mutex.synchronize do
|
143
|
+
raise SetupRequired unless @setup
|
144
|
+
|
129
145
|
# We are going to keep track of the files that were required by our
|
130
146
|
# autoloads to later remove them from $LOADED_FEATURES, thus making them
|
131
147
|
# loadable by Kernel#require again.
|
@@ -181,10 +197,11 @@ module Zeitwerk
|
|
181
197
|
autoloads.clear
|
182
198
|
autoloaded_dirs.clear
|
183
199
|
to_unload.clear
|
184
|
-
|
200
|
+
namespace_dirs.clear
|
201
|
+
shadowed_files.clear
|
185
202
|
|
186
203
|
Registry.on_unload(self)
|
187
|
-
ExplicitNamespace.
|
204
|
+
ExplicitNamespace.__unregister_loader(self)
|
188
205
|
|
189
206
|
@setup = false
|
190
207
|
@eager_loaded = false
|
@@ -201,6 +218,7 @@ module Zeitwerk
|
|
201
218
|
# @sig () -> void
|
202
219
|
def reload
|
203
220
|
raise ReloadingDisabledError unless reloading_enabled?
|
221
|
+
raise SetupRequired unless @setup
|
204
222
|
|
205
223
|
unload
|
206
224
|
recompute_ignored_paths
|
@@ -208,58 +226,6 @@ module Zeitwerk
|
|
208
226
|
setup
|
209
227
|
end
|
210
228
|
|
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
229
|
# Says if the given constant path would be unloaded on reload. This
|
264
230
|
# predicate returns `false` if reloading is disabled.
|
265
231
|
#
|
@@ -282,7 +248,16 @@ module Zeitwerk
|
|
282
248
|
# @sig () -> void
|
283
249
|
def unregister
|
284
250
|
Registry.unregister_loader(self)
|
285
|
-
ExplicitNamespace.
|
251
|
+
ExplicitNamespace.__unregister_loader(self)
|
252
|
+
end
|
253
|
+
|
254
|
+
# The return value of this predicate is only meaningful if the loader has
|
255
|
+
# scanned the file. This is the case in the spots where we use it.
|
256
|
+
#
|
257
|
+
# @private
|
258
|
+
# @sig (String) -> Boolean
|
259
|
+
def shadowed_file?(file)
|
260
|
+
shadowed_files.member?(file)
|
286
261
|
end
|
287
262
|
|
288
263
|
# --- Class methods ---------------------------------------------------------------------------
|
@@ -311,11 +286,32 @@ module Zeitwerk
|
|
311
286
|
Registry.loader_for_gem(called_from, warn_on_extra_files: warn_on_extra_files)
|
312
287
|
end
|
313
288
|
|
314
|
-
# Broadcasts `eager_load` to all loaders.
|
289
|
+
# Broadcasts `eager_load` to all loaders. Those that have not been setup
|
290
|
+
# are skipped.
|
315
291
|
#
|
316
292
|
# @sig () -> void
|
317
293
|
def eager_load_all
|
318
|
-
Registry.loaders.each
|
294
|
+
Registry.loaders.each do |loader|
|
295
|
+
begin
|
296
|
+
loader.eager_load
|
297
|
+
rescue SetupRequired
|
298
|
+
# This is fine, we eager load what can be eager loaded.
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
# Broadcasts `eager_load_namespace` to all loaders. Those that have not
|
304
|
+
# been setup are skipped.
|
305
|
+
#
|
306
|
+
# @sig (Module) -> void
|
307
|
+
def eager_load_namespace(mod)
|
308
|
+
Registry.loaders.each do |loader|
|
309
|
+
begin
|
310
|
+
loader.eager_load_namespace(mod)
|
311
|
+
rescue SetupRequired
|
312
|
+
# This is fine, we eager load what can be eager loaded.
|
313
|
+
end
|
314
|
+
end
|
319
315
|
end
|
320
316
|
|
321
317
|
# Returns an array with the absolute paths of the root directories of all
|
@@ -338,19 +334,11 @@ module Zeitwerk
|
|
338
334
|
cname = inflector.camelize(basename, abspath).to_sym
|
339
335
|
autoload_file(parent, cname, abspath)
|
340
336
|
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)
|
337
|
+
if collapse?(abspath)
|
338
|
+
set_autoloads_in_dir(abspath, parent)
|
339
|
+
else
|
348
340
|
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
|
341
|
+
autoload_subdir(parent, cname, abspath)
|
354
342
|
end
|
355
343
|
end
|
356
344
|
rescue ::NameError => error
|
@@ -380,10 +368,10 @@ module Zeitwerk
|
|
380
368
|
# We do not need to issue another autoload, the existing one is enough
|
381
369
|
# no matter if it is for a file or a directory. Just remember the
|
382
370
|
# subdirectory has to be visited if the namespace is used.
|
383
|
-
|
371
|
+
namespace_dirs[cpath] << subdir
|
384
372
|
elsif !cdef?(parent, cname)
|
385
373
|
# First time we find this namespace, set an autoload for it.
|
386
|
-
|
374
|
+
namespace_dirs[cpath(parent, cname)] << subdir
|
387
375
|
set_autoload(parent, cname, subdir)
|
388
376
|
else
|
389
377
|
# For whatever reason the constant that corresponds to this namespace has
|
@@ -398,6 +386,7 @@ module Zeitwerk
|
|
398
386
|
if autoload_path = strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
|
399
387
|
# First autoload for a Ruby file wins, just ignore subsequent ones.
|
400
388
|
if ruby?(autoload_path)
|
389
|
+
shadowed_files << file
|
401
390
|
log("file #{file} is ignored because #{autoload_path} has precedence") if logger
|
402
391
|
else
|
403
392
|
promote_namespace_from_implicit_to_explicit(
|
@@ -408,6 +397,7 @@ module Zeitwerk
|
|
408
397
|
)
|
409
398
|
end
|
410
399
|
elsif cdef?(parent, cname)
|
400
|
+
shadowed_files << file
|
411
401
|
log("file #{file} is ignored because #{cpath(parent, cname)} is already defined") if logger
|
412
402
|
else
|
413
403
|
set_autoload(parent, cname, file)
|
@@ -460,25 +450,26 @@ module Zeitwerk
|
|
460
450
|
|
461
451
|
# @sig (String) -> void
|
462
452
|
def register_explicit_namespace(cpath)
|
463
|
-
ExplicitNamespace.
|
453
|
+
ExplicitNamespace.__register(cpath, self)
|
464
454
|
end
|
465
455
|
|
466
456
|
# @sig (String) -> void
|
467
457
|
def raise_if_conflicting_directory(dir)
|
468
458
|
MUTEX.synchronize do
|
459
|
+
dir_slash = dir + "/"
|
460
|
+
|
469
461
|
Registry.loaders.each do |loader|
|
470
462
|
next if loader == self
|
471
|
-
next if loader.
|
463
|
+
next if loader.__ignores?(dir)
|
472
464
|
|
473
|
-
|
474
|
-
loader.root_dirs.each do |root_dir, _namespace|
|
465
|
+
loader.__roots.each_key do |root_dir|
|
475
466
|
next if ignores?(root_dir)
|
476
467
|
|
477
|
-
|
478
|
-
if
|
468
|
+
root_dir_slash = root_dir + "/"
|
469
|
+
if dir_slash.start_with?(root_dir_slash) || root_dir_slash.start_with?(dir_slash)
|
479
470
|
require "pp" # Needed for pretty_inspect, even in Ruby 2.5.
|
480
471
|
raise Error,
|
481
|
-
"loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir
|
472
|
+
"loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir}," \
|
482
473
|
" which is already managed by\n\n#{loader.pretty_inspect}\n"
|
483
474
|
EOS
|
484
475
|
end
|
data/lib/zeitwerk/version.rb
CHANGED
data/lib/zeitwerk.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.6
|
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-11-08 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: |2
|
14
14
|
Zeitwerk implements constant autoloading with Ruby semantics. Each gem
|
@@ -28,10 +28,12 @@ files:
|
|
28
28
|
- lib/zeitwerk/gem_inflector.rb
|
29
29
|
- lib/zeitwerk/gem_loader.rb
|
30
30
|
- lib/zeitwerk/inflector.rb
|
31
|
+
- lib/zeitwerk/internal.rb
|
31
32
|
- lib/zeitwerk/kernel.rb
|
32
33
|
- lib/zeitwerk/loader.rb
|
33
34
|
- lib/zeitwerk/loader/callbacks.rb
|
34
35
|
- lib/zeitwerk/loader/config.rb
|
36
|
+
- lib/zeitwerk/loader/eager_load.rb
|
35
37
|
- lib/zeitwerk/loader/helpers.rb
|
36
38
|
- lib/zeitwerk/real_mod_name.rb
|
37
39
|
- lib/zeitwerk/registry.rb
|