homura-runtime 0.1.0
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 +7 -0
- data/CHANGELOG.md +6 -0
- data/README.md +37 -0
- data/bin/cloudflare-workers-build +255 -0
- data/docs/ARCHITECTURE.md +59 -0
- data/exe/auto-await +102 -0
- data/exe/compile-assets +142 -0
- data/exe/compile-erb +262 -0
- data/lib/cloudflare_workers/ai.rb +197 -0
- data/lib/cloudflare_workers/async_registry.rb +198 -0
- data/lib/cloudflare_workers/auto_await/analyzer.rb +264 -0
- data/lib/cloudflare_workers/auto_await/transformer.rb +19 -0
- data/lib/cloudflare_workers/cache.rb +234 -0
- data/lib/cloudflare_workers/durable_object.rb +590 -0
- data/lib/cloudflare_workers/email.rb +180 -0
- data/lib/cloudflare_workers/http.rb +164 -0
- data/lib/cloudflare_workers/multipart.rb +332 -0
- data/lib/cloudflare_workers/queue.rb +407 -0
- data/lib/cloudflare_workers/scheduled.rb +185 -0
- data/lib/cloudflare_workers/stream.rb +317 -0
- data/lib/cloudflare_workers/version.rb +5 -0
- data/lib/cloudflare_workers.rb +801 -0
- data/lib/opal_patches.rb +653 -0
- data/runtime/patch-opal-evals.mjs +66 -0
- data/runtime/setup-node-crypto.mjs +20 -0
- data/runtime/worker.mjs +9 -0
- data/runtime/worker_module.mjs +384 -0
- data/runtime/wrangler.toml.example +40 -0
- data/templates/wrangler.toml.example +8 -0
- metadata +104 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 772c92137bd489f019d78537753770d0ddc73f76825c48d4cab399532b5dee86
|
|
4
|
+
data.tar.gz: b9cfb81748aa74ede6731ff0aa76f5d148df52cc1e3ffc10a3d66c05167a2816
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 58b61c31af0bf8c46ce28e5a027b598539929a543fb429723bd9212defbfab8d5875b5733b14764d6ab964597354674de998e2ef84e4da933a5b1c0d8aec72e2
|
|
7
|
+
data.tar.gz: 36e72393465c2ee4411132447588f356f9e497062aa6a24aa442ebf515d43a96d5f1246593c912a105e509f6206dde397c264b8a5d5e1fc93f76148a76acdefb
|
data/CHANGELOG.md
ADDED
data/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# homura-runtime
|
|
2
|
+
|
|
3
|
+
Core Ruby + Module Worker glue for [Opal](https://opalrb.com/) on [Cloudflare Workers](https://developers.cloudflare.com/workers/). This gem does **not** depend on Sinatra or Sequel; those live in sibling gems.
|
|
4
|
+
|
|
5
|
+
## What you get
|
|
6
|
+
|
|
7
|
+
- `require 'opal_patches'` — additive patches for Opal corelib vs real-world gems (Rack, etc.).
|
|
8
|
+
- `require 'cloudflare_workers'` — Rack handler, Cloudflare bindings, multipart, queue, Durable Objects, etc.
|
|
9
|
+
- `runtime/worker_module.mjs` — fetch / scheduled / queue / DO adapters (**no Opal bundle import**).
|
|
10
|
+
- `runtime/worker.mjs` — thin bootstrap (crypto shim → bundle → `worker_module`) for legacy layouts.
|
|
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.
|
|
13
|
+
- `docs/ARCHITECTURE.md` — wrangler `main`, codegen entrypoint, and fixed-import policy.
|
|
14
|
+
|
|
15
|
+
## Quick start (homura monorepo)
|
|
16
|
+
|
|
17
|
+
1. `Gemfile`: `gem 'homura-runtime', path: 'gems/homura-runtime'` and `gem 'opal-homura', '= 1.8.3.rc1', require: 'opal'` (path or exact pin).
|
|
18
|
+
2. `bundle exec cloudflare-workers-build` — writes `build/hello.no-exit.mjs` and `build/worker.entrypoint.mjs`.
|
|
19
|
+
3. `wrangler.toml`: `main = "build/worker.entrypoint.mjs"`, `compatibility_flags = ["nodejs_compat"]`.
|
|
20
|
+
|
|
21
|
+
## Support matrix (indicative)
|
|
22
|
+
|
|
23
|
+
| Component | Verified (dev) |
|
|
24
|
+
|------------|------------------|
|
|
25
|
+
| Ruby | 3.4.x |
|
|
26
|
+
| Node | ≥ 20 |
|
|
27
|
+
| Wrangler | ^3.99 |
|
|
28
|
+
| Opal | = 1.8.3.rc1 |
|
|
29
|
+
|
|
30
|
+
## Wrangler config
|
|
31
|
+
|
|
32
|
+
- **Supported as generated / documented**: `wrangler.toml` only.
|
|
33
|
+
- `wrangler.json` / `wrangler.jsonc`: manual conversion only; not generated by this toolchain.
|
|
34
|
+
|
|
35
|
+
## License
|
|
36
|
+
|
|
37
|
+
MIT. See the repository `LICENSE`.
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Single happy path: ERB precompile → assets embed → Opal bundle → patch-opal-evals → worker.entrypoint.mjs
|
|
5
|
+
# No subcommands. Use --standalone for generated consumer apps (no homura inline routes).
|
|
6
|
+
|
|
7
|
+
require 'fileutils'
|
|
8
|
+
require 'open3'
|
|
9
|
+
require 'optparse'
|
|
10
|
+
require 'pathname'
|
|
11
|
+
|
|
12
|
+
module CloudflareWorkersBuild
|
|
13
|
+
class << self
|
|
14
|
+
def gem_root
|
|
15
|
+
spec = Gem.loaded_specs['cloudflare-workers-runtime']
|
|
16
|
+
return Pathname(spec.full_gem_path) if spec
|
|
17
|
+
Pathname(__FILE__).expand_path.join('../..')
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def runtime_dir
|
|
21
|
+
gem_root.join('runtime')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def exe_path(name)
|
|
25
|
+
gem_root.join('exe', name)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def gem_lib(name)
|
|
29
|
+
s = Gem.loaded_specs[name] || raise("cloudflare-workers-build: gem #{name} not loaded; use bundle exec from app root")
|
|
30
|
+
File.join(s.full_gem_path, 'lib')
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
options = {
|
|
36
|
+
root: Dir.pwd,
|
|
37
|
+
standalone: false,
|
|
38
|
+
opal_input: ENV['HOMURA_OPAL_INPUT'] || 'app/hello.rb',
|
|
39
|
+
opal_output: ENV['HOMURA_OPAL_OUTPUT'] || 'build/hello.no-exit.mjs',
|
|
40
|
+
patch_input: ENV['HOMURA_OPAL_PATCH_INPUT'],
|
|
41
|
+
setup_import: nil,
|
|
42
|
+
bundle_import: nil,
|
|
43
|
+
worker_module_import: nil,
|
|
44
|
+
entrypoint_out: nil,
|
|
45
|
+
with_db: false
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
OptionParser.new do |o|
|
|
49
|
+
o.banner = 'usage: cloudflare-workers-build [options]'
|
|
50
|
+
o.on('--root PATH', 'Project root (default: cwd)') { |p| options[:root] = p }
|
|
51
|
+
o.on('--standalone', 'Consumer app (skip inline-routes; use Gemfile-resolved load paths)') do
|
|
52
|
+
options[:standalone] = true
|
|
53
|
+
end
|
|
54
|
+
o.on('--with-db', 'Include sequel-d1 on Opal load path (standalone)') do
|
|
55
|
+
options[:with_db] = true
|
|
56
|
+
end
|
|
57
|
+
o.on('--input PATH', 'Opal entry .rb') { |p| options[:opal_input] = p }
|
|
58
|
+
o.on('--output PATH', 'Opal bundle .mjs') { |p| options[:opal_output] = p }
|
|
59
|
+
o.on('--patch-input PATH', 'Override patch target (default: same as --output)') { |p| options[:patch_input] = p }
|
|
60
|
+
o.on('--setup-import PATH', 'setup-node-crypto import in entrypoint') { |p| options[:setup_import] = p }
|
|
61
|
+
o.on('--bundle-import PATH', 'Opal bundle import in entrypoint') { |p| options[:bundle_import] = p }
|
|
62
|
+
o.on('--worker-module-import PATH', 'worker_module.mjs import in entrypoint') { |p| options[:worker_module_import] = p }
|
|
63
|
+
o.on('--entrypoint-out PATH', 'Where to write worker.entrypoint.mjs') { |p| options[:entrypoint_out] = p }
|
|
64
|
+
end.parse!
|
|
65
|
+
|
|
66
|
+
root = Pathname(options[:root]).expand_path
|
|
67
|
+
|
|
68
|
+
if options[:standalone]
|
|
69
|
+
Dir.chdir(root) { require 'bundler/setup' }
|
|
70
|
+
options[:setup_import] ||= './cf-runtime/setup-node-crypto.mjs'
|
|
71
|
+
options[:bundle_import] ||= './build/hello.no-exit.mjs'
|
|
72
|
+
options[:worker_module_import] ||= './cf-runtime/worker_module.mjs'
|
|
73
|
+
options[:entrypoint_out] ||= 'worker.entrypoint.mjs'
|
|
74
|
+
else
|
|
75
|
+
options[:setup_import] ||= '../gems/homura-runtime/runtime/setup-node-crypto.mjs'
|
|
76
|
+
options[:bundle_import] ||= './hello.no-exit.mjs'
|
|
77
|
+
options[:worker_module_import] ||= '../gems/homura-runtime/runtime/worker_module.mjs'
|
|
78
|
+
options[:entrypoint_out] ||= 'build/worker.entrypoint.mjs'
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
patch_target = options[:patch_input] || options[:opal_output]
|
|
82
|
+
|
|
83
|
+
def run!(argv, chdir:)
|
|
84
|
+
Dir.chdir(chdir) do
|
|
85
|
+
system(*argv) || abort("cloudflare-workers-build: failed: #{argv.join(' ')}")
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def run_opal_homura!(root, opal_input, opal_output)
|
|
90
|
+
argv = [
|
|
91
|
+
'bundle', 'exec', 'opal',
|
|
92
|
+
'-c', '-E', '--esm', '--no-source-map',
|
|
93
|
+
'-I', 'build/auto_await/app', '-I', 'build/auto_await', '-I', 'app',
|
|
94
|
+
'-I', 'gems/homura-runtime/lib',
|
|
95
|
+
'-I', 'gems/sinatra-homura/lib',
|
|
96
|
+
'-I', 'gems/sequel-d1/lib',
|
|
97
|
+
'-I', 'lib', '-I', 'vendor', '-I', 'build',
|
|
98
|
+
'-r', 'opal_patches', '-r', 'cloudflare_workers',
|
|
99
|
+
'-r', 'homura_templates', '-r', 'homura_assets',
|
|
100
|
+
'-o', opal_output,
|
|
101
|
+
opal_input
|
|
102
|
+
]
|
|
103
|
+
stderr_log = root.join('build/opal.stderr.log')
|
|
104
|
+
FileUtils.mkdir_p(root.join('build'))
|
|
105
|
+
env = { 'OPAL_PREFORK_DISABLE' => '1' }
|
|
106
|
+
out_err, status = Open3.capture2e(env, *argv, chdir: root.to_s)
|
|
107
|
+
File.write(stderr_log, out_err)
|
|
108
|
+
abort('cloudflare-workers-build: opal failed') unless status.success?
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def homura_vendor_from_gemfile(project_root)
|
|
112
|
+
gf = project_root.join('Gemfile')
|
|
113
|
+
return unless gf.file?
|
|
114
|
+
|
|
115
|
+
txt = gf.read
|
|
116
|
+
return unless (m = txt.match(/cloudflare-workers-runtime['"]\s*,\s*path:\s*['"]([^'"]+)['"]/))
|
|
117
|
+
|
|
118
|
+
runtime_path = Pathname.new(m[1]).expand_path(project_root)
|
|
119
|
+
vend = runtime_path.join('..', '..', 'vendor').expand_path
|
|
120
|
+
vend if vend.directory?
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def run_opal_standalone!(root, opal_input, opal_output, with_db:)
|
|
124
|
+
load_paths = []
|
|
125
|
+
hv = homura_vendor_from_gemfile(root)
|
|
126
|
+
load_paths << hv.to_s if hv
|
|
127
|
+
load_paths += [
|
|
128
|
+
'build/auto_await/app', 'app',
|
|
129
|
+
CloudflareWorkersBuild.gem_lib('cloudflare-workers-runtime'),
|
|
130
|
+
CloudflareWorkersBuild.gem_lib('sinatra-cloudflare-workers')
|
|
131
|
+
]
|
|
132
|
+
load_paths << CloudflareWorkersBuild.gem_lib('sequel-d1') if with_db
|
|
133
|
+
load_paths << 'vendor' if root.join('vendor').directory?
|
|
134
|
+
load_paths << 'build'
|
|
135
|
+
|
|
136
|
+
argv = ['bundle', 'exec', 'opal', '-c', '-E', '--esm', '--no-source-map']
|
|
137
|
+
load_paths.each { |p| argv.push('-I', p) }
|
|
138
|
+
argv += %w[-r opal_patches -r cloudflare_workers -r app_templates -r app_assets -o] + [opal_output, opal_input]
|
|
139
|
+
|
|
140
|
+
stderr_log = root.join('build/opal.stderr.log')
|
|
141
|
+
FileUtils.mkdir_p(root.join('build'))
|
|
142
|
+
env = { 'OPAL_PREFORK_DISABLE' => '1' }
|
|
143
|
+
out_err, status = Open3.capture2e(env, *argv, chdir: root.to_s)
|
|
144
|
+
File.write(stderr_log, out_err)
|
|
145
|
+
abort('cloudflare-workers-build: opal failed') unless status.success?
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def write_entrypoint!(root, out_path, setup:, bundle:, worker_mod:)
|
|
149
|
+
body = <<~JS
|
|
150
|
+
// GENERATED by cloudflare-workers-build — do not edit by hand.
|
|
151
|
+
import "#{setup}";
|
|
152
|
+
import "#{bundle}";
|
|
153
|
+
export { default, HomuraCounterDO } from "#{worker_mod}";
|
|
154
|
+
JS
|
|
155
|
+
File.write(root.join(out_path), body)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
unless options[:standalone]
|
|
159
|
+
run!(['ruby', 'bin/inline-routes-for-opal'], chdir: root)
|
|
160
|
+
run!(
|
|
161
|
+
[
|
|
162
|
+
'ruby', CloudflareWorkersBuild.exe_path('auto-await').to_s,
|
|
163
|
+
'--input', 'app',
|
|
164
|
+
'--output', 'build/auto_await/app'
|
|
165
|
+
],
|
|
166
|
+
chdir: root
|
|
167
|
+
)
|
|
168
|
+
run!(
|
|
169
|
+
[
|
|
170
|
+
'ruby', CloudflareWorkersBuild.exe_path('auto-await').to_s,
|
|
171
|
+
'--input', 'build/routes_app_class_eval.rb',
|
|
172
|
+
'--output', 'build/auto_await/routes_app_class_eval.rb'
|
|
173
|
+
],
|
|
174
|
+
chdir: root
|
|
175
|
+
)
|
|
176
|
+
run!(
|
|
177
|
+
[
|
|
178
|
+
'ruby', CloudflareWorkersBuild.exe_path('compile-erb').to_s,
|
|
179
|
+
'--input', 'views',
|
|
180
|
+
'--output', 'build/homura_templates.rb',
|
|
181
|
+
'--namespace', 'HomuraTemplates'
|
|
182
|
+
],
|
|
183
|
+
chdir: root
|
|
184
|
+
)
|
|
185
|
+
run!(
|
|
186
|
+
[
|
|
187
|
+
'ruby', CloudflareWorkersBuild.exe_path('compile-assets').to_s,
|
|
188
|
+
'--input', 'public',
|
|
189
|
+
'--output', 'build/homura_assets.rb',
|
|
190
|
+
'--namespace', 'HomuraAssets'
|
|
191
|
+
],
|
|
192
|
+
chdir: root
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
opal_in = Pathname(options[:opal_input])
|
|
196
|
+
opal_out = Pathname(options[:opal_output])
|
|
197
|
+
opal_in = root.join(opal_in) unless opal_in.absolute?
|
|
198
|
+
opal_out = root.join(opal_out) unless opal_out.absolute?
|
|
199
|
+
run_opal_homura!(root, opal_in.relative_path_from(root).to_s, opal_out.relative_path_from(root).to_s)
|
|
200
|
+
else
|
|
201
|
+
run!(
|
|
202
|
+
[
|
|
203
|
+
'ruby', CloudflareWorkersBuild.exe_path('compile-erb').to_s,
|
|
204
|
+
'--input', 'views',
|
|
205
|
+
'--output', 'build/app_templates.rb',
|
|
206
|
+
'--namespace', 'AppTemplates'
|
|
207
|
+
],
|
|
208
|
+
chdir: root
|
|
209
|
+
)
|
|
210
|
+
run!(
|
|
211
|
+
[
|
|
212
|
+
'ruby', CloudflareWorkersBuild.exe_path('compile-assets').to_s,
|
|
213
|
+
'--input', 'public',
|
|
214
|
+
'--output', 'build/app_assets.rb',
|
|
215
|
+
'--namespace', 'AppAssets'
|
|
216
|
+
],
|
|
217
|
+
chdir: root
|
|
218
|
+
)
|
|
219
|
+
run!(
|
|
220
|
+
[
|
|
221
|
+
'ruby', CloudflareWorkersBuild.exe_path('auto-await').to_s,
|
|
222
|
+
'--input', 'app',
|
|
223
|
+
'--output', 'build/auto_await/app'
|
|
224
|
+
],
|
|
225
|
+
chdir: root
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
opal_in = Pathname(options[:opal_input])
|
|
229
|
+
opal_out = Pathname(options[:opal_output])
|
|
230
|
+
opal_in = root.join(opal_in) unless opal_in.absolute?
|
|
231
|
+
opal_out = root.join(opal_out) unless opal_out.absolute?
|
|
232
|
+
run_opal_standalone!(root, opal_in.relative_path_from(root).to_s, opal_out.relative_path_from(root).to_s,
|
|
233
|
+
with_db: options[:with_db])
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
patch_rel = Pathname(patch_target)
|
|
237
|
+
patch_rel = root.join(patch_rel) unless patch_rel.absolute?
|
|
238
|
+
run!(
|
|
239
|
+
[
|
|
240
|
+
'node',
|
|
241
|
+
CloudflareWorkersBuild.runtime_dir.join('patch-opal-evals.mjs').to_s,
|
|
242
|
+
patch_rel.relative_path_from(root).to_s
|
|
243
|
+
],
|
|
244
|
+
chdir: root
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
write_entrypoint!(
|
|
248
|
+
root,
|
|
249
|
+
options[:entrypoint_out],
|
|
250
|
+
setup: options[:setup_import],
|
|
251
|
+
bundle: options[:bundle_import],
|
|
252
|
+
worker_mod: options[:worker_module_import]
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
$stderr.puts 'cloudflare-workers-build: ok'
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Cloudflare Workers + Opal — entrypoint topology (Phase 15-E)
|
|
2
|
+
|
|
3
|
+
## 責務分離(確定)
|
|
4
|
+
|
|
5
|
+
| レイヤ | 役割 |
|
|
6
|
+
|--------|------|
|
|
7
|
+
| `wrangler.toml` の `main` | **Workers Module のエントリ**(`fetch` / `scheduled` / `queue` / DO クラスを export) |
|
|
8
|
+
| `worker.entrypoint.mjs`(生成物) | `setup-node-crypto` → **Opal bundle(副作用)** → `worker_module.mjs` の順で import。パスはプロジェクトごとに **固定文字列**(codegen) |
|
|
9
|
+
| Opal bundle(例: `build/hello.no-exit.mjs`) | **ビルド成果物**。Rack ディスパッチャ等を `globalThis` に登録 |
|
|
10
|
+
| `worker_module.mjs`(gem 同梱) | Rack / Cron / Queue / DO への **純粋な JS アダプタ**(Opal bundle を import しない) |
|
|
11
|
+
|
|
12
|
+
## 禁止事項
|
|
13
|
+
|
|
14
|
+
- **ランタイム可変 path import**(例: `import(pathVar)` で Opal bundle を読む)は採用しない。Workers のバンドル解決と並行性の両方で破綻しやすい。
|
|
15
|
+
|
|
16
|
+
## Before / After(概念)
|
|
17
|
+
|
|
18
|
+
```mermaid
|
|
19
|
+
flowchart LR
|
|
20
|
+
subgraph before["Before (monorepo 固定)"]
|
|
21
|
+
W1[worker.mjs] -->|hard-coded| B1[../../../build/hello.mjs]
|
|
22
|
+
end
|
|
23
|
+
subgraph after["After (Phase 15-E)"]
|
|
24
|
+
E[worker.entrypoint.mjs] --> S[setup-node-crypto.mjs]
|
|
25
|
+
E --> O[build/*.mjs Opal bundle]
|
|
26
|
+
E --> M[worker_module.mjs]
|
|
27
|
+
end
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## homura 本体
|
|
31
|
+
|
|
32
|
+
- `wrangler.toml` の `main` は `build/worker.entrypoint.mjs`。
|
|
33
|
+
- `bundle exec cloudflare-workers-build` が ERB / assets / Opal / patch / entrypoint 生成まで一括実行。
|
|
34
|
+
|
|
35
|
+
## スキャフォールド済みアプリ
|
|
36
|
+
|
|
37
|
+
- プロジェクト直下に `worker.entrypoint.mjs`(`main` と一致)。
|
|
38
|
+
- `cf-runtime/` に `setup-node-crypto.mjs` と `worker_module.mjs` をコピー(gem から)。
|
|
39
|
+
- `bundle exec cloudflare-workers-build --standalone` が consumer 向けパイプラインを実行し、`Gemfile` の `path:` から homura の `vendor/` を追加ロードパスへ取り込み(digest / zlib 等の Workers 向け補助ファイル)。
|
|
40
|
+
|
|
41
|
+
## Phase 17 — Email Service(`SEND_EMAIL`)
|
|
42
|
+
|
|
43
|
+
| 項目 | 内容 |
|
|
44
|
+
|------|------|
|
|
45
|
+
| Wrangler | `[[send_email]]` に `name = "SEND_EMAIL"`(Cloudflare Email Service · Agents Week 2026)。 |
|
|
46
|
+
| Rack env | `env['cloudflare.SEND_EMAIL']` は `Cloudflare::Email`(JS `env.SEND_EMAIL.send(...)` を Ruby 側で `await`)。 |
|
|
47
|
+
| 備考 | consumer アプリ側で verified sender を wrangler `[vars]` などに載せ、アプリから `from` に渡す。 |
|
|
48
|
+
|
|
49
|
+
### Phase 17-E — `/cdn-cgi/*`(Rack に渡さない)とバインディング注入
|
|
50
|
+
|
|
51
|
+
| 項目 | 内容 |
|
|
52
|
+
|------|------|
|
|
53
|
+
| **`worker_module.mjs` の `fetch` 先頭** | `env.SEND_EMAIL` があれば **`globalThis.__OPAL_WORKERS__.sendEmailBinding`** にコピーしてから Rack を呼ぶ。Miniflare 等で `js_env.SEND_EMAIL` が欠ける場合でも Ruby が同じ JS オブジェクトを拾える。 |
|
|
54
|
+
| **`/cdn-cgi/*`** | **Sinatra に渡さない**。Miniflare の entry.worker は `/cdn-cgi/mf/scheduled` だけ先処理する。`/cdn-cgi/handler/email` などはユーザ Worker の `fetch` に届くため、ここで処理しないと Rack が 404 になる。将来 Phase 18(Email 受信)で `export async email(...)` と接続する前提。 |
|
|
55
|
+
| **D1 / KV / Queue との違い** | それらは典型的に `env['cloudflare.env']` 経由で JS バインディングを参照する。`SEND_EMAIL` は **Worker の `env` に付く生バインディング**を `worker_module` が先に global に載せ、`Cloudflare::Email` がそれを包む(Rack の `cloudflare.*` はラッパー用の入口)。 |
|
|
56
|
+
|
|
57
|
+
## wrangler.json について
|
|
58
|
+
|
|
59
|
+
- **生成・サポート対象は `wrangler.toml` のみ**。`wrangler.json` / `wrangler.jsonc` は手動変換可だが、本ツールチェーンの前提外。
|
data/exe/auto-await
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Auto-Await transformer — build-time Ruby source rewriter.
|
|
5
|
+
# Scans app/**/*.rb, runs flow analysis, and inserts .__await__
|
|
6
|
+
# where the receiver is a known async-tainted source.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# ruby auto-await --input app --output build/auto_await/app
|
|
10
|
+
#
|
|
11
|
+
# The output directory mirrors the input. Files that need no change
|
|
12
|
+
# are skipped entirely (not copied), so the consumer can place the
|
|
13
|
+
# output directory earlier on the Opal load path and fall back to
|
|
14
|
+
# the original input for unchanged files.
|
|
15
|
+
|
|
16
|
+
require 'fileutils'
|
|
17
|
+
require 'pathname'
|
|
18
|
+
|
|
19
|
+
# cloudflare-workers-runtime lib path resolution
|
|
20
|
+
runtime_lib = ENV['CFW_RUNTIME_LIB']
|
|
21
|
+
unless runtime_lib
|
|
22
|
+
spec = Gem.loaded_specs['cloudflare-workers-runtime']
|
|
23
|
+
runtime_lib = spec ? File.join(spec.full_gem_path, 'lib') : File.expand_path('../lib', __dir__)
|
|
24
|
+
end
|
|
25
|
+
$LOAD_PATH.unshift(runtime_lib) unless $LOAD_PATH.include?(runtime_lib)
|
|
26
|
+
|
|
27
|
+
require 'cloudflare_workers/async_registry'
|
|
28
|
+
require 'cloudflare_workers/auto_await/analyzer'
|
|
29
|
+
require 'cloudflare_workers/auto_await/transformer'
|
|
30
|
+
|
|
31
|
+
options = { input: nil, output: nil, debug: ENV['CLOUDFLARE_WORKERS_AUTO_AWAIT_DEBUG'] == '1' }
|
|
32
|
+
|
|
33
|
+
ARGV.each_with_index do |arg, i|
|
|
34
|
+
case arg
|
|
35
|
+
when '--input' then options[:input] = ARGV[i + 1]
|
|
36
|
+
when '--output' then options[:output] = ARGV[i + 1]
|
|
37
|
+
when '--debug' then options[:debug] = true
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
abort('Usage: auto-await --input DIR --output DIR [--debug]') unless options[:input] && options[:output]
|
|
42
|
+
|
|
43
|
+
input_root = Pathname(options[:input]).expand_path
|
|
44
|
+
output_root = Pathname(options[:output]).expand_path
|
|
45
|
+
|
|
46
|
+
registry = CloudflareWorkers::AsyncRegistry.instance
|
|
47
|
+
|
|
48
|
+
# Auto-load async source registrations from all loaded gems.
|
|
49
|
+
CloudflareWorkers::AsyncRegistry.auto_load_gem_async_sources(debug: options[:debug])
|
|
50
|
+
|
|
51
|
+
# Load project-specific async source registrations if present.
|
|
52
|
+
project_async = File.join(Dir.pwd, 'lib', 'homura_async_sources.rb')
|
|
53
|
+
require project_async if File.exist?(project_async)
|
|
54
|
+
|
|
55
|
+
changed = 0
|
|
56
|
+
skipped = 0
|
|
57
|
+
errors = 0
|
|
58
|
+
|
|
59
|
+
paths = if File.directory?(input_root)
|
|
60
|
+
Dir.glob(input_root.join('**', '*.rb')).sort
|
|
61
|
+
else
|
|
62
|
+
[input_root.to_s]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
paths.each do |path|
|
|
66
|
+
rel = Pathname(path).relative_path_from(input_root)
|
|
67
|
+
source = File.read(path)
|
|
68
|
+
|
|
69
|
+
# If the file already has a user-written # await: magic comment we
|
|
70
|
+
# still run the analyzer (so .__await__ is inserted) but we keep
|
|
71
|
+
# the original magic comment in place. The magic comment is only
|
|
72
|
+
# needed to tell Opal to wrap the file in an async function and to
|
|
73
|
+
# translate #__await__ calls into real JS await keywords.
|
|
74
|
+
has_magic = source.lines.first(5).any? { |l| l.match?(/#\s*await:/) }
|
|
75
|
+
|
|
76
|
+
begin
|
|
77
|
+
analyzer = CloudflareWorkers::AutoAwait::Analyzer.new(registry, debug: options[:debug])
|
|
78
|
+
buffer, nodes = analyzer.process(source, path)
|
|
79
|
+
|
|
80
|
+
if nodes.empty?
|
|
81
|
+
puts "[auto-await] skip (no changes) #{rel}" if options[:debug]
|
|
82
|
+
skipped += 1
|
|
83
|
+
next
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
transformed = CloudflareWorkers::AutoAwait::Transformer.transform(source, nodes, buffer)
|
|
87
|
+
unless has_magic
|
|
88
|
+
transformed = "# await: true\n" + transformed
|
|
89
|
+
end
|
|
90
|
+
out_path = output_root.join(rel)
|
|
91
|
+
FileUtils.mkdir_p(out_path.dirname)
|
|
92
|
+
File.write(out_path, transformed)
|
|
93
|
+
puts "[auto-await] write #{rel} (#{nodes.length} await#{'s' if nodes.length > 1})" if options[:debug]
|
|
94
|
+
changed += 1
|
|
95
|
+
rescue => e
|
|
96
|
+
$stderr.puts "[auto-await] ERROR #{rel}: #{e.message}"
|
|
97
|
+
errors += 1
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
puts "[auto-await] done: #{changed} changed, #{skipped} skipped, #{errors} errors"
|
|
102
|
+
exit(1) if errors > 0
|
data/exe/compile-assets
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
#
|
|
4
|
+
# homura static-asset embedder.
|
|
5
|
+
#
|
|
6
|
+
# Cloudflare Workers has no filesystem, so Sinatra's `static!` (which
|
|
7
|
+
# calls `File.file?` + `send_file`) returns 404 for every asset.
|
|
8
|
+
# This script reads every file under `public/` at build time, wraps
|
|
9
|
+
# the content in a Ruby Hash constant, and generates a Rack middleware
|
|
10
|
+
# that serves matching paths directly from memory.
|
|
11
|
+
#
|
|
12
|
+
# Usage:
|
|
13
|
+
# ruby bin/compile-assets --input public --output build/homura_assets.rb --namespace HomuraAssets
|
|
14
|
+
|
|
15
|
+
require 'fileutils'
|
|
16
|
+
require 'optparse'
|
|
17
|
+
|
|
18
|
+
MIME_TYPES = {
|
|
19
|
+
'.css' => 'text/css; charset=utf-8',
|
|
20
|
+
'.js' => 'application/javascript; charset=utf-8',
|
|
21
|
+
'.json' => 'application/json; charset=utf-8',
|
|
22
|
+
'.html' => 'text/html; charset=utf-8',
|
|
23
|
+
'.svg' => 'image/svg+xml',
|
|
24
|
+
'.png' => 'image/png',
|
|
25
|
+
'.jpg' => 'image/jpeg',
|
|
26
|
+
'.jpeg' => 'image/jpeg',
|
|
27
|
+
'.gif' => 'image/gif',
|
|
28
|
+
'.ico' => 'image/x-icon',
|
|
29
|
+
'.woff' => 'font/woff',
|
|
30
|
+
'.woff2'=> 'font/woff2',
|
|
31
|
+
'.txt' => 'text/plain; charset=utf-8',
|
|
32
|
+
'.xml' => 'application/xml; charset=utf-8',
|
|
33
|
+
'.webp' => 'image/webp',
|
|
34
|
+
'.map' => 'application/json',
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
def mime_for(path)
|
|
38
|
+
ext = File.extname(path).downcase
|
|
39
|
+
MIME_TYPES[ext] || 'application/octet-stream'
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
HELP = <<~USAGE
|
|
43
|
+
Usage:
|
|
44
|
+
ruby bin/compile-assets [--input DIR] [--output FILE] [--namespace NAME]
|
|
45
|
+
|
|
46
|
+
Defaults (Phase 15-A homura):
|
|
47
|
+
--input public --output build/homura_assets.rb --namespace HomuraAssets
|
|
48
|
+
USAGE
|
|
49
|
+
|
|
50
|
+
if ARGV.include?('-h') || ARGV.include?('--help')
|
|
51
|
+
puts HELP
|
|
52
|
+
exit 0
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
options = {
|
|
56
|
+
input_dir: 'public',
|
|
57
|
+
output: 'build/homura_assets.rb',
|
|
58
|
+
namespace: 'HomuraAssets'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
OptionParser.new do |op|
|
|
62
|
+
op.banner = 'Usage: bin/compile-assets [options]'
|
|
63
|
+
op.on('--input DIR', 'Root directory to embed (recursive)') { |d| options[:input_dir] = d }
|
|
64
|
+
op.on('--output PATH', 'Generated Ruby output path') { |p| options[:output] = p }
|
|
65
|
+
op.on('--namespace NAME', 'Ruby module name') { |n| options[:namespace] = n }
|
|
66
|
+
end.parse!
|
|
67
|
+
|
|
68
|
+
public_dir = File.join(Dir.pwd, options[:input_dir])
|
|
69
|
+
unless File.directory?(public_dir)
|
|
70
|
+
warn "compile-assets: no directory #{public_dir.inspect}"
|
|
71
|
+
exit 1
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
ns = options[:namespace]
|
|
75
|
+
|
|
76
|
+
files = Dir.glob(File.join(public_dir, '**', '*')).select { |f| File.file?(f) }.sort
|
|
77
|
+
if files.empty?
|
|
78
|
+
warn 'compile-assets: directory is empty'
|
|
79
|
+
exit 1
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
out_path = options[:output]
|
|
83
|
+
FileUtils.mkdir_p(File.dirname(out_path))
|
|
84
|
+
|
|
85
|
+
File.open(out_path, 'w') do |io|
|
|
86
|
+
io.puts <<~RUBY
|
|
87
|
+
# frozen_string_literal: true
|
|
88
|
+
# Auto-generated by bin/compile-assets — DO NOT EDIT BY HAND.
|
|
89
|
+
#
|
|
90
|
+
# Embeds every file under #{options[:input_dir]}/ as a Ruby constant, then installs a
|
|
91
|
+
# Rack middleware that serves matching paths before they reach Sinatra.
|
|
92
|
+
# This replaces Sinatra's `static!` (which needs a real filesystem)
|
|
93
|
+
# with an in-memory lookup that works on Cloudflare Workers.
|
|
94
|
+
|
|
95
|
+
module #{ns}
|
|
96
|
+
ASSETS = {}
|
|
97
|
+
|
|
98
|
+
class Middleware
|
|
99
|
+
def initialize(app)
|
|
100
|
+
@app = app
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def call(env)
|
|
104
|
+
path = env['PATH_INFO']
|
|
105
|
+
if (asset = #{ns}::ASSETS[path])
|
|
106
|
+
headers = {
|
|
107
|
+
'content-type' => asset[:content_type],
|
|
108
|
+
'content-length' => asset[:body].bytesize.to_s,
|
|
109
|
+
'cache-control' => 'public, max-age=3600',
|
|
110
|
+
}
|
|
111
|
+
[200, headers, [asset[:body]]]
|
|
112
|
+
else
|
|
113
|
+
@app.call(env)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
RUBY
|
|
119
|
+
|
|
120
|
+
files.each do |full_path|
|
|
121
|
+
rel = full_path.sub(public_dir, '') # e.g. "/style.css"
|
|
122
|
+
content = File.read(full_path)
|
|
123
|
+
ct = mime_for(full_path)
|
|
124
|
+
|
|
125
|
+
io.puts
|
|
126
|
+
io.puts "# #{rel} (#{content.bytesize} bytes)"
|
|
127
|
+
io.puts "#{ns}::ASSETS[#{rel.inspect}] = {"
|
|
128
|
+
io.puts " content_type: #{ct.inspect},"
|
|
129
|
+
io.puts " body: #{content.inspect}"
|
|
130
|
+
io.puts "}"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
io.puts <<~RUBY
|
|
134
|
+
|
|
135
|
+
# Auto-install the middleware on Sinatra::Base if Sinatra is loaded.
|
|
136
|
+
if defined?(::Sinatra::Base)
|
|
137
|
+
::Sinatra::Base.use #{ns}::Middleware
|
|
138
|
+
end
|
|
139
|
+
RUBY
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
warn "compile-assets: wrote #{out_path} (#{files.size} assets, #{File.size(out_path)} bytes)"
|