homura-runtime 0.1.2 → 0.1.4

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: 99d546ac1db58f957cf5bd81723cfc0e15019f11c27c8b0bcd78fc2ee833b751
4
- data.tar.gz: 02b08e0580b05b710cfba5d5bc6255ba463a4675a297476583dcacece6b158b6
3
+ metadata.gz: 6e37f705dbc9ddf448634efe039bcc390d577c1683ad1759db58fdb1fbceee02
4
+ data.tar.gz: 9dfa51f60c34241302354b5069f10ba560a61c51d4b70a1799893d785d2ae434
5
5
  SHA512:
6
- metadata.gz: 864e05461b1321d1094bdf27ae5bd971472194df29a440ff24f946449db7b98ec93c14ea53a25956ec1e9254b6e1e9015ac2d25978d8f1aadab02f91c69f287a
7
- data.tar.gz: 20c08fdaad6cef869cb4256ffed49be996a3bd6a0e3dd9038bb05f4240a27e75791fefe9bdae1c8dc3f73622413ea95ed90c166dd4810c19cfdf709db6a185f2
6
+ metadata.gz: 0655d2c8e8f889892df7500cfadc663df079be3c7326815523b2fad12cacdce4eb729f859e145bfdc8c26f8ad0bc12bc2d5ee2dfd3acf14e71c82ffafd4057d3
7
+ data.tar.gz: 93c6e9c5607b995d631060f511633bd94b3fa74f9db3c9adee3806b684a1ea7134b136bde12f9c8a3fa7d1da5f3ff76c95929c253251eb7daf3f1f9685bc5342
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.4 (2026-04-23)
4
+
5
+ - Teach the precompiled ERB runtime to support Sinatra-style layout blocks and
6
+ `<%= yield %>` in layout templates.
7
+ - Keep legacy `@content` / `@docs_inner` layouts working as compatibility
8
+ fallbacks while apps migrate to the least-surprise Sinatra style.
9
+
10
+ ## 0.1.3 (2026-04-23)
11
+
12
+ - Fix binary static asset embedding so image responses preserve exact bytes on
13
+ Workers instead of being mangled through text encoding.
14
+ - Add regression coverage for binary-vs-text compile-assets output.
15
+ - Convert shipped mascot/icon assets to real PNG payloads so their bytes match
16
+ their `.png` filenames and `image/png` content type.
17
+
3
18
  ## 0.1.2 (2026-04-23)
4
19
 
5
20
  - Package the runtime's Opal compile-time vendor shims (`digest`, `zlib`,
@@ -7,7 +22,6 @@
7
22
  - Teach `cloudflare-workers-build --standalone` to add packaged gem `vendor/`
8
23
  directories to the Opal load path, so published gems no longer depend on the
9
24
  monorepo root `vendor/`.
10
-
11
25
  ## 0.1.1 (2026-04-23)
12
26
 
13
27
  - Fix `cloudflare-workers-build --standalone` and `exe/auto-await` to resolve only
data/exe/compile-assets CHANGED
@@ -12,6 +12,7 @@
12
12
  # Usage:
13
13
  # ruby bin/compile-assets --input public --output build/homura_assets.rb --namespace HomuraAssets
14
14
 
15
+ require 'base64'
15
16
  require 'fileutils'
16
17
  require 'optparse'
17
18
 
@@ -39,6 +40,13 @@ def mime_for(path)
39
40
  MIME_TYPES[ext] || 'application/octet-stream'
40
41
  end
41
42
 
43
+ def binary_content_type?(content_type)
44
+ !(content_type.start_with?('text/') ||
45
+ content_type.include?('javascript') ||
46
+ content_type.include?('json') ||
47
+ content_type.include?('xml'))
48
+ end
49
+
42
50
  HELP = <<~USAGE
43
51
  Usage:
44
52
  ruby bin/compile-assets [--input DIR] [--output FILE] [--namespace NAME]
@@ -105,10 +113,19 @@ File.open(out_path, 'w') do |io|
105
113
  if (asset = #{ns}::ASSETS[path])
106
114
  headers = {
107
115
  'content-type' => asset[:content_type],
108
- 'content-length' => asset[:body].bytesize.to_s,
109
116
  'cache-control' => 'public, max-age=3600',
110
117
  }
111
- [200, headers, [asset[:body]]]
118
+ if asset[:binary]
119
+ body = ::Cloudflare::EmbeddedBinaryBody.new(
120
+ asset[:body_base64],
121
+ asset[:content_type],
122
+ headers['cache-control']
123
+ )
124
+ [200, headers, [body.raw_response(200, headers)]]
125
+ else
126
+ headers['content-length'] = asset[:body].bytesize.to_s
127
+ [200, headers, [asset[:body]]]
128
+ end
112
129
  else
113
130
  @app.call(env)
114
131
  end
@@ -119,14 +136,20 @@ File.open(out_path, 'w') do |io|
119
136
 
120
137
  files.each do |full_path|
121
138
  rel = full_path.sub(public_dir, '') # e.g. "/style.css"
122
- content = File.read(full_path)
139
+ content = File.binread(full_path)
123
140
  ct = mime_for(full_path)
141
+ binary = binary_content_type?(ct)
124
142
 
125
143
  io.puts
126
144
  io.puts "# #{rel} (#{content.bytesize} bytes)"
127
145
  io.puts "#{ns}::ASSETS[#{rel.inspect}] = {"
128
146
  io.puts " content_type: #{ct.inspect},"
129
- io.puts " body: #{content.inspect}"
147
+ io.puts " binary: #{binary},"
148
+ if binary
149
+ io.puts " body_base64: #{Base64.strict_encode64(content).inspect}"
150
+ else
151
+ io.puts " body: #{content.inspect}"
152
+ end
130
153
  io.puts "}"
131
154
  end
132
155
 
data/exe/compile-erb CHANGED
@@ -43,11 +43,21 @@ USAGE
43
43
  module HomuraERB
44
44
  module_function
45
45
 
46
+ def normalize_fragment(fragment)
47
+ stripped = fragment.strip
48
+ return '__homura_template_yield__' if stripped == 'yield' || stripped == 'yield()'
49
+
50
+ fragment
51
+ end
52
+
46
53
  # Compile an ERB source string to a Ruby method body that assembles
47
54
  # an HTML string in a local variable `_out`. The body references
48
55
  # `@ivars` and method calls directly, so it must be run via
49
56
  # `instance_exec` on the Sinatra instance the route was dispatched
50
- # on. That gives `<%= @name %>` the usual Sinatra semantics.
57
+ # on. That gives `<%= @name %>` the usual Sinatra semantics. Layout
58
+ # templates can also write `<%= yield %>` — we rewrite that specific
59
+ # form to an instance helper because Ruby's `yield` keyword is not
60
+ # valid inside the Proc body we generate here.
51
61
  def compile(source)
52
62
  ruby = +"_out = ''\n"
53
63
  cursor = 0
@@ -75,15 +85,15 @@ module HomuraERB
75
85
  elsif inner.start_with?('==')
76
86
  # `<%== expression %>` — identical to `<%= %>` in this minimal
77
87
  # dialect (no HTML escaping yet; the author is responsible).
78
- expr = inner[2..].strip
88
+ expr = normalize_fragment(inner[2..].strip)
79
89
  ruby << "_out = _out + ((#{expr})).to_s\n"
80
90
  elsif inner.start_with?('=')
81
91
  # `<%= expression %>`
82
- expr = inner[1..].strip
92
+ expr = normalize_fragment(inner[1..].strip)
83
93
  ruby << "_out = _out + ((#{expr})).to_s\n"
84
94
  else
85
95
  # `<% code %>` — Ruby statement(s), emitted verbatim
86
- ruby << inner.strip << "\n"
96
+ ruby << normalize_fragment(inner.strip) << "\n"
87
97
  end
88
98
 
89
99
  cursor = close_idx + 2
@@ -142,10 +152,16 @@ def emit_header(io, namespace)
142
152
  @templates[name.to_sym] = body
143
153
  end
144
154
 
145
- def render(name, instance, locals = {})
155
+ def render(name, instance, locals = {}, &block)
146
156
  body = @templates[name.to_sym]
147
157
  raise "#{namespace}: no template registered for \#{name.inspect}" unless body
148
- instance.instance_exec(locals, &body)
158
+ previous_block = instance.instance_variable_get(:@__homura_template_block__)
159
+ begin
160
+ instance.instance_variable_set(:@__homura_template_block__, block)
161
+ instance.instance_exec(locals, &body)
162
+ ensure
163
+ instance.instance_variable_set(:@__homura_template_block__, previous_block)
164
+ end
149
165
  end
150
166
 
151
167
  def registered?(name)
@@ -187,6 +203,22 @@ def emit_sinatra_patch(io, namespace)
187
203
 
188
204
  module ::Sinatra
189
205
  module Templates
206
+ def __homura_template_yield__
207
+ block = @__homura_template_block__
208
+ raise LocalJumpError, 'no block given' unless block
209
+
210
+ block.call
211
+ end
212
+
213
+ def __homura_default_template_block_for__(template)
214
+ case template.to_sym
215
+ when :layout
216
+ proc { @content } if defined?(@content)
217
+ when :layout_docs
218
+ proc { @docs_inner } if defined?(@docs_inner)
219
+ end
220
+ end
221
+
190
222
  # homura patch: dispatch to precompiled templates when we
191
223
  # have one. Unknown symbols raise a clear message instead of
192
224
  # wandering into upstream Tilt, which would blow up on
@@ -194,7 +226,20 @@ def emit_sinatra_patch(io, namespace)
194
226
  def erb(template, options = {}, locals = {}, &block)
195
227
  if template.is_a?(::Symbol) && ::#{namespace}.registered?(template)
196
228
  locals ||= {}
197
- ::#{namespace}.render(template, self, locals)
229
+ block ||= __homura_default_template_block_for__(template)
230
+ output = ::#{namespace}.render(template, self, locals, &block)
231
+
232
+ layout = options[:layout]
233
+ layout = false if layout.nil? && options.include?(:layout)
234
+ if layout
235
+ layout = :layout if layout == true
236
+ layout = layout.to_sym
237
+ raise "homura: layout \#{layout.inspect} not precompiled; run bin/compile-erb" unless ::#{namespace}.registered?(layout)
238
+
239
+ return ::#{namespace}.render(layout, self, locals) { output }
240
+ end
241
+
242
+ output
198
243
  else
199
244
  raise "homura: erb \#{template.inspect} not precompiled; run bin/compile-erb"
200
245
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CloudflareWorkers
4
- VERSION = '0.1.2'
4
+ VERSION = '0.1.4'
5
5
  end
@@ -313,6 +313,18 @@ module Rack
313
313
  return `new Response(#{js_stream}, { status: #{status.to_i}, headers: #{js_headers} })`
314
314
  end
315
315
 
316
+ if body.is_a?(::Cloudflare::EmbeddedBinaryBody) || (body.respond_to?(:first) && body.first.is_a?(::Cloudflare::EmbeddedBinaryBody))
317
+ bin = body.is_a?(::Cloudflare::EmbeddedBinaryBody) ? body : body.first
318
+ js_stream = bin.stream
319
+ ct = bin.content_type
320
+ cc = bin.cache_control
321
+ js_headers = `({})`
322
+ headers.each { |k, v| ks = k.to_s; vs = v.to_s; `#{js_headers}[#{ks}] = #{vs}` }
323
+ `#{js_headers}['content-type'] = #{ct}` if ct
324
+ `#{js_headers}['cache-control'] = #{cc}` if cc
325
+ return `new Response(#{js_stream}, { status: #{status.to_i}, headers: #{js_headers} })`
326
+ end
327
+
316
328
  # Phase 10 — Workers AI streaming: a Cloudflare::AI::Stream wraps
317
329
  # a JS ReadableStream<Uint8Array> emitting SSE-formatted bytes
318
330
  # ("data: {json}\n\n"). Pass it straight through so the client
@@ -530,6 +542,35 @@ module Cloudflare
530
542
  def close; end
531
543
  end
532
544
 
545
+ # EmbeddedBinaryBody carries a base64-encoded asset payload produced at
546
+ # build time by `compile-assets`, then reconstructs a Uint8Array in the
547
+ # Worker before building the Response stream.
548
+ class EmbeddedBinaryBody
549
+ attr_reader :body_base64, :content_type, :cache_control
550
+
551
+ def initialize(body_base64, content_type = 'application/octet-stream', cache_control = nil)
552
+ @body_base64 = body_base64 || ''
553
+ @content_type = content_type
554
+ @cache_control = cache_control
555
+ end
556
+
557
+ def each; end
558
+
559
+ def close; end
560
+
561
+ def raw_response(status, headers = {})
562
+ js_headers = `({})`
563
+ headers.each { |k, v| ks = k.to_s; vs = v.to_s; `#{js_headers}[#{ks}] = #{vs}` }
564
+ `#{js_headers}['content-type'] = #{@content_type}` if @content_type
565
+ `#{js_headers}['cache-control'] = #{@cache_control}` if @cache_control
566
+ RawResponse.new(`new Response(#{stream}, { status: #{status.to_i}, headers: #{js_headers} })`)
567
+ end
568
+
569
+ def stream
570
+ `(function(b64) { return new ReadableStream({ start(controller) { var bin = globalThis.atob(b64); var len = bin.length; var out = new Uint8Array(len); for (var i = 0; i < len; i++) { out[i] = bin.charCodeAt(i) & 0xff; } controller.enqueue(out); controller.close(); } }); })(#{@body_base64})`
571
+ end
572
+ end
573
+
533
574
  # NOTE: the single-line backtick `...` form is used below instead of the
534
575
  # multi-line `%x{ ... }` or multi-line backtick form. Opal's compiler
535
576
  # treats a *multi-line* x-string as a raw statement and refuses to use
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.1.2
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kazuhiro NISHIYAMA