homura-runtime 0.1.3 → 0.1.5

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: 328d7e98210fb60bb229a46166aa605b2c9e2a85baca8418cac31a0ac3688311
4
- data.tar.gz: 3e3c0562902a031343eddf788a078d1556649c286ec79e44295626a0be2058a3
3
+ metadata.gz: b3d8286719ce164088136af4d1caf0476c026d4ec2e42b98b08db8c84df6766d
4
+ data.tar.gz: 849f3bb4ae14131b950de3a89efff6032a4204e9878a19c84cfe9dcdfa618e8b
5
5
  SHA512:
6
- metadata.gz: 971c5697869ed0ecc0ba57d6aa5263c6f3b36f5e2bfa0c5e6efc67efe5bb5aade01dd0b522aaa685c4226074924b5a6e8269135fd6a12bdf1f8a6592d3b120cb
7
- data.tar.gz: 0ef026fa796bfedb3960b42c1f2ea6a09a1d382d9883d16fd744767d3967a3649f0ee9cb322e80f1ddff6db752f05f12da94c62b04c238fe8f43aa4143b667d8
6
+ metadata.gz: c692cab5b2375646806e28b72b2e3c82da48563dcfc6a9074b8a8f3d9a756a92caee4ce100eee22bfaf81d00a459b3b85541176565fc1c3de5545f5c08b5216b
7
+ data.tar.gz: 0eda9cfebebb75b1efd6764d6be2b27710769e30a215e9c9cb5b9d635534afbf685cc8aeac632ad09378ecf106768d9b395093f4055c2293179b81817484f8a6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.5 (2026-04-23)
4
+
5
+ - Make `auto-await` emit rewritten files for existing hand-written `.__await__`
6
+ usage when the only missing piece is `# await: true`.
7
+ - Make `cloudflare-workers-build --standalone` restore `cf-runtime/` from the
8
+ packaged gem and derive standalone template/asset namespaces from the project
9
+ name by default, with explicit override flags when needed.
10
+ - Reject unsupported ERB yield forms like `<% yield %>` and `yield(arg)` with
11
+ compile-time guidance toward the supported Sinatra-style layout forms.
12
+
13
+ ## 0.1.4 (2026-04-23)
14
+
15
+ - Teach the precompiled ERB runtime to support Sinatra-style layout blocks and
16
+ `<%= yield %>` in layout templates.
17
+ - Keep legacy `@content` / `@docs_inner` layouts working as compatibility
18
+ fallbacks while apps migrate to the least-surprise Sinatra style.
19
+
3
20
  ## 0.1.3 (2026-04-23)
4
21
 
5
22
  - Fix binary static asset embedding so image responses preserve exact bytes on
data/README.md CHANGED
@@ -9,7 +9,7 @@ Core Ruby + Module Worker glue for [Opal](https://opalrb.com/) on [Cloudflare Wo
9
9
  - `runtime/worker_module.mjs` — fetch / scheduled / queue / DO adapters (**no Opal bundle import**).
10
10
  - `runtime/worker.mjs` — thin bootstrap (crypto shim → bundle → `worker_module`) for legacy layouts.
11
11
  - `runtime/setup-node-crypto.mjs` — `node:crypto` on `globalThis` before the Opal bundle loads.
12
- - `bin/cloudflare-workers-build` — single build pipeline (ERB → assets → Opal → patch → `worker.entrypoint.mjs`). Use `--standalone` in generated apps.
12
+ - `bin/cloudflare-workers-build` — single build pipeline (ERB → assets → Opal → patch → `worker.entrypoint.mjs`). Use `--standalone` in generated apps; it now restores `cf-runtime/` automatically and derives standalone template/asset namespaces from the project name by default.
13
13
  - `docs/ARCHITECTURE.md` — wrangler `main`, codegen entrypoint, and fixed-import policy.
14
14
 
15
15
  ## Quick start (homura monorepo)
@@ -44,7 +44,9 @@ options = {
44
44
  bundle_import: nil,
45
45
  worker_module_import: nil,
46
46
  entrypoint_out: nil,
47
- with_db: false
47
+ with_db: false,
48
+ templates_namespace: nil,
49
+ assets_namespace: nil
48
50
  }
49
51
 
50
52
  OptionParser.new do |o|
@@ -63,9 +65,13 @@ OptionParser.new do |o|
63
65
  o.on('--bundle-import PATH', 'Opal bundle import in entrypoint') { |p| options[:bundle_import] = p }
64
66
  o.on('--worker-module-import PATH', 'worker_module.mjs import in entrypoint') { |p| options[:worker_module_import] = p }
65
67
  o.on('--entrypoint-out PATH', 'Where to write worker.entrypoint.mjs') { |p| options[:entrypoint_out] = p }
68
+ o.on('--templates-namespace NAME', 'Standalone templates module name (default: project-derived)') { |n| options[:templates_namespace] = n }
69
+ o.on('--assets-namespace NAME', 'Standalone assets module name (default: project-derived)') { |n| options[:assets_namespace] = n }
66
70
  end.parse!
67
71
 
68
72
  root = Pathname(options[:root]).expand_path
73
+ options[:templates_namespace] ||= CloudflareWorkers::BuildSupport.standalone_namespace(root, 'Templates') if options[:standalone]
74
+ options[:assets_namespace] ||= CloudflareWorkers::BuildSupport.standalone_namespace(root, 'Assets') if options[:standalone]
69
75
 
70
76
  if options[:standalone]
71
77
  Dir.chdir(root) { require 'bundler/setup' }
@@ -199,12 +205,13 @@ unless options[:standalone]
199
205
  opal_out = root.join(opal_out) unless opal_out.absolute?
200
206
  run_opal_homura!(root, opal_in.relative_path_from(root).to_s, opal_out.relative_path_from(root).to_s)
201
207
  else
208
+ CloudflareWorkers::BuildSupport.ensure_standalone_runtime(root, current_file: __FILE__)
202
209
  run!(
203
210
  [
204
211
  'ruby', CloudflareWorkersBuild.exe_path('compile-erb').to_s,
205
212
  '--input', 'views',
206
213
  '--output', 'build/app_templates.rb',
207
- '--namespace', 'AppTemplates'
214
+ '--namespace', options[:templates_namespace]
208
215
  ],
209
216
  chdir: root
210
217
  )
@@ -213,7 +220,7 @@ else
213
220
  'ruby', CloudflareWorkersBuild.exe_path('compile-assets').to_s,
214
221
  '--input', 'public',
215
222
  '--output', 'build/app_assets.rb',
216
- '--namespace', 'AppAssets'
223
+ '--namespace', options[:assets_namespace]
217
224
  ],
218
225
  chdir: root
219
226
  )
data/exe/auto-await CHANGED
@@ -72,18 +72,25 @@ paths.each do |path|
72
72
  # needed to tell Opal to wrap the file in an async function and to
73
73
  # translate #__await__ calls into real JS await keywords.
74
74
  has_magic = source.lines.first(5).any? { |l| l.match?(/#\s*await:/) }
75
+ has_existing_await = source.include?('.__await__')
75
76
 
76
77
  begin
77
78
  analyzer = CloudflareWorkers::AutoAwait::Analyzer.new(registry, debug: options[:debug])
78
79
  buffer, nodes = analyzer.process(source, path)
80
+ needs_magic_only_output = has_existing_await && !has_magic
79
81
 
80
- if nodes.empty?
82
+ if nodes.empty? && !needs_magic_only_output
81
83
  puts "[auto-await] skip (no changes) #{rel}" if options[:debug]
82
84
  skipped += 1
83
85
  next
84
86
  end
85
87
 
86
- transformed = CloudflareWorkers::AutoAwait::Transformer.transform(source, nodes, buffer)
88
+ transformed =
89
+ if nodes.empty?
90
+ source
91
+ else
92
+ CloudflareWorkers::AutoAwait::Transformer.transform(source, nodes, buffer)
93
+ end
87
94
  unless has_magic
88
95
  transformed = "# await: true\n" + transformed
89
96
  end
data/exe/compile-erb CHANGED
@@ -43,11 +43,48 @@ USAGE
43
43
  module HomuraERB
44
44
  module_function
45
45
 
46
+ def supported_yield_fragment?(fragment)
47
+ stripped = fragment.strip
48
+ stripped.match?(/\Ayield(?:\(\s*\))?\z/)
49
+ end
50
+
51
+ def yield_with_args_fragment?(fragment)
52
+ fragment.strip.match?(/\Ayield\s*\(.+\)\z/)
53
+ end
54
+
55
+ def normalize_fragment(fragment)
56
+ return '__homura_template_yield__' if supported_yield_fragment?(fragment)
57
+ fragment
58
+ end
59
+
60
+ def validate_code_fragment!(fragment)
61
+ stripped = fragment.strip
62
+ return unless supported_yield_fragment?(stripped) || yield_with_args_fragment?(stripped)
63
+
64
+ raise <<~MSG.strip
65
+ Unsupported ERB yield form `<% #{stripped} %>`.
66
+ Use `<%= yield %>` or `<%== yield %>` in layout templates.
67
+ MSG
68
+ end
69
+
70
+ def validate_expression_fragment!(fragment)
71
+ stripped = fragment.strip
72
+ return unless yield_with_args_fragment?(stripped)
73
+
74
+ raise <<~MSG.strip
75
+ Unsupported ERB yield form `<%= #{stripped} %>`.
76
+ `yield(arg)` is not supported; use `<%= yield %>` or `<%== yield %>`, or pass data via ivars/locals instead.
77
+ MSG
78
+ end
79
+
46
80
  # Compile an ERB source string to a Ruby method body that assembles
47
81
  # an HTML string in a local variable `_out`. The body references
48
82
  # `@ivars` and method calls directly, so it must be run via
49
83
  # `instance_exec` on the Sinatra instance the route was dispatched
50
- # on. That gives `<%= @name %>` the usual Sinatra semantics.
84
+ # on. That gives `<%= @name %>` the usual Sinatra semantics. Layout
85
+ # templates can also write `<%= yield %>` — we rewrite that specific
86
+ # form to an instance helper because Ruby's `yield` keyword is not
87
+ # valid inside the Proc body we generate here.
51
88
  def compile(source)
52
89
  ruby = +"_out = ''\n"
53
90
  cursor = 0
@@ -76,14 +113,20 @@ module HomuraERB
76
113
  # `<%== expression %>` — identical to `<%= %>` in this minimal
77
114
  # dialect (no HTML escaping yet; the author is responsible).
78
115
  expr = inner[2..].strip
116
+ validate_expression_fragment!(expr)
117
+ expr = normalize_fragment(expr)
79
118
  ruby << "_out = _out + ((#{expr})).to_s\n"
80
119
  elsif inner.start_with?('=')
81
120
  # `<%= expression %>`
82
121
  expr = inner[1..].strip
122
+ validate_expression_fragment!(expr)
123
+ expr = normalize_fragment(expr)
83
124
  ruby << "_out = _out + ((#{expr})).to_s\n"
84
125
  else
85
126
  # `<% code %>` — Ruby statement(s), emitted verbatim
86
- ruby << inner.strip << "\n"
127
+ code = inner.strip
128
+ validate_code_fragment!(code)
129
+ ruby << normalize_fragment(code) << "\n"
87
130
  end
88
131
 
89
132
  cursor = close_idx + 2
@@ -142,10 +185,16 @@ def emit_header(io, namespace)
142
185
  @templates[name.to_sym] = body
143
186
  end
144
187
 
145
- def render(name, instance, locals = {})
188
+ def render(name, instance, locals = {}, &block)
146
189
  body = @templates[name.to_sym]
147
190
  raise "#{namespace}: no template registered for \#{name.inspect}" unless body
148
- instance.instance_exec(locals, &body)
191
+ previous_block = instance.instance_variable_get(:@__homura_template_block__)
192
+ begin
193
+ instance.instance_variable_set(:@__homura_template_block__, block)
194
+ instance.instance_exec(locals, &body)
195
+ ensure
196
+ instance.instance_variable_set(:@__homura_template_block__, previous_block)
197
+ end
149
198
  end
150
199
 
151
200
  def registered?(name)
@@ -187,6 +236,22 @@ def emit_sinatra_patch(io, namespace)
187
236
 
188
237
  module ::Sinatra
189
238
  module Templates
239
+ def __homura_template_yield__
240
+ block = @__homura_template_block__
241
+ raise LocalJumpError, 'no block given' unless block
242
+
243
+ block.call
244
+ end
245
+
246
+ def __homura_default_template_block_for__(template)
247
+ case template.to_sym
248
+ when :layout
249
+ proc { @content } if defined?(@content)
250
+ when :layout_docs
251
+ proc { @docs_inner } if defined?(@docs_inner)
252
+ end
253
+ end
254
+
190
255
  # homura patch: dispatch to precompiled templates when we
191
256
  # have one. Unknown symbols raise a clear message instead of
192
257
  # wandering into upstream Tilt, which would blow up on
@@ -194,7 +259,20 @@ def emit_sinatra_patch(io, namespace)
194
259
  def erb(template, options = {}, locals = {}, &block)
195
260
  if template.is_a?(::Symbol) && ::#{namespace}.registered?(template)
196
261
  locals ||= {}
197
- ::#{namespace}.render(template, self, locals)
262
+ block ||= __homura_default_template_block_for__(template)
263
+ output = ::#{namespace}.render(template, self, locals, &block)
264
+
265
+ layout = options[:layout]
266
+ layout = false if layout.nil? && options.include?(:layout)
267
+ if layout
268
+ layout = :layout if layout == true
269
+ layout = layout.to_sym
270
+ raise "homura: layout \#{layout.inspect} not precompiled; run bin/compile-erb" unless ::#{namespace}.registered?(layout)
271
+
272
+ return ::#{namespace}.render(layout, self, locals) { output }
273
+ end
274
+
275
+ output
198
276
  else
199
277
  raise "homura: erb \#{template.inspect} not precompiled; run bin/compile-erb"
200
278
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'fileutils'
3
4
  require 'pathname'
4
5
 
5
6
  module CloudflareWorkers
@@ -37,6 +38,30 @@ module CloudflareWorkers
37
38
  nil
38
39
  end
39
40
 
41
+ def runtime_file(*names, current_file: __FILE__, loaded_specs: Gem.loaded_specs)
42
+ runtime_root(current_file: current_file, loaded_specs: loaded_specs).join('runtime', *names)
43
+ end
44
+
45
+ def ensure_standalone_runtime(project_root, current_file: __FILE__, loaded_specs: Gem.loaded_specs)
46
+ target_dir = Pathname(project_root).join('cf-runtime')
47
+ FileUtils.mkdir_p(target_dir)
48
+
49
+ %w[setup-node-crypto.mjs worker_module.mjs].each do |name|
50
+ FileUtils.cp(runtime_file(name, current_file: current_file, loaded_specs: loaded_specs), target_dir.join(name))
51
+ end
52
+
53
+ target_dir
54
+ end
55
+
56
+ def standalone_namespace(project_root, suffix)
57
+ base = Pathname(project_root).basename.to_s
58
+ parts = base.split(/[^A-Za-z0-9]+/).reject(&:empty?)
59
+ module_name = parts.map { |part| part[0].upcase + part[1..].to_s }.join
60
+ module_name = 'App' if module_name.empty?
61
+ module_name = "App#{module_name}" if module_name.match?(/\A\d/)
62
+ "#{module_name}#{suffix}"
63
+ end
64
+
40
65
  def vendor_from_gemfile(project_root)
41
66
  gf = Pathname(project_root).join('Gemfile')
42
67
  return unless gf.file?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CloudflareWorkers
4
- VERSION = '0.1.3'
4
+ VERSION = '0.1.5'
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.1.3
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kazuhiro NISHIYAMA