homura-runtime 0.2.23 → 0.2.25

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: b404ba5cef20ed67d0b7f882c21a4f22b49bb0594e3175cec2137498764bafe9
4
- data.tar.gz: 99dd3e6a2849a57d146c8d8f46b7cd01f805dc1712b858445c06bf5761dcb18a
3
+ metadata.gz: aa815fa7e02b0f798dd69003008576f264eb3a0e2532805a729fee6f4a5aca6b
4
+ data.tar.gz: e8af4871e543adc2ef6c6d8751272111c102987345ea6526ad1fa0e6e2192db8
5
5
  SHA512:
6
- metadata.gz: 97e9d3a324ffe6545b080ce483c1b4446ed14937d3cc70dcf6852e793002d9a246fc9a9ef557b687a42f9cf5903bb85ee2faf6a80fef42f0ca22d7661d9a7074
7
- data.tar.gz: 7b930885e9bad1526363d9b9f5c2940ba71a8f9f65f073352d17c4e679b8039a9f25e3d44653df723ef9204fd90ad17c9db7e15789ec0050d747f8892ca05b17
6
+ metadata.gz: c6f04b2b77f82131dd45148ff7d8467e0093ed005f49269ab2a2a37ab96ad957e6abd264c302331d37a4a95ac84885d9eef3882e51422fec7f6ed2f30aca25b1
7
+ data.tar.gz: ff80cbfcbc5b699eed8c7f79c1da2b40aab678163185b0471ec269c7fe55db06d14515aaa22ece20bbff019ff754bc38d398c3a812ea33a2df3716fbea395716
data/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.25 (2026-04-29)
4
+
5
+ - `BuildSupport`: factor `opal_gem_paths` out of the path:-only
6
+ `path_gemfile_entries`. The new method also picks up
7
+ RubyGems-installed gems that opt in via
8
+ `spec.metadata['homura.auto_await'] = 'true'`. Without this, a
9
+ RubyGems-installed pure-Ruby gem (e.g. `sinatra-inertia >= 0.1.1`)
10
+ was missing from both the Opal load path and the auto-await pass,
11
+ so `require 'sinatra/inertia'` failed during `homura build`.
12
+ - `homura-build` iterates `opal_gem_paths` instead of the old
13
+ path:-only list, so any opted-in gem (path: or RubyGems) gets the
14
+ same auto-await rewrite.
15
+
16
+ ## 0.2.24 (2026-04-29)
17
+
18
+ - `BuildSupport.standalone_load_paths`: auto-discover `path:`-resolved
19
+ gems in the consumer Gemfile (skipping `require: false` and
20
+ `group :development/:test/:ci/...` blocks) and add their `lib/` and
21
+ `vendor/` to the Opal load path. Lets pure-Ruby gems like
22
+ `sinatra-inertia` drop into a project without runtime-side wiring.
23
+ - `homura-build` now runs the `auto-await` analyzer over each `path:`
24
+ gem's `lib/` and writes the rewritten copy under
25
+ `build/auto_await/gem_<basename>/lib`. The transformed directory
26
+ is preferred over the gem's untransformed `lib/` on the load path,
27
+ so async chains inside gem code (e.g. `db[:foo].all` returning a
28
+ Promise) get `__await__` injected exactly like consumer app code.
29
+ - `cloudflare_workers.rb#build_js_response`: emit `Set-Cookie`
30
+ Arrays via `Headers#append` (multiple lines) instead of stringifying
31
+ the Array via `to_s`. Previously, two cookies set by sequential Rack
32
+ middleware (e.g. session + auth) were serialised as
33
+ `'["a=…", "b=…"]'`, which broke cookie parsing on every Inertia /
34
+ CSRF / auth-cookie pattern.
35
+
3
36
  ## 0.2.17 (2026-04-27)
4
37
 
5
38
  - Rewrite class-variable references (`@@foo`) inside precompiled ERB
@@ -22,7 +55,7 @@
22
55
 
23
56
  ## 0.2.10 (2026-04-24)
24
57
 
25
- - Derive `worker.entrypoint.mjs` import paths relative to the actual
58
+ - Derive `build/worker.entrypoint.mjs` import paths relative to the actual
26
59
  `--entrypoint-out` location, so standalone builds keep working when apps move
27
60
  the bundle and entrypoint under custom output directories.
28
61
 
data/README.md CHANGED
@@ -9,14 +9,14 @@ 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
- - `homura build` — single build pipeline (ERB → assets → Opal → patch → `worker.entrypoint.mjs`). Use `--standalone` in generated apps; it restores `cf-runtime/` automatically, derives standalone template/asset namespaces from the project name, and computes entrypoint import paths relative to the actual `--entrypoint-out` location.
12
+ - `homura build` — single build pipeline (ERB → assets → Opal → patch → `build/worker.entrypoint.mjs`). Use `--standalone` in generated apps; it restores `cf-runtime/` automatically, derives standalone template/asset namespaces from the project name, and computes entrypoint import paths relative to the actual `--entrypoint-out` location.
13
13
  - `docs/ARCHITECTURE.md` — wrangler `main`, codegen entrypoint, and fixed-import policy.
14
14
 
15
15
  ## Quick start (homura monorepo)
16
16
 
17
17
  1. `Gemfile`: `gem 'homura-runtime', path: 'gems/homura-runtime'` and `gem 'opal-homura', '= 1.8.3.rc1.3', require: 'opal'` (path or exact pin).
18
- 2. `bundle exec homura build` — in generated standalone apps, writes `build/hello.no-exit.mjs` plus root-level `worker.entrypoint.mjs`; in the monorepo it writes `build/worker.entrypoint.mjs`.
19
- 3. `wrangler.toml`: generated apps use `main = "worker.entrypoint.mjs"` and `compatibility_flags = ["nodejs_compat"]`.
18
+ 2. `bundle exec homura build` — in generated standalone apps, writes `build/hello.no-exit.mjs` plus root-level `build/worker.entrypoint.mjs`; in the monorepo it writes `build/worker.entrypoint.mjs`.
19
+ 3. `wrangler.toml`: generated apps use `main = "build/worker.entrypoint.mjs"` and `compatibility_flags = ["nodejs_compat"]`.
20
20
 
21
21
  ## Support matrix (indicative)
22
22
 
data/docs/ARCHITECTURE.md CHANGED
@@ -5,7 +5,7 @@
5
5
  | レイヤ | 役割 |
6
6
  |--------|------|
7
7
  | `wrangler.toml` の `main` | **Workers Module のエントリ**(`fetch` / `scheduled` / `queue` / DO クラスを export) |
8
- | `worker.entrypoint.mjs`(生成物) | `setup-node-crypto` → **Opal bundle(副作用)** → `worker_module.mjs` の順で import。import path は entrypoint 出力位置からの **相対パス** に自動調整される |
8
+ | `build/worker.entrypoint.mjs`(生成物) | `setup-node-crypto` → **Opal bundle(副作用)** → `worker_module.mjs` の順で import。import path は entrypoint 出力位置からの **相対パス** に自動調整される |
9
9
  | Opal bundle(例: `build/hello.no-exit.mjs`) | **ビルド成果物**。Rack ディスパッチャ等を `globalThis` に登録 |
10
10
  | `worker_module.mjs`(gem 同梱) | Rack / Cron / Queue / DO への **純粋な JS アダプタ**(Opal bundle を import しない) |
11
11
 
@@ -21,7 +21,7 @@ flowchart LR
21
21
  W1[worker.mjs] -->|hard-coded| B1[../../../build/hello.mjs]
22
22
  end
23
23
  subgraph after["After (Phase 15-E)"]
24
- E[worker.entrypoint.mjs] --> S[setup-node-crypto.mjs]
24
+ E[build/worker.entrypoint.mjs] --> S[setup-node-crypto.mjs]
25
25
  E --> O[build/*.mjs Opal bundle]
26
26
  E --> M[worker_module.mjs]
27
27
  end
@@ -34,7 +34,7 @@ flowchart LR
34
34
 
35
35
  ## スキャフォールド済みアプリ
36
36
 
37
- - プロジェクト直下に `worker.entrypoint.mjs`(`main` と一致)。
37
+ - プロジェクト直下に `build/worker.entrypoint.mjs`(`main` と一致)。
38
38
  - `cf-runtime/` に `setup-node-crypto.mjs` と `worker_module.mjs` をコピー(gem から)。
39
39
  - `bundle exec homura build --standalone` が consumer 向けパイプラインを実行し、`Gemfile` の `path:` から homura の `vendor/` を追加ロードパスへ取り込み(digest / zlib 等の Workers 向け補助ファイル)。
40
40
  - low-level `--output` / `--entrypoint-out` を変えても、entrypoint 内 import は出力先からの相対パスに自動調整される。
data/exe/homura-build CHANGED
@@ -275,6 +275,30 @@ else
275
275
  warn 'homura build: no app/ directory or top-level app.rb — skipping auto-await'
276
276
  end
277
277
 
278
+ # Also run auto-await over every gem we ship to Opal:
279
+ # `path:`-resolved gems in the consumer Gemfile *and* RubyGems-
280
+ # installed gems that opt in via
281
+ # `spec.metadata['homura.auto_await'] = 'true'`. Both go to
282
+ # `build/auto_await/gem_<basename>/<sub>` and
283
+ # `standalone_load_paths` puts those rewritten copies ahead of the
284
+ # gem's untransformed `lib/`.
285
+ CloudflareWorkers::BuildSupport.opal_gem_paths(root).each do |gem_path|
286
+ %w[lib].each do |sub|
287
+ src = gem_path.join(sub)
288
+ next unless src.directory?
289
+ out = root.join('build', 'auto_await', "gem_#{gem_path.basename}", sub)
290
+ FileUtils.mkdir_p(out)
291
+ run!(
292
+ [
293
+ 'ruby', CloudflareWorkersBuild.exe_path('auto-await').to_s,
294
+ '--input', src.to_s,
295
+ '--output', out.to_s
296
+ ],
297
+ chdir: root
298
+ )
299
+ end
300
+ end
301
+
278
302
  opal_in = Pathname(resolve_opal_input(root, options[:opal_input]))
279
303
  opal_out = Pathname(options[:opal_output])
280
304
  opal_in = root.join(opal_in) unless opal_in.absolute?
@@ -86,6 +86,28 @@ module CloudflareWorkers
86
86
  end
87
87
  end
88
88
 
89
+ # Pick up any other gems that should ship in the Workers bundle:
90
+ #
91
+ # * `path:`-resolved gems in the consumer's Gemfile (monorepo
92
+ # dev mode), and
93
+ # * RubyGems-installed gems that opt in via
94
+ # `spec.metadata['homura.auto_await'] = 'true'`.
95
+ #
96
+ # Both go through the same auto-await pass during `homura-build`,
97
+ # and we prefer the rewritten copy under
98
+ # `build/auto_await/gem_<basename>/lib` if present so async chains
99
+ # inside gem code get `__await__` inserted just like consumer
100
+ # app code.
101
+ opal_gem_paths(root, loaded_specs: loaded_specs).each do |gem_path|
102
+ basename = gem_path.basename.to_s
103
+ rewritten_lib = root.join('build', 'auto_await', "gem_#{basename}", 'lib')
104
+ load_paths << rewritten_lib.to_s if rewritten_lib.directory?
105
+ %w[lib vendor].each do |sub|
106
+ dir = gem_path.join(sub)
107
+ load_paths << dir.to_s if dir.directory?
108
+ end
109
+ end
110
+
89
111
  load_paths << 'vendor' if root.join('vendor').directory?
90
112
  load_paths << 'build'
91
113
  load_paths.uniq
@@ -111,6 +133,78 @@ module CloudflareWorkers
111
133
  vend = runtime_path.join('..', '..', 'vendor').expand_path
112
134
  vend if vend.directory?
113
135
  end
136
+
137
+ # Returns the union of `path_gemfile_entries(project_root)` and any
138
+ # bundled gems that opt in to the Opal pipeline via
139
+ # `spec.metadata['homura.auto_await']`. This is the single source
140
+ # of truth for both `standalone_load_paths` and the auto-await pass
141
+ # that `homura-build` runs. Returns `Pathname` objects pointing at
142
+ # each gem's root directory.
143
+ def opal_gem_paths(project_root, loaded_specs: Gem.loaded_specs)
144
+ wired = [RUNTIME_GEM_NAME, SINATRA_GEM_NAME, SEQUEL_D1_GEM_NAME]
145
+ out = []
146
+ out.concat(path_gemfile_entries(project_root))
147
+
148
+ loaded_specs.each_value do |spec|
149
+ next if wired.include?(spec.name)
150
+ meta = spec.metadata
151
+ next unless meta.is_a?(Hash)
152
+ flag = meta['homura.auto_await']
153
+ next unless flag == 'true' || flag == true
154
+ next if spec.full_gem_path.nil?
155
+ gem_path = Pathname(spec.full_gem_path)
156
+ out << gem_path if gem_path.directory?
157
+ end
158
+
159
+ out.uniq
160
+ end
161
+
162
+ # Returns absolute Pathnames for every `path:`-declared gem in the
163
+ # project's Gemfile that should ship in the Workers bundle.
164
+ #
165
+ # Excludes:
166
+ # * gems we already wire in explicitly
167
+ # (homura-runtime / sinatra-homura / sequel-d1)
168
+ # * `require: false` gems (dev tooling like `gem 'rspec', path: ..., require: false`)
169
+ # * gems declared inside `group :development do … end` /
170
+ # `group :test do … end` blocks (they don't ship to production)
171
+ EXCLUDED_GROUPS = %i[development test dev_test development_test ci tools].freeze
172
+
173
+ def path_gemfile_entries(project_root)
174
+ gf = Pathname(project_root).join('Gemfile')
175
+ return [] unless gf.file?
176
+
177
+ wired = [RUNTIME_GEM_NAME, SINATRA_GEM_NAME, SEQUEL_D1_GEM_NAME]
178
+ out = []
179
+ group_stack = []
180
+
181
+ gf.read.each_line do |line|
182
+ stripped = line.strip
183
+ next if stripped.empty? || stripped.start_with?('#')
184
+
185
+ if (m = stripped.match(/\Agroup\s+(.+?)\s+do\b/))
186
+ groups = m[1].scan(/[:'"]([A-Za-z0-9_]+)['"]?/).flatten.map(&:to_sym)
187
+ group_stack.push(groups)
188
+ next
189
+ end
190
+ if stripped == 'end'
191
+ group_stack.pop unless group_stack.empty?
192
+ next
193
+ end
194
+
195
+ next if group_stack.flatten.any? { |g| EXCLUDED_GROUPS.include?(g) }
196
+
197
+ m = line.match(/gem\s+['"]([^'"]+)['"][^#]*?path:\s*['"]([^'"]+)['"]/)
198
+ next unless m
199
+ name, rel = m[1], m[2]
200
+ next if wired.include?(name)
201
+ next if line.match?(/require:\s*false/)
202
+
203
+ gem_path = Pathname.new(rel).expand_path(project_root)
204
+ out << gem_path if gem_path.directory?
205
+ end
206
+ out.uniq
207
+ end
114
208
  end
115
209
  end
116
210
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CloudflareWorkers
4
- VERSION = '0.2.23'
4
+ VERSION = '0.2.25'
5
5
  end
@@ -376,13 +376,54 @@ module Rack
376
376
  chunks << body
377
377
  end
378
378
 
379
+ # Build JS-side headers. Set-Cookie is the one HTTP response
380
+ # header that legitimately repeats — Rack 3 surfaces it as an
381
+ # Array of cookie strings (e.g. session middleware + auth
382
+ # middleware both setting cookies). Plain `vs = v.to_s` would
383
+ # collapse that Array into its `inspect` form and break cookie
384
+ # parsing on the client. We use a `__multi__` sentinel here
385
+ # and let `Cloudflare.headers_to_js` (the SSE/streaming code
386
+ # path also uses it) emit each entry as a separate header line
387
+ # via Headers#append.
379
388
  js_headers = `({})`
380
389
  headers.each do |k, v|
381
390
  ks = k.to_s
382
- vs = v.to_s
383
- `#{js_headers}[#{ks}] = #{vs}`
391
+ if v.is_a?(Array)
392
+ arr = `[]`
393
+ v.each { |vi| `#{arr}.push(#{vi.to_s})` }
394
+ `#{js_headers}[#{ks}] = { __multi__: true, values: #{arr} }`
395
+ else
396
+ vs = v.to_s
397
+ `#{js_headers}[#{ks}] = #{vs}`
398
+ end
384
399
  end
385
400
 
401
+ # Convert any `{ __multi__: true, values: [...] }` markers into
402
+ # a real `Headers` object that Workers' `new Response(headers:)`
403
+ # accepts. Single-valued headers stay as plain string values.
404
+ js_headers = `(function(h) {
405
+ var hasMulti = false;
406
+ for (var key in h) {
407
+ if (h[key] && typeof h[key] === 'object' && h[key].__multi__ === true) {
408
+ hasMulti = true;
409
+ break;
410
+ }
411
+ }
412
+ if (!hasMulti) return h;
413
+ var realHeaders = new Headers();
414
+ for (var key in h) {
415
+ var val = h[key];
416
+ if (val && typeof val === 'object' && val.__multi__ === true) {
417
+ for (var j = 0; j < val.values.length; j++) {
418
+ realHeaders.append(key, val.values[j]);
419
+ }
420
+ } else {
421
+ realHeaders.set(key, val);
422
+ }
423
+ }
424
+ return realHeaders;
425
+ })(#{js_headers})`
426
+
386
427
  status_int = status.to_i
387
428
 
388
429
  js_chunks = `[]`
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.23
4
+ version: 0.2.25
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kazuhiro Homma