homura-runtime 0.2.22 → 0.2.24

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: cff255ad823986df011ec56a290aafb83c08a148aee51a0e5f614f9ca80f2d41
4
- data.tar.gz: 9e3cb0a22c2a6c497fea120d38111d9cc299cd43213bfeb7d9f52ff2cd6cda3c
3
+ metadata.gz: 0e4fc42870cef61572354025f9091524f2b65d91f93910cb3470acd4038472e7
4
+ data.tar.gz: b8fafbfe977f2bfa1cbaf42f16f0b2849a3f9f3b0f12525eb2f9809497fa38e2
5
5
  SHA512:
6
- metadata.gz: 1ff4f7099815079d7e3c284b32935c99afb1cabc64a31e0115f69dcda7a72bdb56f9ffb11dee82a2ae37b809112319651fba79d03c7ec7dc7d00210634672641
7
- data.tar.gz: 5f2cc557b7c0484d35fbe4bb08dedcc26044b0c537f57b93f68ddd648b5e665ae32613704052f590bc73005d7730954062e091c429c6afb9ded204d6ae268fc8
6
+ metadata.gz: 3aabb8d2b5678419c57247f834bba6ae02d4de928b154529acf026011cb23c8b08cab01fd95a59b98c74e749b64e86a3f6f4749b6305ab143af54bf5cbc16852
7
+ data.tar.gz: 8b71e4b7a2a297aa463b5d68734ca8dc6d88adab156d4bbc9b33e20f7caa58b1fd188c9a4a208fcce83c3df9a57aa2eaf5d18673f4d36cd4d962932dd59a0b97
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.24 (2026-04-29)
4
+
5
+ - `BuildSupport.standalone_load_paths`: auto-discover `path:`-resolved
6
+ gems in the consumer Gemfile (skipping `require: false` and
7
+ `group :development/:test/:ci/...` blocks) and add their `lib/` and
8
+ `vendor/` to the Opal load path. Lets pure-Ruby gems like
9
+ `sinatra-inertia` drop into a project without runtime-side wiring.
10
+ - `homura-build` now runs the `auto-await` analyzer over each `path:`
11
+ gem's `lib/` and writes the rewritten copy under
12
+ `build/auto_await/gem_<basename>/lib`. The transformed directory
13
+ is preferred over the gem's untransformed `lib/` on the load path,
14
+ so async chains inside gem code (e.g. `db[:foo].all` returning a
15
+ Promise) get `__await__` injected exactly like consumer app code.
16
+ - `cloudflare_workers.rb#build_js_response`: emit `Set-Cookie`
17
+ Arrays via `Headers#append` (multiple lines) instead of stringifying
18
+ the Array via `to_s`. Previously, two cookies set by sequential Rack
19
+ middleware (e.g. session + auth) were serialised as
20
+ `'["a=…", "b=…"]'`, which broke cookie parsing on every Inertia /
21
+ CSRF / auth-cookie pattern.
22
+
3
23
  ## 0.2.17 (2026-04-27)
4
24
 
5
25
  - Rewrite class-variable references (`@@foo`) inside precompiled ERB
@@ -22,7 +42,7 @@
22
42
 
23
43
  ## 0.2.10 (2026-04-24)
24
44
 
25
- - Derive `worker.entrypoint.mjs` import paths relative to the actual
45
+ - Derive `build/worker.entrypoint.mjs` import paths relative to the actual
26
46
  `--entrypoint-out` location, so standalone builds keep working when apps move
27
47
  the bundle and entrypoint under custom output directories.
28
48
 
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
@@ -77,10 +77,13 @@ options[:assets_namespace] ||= CloudflareWorkers::BuildSupport.standalone_namesp
77
77
 
78
78
  if options[:standalone]
79
79
  Dir.chdir(root) { require 'bundler/setup' }
80
- options[:setup_import] ||= 'cf-runtime/setup-node-crypto.mjs'
80
+ # Both .mjs glue files and the entrypoint live under `build/` from
81
+ # 0.2.23 on. write_entrypoint! resolves these as project-root
82
+ # relative paths and computes the entrypoint-relative import spec.
83
+ options[:setup_import] ||= 'build/cf-runtime/setup-node-crypto.mjs'
81
84
  options[:bundle_import] ||= options[:opal_output]
82
- options[:worker_module_import] ||= 'cf-runtime/worker_module.mjs'
83
- options[:entrypoint_out] ||= 'worker.entrypoint.mjs'
85
+ options[:worker_module_import] ||= 'build/cf-runtime/worker_module.mjs'
86
+ options[:entrypoint_out] ||= 'build/worker.entrypoint.mjs'
84
87
  else
85
88
  options[:setup_import] ||= 'gems/homura-runtime/runtime/setup-node-crypto.mjs'
86
89
  options[:bundle_import] ||= options[:opal_output]
@@ -272,6 +275,29 @@ else
272
275
  warn 'homura build: no app/ directory or top-level app.rb — skipping auto-await'
273
276
  end
274
277
 
278
+ # Also run auto-await over any `path:`-resolved gems declared in the
279
+ # consumer's Gemfile (e.g. `sinatra-inertia`). The transformed copies
280
+ # are written to `build/auto_await/gem_<basename>/<original-relative>`,
281
+ # and `standalone_load_paths` puts those directories ahead of the
282
+ # untransformed gem `lib/` so `require 'sinatra/inertia'` resolves to
283
+ # the rewritten file.
284
+ CloudflareWorkers::BuildSupport.path_gemfile_entries(root).each do |gem_path|
285
+ %w[lib].each do |sub|
286
+ src = gem_path.join(sub)
287
+ next unless src.directory?
288
+ out = root.join('build', 'auto_await', "gem_#{gem_path.basename}", sub)
289
+ FileUtils.mkdir_p(out)
290
+ run!(
291
+ [
292
+ 'ruby', CloudflareWorkersBuild.exe_path('auto-await').to_s,
293
+ '--input', src.to_s,
294
+ '--output', out.to_s
295
+ ],
296
+ chdir: root
297
+ )
298
+ end
299
+ end
300
+
275
301
  opal_in = Pathname(resolve_opal_input(root, options[:opal_input]))
276
302
  opal_out = Pathname(options[:opal_output])
277
303
  opal_in = root.join(opal_in) unless opal_in.absolute?
@@ -44,7 +44,13 @@ module CloudflareWorkers
44
44
  end
45
45
 
46
46
  def ensure_standalone_runtime(project_root, current_file: __FILE__, loaded_specs: Gem.loaded_specs)
47
- target_dir = Pathname(project_root).join('cf-runtime')
47
+ # The homura runtime needs two .mjs glue files alongside the
48
+ # generated `worker.entrypoint.mjs`. Until 0.2.22 we wrote them
49
+ # to `cf-runtime/` at the project root, which made every Ruby
50
+ # repo carry two opaque JS files in source control. Hide them
51
+ # under `build/cf-runtime/` so the build artifact tree owns
52
+ # them — `build/` is already in the example .gitignore template.
53
+ target_dir = Pathname(project_root).join('build', 'cf-runtime')
48
54
  FileUtils.mkdir_p(target_dir)
49
55
 
50
56
  %w[setup-node-crypto.mjs worker_module.mjs].each do |name|
@@ -80,6 +86,27 @@ module CloudflareWorkers
80
86
  end
81
87
  end
82
88
 
89
+ # Pick up any other `path:`-resolved gems declared in the consumer's
90
+ # Gemfile (e.g. `sinatra-inertia`). This keeps the build pipeline
91
+ # extensible: users can drop a pure-Ruby gem under `gems/foo`, list
92
+ # it as `gem 'foo', path: '../../gems/foo'`, and Opal will find its
93
+ # `lib/` automatically — no homura-runtime change required.
94
+ #
95
+ # We prefer the auto-await-transformed copy under
96
+ # `build/auto_await/gem_<basename>/lib` if present (homura-build
97
+ # writes one for every path: gem before invoking Opal), so that
98
+ # async chains inside the gem get `__await__` inserted just like
99
+ # consumer app code.
100
+ path_gemfile_entries(root).each do |gem_path|
101
+ basename = gem_path.basename.to_s
102
+ rewritten_lib = root.join('build', 'auto_await', "gem_#{basename}", 'lib')
103
+ load_paths << rewritten_lib.to_s if rewritten_lib.directory?
104
+ %w[lib vendor].each do |sub|
105
+ dir = gem_path.join(sub)
106
+ load_paths << dir.to_s if dir.directory?
107
+ end
108
+ end
109
+
83
110
  load_paths << 'vendor' if root.join('vendor').directory?
84
111
  load_paths << 'build'
85
112
  load_paths.uniq
@@ -105,6 +132,53 @@ module CloudflareWorkers
105
132
  vend = runtime_path.join('..', '..', 'vendor').expand_path
106
133
  vend if vend.directory?
107
134
  end
135
+
136
+ # Returns absolute Pathnames for every `path:`-declared gem in the
137
+ # project's Gemfile that should ship in the Workers bundle.
138
+ #
139
+ # Excludes:
140
+ # * gems we already wire in explicitly
141
+ # (homura-runtime / sinatra-homura / sequel-d1)
142
+ # * `require: false` gems (dev tooling like `gem 'rspec', path: ..., require: false`)
143
+ # * gems declared inside `group :development do … end` /
144
+ # `group :test do … end` blocks (they don't ship to production)
145
+ EXCLUDED_GROUPS = %i[development test dev_test development_test ci tools].freeze
146
+
147
+ def path_gemfile_entries(project_root)
148
+ gf = Pathname(project_root).join('Gemfile')
149
+ return [] unless gf.file?
150
+
151
+ wired = [RUNTIME_GEM_NAME, SINATRA_GEM_NAME, SEQUEL_D1_GEM_NAME]
152
+ out = []
153
+ group_stack = []
154
+
155
+ gf.read.each_line do |line|
156
+ stripped = line.strip
157
+ next if stripped.empty? || stripped.start_with?('#')
158
+
159
+ if (m = stripped.match(/\Agroup\s+(.+?)\s+do\b/))
160
+ groups = m[1].scan(/[:'"]([A-Za-z0-9_]+)['"]?/).flatten.map(&:to_sym)
161
+ group_stack.push(groups)
162
+ next
163
+ end
164
+ if stripped == 'end'
165
+ group_stack.pop unless group_stack.empty?
166
+ next
167
+ end
168
+
169
+ next if group_stack.flatten.any? { |g| EXCLUDED_GROUPS.include?(g) }
170
+
171
+ m = line.match(/gem\s+['"]([^'"]+)['"][^#]*?path:\s*['"]([^'"]+)['"]/)
172
+ next unless m
173
+ name, rel = m[1], m[2]
174
+ next if wired.include?(name)
175
+ next if line.match?(/require:\s*false/)
176
+
177
+ gem_path = Pathname.new(rel).expand_path(project_root)
178
+ out << gem_path if gem_path.directory?
179
+ end
180
+ out.uniq
181
+ end
108
182
  end
109
183
  end
110
184
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CloudflareWorkers
4
- VERSION = '0.2.22'
4
+ VERSION = '0.2.24'
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.22
4
+ version: 0.2.24
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kazuhiro Homma