opal-zeitwerk 0.2.3 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/MIT-LICENSE +20 -20
- data/README.md +1073 -466
- data/lib/opal/zeitwerk/version.rb +6 -6
- data/lib/opal-zeitwerk.rb +4 -4
- data/opal/zeitwerk/error.rb +10 -10
- data/opal/zeitwerk/explicit_namespace.rb +78 -71
- data/opal/zeitwerk/gem_inflector.rb +15 -0
- data/opal/zeitwerk/inflector.rb +44 -47
- data/opal/zeitwerk/kernel.rb +64 -32
- data/opal/zeitwerk/loader/callbacks.rb +88 -58
- data/opal/zeitwerk/loader/config.rb +301 -0
- data/opal/zeitwerk/loader/helpers.rb +115 -0
- data/opal/zeitwerk/loader.rb +170 -435
- data/opal/zeitwerk/real_mod_name.rb +20 -21
- data/opal/zeitwerk/registry.rb +143 -121
- data/opal/zeitwerk.rb +14 -13
- metadata +10 -7
@@ -1,6 +1,6 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
module Opal
|
3
|
-
module Zeitwerk
|
4
|
-
VERSION = "0.
|
5
|
-
end
|
6
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Opal
|
3
|
+
module Zeitwerk
|
4
|
+
VERSION = "0.4.0"
|
5
|
+
end
|
6
|
+
end
|
data/lib/opal-zeitwerk.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require 'opal'
|
2
|
-
require 'opal/zeitwerk/version'
|
3
|
-
|
4
|
-
Opal.append_path File.expand_path(File.join(File.dirname(__FILE__), '..', 'opal')).untaint
|
1
|
+
require 'opal'
|
2
|
+
require 'opal/zeitwerk/version'
|
3
|
+
|
4
|
+
Opal.append_path File.expand_path(File.join(File.dirname(__FILE__), '..', 'opal')).untaint
|
data/opal/zeitwerk/error.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
|
-
module Zeitwerk
|
2
|
-
class Error < StandardError
|
3
|
-
end
|
4
|
-
|
5
|
-
class ReloadingDisabledError < Error
|
6
|
-
end
|
7
|
-
|
8
|
-
class NameError < ::NameError
|
9
|
-
end
|
10
|
-
end
|
1
|
+
module Zeitwerk
|
2
|
+
class Error < StandardError
|
3
|
+
end
|
4
|
+
|
5
|
+
class ReloadingDisabledError < Error
|
6
|
+
end
|
7
|
+
|
8
|
+
class NameError < ::NameError
|
9
|
+
end
|
10
|
+
end
|
@@ -1,71 +1,78 @@
|
|
1
|
-
module Zeitwerk
|
2
|
-
# Centralizes the logic for the trace point used to detect the creation of
|
3
|
-
# explicit namespaces, needed to descend into matching subdirectories right
|
4
|
-
# after the constant has been defined.
|
5
|
-
#
|
6
|
-
# The implementation assumes an explicit namespace is managed by one loader.
|
7
|
-
# Loaders that reopen namespaces owned by other projects are responsible for
|
8
|
-
# loading their constant before setup. This is documented.
|
9
|
-
module ExplicitNamespace # :nodoc: all
|
10
|
-
class << self
|
11
|
-
include RealModName
|
12
|
-
|
13
|
-
# Maps constant paths that correspond to explicit namespaces according to
|
14
|
-
# the file system, to the loader responsible for them.
|
15
|
-
#
|
16
|
-
# @private
|
17
|
-
# @
|
18
|
-
attr_reader :cpaths
|
19
|
-
|
20
|
-
# @private
|
21
|
-
# @
|
22
|
-
attr_reader :tracer
|
23
|
-
|
24
|
-
# Asserts `cpath` corresponds to an explicit namespace for which `loader`
|
25
|
-
# is responsible.
|
26
|
-
#
|
27
|
-
# @private
|
28
|
-
# @
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
def disable_tracer_if_unneeded
|
47
|
-
tracer.disable if cpaths.empty?
|
48
|
-
end
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
#
|
53
|
-
#
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
#
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
1
|
+
module Zeitwerk
|
2
|
+
# Centralizes the logic for the trace point used to detect the creation of
|
3
|
+
# explicit namespaces, needed to descend into matching subdirectories right
|
4
|
+
# after the constant has been defined.
|
5
|
+
#
|
6
|
+
# The implementation assumes an explicit namespace is managed by one loader.
|
7
|
+
# Loaders that reopen namespaces owned by other projects are responsible for
|
8
|
+
# loading their constant before setup. This is documented.
|
9
|
+
module ExplicitNamespace # :nodoc: all
|
10
|
+
class << self
|
11
|
+
include RealModName
|
12
|
+
|
13
|
+
# Maps constant paths that correspond to explicit namespaces according to
|
14
|
+
# the file system, to the loader responsible for them.
|
15
|
+
#
|
16
|
+
# @private
|
17
|
+
# @sig Hash[String, Zeitwerk::Loader]
|
18
|
+
attr_reader :cpaths
|
19
|
+
|
20
|
+
# @private
|
21
|
+
# @sig TracePoint
|
22
|
+
attr_reader :tracer
|
23
|
+
|
24
|
+
# Asserts `cpath` corresponds to an explicit namespace for which `loader`
|
25
|
+
# is responsible.
|
26
|
+
#
|
27
|
+
# @private
|
28
|
+
# @sig (String, Zeitwerk::Loader) -> void
|
29
|
+
def register(cpath, loader)
|
30
|
+
cpaths[cpath] = loader
|
31
|
+
# We check enabled? because, looking at the C source code, enabling an
|
32
|
+
# enabled tracer does not seem to be a simple no-op.
|
33
|
+
tracer.enable unless tracer.enabled?
|
34
|
+
end
|
35
|
+
|
36
|
+
# @private
|
37
|
+
# @sig (Zeitwerk::Loader) -> void
|
38
|
+
def unregister_loader(loader)
|
39
|
+
cpaths.delete_if { |_cpath, l| l == loader }
|
40
|
+
disable_tracer_if_unneeded
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# @sig () -> void
|
46
|
+
def disable_tracer_if_unneeded
|
47
|
+
tracer.disable if cpaths.empty?
|
48
|
+
end
|
49
|
+
|
50
|
+
# @sig (TracePoint) -> void
|
51
|
+
def tracepoint_class_callback(event)
|
52
|
+
# If the class is a singleton class, we won't do anything with it so we
|
53
|
+
# can bail out immediately. This is several orders of magnitude faster
|
54
|
+
# than accessing its name.
|
55
|
+
return if event.self.singleton_class?
|
56
|
+
|
57
|
+
# It might be tempting to return if name.nil?, to avoid the computation
|
58
|
+
# of a hash code and delete call. But Ruby does not trigger the :class
|
59
|
+
# event on Class.new or Module.new, so that would incur in an extra call
|
60
|
+
# for nothing.
|
61
|
+
#
|
62
|
+
# On the other hand, if we were called, cpaths is not empty. Otherwise
|
63
|
+
# the tracer is disabled. So we do need to go ahead with the hash code
|
64
|
+
# computation and delete call.
|
65
|
+
if loader = cpaths.delete(real_mod_name(event.self))
|
66
|
+
loader.on_namespace_loaded(event.self)
|
67
|
+
disable_tracer_if_unneeded
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
@cpaths = {}
|
73
|
+
|
74
|
+
# We go through a method instead of defining a block mainly to have a better
|
75
|
+
# label when profiling.
|
76
|
+
@tracer = TracePoint.new(:class, &method(:tracepoint_class_callback))
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Zeitwerk
|
2
|
+
class GemInflector < Inflector
|
3
|
+
# @sig (String) -> void
|
4
|
+
def initialize(root_file)
|
5
|
+
namespace = File.basename(root_file, ".rb")
|
6
|
+
lib_dir = File.dirname(root_file)
|
7
|
+
@version_file = File.join(lib_dir, namespace, "version")
|
8
|
+
end
|
9
|
+
|
10
|
+
# @sig (String, String) -> String
|
11
|
+
def camelize(basename, abspath)
|
12
|
+
abspath == @version_file ? "VERSION" : super
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/opal/zeitwerk/inflector.rb
CHANGED
@@ -1,47 +1,44 @@
|
|
1
|
-
module Zeitwerk
|
2
|
-
class Inflector
|
3
|
-
# Very basic snake case -> camel case conversion.
|
4
|
-
#
|
5
|
-
# inflector = Zeitwerk::Inflector.new
|
6
|
-
# inflector.camelize("post", ...) # => "Post"
|
7
|
-
# inflector.camelize("users_controller", ...) # => "UsersController"
|
8
|
-
# inflector.camelize("api", ...) # => "Api"
|
9
|
-
#
|
10
|
-
# Takes into account hard-coded mappings configured with `inflect`.
|
11
|
-
#
|
12
|
-
# @
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
# )
|
26
|
-
#
|
27
|
-
# inflector.camelize("
|
28
|
-
#
|
29
|
-
#
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
#
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
1
|
+
module Zeitwerk
|
2
|
+
class Inflector
|
3
|
+
# Very basic snake case -> camel case conversion.
|
4
|
+
#
|
5
|
+
# inflector = Zeitwerk::Inflector.new
|
6
|
+
# inflector.camelize("post", ...) # => "Post"
|
7
|
+
# inflector.camelize("users_controller", ...) # => "UsersController"
|
8
|
+
# inflector.camelize("api", ...) # => "Api"
|
9
|
+
#
|
10
|
+
# Takes into account hard-coded mappings configured with `inflect`.
|
11
|
+
#
|
12
|
+
# @sig (String, String) -> String
|
13
|
+
def camelize(basename, _abspath)
|
14
|
+
overrides[basename] || basename.split('_').map!(&:capitalize).join
|
15
|
+
end
|
16
|
+
|
17
|
+
# Configures hard-coded inflections:
|
18
|
+
#
|
19
|
+
# inflector = Zeitwerk::Inflector.new
|
20
|
+
# inflector.inflect(
|
21
|
+
# "html_parser" => "HTMLParser",
|
22
|
+
# "mysql_adapter" => "MySQLAdapter"
|
23
|
+
# )
|
24
|
+
#
|
25
|
+
# inflector.camelize("html_parser", abspath) # => "HTMLParser"
|
26
|
+
# inflector.camelize("mysql_adapter", abspath) # => "MySQLAdapter"
|
27
|
+
# inflector.camelize("users_controller", abspath) # => "UsersController"
|
28
|
+
#
|
29
|
+
# @sig (Hash[String, String]) -> void
|
30
|
+
def inflect(inflections)
|
31
|
+
overrides.merge!(inflections)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# Hard-coded basename to constant name user maps that override the default
|
37
|
+
# inflection logic.
|
38
|
+
#
|
39
|
+
# @sig () -> Hash[String, String]
|
40
|
+
def overrides
|
41
|
+
@overrides ||= {}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/opal/zeitwerk/kernel.rb
CHANGED
@@ -1,32 +1,64 @@
|
|
1
|
-
module Kernel
|
2
|
-
module_function
|
3
|
-
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
|
8
|
-
|
9
|
-
#
|
10
|
-
#
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
end
|
1
|
+
module Kernel
|
2
|
+
module_function
|
3
|
+
|
4
|
+
# Zeitwerk's main idea is to define autoloads for project constants, and then
|
5
|
+
# intercept them when triggered in this thin `Kernel#require` wrapper.
|
6
|
+
#
|
7
|
+
# That allows us to complete the circle, invoke callbacks, autovivify modules,
|
8
|
+
# define autoloads for just autoloaded namespaces, update internal state, etc.
|
9
|
+
#
|
10
|
+
# On the other hand, if you publish a new version of a gem that is now managed
|
11
|
+
# by Zeitwerk, client code can reference directly your classes and modules and
|
12
|
+
# should not require anything. But if someone has legacy require calls around,
|
13
|
+
# they will work as expected, and in a compatible way. This feature is by now
|
14
|
+
# EXPERIMENTAL and UNDOCUMENTED.
|
15
|
+
#
|
16
|
+
# We cannot decorate with prepend + super because Kernel has already been
|
17
|
+
# included in Object, and changes in ancestors don't get propagated into
|
18
|
+
# already existing ancestor chains on Ruby < 3.0.
|
19
|
+
alias_method :zeitwerk_original_require, :require
|
20
|
+
|
21
|
+
# @sig (String) -> true | false
|
22
|
+
def require(path)
|
23
|
+
path = `Opal.normalize(#{path})`
|
24
|
+
if loader = Zeitwerk::Registry.loader_for(path)
|
25
|
+
if `Opal.modules.hasOwnProperty(path)`
|
26
|
+
required = zeitwerk_original_require(path)
|
27
|
+
loader.on_file_autoloaded(path) if required
|
28
|
+
required
|
29
|
+
else
|
30
|
+
loader.on_dir_autoloaded(path)
|
31
|
+
true
|
32
|
+
end
|
33
|
+
else
|
34
|
+
required = zeitwerk_original_require(path)
|
35
|
+
if required
|
36
|
+
realpath = $LOADED_FEATURES.last
|
37
|
+
if loader = Zeitwerk::Registry.loader_for(realpath)
|
38
|
+
loader.on_file_autoloaded(realpath)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
required
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# By now, I have seen no way so far to decorate require_relative.
|
46
|
+
#
|
47
|
+
# For starters, at least in CRuby, require_relative does not delegate to
|
48
|
+
# require. Both require and require_relative delegate the bulk of their work
|
49
|
+
# to an internal C function called rb_require_safe. So, our require wrapper is
|
50
|
+
# not executed.
|
51
|
+
#
|
52
|
+
# On the other hand, we cannot use the aliasing technique above because
|
53
|
+
# require_relative receives a path relative to the directory of the file in
|
54
|
+
# which the call is performed. If a wrapper here invoked the original method,
|
55
|
+
# Ruby would resolve the relative path taking lib/zeitwerk as base directory.
|
56
|
+
#
|
57
|
+
# A workaround could be to extract the base directory from caller_locations,
|
58
|
+
# but what if someone else decorated require_relative before us? You can't
|
59
|
+
# really know with certainty where's the original call site in the stack.
|
60
|
+
#
|
61
|
+
# However, the main use case for require_relative is to load files from your
|
62
|
+
# own project. Projects managed by Zeitwerk don't do this for files managed by
|
63
|
+
# Zeitwerk, precisely.
|
64
|
+
end
|
@@ -1,58 +1,88 @@
|
|
1
|
-
module Zeitwerk::Loader::Callbacks
|
2
|
-
include Zeitwerk::RealModName
|
3
|
-
|
4
|
-
# Invoked from our decorated Kernel#require when a managed file is autoloaded.
|
5
|
-
#
|
6
|
-
# @private
|
7
|
-
# @
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
#
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
1
|
+
module Zeitwerk::Loader::Callbacks
|
2
|
+
include Zeitwerk::RealModName
|
3
|
+
|
4
|
+
# Invoked from our decorated Kernel#require when a managed file is autoloaded.
|
5
|
+
#
|
6
|
+
# @private
|
7
|
+
# @sig (String) -> void
|
8
|
+
def on_file_autoloaded(file)
|
9
|
+
cref = autoloads.delete(file)
|
10
|
+
cpath = cpath(*cref)
|
11
|
+
|
12
|
+
# If reloading is enabled, we need to put this constant for unloading
|
13
|
+
# regardless of what cdef? says. In Ruby < 3.1 the internal state is not
|
14
|
+
# fully cleared. Module#constants still includes it, and you need to
|
15
|
+
# remove_const. See https://github.com/ruby/ruby/pull/4715.
|
16
|
+
to_unload[cpath] = [file, cref] if reloading_enabled?
|
17
|
+
Zeitwerk::Registry.unregister_autoload(file)
|
18
|
+
|
19
|
+
if cdef?(*cref)
|
20
|
+
run_on_load_callbacks(cpath, cget(*cref), file) unless on_load_callbacks.empty?
|
21
|
+
else
|
22
|
+
raise Zeitwerk::NameError.new("expected file #{file} to define constant #{cpath}, but didn't", cref.last)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Invoked from our decorated Kernel#require when a managed directory is
|
27
|
+
# autoloaded.
|
28
|
+
#
|
29
|
+
# @private
|
30
|
+
# @sig (String) -> void
|
31
|
+
def on_dir_autoloaded(dir)
|
32
|
+
# Module#autoload does not serialize concurrent requires, and we handle
|
33
|
+
# directories ourselves, so the callback needs to account for concurrency.
|
34
|
+
#
|
35
|
+
# Multi-threading would introduce a race condition here in which thread t1
|
36
|
+
# autovivifies the module, and while autoloads for its children are being
|
37
|
+
# set, thread t2 autoloads the same namespace.
|
38
|
+
#
|
39
|
+
# Without the mutex and subsequent delete call, t2 would reset the module.
|
40
|
+
# That not only would reassign the constant (undesirable per se) but, worse,
|
41
|
+
# the module object created by t2 wouldn't have any of the autoloads for its
|
42
|
+
# children, since t1 would have correctly deleted its lazy_subdirs entry.
|
43
|
+
|
44
|
+
# Well, on Opal no mutexes.
|
45
|
+
if cref = autoloads.delete(dir)
|
46
|
+
autovivified_module = cref[0].const_set(cref[1], Module.new)
|
47
|
+
cpath = autovivified_module.name
|
48
|
+
|
49
|
+
to_unload[cpath] = [dir, cref] if reloading_enabled?
|
50
|
+
|
51
|
+
# We don't unregister `dir` in the registry because concurrent threads
|
52
|
+
# wouldn't find a loader associated to it in Kernel#require and would
|
53
|
+
# try to require the directory. Instead, we are going to keep track of
|
54
|
+
# these to be able to unregister later if eager loading.
|
55
|
+
autoloaded_dirs << dir
|
56
|
+
|
57
|
+
on_namespace_loaded(autovivified_module)
|
58
|
+
|
59
|
+
run_on_load_callbacks(cpath, autovivified_module, dir) unless on_load_callbacks.empty?
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Invoked when a class or module is created or reopened, either from the
|
64
|
+
# tracer or from module autovivification. If the namespace has matching
|
65
|
+
# subdirectories, we descend into them now.
|
66
|
+
#
|
67
|
+
# @private
|
68
|
+
# @sig (Module) -> void
|
69
|
+
def on_namespace_loaded(namespace)
|
70
|
+
if subdirs = lazy_subdirs.delete(real_mod_name(namespace))
|
71
|
+
subdirs.each do |subdir|
|
72
|
+
set_autoloads_in_dir(subdir, namespace)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
# @sig (String, Object) -> void
|
80
|
+
def run_on_load_callbacks(cpath, value, abspath)
|
81
|
+
# Order matters. If present, run the most specific one.
|
82
|
+
callbacks = reloading_enabled? ? on_load_callbacks[cpath] : on_load_callbacks.delete(cpath)
|
83
|
+
callbacks&.each { |c| c.call(value, abspath) }
|
84
|
+
|
85
|
+
callbacks = on_load_callbacks[:ANY]
|
86
|
+
callbacks&.each { |c| c.call(cpath, value, abspath) }
|
87
|
+
end
|
88
|
+
end
|