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 +7 -0
- data/lib/barefoot_js/backend/erb.rb +123 -0
- data/lib/barefoot_js/dev_reload.rb +159 -0
- data/lib/barefoot_js/evaluator.rb +714 -0
- data/lib/barefoot_js/search_params.rb +63 -0
- data/lib/barefoot_js.rb +1155 -0
- metadata +51 -0
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
|