barefoot_js 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f77e9970654fbb8ba3c2bcd8c6f0d46348260286f58087a4539ef1e2af68be7c
4
+ data.tar.gz: 0d9dd2e4fcf6e840622d3ab15b4759342c2ad2081665e23bf72c8ac25d1e4a14
5
+ SHA512:
6
+ metadata.gz: c40f59ac73e033c93d60bd87e19192cbd577a0709344f49cf477a9159943d1dcb3c4994109fd240f6571b0b43bf1b745a89ba59eb3c4a76abfa87beb168329b8
7
+ data.tar.gz: 3e13dcb87d4031846592504e2eaa97fdcca6221479dde88095b9ab4d6a073aeca6127d624636970a1fefebda3f1031c7699c27165d5e6ee1c1d94b0561563988
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+ require 'json'
5
+
6
+ module BarefootJS
7
+ module Backend
8
+ # ERB rendering backend for the BarefootJS runtime.
9
+ #
10
+ # The engine-agnostic runtime logic -- the JS-compat value helpers,
11
+ # array/string methods, hydration markers, child rendering -- lives in
12
+ # `BarefootJS::Context`. This backend supplies the four engine-specific
13
+ # operations the runtime delegates to, targeting Ruby stdlib ERB:
14
+ #
15
+ # encode_json(data) -> JSON string (injectable encoder)
16
+ # mark_raw(str) -> identity (ERB has no "safe string"
17
+ # wrapper type -- ERB's own `<%=` is
18
+ # NOT auto-escaping, so the compiled
19
+ # templates that need escaping call
20
+ # `bf.h(...)` explicitly; `mark_raw`
21
+ # only exists so runtime helpers that
22
+ # already produce finished HTML --
23
+ # e.g. spread_attrs -- share one
24
+ # interface with the Kolon/EP ports)
25
+ # materialize(value) -> resolve a captured-children value
26
+ # to a string
27
+ # render_named(name, bf, vars) -> render `<name>.erb` with `bf` and
28
+ # `v` (vars, symbol-keyed) bound
29
+ #
30
+ # Pair it with the @barefootjs/erb compile-time adapter, which emits
31
+ # `.erb` templates that call the runtime as a `bf` local: `<%= bf.h(v[:x])
32
+ # %>`, `<%= bf.spread_attrs(bag) %>`. Unlike the Perl backends, this has
33
+ # no dependency on a web framework: a plain directory of `.erb` files
34
+ # renders under any Rack app (or none at all).
35
+ class Erb
36
+ # ERB template locals contract (spec/compiler.md "ERB emission
37
+ # contract"): every compiled template receives exactly two locals --
38
+ # `bf` (the BarefootJS::Context for this render) and `v` (a Hash with
39
+ # SYMBOL keys holding every prop/signal/memo/module-constant the
40
+ # template references). This is enforced by binding `bf` and `v` as
41
+ # local variables in a dedicated eval scope per render (see
42
+ # `render_named`) rather than passing a generic binding, so a template
43
+ # can never accidentally see Ruby method-local state.
44
+ # `cache:` mirrors the Xslate/Kolon backend's `cache => $DEV ? 0 : 1`
45
+ # constructor option: true (default, production) parses each `.erb`
46
+ # file once and reuses the compiled ERB::Compiler output for the life
47
+ # of the process; false (dev) re-reads and re-parses from disk on
48
+ # every `render_named` call, so `bf build --watch` output is picked up
49
+ # on the next request without restarting the server.
50
+ def initialize(path:, json_encoder: nil, cache: true)
51
+ @dir = path
52
+ @json_encoder = json_encoder || ->(data) { JSON.generate(data) }
53
+ @cache = {}
54
+ @cache_enabled = cache
55
+ end
56
+
57
+ def encode_json(data)
58
+ @json_encoder.call(data)
59
+ end
60
+
61
+ # ERB has no "already-safe" string wrapper the way Kolon's `mark_raw`
62
+ # or Mojo::ByteStream do -- stdlib ERB's `<%=` never auto-escapes, so
63
+ # there is nothing to opt out of. Identity, kept only so runtime
64
+ # helpers (`spread_attrs`) share one `backend.mark_raw(...)` call
65
+ # shape across every BarefootJS backend port.
66
+ def mark_raw(str)
67
+ str
68
+ end
69
+
70
+ # JSX children captured by the adapter's buffer-slice capture
71
+ # resolve to a plain String already; a Proc is called and its
72
+ # result used (mirrors the Perl backends' CODE-ref materialisation
73
+ # for engines whose children-capture mechanism yields a callable).
74
+ def materialize(value)
75
+ value.respond_to?(:call) ? value.call : value
76
+ end
77
+
78
+ # Render `<name>.erb` (relative to `path`) with `child_bf` bound as
79
+ # `bf` and `vars` (a Hash, symbol-keyed) bound as `v`.
80
+ def render_named(name, child_bf, vars)
81
+ template = load_template(name)
82
+ Renderer.new(child_bf, vars || {}).render(template)
83
+ end
84
+
85
+ private
86
+
87
+ def load_template(name)
88
+ return build_template(name) unless @cache_enabled
89
+
90
+ @cache[name] ||= build_template(name)
91
+ end
92
+
93
+ def build_template(name)
94
+ file = File.join(@dir, "#{name}.erb")
95
+ src = File.read(file, encoding: 'UTF-8')
96
+ ERB.new(src, trim_mode: '-', eoutvar: '_erbout')
97
+ end
98
+
99
+ # A dedicated per-render binding host. `bf` and `v` are the ONLY
100
+ # locals a compiled template may reference (see the class docstring);
101
+ # giving each render its own tiny object (rather than reusing a
102
+ # shared binding) means concurrent / nested renders never see each
103
+ # other's `v`.
104
+ class Renderer
105
+ def initialize(bf, v)
106
+ @bf = bf
107
+ @v = v
108
+ end
109
+
110
+ def render(template)
111
+ # `bf` / `v` must be real LOCAL variables (not instance
112
+ # variables) at the point `binding` is captured -- ERB templates
113
+ # reference them as bare identifiers (`bf.h(...)`, `v[:x]`), and
114
+ # `Binding` only exposes locals in scope + the object's own
115
+ # instance variables under their own `@`-prefixed names.
116
+ bf = @bf
117
+ v = @v
118
+ template.result(binding)
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module BarefootJS
6
+ # Framework-agnostic dev-only browser auto-reload for BarefootJS apps.
7
+ #
8
+ # Ruby port of BarefootJS::DevReload (@barefootjs/perl), companion to
9
+ # `barefoot build --watch` in @barefootjs/cli. The CLI drops
10
+ # `<dist>/.dev/build-id` after every successful rebuild that changed output;
11
+ # a browser snippet subscribes to an SSE endpoint that emits `event: reload`
12
+ # when that file changes, so an editor save triggers an automatic reload.
13
+ #
14
+ # require 'barefoot_js/dev_reload'
15
+ #
16
+ # # Mount the SSE endpoint (dev only) as a plain Rack app, e.g. from
17
+ # # config.ru's Rack::Builder:
18
+ # map "#{BASE}/_bf/reload" do
19
+ # run BarefootJS::DevReload.to_app(dist_dir: 'dist')
20
+ # end
21
+ #
22
+ # # And emit the browser snippet before </body> in your layout:
23
+ # BarefootJS::DevReload.snippet("#{BASE}/_bf/reload")
24
+ #
25
+ # This is a plain Rack app (not a Sinatra route) so it works the same way
26
+ # under any Rack-based host, mirroring the Perl module's PSGI-app shape.
27
+ module DevReload
28
+ # Sentinel path contract with @barefootjs/cli (DEV_SENTINEL_SUBDIR /
29
+ # DEV_SENTINEL_FILENAME in packages/cli/src/lib/build.ts). Duplicated so
30
+ # this package avoids a runtime dep on the CLI — keep in sync with the CLI.
31
+ DEV_SUBDIR = '.dev'
32
+ BUILD_ID_FILE = 'build-id'
33
+
34
+ SCROLL_STORAGE_KEY = '__bf_devreload_scroll'
35
+
36
+ # Heartbeat < any reasonable proxy/server idle timeout so a quiet
37
+ # connection doesn't get reaped between rebuilds.
38
+ HEARTBEAT_S = 5
39
+ # Polling instead of a filesystem-event gem (e.g. `listen`) keeps the
40
+ # runtime dependency-free. Sub-second latency is imperceptible next to a
41
+ # browser reload.
42
+ POLL_S = 0.5
43
+
44
+ # <dist>/.dev/build-id — the sentinel `barefoot build --watch` rewrites.
45
+ def self.build_id_path(dist_dir)
46
+ File.join(dist_dir, DEV_SUBDIR, BUILD_ID_FILE)
47
+ end
48
+
49
+ # Ensure <dist>/.dev exists so the watcher can write the sentinel even if
50
+ # the server started first.
51
+ def self.ensure_dev_dir(dist_dir)
52
+ dev = File.join(dist_dir, DEV_SUBDIR)
53
+ FileUtils.mkdir_p(dev)
54
+ dev
55
+ end
56
+
57
+ def self.read_build_id(path)
58
+ return '' unless File.file?(path)
59
+
60
+ File.read(path, encoding: 'UTF-8').strip
61
+ rescue Errno::ENOENT
62
+ ''
63
+ end
64
+
65
+ # The browser snippet: a small IIFE — EventSource subscriber + scrollY
66
+ # preservation across reloads. Idempotent across duplicate mounts (the
67
+ # window.__bfDevReload guard). Returns a plain HTML string; callers embed
68
+ # it directly (ERB's own `<%=` never auto-escapes, so no `mark_raw` needed
69
+ # the way the Kolon/EP ports require).
70
+ def self.snippet(endpoint)
71
+ ep = js_str(endpoint)
72
+ sk = js_str(SCROLL_STORAGE_KEY)
73
+ "<script>(function(){if(window.__bfDevReload)return;window.__bfDevReload=1;" \
74
+ "try{var s=sessionStorage.getItem(#{sk});if(s){sessionStorage.removeItem(#{sk});" \
75
+ "var y=parseInt(s,10);if(!isNaN(y)){var restore=function(){window.scrollTo(0,y)};" \
76
+ "if(document.readyState==='loading'){addEventListener('DOMContentLoaded',restore,{once:true})}" \
77
+ "else{restore()}}}}catch(e){}var es=new EventSource(#{ep});" \
78
+ "es.addEventListener('reload',function(){try{sessionStorage.setItem(#{sk},String(window.scrollY))}" \
79
+ "catch(e){}location.reload()});es.addEventListener('error',function(){})})();</script>"
80
+ end
81
+
82
+ # A ready-made Rack app for the SSE endpoint. Streams `event: reload`
83
+ # whenever <dist>/.dev/build-id changes, with `: hb` heartbeats in
84
+ # between.
85
+ #
86
+ # The response body is a lazy `Enumerator` — Puma (and any Rack server
87
+ # that doesn't buffer the body) writes each yielded chunk to the socket
88
+ # as it's produced, giving true incremental streaming without needing
89
+ # `env['rack.hijack']`. A write failure on a disconnected client surfaces
90
+ # as an exception raised back into the Enumerator's block at the `y <<`
91
+ # call (Ruby re-raises consumer-side exceptions at the fiber's yield
92
+ # point) — the `rescue` below turns that into a clean loop exit, the
93
+ # same role Perl's `local $SIG{PIPE} = 'IGNORE'; eval { ... }` plays.
94
+ #
95
+ # DevReload is automatically a no-op unless mounted, and should only be
96
+ # mounted in development — see app.rb's `DEV` guard.
97
+ def self.to_app(dist_dir: 'dist')
98
+ path = build_id_path(dist_dir)
99
+ ensure_dev_dir(dist_dir)
100
+
101
+ lambda do |env|
102
+ last_event_id = (env['HTTP_LAST_EVENT_ID'] || '').strip
103
+
104
+ body = Enumerator.new do |y|
105
+ begin
106
+ y << "retry: 1000\n\n"
107
+
108
+ initial = read_build_id(path)
109
+ last_sent = ''
110
+ unless initial.empty?
111
+ last_sent = initial
112
+ # A stale Last-Event-ID means a build happened while the client
113
+ # was disconnected — fire `reload` immediately so the missed
114
+ # rebuild doesn't stay unpainted.
115
+ event = (!last_event_id.empty? && last_event_id != initial) ? 'reload' : 'hello'
116
+ y << "event: #{event}\nid: #{initial}\ndata: #{initial}\n\n"
117
+ end
118
+
119
+ since_hb = 0
120
+ loop do
121
+ sleep(POLL_S)
122
+ id = read_build_id(path)
123
+ if !id.empty? && id != last_sent
124
+ last_sent = id
125
+ since_hb = 0
126
+ y << "event: reload\nid: #{id}\ndata: #{id}\n\n"
127
+ else
128
+ since_hb += POLL_S
129
+ if since_hb >= HEARTBEAT_S
130
+ since_hb = 0
131
+ y << ": hb\n\n"
132
+ end
133
+ end
134
+ end
135
+ rescue IOError, Errno::EPIPE, Errno::ECONNRESET
136
+ # Client disconnected — stop producing chunks.
137
+ end
138
+ end
139
+
140
+ # Rack 3 requires lowercase header names (Rack::Lint enforces this in
141
+ # development; Rack 2 accepted either case).
142
+ [200, {
143
+ 'content-type' => 'text/event-stream',
144
+ 'cache-control' => 'no-cache, no-transform',
145
+ 'x-accel-buffering' => 'no',
146
+ }, body]
147
+ end
148
+ end
149
+
150
+ # Minimal JS string escape for the handful of characters that can appear
151
+ # in a URL path or storage key. Good enough for package-internal + trusted
152
+ # operator-supplied strings; never interpolate untrusted input here.
153
+ def self.js_str(s)
154
+ t = s.to_s.gsub('\\', '\\\\\\\\').gsub('"', '\\"').gsub("\n", '\\n').gsub("\r", '\\r')
155
+ %("#{t}")
156
+ end
157
+ private_class_method :js_str
158
+ end
159
+ end