Module: BarefootJS::DevReload
- Defined in:
- lib/barefoot_js/dev_reload.rb
Overview
Framework-agnostic dev-only browser auto-reload for BarefootJS apps.
Ruby port of BarefootJS::DevReload (@barefootjs/perl), companion to
barefoot build --watch in @barefootjs/cli. The CLI drops
<dist>/.dev/build-id after every successful rebuild that changed output;
a browser snippet subscribes to an SSE endpoint that emits event: reload
when that file changes, so an editor save triggers an automatic reload.
require 'barefoot_js/dev_reload'
# Mount the SSE endpoint (dev only) as a plain Rack app, e.g. from
# config.ru's Rack::Builder:
map "#{BASE}/_bf/reload" do
run BarefootJS::DevReload.to_app(dist_dir: 'dist')
end
# And emit the browser snippet before </body> in your layout:
BarefootJS::DevReload.snippet("#{BASE}/_bf/reload")
This is a plain Rack app (not a Sinatra route) so it works the same way under any Rack-based host, mirroring the Perl module's PSGI-app shape.
Constant Summary collapse
- DEV_SUBDIR =
Sentinel path contract with @barefootjs/cli (DEV_SENTINEL_SUBDIR / DEV_SENTINEL_FILENAME in packages/cli/src/lib/build.ts). Duplicated so this package avoids a runtime dep on the CLI — keep in sync with the CLI.
'.dev'- BUILD_ID_FILE =
'build-id'- SCROLL_STORAGE_KEY =
'__bf_devreload_scroll'- HEARTBEAT_S =
Heartbeat < any reasonable proxy/server idle timeout so a quiet connection doesn't get reaped between rebuilds.
5- POLL_S =
Polling instead of a filesystem-event gem (e.g.
listen) keeps the runtime dependency-free. Sub-second latency is imperceptible next to a browser reload. 0.5
Class Method Summary collapse
-
.build_id_path(dist_dir) ⇒ Object
/.dev/build-id — the sentinel barefoot build --watchrewrites. -
.ensure_dev_dir(dist_dir) ⇒ Object
Ensure
/.dev exists so the watcher can write the sentinel even if the server started first. - .read_build_id(path) ⇒ Object
-
.snippet(endpoint) ⇒ Object
The browser snippet: a small IIFE — EventSource subscriber + scrollY preservation across reloads.
-
.to_app(dist_dir: 'dist') ⇒ Object
A ready-made Rack app for the SSE endpoint.
Class Method Details
.build_id_path(dist_dir) ⇒ Object
barefoot build --watch rewrites.
45 46 47 |
# File 'lib/barefoot_js/dev_reload.rb', line 45 def self.build_id_path(dist_dir) File.join(dist_dir, DEV_SUBDIR, BUILD_ID_FILE) end |
.ensure_dev_dir(dist_dir) ⇒ Object
Ensure
51 52 53 54 55 |
# File 'lib/barefoot_js/dev_reload.rb', line 51 def self.ensure_dev_dir(dist_dir) dev = File.join(dist_dir, DEV_SUBDIR) FileUtils.mkdir_p(dev) dev end |
.read_build_id(path) ⇒ Object
57 58 59 60 61 62 63 |
# File 'lib/barefoot_js/dev_reload.rb', line 57 def self.read_build_id(path) return '' unless File.file?(path) File.read(path, encoding: 'UTF-8').strip rescue Errno::ENOENT '' end |
.snippet(endpoint) ⇒ Object
The browser snippet: a small IIFE — EventSource subscriber + scrollY
preservation across reloads. Idempotent across duplicate mounts (the
window.__bfDevReload guard). Returns a plain HTML string; callers embed
it directly (ERB's own <%= never auto-escapes, so no mark_raw needed
the way the Kolon/EP ports require).
70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/barefoot_js/dev_reload.rb', line 70 def self.snippet(endpoint) ep = js_str(endpoint) sk = js_str(SCROLL_STORAGE_KEY) "<script>(function(){if(window.__bfDevReload)return;window.__bfDevReload=1;" \ "try{var s=sessionStorage.getItem(#{sk});if(s){sessionStorage.removeItem(#{sk});" \ "var y=parseInt(s,10);if(!isNaN(y)){var restore=function(){window.scrollTo(0,y)};" \ "if(document.readyState==='loading'){addEventListener('DOMContentLoaded',restore,{once:true})}" \ "else{restore()}}}}catch(e){}var es=new EventSource(#{ep});" \ "es.addEventListener('reload',function(){try{sessionStorage.setItem(#{sk},String(window.scrollY))}" \ "catch(e){}location.reload()});es.addEventListener('error',function(){})})();</script>" end |
.to_app(dist_dir: 'dist') ⇒ Object
A ready-made Rack app for the SSE endpoint. Streams event: reload
whenever : hb heartbeats in
between.
The response body is a lazy Enumerator — Puma (and any Rack server
that doesn't buffer the body) writes each yielded chunk to the socket
as it's produced, giving true incremental streaming without needing
env['rack.hijack']. A write failure on a disconnected client surfaces
as an exception raised back into the Enumerator's block at the y <<
call (Ruby re-raises consumer-side exceptions at the fiber's yield
point) — the rescue below turns that into a clean loop exit, the
same role Perl's local $SIG{PIPE} = 'IGNORE'; eval { ... } plays.
DevReload is automatically a no-op unless mounted, and should only be
mounted in development — see app.rb's DEV guard.
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
# File 'lib/barefoot_js/dev_reload.rb', line 97 def self.to_app(dist_dir: 'dist') path = build_id_path(dist_dir) ensure_dev_dir(dist_dir) lambda do |env| last_event_id = (env['HTTP_LAST_EVENT_ID'] || '').strip body = Enumerator.new do |y| begin y << "retry: 1000\n\n" initial = read_build_id(path) last_sent = '' unless initial.empty? last_sent = initial # A stale Last-Event-ID means a build happened while the client # was disconnected — fire `reload` immediately so the missed # rebuild doesn't stay unpainted. event = (!last_event_id.empty? && last_event_id != initial) ? 'reload' : 'hello' y << "event: #{event}\nid: #{initial}\ndata: #{initial}\n\n" end since_hb = 0 loop do sleep(POLL_S) id = read_build_id(path) if !id.empty? && id != last_sent last_sent = id since_hb = 0 y << "event: reload\nid: #{id}\ndata: #{id}\n\n" else since_hb += POLL_S if since_hb >= HEARTBEAT_S since_hb = 0 y << ": hb\n\n" end end end rescue IOError, Errno::EPIPE, Errno::ECONNRESET # Client disconnected — stop producing chunks. end end # Rack 3 requires lowercase header names (Rack::Lint enforces this in # development; Rack 2 accepted either case). [200, { 'content-type' => 'text/event-stream', 'cache-control' => 'no-cache, no-transform', 'x-accel-buffering' => 'no', }, body] end end |