homura-runtime 0.2.15 → 0.2.17
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 +88 -1
- data/lib/cloudflare_workers/version.rb +1 -1
- data/lib/cloudflare_workers.rb +12 -0
- 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: 484afdeb8e2980e08e029bf515a07530b08296357eaa6bfbf255a721104358bf
|
|
4
|
+
data.tar.gz: 77bf0079ae55ecdbbe137d6b804d8501a90f66daa3859e22d9e9114fad582830
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 89a21e0c74732fa2b2467ccff72a3ca365c706d0928c9ffb3fc4e4f2b96ecf8688fc4bdbf3beda1e806122d8abca9053d787a528c48d13a3f709aeb8c57ab027
|
|
7
|
+
data.tar.gz: 79975dd3ce599d61824919d790ec05727603043887577ece2af6231136e320514837ca54e710095e4c8a3aadfe06fc9ebf97ae791da0255cf19feea6eef83803
|
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)
|
data/lib/cloudflare_workers.rb
CHANGED
|
@@ -685,7 +685,19 @@ module Cloudflare
|
|
|
685
685
|
# Flatten common keys from `result['meta']` to the top of the hash so
|
|
686
686
|
# callers can write `meta['last_row_id']` without descending into the
|
|
687
687
|
# nested D1 metadata object. Preserves the original shape unchanged.
|
|
688
|
+
#
|
|
689
|
+
# The gem's own Ruby sources are NOT processed by the build's
|
|
690
|
+
# auto-await pass — only user app code is. So when `execute_insert`
|
|
691
|
+
# passes the result of `stmt.run` here, `result` may still be a JS
|
|
692
|
+
# Promise. Await it explicitly so the flatten branch actually runs;
|
|
693
|
+
# otherwise the early `unless result.is_a?(Hash)` would short-circuit
|
|
694
|
+
# and `meta['last_row_id']` would come back as nil at the call site
|
|
695
|
+
# (the caller's auto-await resolves the Promise *after* this method
|
|
696
|
+
# returns).
|
|
688
697
|
def self.flatten_meta(result)
|
|
698
|
+
if defined?(::Cloudflare) && ::Cloudflare.js_promise?(result)
|
|
699
|
+
result = result.__await__
|
|
700
|
+
end
|
|
689
701
|
return result unless result.is_a?(Hash)
|
|
690
702
|
nested = result['meta']
|
|
691
703
|
return result unless nested.is_a?(Hash)
|