|
| 1 | +require 'thread' |
| 2 | + |
| 3 | +# [jneen] this reloader is responsible for safely hot-loading Rouge in the dev server. |
| 4 | +# Previously, we were able to simply remove the Rouge constant and load lib/rouge.rb, |
| 5 | +# and all of Rouge would happily be reloaded. However, we have recently switched to |
| 6 | +# require_relative, which maintains its own global cache. Meaning that if we were to |
| 7 | +# remove the Rouge constant and load lib/rouge.rb, the line |
| 8 | +# |
| 9 | +# require_relative 'rouge/lexer' |
| 10 | +# |
| 11 | +# would be a no-op, and hence the constant Rouge::Lexer would not exist. As far as I |
| 12 | +# know there is no way to manually bust this cache, at least until Ruby::Box is stable. |
| 13 | +# |
| 14 | +# This reloader takes the fairly radical approach of replacing require_relative on |
| 15 | +# both main and Kernel with an equivalent implementation that allows us to manually |
| 16 | +# clear the cache. This way, when we load lib/rouge.rb, or trigger lazy { ... } loads |
| 17 | +# in lexers, the caching can be refreshed during reload. |
| 18 | +# |
| 19 | +# When require-ing files *outside* this project, however, it is important that we |
| 20 | +# fall back to the native require cache. |
| 21 | +# |
| 22 | +# Ideally this will all be replaced with an implementation based on Ruby::Box once |
| 23 | +# that is stable. |
| 24 | +class RougeReloader |
| 25 | + MUTEX = Mutex.new |
| 26 | + |
| 27 | + # the targets on which to replace the require_relative method |
| 28 | + TARGETS = [TOPLEVEL_BINDING.receiver.singleton_class, Kernel] |
| 29 | + |
| 30 | + def self.install!(const_name, root, path) |
| 31 | + @instance = new(const_name, root, path) |
| 32 | + @instance.install! |
| 33 | + end |
| 34 | + |
| 35 | + def self.reload! |
| 36 | + @instance.reload! |
| 37 | + end |
| 38 | + |
| 39 | + def initialize(const_name, root, path) |
| 40 | + @const_name = const_name |
| 41 | + @root = root |
| 42 | + @path = File.expand_path(path, root) |
| 43 | + @cache = {} |
| 44 | + end |
| 45 | + |
| 46 | + def reload! |
| 47 | + STDERR.puts "========= reloading #{@const_name} =========" |
| 48 | + |
| 49 | + # remove the global constant |
| 50 | + Object.send(:remove_const, @const_name) if Object.const_defined?(@const_name) |
| 51 | + |
| 52 | + # clear the cache |
| 53 | + @cache.clear |
| 54 | + |
| 55 | + # load rouge.rb |
| 56 | + Kernel.load(@path) |
| 57 | + end |
| 58 | + |
| 59 | + def install! |
| 60 | + # closured variables for visibility in define_method |
| 61 | + cache = @cache |
| 62 | + root = @root |
| 63 | + |
| 64 | + TARGETS.each do |target| |
| 65 | + # undefine the original method to avoid the warning |
| 66 | + target.send(:undef_method, :require_relative) |
| 67 | + |
| 68 | + # somewhat re-implement require_relative |
| 69 | + target.send(:define_method, :require_relative) do |path| |
| 70 | + ref = File.dirname(caller[0].split(":")[0]) |
| 71 | + |
| 72 | + fullpath = File.expand_path(path, ref) |
| 73 | + fullpath += '.rb' unless fullpath.end_with?('.rb') |
| 74 | + |
| 75 | + # files outside the project root (e.g. inside of other gems / stdlib) |
| 76 | + # use the global require cache |
| 77 | + next require(fullpath) unless ref.start_with?(root) |
| 78 | + |
| 79 | + # implement the cache check |
| 80 | + first_load = MUTEX.synchronize do |
| 81 | + next false if cache.key?(fullpath) |
| 82 | + cache[fullpath] = true |
| 83 | + end |
| 84 | + |
| 85 | + # load the file if it is not in cache |
| 86 | + Kernel.load(fullpath) if first_load |
| 87 | + end |
| 88 | + end |
| 89 | + end |
| 90 | +end |
| 91 | + |
0 commit comments