opal-zeitwerk 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|