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.
@@ -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