opal-zeitwerk 0.3.0 → 0.4.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/MIT-LICENSE +20 -20
- data/README.md +1073 -466
- data/lib/opal/zeitwerk/version.rb +6 -6
- data/lib/opal-zeitwerk.rb +4 -4
- data/opal/zeitwerk/error.rb +10 -10
- data/opal/zeitwerk/explicit_namespace.rb +78 -71
- data/opal/zeitwerk/gem_inflector.rb +15 -0
- data/opal/zeitwerk/inflector.rb +44 -47
- data/opal/zeitwerk/kernel.rb +64 -32
- data/opal/zeitwerk/loader/callbacks.rb +88 -58
- data/opal/zeitwerk/loader/config.rb +301 -0
- data/opal/zeitwerk/loader/helpers.rb +115 -0
- data/opal/zeitwerk/loader.rb +170 -435
- data/opal/zeitwerk/real_mod_name.rb +20 -21
- data/opal/zeitwerk/registry.rb +143 -121
- data/opal/zeitwerk.rb +14 -13
- metadata +8 -5
data/opal/zeitwerk/loader.rb
CHANGED
@@ -1,55 +1,17 @@
|
|
1
1
|
require "set"
|
2
|
-
require "securerandom"
|
3
2
|
|
4
3
|
module Zeitwerk
|
5
4
|
class Loader
|
5
|
+
require_relative "loader/helpers"
|
6
6
|
require_relative "loader/callbacks"
|
7
|
-
|
8
|
-
include RealModName
|
9
|
-
|
10
|
-
# @return [String]
|
11
|
-
attr_reader :tag
|
12
|
-
|
13
|
-
# @return [#camelize]
|
14
|
-
attr_accessor :inflector
|
15
|
-
|
16
|
-
# Absolute paths of the root directories. Stored in a hash to preserve
|
17
|
-
# order, easily handle duplicates, and also be able to have a fast lookup,
|
18
|
-
# needed for detecting nested paths.
|
19
|
-
#
|
20
|
-
# "/Users/fxn/blog/app/assets" => true,
|
21
|
-
# "/Users/fxn/blog/app/channels" => true,
|
22
|
-
# ...
|
23
|
-
#
|
24
|
-
# This is a private collection maintained by the loader. The public
|
25
|
-
# interface for it is `push_dir` and `dirs`.
|
26
|
-
#
|
27
|
-
# @private
|
28
|
-
# @return [{String => true}]
|
29
|
-
attr_reader :root_dirs
|
7
|
+
require_relative "loader/config"
|
30
8
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
attr_reader :preloads
|
36
|
-
|
37
|
-
# Absolute paths of files, directories, of glob patterns to be totally
|
38
|
-
# ignored.
|
39
|
-
#
|
40
|
-
# @private
|
41
|
-
# @return [Set<String>]
|
42
|
-
attr_reader :ignored_glob_patterns
|
43
|
-
|
44
|
-
# The actual collection of absolute file and directory names at the time the
|
45
|
-
# ignored glob patterns were expanded. Computed on setup, and recomputed on
|
46
|
-
# reload.
|
47
|
-
#
|
48
|
-
# @private
|
49
|
-
# @return [Set<String>]
|
50
|
-
attr_reader :ignored_paths
|
9
|
+
include RealModName
|
10
|
+
include Callbacks
|
11
|
+
include Helpers
|
12
|
+
include Config
|
51
13
|
|
52
|
-
# Maps
|
14
|
+
# Maps absolute paths for which an autoload has been set ---and not
|
53
15
|
# executed--- to their corresponding parent class or module and constant
|
54
16
|
# name.
|
55
17
|
#
|
@@ -58,7 +20,7 @@ module Zeitwerk
|
|
58
20
|
# ...
|
59
21
|
#
|
60
22
|
# @private
|
61
|
-
# @
|
23
|
+
# @sig Hash[String, [Module, Symbol]]
|
62
24
|
attr_reader :autoloads
|
63
25
|
|
64
26
|
# We keep track of autoloaded directories to remove them from the registry
|
@@ -68,15 +30,15 @@ module Zeitwerk
|
|
68
30
|
# to concurrency (see why in Zeitwerk::Loader::Callbacks#on_dir_autoloaded).
|
69
31
|
#
|
70
32
|
# @private
|
71
|
-
# @
|
33
|
+
# @sig Array[String]
|
72
34
|
attr_reader :autoloaded_dirs
|
73
35
|
|
74
36
|
# Stores metadata needed for unloading. Its entries look like this:
|
75
37
|
#
|
76
38
|
# "Admin::Role" => [".../admin/role.rb", [Admin, :Role]]
|
77
39
|
#
|
78
|
-
# The cpath as key helps implementing unloadable_cpath? The
|
79
|
-
#
|
40
|
+
# The cpath as key helps implementing unloadable_cpath? The file name is
|
41
|
+
# stored in order to be able to delete it from $LOADED_FEATURES, and the
|
80
42
|
# pair [Module, Symbol] is used to remove_const the constant from the class
|
81
43
|
# or module object.
|
82
44
|
#
|
@@ -84,7 +46,7 @@ module Zeitwerk
|
|
84
46
|
# or eager loaded. Otherwise, the collection remains empty.
|
85
47
|
#
|
86
48
|
# @private
|
87
|
-
# @
|
49
|
+
# @sig Hash[String, [String, [Module, Symbol]]]
|
88
50
|
attr_reader :to_unload
|
89
51
|
|
90
52
|
# Maps constant paths of namespaces to arrays of corresponding directories.
|
@@ -102,129 +64,34 @@ module Zeitwerk
|
|
102
64
|
# up the corresponding autoloads.
|
103
65
|
#
|
104
66
|
# @private
|
105
|
-
# @
|
67
|
+
# @sig Hash[String, Array[String]]
|
106
68
|
attr_reader :lazy_subdirs
|
107
69
|
|
108
|
-
# Absolute paths of files or directories not to be eager loaded.
|
109
|
-
#
|
110
|
-
# @private
|
111
|
-
# @return [Set<String>]
|
112
|
-
attr_reader :eager_load_exclusions
|
113
|
-
|
114
|
-
attr_accessor :vivify_mod_dir
|
115
|
-
attr_accessor :vivify_mod_class
|
116
|
-
|
117
70
|
def initialize
|
118
|
-
|
119
|
-
|
120
|
-
@tag = SecureRandom.hex(3)
|
121
|
-
@inflector = Inflector.new
|
71
|
+
super
|
122
72
|
|
123
|
-
@
|
124
|
-
@
|
125
|
-
@
|
126
|
-
@
|
127
|
-
@
|
128
|
-
@
|
129
|
-
@
|
130
|
-
@lazy_subdirs = {}
|
131
|
-
@eager_load_exclusions = Set.new
|
132
|
-
|
133
|
-
@setup = false
|
134
|
-
@eager_loaded = false
|
135
|
-
|
136
|
-
@reloading_enabled = false
|
137
|
-
|
138
|
-
@vivify_mod_dir = false
|
139
|
-
@module_paths
|
73
|
+
@autoloads = {}
|
74
|
+
@autoloaded_dirs = []
|
75
|
+
@to_unload = {}
|
76
|
+
@lazy_subdirs = Hash.new { |h, cpath| h[cpath] = [] }
|
77
|
+
@setup = false
|
78
|
+
@eager_loaded = false
|
79
|
+
@module_paths = nil
|
140
80
|
|
141
81
|
Registry.register_loader(self)
|
142
82
|
end
|
143
83
|
|
144
|
-
# Sets
|
145
|
-
#
|
146
|
-
# @return [void]
|
147
|
-
def tag=(tag)
|
148
|
-
@tag = tag.to_s
|
149
|
-
end
|
150
|
-
|
151
|
-
# Absolute paths of the root directories. This is a read-only collection,
|
152
|
-
# please push here via `push_dir`.
|
84
|
+
# Sets autoloads in the root namespace.
|
153
85
|
#
|
154
|
-
# @
|
155
|
-
def dirs
|
156
|
-
root_dirs.keys
|
157
|
-
end
|
158
|
-
|
159
|
-
# Pushes `path` to the list of root directories.
|
160
|
-
#
|
161
|
-
# Raises `Zeitwerk::Error` if `path` does not exist, or if another loader in
|
162
|
-
# the same process already manages that directory or one of its ascendants
|
163
|
-
# or descendants.
|
164
|
-
#
|
165
|
-
# @param path [<String, Pathname>]
|
166
|
-
# @raise [Zeitwerk::Error]
|
167
|
-
# @return [void]
|
168
|
-
def push_dir(path)
|
169
|
-
abspath = File.expand_path(path)
|
170
|
-
if dir?(abspath)
|
171
|
-
raise_if_conflicting_directory(abspath)
|
172
|
-
root_dirs[abspath] = true
|
173
|
-
else
|
174
|
-
warn_string = "Zeitwerk: the root path #{abspath} does not exist, not added"
|
175
|
-
`console.warn(warn_string)`
|
176
|
-
end
|
177
|
-
end
|
178
|
-
|
179
|
-
# You need to call this method before setup in order to be able to reload.
|
180
|
-
# There is no way to undo this, either you want to reload or you don't.
|
181
|
-
#
|
182
|
-
# @raise [Zeitwerk::Error]
|
183
|
-
# @return [void]
|
184
|
-
def enable_reloading
|
185
|
-
return if @reloading_enabled
|
186
|
-
|
187
|
-
if @setup
|
188
|
-
raise Error, "cannot enable reloading after setup"
|
189
|
-
else
|
190
|
-
@reloading_enabled = true
|
191
|
-
end
|
192
|
-
end
|
193
|
-
|
194
|
-
# @return [Boolean]
|
195
|
-
def reloading_enabled?
|
196
|
-
@reloading_enabled
|
197
|
-
end
|
198
|
-
|
199
|
-
# Files or directories to be preloaded instead of lazy loaded.
|
200
|
-
#
|
201
|
-
# @param paths [<String, Pathname, <String, Pathname>>]
|
202
|
-
# @return [void]
|
203
|
-
def preload(*paths)
|
204
|
-
expand_paths(paths).each do |abspath|
|
205
|
-
preloads << abspath
|
206
|
-
do_preload_abspath(abspath) if @setup
|
207
|
-
end
|
208
|
-
end
|
209
|
-
|
210
|
-
# Configure files, directories, or glob patterns to be totally ignored.
|
211
|
-
#
|
212
|
-
# @param paths [<String, Pathname, <String, Pathname>>]
|
213
|
-
# @return [void]
|
214
|
-
def ignore(*glob_patterns)
|
215
|
-
glob_patterns = expand_paths(glob_patterns)
|
216
|
-
ignored_glob_patterns.merge(glob_patterns)
|
217
|
-
ignored_paths.merge(expand_glob_patterns(glob_patterns))
|
218
|
-
end
|
219
|
-
|
220
|
-
# Sets autoloads in the root namespace and preloads files, if any.
|
221
|
-
#
|
222
|
-
# @return [void]
|
86
|
+
# @sig () -> void
|
223
87
|
def setup
|
224
88
|
return if @setup
|
225
89
|
|
226
|
-
actual_root_dirs.each
|
227
|
-
|
90
|
+
actual_root_dirs.each do |root_dir, namespace|
|
91
|
+
set_autoloads_in_dir(root_dir, namespace)
|
92
|
+
end
|
93
|
+
|
94
|
+
on_setup_callbacks.each(&:call)
|
228
95
|
|
229
96
|
@setup = true
|
230
97
|
end
|
@@ -236,8 +103,11 @@ module Zeitwerk
|
|
236
103
|
# else, they are eligible for garbage collection, which would effectively
|
237
104
|
# unload them.
|
238
105
|
#
|
239
|
-
#
|
240
|
-
#
|
106
|
+
# This method is public but undocumented. Main interface is `reload`, which
|
107
|
+
# means `unload` + `setup`. This one is avaiable to be used together with
|
108
|
+
# `unregister`, which is undocumented too.
|
109
|
+
#
|
110
|
+
# @sig () -> void
|
241
111
|
def unload
|
242
112
|
# We are going to keep track of the files that were required by our
|
243
113
|
# autoloads to later remove them from $LOADED_FEATURES, thus making them
|
@@ -247,21 +117,32 @@ module Zeitwerk
|
|
247
117
|
# is enough.
|
248
118
|
unloaded_files = Set.new
|
249
119
|
|
250
|
-
autoloads.each do |
|
120
|
+
autoloads.each do |abspath, (parent, cname)|
|
251
121
|
if parent.autoload?(cname)
|
252
122
|
unload_autoload(parent, cname)
|
253
123
|
else
|
254
124
|
# Could happen if loaded with require_relative. That is unsupported,
|
255
125
|
# and the constant path would escape unloadable_cpath? This is just
|
256
126
|
# defensive code to clean things up as much as we are able to.
|
257
|
-
unload_cref(parent, cname)
|
258
|
-
unloaded_files.add(
|
127
|
+
unload_cref(parent, cname)
|
128
|
+
unloaded_files.add(abspath) if ruby?(abspath)
|
259
129
|
end
|
260
130
|
end
|
261
131
|
|
262
|
-
to_unload.
|
263
|
-
|
264
|
-
|
132
|
+
to_unload.each do |cpath, (abspath, (parent, cname))|
|
133
|
+
# We have to check cdef? in this condition. Reason is, constants whose
|
134
|
+
# file does not define them have to be kept in to_unload as explained
|
135
|
+
# in the implementation of on_file_autoloaded.
|
136
|
+
#
|
137
|
+
# If the constant is not defined, on_unload should not be triggered
|
138
|
+
# for it.
|
139
|
+
if !on_unload_callbacks.empty? && cdef?(parent, cname)
|
140
|
+
value = parent.const_get(cname)
|
141
|
+
run_on_unload_callbacks(cpath, value, abspath)
|
142
|
+
end
|
143
|
+
|
144
|
+
unload_cref(parent, cname)
|
145
|
+
unloaded_files.add(abspath) if ruby?(abspath)
|
265
146
|
end
|
266
147
|
|
267
148
|
unless unloaded_files.empty?
|
@@ -285,9 +166,9 @@ module Zeitwerk
|
|
285
166
|
lazy_subdirs.clear
|
286
167
|
|
287
168
|
Registry.on_unload(self)
|
288
|
-
ExplicitNamespace.
|
169
|
+
ExplicitNamespace.unregister_loader(self)
|
289
170
|
|
290
|
-
@setup
|
171
|
+
@setup = false
|
291
172
|
@eager_loaded = false
|
292
173
|
end
|
293
174
|
|
@@ -298,11 +179,12 @@ module Zeitwerk
|
|
298
179
|
# client code in the README of the project.
|
299
180
|
#
|
300
181
|
# @raise [Zeitwerk::Error]
|
301
|
-
# @
|
182
|
+
# @sig () -> void
|
302
183
|
def reload
|
303
184
|
if reloading_enabled?
|
304
185
|
unload
|
305
186
|
recompute_ignored_paths
|
187
|
+
recompute_collapse_dirs
|
306
188
|
setup
|
307
189
|
else
|
308
190
|
raise ReloadingDisabledError, "can't reload, please call loader.enable_reloading before setup"
|
@@ -312,27 +194,37 @@ module Zeitwerk
|
|
312
194
|
# Eager loads all files in the root directories, recursively. Files do not
|
313
195
|
# need to be in `$LOAD_PATH`, absolute file names are used. Ignored files
|
314
196
|
# are not eager loaded. You can opt-out specifically in specific files and
|
315
|
-
# directories with `do_not_eager_load
|
197
|
+
# directories with `do_not_eager_load`, and that can be overridden passing
|
198
|
+
# `force: true`.
|
316
199
|
#
|
317
|
-
# @
|
318
|
-
def eager_load
|
200
|
+
# @sig (true | false) -> void
|
201
|
+
def eager_load(force: false)
|
319
202
|
return if @eager_loaded
|
320
203
|
|
321
|
-
|
322
|
-
|
204
|
+
honour_exclusions = !force
|
205
|
+
|
206
|
+
queue = []
|
207
|
+
actual_root_dirs.each do |root_dir, namespace|
|
208
|
+
queue << [namespace, root_dir] unless honour_exclusions && excluded_from_eager_load?(root_dir)
|
209
|
+
end
|
210
|
+
|
323
211
|
while to_eager_load = queue.shift
|
324
212
|
namespace, dir = to_eager_load
|
325
213
|
|
326
214
|
ls(dir) do |basename, abspath|
|
327
|
-
next if
|
215
|
+
next if honour_exclusions && excluded_from_eager_load?(abspath)
|
328
216
|
|
329
217
|
if ruby?(abspath)
|
330
|
-
if cref = autoloads[
|
331
|
-
|
218
|
+
if cref = autoloads[abspath]
|
219
|
+
cget(*cref)
|
332
220
|
end
|
333
221
|
elsif dir?(abspath) && !root_dirs.key?(abspath)
|
334
|
-
|
335
|
-
|
222
|
+
if collapse?(abspath)
|
223
|
+
queue << [namespace, abspath]
|
224
|
+
else
|
225
|
+
cname = inflector.camelize(basename, abspath)
|
226
|
+
queue << [cget(namespace, cname), abspath]
|
227
|
+
end
|
336
228
|
end
|
337
229
|
end
|
338
230
|
end
|
@@ -345,20 +237,10 @@ module Zeitwerk
|
|
345
237
|
@eager_loaded = true
|
346
238
|
end
|
347
239
|
|
348
|
-
# Let eager load ignore the given files or directories. The constants
|
349
|
-
# defined in those files are still autoloadable.
|
350
|
-
#
|
351
|
-
# @param paths [<String, Pathname, <String, Pathname>>]
|
352
|
-
# @return [void]
|
353
|
-
def do_not_eager_load(*paths)
|
354
|
-
eager_load_exclusions.merge(expand_paths(paths))
|
355
|
-
end
|
356
|
-
|
357
240
|
# Says if the given constant path would be unloaded on reload. This
|
358
241
|
# predicate returns `false` if reloading is disabled.
|
359
242
|
#
|
360
|
-
# @
|
361
|
-
# @return [Boolean]
|
243
|
+
# @sig (String) -> bool
|
362
244
|
def unloadable_cpath?(cpath)
|
363
245
|
to_unload.key?(cpath)
|
364
246
|
end
|
@@ -366,33 +248,45 @@ module Zeitwerk
|
|
366
248
|
# Returns an array with the constant paths that would be unloaded on reload.
|
367
249
|
# This predicate returns an empty array if reloading is disabled.
|
368
250
|
#
|
369
|
-
# @
|
251
|
+
# @sig () -> Array[String]
|
370
252
|
def unloadable_cpaths
|
371
253
|
to_unload.keys
|
372
254
|
end
|
373
255
|
|
374
|
-
#
|
375
|
-
#
|
376
|
-
# @
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
end
|
382
|
-
|
383
|
-
root_dirs.each_key do |root_dir|
|
384
|
-
return true if root_dir.start_with?(dir) || dir.start_with?(root_dir + "/")
|
385
|
-
end
|
386
|
-
|
387
|
-
false
|
256
|
+
# This is a dangerous method.
|
257
|
+
#
|
258
|
+
# @experimental
|
259
|
+
# @sig () -> void
|
260
|
+
def unregister
|
261
|
+
Registry.unregister_loader(self)
|
262
|
+
ExplicitNamespace.unregister_loader(self)
|
388
263
|
end
|
389
264
|
|
390
265
|
# --- Class methods ---------------------------------------------------------------------------
|
391
266
|
|
392
267
|
class << self
|
268
|
+
# @sig #call | #debug | nil
|
269
|
+
attr_accessor :default_logger
|
270
|
+
|
271
|
+
# This is a shortcut for
|
272
|
+
#
|
273
|
+
# require "zeitwerk"
|
274
|
+
# loader = Zeitwerk::Loader.new
|
275
|
+
# loader.tag = File.basename(__FILE__, ".rb")
|
276
|
+
# loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
|
277
|
+
# loader.push_dir(__dir__)
|
278
|
+
#
|
279
|
+
# except that this method returns the same object in subsequent calls from
|
280
|
+
# the same file, in the unlikely case the gem wants to be able to reload.
|
281
|
+
#
|
282
|
+
# @sig () -> Zeitwerk::Loader
|
283
|
+
def for_gem(called_from)
|
284
|
+
Registry.loader_for_gem(called_from)
|
285
|
+
end
|
286
|
+
|
393
287
|
# Broadcasts `eager_load` to all loaders.
|
394
288
|
#
|
395
|
-
# @
|
289
|
+
# @sig () -> void
|
396
290
|
def eager_load_all
|
397
291
|
Registry.loaders.each(&:eager_load)
|
398
292
|
end
|
@@ -400,20 +294,20 @@ module Zeitwerk
|
|
400
294
|
# Returns an array with the absolute paths of the root directories of all
|
401
295
|
# registered loaders. This is a read-only collection.
|
402
296
|
#
|
403
|
-
# @
|
297
|
+
# @sig () -> Array[String]
|
404
298
|
def all_dirs
|
405
299
|
Registry.loaders.flat_map(&:dirs)
|
406
300
|
end
|
407
301
|
end
|
408
302
|
|
409
|
-
#
|
410
|
-
|
411
|
-
# @
|
303
|
+
private # -------------------------------------------------------------------------------------
|
304
|
+
|
305
|
+
# @sig (String, Module) -> void
|
412
306
|
def set_autoloads_in_dir(dir, parent)
|
413
307
|
ls(dir) do |basename, abspath|
|
414
308
|
begin
|
415
309
|
if ruby?(abspath)
|
416
|
-
# basename
|
310
|
+
# basename.delete_suffix!(".rb")
|
417
311
|
cname = inflector.camelize(basename, abspath).to_sym
|
418
312
|
autoload_file(parent, cname, abspath)
|
419
313
|
elsif dir?(abspath)
|
@@ -423,14 +317,19 @@ module Zeitwerk
|
|
423
317
|
# To resolve the ambiguity file name -> constant path this introduces,
|
424
318
|
# the `app/models/concerns` directory is totally ignored as a namespace,
|
425
319
|
# it counts only as root. The guard checks that.
|
426
|
-
unless
|
320
|
+
unless root_dir?(abspath)
|
427
321
|
cname = inflector.camelize(basename, abspath).to_sym
|
428
|
-
|
322
|
+
if collapse?(abspath)
|
323
|
+
set_autoloads_in_dir(abspath, parent)
|
324
|
+
else
|
325
|
+
autoload_subdir(parent, cname, abspath)
|
326
|
+
end
|
429
327
|
end
|
430
328
|
end
|
431
329
|
rescue ::NameError => error
|
432
330
|
path_type = ruby?(abspath) ? "file" : "directory"
|
433
|
-
|
331
|
+
|
332
|
+
raise NameError.new(<<~MESSAGE, error.name)
|
434
333
|
#{error.message} inferred by #{inflector.class} from #{path_type}
|
435
334
|
|
436
335
|
#{abspath}
|
@@ -442,49 +341,33 @@ module Zeitwerk
|
|
442
341
|
* Rename the #{path_type} to comply with the naming conventions.
|
443
342
|
* Modify the inflector to handle this case.
|
444
343
|
MESSAGE
|
445
|
-
raise NameError.new(message, error.name)
|
446
344
|
end
|
447
345
|
end
|
448
346
|
end
|
449
347
|
|
450
|
-
|
451
|
-
|
452
|
-
# @return [<String>]
|
453
|
-
def actual_root_dirs
|
454
|
-
root_dirs.keys.delete_if do |root_dir|
|
455
|
-
!dir?(root_dir) || ignored_paths.member?(root_dir)
|
456
|
-
end
|
457
|
-
end
|
458
|
-
|
459
|
-
# @param parent [Module]
|
460
|
-
# @param cname [Symbol]
|
461
|
-
# @param subdir [String]
|
462
|
-
# @return [void]
|
348
|
+
# @sig (Module, Symbol, String) -> void
|
463
349
|
def autoload_subdir(parent, cname, subdir)
|
464
|
-
if autoload_path =
|
350
|
+
if autoload_path = autoload_path_set_by_me_for?(parent, cname)
|
465
351
|
cpath = cpath(parent, cname)
|
466
352
|
register_explicit_namespace(cpath) if ruby?(autoload_path)
|
467
353
|
# We do not need to issue another autoload, the existing one is enough
|
468
354
|
# no matter if it is for a file or a directory. Just remember the
|
469
355
|
# subdirectory has to be visited if the namespace is used.
|
470
|
-
|
356
|
+
lazy_subdirs[cpath] << subdir
|
471
357
|
elsif !cdef?(parent, cname)
|
472
358
|
# First time we find this namespace, set an autoload for it.
|
473
|
-
|
359
|
+
lazy_subdirs[cpath(parent, cname)] << subdir
|
474
360
|
set_autoload(parent, cname, subdir)
|
475
361
|
else
|
476
362
|
# For whatever reason the constant that corresponds to this namespace has
|
477
363
|
# already been defined, we have to recurse.
|
478
|
-
set_autoloads_in_dir(subdir, parent
|
364
|
+
set_autoloads_in_dir(subdir, cget(parent, cname))
|
479
365
|
end
|
480
366
|
end
|
481
367
|
|
482
|
-
# @
|
483
|
-
# @param cname [Symbol]
|
484
|
-
# @param file [String]
|
485
|
-
# @return [void]
|
368
|
+
# @sig (Module, Symbol, String) -> void
|
486
369
|
def autoload_file(parent, cname, file)
|
487
|
-
if autoload_path =
|
370
|
+
if autoload_path = strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
|
488
371
|
# First autoload for a Ruby file wins, just ignore subsequent ones.
|
489
372
|
if ruby?(autoload_path)
|
490
373
|
# "file #{file} is ignored because #{autoload_path} has precedence"
|
@@ -500,25 +383,13 @@ module Zeitwerk
|
|
500
383
|
# "file #{file} is ignored because #{cpath(parent, cname)} is already defined"
|
501
384
|
else
|
502
385
|
set_autoload(parent, cname, file)
|
503
|
-
if autoload_path = autoload_for?(parent, cname)
|
504
|
-
if dir?(autoload_path)
|
505
|
-
promote_namespace_from_implicit_to_explicit(
|
506
|
-
dir: autoload_path,
|
507
|
-
file: file,
|
508
|
-
parent: parent,
|
509
|
-
cname: cname
|
510
|
-
)
|
511
|
-
(lazy_subdirs[cpath(parent, cname)] ||= []) << autoload_path
|
512
|
-
end
|
513
|
-
end
|
514
386
|
end
|
515
387
|
end
|
516
388
|
|
517
|
-
#
|
518
|
-
#
|
519
|
-
#
|
520
|
-
# @
|
521
|
-
# @return [void]
|
389
|
+
# `dir` is the directory that would have autovivified a namespace. `file` is
|
390
|
+
# the file where we've found the namespace is explicitly defined.
|
391
|
+
#
|
392
|
+
# @sig (dir: String, file: String, parent: Module, cname: Symbol) -> void
|
522
393
|
def promote_namespace_from_implicit_to_explicit(dir:, file:, parent:, cname:)
|
523
394
|
autoloads.delete(dir)
|
524
395
|
Registry.unregister_autoload(dir)
|
@@ -527,210 +398,74 @@ module Zeitwerk
|
|
527
398
|
register_explicit_namespace(cpath(parent, cname))
|
528
399
|
end
|
529
400
|
|
530
|
-
# @
|
531
|
-
# @param cname [Symbol]
|
532
|
-
# @param abspath [String]
|
533
|
-
# @return [void]
|
401
|
+
# @sig (Module, Symbol, String) -> void
|
534
402
|
def set_autoload(parent, cname, abspath)
|
535
|
-
|
536
|
-
# real path to be able to delete it from $LOADED_FEATURES on unload, and to
|
537
|
-
# be able to do a lookup later in Kernel#require for manual require calls.
|
538
|
-
realpath = `Opal.modules.hasOwnProperty(abspath)` ? abspath : File.realpath(abspath)
|
539
|
-
parent.autoload(cname, realpath)
|
403
|
+
parent.autoload(cname, abspath)
|
540
404
|
|
541
|
-
autoloads[
|
542
|
-
Registry.register_autoload(self,
|
405
|
+
autoloads[abspath] = [parent, cname]
|
406
|
+
Registry.register_autoload(self, abspath)
|
543
407
|
|
544
408
|
# See why in the documentation of Zeitwerk::Registry.inceptions.
|
545
409
|
unless parent.autoload?(cname)
|
546
|
-
Registry.register_inception(cpath(parent, cname),
|
410
|
+
Registry.register_inception(cpath(parent, cname), abspath, self)
|
547
411
|
end
|
548
412
|
end
|
549
413
|
|
550
|
-
# @
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
# The autoload? predicate takes into account the ancestor chain of the
|
558
|
-
# receiver, like const_defined? and other methods in the constants API do.
|
559
|
-
#
|
560
|
-
# For example, given
|
561
|
-
#
|
562
|
-
# class A
|
563
|
-
# autoload :X, "x.rb"
|
564
|
-
# end
|
565
|
-
#
|
566
|
-
# class B < A
|
567
|
-
# end
|
568
|
-
#
|
569
|
-
# B.autoload?(:X) returns "x.rb".
|
570
|
-
#
|
571
|
-
# We need a way to strictly check in parent ignoring ancestors.
|
572
|
-
#
|
573
|
-
# @param parent [Module]
|
574
|
-
# @param cname [Symbol]
|
575
|
-
# @return [String, nil]
|
576
|
-
def strict_autoload_path(parent, cname)
|
577
|
-
parent.autoload?(cname, false)
|
578
|
-
end
|
579
|
-
|
580
|
-
# This method is called this way because I prefer `preload` to be the method
|
581
|
-
# name to configure preloads in the public interface.
|
582
|
-
#
|
583
|
-
# @return [void]
|
584
|
-
def do_preload
|
585
|
-
preloads.each do |abspath|
|
586
|
-
do_preload_abspath(abspath)
|
587
|
-
end
|
588
|
-
end
|
589
|
-
|
590
|
-
# @param abspath [String]
|
591
|
-
# @return [void]
|
592
|
-
def do_preload_abspath(abspath)
|
593
|
-
if ruby?(abspath)
|
594
|
-
do_preload_file(abspath)
|
595
|
-
elsif dir?(abspath)
|
596
|
-
do_preload_dir(abspath)
|
597
|
-
end
|
598
|
-
end
|
599
|
-
|
600
|
-
# @param dir [String]
|
601
|
-
# @return [void]
|
602
|
-
def do_preload_dir(dir)
|
603
|
-
ls(dir) do |_basename, abspath|
|
604
|
-
do_preload_abspath(abspath)
|
605
|
-
end
|
606
|
-
end
|
607
|
-
|
608
|
-
# @param file [String]
|
609
|
-
# @return [Boolean]
|
610
|
-
def do_preload_file(file)
|
611
|
-
require file
|
612
|
-
end
|
613
|
-
|
614
|
-
# @param parent [Module]
|
615
|
-
# @param cname [Symbol]
|
616
|
-
# @return [String]
|
617
|
-
def cpath(parent, cname)
|
618
|
-
parent.equal?(Object) ? cname.to_s : "#{real_mod_name(parent)}::#{cname}"
|
619
|
-
end
|
620
|
-
|
621
|
-
# @param dir [String]
|
622
|
-
# @yieldparam path [String, String]
|
623
|
-
# @return [void]
|
624
|
-
def ls(dir)
|
625
|
-
# `console.log("dir:", dir)`
|
626
|
-
outer_ls = false
|
627
|
-
# cache the Opal.modules keys array for subsequent ls calls during setup
|
628
|
-
%x{
|
629
|
-
if (#@module_paths === nil) {
|
630
|
-
#@module_paths = Object.keys(Opal.modules);
|
631
|
-
outer_ls = true;
|
632
|
-
}
|
633
|
-
}
|
634
|
-
visited_abspaths = `{}`
|
635
|
-
dir_first_char = dir[0]
|
636
|
-
path_start = dir.size + 1
|
637
|
-
path_parts = `[]`
|
638
|
-
basename = ''
|
639
|
-
@module_paths.each do |abspath|
|
640
|
-
%x{
|
641
|
-
if (abspath[0] === dir_first_char) {
|
642
|
-
if (!abspath.startsWith(dir)) { #{next} }
|
643
|
-
path_parts = abspath.slice(path_start).split('/');
|
644
|
-
basename = path_parts[0];
|
645
|
-
abspath = dir + '/' + basename;
|
646
|
-
if (visited_abspaths.hasOwnProperty(abspath)) { #{next} }
|
647
|
-
visited_abspaths[abspath] = true;
|
648
|
-
// console.log("basename:", basename, "abspath:", abspath);
|
649
|
-
#{yield basename, abspath unless ignored_paths.member?(abspath)}
|
650
|
-
}
|
651
|
-
}
|
652
|
-
end
|
653
|
-
# remove cache, because Opal.modules may change after setup
|
654
|
-
%x{
|
655
|
-
if (outer_ls) { #@module_paths = nil }
|
656
|
-
}
|
657
|
-
end
|
658
|
-
|
659
|
-
# @param path [String]
|
660
|
-
# @return [Boolean]
|
661
|
-
def ruby?(abspath)
|
662
|
-
`Opal.modules.hasOwnProperty(abspath)`
|
663
|
-
end
|
664
|
-
|
665
|
-
# @param path [String]
|
666
|
-
# @return [Boolean]
|
667
|
-
def dir?(path)
|
668
|
-
dir_path = path + '/'
|
669
|
-
module_paths = if @module_paths # possibly set by ls
|
670
|
-
@module_paths
|
671
|
-
else
|
672
|
-
`Object.keys(Opal.modules)`
|
673
|
-
end
|
674
|
-
path_first = `path[0]`
|
675
|
-
module_paths.each do |m_path|
|
676
|
-
%x{
|
677
|
-
if (m_path[0] !== path_first) { #{ next } }
|
678
|
-
if (m_path.startsWith(dir_path)) { #{return true} }
|
679
|
-
}
|
414
|
+
# @sig (Module, Symbol) -> String?
|
415
|
+
def autoload_path_set_by_me_for?(parent, cname)
|
416
|
+
if autoload_path = strict_autoload_path(parent, cname)
|
417
|
+
autoload_path if autoloads.key?(autoload_path)
|
418
|
+
else
|
419
|
+
Registry.inception?(cpath(parent, cname))
|
680
420
|
end
|
681
|
-
false
|
682
|
-
end
|
683
|
-
|
684
|
-
# @param paths [<String, Pathname, <String, Pathname>>]
|
685
|
-
# @return [<String>]
|
686
|
-
def expand_paths(paths)
|
687
|
-
paths.flatten.map! { |path| File.expand_path(path) }
|
688
|
-
end
|
689
|
-
|
690
|
-
# @param glob_patterns [<String>]
|
691
|
-
# @return [<String>]
|
692
|
-
def expand_glob_patterns(glob_patterns)
|
693
|
-
# Note that Dir.glob works with regular file names just fine. That is,
|
694
|
-
# glob patterns technically need no wildcards.
|
695
|
-
glob_patterns.flat_map { |glob_pattern| Dir.glob(glob_pattern) }
|
696
|
-
end
|
697
|
-
|
698
|
-
# @return [void]
|
699
|
-
def recompute_ignored_paths
|
700
|
-
ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns))
|
701
|
-
end
|
702
|
-
|
703
|
-
def cdef?(parent, cname)
|
704
|
-
parent.const_defined?(cname, false)
|
705
421
|
end
|
706
422
|
|
423
|
+
# @sig (String) -> void
|
707
424
|
def register_explicit_namespace(cpath)
|
708
425
|
ExplicitNamespace.register(cpath, self)
|
709
426
|
end
|
710
427
|
|
428
|
+
# @sig (String) -> void
|
711
429
|
def raise_if_conflicting_directory(dir)
|
712
430
|
Registry.loaders.each do |loader|
|
713
|
-
if loader
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
431
|
+
next if loader == self
|
432
|
+
next if loader.ignores?(dir)
|
433
|
+
|
434
|
+
dir = dir + "/"
|
435
|
+
loader.root_dirs.each do |root_dir, _namespace|
|
436
|
+
next if ignores?(root_dir)
|
437
|
+
|
438
|
+
root_dir = root_dir + "/"
|
439
|
+
if dir.start_with?(root_dir) || root_dir.start_with?(dir)
|
440
|
+
raise Error,
|
441
|
+
"loader\n\n#{loader}\n\nwants to manage directory #{dir.chop}," \
|
442
|
+
" which is already managed by\n\n#{loader}\n"
|
443
|
+
EOS
|
444
|
+
end
|
718
445
|
end
|
719
446
|
end
|
720
447
|
end
|
721
448
|
|
722
|
-
# @
|
723
|
-
|
724
|
-
|
449
|
+
# @sig (String, Object, String) -> void
|
450
|
+
def run_on_unload_callbacks(cpath, value, abspath)
|
451
|
+
# Order matters. If present, run the most specific one.
|
452
|
+
on_unload_callbacks[cpath]&.each { |c| c.call(value, abspath) }
|
453
|
+
on_unload_callbacks[:ANY]&.each { |c| c.call(cpath, value, abspath) }
|
454
|
+
end
|
455
|
+
|
456
|
+
# @sig (Module, Symbol) -> void
|
725
457
|
def unload_autoload(parent, cname)
|
726
|
-
parent.
|
458
|
+
parent.__send__(:remove_const, cname)
|
727
459
|
end
|
728
460
|
|
729
|
-
# @
|
730
|
-
# @param cname [Symbol]
|
731
|
-
# @return [void]
|
461
|
+
# @sig (Module, Symbol) -> void
|
732
462
|
def unload_cref(parent, cname)
|
733
|
-
|
463
|
+
# Let's optimistically remove_const. The way we use it, this is going to
|
464
|
+
# succeed always if all is good.
|
465
|
+
parent.__send__(:remove_const, cname)
|
466
|
+
rescue ::NameError
|
467
|
+
# There are a few edge scenarios in which this may happen. If the constant
|
468
|
+
# is gone, that is OK, anyway.
|
734
469
|
end
|
735
470
|
end
|
736
471
|
end
|