zeitwerk 2.4.2 → 2.5.0
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 +287 -69
- data/lib/zeitwerk/autoloads.rb +71 -0
- data/lib/zeitwerk/error.rb +2 -0
- data/lib/zeitwerk/explicit_namespace.rb +11 -3
- data/lib/zeitwerk/kernel.rb +6 -5
- data/lib/zeitwerk/loader/callbacks.rb +18 -12
- data/lib/zeitwerk/loader/config.rb +321 -0
- data/lib/zeitwerk/loader/helpers.rb +97 -0
- data/lib/zeitwerk/loader.rb +114 -419
- data/lib/zeitwerk/real_mod_name.rb +2 -0
- data/lib/zeitwerk/registry.rb +16 -7
- data/lib/zeitwerk/version.rb +1 -1
- data/lib/zeitwerk.rb +13 -0
- metadata +7 -4
data/lib/zeitwerk/loader.rb
CHANGED
@@ -5,78 +5,31 @@ require "securerandom"
|
|
5
5
|
|
6
6
|
module Zeitwerk
|
7
7
|
class Loader
|
8
|
+
require_relative "loader/helpers"
|
8
9
|
require_relative "loader/callbacks"
|
9
|
-
|
10
|
-
include RealModName
|
11
|
-
|
12
|
-
# @sig String
|
13
|
-
attr_reader :tag
|
10
|
+
require_relative "loader/config"
|
14
11
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
attr_accessor :logger
|
20
|
-
|
21
|
-
# Absolute paths of the root directories. Stored in a hash to preserve
|
22
|
-
# order, easily handle duplicates, and also be able to have a fast lookup,
|
23
|
-
# needed for detecting nested paths.
|
24
|
-
#
|
25
|
-
# "/Users/fxn/blog/app/assets" => true,
|
26
|
-
# "/Users/fxn/blog/app/channels" => true,
|
27
|
-
# ...
|
28
|
-
#
|
29
|
-
# This is a private collection maintained by the loader. The public
|
30
|
-
# interface for it is `push_dir` and `dirs`.
|
31
|
-
#
|
32
|
-
# @private
|
33
|
-
# @sig Hash[String, true]
|
34
|
-
attr_reader :root_dirs
|
35
|
-
|
36
|
-
# Absolute paths of files or directories that have to be preloaded.
|
37
|
-
#
|
38
|
-
# @private
|
39
|
-
# @sig Array[String]
|
40
|
-
attr_reader :preloads
|
41
|
-
|
42
|
-
# Absolute paths of files, directories, or glob patterns to be totally
|
43
|
-
# ignored.
|
44
|
-
#
|
45
|
-
# @private
|
46
|
-
# @sig Set[String]
|
47
|
-
attr_reader :ignored_glob_patterns
|
12
|
+
include RealModName
|
13
|
+
include Callbacks
|
14
|
+
include Helpers
|
15
|
+
include Config
|
48
16
|
|
49
|
-
#
|
50
|
-
#
|
51
|
-
# reload.
|
17
|
+
# Keeps track of autoloads defined by the loader which have not been
|
18
|
+
# executed so far.
|
52
19
|
#
|
53
|
-
#
|
54
|
-
# @sig Set[String]
|
55
|
-
attr_reader :ignored_paths
|
56
|
-
|
57
|
-
# Absolute paths of directories or glob patterns to be collapsed.
|
20
|
+
# This metadata helps us implement a few things:
|
58
21
|
#
|
59
|
-
#
|
60
|
-
#
|
61
|
-
|
62
|
-
|
63
|
-
# The actual collection of absolute directory names at the time the collapse
|
64
|
-
# glob patterns were expanded. Computed on setup, and recomputed on reload.
|
22
|
+
# 1. When autoloads are triggered, ensure they define the expected constant
|
23
|
+
# and invoke user callbacks. If reloading is enabled, remember cref and
|
24
|
+
# abspath for later unloading logic.
|
65
25
|
#
|
66
|
-
#
|
67
|
-
# @sig Set[String]
|
68
|
-
attr_reader :collapse_dirs
|
69
|
-
|
70
|
-
# Maps real absolute paths for which an autoload has been set ---and not
|
71
|
-
# executed--- to their corresponding parent class or module and constant
|
72
|
-
# name.
|
26
|
+
# 2. When unloading, remove autoloads that have not been executed.
|
73
27
|
#
|
74
|
-
#
|
75
|
-
#
|
76
|
-
# ...
|
28
|
+
# 3. Eager load with a recursive const_get, rather than a recursive require,
|
29
|
+
# for consistency with lazy loading.
|
77
30
|
#
|
78
31
|
# @private
|
79
|
-
# @sig
|
32
|
+
# @sig Zeitwerk::Autoloads
|
80
33
|
attr_reader :autoloads
|
81
34
|
|
82
35
|
# We keep track of autoloaded directories to remove them from the registry
|
@@ -93,8 +46,8 @@ module Zeitwerk
|
|
93
46
|
#
|
94
47
|
# "Admin::Role" => [".../admin/role.rb", [Admin, :Role]]
|
95
48
|
#
|
96
|
-
# The cpath as key helps implementing unloadable_cpath? The
|
97
|
-
#
|
49
|
+
# The cpath as key helps implementing unloadable_cpath? The file name is
|
50
|
+
# stored in order to be able to delete it from $LOADED_FEATURES, and the
|
98
51
|
# pair [Module, Symbol] is used to remove_const the constant from the class
|
99
52
|
# or module object.
|
100
53
|
#
|
@@ -123,15 +76,6 @@ module Zeitwerk
|
|
123
76
|
# @sig Hash[String, Array[String]]
|
124
77
|
attr_reader :lazy_subdirs
|
125
78
|
|
126
|
-
# Absolute paths of files or directories not to be eager loaded.
|
127
|
-
#
|
128
|
-
# @private
|
129
|
-
# @sig Set[String]
|
130
|
-
attr_reader :eager_load_exclusions
|
131
|
-
|
132
|
-
# User-oriented callbacks to be fired when a constant is loaded.
|
133
|
-
attr_reader :on_load_callbacks
|
134
|
-
|
135
79
|
# @private
|
136
80
|
# @sig Mutex
|
137
81
|
attr_reader :mutex
|
@@ -141,150 +85,21 @@ module Zeitwerk
|
|
141
85
|
attr_reader :mutex2
|
142
86
|
|
143
87
|
def initialize
|
144
|
-
|
145
|
-
|
146
|
-
@tag = SecureRandom.hex(3)
|
147
|
-
@inflector = Inflector.new
|
148
|
-
@logger = self.class.default_logger
|
149
|
-
|
150
|
-
@root_dirs = {}
|
151
|
-
@preloads = []
|
152
|
-
@ignored_glob_patterns = Set.new
|
153
|
-
@ignored_paths = Set.new
|
154
|
-
@collapse_glob_patterns = Set.new
|
155
|
-
@collapse_dirs = Set.new
|
156
|
-
@autoloads = {}
|
157
|
-
@autoloaded_dirs = []
|
158
|
-
@to_unload = {}
|
159
|
-
@lazy_subdirs = {}
|
160
|
-
@eager_load_exclusions = Set.new
|
161
|
-
@on_load_callbacks = {}
|
162
|
-
|
163
|
-
# TODO: find a better name for these mutexes.
|
164
|
-
@mutex = Mutex.new
|
165
|
-
@mutex2 = Mutex.new
|
166
|
-
@setup = false
|
167
|
-
@eager_loaded = false
|
168
|
-
|
169
|
-
@reloading_enabled = false
|
170
|
-
|
171
|
-
Registry.register_loader(self)
|
172
|
-
end
|
173
|
-
|
174
|
-
# Sets a tag for the loader, useful for logging.
|
175
|
-
#
|
176
|
-
# @param tag [#to_s]
|
177
|
-
# @sig (#to_s) -> void
|
178
|
-
def tag=(tag)
|
179
|
-
@tag = tag.to_s
|
180
|
-
end
|
181
|
-
|
182
|
-
# Absolute paths of the root directories. This is a read-only collection,
|
183
|
-
# please push here via `push_dir`.
|
184
|
-
#
|
185
|
-
# @sig () -> Array[String]
|
186
|
-
def dirs
|
187
|
-
root_dirs.keys.freeze
|
188
|
-
end
|
189
|
-
|
190
|
-
# Pushes `path` to the list of root directories.
|
191
|
-
#
|
192
|
-
# Raises `Zeitwerk::Error` if `path` does not exist, or if another loader in
|
193
|
-
# the same process already manages that directory or one of its ascendants
|
194
|
-
# or descendants.
|
195
|
-
#
|
196
|
-
# @raise [Zeitwerk::Error]
|
197
|
-
# @sig (String | Pathname, Module) -> void
|
198
|
-
def push_dir(path, namespace: Object)
|
199
|
-
# Note that Class < Module.
|
200
|
-
unless namespace.is_a?(Module)
|
201
|
-
raise Error, "#{namespace.inspect} is not a class or module object, should be"
|
202
|
-
end
|
203
|
-
|
204
|
-
abspath = File.expand_path(path)
|
205
|
-
if dir?(abspath)
|
206
|
-
raise_if_conflicting_directory(abspath)
|
207
|
-
root_dirs[abspath] = namespace
|
208
|
-
else
|
209
|
-
raise Error, "the root directory #{abspath} does not exist"
|
210
|
-
end
|
211
|
-
end
|
212
|
-
|
213
|
-
# You need to call this method before setup in order to be able to reload.
|
214
|
-
# There is no way to undo this, either you want to reload or you don't.
|
215
|
-
#
|
216
|
-
# @raise [Zeitwerk::Error]
|
217
|
-
# @sig () -> void
|
218
|
-
def enable_reloading
|
219
|
-
mutex.synchronize do
|
220
|
-
break if @reloading_enabled
|
221
|
-
|
222
|
-
if @setup
|
223
|
-
raise Error, "cannot enable reloading after setup"
|
224
|
-
else
|
225
|
-
@reloading_enabled = true
|
226
|
-
end
|
227
|
-
end
|
228
|
-
end
|
229
|
-
|
230
|
-
# @sig () -> bool
|
231
|
-
def reloading_enabled?
|
232
|
-
@reloading_enabled
|
233
|
-
end
|
234
|
-
|
235
|
-
# Files or directories to be preloaded instead of lazy loaded.
|
236
|
-
#
|
237
|
-
# @sig (*(String | Pathname | Array[String | Pathname])) -> void
|
238
|
-
def preload(*paths)
|
239
|
-
mutex.synchronize do
|
240
|
-
expand_paths(paths).each do |abspath|
|
241
|
-
preloads << abspath
|
242
|
-
do_preload_abspath(abspath) if @setup
|
243
|
-
end
|
244
|
-
end
|
245
|
-
end
|
88
|
+
super
|
246
89
|
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
end
|
256
|
-
end
|
257
|
-
|
258
|
-
# Configure directories or glob patterns to be collapsed.
|
259
|
-
#
|
260
|
-
# @sig (*(String | Pathname | Array[String | Pathname])) -> void
|
261
|
-
def collapse(*glob_patterns)
|
262
|
-
glob_patterns = expand_paths(glob_patterns)
|
263
|
-
mutex.synchronize do
|
264
|
-
collapse_glob_patterns.merge(glob_patterns)
|
265
|
-
collapse_dirs.merge(expand_glob_patterns(glob_patterns))
|
266
|
-
end
|
267
|
-
end
|
90
|
+
@autoloads = Autoloads.new
|
91
|
+
@autoloaded_dirs = []
|
92
|
+
@to_unload = {}
|
93
|
+
@lazy_subdirs = Hash.new { |h, cpath| h[cpath] = [] }
|
94
|
+
@mutex = Mutex.new
|
95
|
+
@mutex2 = Mutex.new
|
96
|
+
@setup = false
|
97
|
+
@eager_loaded = false
|
268
98
|
|
269
|
-
|
270
|
-
# Supports multiple callbacks, and if there are many, they are executed in
|
271
|
-
# the order in which they were defined.
|
272
|
-
#
|
273
|
-
# loader.on_load("SomeApiClient") do
|
274
|
-
# SomeApiClient.endpoint = "https://api.dev"
|
275
|
-
# end
|
276
|
-
#
|
277
|
-
# @raise [TypeError]
|
278
|
-
# @sig (String) { () -> void } -> void
|
279
|
-
def on_load(cpath, &block)
|
280
|
-
raise TypeError, "on_load only accepts strings" unless cpath.is_a?(String)
|
281
|
-
|
282
|
-
mutex.synchronize do
|
283
|
-
(on_load_callbacks[cpath] ||= []) << block
|
284
|
-
end
|
99
|
+
Registry.register_loader(self)
|
285
100
|
end
|
286
101
|
|
287
|
-
# Sets autoloads in the root namespace
|
102
|
+
# Sets autoloads in the root namespace.
|
288
103
|
#
|
289
104
|
# @sig () -> void
|
290
105
|
def setup
|
@@ -294,7 +109,8 @@ module Zeitwerk
|
|
294
109
|
actual_root_dirs.each do |root_dir, namespace|
|
295
110
|
set_autoloads_in_dir(root_dir, namespace)
|
296
111
|
end
|
297
|
-
|
112
|
+
|
113
|
+
on_setup_callbacks.each(&:call)
|
298
114
|
|
299
115
|
@setup = true
|
300
116
|
end
|
@@ -307,7 +123,10 @@ module Zeitwerk
|
|
307
123
|
# else, they are eligible for garbage collection, which would effectively
|
308
124
|
# unload them.
|
309
125
|
#
|
310
|
-
#
|
126
|
+
# This method is public but undocumented. Main interface is `reload`, which
|
127
|
+
# means `unload` + `setup`. This one is avaiable to be used together with
|
128
|
+
# `unregister`, which is undocumented too.
|
129
|
+
#
|
311
130
|
# @sig () -> void
|
312
131
|
def unload
|
313
132
|
mutex.synchronize do
|
@@ -319,21 +138,26 @@ module Zeitwerk
|
|
319
138
|
# is enough.
|
320
139
|
unloaded_files = Set.new
|
321
140
|
|
322
|
-
autoloads.each do |
|
141
|
+
autoloads.each do |(parent, cname), abspath|
|
323
142
|
if parent.autoload?(cname)
|
324
143
|
unload_autoload(parent, cname)
|
325
144
|
else
|
326
145
|
# Could happen if loaded with require_relative. That is unsupported,
|
327
146
|
# and the constant path would escape unloadable_cpath? This is just
|
328
147
|
# defensive code to clean things up as much as we are able to.
|
329
|
-
unload_cref(parent, cname)
|
330
|
-
unloaded_files.add(
|
148
|
+
unload_cref(parent, cname)
|
149
|
+
unloaded_files.add(abspath) if ruby?(abspath)
|
331
150
|
end
|
332
151
|
end
|
333
152
|
|
334
|
-
to_unload.
|
335
|
-
|
336
|
-
|
153
|
+
to_unload.each do |cpath, (abspath, (parent, cname))|
|
154
|
+
unless on_unload_callbacks.empty?
|
155
|
+
value = parent.const_get(cname)
|
156
|
+
run_on_unload_callbacks(cpath, value, abspath)
|
157
|
+
end
|
158
|
+
|
159
|
+
unload_cref(parent, cname)
|
160
|
+
unloaded_files.add(abspath) if ruby?(abspath)
|
337
161
|
end
|
338
162
|
|
339
163
|
unless unloaded_files.empty?
|
@@ -357,7 +181,7 @@ module Zeitwerk
|
|
357
181
|
lazy_subdirs.clear
|
358
182
|
|
359
183
|
Registry.on_unload(self)
|
360
|
-
ExplicitNamespace.
|
184
|
+
ExplicitNamespace.unregister_loader(self)
|
361
185
|
|
362
186
|
@setup = false
|
363
187
|
@eager_loaded = false
|
@@ -386,34 +210,39 @@ module Zeitwerk
|
|
386
210
|
# Eager loads all files in the root directories, recursively. Files do not
|
387
211
|
# need to be in `$LOAD_PATH`, absolute file names are used. Ignored files
|
388
212
|
# are not eager loaded. You can opt-out specifically in specific files and
|
389
|
-
# directories with `do_not_eager_load
|
213
|
+
# directories with `do_not_eager_load`, and that can be overridden passing
|
214
|
+
# `force: true`.
|
390
215
|
#
|
391
|
-
# @sig () -> void
|
392
|
-
def eager_load
|
216
|
+
# @sig (true | false) -> void
|
217
|
+
def eager_load(force: false)
|
393
218
|
mutex.synchronize do
|
394
219
|
break if @eager_loaded
|
395
220
|
|
221
|
+
log("eager load start") if logger
|
222
|
+
|
223
|
+
honour_exclusions = !force
|
224
|
+
|
396
225
|
queue = []
|
397
226
|
actual_root_dirs.each do |root_dir, namespace|
|
398
|
-
queue << [namespace, root_dir] unless
|
227
|
+
queue << [namespace, root_dir] unless honour_exclusions && excluded_from_eager_load?(root_dir)
|
399
228
|
end
|
400
229
|
|
401
230
|
while to_eager_load = queue.shift
|
402
231
|
namespace, dir = to_eager_load
|
403
232
|
|
404
233
|
ls(dir) do |basename, abspath|
|
405
|
-
next if
|
234
|
+
next if honour_exclusions && excluded_from_eager_load?(abspath)
|
406
235
|
|
407
236
|
if ruby?(abspath)
|
408
|
-
if cref = autoloads
|
409
|
-
|
237
|
+
if cref = autoloads.cref_for(abspath)
|
238
|
+
cget(*cref)
|
410
239
|
end
|
411
240
|
elsif dir?(abspath) && !root_dirs.key?(abspath)
|
412
|
-
if
|
241
|
+
if collapse?(abspath)
|
413
242
|
queue << [namespace, abspath]
|
414
243
|
else
|
415
244
|
cname = inflector.camelize(basename, abspath)
|
416
|
-
queue << [namespace
|
245
|
+
queue << [cget(namespace, cname), abspath]
|
417
246
|
end
|
418
247
|
end
|
419
248
|
end
|
@@ -425,15 +254,9 @@ module Zeitwerk
|
|
425
254
|
autoloaded_dirs.clear
|
426
255
|
|
427
256
|
@eager_loaded = true
|
428
|
-
end
|
429
|
-
end
|
430
257
|
|
431
|
-
|
432
|
-
|
433
|
-
#
|
434
|
-
# @sig (*(String | Pathname | Array[String | Pathname])) -> void
|
435
|
-
def do_not_eager_load(*paths)
|
436
|
-
mutex.synchronize { eager_load_exclusions.merge(expand_paths(paths)) }
|
258
|
+
log("eager load end") if logger
|
259
|
+
end
|
437
260
|
end
|
438
261
|
|
439
262
|
# Says if the given constant path would be unloaded on reload. This
|
@@ -452,26 +275,13 @@ module Zeitwerk
|
|
452
275
|
to_unload.keys.freeze
|
453
276
|
end
|
454
277
|
|
455
|
-
#
|
278
|
+
# This is a dangerous method.
|
456
279
|
#
|
280
|
+
# @experimental
|
457
281
|
# @sig () -> void
|
458
|
-
def
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
# @private
|
463
|
-
# @sig (String) -> bool
|
464
|
-
def manages?(dir)
|
465
|
-
dir = dir + "/"
|
466
|
-
ignored_paths.each do |ignored_path|
|
467
|
-
return false if dir.start_with?(ignored_path + "/")
|
468
|
-
end
|
469
|
-
|
470
|
-
root_dirs.each_key do |root_dir|
|
471
|
-
return true if root_dir.start_with?(dir) || dir.start_with?(root_dir + "/")
|
472
|
-
end
|
473
|
-
|
474
|
-
false
|
282
|
+
def unregister
|
283
|
+
Registry.unregister_loader(self)
|
284
|
+
ExplicitNamespace.unregister_loader(self)
|
475
285
|
end
|
476
286
|
|
477
287
|
# --- Class methods ---------------------------------------------------------------------------
|
@@ -521,19 +331,12 @@ module Zeitwerk
|
|
521
331
|
|
522
332
|
private # -------------------------------------------------------------------------------------
|
523
333
|
|
524
|
-
# @sig () -> Array[String]
|
525
|
-
def actual_root_dirs
|
526
|
-
root_dirs.reject do |root_dir, _namespace|
|
527
|
-
!dir?(root_dir) || ignored_paths.member?(root_dir)
|
528
|
-
end
|
529
|
-
end
|
530
|
-
|
531
334
|
# @sig (String, Module) -> void
|
532
335
|
def set_autoloads_in_dir(dir, parent)
|
533
336
|
ls(dir) do |basename, abspath|
|
534
337
|
begin
|
535
338
|
if ruby?(basename)
|
536
|
-
basename
|
339
|
+
basename.delete_suffix!(".rb")
|
537
340
|
cname = inflector.camelize(basename, abspath).to_sym
|
538
341
|
autoload_file(parent, cname, abspath)
|
539
342
|
elsif dir?(abspath)
|
@@ -543,9 +346,9 @@ module Zeitwerk
|
|
543
346
|
# To resolve the ambiguity file name -> constant path this introduces,
|
544
347
|
# the `app/models/concerns` directory is totally ignored as a namespace,
|
545
348
|
# it counts only as root. The guard checks that.
|
546
|
-
unless
|
349
|
+
unless root_dir?(abspath)
|
547
350
|
cname = inflector.camelize(basename, abspath).to_sym
|
548
|
-
if
|
351
|
+
if collapse?(abspath)
|
549
352
|
set_autoloads_in_dir(abspath, parent)
|
550
353
|
else
|
551
354
|
autoload_subdir(parent, cname, abspath)
|
@@ -573,27 +376,28 @@ module Zeitwerk
|
|
573
376
|
|
574
377
|
# @sig (Module, Symbol, String) -> void
|
575
378
|
def autoload_subdir(parent, cname, subdir)
|
576
|
-
if autoload_path =
|
379
|
+
if autoload_path = autoloads.abspath_for(parent, cname)
|
577
380
|
cpath = cpath(parent, cname)
|
578
381
|
register_explicit_namespace(cpath) if ruby?(autoload_path)
|
579
382
|
# We do not need to issue another autoload, the existing one is enough
|
580
383
|
# no matter if it is for a file or a directory. Just remember the
|
581
384
|
# subdirectory has to be visited if the namespace is used.
|
582
|
-
|
385
|
+
lazy_subdirs[cpath] << subdir
|
583
386
|
elsif !cdef?(parent, cname)
|
584
387
|
# First time we find this namespace, set an autoload for it.
|
585
|
-
|
388
|
+
lazy_subdirs[cpath(parent, cname)] << subdir
|
586
389
|
set_autoload(parent, cname, subdir)
|
587
390
|
else
|
588
391
|
# For whatever reason the constant that corresponds to this namespace has
|
589
392
|
# already been defined, we have to recurse.
|
590
|
-
|
393
|
+
log("the namespace #{cpath(parent, cname)} already exists, descending into #{subdir}") if logger
|
394
|
+
set_autoloads_in_dir(subdir, cget(parent, cname))
|
591
395
|
end
|
592
396
|
end
|
593
397
|
|
594
398
|
# @sig (Module, Symbol, String) -> void
|
595
399
|
def autoload_file(parent, cname, file)
|
596
|
-
if autoload_path =
|
400
|
+
if autoload_path = strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
|
597
401
|
# First autoload for a Ruby file wins, just ignore subsequent ones.
|
598
402
|
if ruby?(autoload_path)
|
599
403
|
log("file #{file} is ignored because #{autoload_path} has precedence") if logger
|
@@ -620,163 +424,32 @@ module Zeitwerk
|
|
620
424
|
autoloads.delete(dir)
|
621
425
|
Registry.unregister_autoload(dir)
|
622
426
|
|
427
|
+
log("earlier autoload for #{cpath(parent, cname)} discarded, it is actually an explicit namespace defined in #{file}") if logger
|
428
|
+
|
623
429
|
set_autoload(parent, cname, file)
|
624
430
|
register_explicit_namespace(cpath(parent, cname))
|
625
431
|
end
|
626
432
|
|
627
433
|
# @sig (Module, Symbol, String) -> void
|
628
434
|
def set_autoload(parent, cname, abspath)
|
629
|
-
|
630
|
-
|
631
|
-
# be able to do a lookup later in Kernel#require for manual require calls.
|
632
|
-
#
|
633
|
-
# We freeze realpath because that saves allocations in Module#autoload.
|
634
|
-
# See #125.
|
635
|
-
realpath = File.realpath(abspath).freeze
|
636
|
-
parent.autoload(cname, realpath)
|
435
|
+
autoloads.define(parent, cname, abspath)
|
436
|
+
|
637
437
|
if logger
|
638
|
-
if ruby?(
|
639
|
-
log("autoload set for #{cpath(parent, cname)}, to be loaded from #{
|
438
|
+
if ruby?(abspath)
|
439
|
+
log("autoload set for #{cpath(parent, cname)}, to be loaded from #{abspath}")
|
640
440
|
else
|
641
|
-
log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{
|
441
|
+
log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{abspath}")
|
642
442
|
end
|
643
443
|
end
|
644
444
|
|
645
|
-
|
646
|
-
Registry.register_autoload(self, realpath)
|
445
|
+
Registry.register_autoload(self, abspath)
|
647
446
|
|
648
447
|
# See why in the documentation of Zeitwerk::Registry.inceptions.
|
649
448
|
unless parent.autoload?(cname)
|
650
|
-
Registry.register_inception(cpath(parent, cname),
|
651
|
-
end
|
652
|
-
end
|
653
|
-
|
654
|
-
# @sig (Module, Symbol) -> String?
|
655
|
-
def autoload_for?(parent, cname)
|
656
|
-
strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
|
657
|
-
end
|
658
|
-
|
659
|
-
# The autoload? predicate takes into account the ancestor chain of the
|
660
|
-
# receiver, like const_defined? and other methods in the constants API do.
|
661
|
-
#
|
662
|
-
# For example, given
|
663
|
-
#
|
664
|
-
# class A
|
665
|
-
# autoload :X, "x.rb"
|
666
|
-
# end
|
667
|
-
#
|
668
|
-
# class B < A
|
669
|
-
# end
|
670
|
-
#
|
671
|
-
# B.autoload?(:X) returns "x.rb".
|
672
|
-
#
|
673
|
-
# We need a way to strictly check in parent ignoring ancestors.
|
674
|
-
#
|
675
|
-
# @sig (Module, Symbol) -> String?
|
676
|
-
if method(:autoload?).arity == 1
|
677
|
-
def strict_autoload_path(parent, cname)
|
678
|
-
parent.autoload?(cname) if cdef?(parent, cname)
|
679
|
-
end
|
680
|
-
else
|
681
|
-
def strict_autoload_path(parent, cname)
|
682
|
-
parent.autoload?(cname, false)
|
683
|
-
end
|
684
|
-
end
|
685
|
-
|
686
|
-
# This method is called this way because I prefer `preload` to be the method
|
687
|
-
# name to configure preloads in the public interface.
|
688
|
-
#
|
689
|
-
# @sig () -> void
|
690
|
-
def do_preload
|
691
|
-
preloads.each do |abspath|
|
692
|
-
do_preload_abspath(abspath)
|
693
|
-
end
|
694
|
-
end
|
695
|
-
|
696
|
-
# @sig (String) -> void
|
697
|
-
def do_preload_abspath(abspath)
|
698
|
-
if ruby?(abspath)
|
699
|
-
do_preload_file(abspath)
|
700
|
-
elsif dir?(abspath)
|
701
|
-
do_preload_dir(abspath)
|
449
|
+
Registry.register_inception(cpath(parent, cname), abspath, self)
|
702
450
|
end
|
703
451
|
end
|
704
452
|
|
705
|
-
# @sig (String) -> void
|
706
|
-
def do_preload_dir(dir)
|
707
|
-
ls(dir) do |_basename, abspath|
|
708
|
-
do_preload_abspath(abspath)
|
709
|
-
end
|
710
|
-
end
|
711
|
-
|
712
|
-
# @sig (String) -> bool
|
713
|
-
def do_preload_file(file)
|
714
|
-
log("preloading #{file}") if logger
|
715
|
-
require file
|
716
|
-
end
|
717
|
-
|
718
|
-
# @sig (Module, Symbol) -> String
|
719
|
-
def cpath(parent, cname)
|
720
|
-
parent.equal?(Object) ? cname.to_s : "#{real_mod_name(parent)}::#{cname}"
|
721
|
-
end
|
722
|
-
|
723
|
-
# @sig (String) { (String, String) -> void } -> void
|
724
|
-
def ls(dir)
|
725
|
-
Dir.foreach(dir) do |basename|
|
726
|
-
next if basename.start_with?(".")
|
727
|
-
|
728
|
-
abspath = File.join(dir, basename)
|
729
|
-
next if ignored_paths.member?(abspath)
|
730
|
-
|
731
|
-
# We freeze abspath because that saves allocations when passed later to
|
732
|
-
# File methods. See #125.
|
733
|
-
yield basename, abspath.freeze
|
734
|
-
end
|
735
|
-
end
|
736
|
-
|
737
|
-
# @sig (String) -> bool
|
738
|
-
def ruby?(path)
|
739
|
-
path.end_with?(".rb")
|
740
|
-
end
|
741
|
-
|
742
|
-
# @sig (String) -> bool
|
743
|
-
def dir?(path)
|
744
|
-
File.directory?(path)
|
745
|
-
end
|
746
|
-
|
747
|
-
# @sig (String | Pathname | Array[String | Pathname]) -> Array[String]
|
748
|
-
def expand_paths(paths)
|
749
|
-
paths.flatten.map! { |path| File.expand_path(path) }
|
750
|
-
end
|
751
|
-
|
752
|
-
# @sig (Array[String]) -> Array[String]
|
753
|
-
def expand_glob_patterns(glob_patterns)
|
754
|
-
# Note that Dir.glob works with regular file names just fine. That is,
|
755
|
-
# glob patterns technically need no wildcards.
|
756
|
-
glob_patterns.flat_map { |glob_pattern| Dir.glob(glob_pattern) }
|
757
|
-
end
|
758
|
-
|
759
|
-
# @sig () -> void
|
760
|
-
def recompute_ignored_paths
|
761
|
-
ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns))
|
762
|
-
end
|
763
|
-
|
764
|
-
# @sig () -> void
|
765
|
-
def recompute_collapse_dirs
|
766
|
-
collapse_dirs.replace(expand_glob_patterns(collapse_glob_patterns))
|
767
|
-
end
|
768
|
-
|
769
|
-
# @sig (String) -> void
|
770
|
-
def log(message)
|
771
|
-
method_name = logger.respond_to?(:debug) ? :debug : :call
|
772
|
-
logger.send(method_name, "Zeitwerk@#{tag}: #{message}")
|
773
|
-
end
|
774
|
-
|
775
|
-
# @sig (Module, Symbol) -> bool
|
776
|
-
def cdef?(parent, cname)
|
777
|
-
parent.const_defined?(cname, false)
|
778
|
-
end
|
779
|
-
|
780
453
|
# @sig (String) -> void
|
781
454
|
def register_explicit_namespace(cpath)
|
782
455
|
ExplicitNamespace.register(cpath, self)
|
@@ -786,17 +459,33 @@ module Zeitwerk
|
|
786
459
|
def raise_if_conflicting_directory(dir)
|
787
460
|
self.class.mutex.synchronize do
|
788
461
|
Registry.loaders.each do |loader|
|
789
|
-
if loader
|
790
|
-
|
791
|
-
|
792
|
-
|
793
|
-
|
794
|
-
|
462
|
+
next if loader == self
|
463
|
+
next if loader.ignores?(dir)
|
464
|
+
|
465
|
+
dir = dir + "/"
|
466
|
+
loader.root_dirs.each do |root_dir, _namespace|
|
467
|
+
next if ignores?(root_dir)
|
468
|
+
|
469
|
+
root_dir = root_dir + "/"
|
470
|
+
if dir.start_with?(root_dir) || root_dir.start_with?(dir)
|
471
|
+
require "pp" # Needed for pretty_inspect, even in Ruby 2.5.
|
472
|
+
raise Error,
|
473
|
+
"loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir.chop}," \
|
474
|
+
" which is already managed by\n\n#{loader.pretty_inspect}\n"
|
475
|
+
EOS
|
476
|
+
end
|
795
477
|
end
|
796
478
|
end
|
797
479
|
end
|
798
480
|
end
|
799
481
|
|
482
|
+
# @sig (String, Object, String) -> void
|
483
|
+
def run_on_unload_callbacks(cpath, value, abspath)
|
484
|
+
# Order matters. If present, run the most specific one.
|
485
|
+
on_unload_callbacks[cpath]&.each { |c| c.call(value, abspath) }
|
486
|
+
on_unload_callbacks[:ANY]&.each { |c| c.call(cpath, value, abspath) }
|
487
|
+
end
|
488
|
+
|
800
489
|
# @sig (Module, Symbol) -> void
|
801
490
|
def unload_autoload(parent, cname)
|
802
491
|
parent.__send__(:remove_const, cname)
|
@@ -805,7 +494,13 @@ module Zeitwerk
|
|
805
494
|
|
806
495
|
# @sig (Module, Symbol) -> void
|
807
496
|
def unload_cref(parent, cname)
|
497
|
+
# Let's optimistically remove_const. The way we use it, this is going to
|
498
|
+
# succeed always if all is good.
|
808
499
|
parent.__send__(:remove_const, cname)
|
500
|
+
rescue ::NameError
|
501
|
+
# There are a few edge scenarios in which this may happen. If the constant
|
502
|
+
# is gone, that is OK, anyway.
|
503
|
+
else
|
809
504
|
log("#{cpath(parent, cname)} unloaded") if logger
|
810
505
|
end
|
811
506
|
end
|