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.
@@ -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
- include Callbacks
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
- # Absolute paths of files or directories that have to be preloaded.
32
- #
33
- # @private
34
- # @return [<String>]
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 real absolute paths for which an autoload has been set ---and not
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
- # @return [{String => (Module, Symbol)}]
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
- # @return [<String>]
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 real file name
79
- # is stored in order to be able to delete it from $LOADED_FEATURES, and the
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
- # @return [{String => (String, (Module, Symbol))}]
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
- # @return [{String => <String>}]
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
- @initialized_at = Time.now
119
-
120
- @tag = SecureRandom.hex(3)
121
- @inflector = Inflector.new
71
+ super
122
72
 
123
- @root_dirs = {}
124
- @preloads = []
125
- @ignored_glob_patterns = Set.new
126
- @ignored_paths = Set.new
127
- @autoloads = {}
128
- @autoloaded_dirs = []
129
- @to_unload = {}
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 a tag for the loader, useful for logging.
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
- # @return [<String>]
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 { |root_dir| set_autoloads_in_dir(root_dir, Object) }
227
- do_preload
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
- # @private
240
- # @return [void]
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 |realpath, (parent, cname)|
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) if cdef?(parent, cname)
258
- unloaded_files.add(realpath) if ruby?(realpath)
127
+ unload_cref(parent, cname)
128
+ unloaded_files.add(abspath) if ruby?(abspath)
259
129
  end
260
130
  end
261
131
 
262
- to_unload.each_value do |(realpath, (parent, cname))|
263
- unload_cref(parent, cname) if cdef?(parent, cname)
264
- unloaded_files.add(realpath) if ruby?(realpath)
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.unregister(self)
169
+ ExplicitNamespace.unregister_loader(self)
289
170
 
290
- @setup = false
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
- # @return [void]
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
- # @return [void]
318
- def eager_load
200
+ # @sig (true | false) -> void
201
+ def eager_load(force: false)
319
202
  return if @eager_loaded
320
203
 
321
- queue = actual_root_dirs.reject { |dir| eager_load_exclusions.member?(dir) }
322
- queue.map! { |dir| [Object, dir] }
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 eager_load_exclusions.member?(abspath)
215
+ next if honour_exclusions && excluded_from_eager_load?(abspath)
328
216
 
329
217
  if ruby?(abspath)
330
- if cref = autoloads[File.realpath(abspath)]
331
- cref[0].const_get(cref[1], false)
218
+ if cref = autoloads[abspath]
219
+ cget(*cref)
332
220
  end
333
221
  elsif dir?(abspath) && !root_dirs.key?(abspath)
334
- cname = inflector.camelize(basename, abspath)
335
- queue << [namespace.const_get(cname, false), abspath]
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
- # @param cpath [String]
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
- # @return [<String>]
251
+ # @sig () -> Array[String]
370
252
  def unloadable_cpaths
371
253
  to_unload.keys
372
254
  end
373
255
 
374
- # @private
375
- # @param dir [String]
376
- # @return [Boolean]
377
- def manages?(dir)
378
- dir = dir + "/"
379
- ignored_paths.each do |ignored_path|
380
- return false if dir.start_with?(ignored_path + "/")
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
- # @return [void]
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
- # @return [<String>]
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
- # @param dir [String]
410
- # @param parent [Module]
411
- # @return [void]
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 = basename.slice(-3, 3)
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 root_dirs.key?(abspath)
320
+ unless root_dir?(abspath)
427
321
  cname = inflector.camelize(basename, abspath).to_sym
428
- autoload_subdir(parent, cname, abspath)
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
- message = <<~MESSAGE
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
- private # -------------------------------------------------------------------------------------
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 = autoload_for?(parent, cname)
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
- (lazy_subdirs[cpath] ||= []) << subdir
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
- (lazy_subdirs[cpath(parent, cname)] ||= []) << subdir
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.const_get(cname))
364
+ set_autoloads_in_dir(subdir, cget(parent, cname))
479
365
  end
480
366
  end
481
367
 
482
- # @param parent [Module]
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 = autoload_for?(parent, cname)
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
- # @param dir [String] directory that would have autovivified a module
518
- # @param file [String] the file where the namespace is explictly defined
519
- # @param parent [Module]
520
- # @param cname [Symbol]
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
- # @param parent [Module]
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
- # $LOADED_FEATURES stores real paths since Ruby 2.4.4. We set and save the
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[realpath] = [parent, cname]
542
- Registry.register_autoload(self, realpath)
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), realpath, self)
410
+ Registry.register_inception(cpath(parent, cname), abspath, self)
547
411
  end
548
412
  end
549
413
 
550
- # @param parent [Module]
551
- # @param cname [Symbol]
552
- # @return [String, nil]
553
- def autoload_for?(parent, cname)
554
- strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
555
- end
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 != self && loader.manages?(dir)
714
- raise Error,
715
- "loader\n\n#{self}\n\nwants to manage directory #{dir}," \
716
- " which is already managed by\n\n#{loader}\n"
717
- EOS
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
- # @param parent [Module]
723
- # @param cname [Symbol]
724
- # @return [void]
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.send(:remove_const, cname)
458
+ parent.__send__(:remove_const, cname)
727
459
  end
728
460
 
729
- # @param parent [Module]
730
- # @param cname [Symbol]
731
- # @return [void]
461
+ # @sig (Module, Symbol) -> void
732
462
  def unload_cref(parent, cname)
733
- parent.send(:remove_const, cname)
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