homura-runtime 0.1.4 → 0.1.6

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: 6e37f705dbc9ddf448634efe039bcc390d577c1683ad1759db58fdb1fbceee02
4
- data.tar.gz: 9dfa51f60c34241302354b5069f10ba560a61c51d4b70a1799893d785d2ae434
3
+ metadata.gz: aae4102ce898507966e1a993b16033cf74ede5f2543d17e56d552c6b3ecfabaf
4
+ data.tar.gz: 16b3be88c09f2189193da02cde5904a18749a74edf80e38927a2e47329ec06a8
5
5
  SHA512:
6
- metadata.gz: 0655d2c8e8f889892df7500cfadc663df079be3c7326815523b2fad12cacdce4eb729f859e145bfdc8c26f8ad0bc12bc2d5ee2dfd3acf14e71c82ffafd4057d3
7
- data.tar.gz: 93c6e9c5607b995d631060f511633bd94b3fa74f9db3c9adee3806b684a1ea7134b136bde12f9c8a3fa7d1da5f3ff76c95929c253251eb7daf3f1f9685bc5342
6
+ metadata.gz: 57f457bf20205a3bd68d81d5ad616b971892c513023d61dd9b56f575bc0cf70063b9753810abb1163084ebd48d7ae8439bcfd5c72a53e3dd25bdc3f98148b796
7
+ data.tar.gz: 6c69fb69b6c79a3309a9e5796534023ee47066027a97c809034615635b1df49e284ee0c010fce48059f062d43ec94e12f01ace5b39525b5f6e7d64940a4affa2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.6 (2026-04-23)
4
+
5
+ - Teach `cloudflare-workers-build --standalone --with-db` to add the packaged
6
+ `sequel-d1` gem `vendor/` directory to the Opal load path before the gem's
7
+ `lib/`, so `require 'sequel'` resolves to the bundled Opal-compatible Sequel
8
+ subset instead of the CRuby gem.
9
+
10
+ ## 0.1.5 (2026-04-23)
11
+
12
+ - Make `auto-await` emit rewritten files for existing hand-written `.__await__`
13
+ usage when the only missing piece is `# await: true`.
14
+ - Make `cloudflare-workers-build --standalone` restore `cf-runtime/` from the
15
+ packaged gem and derive standalone template/asset namespaces from the project
16
+ name by default, with explicit override flags when needed.
17
+ - Reject unsupported ERB yield forms like `<% yield %>` and `yield(arg)` with
18
+ compile-time guidance toward the supported Sinatra-style layout forms.
19
+
3
20
  ## 0.1.4 (2026-04-23)
4
21
 
5
22
  - Teach the precompiled ERB runtime to support Sinatra-style layout blocks and
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' }
@@ -115,24 +121,7 @@ def homura_vendor_from_gemfile(project_root)
115
121
  end
116
122
 
117
123
  def run_opal_standalone!(root, opal_input, opal_output, with_db:)
118
- load_paths = []
119
- hv = homura_vendor_from_gemfile(root)
120
- load_paths << hv.to_s if hv
121
- runtime_name = CloudflareWorkers::BuildSupport::RUNTIME_GEM_NAME
122
- sinatra_name = CloudflareWorkers::BuildSupport::SINATRA_GEM_NAME
123
-
124
- load_paths += ['build/auto_await/app', 'app']
125
- [
126
- CloudflareWorkersBuild.gem_lib(runtime_name),
127
- CloudflareWorkersBuild.gem_vendor(runtime_name),
128
- CloudflareWorkersBuild.gem_lib(sinatra_name),
129
- CloudflareWorkersBuild.gem_vendor(sinatra_name)
130
- ].compact.each do |path|
131
- load_paths << path
132
- end
133
- load_paths << CloudflareWorkersBuild.gem_lib('sequel-d1') if with_db
134
- load_paths << 'vendor' if root.join('vendor').directory?
135
- load_paths << 'build'
124
+ load_paths = CloudflareWorkers::BuildSupport.standalone_load_paths(root, with_db: with_db)
136
125
 
137
126
  argv = ['bundle', 'exec', 'opal', '-c', '-E', '--esm', '--no-source-map']
138
127
  load_paths.each { |p| argv.push('-I', p) }
@@ -199,12 +188,13 @@ unless options[:standalone]
199
188
  opal_out = root.join(opal_out) unless opal_out.absolute?
200
189
  run_opal_homura!(root, opal_in.relative_path_from(root).to_s, opal_out.relative_path_from(root).to_s)
201
190
  else
191
+ CloudflareWorkers::BuildSupport.ensure_standalone_runtime(root, current_file: __FILE__)
202
192
  run!(
203
193
  [
204
194
  'ruby', CloudflareWorkersBuild.exe_path('compile-erb').to_s,
205
195
  '--input', 'views',
206
196
  '--output', 'build/app_templates.rb',
207
- '--namespace', 'AppTemplates'
197
+ '--namespace', options[:templates_namespace]
208
198
  ],
209
199
  chdir: root
210
200
  )
@@ -213,7 +203,7 @@ else
213
203
  'ruby', CloudflareWorkersBuild.exe_path('compile-assets').to_s,
214
204
  '--input', 'public',
215
205
  '--output', 'build/app_assets.rb',
216
- '--namespace', 'AppAssets'
206
+ '--namespace', options[:assets_namespace]
217
207
  ],
218
208
  chdir: root
219
209
  )
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,13 +43,40 @@ USAGE
43
43
  module HomuraERB
44
44
  module_function
45
45
 
46
- def normalize_fragment(fragment)
46
+ def supported_yield_fragment?(fragment)
47
47
  stripped = fragment.strip
48
- return '__homura_template_yield__' if stripped == 'yield' || stripped == 'yield()'
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
49
54
 
55
+ def normalize_fragment(fragment)
56
+ return '__homura_template_yield__' if supported_yield_fragment?(fragment)
50
57
  fragment
51
58
  end
52
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
+
53
80
  # Compile an ERB source string to a Ruby method body that assembles
54
81
  # an HTML string in a local variable `_out`. The body references
55
82
  # `@ivars` and method calls directly, so it must be run via
@@ -85,15 +112,21 @@ module HomuraERB
85
112
  elsif inner.start_with?('==')
86
113
  # `<%== expression %>` — identical to `<%= %>` in this minimal
87
114
  # dialect (no HTML escaping yet; the author is responsible).
88
- expr = normalize_fragment(inner[2..].strip)
115
+ expr = inner[2..].strip
116
+ validate_expression_fragment!(expr)
117
+ expr = normalize_fragment(expr)
89
118
  ruby << "_out = _out + ((#{expr})).to_s\n"
90
119
  elsif inner.start_with?('=')
91
120
  # `<%= expression %>`
92
- expr = normalize_fragment(inner[1..].strip)
121
+ expr = inner[1..].strip
122
+ validate_expression_fragment!(expr)
123
+ expr = normalize_fragment(expr)
93
124
  ruby << "_out = _out + ((#{expr})).to_s\n"
94
125
  else
95
126
  # `<% code %>` — Ruby statement(s), emitted verbatim
96
- ruby << normalize_fragment(inner.strip) << "\n"
127
+ code = inner.strip
128
+ validate_code_fragment!(code)
129
+ ruby << normalize_fragment(code) << "\n"
97
130
  end
98
131
 
99
132
  cursor = close_idx + 2
@@ -1,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'fileutils'
3
4
  require 'pathname'
4
5
 
5
6
  module CloudflareWorkers
6
7
  module BuildSupport
7
8
  RUNTIME_GEM_NAME = 'homura-runtime'
8
9
  SINATRA_GEM_NAME = 'sinatra-homura'
10
+ SEQUEL_D1_GEM_NAME = 'sequel-d1'
9
11
 
10
12
  class << self
11
13
  def loaded_spec(name, loaded_specs: Gem.loaded_specs)
@@ -37,6 +39,61 @@ module CloudflareWorkers
37
39
  nil
38
40
  end
39
41
 
42
+ def runtime_file(*names, current_file: __FILE__, loaded_specs: Gem.loaded_specs)
43
+ runtime_root(current_file: current_file, loaded_specs: loaded_specs).join('runtime', *names)
44
+ end
45
+
46
+ def ensure_standalone_runtime(project_root, current_file: __FILE__, loaded_specs: Gem.loaded_specs)
47
+ target_dir = Pathname(project_root).join('cf-runtime')
48
+ FileUtils.mkdir_p(target_dir)
49
+
50
+ %w[setup-node-crypto.mjs worker_module.mjs].each do |name|
51
+ FileUtils.cp(runtime_file(name, current_file: current_file, loaded_specs: loaded_specs), target_dir.join(name))
52
+ end
53
+
54
+ target_dir
55
+ end
56
+
57
+ def standalone_load_paths(project_root, with_db:, loaded_specs: Gem.loaded_specs)
58
+ root = Pathname(project_root)
59
+ load_paths = []
60
+
61
+ hv = vendor_from_gemfile(root)
62
+ load_paths << hv.to_s if hv
63
+
64
+ load_paths += ['build/auto_await/app', 'app']
65
+ [
66
+ gem_lib(RUNTIME_GEM_NAME, loaded_specs: loaded_specs),
67
+ gem_vendor(RUNTIME_GEM_NAME, loaded_specs: loaded_specs),
68
+ gem_lib(SINATRA_GEM_NAME, loaded_specs: loaded_specs),
69
+ gem_vendor(SINATRA_GEM_NAME, loaded_specs: loaded_specs)
70
+ ].compact.each do |path|
71
+ load_paths << path
72
+ end
73
+
74
+ if with_db
75
+ [
76
+ gem_vendor(SEQUEL_D1_GEM_NAME, loaded_specs: loaded_specs),
77
+ gem_lib(SEQUEL_D1_GEM_NAME, loaded_specs: loaded_specs)
78
+ ].compact.each do |path|
79
+ load_paths << path
80
+ end
81
+ end
82
+
83
+ load_paths << 'vendor' if root.join('vendor').directory?
84
+ load_paths << 'build'
85
+ load_paths.uniq
86
+ end
87
+
88
+ def standalone_namespace(project_root, suffix)
89
+ base = Pathname(project_root).basename.to_s
90
+ parts = base.split(/[^A-Za-z0-9]+/).reject(&:empty?)
91
+ module_name = parts.map { |part| part[0].upcase + part[1..].to_s }.join
92
+ module_name = 'App' if module_name.empty?
93
+ module_name = "App#{module_name}" if module_name.match?(/\A\d/)
94
+ "#{module_name}#{suffix}"
95
+ end
96
+
40
97
  def vendor_from_gemfile(project_root)
41
98
  gf = Pathname(project_root).join('Gemfile')
42
99
  return unless gf.file?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CloudflareWorkers
4
- VERSION = '0.1.4'
4
+ VERSION = '0.1.6'
5
5
  end
@@ -20,9 +20,10 @@
20
20
  # style entry point and never sees a Cloudflare-specific symbol.
21
21
  #
22
22
  # 3. Cloudflare::D1Database / KVNamespace / R2Bucket — tiny Ruby wrappers
23
- # around the JS bindings. They expose the binding methods as regular
24
- # Ruby method calls returning native JS Promises, which the user
25
- # routes can `.__await__` inside a `# await: true` block.
23
+ # around the JS bindings. At the raw runtime layer they still return
24
+ # Promises, but homura's build-time auto-await pass rewrites the
25
+ # common Sinatra-facing call sites (`db.execute`, `kv.get`, etc.) so
26
+ # app code usually stays sync-shaped.
26
27
  #
27
28
  # Note: Opal Strings are immutable (they map to JS Strings), so this file
28
29
  # uses reassignment (`@buffer = @buffer + str`) instead of `<<` mutation.
@@ -209,8 +210,10 @@ module Rack
209
210
 
210
211
  # Expose D1 / KV / R2 bindings as plain Ruby wrapper objects.
211
212
  # The user Sinatra routes reach them via
212
- # `env['cloudflare.DB']` / `.KV` / `.BUCKET`, call normal-looking
213
- # Ruby methods on them, and `.__await__` the resulting JS Promise.
213
+ # `env['cloudflare.DB']` / `.KV` / `.BUCKET` and call ordinary
214
+ # Ruby methods on them. Under the hood those methods are async,
215
+ # but homura's auto-await build step inserts `.__await__` for the
216
+ # common binding/helper patterns so app source usually does not.
214
217
  js_db = `#{js_env} && #{js_env}.DB`
215
218
  js_kv = `#{js_env} && #{js_env}.KV`
216
219
  js_r2 = `#{js_env} && #{js_env}.BUCKET`
@@ -603,8 +606,10 @@ module Cloudflare
603
606
 
604
607
  # ---- sqlite3-ruby compatible high-level API ----------------------
605
608
 
606
- # Execute a SQL statement with optional bind parameters and return
607
- # all result rows as an Array of Hashes.
609
+ # Execute a SQL statement with optional bind parameters.
610
+ # Returns a JS Promise resolving to an Array of Hashes; the build-time
611
+ # auto-await pass rewrites the usual Sinatra call sites so app code can
612
+ # stay `db.execute(...)` instead of spelling `.__await__`.
608
613
  #
609
614
  # db.execute("SELECT * FROM users") → Array<Hash>
610
615
  # db.execute("SELECT * FROM users WHERE id = ?", [1]) → Array<Hash>
@@ -616,6 +621,7 @@ module Cloudflare
616
621
  end
617
622
 
618
623
  # Execute and return only the first row (or nil).
624
+ # Returns a JS Promise resolving to a Hash or nil.
619
625
  #
620
626
  # db.get_first_row("SELECT * FROM users WHERE id = ?", [1]) → Hash or nil
621
627
  def get_first_row(sql, bind_params = [])
@@ -626,6 +632,7 @@ module Cloudflare
626
632
 
627
633
  # Execute a write statement (INSERT / UPDATE / DELETE) and return
628
634
  # a metadata Hash with `changes`, `last_row_id`, `duration`, etc.
635
+ # Returns a JS Promise resolving to that metadata Hash.
629
636
  #
630
637
  # meta = db.execute_insert("INSERT INTO users (name) VALUES (?)", ["alice"])
631
638
  # meta['last_row_id'] # → 7
@@ -636,7 +643,7 @@ module Cloudflare
636
643
  end
637
644
 
638
645
  # Execute one or more raw SQL statements separated by semicolons.
639
- # Useful for schema migrations. Returns the D1 exec result.
646
+ # Useful for schema migrations. Returns the raw async D1 exec result.
640
647
  def execute_batch(sql)
641
648
  exec(sql)
642
649
  end
@@ -697,6 +704,7 @@ module Cloudflare
697
704
  end
698
705
 
699
706
  # KV#get returns a JS Promise resolving to a String or nil.
707
+ # In normal Sinatra app code, auto-await usually hides that Promise.
700
708
  def get(key)
701
709
  js_kv = @js
702
710
  err_cls = Cloudflare::KVError
@@ -705,7 +713,8 @@ module Cloudflare
705
713
 
706
714
  # Put a value. `expiration_ttl:` (seconds) maps to the Workers KV
707
715
  # `expirationTtl` option so callers can set TTLs without reaching
708
- # for backticks. Returns a JS Promise.
716
+ # for backticks. Returns a JS Promise; common route/helper call sites
717
+ # are auto-awaited during the build.
709
718
  def put(key, value, expiration_ttl: nil)
710
719
  js_kv = @js
711
720
  err_cls = Cloudflare::KVError
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.4
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kazuhiro NISHIYAMA