|
| 1 | +# frozen_string_literal: true |
| 2 | + |
| 3 | +if !defined?(Ruby::Box) || !Ruby::Box.enabled? |
| 4 | + raise "ReloadableRequire requires Ruby::Box to be enabled" |
| 5 | +end |
| 6 | + |
| 7 | +module IRB |
| 8 | + # Provides reload-aware require functionality for IRB. |
| 9 | + # |
| 10 | + # Limitations: |
| 11 | + # - Native extensions cannot be reloaded (load doesn't support them) |
| 12 | + # - Files loaded via box.require are not tracked |
| 13 | + # - Constant redefinition warnings will appear on reload (uses load internally) |
| 14 | + # - Context mode 5 (running IRB inside a Ruby::Box) is not supported |
| 15 | + # |
| 16 | + # This feature requires Ruby::Box (Ruby 4.0+). |
| 17 | + |
| 18 | + class << self |
| 19 | + def track_and_load_files(source, current_box) |
| 20 | + before = source.dup |
| 21 | + result = yield |
| 22 | + new_files = source - before |
| 23 | + |
| 24 | + return result if new_files.empty? |
| 25 | + |
| 26 | + ruby_files, native_extensions = new_files.partition { |path| path.end_with?('.rb') } |
| 27 | + |
| 28 | + native_extensions.each { |path| current_box.require(path) } |
| 29 | + |
| 30 | + IRB.conf[:__RELOADABLE_FILES__].merge(ruby_files) |
| 31 | + |
| 32 | + main_loaded_features = current_box.eval('$LOADED_FEATURES') |
| 33 | + main_loaded_features.concat(ruby_files - main_loaded_features) |
| 34 | + ruby_files.each { |path| current_box.load(path) } |
| 35 | + |
| 36 | + result |
| 37 | + end |
| 38 | + end |
| 39 | + |
| 40 | + unless Ruby::Box.method_defined?(:__irb_original_require__) |
| 41 | + Ruby::Box.class_eval do |
| 42 | + alias_method :__irb_original_require__, :require |
| 43 | + alias_method :__irb_original_require_relative__, :require_relative |
| 44 | + end |
| 45 | + end |
| 46 | + |
| 47 | + Ruby::Box.class_eval do |
| 48 | + |
| 49 | + def __irb_reloadable_require__(feature) |
| 50 | + unless IRB.conf[:__AUTOLOAD_FILES__].include?(feature) |
| 51 | + return __irb_original_require__(feature) |
| 52 | + end |
| 53 | + |
| 54 | + IRB.conf[:__AUTOLOAD_FILES__].delete(feature) |
| 55 | + IRB.track_and_load_files($LOADED_FEATURES, Ruby::Box.main) { __irb_original_require__(feature) } |
| 56 | + end |
| 57 | + |
| 58 | + def __irb_reloadable_require_relative__(feature) |
| 59 | + __irb_original_require_relative__(feature) |
| 60 | + end |
| 61 | + end |
| 62 | + |
| 63 | + module ReloadableRequire |
| 64 | + class << self |
| 65 | + def extended(base) |
| 66 | + apply_autoload_hook |
| 67 | + end |
| 68 | + |
| 69 | + def apply_autoload_hook |
| 70 | + Ruby::Box.class_eval do |
| 71 | + alias_method :require, :__irb_reloadable_require__ |
| 72 | + alias_method :require_relative, :__irb_reloadable_require_relative__ |
| 73 | + end |
| 74 | + end |
| 75 | + end |
| 76 | + |
| 77 | + private |
| 78 | + |
| 79 | + def reloadable_require_internal(feature, caller_box) |
| 80 | + box = Ruby::Box.new |
| 81 | + box.eval("$LOAD_PATH.concat(#{caller_box.eval('$LOAD_PATH')})") |
| 82 | + box.eval("$LOADED_FEATURES.concat(#{caller_box.eval('$LOADED_FEATURES')})") |
| 83 | + |
| 84 | + IRB.track_and_load_files(box.eval('$LOADED_FEATURES'), caller_box) { box.__irb_original_require__(feature) } |
| 85 | + end |
| 86 | + |
| 87 | + def require(feature) |
| 88 | + caller_loc = caller_locations(1, 1).first |
| 89 | + current_box = Ruby::Box.main |
| 90 | + return current_box.__irb_original_require__(feature) unless caller_loc.path.end_with?("(irb)") |
| 91 | + |
| 92 | + resolved = current_box.eval("$LOAD_PATH.resolve_feature_path(#{feature.dump})") |
| 93 | + return current_box.__irb_original_require__(feature) unless resolved&.first == :rb |
| 94 | + |
| 95 | + reloadable_require_internal(feature, current_box) |
| 96 | + end |
| 97 | + |
| 98 | + def require_relative(feature) |
| 99 | + caller_loc = caller_locations(1, 1).first |
| 100 | + current_box = Ruby::Box.main |
| 101 | + |
| 102 | + unless caller_loc.path.end_with?("(irb)") |
| 103 | + file_path = caller_loc.absolute_path || caller_loc.path |
| 104 | + return current_box.eval("eval('Kernel.require_relative(#{feature.dump})', nil, #{file_path.dump}, #{caller_loc.lineno})") |
| 105 | + end |
| 106 | + |
| 107 | + reloadable_require_internal(File.expand_path(feature, Dir.pwd), current_box) |
| 108 | + end |
| 109 | + |
| 110 | + def autoload(const, feature) |
| 111 | + IRB.conf[:__AUTOLOAD_FILES__] << feature |
| 112 | + Ruby::Box.main.eval("Kernel.autoload(:#{const}, #{feature.dump})") |
| 113 | + end |
| 114 | + end |
| 115 | +end |
0 commit comments