picoruby-cloudflare-template 0.1.0.rc1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0514b3333048a221efffa3fa803528f1c2aafac2c14b4f545cfb59484c67ac11
4
+ data.tar.gz: b5db10ca44c30093aee69c5675055432e89c2761ed70279d518104408fda9c1b
5
+ SHA512:
6
+ metadata.gz: 7c0531c2e85494b47656f0b621de96bde954776951aa4ede77d8f0ce682ec8328cee3d220ffb6015002e0cbffb933326cc37292e37759b9672d36dd5cc572b77
7
+ data.tar.gz: e99ef3c75479ee721aa68698a72366a4d8b278385b5d663505bf8214636f5faced03e5cc5ad1cc1abad5f40d29db31df7fe7ada861ac190e362e9c98461b76f4
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Uchio Kondo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.ja.md ADDED
@@ -0,0 +1,183 @@
1
+ # picoruby-cloudflare-template
2
+
3
+ [English](README.md) | 日本語
4
+
5
+ PicoRuby向けCloudflare Workerプロジェクトの生成、CrossBuild設定、ローカルES module出力を担当するCRuby gemです。
6
+ Wasm実行時には不要です。npmパッケージの公開も不要です。
7
+
8
+ ## クイックスタート(未公開版の開発中)
9
+
10
+ macOSでは、まず[Homebrew](https://formulae.brew.sh/formula/emscripten)経由でEmscriptenをインストールします。
11
+
12
+ ```sh
13
+ brew install emscripten
14
+ export PATH="$(brew --prefix emscripten)/bin:$PATH"
15
+ emcc --version
16
+ ```
17
+
18
+ Emscripten 5.0.0以上を使用できます。5.0.7およびHomebrew版6.0.9で動作確認済みです。
19
+ 新しい版もバージョンチェックを通りますが、すべての版を検証済みという意味ではありません。
20
+ emsdkから切り替える場合は、`emsdk_env.sh`を読み込まないシェルを使い、`EMSDK`・`EM_CONFIG`・`EM_CACHE`をunsetしてツールチェーンの混在を避けてください。
21
+
22
+ 続いて、このリポジトリで実行します。
23
+
24
+ ```sh
25
+ bundle install
26
+ bundle exec ruby exe/picoruby-cloudflare new ../my-worker --gem-path "$PWD"
27
+ cd ../my-worker
28
+ bundle install
29
+ npm install
30
+
31
+ export PICORUBY_ROOT=/path/to/picoruby
32
+ # ビルド前に、下記の例に従ってbuild_config.rbのローカルmrbgemパスを設定
33
+
34
+ bundle exec rake doctor
35
+ bundle exec rake
36
+ npm run dev
37
+ ```
38
+
39
+ PicoRubyはsubmodule初期化済みのチェックアウトを指定します。初期化・Emscripten導入はgemでは行いません。
40
+ `doctor` はPicoRubyの主要ファイル、emcc、emar、Node.js、jsonc-parserを確認します。
41
+ ビルド時にはEmscriptenが対応する版かも検査します。Node.jsはWranglerがサポートする版を使用してください。
42
+ 上流のシェルコマンド展開の制限により、ビルド用パスでは空白やシェル特殊文字を拒否します。
43
+
44
+ 配布gemを試す場合は `gem build picoruby-cloudflare-template.gemspec`、
45
+ `gem install ./picoruby-cloudflare-template-0.1.0.rc1.gem` の後、
46
+ `picoruby-cloudflare new my-worker` を使えます。生成先で未公開版を解決するには `bundle install --local` を使用します。
47
+ このリリース候補版を公開した後は `gem install picoruby-cloudflare-template --pre --version 0.1.0.rc1` でインストールできます。
48
+ `VERSION` とRubyGemsのメタデータ上のバージョンは、どちらも `0.1.0.rc1` です。
49
+
50
+ ## 生成物とビルド設定
51
+
52
+ `new PATH [--name NAME] [--gem-path PATH]` はGemfile、Rakefile、build_config.rb、最小Rackアプリのapp.rb、
53
+ src/index.js、package.json、wrangler.jsonc、.gitignore、README.mdを生成します。
54
+ 生成先が存在する場合は空ディレクトリでも上書きしません。Gemfile.lockとpackage-lock.jsonはアプリ側でコミットしてください。
55
+
56
+ ```ruby
57
+ require "picoruby/cloudflare/build"
58
+
59
+ MRuby::CrossBuild.new("worker") do |conf|
60
+ conf.cloudflare_worker! do |cf|
61
+ # 任意: ローカルチェックアウトはrevision指定より優先されます。
62
+ # cf.picoruby_cloudflare_worker_wasm_mgem_dir = "/path/to/picoruby-cloudflare-worker-wasm"
63
+ # cf.mruby_rack_mgem_dir = "/path/to/mruby-rack"
64
+ # cf.picoruby_cloudflare_worker_wasm_revision = "<commit SHA>"
65
+ # cf.mruby_rack_mgem_revision = "<commit SHA>"
66
+ end
67
+ # conf.gem gemdir: File.join(__dir__, "vendor/my-gem")
68
+ conf.worker_export(
69
+ app: "app.rb",
70
+ output_dir: "generated/worker",
71
+ wrangler_config: "wrangler.jsonc",
72
+ environment: ENV["CLOUDFLARE_ENV"],
73
+ project_root: __dir__,
74
+ )
75
+ end
76
+ ```
77
+
78
+ このrequireはPicoRubyのビルドシステム読込後、build_config.rb内で行います。
79
+ `cloudflare_worker!` はEmscripten、Wasm longjmp、Worker HAL、PicoRuby、Rackと必要なcore mrbgemを設定します。
80
+ Sinatra等のフレームワークはアプリ側で追加します。ABI固有の最終リンク設定(JSPI export等)は実行時ライブラリが所有します。
81
+ 属性は `cloudflare_worker!` のブロック内で設定します。ブロックにはCrossBuild自身が渡され、検証やビルド設定の本処理より先に実行されます。
82
+ ブロックなしでも呼べます。その場合は事前に属性を設定してください。`!` はビルド設定を書き換えることを示します。
83
+ 両方のディレクトリ属性はデフォルト `nil` で、その場合はrevision属性を使い、`github:` と `checksum_hash:` でgemを宣言します。
84
+ ディレクトリ指定はrevision指定より優先され、相対ディレクトリはbuild_configのディレクトリ基準で解決します。
85
+ revision属性のデフォルトはこのgemに組み込まれた値です。`nil` を代入するとデフォルトに戻ります。
86
+ 取得先の選択で `PICORUBY_WORKER_WASM_GEM_DIR` / `MRUBY_RACK_GEM_DIR` は参照せず、`worker:` / `rack:` 引数も受け取りません。
87
+
88
+ 既定のWorker revisionは `e6235bca616dbd4cec619cc0141facdea59a5541`、
89
+ Rackは `05ba46eb0ab490a624a5f2dcb33249670933ff6b` に固定しています。
90
+ revisionがリモート未公開の場合はローカル指定が必要です。gem公開前に、新規チェックアウトから固定revisionを取得できることも確認してください。
91
+
92
+ `worker_export` に渡す相対パスは `project_root` 基準(省略時はbuild_configのディレクトリ)です。
93
+ 生成されたRakefileはPicoRubyのRakeを別プロセスで実行し、ビルドをアプリ内の `.picoruby-build/` に分離します。
94
+ アプリはCrossBuildが解決した `mrbcfile` でコンパイルし、既存の `build/host/bin/mrbc` には依存しません。
95
+
96
+ ## ES module出力と責務
97
+
98
+ ```text
99
+ generated/worker/
100
+ app.bin
101
+ bindings.js
102
+ package.json # private: true, type: module
103
+ manifest.json # generator版・Worker revision・成果物SHA256
104
+ runtime/
105
+ index.js # createWorker({ app, bindingTypes })
106
+ runtime.js
107
+ host-bridge.js
108
+ picoruby-worker.js
109
+ picoruby-worker.wasm
110
+ tools/ # binding registry生成スクリプト
111
+ ```
112
+
113
+ Ruby/C・HAL・共通JS bridgeは実行時ライブラリが、このgemはテンプレート・CrossBuild DSL・export処理・薄いcreateWorkerエントリを所有します。
114
+ 共通JSとregistry生成スクリプトは、Wasmをビルドした**同じmrbgemチェックアウト**からコピーします。
115
+ 現在の取得場所は `spike/src/` と `spike/scripts/` です。別コピーの実装をこのgemで管理しません。
116
+ レイアウト変更時はexporterと固定revisionを一緒に更新します。
117
+
118
+ `createWorker` はリクエストごとにVMを生成・破棄し、異なるリクエストのenvを共有しません。
119
+ 低レベルの `createRuntime` / `dispatch` / `closeRuntime` も再exportします。
120
+ 明示的にVMを再利用した場合、同じVMへのdispatchは実行時ライブラリが直列化します。
121
+ 出力はWranglerでバンドルする前提です。Node.jsがそのまま `.wasm` / `.bin` importできるという意味ではありません。
122
+ 再配布する場合は、元のmrbgemや追加依存のライセンス条件も確認してください。
123
+
124
+ アプリの再コンパイルはRakeの依存関係で判定し、exportは同じ内容なら書き直しません。
125
+ registryは環境変更を反映するため毎回検証・生成します。元ビルドのWasmが単独で欠けた場合も再リンクします。
126
+ ただし検証時のPicoRubyはsrc/version.cを毎回更新するため、無変更ビルドでもそのコンパイルと最終リンクが走ります。
127
+
128
+ ## bindings・環境・Wrangler
129
+
130
+ wrangler.jsoncの `kv_namespaces` / `queues.producers` から型registryを生成します。
131
+ JSONCのコメント・末尾カンマに対応し、不正な設定・重複名・存在しない環境はビルドエラーにします。
132
+ varsの値やsecretはビルド成果物へ埋め込みません。
133
+
134
+ ```ruby
135
+ kv = Cloudflare::KV.from_env(env, "CACHE_KV")
136
+ kv.put("key", "value", ttl: 60)
137
+ value = env["cloudflare.env"].CACHE_KV.get("key")
138
+ Cloudflare::Queue.from_env(env, "EVENTS").send("created")
139
+ token = ENV["API_TOKEN"]
140
+ ```
141
+
142
+ Queueは現行APIに合わせてUTF-8文字列送信のみです。secretは.dev.varsまたは `wrangler secret put` で管理し、Gitへ追加しないでください。
143
+ `npm run dev` / `npm run deploy` ではWranglerのcustom buildがRakeを実行します。
144
+ `build.watch_dir` はapp.rbとbuild_config.rbです。Rubyファイルを増やしたときは監視対象も更新してください。
145
+
146
+ ```sh
147
+ CLOUDFLARE_ENV=staging npm run dev
148
+ CLOUDFLARE_ENV=staging npm run deploy
149
+ ```
150
+
151
+ 名前付き環境を `--env staging` だけで選ぶとcustom buildに環境名が伝わりません。
152
+ Wranglerとexporterが同じ `CLOUDFLARE_ENV` を参照するようにしてください。
153
+ リソースbindingは環境ごとに定義し、トップレベルから継承しません。
154
+
155
+ ## テスト・最小再現
156
+
157
+ ```sh
158
+ bundle exec rake test
159
+
160
+ PICORUBY_ROOT=/path/to/picoruby \
161
+ PICORUBY_WORKER_WASM_GEM_DIR=/path/to/picoruby-cloudflare-worker-wasm \
162
+ MRUBY_RACK_GEM_DIR=/path/to/mruby-rack \
163
+ bundle exec rake test:integration
164
+ ```
165
+
166
+ このテストコマンドのmrbgem環境変数は、統合テスト用ハーネスへの入力に限定しています。
167
+ ハーネスは生成したbuild_configへディレクトリ属性を明示的に書き込み、ビルド実行前にこれらの環境変数を解除します。
168
+
169
+ 単体テスト: CLI、非上書き、パス検証、DSL、compiler選択、増分export、欠損Wasm復旧、コンパイル失敗時の保護。
170
+ 統合テスト: 新規生成・依存インストール・ビルド・Wrangler dry-run・ローカルHTTPとhot reload・実Wasm経由のENV/KV TTL/Queue・
171
+ 並行リクエスト間のenv分離・環境切り替え・欠損Wasm復旧。
172
+ 統合テスト用Node.jsはJSPI対応が必要です。デプロイやCloudflareリソースの作成はしません。
173
+ 調査用に、表示した一時ディレクトリへ全成果物とステップ別ログを残します。
174
+
175
+ 検証対象: PicoRuby `33540f66d9aba633d4d3ebd6707d5c12baebb652`、上記Worker/Rack revision、
176
+ Ruby 4.0.5、Emscripten 5.0.7およびHomebrew版6.0.9、Node.js 26.8.1、Wrangler 4.125.0。
177
+ compatibility dateは固定Wranglerと組み合わせて確認した `2026-08-22` を使います。
178
+ dry-runはworkerdを起動しないため、Wrangler/dateの更新時はローカルHTTP確認も必要です。
179
+ PicoRubyやmruby submoduleのビルドAPI変更時には統合テストを再実行してください。
180
+
181
+ ## ライセンス
182
+
183
+ このgemは [MIT License](LICENSE) で公開しています。
data/README.md ADDED
@@ -0,0 +1,183 @@
1
+ # picoruby-cloudflare-template
2
+
3
+ English | [日本語](README.ja.md)
4
+
5
+ A CRuby gem for generating PicoRuby Cloudflare Worker projects, configuring CrossBuild, and exporting local ES modules.
6
+ It is not needed at Wasm runtime, and publishing an npm package is not required.
7
+
8
+ ## Quick start (before publication)
9
+
10
+ On macOS, install Emscripten via [Homebrew](https://formulae.brew.sh/formula/emscripten) first:
11
+
12
+ ```sh
13
+ brew install emscripten
14
+ export PATH="$(brew --prefix emscripten)/bin:$PATH"
15
+ emcc --version
16
+ ```
17
+
18
+ Emscripten 5.0.0 or later is accepted. Versions 5.0.7 and Homebrew 6.0.9 are tested;
19
+ accepting newer versions does not imply they have all been tested.
20
+ If switching from emsdk, use a shell without `emsdk_env.sh` and unset `EMSDK`, `EM_CONFIG`, and `EM_CACHE` to avoid mixing toolchains.
21
+
22
+ Then run the following from this repository:
23
+
24
+ ```sh
25
+ bundle install
26
+ bundle exec ruby exe/picoruby-cloudflare new ../my-worker --gem-path "$PWD"
27
+ cd ../my-worker
28
+ bundle install
29
+ npm install
30
+
31
+ export PICORUBY_ROOT=/path/to/picoruby
32
+ # Set local mrbgem paths in build_config.rb as shown below before building
33
+
34
+ bundle exec rake doctor
35
+ bundle exec rake
36
+ npm run dev
37
+ ```
38
+
39
+ Use a PicoRuby checkout with its submodules initialized. This gem does not initialize submodules or install Emscripten.
40
+ `doctor` checks key PicoRuby files, emcc, emar, Node.js, and jsonc-parser.
41
+ The build also checks that the Emscripten version is supported. Use a Node.js version supported by Wrangler.
42
+ Build paths containing spaces or shell metacharacters are rejected because of upstream shell command expansion limitations.
43
+
44
+ To try a packaged gem, run `gem build picoruby-cloudflare-template.gemspec`, followed by
45
+ `gem install ./picoruby-cloudflare-template-0.1.0.rc1.gem` and
46
+ `picoruby-cloudflare new my-worker`. Use `bundle install --local` in the generated project to resolve the unpublished version.
47
+ After this release candidate is published, install it with `gem install picoruby-cloudflare-template --pre --version 0.1.0.rc1`.
48
+ `VERSION` and the RubyGems metadata version are both `0.1.0.rc1`.
49
+
50
+ ## Generated files and build configuration
51
+
52
+ `new PATH [--name NAME] [--gem-path PATH]` generates a Gemfile, Rakefile, build_config.rb, a minimal Rack app in app.rb,
53
+ src/index.js, package.json, wrangler.jsonc, .gitignore, and README.md.
54
+ An existing destination is never overwritten, even if it is an empty directory. Commit Gemfile.lock and package-lock.json in your application repository.
55
+
56
+ ```ruby
57
+ require "picoruby/cloudflare/build"
58
+
59
+ MRuby::CrossBuild.new("worker") do |conf|
60
+ conf.cloudflare_worker! do |cf|
61
+ # Optional: local checkouts take precedence over revisions.
62
+ # cf.picoruby_cloudflare_worker_wasm_mgem_dir = "/path/to/picoruby-cloudflare-worker-wasm"
63
+ # cf.mruby_rack_mgem_dir = "/path/to/mruby-rack"
64
+ # cf.picoruby_cloudflare_worker_wasm_revision = "<commit SHA>"
65
+ # cf.mruby_rack_mgem_revision = "<commit SHA>"
66
+ end
67
+ # conf.gem gemdir: File.join(__dir__, "vendor/my-gem")
68
+ conf.worker_export(
69
+ app: "app.rb",
70
+ output_dir: "generated/worker",
71
+ wrangler_config: "wrangler.jsonc",
72
+ environment: ENV["CLOUDFLARE_ENV"],
73
+ project_root: __dir__,
74
+ )
75
+ end
76
+ ```
77
+
78
+ Place this require in build_config.rb, after PicoRuby has loaded its build system.
79
+ `cloudflare_worker!` configures Emscripten, Wasm longjmp, the Worker HAL, PicoRuby, Rack, and the required core mrbgems.
80
+ Add frameworks such as Sinatra in your application configuration. ABI-specific final link settings, such as JSPI exports, belong to the runtime library.
81
+ Set the attributes in the `cloudflare_worker!` block. It receives the CrossBuild object itself and runs before validation and build setup.
82
+ Calling without a block is also supported; in that case, set any overrides beforehand. The `!` marks its changes to the build configuration.
83
+ Both directory attributes default to `nil`; in that case the gem is declared with `github:` and `checksum_hash:` using its revision attribute.
84
+ A directory takes precedence over its revision, and relative directory paths are resolved against the build_config directory.
85
+ Revision attributes default to the values bundled in this gem; assigning `nil` restores those defaults.
86
+ Dependency source selection no longer reads `PICORUBY_WORKER_WASM_GEM_DIR` or `MRUBY_RACK_GEM_DIR`, or accepts `worker:` / `rack:` arguments.
87
+
88
+ The default Worker revision is pinned to `e6235bca616dbd4cec619cc0141facdea59a5541`,
89
+ and Rack to `05ba46eb0ab490a624a5f2dcb33249670933ff6b`.
90
+ Use a local checkout if a revision has not been published to the remote repository. Before publishing this gem, verify that a fresh checkout can fetch the pinned revisions.
91
+
92
+ Relative paths passed to `worker_export` are resolved against `project_root`, which defaults to the build_config directory.
93
+ The generated Rakefile runs PicoRuby's Rake in a separate process and keeps build output in the application's `.picoruby-build/` directory.
94
+ The application is compiled with the `mrbcfile` resolved by CrossBuild, without relying on an existing `build/host/bin/mrbc`.
95
+
96
+ ## ES module output and responsibilities
97
+
98
+ ```text
99
+ generated/worker/
100
+ app.bin
101
+ bindings.js
102
+ package.json # private: true, type: module
103
+ manifest.json # Generator version, Worker revision, artifact SHA256 hashes
104
+ runtime/
105
+ index.js # createWorker({ app, bindingTypes })
106
+ runtime.js
107
+ host-bridge.js
108
+ picoruby-worker.js
109
+ picoruby-worker.wasm
110
+ tools/ # Binding registry generation scripts
111
+ ```
112
+
113
+ The runtime library owns the Ruby/C code, HAL, and shared JS bridge. This gem owns the templates, CrossBuild DSL, export logic, and thin createWorker entry point.
114
+ Shared JS and registry generation scripts are copied from **the same mrbgem checkout** used to build Wasm.
115
+ Their current locations are `spike/src/` and `spike/scripts/`. This gem does not maintain a separate copy of those implementations.
116
+ If that layout changes, update the exporter and pinned revision together.
117
+
118
+ `createWorker` creates and closes a VM for each request, without sharing env between requests.
119
+ The low-level `createRuntime` / `dispatch` / `closeRuntime` functions are also re-exported.
120
+ If you explicitly reuse a VM, the runtime library serializes dispatches to that VM.
121
+ The output is intended to be bundled with Wrangler; it does not make `.wasm` / `.bin` imports directly usable in Node.js.
122
+ Check the license requirements of the original mrbgems and any additional dependencies before redistributing artifacts.
123
+
124
+ Rake dependencies determine when to recompile the application, and export leaves files unchanged when their content is identical.
125
+ The registry is validated and generated on every build to reflect environment changes. A missing Wasm file in the original build output also triggers relinking.
126
+ However, the tested PicoRuby version rewrites src/version.c on every build, so that file is recompiled and the runtime is relinked even when nothing else has changed.
127
+
128
+ ## Bindings, environments, and Wrangler
129
+
130
+ The type registry is generated from `kv_namespaces` / `queues.producers` in wrangler.jsonc.
131
+ JSONC comments and trailing commas are supported. Invalid configuration, duplicate binding names, and nonexistent environments cause build errors.
132
+ Variable values and secrets are not embedded in build artifacts.
133
+
134
+ ```ruby
135
+ kv = Cloudflare::KV.from_env(env, "CACHE_KV")
136
+ kv.put("key", "value", ttl: 60)
137
+ value = env["cloudflare.env"].CACHE_KV.get("key")
138
+ Cloudflare::Queue.from_env(env, "EVENTS").send("created")
139
+ token = ENV["API_TOKEN"]
140
+ ```
141
+
142
+ Queue sending currently supports UTF-8 strings only, matching the runtime API. Manage secrets through .dev.vars or `wrangler secret put`, and keep them out of Git.
143
+ With `npm run dev` / `npm run deploy`, Wrangler's custom build runs Rake.
144
+ `build.watch_dir` covers app.rb and build_config.rb. Update the watch list when adding Ruby files.
145
+
146
+ ```sh
147
+ CLOUDFLARE_ENV=staging npm run dev
148
+ CLOUDFLARE_ENV=staging npm run deploy
149
+ ```
150
+
151
+ Selecting a named environment with only `--env staging` does not pass the environment name to the custom build.
152
+ Make sure Wrangler and the exporter use the same `CLOUDFLARE_ENV` value.
153
+ Define resource bindings for each environment; they are not inherited from the top-level configuration.
154
+
155
+ ## Tests and reproduction
156
+
157
+ ```sh
158
+ bundle exec rake test
159
+
160
+ PICORUBY_ROOT=/path/to/picoruby \
161
+ PICORUBY_WORKER_WASM_GEM_DIR=/path/to/picoruby-cloudflare-worker-wasm \
162
+ MRUBY_RACK_GEM_DIR=/path/to/mruby-rack \
163
+ bundle exec rake test:integration
164
+ ```
165
+
166
+ The mrbgem environment variables in this test command are inputs to the integration harness only.
167
+ It writes explicit directory attributes into the generated build_config and clears those variables before invoking the build.
168
+
169
+ Unit tests cover the CLI, overwrite protection, path validation, DSL, compiler selection, incremental export, missing-Wasm recovery, and preservation of bytecode after compilation failures.
170
+ Integration tests cover project generation, dependency installation, builds, Wrangler dry-run, local HTTP and hot reload, ENV/KV TTL/Queue through actual Wasm,
171
+ env isolation between concurrent requests, environment switching, and missing-Wasm recovery.
172
+ Integration tests require a Node.js version with JSPI support. They do not deploy Workers or create Cloudflare resources.
173
+ All artifacts and step-by-step logs are retained in the temporary directory printed by the test for troubleshooting.
174
+
175
+ Tested with PicoRuby `33540f66d9aba633d4d3ebd6707d5c12baebb652`, the Worker/Rack revisions above,
176
+ Ruby 4.0.5, Emscripten 5.0.7 and Homebrew Emscripten 6.0.9, Node.js 26.8.1, and Wrangler 4.125.0.
177
+ The compatibility date is `2026-08-22`, tested with the pinned Wrangler version.
178
+ A dry-run does not start workerd, so verify local HTTP responses when updating Wrangler or the compatibility date.
179
+ Rerun the integration tests when PicoRuby or the mruby submodule's build API changes.
180
+
181
+ ## License
182
+
183
+ This gem is available under the [MIT License](LICENSE).
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "picoruby/cloudflare/template/cli"
5
+ exit Picoruby::Cloudflare::Template::CLI.run(ARGV)
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "template/exporter"
4
+
5
+ unless defined?(MRuby::CrossBuild)
6
+ raise Picoruby::Cloudflare::Template::Error, "Require picoruby/cloudflare/build from a PicoRuby build_config.rb, after its build system is loaded"
7
+ end
8
+
9
+ module Picoruby::Cloudflare::Template
10
+ module CrossBuild
11
+ WORKER_REVISION = "e6235bca616dbd4cec619cc0141facdea59a5541".freeze
12
+ RACK_REVISION = "05ba46eb0ab490a624a5f2dcb33249670933ff6b".freeze
13
+ CORE_GEMS = %w[mruby-array-ext mruby-catch mruby-class-ext mruby-enum-ext
14
+ mruby-hash-ext mruby-kernel-ext mruby-metaprog mruby-method
15
+ mruby-numeric-ext mruby-object-ext mruby-proc-ext mruby-sprintf
16
+ mruby-string-ext mruby-struct mruby-regexp].freeze
17
+
18
+ attr_accessor :picoruby_cloudflare_worker_wasm_mgem_dir, :mruby_rack_mgem_dir
19
+ attr_writer :picoruby_cloudflare_worker_wasm_revision, :mruby_rack_mgem_revision
20
+
21
+ def picoruby_cloudflare_worker_wasm_revision
22
+ @picoruby_cloudflare_worker_wasm_revision.nil? ? WORKER_REVISION : @picoruby_cloudflare_worker_wasm_revision
23
+ end
24
+
25
+ def mruby_rack_mgem_revision
26
+ @mruby_rack_mgem_revision.nil? ? RACK_REVISION : @mruby_rack_mgem_revision
27
+ end
28
+
29
+ def cloudflare_worker!
30
+ yield self if block_given?
31
+ raise Error, "cloudflare_worker! may only be configured once per target" if @cloudflare_configured
32
+ Picoruby::Cloudflare::Template.validate_build_path!(MRUBY_ROOT)
33
+ Picoruby::Cloudflare::Template.validate_build_path!(build_dir)
34
+ worker = cloudflare_gem_source(picoruby_cloudflare_worker_wasm_mgem_dir,
35
+ "udzura/picoruby-cloudflare-worker-wasm", picoruby_cloudflare_worker_wasm_revision)
36
+ rack = cloudflare_gem_source(mruby_rack_mgem_dir, "udzura/mruby-rack", mruby_rack_mgem_revision)
37
+ @cloudflare_configured = true
38
+ toolchain :clang
39
+ cc.command = linker.command = "emcc"
40
+ archiver.command = "emar"
41
+ [cc, linker].each { _1.flags.concat(%w[-sSUPPORT_LONGJMP=wasm -sWASM_LEGACY_EXCEPTIONS=0]) }
42
+ cc.defines.concat(%w[PICORB_PLATFORM_WASM PICORB_PLATFORM_CLOUDFLARE_WORKERS MRB_32BIT MRB_INT64 MRB_NO_BOXING MRB_UTF8_STRING])
43
+ ports :worker_wasm
44
+ picoruby(alloc_estalloc: false)
45
+ core_dir = File.join(MRUBY_ROOT, "mrbgems/picoruby-mruby/lib/mruby/mrbgems")
46
+ CORE_GEMS.each { gem gemdir: File.join(core_dir, _1) }
47
+ gem(rack)
48
+ target = self
49
+ gem(worker) do |spec|
50
+ # This runs during gems.setup, after all build_config DSL calls and mrbc resolution.
51
+ target.send(:setup_cloudflare_export, spec)
52
+ end
53
+ end
54
+
55
+ def worker_export(app:, output_dir:, wrangler_config:, environment: nil, project_root: nil)
56
+ raise Error, "Call cloudflare_worker! before worker_export" unless @cloudflare_configured
57
+ raise Error, "worker_export may only be configured once per target" if @cloudflare_export
58
+ config = cloudflare_build_config
59
+ @cloudflare_export = {
60
+ app: app, output_dir: output_dir, wrangler_config: wrangler_config,
61
+ environment: environment, project_root: project_root || File.dirname(config), config: config,
62
+ }
63
+ end
64
+
65
+ private
66
+
67
+ def cloudflare_gem_source(gem_dir, github, revision)
68
+ if gem_dir
69
+ path = File.expand_path(gem_dir, File.dirname(cloudflare_build_config))
70
+ { gemdir: Picoruby::Cloudflare::Template.validate_build_path!(path) }
71
+ else
72
+ raise Error, "Revision for #{github} must be a nonempty String" unless revision.is_a?(String) && !revision.empty?
73
+ { github: github, checksum_hash: revision }
74
+ end
75
+ end
76
+
77
+ def cloudflare_build_config
78
+ File.expand_path(defined?(::MRUBY_CONFIG) ? ::MRUBY_CONFIG : ENV["MRUBY_CONFIG"] || ENV.fetch("CONFIG"))
79
+ end
80
+
81
+ def setup_cloudflare_export(spec)
82
+ Exporter.new(self, spec.dir, **@cloudflare_export).define_tasks if @cloudflare_export
83
+ end
84
+ end
85
+ end
86
+
87
+ MRuby::CrossBuild.include(Picoruby::Cloudflare::Template::CrossBuild)
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require_relative "generator"
5
+ require_relative "project"
6
+
7
+ module Picoruby::Cloudflare::Template
8
+ class CLI
9
+ HELP = <<~TEXT.freeze
10
+ Usage: picoruby-cloudflare COMMAND [OPTIONS]
11
+
12
+ Commands:
13
+ new PATH Generate a PicoRuby Cloudflare Worker project
14
+ doctor [PROJECT] Check local Worker build prerequisites (default: current directory)
15
+
16
+ Options:
17
+ -h, --help Show this help
18
+
19
+ Run `picoruby-cloudflare new --help` for project generation options.
20
+ TEXT
21
+
22
+ def self.run(argv, out: $stdout, err: $stderr)
23
+ argv = argv.dup
24
+ command = argv.shift
25
+ if command.nil? || command == "-h" || command == "--help"
26
+ out.puts HELP
27
+ return 0
28
+ end
29
+
30
+ options = {}
31
+ parser = OptionParser.new do |opts|
32
+ opts.banner = "Usage: picoruby-cloudflare new PATH [--name NAME] [--gem-path PATH]\n picoruby-cloudflare doctor [PROJECT]"
33
+ opts.on("--name NAME", "Worker name (defaults to directory name)") { options[:name] = _1 }
34
+ opts.on("--gem-path PATH", "Use an unpublished local template gem") { options[:gem_path] = _1 }
35
+ end
36
+ parser.parse!(argv)
37
+ case command
38
+ when "new"
39
+ raise Error, parser.to_s unless argv.length == 1
40
+ path = Generator.new(argv.first, **options).generate
41
+ out.puts "Created #{path}\nNext: cd #{path}\n # On macOS: brew install emscripten (see README for PATH setup)\n bundle install\n npm install\n # Set PICORUBY_ROOT, then:\n bundle exec rake doctor\n npm run dev"
42
+ when "doctor"
43
+ raise Error, parser.to_s unless argv.length <= 1 && options.empty?
44
+ Project.new(root: argv.first || Dir.pwd).doctor(out: out)
45
+ else
46
+ raise Error, parser.to_s
47
+ end
48
+ 0
49
+ rescue Error, OptionParser::ParseError => e
50
+ err.puts e.message
51
+ 1
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "fileutils"
5
+ require "json"
6
+ require "open3"
7
+ require "rake"
8
+ require "tempfile"
9
+ require_relative "../template"
10
+
11
+ module Picoruby::Cloudflare::Template
12
+ class Exporter
13
+ include Rake::DSL
14
+
15
+ def initialize(build, gem_dir, app:, output_dir:, wrangler_config:, project_root:, config:, environment: nil)
16
+ @build = build
17
+ @gem_dir = gem_dir
18
+ @root = File.expand_path(project_root)
19
+ @app = File.expand_path(app, @root)
20
+ @output = File.expand_path(output_dir, @root)
21
+ @wrangler = File.expand_path(wrangler_config, @root)
22
+ @config = config
23
+ @environment = environment
24
+ unless @output.start_with?("#{@root}/") && !@output.delete_prefix("#{@root}/").split("/").include?("node_modules")
25
+ raise Error, "output_dir must be a subdirectory of the project, outside node_modules"
26
+ end
27
+ raise Error, "app must be outside output_dir" if @app.start_with?("#{@output}/")
28
+ end
29
+
30
+ def define_tasks
31
+ Picoruby::Cloudflare::Template.validate_build_path!(@gem_dir)
32
+ check_assets!
33
+ verify_emscripten!
34
+ bytecode = File.join(@output, "app.bin")
35
+ check_output_path!(bytecode)
36
+ file bytecode => [@app, @build.mrbcfile, @config, __FILE__] do
37
+ check_output_path!(bytecode)
38
+ FileUtils.mkdir_p(@output)
39
+ Tempfile.create(["app", ".bin"], @output) do |temp|
40
+ temp.close
41
+ sh @build.mrbcfile, "-o#{temp.path}", @app
42
+ File.rename(temp.path, bytecode)
43
+ end
44
+ end
45
+ runtime_js = File.join(@build.build_dir, "bin/picoruby-worker.js")
46
+ runtime_wasm = File.join(@build.build_dir, "bin/picoruby-worker.wasm")
47
+ # The current mrbgem emits Wasm as a side effect of linking JS. Repair a
48
+ # missing side output without using old artifacts or deleting the build.
49
+ file runtime_wasm => runtime_js do
50
+ Rake::Task[runtime_js].execute unless File.file?(runtime_wasm)
51
+ raise Error, "Linker did not produce #{runtime_wasm}" unless File.file?(runtime_wasm)
52
+ end
53
+ prepare = "cloudflare:prepare:#{@build.name}"
54
+ task prepare => [runtime_js, runtime_wasm, bytecode]
55
+ manifest = File.join(@output, "manifest.json")
56
+ # The task prerequisite deliberately runs export on every build: environment
57
+ # selection is not a file timestamp. All writes are content-aware.
58
+ file manifest => prepare do
59
+ export(runtime_js, runtime_wasm)
60
+ end
61
+ @build.products << manifest
62
+ end
63
+
64
+ def export(runtime_js, runtime_wasm)
65
+ %w[runtime.js host-bridge.js].each do |name|
66
+ write(File.join("runtime", name), File.binread(File.join(@gem_dir, "spike/src", name)))
67
+ end
68
+ write("runtime/picoruby-worker.js", File.binread(runtime_js))
69
+ write("runtime/picoruby-worker.wasm", File.binread(runtime_wasm))
70
+ # Keep parser and runtime from the same mrbgem checkout. Scripts are copied
71
+ # below the project so Node resolves its jsonc-parser dependency there.
72
+ %w[cloudflare-binding-registry.mjs generate-bindings.mjs].each do |name|
73
+ write(File.join("tools", name), File.binread(File.join(@gem_dir, "spike/scripts", name)))
74
+ end
75
+ Tempfile.create(["bindings", ".js"], @output) do |temp|
76
+ temp.close
77
+ args = ["node", File.join(@output, "tools/generate-bindings.mjs"), "--config", @wrangler, "--output", temp.path]
78
+ args.concat(["--env", @environment]) if @environment
79
+ output, status = Open3.capture2e(*args, chdir: @root)
80
+ raise Error, "Binding registry generation failed:\n#{output}" unless status.success?
81
+ write("bindings.js", File.binread(temp.path))
82
+ end
83
+ write("runtime/index.js", File.read(File.join(templates, "runtime/index.js")))
84
+ write("package.json", JSON.pretty_generate({ private: true, type: "module", exports: "./runtime/index.js" }) + "\n")
85
+ artifacts = %w[app.bin bindings.js package.json runtime/index.js runtime/runtime.js runtime/host-bridge.js runtime/picoruby-worker.js runtime/picoruby-worker.wasm]
86
+ write("manifest.json", JSON.pretty_generate({
87
+ format_version: 1, generator_version: VERSION, environment: @environment,
88
+ worker_revision: worker_revision,
89
+ sha256: artifacts.to_h { [_1, Digest::SHA256.file(File.join(@output, _1)).hexdigest] },
90
+ }) + "\n")
91
+ end
92
+
93
+ private
94
+
95
+ def templates
96
+ File.expand_path("../../../../templates", __dir__)
97
+ end
98
+
99
+ def check_assets!
100
+ %w[spike/src/runtime.js spike/src/host-bridge.js spike/scripts/cloudflare-binding-registry.mjs spike/scripts/generate-bindings.mjs].each do |path|
101
+ raise Error, "Worker mrbgem is missing export asset #{path}; use the documented runtime revision" unless File.file?(File.join(@gem_dir, path))
102
+ end
103
+ end
104
+
105
+ def verify_emscripten!
106
+ output, status = Open3.capture2e("emcc", "--version")
107
+ actual = output[/emcc.*? (\d+\.\d+\.\d+)/, 1]
108
+ raise Error, "Expected Emscripten >= 5.0.0, got #{actual || output.lines.first}; see README for brew install emscripten and PATH setup" unless status.success? && actual && actual.split('.').first.to_i >= 5
109
+ rescue Errno::ENOENT
110
+ raise Error, "emcc is not on PATH; on macOS, run brew install emscripten and add its bin directory to PATH (expected >= 5.0.0; see README)"
111
+ end
112
+
113
+ def worker_revision
114
+ output, status = Open3.capture2e("git", "-C", @gem_dir, "rev-parse", "HEAD")
115
+ status.success? ? output.strip : nil
116
+ end
117
+
118
+ def write(relative, content)
119
+ path = File.join(@output, relative)
120
+ check_output_path!(path)
121
+ return if File.file?(path) && File.binread(path) == content.b
122
+ FileUtils.mkdir_p(File.dirname(path))
123
+ Tempfile.create(["export", ".tmp"], File.dirname(path)) do |temp|
124
+ temp.binmode
125
+ temp.write(content)
126
+ temp.close
127
+ File.rename(temp.path, path)
128
+ end
129
+ end
130
+
131
+ def check_output_path!(path)
132
+ current = path
133
+ while current.start_with?("#{@root}/")
134
+ raise Error, "Refusing to export through a symlink: #{current}" if File.symlink?(current)
135
+ current = File.dirname(current)
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+ require "fileutils"
5
+ require_relative "../template"
6
+
7
+ module Picoruby::Cloudflare::Template
8
+ class Generator
9
+ TEMPLATES = File.expand_path("../../../../templates", __dir__)
10
+
11
+ def initialize(destination, name: nil, gem_path: nil)
12
+ @destination = File.expand_path(destination)
13
+ @name = name || File.basename(@destination)
14
+ @gem_path = File.expand_path(gem_path) if gem_path
15
+ unless @name.match?(/\A[a-z0-9][a-z0-9-]{0,62}\z/)
16
+ raise Error, "Worker name must be 1–63 lowercase letters, digits or hyphens, starting with a letter or digit"
17
+ end
18
+ if @gem_path && !File.file?(File.join(@gem_path, "picoruby-cloudflare-template.gemspec"))
19
+ raise Error, "--gem-path must point to picoruby-cloudflare-template"
20
+ end
21
+ end
22
+
23
+ def generate
24
+ if File.exist?(@destination) || File.symlink?(@destination)
25
+ raise Error, "Destination already exists: #{@destination}; choose a new directory"
26
+ end
27
+ # Render everything before creating the project. Never overwrite an existing project.
28
+ files = Dir[File.join(TEMPLATES, "project", "**", "*.erb")].sort.to_h do |source|
29
+ relative = source.delete_prefix("#{TEMPLATES}/project/").delete_suffix(".erb")
30
+ relative = ".gitignore" if relative == "gitignore"
31
+ [relative, ERB.new(File.read(source), trim_mode: "-").result(binding)]
32
+ end
33
+ raise Error, "Project templates are missing from the installed gem" if files.empty?
34
+ FileUtils.mkdir_p(File.dirname(@destination))
35
+ Dir.mkdir(@destination)
36
+ files.each do |relative, content|
37
+ path = File.join(@destination, relative)
38
+ FileUtils.mkdir_p(File.dirname(path))
39
+ File.write(path, content)
40
+ end
41
+ @destination
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "rbconfig"
5
+ require "rake"
6
+ require_relative "../template"
7
+
8
+ module Picoruby::Cloudflare::Template
9
+ class Project
10
+ include Rake::DSL
11
+ def initialize(root:)
12
+ @root = File.expand_path(root)
13
+ end
14
+
15
+ def picoruby_root
16
+ path = ENV["PICORUBY_ROOT"]
17
+ raise Error, "Set PICORUBY_ROOT to a PicoRuby checkout with initialized submodules" if path.nil? || path.empty?
18
+ Picoruby::Cloudflare::Template.validate_build_path!(@root)
19
+ Picoruby::Cloudflare::Template.validate_build_path!(File.expand_path(path, @root))
20
+ end
21
+
22
+ def doctor(out: $stdout)
23
+ root = picoruby_root
24
+ %w[Rakefile lib/picoruby/build.rb mrbgems/picoruby-mruby/lib/mruby/lib/mruby/build.rb mrbgems/mruby-compiler/lib/prism/include/prism.h].each do |file|
25
+ raise Error, "Missing #{file} in #{root}; initialize PicoRuby submodules" unless File.file?(File.join(root, file))
26
+ end
27
+ %w[emcc emar node].each do |command|
28
+ hint = command == "node" ? "install Node.js supported by Wrangler" : "on macOS, run brew install emscripten and add its bin directory to PATH (see README)"
29
+ output, status = Open3.capture2e(command, "--version")
30
+ raise Error, "#{command} is unavailable: #{output}; #{hint}" unless status.success?
31
+ out.puts output.lines.first
32
+ rescue Errno::ENOENT
33
+ raise Error, "#{command} is not on PATH; #{hint}"
34
+ end
35
+ _, status = Open3.capture2e("node", "-e", "require('jsonc-parser')", chdir: @root)
36
+ raise Error, "jsonc-parser is missing; run npm install in #{@root}" unless status.success?
37
+ out.puts "PicoRuby: #{root}\nReady (compiler version and runtime assets are checked during build)."
38
+ true
39
+ end
40
+
41
+ def build
42
+ config = File.join(@root, "build_config.rb")
43
+ raise Error, "Missing #{config}" unless File.file?(config)
44
+ root = picoruby_root
45
+ raise Error, "Missing PicoRuby Rakefile: #{root}" unless File.file?(File.join(root, "Rakefile"))
46
+ env = {
47
+ "MRUBY_CONFIG" => config, "CONFIG" => config,
48
+ "MRUBY_BUILD_DIR" => File.join(@root, ".picoruby-build"),
49
+ }
50
+ library = File.expand_path("../../..", __dir__)
51
+ command = [RbConfig.ruby, "-I", library, Gem.bin_path("rake", "rake"), "-f", File.join(root, "Rakefile"), "all"]
52
+ raise Error, "PicoRuby build failed" unless system(env, *command, chdir: root)
53
+ end
54
+
55
+ def define_tasks
56
+ desc "Build PicoRuby and export the Worker ES module"
57
+ task(:build) { build }
58
+ desc "Check local Worker build prerequisites"
59
+ task(:doctor) { doctor }
60
+ task default: :build
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Picoruby
4
+ module Cloudflare
5
+ module Template
6
+ VERSION = "0.1.0.rc1"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "template/version"
4
+
5
+ module Picoruby
6
+ module Cloudflare
7
+ module Template
8
+ class Error < StandardError; end
9
+
10
+ # The upstream mrbgem linker still interpolates build paths into a shell
11
+ # command. Reject whitespace and shell metacharacters before invoking it.
12
+ def self.validate_build_path!(path)
13
+ unless path.match?(%r{\A[A-Za-z0-9_./+\-]+\z})
14
+ raise Error, "Build paths must use ASCII letters, digits, /, _, ., + or - (upstream shell limitation): #{path}"
15
+ end
16
+ path
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,40 @@
1
+ module Picoruby
2
+ module Cloudflare
3
+ module Template
4
+ VERSION: String
5
+ class Error < StandardError
6
+ end
7
+
8
+ def self.validate_build_path!: (String path) -> String
9
+
10
+ module CrossBuild
11
+ attr_accessor picoruby_cloudflare_worker_wasm_mgem_dir: String?
12
+ attr_accessor mruby_rack_mgem_dir: String?
13
+ attr_writer picoruby_cloudflare_worker_wasm_revision: String?
14
+ attr_writer mruby_rack_mgem_revision: String?
15
+ def picoruby_cloudflare_worker_wasm_revision: () -> String
16
+ def mruby_rack_mgem_revision: () -> String
17
+ def cloudflare_worker!: () ?{ (self) -> untyped } -> untyped
18
+ def worker_export: (app: String, output_dir: String, wrangler_config: String,
19
+ ?environment: String?, ?project_root: String?) -> untyped
20
+ end
21
+
22
+ class Generator
23
+ def initialize: (String destination, ?name: String?, ?gem_path: String?) -> void
24
+ def generate: () -> String
25
+ end
26
+
27
+ class CLI
28
+ def self.run: (Array[String] argv, ?out: untyped, ?err: untyped) -> Integer
29
+ end
30
+
31
+ class Project
32
+ def initialize: (root: String) -> void
33
+ def picoruby_root: () -> String
34
+ def doctor: (?out: untyped) -> bool
35
+ def build: () -> void
36
+ def define_tasks: () -> untyped
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "picoruby-cloudflare-template", "~> <%= VERSION %>"<%= @gem_path ? ", path: #{@gem_path.dump}" : "" %>
@@ -0,0 +1,59 @@
1
+ # <%= @name %>
2
+
3
+ PicoRuby Rack application on Cloudflare Workers.
4
+
5
+ 1. Install Ruby >= 3.2 and Node.js (supported by Wrangler). On macOS, install Emscripten via Homebrew as shown below.
6
+ 2. Set `PICORUBY_ROOT` to a PicoRuby checkout with its submodules initialized.
7
+ 3. Run `bundle install` and `npm install`.
8
+ 4. Run `bundle exec rake doctor`, then `bundle exec rake`.
9
+ 5. Run `npm run dev`. Wrangler's custom build also runs Rake on startup and Ruby changes.
10
+
11
+ ```sh
12
+ brew install emscripten
13
+ export PATH="$(brew --prefix emscripten)/bin:$PATH"
14
+ emcc --version
15
+ ```
16
+
17
+ Emscripten 5.0.0 or later is accepted; 5.0.7 and Homebrew 6.0.9 are tested.
18
+ Newer versions are accepted but not necessarily tested. If switching from emsdk,
19
+ use a shell without `emsdk_env.sh` and unset `EMSDK`, `EM_CONFIG`, and `EM_CACHE`.
20
+
21
+ The pinned Wrangler version is paired with compatibility date `2026-08-22`.
22
+ Update and test them together; a dry-run alone does not start workerd.
23
+
24
+ During development of the unpublished runtime, set these attributes in the
25
+ `cloudflare_worker!` block in `build_config.rb`:
26
+
27
+ ```ruby
28
+ conf.cloudflare_worker! do |cf|
29
+ cf.picoruby_cloudflare_worker_wasm_mgem_dir = "/path/to/picoruby-cloudflare-worker-wasm"
30
+ # Optional: use a local Rack checkout too.
31
+ cf.mruby_rack_mgem_dir = "/path/to/mruby-rack"
32
+ end
33
+ ```
34
+
35
+ The block receives the CrossBuild object itself and runs before build setup.
36
+ Calling without a block is also supported. Both directory attributes default to `nil`, which selects GitHub sources at the
37
+ revisions bundled in the template gem. Override those revisions in the same block using
38
+ `cf.picoruby_cloudflare_worker_wasm_revision` and `cf.mruby_rack_mgem_revision`.
39
+ Directory attributes take precedence over revision attributes; relative paths
40
+ are resolved against `build_config.rb`. These sources are not selected through
41
+ environment variables. See the template gem README for tested revisions.
42
+
43
+ `build_config.rb` selects mrbgems and exports an ES module to `generated/worker/`.
44
+ Do not edit generated files. The generated Wasm, JS and app bytecode are one unit;
45
+ use Wrangler to bundle them, not a plain Node.js import of `src/index.js`.
46
+
47
+ Configure bindings in `wrangler.jsonc`; the build regenerates `bindings.js`.
48
+ Ruby can use `env["cloudflare.env"].CACHE_KV` or
49
+ `Cloudflare::KV.from_env(env, "CACHE_KV")`, and similarly `Cloudflare::Queue.from_env`.
50
+ String/JSON vars and secrets are accessible through Ruby `ENV`.
51
+
52
+ Keep secrets in `.dev.vars` locally and use `npx wrangler secret put NAME` remotely.
53
+ Never put them in `app.rb`, `build_config.rb` or committed config files.
54
+
55
+ For named environments, set `CLOUDFLARE_ENV=staging npm run dev` (or `npm run deploy`)
56
+ so Wrangler and the binding registry select the same environment. Do not select an
57
+ environment using only `--env`; custom build processes need `CLOUDFLARE_ENV` too.
58
+
59
+ Commit `Gemfile.lock` and `package-lock.json`. Deploy explicitly with `npm run deploy`.
@@ -0,0 +1,3 @@
1
+ require "picoruby/cloudflare/template/project"
2
+
3
+ Picoruby::Cloudflare::Template::Project.new(root: __dir__).define_tasks
@@ -0,0 +1,10 @@
1
+ class App
2
+ def self.call(env)
3
+ # env["cloudflare.env"].CACHE_KV and
4
+ # Cloudflare::KV.from_env(env, "CACHE_KV") refer to the same binding.
5
+ message = ENV["GREETING"] || "Hello from PicoRuby on Cloudflare!"
6
+ [200, { "content-type" => "text/plain; charset=utf-8" }, [message + "\n"]]
7
+ end
8
+ end
9
+
10
+ Rackup::Handler::CloudflareWorker.run(App)
@@ -0,0 +1,22 @@
1
+ require "picoruby/cloudflare/build"
2
+
3
+ MRuby::CrossBuild.new("worker") do |conf|
4
+ conf.cloudflare_worker! do |cf|
5
+ # Optional overrides, applied before build setup:
6
+ # cf.picoruby_cloudflare_worker_wasm_mgem_dir = "vendor/picoruby-cloudflare-worker-wasm"
7
+ # cf.mruby_rack_mgem_dir = "vendor/mruby-rack"
8
+ # cf.picoruby_cloudflare_worker_wasm_revision = "<commit SHA>"
9
+ # cf.mruby_rack_mgem_revision = "<commit SHA>"
10
+ end
11
+
12
+ # Add application mrbgems here, for example:
13
+ # conf.gem gemdir: File.join(__dir__, "vendor/my-gem")
14
+
15
+ conf.worker_export(
16
+ app: "app.rb",
17
+ output_dir: "generated/worker",
18
+ wrangler_config: "wrangler.jsonc",
19
+ environment: ENV["CLOUDFLARE_ENV"],
20
+ project_root: __dir__,
21
+ )
22
+ end
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.picoruby-build/
3
+ /generated/
4
+ /node_modules/
5
+ /.wrangler/
6
+ /.dev.vars
7
+ /.dev.vars.*
8
+ /.env
9
+ /.env.*
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "<%= @name %>",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "build": "bundle exec rake build",
7
+ "generate:bindings": "bundle exec rake build",
8
+ "dev": "wrangler dev",
9
+ "deploy": "wrangler deploy"
10
+ },
11
+ "devDependencies": {
12
+ "jsonc-parser": "3.2.0",
13
+ "wrangler": "4.125.0"
14
+ }
15
+ }
@@ -0,0 +1,5 @@
1
+ import { createWorker } from "../generated/worker/runtime/index.js";
2
+ import app from "../generated/worker/app.bin";
3
+ import { cloudflareBindingTypes } from "../generated/worker/bindings.js";
4
+
5
+ export default createWorker({ app, bindingTypes: cloudflareBindingTypes });
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "node_modules/wrangler/config-schema.json",
3
+ "name": "<%= @name %>",
4
+ "main": "src/index.js",
5
+ "compatibility_date": "2026-08-22",
6
+ "build": {
7
+ "command": "bundle exec rake build",
8
+ "watch_dir": ["app.rb", "build_config.rb"]
9
+ },
10
+ "rules": [
11
+ { "type": "Data", "globs": ["**/*.bin"], "fallthrough": true }
12
+ ],
13
+ "vars": {
14
+ "GREETING": "Hello from PicoRuby on Cloudflare!"
15
+ }
16
+ // Add kv_namespaces / queues.producers here, then rebuild.
17
+ // Keep secrets out of this file; use .dev.vars and wrangler secret put.
18
+ }
@@ -0,0 +1,22 @@
1
+ import createPicoRuby from "./picoruby-worker.js";
2
+ import wasm from "./picoruby-worker.wasm";
3
+ import { createCloudflareBindings, handleRequest, RequestBodyTooLargeError } from "./runtime.js";
4
+
5
+ export function createWorker({ app, bindingTypes }) {
6
+ return {
7
+ async fetch(request, env) {
8
+ try {
9
+ return await handleRequest(createPicoRuby, wasm, app, request, createCloudflareBindings(env, bindingTypes));
10
+ } catch (error) {
11
+ // Do not log host error messages: upstream services may include secrets.
12
+ if (error instanceof RequestBodyTooLargeError) {
13
+ return new Response("Request body too large", { status: 413 });
14
+ }
15
+ console.error("PicoRuby Worker request failed");
16
+ return new Response("PicoRuby Worker runtime error", { status: 500 });
17
+ }
18
+ },
19
+ };
20
+ }
21
+
22
+ export { createCloudflareBindings, createRuntime, dispatch, closeRuntime } from "./runtime.js";
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: picoruby-cloudflare-template
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.rc1
5
+ platform: ruby
6
+ authors:
7
+ - Uchio Kondo
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rake
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '13.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '13.0'
26
+ description: Generate Rack-based Cloudflare Worker projects and configure PicoRuby
27
+ cross builds. Export matching WebAssembly, ES modules, application bytecode, and
28
+ binding registries for Wrangler.
29
+ email:
30
+ - udzura@udzura.jp
31
+ executables:
32
+ - picoruby-cloudflare
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - LICENSE
37
+ - README.ja.md
38
+ - README.md
39
+ - exe/picoruby-cloudflare
40
+ - lib/picoruby/cloudflare/build.rb
41
+ - lib/picoruby/cloudflare/template.rb
42
+ - lib/picoruby/cloudflare/template/cli.rb
43
+ - lib/picoruby/cloudflare/template/exporter.rb
44
+ - lib/picoruby/cloudflare/template/generator.rb
45
+ - lib/picoruby/cloudflare/template/project.rb
46
+ - lib/picoruby/cloudflare/template/version.rb
47
+ - sig/picoruby/cloudflare/template.rbs
48
+ - templates/project/Gemfile.erb
49
+ - templates/project/README.md.erb
50
+ - templates/project/Rakefile.erb
51
+ - templates/project/app.rb.erb
52
+ - templates/project/build_config.rb.erb
53
+ - templates/project/gitignore.erb
54
+ - templates/project/package.json.erb
55
+ - templates/project/src/index.js.erb
56
+ - templates/project/wrangler.jsonc.erb
57
+ - templates/runtime/index.js
58
+ homepage: https://github.com/udzura/picoruby-cloudflare-template
59
+ licenses:
60
+ - MIT
61
+ metadata:
62
+ allowed_push_host: https://rubygems.org
63
+ source_code_uri: https://github.com/udzura/picoruby-cloudflare-template/tree/main
64
+ documentation_uri: https://github.com/udzura/picoruby-cloudflare-template#readme
65
+ bug_tracker_uri: https://github.com/udzura/picoruby-cloudflare-template/issues
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: 3.2.0
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubygems_version: 4.0.10
81
+ specification_version: 4
82
+ summary: Generate and build PicoRuby applications for Cloudflare Workers
83
+ test_files: []