opal-zeitwerk 0.0.3 → 0.2.1

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