opal-zeitwerk 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +8 -5
@@ -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
|