require-hooks 0.2.3 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 17073754a96afb51c85cb5848e11b6348969d05107027081a4b3242552ffc3a8
4
- data.tar.gz: 89167c71710dc23e4d39176246f55d1996f0f4d51b92b2778624d13939bac72d
3
+ metadata.gz: 046c644cbb446d1abd3263c3c1ee0c578cddac17693d102172e123f1a880966a
4
+ data.tar.gz: a102736a0665a9de69c9aba242033a479af9b090bf135fa729e74b9bd8f6e006
5
5
  SHA512:
6
- metadata.gz: e99100a54066cff2c1bea42a727bdd828def92516037927625ec02f874d306e26533f76112a57beceb8cd7abc5c4bd5b2c435572d5fccb6de3627b6d588f8731
7
- data.tar.gz: b3da21f9a2fff2e8796d093fcbcb67fa7e35c226ab45389a49c9749c0fa1f7b9faebbe3461f937afbbf0efd188068c0251722b7664c79b6f5ba251fa996a86ad
6
+ metadata.gz: 35e088739e021cc4e83827122ce2daa63934439f6e4d0c9597ccafdaf6f6a08b50eba9302cfed1ced10538a62b317406ded4da1882b06f4d04b527a5e4902b47
7
+ data.tar.gz: 801d491df8a08926067f47b44ee2372d73a53fada8cdfe36b62c0d66191f52f6900499121fc87a326125f00e443848734e5f7756df541d7e92f688ae293e5026
data/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 0.4.0 (2026-04-29)
6
+
7
+ - Improved Bootsnap cache invalidation logic on hooks configuration changes.
8
+
9
+ - Latest Bootsnap compatibility
10
+
11
+ - Coverage compatibility (w/ some limitations)
12
+
13
+ ## 0.3.0 (2026-04-22)
14
+
15
+ - Fix the order of around hooks execution (after part) when using `#load_iseq` driven hooks.
16
+
17
+ - Improve `Kernel#require` patch performance.
18
+
19
+ - Reduce context object creation and use a single object when only one context defined.
20
+
5
21
  ## 0.2.3 (2026-01-13)
6
22
 
7
23
  - Gem metadata fixes.
data/README.md CHANGED
@@ -9,6 +9,8 @@ Require Hooks is a library providing universal interface for injecting custom co
9
9
 
10
10
  Require hooks allows you to interfere with `Kernel#require` (incl. `Kernel#require_relative`) and `Kernel#load`.
11
11
 
12
+ > Check the ["Require Hooks: Filling the Gap in Ruby's Extensibility"](https://evilmartians.com/events/require-hooks-rubykaigi) talk from RubyKaigi 2026 to learn more.
13
+
12
14
  <a href="https://evilmartians.com/">
13
15
  <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
14
16
 
@@ -126,6 +128,7 @@ Thus, if you introduce new source transformers or hijackers, you must invalidate
126
128
 
127
129
  ## Limitations
128
130
 
131
+ - Coverage tracking is only supported if `eval` coverage tracking is enabled (`Coverage.start(eval: true, ...)` or `Simplecov.start { enable_coverage_for_eval; ... }`). Currently requires **Ruby 3.4+**.
129
132
  - `Kernel#load` with a wrap argument (e.g., `load "some_path", true` or `load "some_path", MyModule)`) is not supported (fallbacked to the original implementation). The biggest challenge here is to support constants nesting.
130
133
  - Some very edgy symlinking scenarios are not supported (unlikely to affect real-world projects).
131
134
 
@@ -175,9 +178,9 @@ Test script: `time bundle exec rails runner 'puts "done"'`.
175
178
  | rhooks (patch)  | **8m** |
176
179
  | rhooks (bootsnap)  | 12s |
177
180
 
178
- You can see that requiring tons of files with Require Hooks in patch mode is very slow for now. Why? Mostly because we MUST check `$LOADED_FEATURES` for the presence of the file we want to load and currently we do this via `$LOADED_FEATURES.include?(path)` call, which becomes very slow when `$LOADED_FEATURES` is huge. Thus, we recommend activating Require Hooks after loading all the dependencies and limiting the scope of affected files (via the `patterns` option) on non-MRI platforms to avoid this overhead.
181
+ You can see that requiring tons of files with Require Hooks in patch mode is very slow for now. Why? Manipulating the `$LOADED_FEATURES` index from Ruby triggers costly invalidation at the VM side when a regular `#require` occurs. We recommend activating Require Hooks after loading all the dependencies and limiting the scope of affected files (via the `patterns` option) on non-MRI platforms to avoid this overhead.
179
182
 
180
- **NOTE:** Why Ruby's internal implementations is fast despite from doing the same checks? It uses an internal hash table to keep track of the loaded features (`vm->loaded_features_realpaths`), not an array. Unfortunately, it's not accessible from Ruby.
183
+ **NOTE:** This could be improved in the future if we get [optimized API](https://github.com/palkan/ruby/pull/1) for managing `$LOADED_FEATURES` from Ruby code.
181
184
 
182
185
  Here are the numbers for the same project with scoped hooks (only some folders) activated after `Bundler.require(*)`:
183
186
 
@@ -1,19 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RequireHooks
4
- @@around_load = []
5
- @@source_transform = []
6
- @@hijack_load = []
7
-
8
4
  class Context
9
- def initialize(around_load, source_transform, hijack_load)
10
- @around_load = around_load
11
- @source_transform = source_transform
12
- @hijack_load = hijack_load
5
+ attr_reader :around_load, :source_transform, :hijack_load,
6
+ :patterns, :exclude_patterns
7
+
8
+ def initialize(patterns: nil, exclude_patterns: nil)
9
+ @patterns = patterns.freeze
10
+ @exclude_patterns = exclude_patterns.freeze
11
+
12
+ @around_load = []
13
+ @source_transform = []
14
+ @hijack_load = []
15
+
16
+ @empty = nil
17
+ @readonly = nil
18
+ end
19
+
20
+ def to_key
21
+ [patterns, exclude_patterns]
22
+ end
23
+
24
+ def match?(path)
25
+ return false unless !patterns || patterns.any? { |pattern| File.fnmatch?(pattern, path) }
26
+ return false if exclude_patterns&.any? { |pattern| File.fnmatch?(pattern, path) }
27
+ true
13
28
  end
14
29
 
15
30
  def empty?
16
- @around_load.empty? && @source_transform.empty? && @hijack_load.empty?
31
+ return @empty unless @empty.nil?
32
+ @empty = @around_load.empty? && @source_transform.empty? && @hijack_load.empty?
33
+ end
34
+
35
+ def readonly?
36
+ return @readonly unless @readonly.nil?
37
+
38
+ @readonly = @source_transform.empty? && @hijack_load.empty?
17
39
  end
18
40
 
19
41
  def source_transform?
@@ -55,8 +77,18 @@ module RequireHooks
55
77
  end
56
78
  nil
57
79
  end
80
+
81
+ def merge!(another_ctx)
82
+ around_load.concat(another_ctx.around_load)
83
+ source_transform.concat(another_ctx.source_transform)
84
+ hijack_load.concat(another_ctx.hijack_load)
85
+ end
58
86
  end
59
87
 
88
+ @@default_context = Context.new
89
+ @@noop_context = Context.new
90
+ @@contexts = {}
91
+
60
92
  class << self
61
93
  attr_accessor :print_warnings
62
94
 
@@ -69,7 +101,7 @@ module RequireHooks
69
101
  # block.call.tap { puts "Loaded #{path}" }
70
102
  # end
71
103
  def around_load(patterns: nil, exclude_patterns: nil, &block)
72
- @@around_load << [patterns, exclude_patterns, block]
104
+ register_hook(:around_load, block, patterns: patterns, exclude_patterns: exclude_patterns)
73
105
  end
74
106
 
75
107
  # Define hooks to perform source-to-source transformations.
@@ -83,7 +115,7 @@ module RequireHooks
83
115
  # "# frozen_string_literal: true\n#{source}"
84
116
  # end
85
117
  def source_transform(patterns: nil, exclude_patterns: nil, &block)
86
- @@source_transform << [patterns, exclude_patterns, block]
118
+ register_hook(:source_transform, block, patterns: patterns, exclude_patterns: exclude_patterns)
87
119
  end
88
120
 
89
121
  # This hook should be used to manually compile byte code to be loaded by the VM.
@@ -101,32 +133,60 @@ module RequireHooks
101
133
  # end
102
134
  # end
103
135
  def hijack_load(patterns: nil, exclude_patterns: nil, &block)
104
- @@hijack_load << [patterns, exclude_patterns, block]
136
+ register_hook(:hijack_load, block, patterns: patterns, exclude_patterns: exclude_patterns)
105
137
  end
106
138
 
107
139
  def context_for(path)
108
- around_load = @@around_load.select do |patterns, exclude_patterns, _block|
109
- next unless !patterns || patterns.any? { |pattern| File.fnmatch?(pattern, path) }
110
- next if exclude_patterns&.any? { |pattern| File.fnmatch?(pattern, path) }
140
+ # Fast-track in case we have just a single non-global context defined
141
+ if @@default_context
142
+ return @@default_context if @@default_context.match?(path)
111
143
 
112
- true
113
- end.map { |_patterns, _exclude_patterns, block| block }
144
+ return @@noop_context
145
+ end
146
+
147
+ matching = @@contexts.values.select { |ctx| ctx.match?(path) }
148
+
149
+ return matching[0] || @@noop_context if matching.size < 2
150
+
151
+ ctx = Context.new
152
+ matching.each { |mctx| ctx.merge!(mctx) }
153
+
154
+ ctx
155
+ end
156
+
157
+ # Hack to enable coverage for hooked files.
158
+ # Requires eval coverage to be on.
159
+ # See https://bugs.ruby-lang.org/issues/22018 (https://github.com/ruby/ruby/pull/16805)
160
+ def setup_path_coverage(path, contents = nil)
161
+ return unless defined?(Coverage) && Coverage.running?
114
162
 
115
- source_transform = @@source_transform.select do |patterns, exclude_patterns, _block|
116
- next unless !patterns || patterns.any? { |pattern| File.fnmatch?(pattern, path) }
117
- next if exclude_patterns&.any? { |pattern| File.fnmatch?(pattern, path) }
163
+ return unless eval_coverage_enabled?
118
164
 
119
- true
120
- end.map { |_patterns, _exclude_patterns, block| block }
165
+ Kernel.eval("\n" * (contents || File.read(path)).lines.size, TOPLEVEL_BINDING, path, 1) # rubocop:disable Style/EvalWithLocation,Security/Eval
166
+ end
167
+
168
+ def contexts
169
+ @@contexts
170
+ end
171
+
172
+ private
121
173
 
122
- hijack_load = @@hijack_load.select do |patterns, exclude_patterns, _block|
123
- next unless !patterns || patterns.any? { |pattern| File.fnmatch?(pattern, path) }
124
- next if exclude_patterns&.any? { |pattern| File.fnmatch?(pattern, path) }
174
+ def register_hook(type, block, patterns: nil, exclude_patterns: nil)
175
+ @@default_context = nil
176
+ ctx = Context.new(patterns: patterns, exclude_patterns: exclude_patterns)
177
+
178
+ @@contexts[ctx.to_key] ||= ctx
179
+ @@contexts[ctx.to_key].public_send(type) << block
180
+
181
+ @@default_context = @@contexts.values.first if @@contexts.size == 1
182
+ end
125
183
 
126
- true
127
- end.map { |_patterns, _exclude_patterns, block| block }
184
+ def eval_coverage_enabled?
185
+ return @eval_coverage_enabled if defined?(@eval_coverage_enabled)
186
+ probe_path = File.join(__dir__, "coverage_probe.rb")
187
+ Kernel.eval("proc { |val| val }", TOPLEVEL_BINDING, probe_path, 1) # rubocop:disable Style/EvalWithLocation
128
188
 
129
- Context.new(around_load, source_transform, hijack_load)
189
+ @eval_coverage_enabled = Coverage.peek_result.key?(probe_path)
130
190
  end
131
191
  end
132
192
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RequireHooks
4
+ module Iseq
5
+ class << self
6
+ def compile_with_coverage(ctx, path)
7
+ iseq =
8
+ if ctx.source_transform? || ctx.hijack?
9
+ new_contents = ctx.perform_source_transform(path)
10
+
11
+ RequireHooks.setup_path_coverage(path, new_contents)
12
+
13
+ hijacked = ctx.try_hijack_load(path, new_contents)
14
+
15
+ if hijacked
16
+ raise TypeError, "Unsupported bytecode format for #{path}: #{hijack.class}" unless hijacked.is_a?(::RubyVM::InstructionSequence)
17
+ hijacked
18
+ elsif new_contents
19
+ RubyVM::InstructionSequence.compile(new_contents, path, path, 1)
20
+ end
21
+ end
22
+
23
+ RequireHooks.setup_path_coverage(path, new_contents)
24
+
25
+ iseq ||= yield if block_given?
26
+
27
+ iseq ||= RubyVM::InstructionSequence.compile_file(path)
28
+
29
+ iseq
30
+ end
31
+ end
32
+ end
33
+ end
@@ -1,10 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "require-hooks/iseq"
4
+
3
5
  module RequireHooks
4
6
  module Bootsnap
7
+ EMPTY_ISEQ = RubyVM::InstructionSequence.compile("").freeze
8
+
9
+ # For older Bootsnap
5
10
  module CompileCacheExt
6
11
  def input_to_storage(source, path, *)
7
12
  ctx = RequireHooks.context_for(path)
13
+ return super if ctx.empty?
8
14
 
9
15
  new_contents = ctx.perform_source_transform(path)
10
16
  hijacked = ctx.try_hijack_load(path, new_contents)
@@ -22,15 +28,87 @@ module RequireHooks
22
28
  end
23
29
  end
24
30
 
31
+ # For new Bootsnap
32
+ module CompilerExt
33
+ def input_to_storage(source, path, *)
34
+ ctx = RequireHooks.context_for(path)
35
+ return super if ctx.empty?
36
+
37
+ new_contents = ctx.perform_source_transform(path)
38
+ hijacked = ctx.try_hijack_load(path, new_contents)
39
+
40
+ if hijacked
41
+ raise TypeError, "Unsupported bytecode format for #{path}: #{hijack.class}" unless hijacked.is_a?(::RubyVM::InstructionSequence)
42
+ return hijacked.to_binary
43
+ elsif new_contents
44
+ return RubyVM::InstructionSequence.compile(new_contents, path, path, 1, @compile_options).to_binary
45
+ end
46
+
47
+ super
48
+ rescue SyntaxError, TypeError
49
+ ::Bootsnap::CompileCache::UNCOMPILABLE
50
+ end
51
+ end
52
+
25
53
  module LoadIseqExt
54
+ class << self
55
+ attr_accessor :orig_cache_dir
56
+ end
57
+
26
58
  # Around hooks must be performed every time we trigger a file load, even if
27
59
  # the file is already cached.
28
60
  def load_iseq(path)
29
- RequireHooks.context_for(path).run_around_load_callbacks(path) { super }
61
+ ctx = RequireHooks.context_for(path)
62
+ # Early-return for non-trackable paths
63
+ if ctx.empty?
64
+ ::Bootsnap::CompileCache::ISeq.cache_dir = LoadIseqExt.orig_cache_dir if LoadIseqExt.orig_cache_dir
65
+ return super
66
+ end
67
+
68
+ LoadIseqExt.orig_cache_dir ||= ::Bootsnap::CompileCache::ISeq.cache_dir
69
+ ::Bootsnap::CompileCache::ISeq.cache_dir = File.join(LoadIseqExt.orig_cache_dir, RequireHooks::Bootsnap.version_hash)
70
+
71
+ ctx.run_around_load_callbacks(path) do
72
+ iseq = super
73
+
74
+ ::Bootsnap::CompileCache::ISeq.cache_dir = LoadIseqExt.orig_cache_dir
75
+
76
+ # Bootsnap returns nil when the coverage is on,
77
+ # we fallback to our custom #compile_with_coverage
78
+ unless iseq
79
+ next unless defined?(Coverage) && Coverage.running?
80
+
81
+ iseq = RequireHooks::Iseq.compile_with_coverage(ctx, path)
82
+ end
83
+
84
+ iseq.eval
85
+ EMPTY_ISEQ
86
+ ensure
87
+ ::Bootsnap::CompileCache::ISeq.cache_dir = LoadIseqExt.orig_cache_dir
88
+ end
89
+ end
90
+ end
91
+
92
+ class << self
93
+ def version_hash
94
+ @version_key ||= RequireHooks.contexts.values.map(&:to_cache_key).join("-")
30
95
  end
96
+
97
+ attr_writer :version_hash
98
+ end
99
+ end
100
+
101
+ class Context
102
+ def to_cache_key
103
+ Zlib.crc32(
104
+ (around_load + source_transform + hijack_load).map do |pr|
105
+ RubyVM::InstructionSequence.disasm(pr)
106
+ end.join("\n")
107
+ ).to_s
31
108
  end
32
109
  end
33
110
  end
34
111
 
35
112
  Bootsnap::CompileCache::ISeq.singleton_class.prepend(RequireHooks::Bootsnap::CompileCacheExt)
113
+ Bootsnap::CompileCache::ISeq::Compiler.prepend(RequireHooks::Bootsnap::CompilerExt) if defined?(Bootsnap::CompileCache::ISeq::Compiler)
36
114
  RubyVM::InstructionSequence.singleton_class.prepend(RequireHooks::Bootsnap::LoadIseqExt)
@@ -5,8 +5,8 @@ require "pathname"
5
5
  module RequireHooks
6
6
  module KernelPatch
7
7
  class << self
8
- def load(path)
9
- ctx = RequireHooks.context_for(path)
8
+ def load(path, ctx: nil)
9
+ ctx ||= RequireHooks.context_for(path)
10
10
 
11
11
  ctx.run_around_load_callbacks(path) do
12
12
  next load_without_require_hooks(path) unless ctx.source_transform? || ctx.hijack?
@@ -14,7 +14,9 @@ module RequireHooks
14
14
  new_contents = ctx.perform_source_transform(path)
15
15
  hijacked = ctx.try_hijack_load(path, new_contents)
16
16
 
17
- return try_evaluate(path, hijacked) if hijacked
17
+ if hijacked
18
+ return try_evaluate(path, hijacked)
19
+ end
18
20
 
19
21
  if new_contents
20
22
  evaluate(new_contents, path)
@@ -29,6 +31,7 @@ module RequireHooks
29
31
 
30
32
  def try_evaluate(path, bytecode)
31
33
  if defined?(::RubyVM::InstructionSequence) && bytecode.is_a?(::RubyVM::InstructionSequence)
34
+ RequireHooks.setup_path_coverage(path)
32
35
  bytecode.eval
33
36
  else
34
37
  raise TypeError, "Unknown bytecode format for #{path}: #{bytecode.inspect}"
@@ -49,6 +52,8 @@ module RequireHooks
49
52
  end
50
53
  else
51
54
  def evaluate(code, filepath)
55
+ RequireHooks.setup_path_coverage(filepath, code)
56
+
52
57
  # This is workaround to solve the "leaking refinements" problem in MRI
53
58
  RubyVM::InstructionSequence.compile(code, filepath).tap do |iseq|
54
59
  iseq.eval
@@ -140,32 +145,6 @@ module RequireHooks
140
145
  path
141
146
  end
142
147
 
143
- # Based on https://github.com/ruby/ruby/blob/b588fd552390c55809719100d803c36bc7430f2f/load.c#L403-L415
144
- def feature_loaded?(feature)
145
- return true if $LOADED_FEATURES.include?(feature) && !LOCK.locked_feature?(feature)
146
-
147
- feature = Pathname.new(feature).cleanpath.to_s
148
- efeature = File.expand_path(feature)
149
-
150
- # Check absoulute and relative paths
151
- return true if $LOADED_FEATURES.include?(efeature) && !LOCK.locked_feature?(efeature)
152
-
153
- candidates = []
154
-
155
- $LOADED_FEATURES.each do |lf|
156
- candidates << lf if lf.end_with?("/#{feature}")
157
- end
158
-
159
- return false if candidates.empty?
160
-
161
- $LOAD_PATH.each do |lp|
162
- lp_feature = File.join(lp, feature)
163
- return true if candidates.include?(lp_feature) && !LOCK.locked_feature?(lp_feature)
164
- end
165
-
166
- false
167
- end
168
-
169
148
  private
170
149
 
171
150
  def lookup_feature_path(path, implitic_ext: true)
@@ -250,24 +229,26 @@ module Kernel
250
229
 
251
230
  return require_without_require_hooks(path) if ctx.empty?
252
231
 
253
- return false if RequireHooks::KernelPatch::Features.feature_loaded?(feature)
232
+ return false if $LOADED_FEATURES.include?(realpath)
254
233
 
255
- RequireHooks::KernelPatch::Features::LOCK.lock_feature(feature) do |loaded|
256
- return false if loaded
234
+ begin
235
+ RequireHooks::KernelPatch::Features::LOCK.lock_feature(feature) do |loaded|
236
+ return false if loaded
257
237
 
258
- $LOADED_FEATURES << realpath
259
- RequireHooks::KernelPatch.load(realpath)
260
- true
238
+ $LOADED_FEATURES << realpath
239
+ RequireHooks::KernelPatch.load(realpath, ctx: ctx)
240
+ true
241
+ end
242
+ rescue LoadError => e
243
+ $LOADED_FEATURES.delete(realpath) if realpath
244
+ warn "RequireHooks failed to require '#{path}' from '#{realpath}': #{e.message}" if RequireHooks.print_warnings
245
+ require_without_require_hooks(path)
246
+ rescue Errno::ENOENT, Errno::EACCES
247
+ raise LoadError, "cannot load such file -- #{path}"
248
+ rescue
249
+ $LOADED_FEATURES.delete(realpath) if realpath
250
+ raise
261
251
  end
262
- rescue LoadError => e
263
- $LOADED_FEATURES.delete(realpath) if realpath
264
- warn "RequireHooks failed to require '#{path}': #{e.message}" if RequireHooks.print_warnings
265
- require_without_require_hooks(path)
266
- rescue Errno::ENOENT, Errno::EACCES
267
- raise LoadError, "cannot load such file -- #{path}"
268
- rescue
269
- $LOADED_FEATURES.delete(realpath) if realpath
270
- raise
271
252
  end
272
253
 
273
254
  alias_method :require_relative_without_require_hooks, :require_relative
@@ -1,24 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "require-hooks/iseq"
4
+
3
5
  module RequireHooks
6
+ EMPTY_ISEQ = RubyVM::InstructionSequence.compile("").freeze
7
+
4
8
  module LoadIseq
5
9
  def load_iseq(path)
6
10
  ctx = RequireHooks.context_for(path)
7
11
 
12
+ # Early-return for non-trackable paths
13
+ return if ctx.empty?
14
+
8
15
  ctx.run_around_load_callbacks(path) do
9
- if ctx.source_transform? || ctx.hijack?
10
- new_contents = ctx.perform_source_transform(path)
11
- hijacked = ctx.try_hijack_load(path, new_contents)
12
-
13
- if hijacked
14
- raise TypeError, "Unsupported bytecode format for #{path}: #{hijack.class}" unless hijacked.is_a?(::RubyVM::InstructionSequence)
15
- return hijacked
16
- elsif new_contents
17
- return RubyVM::InstructionSequence.compile(new_contents, path, path, 1)
18
- end
19
- end
20
-
21
- defined?(super) ? super : RubyVM::InstructionSequence.compile_file(path)
16
+ iseq = RequireHooks::Iseq.compile_with_coverage(ctx, path) { defined?(super) && super }
17
+
18
+ iseq.eval
19
+ EMPTY_ISEQ
22
20
  end
23
21
  end
24
22
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RequireHooks
4
- VERSION = "0.2.3"
4
+ VERSION = "0.4.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: require-hooks
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dementyev
@@ -22,6 +22,7 @@ files:
22
22
  - README.md
23
23
  - lib/require-hooks.rb
24
24
  - lib/require-hooks/api.rb
25
+ - lib/require-hooks/iseq.rb
25
26
  - lib/require-hooks/mode/bootsnap.rb
26
27
  - lib/require-hooks/mode/kernel_patch.rb
27
28
  - lib/require-hooks/mode/load_iseq.rb
@@ -37,6 +38,7 @@ metadata:
37
38
  homepage_uri: https://github.com/ruby-next/require-hooks
38
39
  source_code_uri: https://github.com/ruby-next/require-hooks
39
40
  funding_uri: https://github.com/sponsors/palkan
41
+ rubygems_mfa_required: 'true'
40
42
  rdoc_options: []
41
43
  require_paths:
42
44
  - lib