homura-runtime 0.2.16 → 0.2.18
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 +4 -4
- data/CHANGELOG.md +14 -0
- data/exe/compile-erb +153 -1
- data/lib/cloudflare_workers/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1bd6f3a366405aeb0ba2673c4827c7d34d8bef757d9194c64e71ca881bfe821f
|
|
4
|
+
data.tar.gz: 1ccc18ce83b42969bac914ecda37c21cdbfe79fd86971a1de1df1b25c3014302
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 777ce1497af06f6a5168bef5e195576fcdbd216a7da25b5d0eda657fea4757f074fb48f873f28eb3f6c5854c5ce98bf1f01dfa870b31a067eab93ef7dd8b9dce
|
|
7
|
+
data.tar.gz: 6a9e88cbfea6b27e46dc35c1bdb24bc02cdba8899839066d9d547057e18d9d67a319fdabed88a4ad36f852af49fd97bf41ef3aad964b720333bdd29c5c1438f6
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.17 (2026-04-27)
|
|
4
|
+
|
|
5
|
+
- Rewrite class-variable references (`@@foo`) inside precompiled ERB
|
|
6
|
+
templates into explicit `class_variable_get` / `class_variable_set`
|
|
7
|
+
calls on the instance's class. Opal evaluates compiled template
|
|
8
|
+
bodies via `instance_exec` on a Sinatra instance whose `$$cvars`
|
|
9
|
+
slot is undefined at that runtime path, so the previous build emit
|
|
10
|
+
blew up with `TypeError: Cannot read properties of undefined (reading
|
|
11
|
+
'$$cvars')` whenever a template touched `<%= @@todos %>` or
|
|
12
|
+
`<% @@todos.each ... %>` directly. Templates can now use the
|
|
13
|
+
natural Sinatra style (`@@cvar` reads, `@@cvar = expr` and compound
|
|
14
|
+
`@@cvar op= expr` assignments) without route-level `@todos = @@todos`
|
|
15
|
+
shims. Fixes #28.
|
|
16
|
+
|
|
3
17
|
## 0.2.11 (2026-04-25)
|
|
4
18
|
|
|
5
19
|
- Normalize bare JS `undefined` / `null` values to Ruby `nil` while converting
|
data/exe/compile-erb
CHANGED
|
@@ -54,7 +54,94 @@ module HomuraERB
|
|
|
54
54
|
|
|
55
55
|
def normalize_fragment(fragment)
|
|
56
56
|
return '__homura_template_yield__' if supported_yield_fragment?(fragment)
|
|
57
|
-
fragment
|
|
57
|
+
rewrite_class_variables(fragment)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Opal compiles class-variable reads/writes (`@@foo`) into accessors
|
|
61
|
+
# bound to the lexically enclosing class, but precompiled ERB bodies
|
|
62
|
+
# are evaluated through `instance_exec` on a Sinatra instance whose
|
|
63
|
+
# `$$cvars` slot is undefined at that runtime path. The result is a
|
|
64
|
+
# `TypeError: Cannot read properties of undefined (reading '$$cvars')`
|
|
65
|
+
# the moment a template touches `<%= @@todos %>` directly — exactly
|
|
66
|
+
# the natural Sinatra style we want users to be able to keep writing.
|
|
67
|
+
#
|
|
68
|
+
# Rewrite both reads (`@@foo`) and writes (`@@foo = expr`) into
|
|
69
|
+
# explicit `class_variable_get` / `class_variable_set` calls on the
|
|
70
|
+
# instance's class so the same source compiles cleanly under Opal
|
|
71
|
+
# without requiring `@foo = @@foo` shims at the route layer.
|
|
72
|
+
#
|
|
73
|
+
# The rewrite is a deliberately small regex pass: ERB fragments are
|
|
74
|
+
# short, and class-variable tokens inside Ruby string literals or
|
|
75
|
+
# comments are vanishingly rare in real templates. Anything more
|
|
76
|
+
# ambitious would need a Ruby parser, which is overkill for the
|
|
77
|
+
# `@@ivar` shape we actually have to handle here.
|
|
78
|
+
CVAR_OP_ASSIGN_RE = /(?<![@\w])@@([A-Za-z_]\w*)\s*(\|\||&&|\+|-|\*|\/|%|\*\*|<<|>>|\||&|\^)=\s*([^;\n]+)/.freeze
|
|
79
|
+
CVAR_ASSIGN_RE = /(?<![@\w])@@([A-Za-z_]\w*)\s*=(?!=)\s*([^;\n]+)/.freeze
|
|
80
|
+
# Read pattern excludes `@@name` that appears on the left-hand side
|
|
81
|
+
# of an assignment (`@@name =`, `@@name +=`, `@@name ||=`, etc.) so
|
|
82
|
+
# the assignment passes still see the raw token to rewrite. The
|
|
83
|
+
# `=(?!=)` guard keeps `==` (comparison) treated as a read.
|
|
84
|
+
CVAR_READ_RE = %r{
|
|
85
|
+
(?<![@\w])@@([A-Za-z_]\w*)\b
|
|
86
|
+
(?!
|
|
87
|
+
\s*
|
|
88
|
+
(?:\|\||&&|\+|-|\*|/|%|\*\*|<<|>>|\||&|\^)?
|
|
89
|
+
=(?!=)
|
|
90
|
+
)
|
|
91
|
+
}x.freeze
|
|
92
|
+
|
|
93
|
+
CVAR_PLACEHOLDER_PREFIX = "\x01HOMURA_CVAR\x01".freeze
|
|
94
|
+
CVAR_PLACEHOLDER_SUFFIX = "\x01".freeze
|
|
95
|
+
|
|
96
|
+
def rewrite_class_variables(fragment)
|
|
97
|
+
return fragment unless fragment.include?('@@')
|
|
98
|
+
|
|
99
|
+
placeholders = []
|
|
100
|
+
record = lambda do |replacement|
|
|
101
|
+
placeholders << replacement
|
|
102
|
+
"#{CVAR_PLACEHOLDER_PREFIX}#{placeholders.length - 1}#{CVAR_PLACEHOLDER_SUFFIX}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Reads first: rewrite every `@@name` that is *not* the target of
|
|
106
|
+
# an assignment, including occurrences on the right-hand side of
|
|
107
|
+
# an assignment. Each match collapses into a sentinel so the
|
|
108
|
+
# later assignment passes see the original `@@name = ...` shape
|
|
109
|
+
# (without their RHS reads being clobbered) and so the final
|
|
110
|
+
# output never re-enters this same rewrite loop.
|
|
111
|
+
work = fragment.gsub(CVAR_READ_RE) do
|
|
112
|
+
name = Regexp.last_match(1)
|
|
113
|
+
record.call("self.class.class_variable_get(:@@#{name})")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
work = work.gsub(CVAR_OP_ASSIGN_RE) do
|
|
117
|
+
name = Regexp.last_match(1)
|
|
118
|
+
op = Regexp.last_match(2)
|
|
119
|
+
value = Regexp.last_match(3)
|
|
120
|
+
replacement =
|
|
121
|
+
case op
|
|
122
|
+
when '||'
|
|
123
|
+
"self.class.class_variable_set(:@@#{name}, (self.class.class_variable_defined?(:@@#{name}) ? self.class.class_variable_get(:@@#{name}) : nil) || (#{value}))"
|
|
124
|
+
when '&&'
|
|
125
|
+
"self.class.class_variable_set(:@@#{name}, (self.class.class_variable_defined?(:@@#{name}) ? self.class.class_variable_get(:@@#{name}) : nil) && (#{value}))"
|
|
126
|
+
else
|
|
127
|
+
"self.class.class_variable_set(:@@#{name}, self.class.class_variable_get(:@@#{name}) #{op} (#{value}))"
|
|
128
|
+
end
|
|
129
|
+
record.call(replacement)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
work = work.gsub(CVAR_ASSIGN_RE) do
|
|
133
|
+
name = Regexp.last_match(1)
|
|
134
|
+
value = Regexp.last_match(2)
|
|
135
|
+
record.call("self.class.class_variable_set(:@@#{name}, (#{value}))")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
placeholder_re = /#{Regexp.escape(CVAR_PLACEHOLDER_PREFIX)}(\d+)#{Regexp.escape(CVAR_PLACEHOLDER_SUFFIX)}/
|
|
139
|
+
loop do
|
|
140
|
+
replaced = work.gsub(placeholder_re) { placeholders[Regexp.last_match(1).to_i] }
|
|
141
|
+
break if replaced == work
|
|
142
|
+
work = replaced
|
|
143
|
+
end
|
|
144
|
+
work
|
|
58
145
|
end
|
|
59
146
|
|
|
60
147
|
def validate_code_fragment!(fragment)
|
|
@@ -200,11 +287,21 @@ def emit_header(io, namespace)
|
|
|
200
287
|
body = @templates[name.to_sym]
|
|
201
288
|
raise "#{namespace}: no template registered for \#{name.inspect}" unless body
|
|
202
289
|
previous_block = instance.instance_variable_get(:@__homura_template_block__)
|
|
290
|
+
previous_locals = instance.instance_variable_get(:@__homura_template_locals__)
|
|
203
291
|
begin
|
|
204
292
|
instance.instance_variable_set(:@__homura_template_block__, block)
|
|
293
|
+
# Stack locals so nested `erb :_partial, locals: {...}` calls
|
|
294
|
+
# restore the outer scope on the way out — Sinatra's standard
|
|
295
|
+
# `locals[:t]` -> bare `t` lookup is implemented via
|
|
296
|
+
# method_missing on the Sinatra instance (see Sinatra::Base
|
|
297
|
+
# patch installed by emit_sinatra_patch).
|
|
298
|
+
stack = previous_locals.is_a?(::Array) ? previous_locals.dup : []
|
|
299
|
+
stack.push(locals || {})
|
|
300
|
+
instance.instance_variable_set(:@__homura_template_locals__, stack)
|
|
205
301
|
instance.instance_exec(locals, &body)
|
|
206
302
|
ensure
|
|
207
303
|
instance.instance_variable_set(:@__homura_template_block__, previous_block)
|
|
304
|
+
instance.instance_variable_set(:@__homura_template_locals__, previous_locals)
|
|
208
305
|
end
|
|
209
306
|
end
|
|
210
307
|
|
|
@@ -254,6 +351,61 @@ def emit_sinatra_patch(io, namespace)
|
|
|
254
351
|
block.call
|
|
255
352
|
end
|
|
256
353
|
|
|
354
|
+
# Resolve a Sinatra-style local variable reference.
|
|
355
|
+
# `erb :_partial, locals: { t: row }` causes the precompiled body
|
|
356
|
+
# to reference a bare `t`. Stock Sinatra uses Ruby's `binding`
|
|
357
|
+
# eval to install locals into the template's scope; we don't have
|
|
358
|
+
# `binding` under Opal/Workers, so we hand-roll the lookup
|
|
359
|
+
# through method_missing against the locals stack maintained by
|
|
360
|
+
# the renderer.
|
|
361
|
+
def __homura_template_local__(name)
|
|
362
|
+
stack = @__homura_template_locals__
|
|
363
|
+
return nil unless stack.is_a?(::Array)
|
|
364
|
+
key = name.to_sym
|
|
365
|
+
stack.reverse_each do |frame|
|
|
366
|
+
next unless frame.is_a?(::Hash)
|
|
367
|
+
return frame[key] if frame.key?(key)
|
|
368
|
+
sk = name.to_s
|
|
369
|
+
return frame[sk] if frame.key?(sk)
|
|
370
|
+
end
|
|
371
|
+
nil
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def __homura_template_local_defined?(name)
|
|
375
|
+
stack = @__homura_template_locals__
|
|
376
|
+
return false unless stack.is_a?(::Array)
|
|
377
|
+
key = name.to_sym
|
|
378
|
+
sk = name.to_s
|
|
379
|
+
stack.reverse_each do |frame|
|
|
380
|
+
next unless frame.is_a?(::Hash)
|
|
381
|
+
return true if frame.key?(key) || frame.key?(sk)
|
|
382
|
+
end
|
|
383
|
+
false
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# Method-missing fallback that surfaces template locals as bare names.
|
|
389
|
+
# Installed on Sinatra::Base so `<%= t['title'] %>` inside a
|
|
390
|
+
# precompiled partial Just Works without the user having to write
|
|
391
|
+
# `<%= locals[:t]['title'] %>`.
|
|
392
|
+
class ::Sinatra::Base
|
|
393
|
+
def method_missing(name, *args, &block)
|
|
394
|
+
if args.empty? && block.nil? && __homura_template_local_defined?(name)
|
|
395
|
+
return __homura_template_local__(name)
|
|
396
|
+
end
|
|
397
|
+
super
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def respond_to_missing?(name, include_private = false)
|
|
401
|
+
return true if __homura_template_local_defined?(name)
|
|
402
|
+
super
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
module ::Sinatra
|
|
407
|
+
module Templates
|
|
408
|
+
|
|
257
409
|
def __homura_default_template_block_for__(template)
|
|
258
410
|
case template.to_sym
|
|
259
411
|
when :layout
|