homura-runtime 0.2.6 → 0.2.8

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: 37cf4aa4a816991923d004144655459aef2da15445ee0c23aaf94105e36e892d
4
- data.tar.gz: ceae9042b8c7613d03f12be573977846838888d74429e829c01b6328f81f2286
3
+ metadata.gz: f2a642d762f1d0c562e0c54d9fc9b1a5299833ad7f58620af82421022f9c6f46
4
+ data.tar.gz: b2858fc319f7e895221437cfafedcf6f559cc7cdddfdc8659ef5195b7f2e4bb5
5
5
  SHA512:
6
- metadata.gz: 96c093338500d78dde842fc48fe28807749e9ccb68e4fb3157e34fa81e95ae54e75b65b7d6708b38d60e782670553ff4b007aa40f73c8ad2fda4a2d6b9d38d91
7
- data.tar.gz: 44967c5dfb3edf5b3bfbb52d4f9e8c6e996436de41121b8324f74aad07c694ef133b725906b156d35df10abf023242003236d1cd955846a8a86d3030bc0d5561
6
+ metadata.gz: 0c6b623fb0618ff32c105ea8dfd104963e85967c154e4fb9b28beb7ce87d625d7889bda1c658133671305bc2c312b569742c90c4b544db646d5cf701de5ea13c
7
+ data.tar.gz: 3a69ce9560b4ef766a247e26f12fc173c56239842aee7fa78e16b7af2679885bc2ef762445b69d89c5c916911968ea817937adee64ac133f00a4c9e44cf1413b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.8 (2026-04-24)
4
+
5
+ - Compile standalone `config.ru` inputs from `build/auto_await/` so ordinary
6
+ `require_relative 'app/app'` entrypoints pick up the auto-awaited app copy
7
+ instead of silently falling back to the original source.
8
+ - Teach the auto-await analyzer to prefer locally inferred helper return types
9
+ over generic registry defaults, and infer memoized `foo ||= ...` helper bodies.
10
+ - Preserve redirect headers when awaited routes resolve to Rack tuples through
11
+ the Promise response builder.
12
+ - Add `layout.erb` by default in the precompiled ERB runtime so `erb :index`
13
+ behaves like ordinary Sinatra unless the app passes `layout: false`.
14
+
15
+ ## 0.2.7 (2026-04-24)
16
+
17
+ - Auto-detect standalone app entrypoints from `config.ru`, `app/hello.rb`, then
18
+ `app/app.rb`, and accept `config.ru` inputs by compiling a temporary Ruby copy
19
+ under the hood.
20
+ - Add the project root to standalone Opal load paths so standard
21
+ `require_relative 'app/app'` config.ru setups compile without extra wrappers.
22
+ - Synthesize `HTTP_HOST` from the request URL inside the Rack env so absolute
23
+ redirects preserve the current host and non-default port in local dev.
24
+
3
25
  ## 0.2.6 (2026-04-24)
4
26
 
5
27
  - Remove the shipped `cloudflare-workers-build` filename entirely and keep the
data/exe/compile-erb CHANGED
@@ -264,6 +264,9 @@ def emit_sinatra_patch(io, namespace)
264
264
 
265
265
  layout = options[:layout]
266
266
  layout = false if layout.nil? && options.include?(:layout)
267
+ if layout.nil? && template.to_sym != :layout && ::#{namespace}.registered?(:layout)
268
+ layout = :layout
269
+ end
267
270
  if layout
268
271
  layout = :layout if layout == true
269
272
  layout = layout.to_sym
data/exe/homura-build CHANGED
@@ -37,7 +37,7 @@ end
37
37
  options = {
38
38
  root: Dir.pwd,
39
39
  standalone: false,
40
- opal_input: ENV['HOMURA_OPAL_INPUT'] || 'app/hello.rb',
40
+ opal_input: ENV['HOMURA_OPAL_INPUT'],
41
41
  opal_output: ENV['HOMURA_OPAL_OUTPUT'] || 'build/hello.no-exit.mjs',
42
42
  patch_input: ENV['HOMURA_OPAL_PATCH_INPUT'],
43
43
  setup_import: nil,
@@ -100,6 +100,7 @@ def run_opal_homura!(root, opal_input, opal_output)
100
100
  argv = [
101
101
  'bundle', 'exec', 'opal',
102
102
  '-c', '-E', '--esm', '--no-source-map',
103
+ '-I', '.',
103
104
  '-I', 'build/auto_await/app', '-I', 'build/auto_await', '-I', 'app',
104
105
  '-I', 'gems/homura-runtime/lib',
105
106
  '-I', 'gems/sinatra-homura/lib',
@@ -147,6 +148,29 @@ def write_entrypoint!(root, out_path, setup:, bundle:, worker_mod:)
147
148
  File.write(root.join(out_path), body)
148
149
  end
149
150
 
151
+ def resolve_opal_input(root, explicit_input)
152
+ return explicit_input if explicit_input
153
+
154
+ candidates = ['config.ru', 'app/hello.rb', 'app/app.rb']
155
+ found = candidates.find { |path| root.join(path).file? }
156
+ return found if found
157
+
158
+ abort("homura build: no default entrypoint found (looked for #{candidates.join(', ')})")
159
+ end
160
+
161
+ def prepare_opal_input(root, input_path)
162
+ rel = Pathname(input_path)
163
+ rel = root.join(rel).relative_path_from(root) if rel.absolute?
164
+ return [rel.to_s, nil] unless rel.extname == '.ru'
165
+
166
+ tmp_dir = root.join('build', 'auto_await')
167
+ tmp_dir = root unless tmp_dir.directory?
168
+ tmp = tmp_dir.join(".homura-build-#{rel.basename}.rb")
169
+ FileUtils.mkdir_p(tmp.dirname)
170
+ File.write(tmp, root.join(rel).read)
171
+ [tmp.relative_path_from(root).to_s, tmp]
172
+ end
173
+
150
174
  unless options[:standalone]
151
175
  run!(['ruby', 'bin/inline-routes-for-opal'], chdir: root)
152
176
  run!(
@@ -184,11 +208,16 @@ unless options[:standalone]
184
208
  chdir: root
185
209
  )
186
210
 
187
- opal_in = Pathname(options[:opal_input])
211
+ opal_in = Pathname(resolve_opal_input(root, options[:opal_input]))
188
212
  opal_out = Pathname(options[:opal_output])
189
213
  opal_in = root.join(opal_in) unless opal_in.absolute?
190
214
  opal_out = root.join(opal_out) unless opal_out.absolute?
191
- run_opal_homura!(root, opal_in.relative_path_from(root).to_s, opal_out.relative_path_from(root).to_s)
215
+ prepared_in, temp_input = prepare_opal_input(root, opal_in)
216
+ begin
217
+ run_opal_homura!(root, prepared_in, opal_out.relative_path_from(root).to_s)
218
+ ensure
219
+ FileUtils.rm_f(temp_input) if temp_input
220
+ end
192
221
  else
193
222
  CloudflareWorkers::BuildSupport.ensure_standalone_runtime(root, current_file: __FILE__)
194
223
  run!(
@@ -218,12 +247,17 @@ else
218
247
  chdir: root
219
248
  )
220
249
 
221
- opal_in = Pathname(options[:opal_input])
250
+ opal_in = Pathname(resolve_opal_input(root, options[:opal_input]))
222
251
  opal_out = Pathname(options[:opal_output])
223
252
  opal_in = root.join(opal_in) unless opal_in.absolute?
224
253
  opal_out = root.join(opal_out) unless opal_out.absolute?
225
- run_opal_standalone!(root, opal_in.relative_path_from(root).to_s, opal_out.relative_path_from(root).to_s,
226
- with_db: options[:with_db])
254
+ prepared_in, temp_input = prepare_opal_input(root, opal_in)
255
+ begin
256
+ run_opal_standalone!(root, prepared_in, opal_out.relative_path_from(root).to_s,
257
+ with_db: options[:with_db])
258
+ ensure
259
+ FileUtils.rm_f(temp_input) if temp_input
260
+ end
227
261
  end
228
262
 
229
263
  patch_rel = Pathname(patch_target)
@@ -144,16 +144,20 @@ module CloudflareWorkers
144
144
  when :send
145
145
  receiver, method_name = *node
146
146
  if receiver.nil?
147
- return @env[method_name] if @env.key?(method_name)
148
147
  return @method_returns[method_name] if @method_returns.key?(method_name)
148
+ return @env[method_name] if @env.key?(method_name)
149
149
  end
150
150
  infer_send_class(node)
151
151
  when :index
152
152
  infer_index_class(node)
153
+ when :begin
154
+ infer_class(node.children.last)
153
155
  when :lvar
154
156
  @env[node.children[0]]
155
157
  when :ivar
156
158
  nil
159
+ when :or_asgn
160
+ infer_class(node.children.last)
157
161
  when :const
158
162
  const_path(node)
159
163
  else
@@ -61,7 +61,7 @@ module CloudflareWorkers
61
61
  hv = vendor_from_gemfile(root)
62
62
  load_paths << hv.to_s if hv
63
63
 
64
- load_paths += ['build/auto_await/app', 'app']
64
+ load_paths += ['.', 'build/auto_await', 'build/auto_await/app', 'app']
65
65
  [
66
66
  gem_lib(RUNTIME_GEM_NAME, loaded_specs: loaded_specs),
67
67
  gem_vendor(RUNTIME_GEM_NAME, loaded_specs: loaded_specs),
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CloudflareWorkers
4
- VERSION = '0.2.6'
4
+ VERSION = '0.2.8'
5
5
  end
@@ -204,6 +204,11 @@ module Rack
204
204
  }
205
205
 
206
206
  copy_headers_into_env(js_req, env)
207
+ env['HTTP_HOST'] = if port == (scheme == 'https' ? '443' : '80')
208
+ host
209
+ else
210
+ "#{host}:#{port}"
211
+ end
207
212
 
208
213
  # Cloudflare-specific extras under their own namespace, per the
209
214
  # Rack convention that env keys other than the standard ones
@@ -400,7 +405,7 @@ module Rack
400
405
  # used for 101 WebSocket upgrades where the Workers
401
406
  # runtime's own Response carries runtime-only properties
402
407
  # (`.webSocket`) that a reconstructed Response would lose.
403
- `Promise.all(#{js_chunks}).then(function(resolved) { var bodyToText = function(v) { if (v == null) { return ''; } if (Array.isArray(v)) { var joined = ''; for (var j = 0; j < v.length; j++) { joined += bodyToText(v[j]); } return joined; } if (typeof v === 'string') { return v; } if (v != null && v.$$is_string) { return v.toString(); } try { return JSON.stringify(v); } catch (e) { return String(v); } }; for (var i = 0; i < resolved.length; i++) { var r = resolved[i]; if (r != null && typeof r === 'object' && typeof r['$raw_response?'] === 'function' && typeof r['$js_response'] === 'function') { try { if (r['$raw_response?']()) { return r['$js_response'](); } } catch (_) {} } } for (var i = 0; i < resolved.length; i++) { var r = resolved[i]; if (r != null && r.stream != null && r.content_type != null) { var bh = {}; bh['content-type'] = r.content_type; if (r.cache_control) bh['cache-control'] = r.cache_control; return new Response(r.stream, { status: #{status_int}, headers: bh }); } } if (resolved.length === 1 && resolved[0] != null && Array.isArray(resolved[0]) && resolved[0].length >= 1 && typeof resolved[0][0] === 'number') { var ov = resolved[0]; var ovs = ov[0]|0; var ovh = #{js_headers}; var ovb = ''; if (ov.length >= 3 && ov[1] != null && typeof ov[1] === 'object' && !Array.isArray(ov[1])) { ovh = {}; var keys = Object.keys(ov[1]); for (var k = 0; k < keys.length; k++) { var key = keys[k]; ovh[key] = String(ov[1][key]); } ovb = bodyToText(ov[2]); } else if (ov.length >= 2) { ovb = bodyToText(ov[ov.length - 1]); } return new Response(ovb, { status: ovs, headers: ovh }); } var parts = []; for (var i = 0; i < resolved.length; i++) { var r = resolved[i]; if (r == null) { parts.push(''); continue; } if (typeof r === 'string') { parts.push(r); continue; } if (r != null && r.$$is_string) { parts.push(r.toString()); continue; } try { parts.push(JSON.stringify(r)); } catch (e) { parts.push(String(r)); } } return new Response(parts.join(''), { status: #{status_int}, headers: #{js_headers} }); })`
408
+ `Promise.all(#{js_chunks}).then(function(resolved) { var bodyToText = function(v) { if (v == null) { return ''; } if (Array.isArray(v)) { var joined = ''; for (var j = 0; j < v.length; j++) { joined += bodyToText(v[j]); } return joined; } if (typeof v === 'string') { return v; } if (v != null && v.$$is_string) { return v.toString(); } try { return JSON.stringify(v); } catch (e) { return String(v); } }; for (var i = 0; i < resolved.length; i++) { var r = resolved[i]; if (r != null && typeof r === 'object' && typeof r['$raw_response?'] === 'function' && typeof r['$js_response'] === 'function') { try { if (r['$raw_response?']()) { return r['$js_response'](); } } catch (_) {} } } for (var i = 0; i < resolved.length; i++) { var r = resolved[i]; if (r != null && r.stream != null && r.content_type != null) { var bh = {}; bh['content-type'] = r.content_type; if (r.cache_control) bh['cache-control'] = r.cache_control; return new Response(r.stream, { status: #{status_int}, headers: bh }); } } if (resolved.length === 1 && resolved[0] != null && Array.isArray(resolved[0]) && resolved[0].length >= 1 && typeof resolved[0][0] === 'number') { var ov = resolved[0]; var ovs = ov[0]|0; var ovh = #{Cloudflare}.$headers_to_js(nil, #{js_headers}); var ovb = ''; if (ov.length >= 3 && ov[1] != null) { ovh = #{Cloudflare}.$headers_to_js(ov[1], #{js_headers}); ovb = bodyToText(ov[2]); } else if (ov.length >= 2) { ovb = bodyToText(ov[ov.length - 1]); } return new Response(ovb, { status: ovs, headers: ovh }); } var parts = []; for (var i = 0; i < resolved.length; i++) { var r = resolved[i]; if (r == null) { parts.push(''); continue; } if (typeof r === 'string') { parts.push(r); continue; } if (r != null && r.$$is_string) { parts.push(r.toString()); continue; } try { parts.push(JSON.stringify(r)); } catch (e) { parts.push(String(r)); } } return new Response(parts.join(''), { status: #{status_int}, headers: #{js_headers} }); })`
404
409
  else
405
410
  body_str = ''
406
411
  chunks.each { |c| body_str = body_str + c.to_s }
@@ -507,6 +512,21 @@ module Cloudflare
507
512
  h
508
513
  end
509
514
 
515
+ def self.headers_to_js(headers, fallback = nil)
516
+ return fallback || `({})` if headers.nil?
517
+ return headers if `#{headers} != null && #{headers}.constructor === Object`
518
+
519
+ js_headers = fallback || `({})`
520
+ if headers.respond_to?(:each)
521
+ headers.each do |key, value|
522
+ ks = key.to_s
523
+ vs = value.to_s
524
+ `#{js_headers}[#{ks}] = #{vs}`
525
+ end
526
+ end
527
+ js_headers
528
+ end
529
+
510
530
  # RawResponse wraps an already-constructed JS `Response` so routes
511
531
  # can return it through Sinatra and have `build_js_response` pass
512
532
  # it through to the Workers runtime untouched. Needed when the
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.6
4
+ version: 0.2.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kazuhiro Homma