Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions lib/funicular/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ class Railtie < Rails::Railtie
initializer "funicular.middleware" do |app|
if Rails.env.development?
app.middleware.use Funicular::Middleware
# The middleware recompiles the client bundle on change; SSR
# must reload the same sources, or server-rendered markup goes
# stale until a restart while hydration is already fresh.
require "funicular/ssr/runtime"
Funicular::SSR::Runtime.auto_reload = true
end
end

Expand Down
25 changes: 23 additions & 2 deletions lib/funicular/ssr/runtime.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,16 @@ def load_framework!
# In that case we rescue and continue: the AR constant is already defined
# and is all that load_schemas needs (it ignores the hash on the server).
#
# Loaded once per process. Restart the server to pick up changes.
# Loaded once per process. With auto_reload (the railtie enables
# it in development), edited sources are picked up on the next
# render: reloading re-runs the initializer, and the server-side
# Funicular.start builds a fresh router, so routes refresh too.
# Without it, restart the server to pick up changes.
def boot!(source_dir)
load_framework!
return if @app_loaded
if @app_loaded
return unless auto_reload && sources_changed?(source_dir)
end

files = Funicular::Compiler.source_files(source_dir.to_s)
files.each do |file|
Expand All @@ -89,6 +95,21 @@ def boot!(source_dir)
end
end
@app_loaded = true
@sources_snapshot = sources_snapshot(source_dir)
end

# Reload edited app sources on the next boot! instead of
# requiring a server restart (meant for development).
attr_accessor :auto_reload

def sources_changed?(source_dir)
@sources_snapshot != sources_snapshot(source_dir)
end

def sources_snapshot(source_dir)
Funicular::Compiler.source_files(source_dir.to_s).map do |file|
[ file, File.exist?(file) ? File.mtime(file).to_f : nil ]
end
end
Comment on lines +119 to 123

# Test/escape hatch: forget loaded application state so a different
Expand Down
94 changes: 94 additions & 0 deletions minitest/ssr_reload_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# frozen_string_literal: true

require "fileutils"
require "tmpdir"

require "test_helper"

# Development-mode SSR reload: with auto_reload enabled (the railtie
# turns it on in development), editing an app source makes the next
# boot! re-load the sources, so server-rendered markup matches what the
# recompiled client bundle will hydrate.
class SSRReloadTest < Minitest::Test
def setup
Funicular::SSR::Runtime.load_framework!
end

def teardown
Funicular::SSR::Runtime.auto_reload = false
Funicular::SSR::Runtime.reset_app!
end

def write_component(dir, title)
File.write(File.join(dir, "components", "reload_probe_component.rb"), <<~RUBY)
class ReloadProbeComponent < Funicular::Component
def render
div { "#{title}" }
end
end
RUBY
end

def build_app(dir)
FileUtils.mkdir_p(File.join(dir, "components"))
write_component(dir, "before reload")
File.write(File.join(dir, "initializer.rb"), <<~RUBY)
Funicular.start(container: "app") do |router|
router.get("/probe", to: ReloadProbeComponent, as: "probe")
end
RUBY
end

def render_probe(dir)
Funicular::SSR.render(path: "/probe", source_dir: dir)[:html]
end

def quietly
original_verbose = $VERBOSE
$VERBOSE = nil
yield
ensure
$VERBOSE = original_verbose
end

def test_edited_sources_are_reloaded_when_auto_reload_is_on
Dir.mktmpdir do |dir|
build_app(dir)
Funicular::SSR::Runtime.reset_app!
assert_includes render_probe(dir), "before reload"

write_component(dir, "after reload")
bump_mtime(dir)

# Off (the default): still the stale markup.
assert_includes render_probe(dir), "before reload"

Funicular::SSR::Runtime.auto_reload = true
quietly do
assert_includes render_probe(dir), "after reload"
end
end
end

def test_unchanged_sources_are_not_reloaded
Dir.mktmpdir do |dir|
build_app(dir)
Funicular::SSR::Runtime.reset_app!
Funicular::SSR::Runtime.auto_reload = true
quietly { render_probe(dir) }

refute Funicular::SSR::Runtime.sources_changed?(dir)
end
end

private

# mtime comparison, not equality of content: make the edit observable
# even on filesystems with coarse timestamps.
def bump_mtime(dir)
future = Time.now + 2
Dir.glob(File.join(dir, "**", "*.rb")).each do |file|
File.utime(future, future, file)
end
end
end
Loading