zeitwerk 2.7.4 → 2.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +19 -0
- data/lib/zeitwerk/gem_loader.rb +2 -2
- data/lib/zeitwerk/loader/callbacks.rb +3 -3
- data/lib/zeitwerk/loader/config.rb +9 -9
- data/lib/zeitwerk/loader/eager_load.rb +26 -28
- data/lib/zeitwerk/loader/file_system.rb +165 -0
- data/lib/zeitwerk/loader/helpers.rb +1 -100
- data/lib/zeitwerk/loader.rb +43 -48
- data/lib/zeitwerk/real_mod_name.rb +1 -1
- data/lib/zeitwerk/registry/loaders.rb +2 -2
- data/lib/zeitwerk/registry.rb +26 -0
- data/lib/zeitwerk/version.rb +1 -1
- metadata +4 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '06683acd85eeb772fdb9757ae20bfb05e42ec465acfff3edcada546a399d9131'
|
|
4
|
+
data.tar.gz: 80e3cdede4e05607ef5c58c60a3f7824c6750dd1d19d8786352356025bde1d65
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b37beb740e461d73a1bd1ae21ec4f2bf63bb5022f3a193e2718bb8e5d4f172bf9c1222ff2a33acc47fb390a34e50b8b4b65b5b762dc5f71c1971b25720081437
|
|
7
|
+
data.tar.gz: 02c85e17a3ab801c925157239386c9a5a105cc1b8b9d735f64c27c89d3a0371613f2cb3f27d49647d98bf3ad8220d3a3538c2bdae215a5890145136374eb1d3e
|
data/README.md
CHANGED
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
- [Reopening third-party namespaces](#reopening-third-party-namespaces)
|
|
59
59
|
- [Introspection](#introspection)
|
|
60
60
|
- [`Zeitwerk::Loader#dirs`](#zeitwerkloaderdirs)
|
|
61
|
+
- [Autoloaded Constants](#autoloaded-constants)
|
|
61
62
|
- [`Zeitwerk::Loader#cpath_expected_at`](#zeitwerkloadercpath_expected_at)
|
|
62
63
|
- [`Zeitwerk::Loader#all_expected_cpaths`](#zeitwerkloaderall_expected_cpaths)
|
|
63
64
|
- [Encodings](#encodings)
|
|
@@ -1263,6 +1264,18 @@ By default, ignored root directories are filtered out. If you want them included
|
|
|
1263
1264
|
|
|
1264
1265
|
These collections are read-only. Please add to them with `Zeitwerk::Loader#push_dir`.
|
|
1265
1266
|
|
|
1267
|
+
<a id="markdown-autoloaded-constants" name="autoloaded-constants"></a>
|
|
1268
|
+
#### Autoloaded Constants
|
|
1269
|
+
|
|
1270
|
+
Zeitwerk does not keep track of autoloaded constants to minimize its memory footprint, but you can collect them with `on_load` if you will:
|
|
1271
|
+
|
|
1272
|
+
```ruby
|
|
1273
|
+
autoloaded_cpaths = []
|
|
1274
|
+
loader.on_load do |cpath, _value, _abspath|
|
|
1275
|
+
autoloaded_cpaths << cpath
|
|
1276
|
+
end
|
|
1277
|
+
```
|
|
1278
|
+
|
|
1266
1279
|
<a id="markdown-zeitwerkloadercpath_expected_at" name="zeitwerkloadercpath_expected_at"></a>
|
|
1267
1280
|
#### `Zeitwerk::Loader#cpath_expected_at`
|
|
1268
1281
|
|
|
@@ -1408,6 +1421,12 @@ To run one particular suite, pass its file name as an argument:
|
|
|
1408
1421
|
bin/test test/lib/zeitwerk/test_eager_load.rb
|
|
1409
1422
|
```
|
|
1410
1423
|
|
|
1424
|
+
That also accepts a line number:
|
|
1425
|
+
|
|
1426
|
+
```
|
|
1427
|
+
bin/test test/lib/zeitwerk/test_eager_load.rb:52
|
|
1428
|
+
```
|
|
1429
|
+
|
|
1411
1430
|
Furthermore, the project has a development dependency on [`minitest-focus`](https://github.com/seattlerb/minitest-focus). To run an individual test mark it with `focus`:
|
|
1412
1431
|
|
|
1413
1432
|
```ruby
|
data/lib/zeitwerk/gem_loader.rb
CHANGED
|
@@ -42,12 +42,12 @@ module Zeitwerk
|
|
|
42
42
|
def warn_on_extra_files
|
|
43
43
|
expected_namespace_dir = @root_file.delete_suffix(".rb")
|
|
44
44
|
|
|
45
|
-
ls(@root_dir) do |basename, abspath, ftype|
|
|
45
|
+
@fs.ls(@root_dir) do |basename, abspath, ftype|
|
|
46
46
|
next if abspath == @root_file
|
|
47
47
|
next if abspath == expected_namespace_dir
|
|
48
48
|
|
|
49
49
|
basename_without_ext = basename.delete_suffix(".rb")
|
|
50
|
-
cname =
|
|
50
|
+
cname = cname_for(basename_without_ext, abspath)
|
|
51
51
|
|
|
52
52
|
warn(<<~EOS)
|
|
53
53
|
WARNING: Zeitwerk defines the constant #{cname} after the #{ftype}
|
|
@@ -12,12 +12,12 @@ module Zeitwerk::Loader::Callbacks # :nodoc: all
|
|
|
12
12
|
Zeitwerk::Registry.autoloads.unregister(file)
|
|
13
13
|
|
|
14
14
|
if cref.defined?
|
|
15
|
-
log
|
|
15
|
+
log { "constant #{cref} loaded from file #{file}" }
|
|
16
16
|
to_unload[file] = cref if reloading_enabled?
|
|
17
17
|
run_on_load_callbacks(cref.path, cref.get, file) unless on_load_callbacks.empty?
|
|
18
18
|
else
|
|
19
19
|
msg = "expected file #{file} to define constant #{cref}, but didn't"
|
|
20
|
-
log
|
|
20
|
+
log { msg }
|
|
21
21
|
|
|
22
22
|
# Ruby still keeps the autoload defined, but we remove it because the
|
|
23
23
|
# contract in Zeitwerk is more strict.
|
|
@@ -52,7 +52,7 @@ module Zeitwerk::Loader::Callbacks # :nodoc: all
|
|
|
52
52
|
dirs_autoload_monitor.synchronize do
|
|
53
53
|
if cref = autoloads.delete(dir)
|
|
54
54
|
implicit_namespace = cref.set(Module.new)
|
|
55
|
-
log
|
|
55
|
+
log { "module #{cref} autovivified from directory #{dir}" }
|
|
56
56
|
|
|
57
57
|
to_unload[dir] = cref if reloading_enabled?
|
|
58
58
|
|
|
@@ -116,8 +116,8 @@ module Zeitwerk::Loader::Config
|
|
|
116
116
|
end
|
|
117
117
|
|
|
118
118
|
abspath = File.expand_path(path)
|
|
119
|
-
if dir?(abspath)
|
|
120
|
-
|
|
119
|
+
if @fs.dir?(abspath)
|
|
120
|
+
raise_if_conflicting_root_dir(abspath)
|
|
121
121
|
roots[abspath] = namespace
|
|
122
122
|
else
|
|
123
123
|
raise Zeitwerk::Error, "the root directory #{abspath} does not exist"
|
|
@@ -290,28 +290,28 @@ module Zeitwerk::Loader::Config
|
|
|
290
290
|
# Common use case.
|
|
291
291
|
return false if ignored_paths.empty?
|
|
292
292
|
|
|
293
|
-
walk_up(abspath) do |path|
|
|
293
|
+
@fs.walk_up(abspath) do |path|
|
|
294
294
|
return true if ignored_path?(path)
|
|
295
|
-
return false if
|
|
295
|
+
return false if root_dir?(path)
|
|
296
296
|
end
|
|
297
297
|
|
|
298
298
|
false
|
|
299
299
|
end
|
|
300
300
|
|
|
301
301
|
#: (String) -> bool
|
|
302
|
-
|
|
302
|
+
internal def ignored_path?(abspath)
|
|
303
303
|
ignored_paths.member?(abspath)
|
|
304
304
|
end
|
|
305
305
|
|
|
306
306
|
#: () -> Array[String]
|
|
307
307
|
private def actual_roots
|
|
308
308
|
roots.reject do |root_dir, _root_namespace|
|
|
309
|
-
|
|
309
|
+
!@fs.dir?(root_dir) || ignored_path?(root_dir)
|
|
310
310
|
end
|
|
311
311
|
end
|
|
312
312
|
|
|
313
313
|
#: (String) -> bool
|
|
314
|
-
|
|
314
|
+
internal def root_dir?(dir)
|
|
315
315
|
roots.key?(dir)
|
|
316
316
|
end
|
|
317
317
|
|
|
@@ -320,9 +320,9 @@ module Zeitwerk::Loader::Config
|
|
|
320
320
|
# Optimize this common use case.
|
|
321
321
|
return false if eager_load_exclusions.empty?
|
|
322
322
|
|
|
323
|
-
walk_up(abspath) do |path|
|
|
323
|
+
@fs.walk_up(abspath) do |path|
|
|
324
324
|
return true if eager_load_exclusions.member?(path)
|
|
325
|
-
return false if
|
|
325
|
+
return false if root_dir?(path)
|
|
326
326
|
end
|
|
327
327
|
|
|
328
328
|
false
|
|
@@ -11,7 +11,7 @@ module Zeitwerk::Loader::EagerLoad
|
|
|
11
11
|
break if @eager_loaded
|
|
12
12
|
raise Zeitwerk::SetupRequired unless @setup
|
|
13
13
|
|
|
14
|
-
log
|
|
14
|
+
log { "eager load start" }
|
|
15
15
|
|
|
16
16
|
actual_roots.each do |root_dir, root_namespace|
|
|
17
17
|
actual_eager_load_dir(root_dir, root_namespace, force: force)
|
|
@@ -24,7 +24,7 @@ module Zeitwerk::Loader::EagerLoad
|
|
|
24
24
|
|
|
25
25
|
@eager_loaded = true
|
|
26
26
|
|
|
27
|
-
log
|
|
27
|
+
log { "eager load end" }
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
|
|
@@ -34,23 +34,21 @@ module Zeitwerk::Loader::EagerLoad
|
|
|
34
34
|
|
|
35
35
|
abspath = File.expand_path(path)
|
|
36
36
|
|
|
37
|
-
raise Zeitwerk::Error.new("#{abspath} is not a directory") unless dir?(abspath)
|
|
37
|
+
raise Zeitwerk::Error.new("#{abspath} is not a directory") unless @fs.dir?(abspath)
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
paths = []
|
|
40
40
|
|
|
41
41
|
root_namespace = nil
|
|
42
|
-
walk_up(abspath) do |dir|
|
|
42
|
+
@fs.walk_up(abspath) do |dir|
|
|
43
43
|
return if ignored_path?(dir)
|
|
44
44
|
return if eager_load_exclusions.member?(dir)
|
|
45
45
|
|
|
46
46
|
break if root_namespace = roots[dir]
|
|
47
47
|
|
|
48
48
|
basename = File.basename(dir)
|
|
49
|
-
return if hidden?(basename)
|
|
49
|
+
return if @fs.hidden?(basename)
|
|
50
50
|
|
|
51
|
-
unless collapse?(dir)
|
|
52
|
-
cnames << inflector.camelize(basename, dir).to_sym
|
|
53
|
-
end
|
|
51
|
+
paths << [basename, dir] unless collapse?(dir)
|
|
54
52
|
end
|
|
55
53
|
|
|
56
54
|
raise Zeitwerk::Error.new("I do not manage #{abspath}") unless root_namespace
|
|
@@ -58,7 +56,8 @@ module Zeitwerk::Loader::EagerLoad
|
|
|
58
56
|
return if @eager_loaded
|
|
59
57
|
|
|
60
58
|
namespace = root_namespace
|
|
61
|
-
|
|
59
|
+
paths.reverse_each do |basename, dir|
|
|
60
|
+
cname = cname_for(basename, dir)
|
|
62
61
|
# Can happen if there are no Ruby files. This is not an error condition,
|
|
63
62
|
# the directory is actually managed. Could have Ruby files later.
|
|
64
63
|
return unless namespace.const_defined?(cname, false)
|
|
@@ -117,34 +116,33 @@ module Zeitwerk::Loader::EagerLoad
|
|
|
117
116
|
abspath = File.expand_path(path)
|
|
118
117
|
|
|
119
118
|
raise Zeitwerk::Error.new("#{abspath} does not exist") unless File.exist?(abspath)
|
|
120
|
-
raise Zeitwerk::Error.new("#{abspath} is not a Ruby file") if
|
|
119
|
+
raise Zeitwerk::Error.new("#{abspath} is not a Ruby file") if !@fs.rb_extension?(abspath)
|
|
121
120
|
raise Zeitwerk::Error.new("#{abspath} is ignored") if ignored_path?(abspath)
|
|
122
121
|
|
|
123
|
-
|
|
124
|
-
raise Zeitwerk::Error.new("#{abspath} is ignored") if hidden?(
|
|
125
|
-
|
|
126
|
-
base_cname = inflector.camelize(basename, abspath).to_sym
|
|
122
|
+
file_basename = File.basename(abspath, ".rb")
|
|
123
|
+
raise Zeitwerk::Error.new("#{abspath} is ignored") if @fs.hidden?(file_basename)
|
|
127
124
|
|
|
128
125
|
root_namespace = nil
|
|
129
|
-
|
|
126
|
+
paths = []
|
|
130
127
|
|
|
131
|
-
walk_up(File.dirname(abspath)) do |dir|
|
|
128
|
+
@fs.walk_up(File.dirname(abspath)) do |dir|
|
|
132
129
|
raise Zeitwerk::Error.new("#{abspath} is ignored") if ignored_path?(dir)
|
|
133
130
|
|
|
134
131
|
break if root_namespace = roots[dir]
|
|
135
132
|
|
|
136
133
|
basename = File.basename(dir)
|
|
137
|
-
raise Zeitwerk::Error.new("#{abspath} is ignored") if hidden?(basename)
|
|
134
|
+
raise Zeitwerk::Error.new("#{abspath} is ignored") if @fs.hidden?(basename)
|
|
138
135
|
|
|
139
|
-
unless collapse?(dir)
|
|
140
|
-
cnames << inflector.camelize(basename, dir).to_sym
|
|
141
|
-
end
|
|
136
|
+
paths << [basename, dir] unless collapse?(dir)
|
|
142
137
|
end
|
|
143
138
|
|
|
144
139
|
raise Zeitwerk::Error.new("I do not manage #{abspath}") unless root_namespace
|
|
145
140
|
|
|
141
|
+
base_cname = cname_for(file_basename, abspath)
|
|
142
|
+
|
|
146
143
|
namespace = root_namespace
|
|
147
|
-
|
|
144
|
+
paths.reverse_each do |basename, dir|
|
|
145
|
+
cname = cname_for(basename, dir)
|
|
148
146
|
namespace = namespace.const_get(cname, false)
|
|
149
147
|
end
|
|
150
148
|
|
|
@@ -161,11 +159,11 @@ module Zeitwerk::Loader::EagerLoad
|
|
|
161
159
|
honour_exclusions = !force
|
|
162
160
|
return if honour_exclusions && excluded_from_eager_load?(dir)
|
|
163
161
|
|
|
164
|
-
log
|
|
162
|
+
log { "eager load directory #{dir} start" }
|
|
165
163
|
|
|
166
164
|
queue = [[dir, namespace]]
|
|
167
165
|
while (current_dir, namespace = queue.shift)
|
|
168
|
-
ls(current_dir) do |basename, abspath, ftype|
|
|
166
|
+
@fs.ls(current_dir) do |basename, abspath, ftype|
|
|
169
167
|
next if honour_exclusions && eager_load_exclusions.member?(abspath)
|
|
170
168
|
|
|
171
169
|
if ftype == :file
|
|
@@ -176,14 +174,14 @@ module Zeitwerk::Loader::EagerLoad
|
|
|
176
174
|
if collapse?(abspath)
|
|
177
175
|
queue << [abspath, namespace]
|
|
178
176
|
else
|
|
179
|
-
cname =
|
|
177
|
+
cname = cname_for(basename, abspath)
|
|
180
178
|
queue << [abspath, namespace.const_get(cname, false)]
|
|
181
179
|
end
|
|
182
180
|
end
|
|
183
181
|
end
|
|
184
182
|
end
|
|
185
183
|
|
|
186
|
-
log
|
|
184
|
+
log { "eager load directory #{dir} end" }
|
|
187
185
|
end
|
|
188
186
|
|
|
189
187
|
# In order to invoke this method, the caller has to ensure `child` is a
|
|
@@ -208,12 +206,12 @@ module Zeitwerk::Loader::EagerLoad
|
|
|
208
206
|
|
|
209
207
|
suffix.split("::").each do |segment|
|
|
210
208
|
while (dir = dirs.shift)
|
|
211
|
-
ls(dir) do |basename, abspath, ftype|
|
|
209
|
+
@fs.ls(dir) do |basename, abspath, ftype|
|
|
212
210
|
next unless ftype == :directory
|
|
213
211
|
|
|
214
212
|
if collapse?(abspath)
|
|
215
213
|
dirs << abspath
|
|
216
|
-
elsif segment ==
|
|
214
|
+
elsif segment == cname_for(basename, abspath).to_s
|
|
217
215
|
next_dirs << abspath
|
|
218
216
|
end
|
|
219
217
|
end
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This private class encapsulates interactions with the file system.
|
|
4
|
+
#
|
|
5
|
+
# It is used to list directories and check file types, and it encodes the
|
|
6
|
+
# conventions documented in the README.
|
|
7
|
+
#
|
|
8
|
+
# @private
|
|
9
|
+
class Zeitwerk::Loader::FileSystem # :nodoc:
|
|
10
|
+
#: (Zeitwerk::Loader) -> void
|
|
11
|
+
def initialize(loader)
|
|
12
|
+
@loader = loader
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
#: (String) { (String, String, Symbol) -> void } -> void
|
|
16
|
+
def ls(dir)
|
|
17
|
+
children = relevant_dir_entries(dir)
|
|
18
|
+
|
|
19
|
+
# The order in which a directory is listed depends on the file system.
|
|
20
|
+
#
|
|
21
|
+
# Since client code may run on different platforms, it seems convenient to
|
|
22
|
+
# sort directory entries. This provides more deterministic behavior, with
|
|
23
|
+
# consistent eager loading in particular.
|
|
24
|
+
children.sort_by!(&:first)
|
|
25
|
+
|
|
26
|
+
children.each do |basename, abspath, ftype|
|
|
27
|
+
if :directory == ftype && !has_at_least_one_ruby_file?(abspath)
|
|
28
|
+
@loader.__log { "directory #{abspath} is ignored because it has no Ruby files" }
|
|
29
|
+
next
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
yield basename, abspath, ftype
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
#: (String) { (String) -> void } -> void
|
|
37
|
+
def walk_up(abspath)
|
|
38
|
+
loop do
|
|
39
|
+
yield abspath
|
|
40
|
+
abspath, basename = File.split(abspath)
|
|
41
|
+
break if basename == "/"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Encodes the documented conventions.
|
|
46
|
+
#
|
|
47
|
+
#: (String) -> Symbol?
|
|
48
|
+
def supported_ftype?(abspath)
|
|
49
|
+
if rb_extension?(abspath)
|
|
50
|
+
:file # By convention, we can avoid a syscall here.
|
|
51
|
+
elsif dir?(abspath)
|
|
52
|
+
:directory
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
#: (String) -> bool
|
|
57
|
+
def rb_extension?(path)
|
|
58
|
+
path.end_with?(".rb")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
#: (String) -> bool
|
|
62
|
+
def dir?(path)
|
|
63
|
+
File.directory?(path)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
#: (String) -> bool
|
|
67
|
+
def hidden?(basename)
|
|
68
|
+
basename.start_with?(".")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
# Looks for a Ruby file using breadth-first search. This type of search is
|
|
74
|
+
# important to list as less directories as possible and return fast in the
|
|
75
|
+
# common case in which there are Ruby files in the passed directory.
|
|
76
|
+
#
|
|
77
|
+
#: (String) -> bool
|
|
78
|
+
def has_at_least_one_ruby_file?(dir)
|
|
79
|
+
to_visit = [dir]
|
|
80
|
+
|
|
81
|
+
while (dir = to_visit.shift)
|
|
82
|
+
relevant_dir_entries(dir) do |_, abspath, ftype|
|
|
83
|
+
return true if :file == ftype
|
|
84
|
+
to_visit << abspath
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
false
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
#: (String) { (String, String, Symbol) -> void } -> void
|
|
92
|
+
#: (String) -> [[String, String, Symbol]]
|
|
93
|
+
def relevant_dir_entries(dir)
|
|
94
|
+
return enum_for(__method__, dir).to_a unless block_given?
|
|
95
|
+
|
|
96
|
+
each_ruby_file_or_directory(dir) do |basename, abspath, ftype|
|
|
97
|
+
next if @loader.__ignored_path?(abspath)
|
|
98
|
+
|
|
99
|
+
if :link == ftype
|
|
100
|
+
begin
|
|
101
|
+
ftype = File.stat(abspath).ftype.to_sym
|
|
102
|
+
rescue Errno::ENOENT
|
|
103
|
+
warn "ignoring broken symlink #{abspath}"
|
|
104
|
+
next
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
if :file == ftype
|
|
109
|
+
yield basename, abspath, ftype if rb_extension?(basename)
|
|
110
|
+
elsif :directory == ftype
|
|
111
|
+
# Conceptually, root directories represent a separate project tree.
|
|
112
|
+
yield basename, abspath, ftype unless @loader.__root_dir?(abspath)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Dir.scan is more efficient in common platforms, but it is going to take a
|
|
118
|
+
# while for it to be available.
|
|
119
|
+
#
|
|
120
|
+
# The following compatibility methods have the same semantics but are written
|
|
121
|
+
# to favor the performance of the Ruby fallback, which can save syscalls.
|
|
122
|
+
#
|
|
123
|
+
# In particular, by convention, any directory entry with a .rb extension is
|
|
124
|
+
# assumed to be a file or a symlink to a file.
|
|
125
|
+
#
|
|
126
|
+
# These methods also freeze abspaths because that saves allocations when
|
|
127
|
+
# passed later to File methods. See https://github.com/fxn/zeitwerk/pull/125.
|
|
128
|
+
|
|
129
|
+
if Dir.respond_to?(:scan) # Available in Ruby 4.1.
|
|
130
|
+
#: (String) { (String, String, Symbol) -> void } -> void
|
|
131
|
+
def each_ruby_file_or_directory(dir)
|
|
132
|
+
Dir.scan(dir) do |basename, ftype|
|
|
133
|
+
next if hidden?(basename)
|
|
134
|
+
|
|
135
|
+
if rb_extension?(basename)
|
|
136
|
+
abspath = File.join(dir, basename).freeze
|
|
137
|
+
yield basename, abspath, :file # By convention.
|
|
138
|
+
elsif :directory == ftype
|
|
139
|
+
abspath = File.join(dir, basename).freeze
|
|
140
|
+
yield basename, abspath, :directory
|
|
141
|
+
elsif :link == ftype
|
|
142
|
+
abspath = File.join(dir, basename).freeze
|
|
143
|
+
yield basename, abspath, :directory if dir?(abspath)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
else
|
|
148
|
+
#: (String) { (String, String, Symbol) -> void } -> void
|
|
149
|
+
def each_ruby_file_or_directory(dir)
|
|
150
|
+
Dir.each_child(dir) do |basename|
|
|
151
|
+
next if hidden?(basename)
|
|
152
|
+
|
|
153
|
+
if rb_extension?(basename)
|
|
154
|
+
abspath = File.join(dir, basename).freeze
|
|
155
|
+
yield basename, abspath, :file # By convention.
|
|
156
|
+
else
|
|
157
|
+
abspath = File.join(dir, basename).freeze
|
|
158
|
+
if dir?(abspath)
|
|
159
|
+
yield basename, abspath, :directory
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -1,105 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Zeitwerk::Loader::Helpers
|
|
4
|
-
# --- Logging -----------------------------------------------------------------------------------
|
|
5
|
-
|
|
6
|
-
#: (to_s() -> String) -> void
|
|
7
|
-
private def log(message)
|
|
8
|
-
method_name = logger.respond_to?(:debug) ? :debug : :call
|
|
9
|
-
logger.send(method_name, "Zeitwerk@#{tag}: #{message}")
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
# --- Files and directories ---------------------------------------------------------------------
|
|
13
|
-
|
|
14
|
-
#: (String) { (String, String, Symbol) -> void } -> void
|
|
15
|
-
private def ls(dir)
|
|
16
|
-
children = Dir.children(dir)
|
|
17
|
-
|
|
18
|
-
# The order in which a directory is listed depends on the file system.
|
|
19
|
-
#
|
|
20
|
-
# Since client code may run in different platforms, it seems convenient to
|
|
21
|
-
# order directory entries. This provides consistent eager loading across
|
|
22
|
-
# platforms, for example.
|
|
23
|
-
children.sort!
|
|
24
|
-
|
|
25
|
-
children.each do |basename|
|
|
26
|
-
next if hidden?(basename)
|
|
27
|
-
|
|
28
|
-
abspath = File.join(dir, basename)
|
|
29
|
-
next if ignored_path?(abspath)
|
|
30
|
-
|
|
31
|
-
if dir?(abspath)
|
|
32
|
-
next if roots.key?(abspath)
|
|
33
|
-
|
|
34
|
-
if !has_at_least_one_ruby_file?(abspath)
|
|
35
|
-
log("directory #{abspath} is ignored because it has no Ruby files") if logger
|
|
36
|
-
next
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
ftype = :directory
|
|
40
|
-
else
|
|
41
|
-
next unless ruby?(abspath)
|
|
42
|
-
ftype = :file
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# We freeze abspath because that saves allocations when passed later to
|
|
46
|
-
# File methods. See #125.
|
|
47
|
-
yield basename, abspath.freeze, ftype
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
# Looks for a Ruby file using breadth-first search. This type of search is
|
|
52
|
-
# important to list as less directories as possible and return fast in the
|
|
53
|
-
# common case in which there are Ruby files.
|
|
54
|
-
#
|
|
55
|
-
#: (String) -> bool
|
|
56
|
-
private def has_at_least_one_ruby_file?(dir)
|
|
57
|
-
to_visit = [dir]
|
|
58
|
-
|
|
59
|
-
while (dir = to_visit.shift)
|
|
60
|
-
Dir.each_child(dir) do |basename|
|
|
61
|
-
next if hidden?(basename)
|
|
62
|
-
|
|
63
|
-
abspath = File.join(dir, basename)
|
|
64
|
-
next if ignored_path?(abspath)
|
|
65
|
-
|
|
66
|
-
if dir?(abspath)
|
|
67
|
-
to_visit << abspath unless roots.key?(abspath)
|
|
68
|
-
else
|
|
69
|
-
return true if ruby?(abspath)
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
false
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
#: (String) -> bool
|
|
78
|
-
private def ruby?(path)
|
|
79
|
-
path.end_with?(".rb")
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
#: (String) -> bool
|
|
83
|
-
private def dir?(path)
|
|
84
|
-
File.directory?(path)
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
#: (String) -> bool
|
|
88
|
-
private def hidden?(basename)
|
|
89
|
-
basename.start_with?(".")
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
#: (String) { (String) -> void } -> void
|
|
93
|
-
private def walk_up(abspath)
|
|
94
|
-
loop do
|
|
95
|
-
yield abspath
|
|
96
|
-
abspath, basename = File.split(abspath)
|
|
97
|
-
break if basename == "/"
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# --- Inflection --------------------------------------------------------------------------------
|
|
102
|
-
|
|
103
4
|
CNAME_VALIDATOR = Module.new #: Module
|
|
104
5
|
private_constant :CNAME_VALIDATOR
|
|
105
6
|
|
|
@@ -124,7 +25,7 @@ module Zeitwerk::Loader::Helpers
|
|
|
124
25
|
begin
|
|
125
26
|
CNAME_VALIDATOR.const_defined?(cname, false)
|
|
126
27
|
rescue ::NameError => error
|
|
127
|
-
path_type =
|
|
28
|
+
path_type = @fs.rb_extension?(abspath) ? "file" : "directory"
|
|
128
29
|
|
|
129
30
|
raise Zeitwerk::NameError.new(<<~MESSAGE, error.name)
|
|
130
31
|
#{error.message} inferred by #{inflector.class} from #{path_type}
|
data/lib/zeitwerk/loader.rb
CHANGED
|
@@ -9,6 +9,7 @@ module Zeitwerk
|
|
|
9
9
|
require_relative "loader/callbacks"
|
|
10
10
|
require_relative "loader/config"
|
|
11
11
|
require_relative "loader/eager_load"
|
|
12
|
+
require_relative "loader/file_system"
|
|
12
13
|
|
|
13
14
|
extend Internal
|
|
14
15
|
|
|
@@ -18,9 +19,6 @@ module Zeitwerk
|
|
|
18
19
|
include Config
|
|
19
20
|
include EagerLoad
|
|
20
21
|
|
|
21
|
-
MUTEX = Mutex.new #: Mutex
|
|
22
|
-
private_constant :MUTEX
|
|
23
|
-
|
|
24
22
|
# Maps absolute paths for which an autoload has been set ---and not
|
|
25
23
|
# executed--- to their corresponding Zeitwerk::Cref object.
|
|
26
24
|
#
|
|
@@ -115,6 +113,7 @@ module Zeitwerk
|
|
|
115
113
|
@shadowed_files = Set.new
|
|
116
114
|
@setup = false
|
|
117
115
|
@eager_loaded = false
|
|
116
|
+
@fs = FileSystem.new(self)
|
|
118
117
|
|
|
119
118
|
@mutex = Mutex.new
|
|
120
119
|
@dirs_autoload_monitor = Monitor.new
|
|
@@ -171,7 +170,7 @@ module Zeitwerk
|
|
|
171
170
|
# and the constant path would escape unloadable_cpath? This is just
|
|
172
171
|
# defensive code to clean things up as much as we are able to.
|
|
173
172
|
unload_cref(cref)
|
|
174
|
-
unloaded_files.add(abspath) if
|
|
173
|
+
unloaded_files.add(abspath) if @fs.rb_extension?(abspath)
|
|
175
174
|
end
|
|
176
175
|
end
|
|
177
176
|
|
|
@@ -189,7 +188,7 @@ module Zeitwerk
|
|
|
189
188
|
end
|
|
190
189
|
|
|
191
190
|
unload_cref(cref)
|
|
192
|
-
unloaded_files.add(abspath) if
|
|
191
|
+
unloaded_files.add(abspath) if @fs.rb_extension?(abspath)
|
|
193
192
|
end
|
|
194
193
|
|
|
195
194
|
unless unloaded_files.empty?
|
|
@@ -199,7 +198,7 @@ module Zeitwerk
|
|
|
199
198
|
# To make it aware of changes, the gem defines singleton methods in
|
|
200
199
|
# $LOADED_FEATURES:
|
|
201
200
|
#
|
|
202
|
-
# https://github.com/
|
|
201
|
+
# https://github.com/rails/bootsnap/blob/main/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb
|
|
203
202
|
#
|
|
204
203
|
# Rails applications may depend on bootsnap, so for unloading to work
|
|
205
204
|
# in that setting it is preferable that we restrict our API choice to
|
|
@@ -255,15 +254,15 @@ module Zeitwerk
|
|
|
255
254
|
|
|
256
255
|
prefix = cpath == "Object" ? "" : cpath + "::"
|
|
257
256
|
|
|
258
|
-
ls(dir) do |basename, abspath, ftype|
|
|
257
|
+
@fs.ls(dir) do |basename, abspath, ftype|
|
|
259
258
|
if ftype == :file
|
|
260
259
|
basename.delete_suffix!(".rb")
|
|
261
|
-
result[abspath] = prefix
|
|
260
|
+
result[abspath] = "#{prefix}#{cname_for(basename, abspath)}"
|
|
262
261
|
else
|
|
263
262
|
if collapse?(abspath)
|
|
264
263
|
queue << [abspath, cpath]
|
|
265
264
|
else
|
|
266
|
-
queue << [abspath, prefix
|
|
265
|
+
queue << [abspath, "#{prefix}#{cname_for(basename, abspath)}"]
|
|
267
266
|
end
|
|
268
267
|
end
|
|
269
268
|
end
|
|
@@ -279,14 +278,16 @@ module Zeitwerk
|
|
|
279
278
|
|
|
280
279
|
raise Zeitwerk::Error.new("#{abspath} does not exist") unless File.exist?(abspath)
|
|
281
280
|
|
|
282
|
-
|
|
281
|
+
ftype = @fs.supported_ftype?(abspath)
|
|
282
|
+
return unless ftype
|
|
283
|
+
|
|
283
284
|
return if ignored_path?(abspath)
|
|
284
285
|
|
|
285
286
|
paths = []
|
|
286
287
|
|
|
287
|
-
if
|
|
288
|
+
if :file == ftype
|
|
288
289
|
basename = File.basename(abspath, ".rb")
|
|
289
|
-
return if hidden?(basename)
|
|
290
|
+
return if @fs.hidden?(basename)
|
|
290
291
|
|
|
291
292
|
paths << [basename, abspath]
|
|
292
293
|
walk_up_from = File.dirname(abspath)
|
|
@@ -296,12 +297,12 @@ module Zeitwerk
|
|
|
296
297
|
|
|
297
298
|
root_namespace = nil
|
|
298
299
|
|
|
299
|
-
walk_up(walk_up_from) do |dir|
|
|
300
|
+
@fs.walk_up(walk_up_from) do |dir|
|
|
300
301
|
break if root_namespace = roots[dir]
|
|
301
302
|
return if ignored_path?(dir)
|
|
302
303
|
|
|
303
304
|
basename = File.basename(dir)
|
|
304
|
-
return if hidden?(basename)
|
|
305
|
+
return if @fs.hidden?(basename)
|
|
305
306
|
|
|
306
307
|
paths << [basename, dir] unless collapse?(dir)
|
|
307
308
|
end
|
|
@@ -363,6 +364,16 @@ module Zeitwerk
|
|
|
363
364
|
shadowed_files.member?(file)
|
|
364
365
|
end
|
|
365
366
|
|
|
367
|
+
#: { () -> String } -> void
|
|
368
|
+
internal def log
|
|
369
|
+
return unless logger
|
|
370
|
+
|
|
371
|
+
message = yield
|
|
372
|
+
method_name = logger.respond_to?(:debug) ? :debug : :call
|
|
373
|
+
logger.send(method_name, "Zeitwerk@#{tag}: #{message}")
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
|
|
366
377
|
# --- Class methods ---------------------------------------------------------------------------
|
|
367
378
|
|
|
368
379
|
class << self
|
|
@@ -464,7 +475,7 @@ module Zeitwerk
|
|
|
464
475
|
|
|
465
476
|
#: (String, Module) -> void
|
|
466
477
|
private def define_autoloads_for_dir(dir, parent)
|
|
467
|
-
ls(dir) do |basename, abspath, ftype|
|
|
478
|
+
@fs.ls(dir) do |basename, abspath, ftype|
|
|
468
479
|
if ftype == :file
|
|
469
480
|
basename.delete_suffix!(".rb")
|
|
470
481
|
cref = Cref.new(parent, cname_for(basename, abspath))
|
|
@@ -483,7 +494,7 @@ module Zeitwerk
|
|
|
483
494
|
#: (Zeitwerk::Cref, String) -> void
|
|
484
495
|
private def autoload_subdir(cref, subdir)
|
|
485
496
|
if autoload_path = autoload_path_set_by_me_for?(cref)
|
|
486
|
-
if
|
|
497
|
+
if @fs.rb_extension?(autoload_path)
|
|
487
498
|
# Scanning visited a Ruby file first, and now a directory for the same
|
|
488
499
|
# constant has been found. This means we are dealing with an explicit
|
|
489
500
|
# namespace whose definition was seen first.
|
|
@@ -504,7 +515,7 @@ module Zeitwerk
|
|
|
504
515
|
else
|
|
505
516
|
# For whatever reason the constant that corresponds to this namespace has
|
|
506
517
|
# already been defined, we have to recurse.
|
|
507
|
-
log
|
|
518
|
+
log { "the namespace #{cref} already exists, descending into #{subdir}" }
|
|
508
519
|
define_autoloads_for_dir(subdir, cref.get)
|
|
509
520
|
end
|
|
510
521
|
end
|
|
@@ -513,15 +524,15 @@ module Zeitwerk
|
|
|
513
524
|
private def autoload_file(cref, file)
|
|
514
525
|
if autoload_path = cref.autoload? || Registry.inceptions.registered?(cref)
|
|
515
526
|
# First autoload for a Ruby file wins, just ignore subsequent ones.
|
|
516
|
-
if
|
|
527
|
+
if @fs.rb_extension?(autoload_path)
|
|
517
528
|
shadowed_files << file
|
|
518
|
-
log
|
|
529
|
+
log { "file #{file} is ignored because #{autoload_path} has precedence" }
|
|
519
530
|
else
|
|
520
531
|
promote_namespace_from_implicit_to_explicit(dir: autoload_path, file: file, cref: cref)
|
|
521
532
|
end
|
|
522
533
|
elsif cref.defined?
|
|
523
534
|
shadowed_files << file
|
|
524
|
-
log
|
|
535
|
+
log { "file #{file} is ignored because #{cref} is already defined" }
|
|
525
536
|
else
|
|
526
537
|
define_autoload(cref, file)
|
|
527
538
|
end
|
|
@@ -535,7 +546,7 @@ module Zeitwerk
|
|
|
535
546
|
autoloads.delete(dir)
|
|
536
547
|
Registry.autoloads.unregister(dir)
|
|
537
548
|
|
|
538
|
-
log
|
|
549
|
+
log { "earlier autoload for #{cref} discarded, it is actually an explicit namespace defined in #{file}" }
|
|
539
550
|
|
|
540
551
|
# Order matters: When Module#const_added is triggered by the autoload, we
|
|
541
552
|
# don't want the namespace to be registered yet.
|
|
@@ -548,10 +559,10 @@ module Zeitwerk
|
|
|
548
559
|
cref.autoload(abspath)
|
|
549
560
|
|
|
550
561
|
if logger
|
|
551
|
-
if
|
|
552
|
-
log
|
|
562
|
+
if @fs.rb_extension?(abspath)
|
|
563
|
+
log { "autoload set for #{cref}, to be loaded from #{abspath}" }
|
|
553
564
|
else
|
|
554
|
-
log
|
|
565
|
+
log { "autoload set for #{cref}, to be autovivified from #{abspath}" }
|
|
555
566
|
end
|
|
556
567
|
end
|
|
557
568
|
|
|
@@ -595,28 +606,12 @@ module Zeitwerk
|
|
|
595
606
|
end
|
|
596
607
|
|
|
597
608
|
#: (String) -> void
|
|
598
|
-
private def
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
loader.
|
|
604
|
-
# Conflicting directories are rare, optimize for the common case.
|
|
605
|
-
next if !dir.start_with?(root_dir) && !root_dir.start_with?(dir)
|
|
606
|
-
|
|
607
|
-
dir_slash = dir + "/"
|
|
608
|
-
root_dir_slash = root_dir + "/"
|
|
609
|
-
next if !dir_slash.start_with?(root_dir_slash) && !root_dir_slash.start_with?(dir_slash)
|
|
610
|
-
|
|
611
|
-
next if ignores?(root_dir)
|
|
612
|
-
break if loader.__ignores?(dir)
|
|
613
|
-
|
|
614
|
-
require "pp" # Needed to have pretty_inspect available.
|
|
615
|
-
raise Error,
|
|
616
|
-
"loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir}," \
|
|
617
|
-
" which is already managed by\n\n#{loader.pretty_inspect}\n"
|
|
618
|
-
end
|
|
619
|
-
end
|
|
609
|
+
private def raise_if_conflicting_root_dir(root_dir)
|
|
610
|
+
if loader = Registry.conflicting_root_dir?(self, root_dir)
|
|
611
|
+
require "pp" # Needed to have pretty_inspect available.
|
|
612
|
+
raise Error,
|
|
613
|
+
"loader\n\n#{pretty_inspect}\n\nwants to manage directory #{root_dir}," \
|
|
614
|
+
" which is already managed by\n\n#{loader.pretty_inspect}\n"
|
|
620
615
|
end
|
|
621
616
|
end
|
|
622
617
|
|
|
@@ -630,7 +625,7 @@ module Zeitwerk
|
|
|
630
625
|
#: (Zeitwerk::Cref) -> void
|
|
631
626
|
private def unload_autoload(cref)
|
|
632
627
|
cref.remove
|
|
633
|
-
log
|
|
628
|
+
log { "autoload for #{cref} removed" }
|
|
634
629
|
end
|
|
635
630
|
|
|
636
631
|
#: (Zeitwerk::Cref) -> void
|
|
@@ -642,7 +637,7 @@ module Zeitwerk
|
|
|
642
637
|
# There are a few edge scenarios in which this may happen. If the constant
|
|
643
638
|
# is gone, that is OK, anyway.
|
|
644
639
|
else
|
|
645
|
-
log
|
|
640
|
+
log { "#{cref} unloaded" }
|
|
646
641
|
end
|
|
647
642
|
end
|
|
648
643
|
end
|
|
@@ -7,7 +7,7 @@ module Zeitwerk::RealModName
|
|
|
7
7
|
|
|
8
8
|
# Returns the real name of the class or module.
|
|
9
9
|
#
|
|
10
|
-
# We need this indirection
|
|
10
|
+
# We need this indirection because the `name` method can be overridden, and
|
|
11
11
|
# because in practice what we really need is the constant paths of modules
|
|
12
12
|
# with a permanent name, not so much what the user considers to be the name of
|
|
13
13
|
# a certain class or module of theirs.
|
data/lib/zeitwerk/registry.rb
CHANGED
|
@@ -44,6 +44,31 @@ module Zeitwerk
|
|
|
44
44
|
gem_loaders_by_root_file.delete_if { |_, l| l == loader }
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
#: (Zeitwerk::Loader, String) -> Zeitwerk::Loader?
|
|
48
|
+
def conflicting_root_dir?(loader, new_root_dir)
|
|
49
|
+
@mutex.synchronize do
|
|
50
|
+
loaders.each do |existing_loader|
|
|
51
|
+
next if existing_loader == loader
|
|
52
|
+
|
|
53
|
+
existing_loader.__roots.each_key do |existing_root_dir|
|
|
54
|
+
# Conflicting directories are rare, optimize for the common case.
|
|
55
|
+
next if !new_root_dir.start_with?(existing_root_dir) && !existing_root_dir.start_with?(new_root_dir)
|
|
56
|
+
|
|
57
|
+
new_root_dir_slash = new_root_dir + "/"
|
|
58
|
+
existing_root_dir_slash = existing_root_dir + "/"
|
|
59
|
+
next if !new_root_dir_slash.start_with?(existing_root_dir_slash) && !existing_root_dir_slash.start_with?(new_root_dir_slash)
|
|
60
|
+
|
|
61
|
+
next if loader.__ignores?(existing_root_dir)
|
|
62
|
+
break if existing_loader.__ignores?(new_root_dir)
|
|
63
|
+
|
|
64
|
+
return existing_loader
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
47
72
|
# This method returns always a loader, the same instance for the same root
|
|
48
73
|
# file. That is how Zeitwerk::Loader.for_gem is idempotent.
|
|
49
74
|
#
|
|
@@ -59,5 +84,6 @@ module Zeitwerk
|
|
|
59
84
|
@autoloads = Autoloads.new
|
|
60
85
|
@explicit_namespaces = ExplicitNamespaces.new
|
|
61
86
|
@inceptions = Inceptions.new
|
|
87
|
+
@mutex = Mutex.new
|
|
62
88
|
end
|
|
63
89
|
end
|
data/lib/zeitwerk/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: zeitwerk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.7.
|
|
4
|
+
version: 2.7.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Xavier Noria
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies: []
|
|
13
12
|
description: |2
|
|
14
13
|
Zeitwerk implements constant autoloading with Ruby semantics. Each gem
|
|
@@ -36,6 +35,7 @@ files:
|
|
|
36
35
|
- lib/zeitwerk/loader/callbacks.rb
|
|
37
36
|
- lib/zeitwerk/loader/config.rb
|
|
38
37
|
- lib/zeitwerk/loader/eager_load.rb
|
|
38
|
+
- lib/zeitwerk/loader/file_system.rb
|
|
39
39
|
- lib/zeitwerk/loader/helpers.rb
|
|
40
40
|
- lib/zeitwerk/null_inflector.rb
|
|
41
41
|
- lib/zeitwerk/real_mod_name.rb
|
|
@@ -53,7 +53,6 @@ metadata:
|
|
|
53
53
|
changelog_uri: https://github.com/fxn/zeitwerk/blob/main/CHANGELOG.md
|
|
54
54
|
source_code_uri: https://github.com/fxn/zeitwerk
|
|
55
55
|
bug_tracker_uri: https://github.com/fxn/zeitwerk/issues
|
|
56
|
-
post_install_message:
|
|
57
56
|
rdoc_options: []
|
|
58
57
|
require_paths:
|
|
59
58
|
- lib
|
|
@@ -68,8 +67,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
68
67
|
- !ruby/object:Gem::Version
|
|
69
68
|
version: '0'
|
|
70
69
|
requirements: []
|
|
71
|
-
rubygems_version:
|
|
72
|
-
signing_key:
|
|
70
|
+
rubygems_version: 4.0.3
|
|
73
71
|
specification_version: 4
|
|
74
72
|
summary: Efficient and thread-safe constant autoloader
|
|
75
73
|
test_files: []
|