zeitwerk 2.4.2 → 2.5.0.beta
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 +163 -60
- data/lib/zeitwerk.rb +2 -0
- data/lib/zeitwerk/autoloads.rb +69 -0
- data/lib/zeitwerk/explicit_namespace.rb +8 -2
- data/lib/zeitwerk/kernel.rb +5 -4
- data/lib/zeitwerk/loader.rb +75 -412
- data/lib/zeitwerk/loader/callbacks.rb +9 -8
- data/lib/zeitwerk/loader/config.rb +308 -0
- data/lib/zeitwerk/loader/helpers.rb +95 -0
- data/lib/zeitwerk/registry.rb +7 -7
- data/lib/zeitwerk/version.rb +1 -1
- metadata +9 -6
@@ -18,7 +18,7 @@ module Zeitwerk::Loader::Callbacks
|
|
18
18
|
raise Zeitwerk::NameError.new("expected file #{file} to define constant #{cpath}, but didn't", cref.last)
|
19
19
|
end
|
20
20
|
|
21
|
-
run_on_load_callbacks(cpath)
|
21
|
+
run_on_load_callbacks(cpath, cget(*cref), file) unless on_load_callbacks.empty?
|
22
22
|
end
|
23
23
|
|
24
24
|
# Invoked from our decorated Kernel#require when a managed directory is
|
@@ -54,7 +54,7 @@ module Zeitwerk::Loader::Callbacks
|
|
54
54
|
|
55
55
|
on_namespace_loaded(autovivified_module)
|
56
56
|
|
57
|
-
run_on_load_callbacks(cpath)
|
57
|
+
run_on_load_callbacks(cpath, autovivified_module, dir) unless on_load_callbacks.empty?
|
58
58
|
end
|
59
59
|
end
|
60
60
|
end
|
@@ -75,12 +75,13 @@ module Zeitwerk::Loader::Callbacks
|
|
75
75
|
|
76
76
|
private
|
77
77
|
|
78
|
-
# @sig (String) -> void
|
79
|
-
def run_on_load_callbacks(cpath)
|
80
|
-
#
|
81
|
-
return if on_load_callbacks.empty?
|
82
|
-
|
78
|
+
# @sig (String, Object) -> void
|
79
|
+
def run_on_load_callbacks(cpath, value, abspath)
|
80
|
+
# Order matters. If present, run the most specific one.
|
83
81
|
callbacks = reloading_enabled? ? on_load_callbacks[cpath] : on_load_callbacks.delete(cpath)
|
84
|
-
callbacks.
|
82
|
+
callbacks&.each { |c| c.call(value, abspath) }
|
83
|
+
|
84
|
+
callbacks = on_load_callbacks[:ANY]
|
85
|
+
callbacks&.each { |c| c.call(cpath, value, abspath) }
|
85
86
|
end
|
86
87
|
end
|
@@ -0,0 +1,308 @@
|
|
1
|
+
require "set"
|
2
|
+
require "securerandom"
|
3
|
+
|
4
|
+
module Zeitwerk::Loader::Config
|
5
|
+
# Absolute paths of the root directories. Stored in a hash to preserve
|
6
|
+
# order, easily handle duplicates, and also be able to have a fast lookup,
|
7
|
+
# needed for detecting nested paths.
|
8
|
+
#
|
9
|
+
# "/Users/fxn/blog/app/assets" => true,
|
10
|
+
# "/Users/fxn/blog/app/channels" => true,
|
11
|
+
# ...
|
12
|
+
#
|
13
|
+
# This is a private collection maintained by the loader. The public
|
14
|
+
# interface for it is `push_dir` and `dirs`.
|
15
|
+
#
|
16
|
+
# @private
|
17
|
+
# @sig Hash[String, true]
|
18
|
+
attr_reader :root_dirs
|
19
|
+
|
20
|
+
# @sig #camelize
|
21
|
+
attr_accessor :inflector
|
22
|
+
|
23
|
+
# Absolute paths of files, directories, or glob patterns to be totally
|
24
|
+
# ignored.
|
25
|
+
#
|
26
|
+
# @private
|
27
|
+
# @sig Set[String]
|
28
|
+
attr_reader :ignored_glob_patterns
|
29
|
+
|
30
|
+
# The actual collection of absolute file and directory names at the time the
|
31
|
+
# ignored glob patterns were expanded. Computed on setup, and recomputed on
|
32
|
+
# reload.
|
33
|
+
#
|
34
|
+
# @private
|
35
|
+
# @sig Set[String]
|
36
|
+
attr_reader :ignored_paths
|
37
|
+
|
38
|
+
# Absolute paths of directories or glob patterns to be collapsed.
|
39
|
+
#
|
40
|
+
# @private
|
41
|
+
# @sig Set[String]
|
42
|
+
attr_reader :collapse_glob_patterns
|
43
|
+
|
44
|
+
# The actual collection of absolute directory names at the time the collapse
|
45
|
+
# glob patterns were expanded. Computed on setup, and recomputed on reload.
|
46
|
+
#
|
47
|
+
# @private
|
48
|
+
# @sig Set[String]
|
49
|
+
attr_reader :collapse_dirs
|
50
|
+
|
51
|
+
# Absolute paths of files or directories not to be eager loaded.
|
52
|
+
#
|
53
|
+
# @private
|
54
|
+
# @sig Set[String]
|
55
|
+
attr_reader :eager_load_exclusions
|
56
|
+
|
57
|
+
# User-oriented callbacks to be fired when a constant is loaded.
|
58
|
+
#
|
59
|
+
# @private
|
60
|
+
# @sig Hash[String, Array[{ (Object, String) -> void }]]
|
61
|
+
# Hash[Symbol, Array[{ (String, Object, String) -> void }]]
|
62
|
+
attr_reader :on_load_callbacks
|
63
|
+
|
64
|
+
# User-oriented callbacks to be fired before constants are removed.
|
65
|
+
#
|
66
|
+
# @private
|
67
|
+
# @sig Hash[String, Array[{ (Object, String) -> void }]]
|
68
|
+
# Hash[Symbol, Array[{ (String, Object, String) -> void }]]
|
69
|
+
attr_reader :on_unload_callbacks
|
70
|
+
|
71
|
+
# @sig #call | #debug | nil
|
72
|
+
attr_accessor :logger
|
73
|
+
|
74
|
+
def initialize
|
75
|
+
@initialized_at = Time.now
|
76
|
+
@root_dirs = {}
|
77
|
+
@inflector = Zeitwerk::Inflector.new
|
78
|
+
@ignored_glob_patterns = Set.new
|
79
|
+
@ignored_paths = Set.new
|
80
|
+
@collapse_glob_patterns = Set.new
|
81
|
+
@collapse_dirs = Set.new
|
82
|
+
@eager_load_exclusions = Set.new
|
83
|
+
@reloading_enabled = false
|
84
|
+
@on_load_callbacks = {}
|
85
|
+
@on_unload_callbacks = {}
|
86
|
+
@logger = self.class.default_logger
|
87
|
+
@tag = SecureRandom.hex(3)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Pushes `path` to the list of root directories.
|
91
|
+
#
|
92
|
+
# Raises `Zeitwerk::Error` if `path` does not exist, or if another loader in
|
93
|
+
# the same process already manages that directory or one of its ascendants or
|
94
|
+
# descendants.
|
95
|
+
#
|
96
|
+
# @raise [Zeitwerk::Error]
|
97
|
+
# @sig (String | Pathname, Module) -> void
|
98
|
+
def push_dir(path, namespace: Object)
|
99
|
+
# Note that Class < Module.
|
100
|
+
unless namespace.is_a?(Module)
|
101
|
+
raise Zeitwerk::Error, "#{namespace.inspect} is not a class or module object, should be"
|
102
|
+
end
|
103
|
+
|
104
|
+
abspath = File.expand_path(path)
|
105
|
+
if dir?(abspath)
|
106
|
+
raise_if_conflicting_directory(abspath)
|
107
|
+
root_dirs[abspath] = namespace
|
108
|
+
else
|
109
|
+
raise Zeitwerk::Error, "the root directory #{abspath} does not exist"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Returns the loader's tag.
|
114
|
+
#
|
115
|
+
# Implemented as a method instead of via attr_reader for symmetry with the
|
116
|
+
# writer below.
|
117
|
+
#
|
118
|
+
# @sig () -> String
|
119
|
+
def tag
|
120
|
+
@tag
|
121
|
+
end
|
122
|
+
|
123
|
+
# Sets a tag for the loader, useful for logging.
|
124
|
+
#
|
125
|
+
# @param tag [#to_s]
|
126
|
+
# @sig (#to_s) -> void
|
127
|
+
def tag=(tag)
|
128
|
+
@tag = tag.to_s
|
129
|
+
end
|
130
|
+
|
131
|
+
# Absolute paths of the root directories. This is a read-only collection,
|
132
|
+
# please push here via `push_dir`.
|
133
|
+
#
|
134
|
+
# @sig () -> Array[String]
|
135
|
+
def dirs
|
136
|
+
root_dirs.keys.freeze
|
137
|
+
end
|
138
|
+
|
139
|
+
# You need to call this method before setup in order to be able to reload.
|
140
|
+
# There is no way to undo this, either you want to reload or you don't.
|
141
|
+
#
|
142
|
+
# @raise [Zeitwerk::Error]
|
143
|
+
# @sig () -> void
|
144
|
+
def enable_reloading
|
145
|
+
mutex.synchronize do
|
146
|
+
break if @reloading_enabled
|
147
|
+
|
148
|
+
if @setup
|
149
|
+
raise Zeitwerk::Error, "cannot enable reloading after setup"
|
150
|
+
else
|
151
|
+
@reloading_enabled = true
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# @sig () -> bool
|
157
|
+
def reloading_enabled?
|
158
|
+
@reloading_enabled
|
159
|
+
end
|
160
|
+
|
161
|
+
# Let eager load ignore the given files or directories. The constants defined
|
162
|
+
# in those files are still autoloadable.
|
163
|
+
#
|
164
|
+
# @sig (*(String | Pathname | Array[String | Pathname])) -> void
|
165
|
+
def do_not_eager_load(*paths)
|
166
|
+
mutex.synchronize { eager_load_exclusions.merge(expand_paths(paths)) }
|
167
|
+
end
|
168
|
+
|
169
|
+
# Configure files, directories, or glob patterns to be totally ignored.
|
170
|
+
#
|
171
|
+
# @sig (*(String | Pathname | Array[String | Pathname])) -> void
|
172
|
+
def ignore(*glob_patterns)
|
173
|
+
glob_patterns = expand_paths(glob_patterns)
|
174
|
+
mutex.synchronize do
|
175
|
+
ignored_glob_patterns.merge(glob_patterns)
|
176
|
+
ignored_paths.merge(expand_glob_patterns(glob_patterns))
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# Configure directories or glob patterns to be collapsed.
|
181
|
+
#
|
182
|
+
# @sig (*(String | Pathname | Array[String | Pathname])) -> void
|
183
|
+
def collapse(*glob_patterns)
|
184
|
+
glob_patterns = expand_paths(glob_patterns)
|
185
|
+
mutex.synchronize do
|
186
|
+
collapse_glob_patterns.merge(glob_patterns)
|
187
|
+
collapse_dirs.merge(expand_glob_patterns(glob_patterns))
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# Configure a block to be invoked once a certain constant path is loaded.
|
192
|
+
# Supports multiple callbacks, and if there are many, they are executed in
|
193
|
+
# the order in which they were defined.
|
194
|
+
#
|
195
|
+
# loader.on_load("SomeApiClient") do |klass, _abspath|
|
196
|
+
# klass.endpoint = "https://api.dev"
|
197
|
+
# end
|
198
|
+
#
|
199
|
+
# Can also be configured for any constant loaded:
|
200
|
+
#
|
201
|
+
# loader.on_load do |cpath, value, abspath|
|
202
|
+
# # ...
|
203
|
+
# end
|
204
|
+
#
|
205
|
+
# @raise [TypeError]
|
206
|
+
# @sig (String) { (Object, String) -> void } -> void
|
207
|
+
# (:ANY) { (String, Object, String) -> void } -> void
|
208
|
+
def on_load(cpath = :ANY, &block)
|
209
|
+
raise TypeError, "on_load only accepts strings" unless cpath.is_a?(String) || cpath == :ANY
|
210
|
+
|
211
|
+
mutex.synchronize do
|
212
|
+
(on_load_callbacks[cpath] ||= []) << block
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
# Configure a block to be invoked right before a certain constant is removed.
|
217
|
+
# Supports multiple callbacks, and if there are many, they are executed in the
|
218
|
+
# order in which they were defined.
|
219
|
+
#
|
220
|
+
# loader.on_unload("Country") do |klass, _abspath|
|
221
|
+
# klass.clear_cache
|
222
|
+
# end
|
223
|
+
#
|
224
|
+
# Can also be configured for any removed constant:
|
225
|
+
#
|
226
|
+
# loader.on_unload do |cpath, value, abspath|
|
227
|
+
# # ...
|
228
|
+
# end
|
229
|
+
#
|
230
|
+
# @raise [TypeError]
|
231
|
+
# @sig (String) { (Object) -> void } -> void
|
232
|
+
# (:ANY) { (String, Object) -> void } -> void
|
233
|
+
def on_unload(cpath = :ANY, &block)
|
234
|
+
raise TypeError, "on_unload only accepts strings" unless cpath.is_a?(String) || cpath == :ANY
|
235
|
+
|
236
|
+
mutex.synchronize do
|
237
|
+
(on_unload_callbacks[cpath] ||= []) << block
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
# Logs to `$stdout`, handy shortcut for debugging.
|
242
|
+
#
|
243
|
+
# @sig () -> void
|
244
|
+
def log!
|
245
|
+
@logger = ->(msg) { puts msg }
|
246
|
+
end
|
247
|
+
|
248
|
+
# @private
|
249
|
+
# @sig (String) -> bool
|
250
|
+
def manages?(dir)
|
251
|
+
dir = dir + "/"
|
252
|
+
ignored_paths.each do |ignored_path|
|
253
|
+
return false if dir.start_with?(ignored_path + "/")
|
254
|
+
end
|
255
|
+
|
256
|
+
root_dirs.each_key do |root_dir|
|
257
|
+
return true if root_dir.start_with?(dir) || dir.start_with?(root_dir + "/")
|
258
|
+
end
|
259
|
+
|
260
|
+
false
|
261
|
+
end
|
262
|
+
|
263
|
+
private
|
264
|
+
|
265
|
+
# @sig () -> Array[String]
|
266
|
+
def actual_root_dirs
|
267
|
+
root_dirs.reject do |root_dir, _namespace|
|
268
|
+
!dir?(root_dir) || ignored_paths.member?(root_dir)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
# @sig (String) -> bool
|
273
|
+
def root_dir?(dir)
|
274
|
+
root_dirs.key?(dir)
|
275
|
+
end
|
276
|
+
|
277
|
+
# @sig (String) -> bool
|
278
|
+
def excluded_from_eager_load?(abspath)
|
279
|
+
eager_load_exclusions.member?(abspath)
|
280
|
+
end
|
281
|
+
|
282
|
+
# @sig (String) -> bool
|
283
|
+
def collapse?(dir)
|
284
|
+
collapse_dirs.member?(dir)
|
285
|
+
end
|
286
|
+
|
287
|
+
# @sig (String | Pathname | Array[String | Pathname]) -> Array[String]
|
288
|
+
def expand_paths(paths)
|
289
|
+
paths.flatten.map! { |path| File.expand_path(path) }
|
290
|
+
end
|
291
|
+
|
292
|
+
# @sig (Array[String]) -> Array[String]
|
293
|
+
def expand_glob_patterns(glob_patterns)
|
294
|
+
# Note that Dir.glob works with regular file names just fine. That is,
|
295
|
+
# glob patterns technically need no wildcards.
|
296
|
+
glob_patterns.flat_map { |glob_pattern| Dir.glob(glob_pattern) }
|
297
|
+
end
|
298
|
+
|
299
|
+
# @sig () -> void
|
300
|
+
def recompute_ignored_paths
|
301
|
+
ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns))
|
302
|
+
end
|
303
|
+
|
304
|
+
# @sig () -> void
|
305
|
+
def recompute_collapse_dirs
|
306
|
+
collapse_dirs.replace(expand_glob_patterns(collapse_glob_patterns))
|
307
|
+
end
|
308
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module Zeitwerk::Loader::Helpers
|
2
|
+
private
|
3
|
+
|
4
|
+
# --- Logging -----------------------------------------------------------------------------------
|
5
|
+
|
6
|
+
# @sig (String) -> void
|
7
|
+
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
|
+
# @sig (String) { (String, String) -> void } -> void
|
15
|
+
def ls(dir)
|
16
|
+
Dir.each_child(dir) do |basename|
|
17
|
+
next if hidden?(basename)
|
18
|
+
|
19
|
+
abspath = File.join(dir, basename)
|
20
|
+
next if ignored_paths.member?(abspath)
|
21
|
+
|
22
|
+
# We freeze abspath because that saves allocations when passed later to
|
23
|
+
# File methods. See #125.
|
24
|
+
yield basename, abspath.freeze
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# @sig (String) -> bool
|
29
|
+
def ruby?(path)
|
30
|
+
path.end_with?(".rb")
|
31
|
+
end
|
32
|
+
|
33
|
+
# @sig (String) -> bool
|
34
|
+
def dir?(path)
|
35
|
+
File.directory?(path)
|
36
|
+
end
|
37
|
+
|
38
|
+
# @sig String -> bool
|
39
|
+
def hidden?(basename)
|
40
|
+
basename.start_with?(".")
|
41
|
+
end
|
42
|
+
|
43
|
+
# --- Constants ---------------------------------------------------------------------------------
|
44
|
+
|
45
|
+
# The autoload? predicate takes into account the ancestor chain of the
|
46
|
+
# receiver, like const_defined? and other methods in the constants API do.
|
47
|
+
#
|
48
|
+
# For example, given
|
49
|
+
#
|
50
|
+
# class A
|
51
|
+
# autoload :X, "x.rb"
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# class B < A
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
# B.autoload?(:X) returns "x.rb".
|
58
|
+
#
|
59
|
+
# We need a way to strictly check in parent ignoring ancestors.
|
60
|
+
#
|
61
|
+
# @sig (Module, Symbol) -> String?
|
62
|
+
if method(:autoload?).arity == 1
|
63
|
+
def strict_autoload_path(parent, cname)
|
64
|
+
parent.autoload?(cname) if cdef?(parent, cname)
|
65
|
+
end
|
66
|
+
else
|
67
|
+
def strict_autoload_path(parent, cname)
|
68
|
+
parent.autoload?(cname, false)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# @sig (Module, Symbol) -> String
|
73
|
+
if Symbol.method_defined?(:name)
|
74
|
+
# Symbol#name was introduced in Ruby 3.0. It returns always the same
|
75
|
+
# frozen object, so we may save a few string allocations.
|
76
|
+
def cpath(parent, cname)
|
77
|
+
Object == parent ? cname.name : "#{real_mod_name(parent)}::#{cname.name}"
|
78
|
+
end
|
79
|
+
else
|
80
|
+
def cpath(parent, cname)
|
81
|
+
Object == parent ? cname.to_s : "#{real_mod_name(parent)}::#{cname}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# @sig (Module, Symbol) -> bool
|
86
|
+
def cdef?(parent, cname)
|
87
|
+
parent.const_defined?(cname, false)
|
88
|
+
end
|
89
|
+
|
90
|
+
# @raise [NameError]
|
91
|
+
# @sig (Module, Symbol) -> Object
|
92
|
+
def cget(parent, cname)
|
93
|
+
parent.const_get(cname, false)
|
94
|
+
end
|
95
|
+
end
|
data/lib/zeitwerk/registry.rb
CHANGED
@@ -17,7 +17,7 @@ module Zeitwerk
|
|
17
17
|
# @sig Hash[String, Zeitwerk::Loader]
|
18
18
|
attr_reader :loaders_managing_gems
|
19
19
|
|
20
|
-
# Maps
|
20
|
+
# Maps absolute paths to the loaders responsible for them.
|
21
21
|
#
|
22
22
|
# This information is used by our decorated `Kernel#require` to be able to
|
23
23
|
# invoke callbacks and autovivify modules.
|
@@ -90,20 +90,20 @@ module Zeitwerk
|
|
90
90
|
|
91
91
|
# @private
|
92
92
|
# @sig (Zeitwerk::Loader, String) -> String
|
93
|
-
def register_autoload(loader,
|
94
|
-
autoloads[
|
93
|
+
def register_autoload(loader, abspath)
|
94
|
+
autoloads[abspath] = loader
|
95
95
|
end
|
96
96
|
|
97
97
|
# @private
|
98
98
|
# @sig (String) -> void
|
99
|
-
def unregister_autoload(
|
100
|
-
autoloads.delete(
|
99
|
+
def unregister_autoload(abspath)
|
100
|
+
autoloads.delete(abspath)
|
101
101
|
end
|
102
102
|
|
103
103
|
# @private
|
104
104
|
# @sig (String, String, Zeitwerk::Loader) -> void
|
105
|
-
def register_inception(cpath,
|
106
|
-
inceptions[cpath] = [
|
105
|
+
def register_inception(cpath, abspath, loader)
|
106
|
+
inceptions[cpath] = [abspath, loader]
|
107
107
|
end
|
108
108
|
|
109
109
|
# @private
|