require-hooks 0.2.3 → 0.3.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: 36e7f23cef600d120d9139555f3e3ef4b8bd46b9929387b99dce99280108615c
4
+ data.tar.gz: c89a896109b87d81d2e1a7690398bc337b6723c2ae006c5a2a0f0c7a8e01c52a
5
5
  SHA512:
6
- metadata.gz: e99100a54066cff2c1bea42a727bdd828def92516037927625ec02f874d306e26533f76112a57beceb8cd7abc5c4bd5b2c435572d5fccb6de3627b6d588f8731
7
- data.tar.gz: b3da21f9a2fff2e8796d093fcbcb67fa7e35c226ab45389a49c9749c0fa1f7b9faebbe3461f937afbbf0efd188068c0251722b7664c79b6f5ba251fa996a86ad
6
+ metadata.gz: 4af96e3f1d117b10e7cedc4cb564e47768ac5eb44e8f83e666e90b9f98178fd827a19641caa0f2dd3e674736ec7aa0982e11d184fa780e46fc89a0c399098f00
7
+ data.tar.gz: c0c60d8e40d22743aa24c671b88989c6aef61c58d39cdefe39e26c73905fc08286d0fb68d09e409e0762e8ccbfe5334926d7c35321f2442bf4158e5e8dda0d1e
data/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 0.3.0 (2026-04-22)
6
+
7
+ - Fix the order of around hooks execution (after part) when using `#load_iseq` driven hooks.
8
+
9
+ - Improve `Kernel#require` patch performance.
10
+
11
+ - Reduce context object creation and use a single object when only one context defined.
12
+
5
13
  ## 0.2.3 (2026-01-13)
6
14
 
7
15
  - Gem metadata fixes.
@@ -1,19 +1,34 @@
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
+ end
18
+
19
+ def to_key
20
+ [patterns, exclude_patterns]
21
+ end
22
+
23
+ def match?(path)
24
+ return false unless !patterns || patterns.any? { |pattern| File.fnmatch?(pattern, path) }
25
+ return false if exclude_patterns&.any? { |pattern| File.fnmatch?(pattern, path) }
26
+ true
13
27
  end
14
28
 
15
29
  def empty?
16
- @around_load.empty? && @source_transform.empty? && @hijack_load.empty?
30
+ return @empty unless @empty.nil?
31
+ @empty = @around_load.empty? && @source_transform.empty? && @hijack_load.empty?
17
32
  end
18
33
 
19
34
  def source_transform?
@@ -57,6 +72,10 @@ module RequireHooks
57
72
  end
58
73
  end
59
74
 
75
+ @@default_context = Context.new
76
+ @@noop_context = Context.new
77
+ @@contexts = {}
78
+
60
79
  class << self
61
80
  attr_accessor :print_warnings
62
81
 
@@ -69,7 +88,13 @@ module RequireHooks
69
88
  # block.call.tap { puts "Loaded #{path}" }
70
89
  # end
71
90
  def around_load(patterns: nil, exclude_patterns: nil, &block)
72
- @@around_load << [patterns, exclude_patterns, block]
91
+ @@default_context = nil
92
+ ctx = Context.new(patterns: patterns, exclude_patterns: exclude_patterns)
93
+
94
+ @@contexts[ctx.to_key] ||= ctx
95
+ @@contexts[ctx.to_key].around_load << block
96
+
97
+ @@default_context = @@contexts.values.first if @@contexts.size == 1
73
98
  end
74
99
 
75
100
  # Define hooks to perform source-to-source transformations.
@@ -83,7 +108,13 @@ module RequireHooks
83
108
  # "# frozen_string_literal: true\n#{source}"
84
109
  # end
85
110
  def source_transform(patterns: nil, exclude_patterns: nil, &block)
86
- @@source_transform << [patterns, exclude_patterns, block]
111
+ @@default_context = nil
112
+ ctx = Context.new(patterns: patterns, exclude_patterns: exclude_patterns)
113
+
114
+ @@contexts[ctx.to_key] ||= ctx
115
+ @@contexts[ctx.to_key].source_transform << block
116
+
117
+ @@default_context = @@contexts.values.first if @@contexts.size == 1
87
118
  end
88
119
 
89
120
  # This hook should be used to manually compile byte code to be loaded by the VM.
@@ -101,32 +132,35 @@ module RequireHooks
101
132
  # end
102
133
  # end
103
134
  def hijack_load(patterns: nil, exclude_patterns: nil, &block)
104
- @@hijack_load << [patterns, exclude_patterns, block]
135
+ @@default_context = nil
136
+ ctx = Context.new(patterns: patterns, exclude_patterns: exclude_patterns)
137
+
138
+ @@contexts[ctx.to_key] ||= ctx
139
+ @@contexts[ctx.to_key].hijack_load << block
140
+
141
+ @@default_context = @@contexts.values.first if @@contexts.size == 1
105
142
  end
106
143
 
107
144
  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) }
111
-
112
- true
113
- end.map { |_patterns, _exclude_patterns, block| block }
145
+ # Fast-track in case we have just a single context defined
146
+ if @@default_context
147
+ return @@noop_context unless @@default_context.match?(path)
114
148
 
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) }
149
+ return @@default_context
150
+ end
118
151
 
119
- true
120
- end.map { |_patterns, _exclude_patterns, block| block }
152
+ matching = @@contexts.values.select { |ctx| ctx.match?(path) }
121
153
 
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) }
154
+ return matching[0] || @@noop_context if matching.size < 2
125
155
 
126
- true
127
- end.map { |_patterns, _exclude_patterns, block| block }
156
+ ctx = Context.new
157
+ matching.each do |mctx|
158
+ ctx.around_load.concat(mctx.around_load)
159
+ ctx.source_transform.concat(mctx.source_transform)
160
+ ctx.hijack_load.concat(mctx.hijack_load)
161
+ end
128
162
 
129
- Context.new(around_load, source_transform, hijack_load)
163
+ ctx
130
164
  end
131
165
  end
132
166
  end
@@ -2,9 +2,12 @@
2
2
 
3
3
  module RequireHooks
4
4
  module Bootsnap
5
+ EMPTY_ISEQ = RubyVM::InstructionSequence.compile("").freeze
6
+
5
7
  module CompileCacheExt
6
8
  def input_to_storage(source, path, *)
7
9
  ctx = RequireHooks.context_for(path)
10
+ return super if ctx.empty?
8
11
 
9
12
  new_contents = ctx.perform_source_transform(path)
10
13
  hijacked = ctx.try_hijack_load(path, new_contents)
@@ -26,7 +29,17 @@ module RequireHooks
26
29
  # Around hooks must be performed every time we trigger a file load, even if
27
30
  # the file is already cached.
28
31
  def load_iseq(path)
29
- RequireHooks.context_for(path).run_around_load_callbacks(path) { super }
32
+ ctx = RequireHooks.context_for(path)
33
+ # Early-return for non-trackable paths
34
+ return super if ctx.empty?
35
+
36
+ ctx.run_around_load_callbacks(path) do
37
+ iseq = super
38
+ return unless iseq
39
+
40
+ iseq.eval
41
+ EMPTY_ISEQ
42
+ end
30
43
  end
31
44
  end
32
45
  end
@@ -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?
@@ -140,32 +140,6 @@ module RequireHooks
140
140
  path
141
141
  end
142
142
 
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
143
  private
170
144
 
171
145
  def lookup_feature_path(path, implitic_ext: true)
@@ -250,13 +224,13 @@ module Kernel
250
224
 
251
225
  return require_without_require_hooks(path) if ctx.empty?
252
226
 
253
- return false if RequireHooks::KernelPatch::Features.feature_loaded?(feature)
227
+ return false if $LOADED_FEATURES.include?(realpath)
254
228
 
255
229
  RequireHooks::KernelPatch::Features::LOCK.lock_feature(feature) do |loaded|
256
230
  return false if loaded
257
231
 
258
232
  $LOADED_FEATURES << realpath
259
- RequireHooks::KernelPatch.load(realpath)
233
+ RequireHooks::KernelPatch.load(realpath, ctx: ctx)
260
234
  true
261
235
  end
262
236
  rescue LoadError => e
@@ -1,24 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RequireHooks
4
+ EMPTY_ISEQ = RubyVM::InstructionSequence.compile("").freeze
5
+
4
6
  module LoadIseq
5
7
  def load_iseq(path)
6
8
  ctx = RequireHooks.context_for(path)
7
9
 
10
+ # Early-return for non-trackable paths
11
+ return if ctx.empty?
12
+
8
13
  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)
14
+ iseq =
15
+ if ctx.source_transform? || ctx.hijack?
16
+ new_contents = ctx.perform_source_transform(path)
17
+ hijacked = ctx.try_hijack_load(path, new_contents)
18
+
19
+ if hijacked
20
+ raise TypeError, "Unsupported bytecode format for #{path}: #{hijack.class}" unless hijacked.is_a?(::RubyVM::InstructionSequence)
21
+ hijacked
22
+ elsif new_contents
23
+ RubyVM::InstructionSequence.compile(new_contents, path, path, 1)
24
+ end
18
25
  end
19
- end
20
26
 
21
- defined?(super) ? super : RubyVM::InstructionSequence.compile_file(path)
27
+ iseq ||= (defined?(super) ? super : RubyVM::InstructionSequence.compile_file(path))
28
+ iseq.eval
29
+ EMPTY_ISEQ
22
30
  end
23
31
  end
24
32
  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.3.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.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dementyev
@@ -37,6 +37,7 @@ metadata:
37
37
  homepage_uri: https://github.com/ruby-next/require-hooks
38
38
  source_code_uri: https://github.com/ruby-next/require-hooks
39
39
  funding_uri: https://github.com/sponsors/palkan
40
+ rubygems_mfa_required: 'true'
40
41
  rdoc_options: []
41
42
  require_paths:
42
43
  - lib