require-hooks 0.4.1 → 0.5.1

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: 5179f6010729ac30b40c0040f2d00956164c8f765d41e2d095eeefa9b96da842
4
- data.tar.gz: f836cd71ec64084e4145ac572a555b7a32b7abdacd7d7d25965766bca2d264bc
3
+ metadata.gz: 9e75b55ac6829e71745efc569fd472288bfc17f85b569413c947f145c8f3a866
4
+ data.tar.gz: 72a6083f17c215f429b7f1ae14b637c38c613cd2f7dffe2a8ead2ce8be2de956
5
5
  SHA512:
6
- metadata.gz: 93559e1cc1b84d23e17b60abb8f92e47c67ea6f66f1dcbc4243b34f3d68345674c96da73aa890ba8c79e226209877a48aa4438343a51ba3ce736160403597abb
7
- data.tar.gz: 4c36e011286728aee0f43a81411875c38fee956e34105c4dcba87bc4cf52aaee6a2d4e97d332bd2727551c85bc009ddba646b3a9764ca19c3521b7687bf43a1c
6
+ metadata.gz: ccb59bc078a1bd1b748b721427240fb136ccc269148472b4963d5db052f7696b2ba8356bb3a14228561ca4f6e3e830962f182a834d6ae0dd036d0c73faac22d8
7
+ data.tar.gz: 47cd3bc2f622f9062361099e64dd179166f2d72f02e8d1366fa548b14fd62265f1a35b4db046755b84feafc34dab3bbcaa9eb28430600da3ccb09edf2a9db5bb
data/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 0.5.1 (2026-08-26)
6
+
7
+ - Add `RequireHooks::Bootsnap.add_version_hash` for external cache inputs.
8
+
9
+ ## 0.5.0 (2026-08-26)
10
+
11
+ - Use Bootsnap compiler namespaces for cache invalidation without changing Bootsnap's cache directory.
12
+
5
13
  ## 0.4.1 (2026-07-22)
6
14
 
7
15
  - Bring back Ruby 2.3 compatibility.
data/README.md CHANGED
@@ -124,7 +124,15 @@ require "require-hooks/setup"
124
124
 
125
125
  The _around load_ hooks are executed for all files independently of whether they are cached or not. Source transformation and hijacking is only done for non-cached files.
126
126
 
127
- Thus, if you introduce new source transformers or hijackers, you must invalidate the cache. (We plan to implement automatic invalidation in future versions.)
127
+ Require Hooks separates cached instructions by hook configuration. New source transformers and hijackers invalidate affected cache entries automatically.
128
+
129
+ If a transform depends on an external library or configuration, add that input to the cache key:
130
+
131
+ ```ruby
132
+ RequireHooks::Bootsnap.add_version_hash { "parser-#{Parser::VERSION}-#{options_digest}" }
133
+ ```
134
+
135
+ The value can be a String or a callable that returns a String. Require Hooks hashes each contribution and combines all contributions with its hook key. The callable runs when Require Hooks computes the key, so it does not freeze the key before later hooks register.
128
136
 
129
137
  ## Limitations
130
138
 
@@ -28,75 +28,136 @@ module RequireHooks
28
28
  end
29
29
  end
30
30
 
31
- # For new Bootsnap
32
- module CompilerExt
33
- def input_to_storage(source, path, *)
31
+ # Bootsnap before 1.24 does not support compiler namespaces.
32
+ module LegacyCacheExt
33
+ def fetch(path, cache_dir: self.cache_dir)
34
34
  ctx = RequireHooks.context_for(path)
35
- return super if ctx.empty?
35
+ cache_dir = File.join(cache_dir, RequireHooks::Bootsnap.version_hash) unless ctx.empty?
36
+
37
+ super
38
+ end
39
+ end
40
+
41
+ # Bootsnap 1.24+ compiler wrapper. Its namespace isolates transformed ISeqs
42
+ # without changing Bootsnap's process-wide cache directory.
43
+ class VersionedCompiler
44
+ attr_reader :namespace, :version_hash
36
45
 
46
+ def initialize(compiler, version_hash)
47
+ @compiler = compiler
48
+ @version_hash = version_hash
49
+ @namespace = "#{compiler.namespace}-require-hooks-#{version_hash}"
50
+ end
51
+
52
+ def input_to_storage(source, path, *args)
53
+ iseq = compile(RequireHooks.context_for(path), path, nil)
54
+ return @compiler.input_to_storage(source, path, *args) unless iseq
55
+
56
+ iseq.to_binary
57
+ rescue SyntaxError, TypeError
58
+ ::Bootsnap::CompileCache::UNCOMPILABLE
59
+ end
60
+
61
+ def storage_to_output(*args)
62
+ @compiler.storage_to_output(*args)
63
+ end
64
+
65
+ def input_to_output(source, path, args)
66
+ compile(RequireHooks.context_for(path), path, args) || @compiler.input_to_output(source, path, args)
67
+ end
68
+
69
+ private
70
+
71
+ def compile(ctx, path, args)
37
72
  new_contents = ctx.perform_source_transform(path)
38
73
  hijacked = ctx.try_hijack_load(path, new_contents)
39
74
 
40
75
  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
76
+ raise TypeError, "Unsupported bytecode format for #{path}: #{hijacked.class}" unless hijacked.is_a?(::RubyVM::InstructionSequence)
77
+ return hijacked
45
78
  end
46
79
 
47
- super
48
- rescue SyntaxError, TypeError
49
- ::Bootsnap::CompileCache::UNCOMPILABLE
80
+ return unless new_contents
81
+
82
+ @compiler.input_to_output(new_contents, path, args) ||
83
+ RubyVM::InstructionSequence.compile(new_contents, path, path, 1)
50
84
  end
51
85
  end
52
86
 
53
87
  module LoadIseqExt
54
- class << self
55
- attr_accessor :orig_cache_dir
56
- end
57
-
58
88
  # Around hooks must be performed every time we trigger a file load, even if
59
89
  # the file is already cached.
60
90
  def load_iseq(path)
61
91
  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)
92
+ return super if ctx.empty?
70
93
 
71
94
  ctx.run_around_load_callbacks(path) do
72
- begin
73
- iseq = super
95
+ iseq = super
74
96
 
75
- ::Bootsnap::CompileCache::ISeq.cache_dir = LoadIseqExt.orig_cache_dir
97
+ # Bootsnap returns nil when the coverage is on,
98
+ # we fallback to our custom #compile_with_coverage
99
+ unless iseq
100
+ next unless defined?(Coverage) && Coverage.running?
76
101
 
77
- # Bootsnap returns nil when the coverage is on,
78
- # we fallback to our custom #compile_with_coverage
79
- unless iseq
80
- next unless defined?(Coverage) && Coverage.running?
81
-
82
- iseq = RequireHooks::Iseq.compile_with_coverage(ctx, path)
83
- end
84
-
85
- iseq.eval
86
- EMPTY_ISEQ
87
- ensure
88
- ::Bootsnap::CompileCache::ISeq.cache_dir = LoadIseqExt.orig_cache_dir
102
+ iseq = RequireHooks::Iseq.compile_with_coverage(ctx, path)
89
103
  end
104
+
105
+ iseq.eval
106
+ EMPTY_ISEQ
90
107
  end
91
108
  end
92
109
  end
93
110
 
94
111
  class << self
112
+ def install_compiler_selector
113
+ iseq = ::Bootsnap::CompileCache::ISeq
114
+ @original_compiler_selector = iseq.compiler_selector
115
+ @compilers = {}.compare_by_identity
116
+ iseq.compiler_selector = method(:compiler_for)
117
+ end
118
+
119
+ def compiler_for(path)
120
+ iseq = ::Bootsnap::CompileCache::ISeq
121
+ compiler = @original_compiler_selector&.call(path) || iseq.default_compiler
122
+ return compiler if RequireHooks.context_for(path).empty?
123
+
124
+ version_hash = RequireHooks::Bootsnap.version_hash
125
+ cached = @compilers[compiler]
126
+ return cached if cached&.version_hash == version_hash
127
+
128
+ @compilers[compiler] = VersionedCompiler.new(compiler, version_hash)
129
+ end
130
+
131
+ def add_version_hash(value = nil, &block)
132
+ contributor = block || value
133
+ unless contributor.is_a?(String) || contributor.respond_to?(:call)
134
+ raise ArgumentError, "version hash contribution must be a String or callable"
135
+ end
136
+
137
+ @version_hash_contributors ||= []
138
+ @version_hash_contributors << (contributor.is_a?(String) ? contributor.dup.freeze : contributor)
139
+ @compilers&.clear
140
+ contributor
141
+ end
142
+
95
143
  def version_hash
96
- @version_key ||= RequireHooks.contexts.values.map(&:to_cache_key).join("-")
144
+ return @version_hash unless @version_hash.nil?
145
+
146
+ context_keys = RequireHooks.contexts.values.map(&:to_cache_key)
147
+ contribution_keys = Array(@version_hash_contributors).map do |contributor|
148
+ value = contributor.respond_to?(:call) ? contributor.call : contributor
149
+ raise TypeError, "version hash contribution must return a String" unless value.is_a?(String)
150
+
151
+ Zlib.crc32(value).to_s
152
+ end.sort
153
+
154
+ (context_keys + contribution_keys).join("-")
97
155
  end
98
156
 
99
- attr_writer :version_hash
157
+ def version_hash=(version_hash)
158
+ @version_hash = version_hash
159
+ @compilers&.clear
160
+ end
100
161
  end
101
162
  end
102
163
 
@@ -111,6 +172,10 @@ module RequireHooks
111
172
  end
112
173
  end
113
174
 
114
- Bootsnap::CompileCache::ISeq.singleton_class.prepend(RequireHooks::Bootsnap::CompileCacheExt)
115
- Bootsnap::CompileCache::ISeq::Compiler.prepend(RequireHooks::Bootsnap::CompilerExt) if defined?(Bootsnap::CompileCache::ISeq::Compiler)
175
+ if defined?(Bootsnap::CompileCache::ISeq::Compiler)
176
+ RequireHooks::Bootsnap.install_compiler_selector
177
+ else
178
+ Bootsnap::CompileCache::ISeq.singleton_class.prepend(RequireHooks::Bootsnap::CompileCacheExt)
179
+ Bootsnap::CompileCache::ISeq.singleton_class.prepend(RequireHooks::Bootsnap::LegacyCacheExt)
180
+ end
116
181
  RubyVM::InstructionSequence.singleton_class.prepend(RequireHooks::Bootsnap::LoadIseqExt)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RequireHooks
4
- VERSION = "0.4.1"
4
+ VERSION = "0.5.1"
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.4.1
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dementyev