Skip to content

Commit f597012

Browse files
author
jneen
committed
abandon guard, use a manual reloader for the dev server
1 parent 8636f5c commit f597012

4 files changed

Lines changed: 99 additions & 5 deletions

File tree

Gemfile

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ gem 'memory_profiler', require: false
1818

1919
group :development do
2020
gem 'pry'
21-
gem 'guard'
22-
gem 'guard-puma'
2321

2422
# Needed for a Rake task
2523
gem 'git'

config.ru

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
# frozen_string_literal: true
22

3-
require 'bundler'
4-
Bundler.require(:default, :development)
5-
63
require_relative 'spec/visual/app'
74

85
run VisualTestApp

spec/visual/app.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,21 @@
33

44
$VERBOSE = true
55

6+
require 'sinatra'
7+
require 'pry'
8+
69
# stdlib
710
require 'pathname'
811

12+
require_relative 'rouge_reloader'
13+
914
class VisualTestApp < Sinatra::Application
1015
BASE = Pathname.new(__dir__)
1116
SAMPLES = BASE.join('samples')
1217
ROOT = BASE.parent.parent
1318

19+
RougeReloader.install!(:Rouge, ROOT.to_s, 'lib/rouge.rb')
20+
1421
DEMOS = ROOT.join('lib/rouge/demos')
1522

1623
def query_string
@@ -58,6 +65,7 @@ def as_boolean(value)
5865
end
5966

6067
before do
68+
RougeReloader.reload!
6169
Rouge::Lexer.enable_debug!
6270
Rouge::Formatter.enable_escape! if params[:escape]
6371

spec/visual/rouge_reloader.rb

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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

Comments
 (0)