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 +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +1 -1
- data/bin/cloudflare-workers-build +10 -3
- data/exe/auto-await +9 -2
- data/exe/compile-erb +83 -5
- data/lib/cloudflare_workers/build_support.rb +25 -0
- data/lib/cloudflare_workers/version.rb +1 -1
- 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: b3d8286719ce164088136af4d1caf0476c026d4ec2e42b98b08db8c84df6766d
|
|
4
|
+
data.tar.gz: 849f3bb4ae14131b950de3a89efff6032a4204e9878a19c84cfe9dcdfa618e8b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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',
|
|
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',
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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?
|