homura-runtime 0.2.16 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 411bf1884ba07170427e2f1a905b2848c81e8de9d53caea247006c7d41377f3e
4
- data.tar.gz: 83b2fc17572d59b4c93625553d2e46477e9df01161feaa76be41a8cb8e7acf5d
3
+ metadata.gz: 484afdeb8e2980e08e029bf515a07530b08296357eaa6bfbf255a721104358bf
4
+ data.tar.gz: 77bf0079ae55ecdbbe137d6b804d8501a90f66daa3859e22d9e9114fad582830
5
5
  SHA512:
6
- metadata.gz: 1d8fb86ad2a4aee75cc5c1864b954d04ac8bbda9fd4c10a1151076eeb0996db4eca85f59fcd25ae6721e060f409097a8a83856c30b57bc063e9145156973380e
7
- data.tar.gz: 0a451bc7da52d28fd1d253bfe7b6885caa463b2f5790d53bbd2de0bc49f9ea7efe282c690a8131a0cc4fafa4dd720bc7f3571744280631acf5e423800343f3d5
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CloudflareWorkers
4
- VERSION = '0.2.16'
4
+ VERSION = '0.2.17'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: homura-runtime
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.16
4
+ version: 0.2.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kazuhiro Homma