opal-zeitwerk 0.0.4 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +5 -10
- data/lib/opal/zeitwerk/version.rb +1 -1
- data/opal/zeitwerk/loader/callbacks.rb +3 -6
- data/opal/zeitwerk/loader.rb +736 -731
- metadata +8 -10
data/opal/zeitwerk/loader.rb
CHANGED
@@ -1,731 +1,736 @@
|
|
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
|
-
|
292
|
-
|
293
|
-
|
294
|
-
#
|
295
|
-
#
|
296
|
-
#
|
297
|
-
#
|
298
|
-
#
|
299
|
-
#
|
300
|
-
# @
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
#
|
313
|
-
#
|
314
|
-
#
|
315
|
-
#
|
316
|
-
#
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
queue.
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
#
|
349
|
-
#
|
350
|
-
#
|
351
|
-
# @
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
#
|
358
|
-
#
|
359
|
-
#
|
360
|
-
# @
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
#
|
367
|
-
#
|
368
|
-
#
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
# @
|
375
|
-
# @
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
#
|
394
|
-
#
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
#
|
401
|
-
#
|
402
|
-
#
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
# @param
|
410
|
-
# @
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
# `app/models
|
421
|
-
#
|
422
|
-
#
|
423
|
-
# the
|
424
|
-
#
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
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
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
# @param
|
460
|
-
# @param
|
461
|
-
# @
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
#
|
468
|
-
#
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
#
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
# @param
|
483
|
-
# @param
|
484
|
-
# @
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
# @param
|
519
|
-
# @param
|
520
|
-
# @param
|
521
|
-
# @return [void]
|
522
|
-
def
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
parent
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
#
|
551
|
-
#
|
552
|
-
#
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
#
|
558
|
-
#
|
559
|
-
#
|
560
|
-
#
|
561
|
-
#
|
562
|
-
#
|
563
|
-
#
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
#
|
575
|
-
#
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
end
|
607
|
-
|
608
|
-
# @param
|
609
|
-
# @
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
# @param
|
616
|
-
# @
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
end
|
658
|
-
|
659
|
-
# @param path [String]
|
660
|
-
# @return [Boolean]
|
661
|
-
def
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
module_paths
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
end
|
683
|
-
|
684
|
-
# @param
|
685
|
-
# @return [<String>]
|
686
|
-
def
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
# @
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
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
|