picoruby-cloudflare-template
English | 日本語
A CRuby gem for generating PicoRuby Cloudflare Worker projects, configuring CrossBuild, and exporting local ES modules. It is not needed at Wasm runtime, and publishing an npm package is not required.
Quick start (before publication)
On macOS, install Emscripten via Homebrew first:
brew install emscripten
export PATH="$(brew --prefix emscripten)/bin:$PATH"
emcc --versionEmscripten 5.0.0 or later is accepted. Versions 5.0.7 and Homebrew 6.0.9 are tested;
accepting newer versions does not imply they have all been tested.
If switching from emsdk, use a shell without emsdk_env.sh and unset EMSDK, EM_CONFIG, and EM_CACHE to avoid mixing toolchains.
Then run the following from this repository:
bundle install
bundle exec ruby exe/picoruby-cloudflare new ../my-worker --bindings --gem-path "$PWD"
cd ../my-worker
bundle install
npm install
export PICORUBY_ROOT=/path/to/picoruby
# Set local mrbgem paths in build_config.rb as shown below before building
bundle exec rake doctor
bundle exec rake
npm run devUse a PicoRuby checkout with its submodules initialized. This gem does not initialize submodules or install Emscripten.
doctor checks key PicoRuby files, emcc, emar, Node.js, and jsonc-parser.
The build also checks that the Emscripten version is supported. Use a Node.js version supported by Wrangler.
Build paths containing spaces or shell metacharacters are rejected because of upstream shell command expansion limitations.
To try a packaged gem, run gem build picoruby-cloudflare-template.gemspec, followed by
gem install ./picoruby-cloudflare-template-0.1.0.rc2.gem and
picoruby-cloudflare new my-worker. Use bundle install --local in the generated project to resolve the unpublished version.
After this release candidate is published, install it with gem install picoruby-cloudflare-template --pre --version 0.1.0.rc2.
VERSION and the RubyGems metadata version are both 0.1.0.rc2.
Generated files and build configuration
new PATH [--bindings] [--name NAME] [--gem-path PATH] generates a Gemfile, Rakefile, build_config.rb, a minimal Rack app in app.rb,
src/index.js, package.json, wrangler.jsonc, .gitignore, and README.md.
An existing destination is never overwritten, even if it is an empty directory. Commit Gemfile.lock and package-lock.json in your application repository.
Pass --bindings to include KV, Queue, Durable Object and Access identity examples in app.rb and wrangler.jsonc.
The /access example requires CF_ACCESS_TEAM and a CF_Authorization cookie.
The generated .picoruby-cloudflare-template.json records hashes of those two managed examples.
Run picoruby-cloudflare bindings PROJECT with a future template version to refresh them as supported bindings expand.
Regeneration checks every managed file before writing and changes nothing if either was edited; merge those changes manually instead.
require "picoruby/cloudflare/build"
MRuby::CrossBuild.new("worker") do |conf|
conf.cloudflare_worker! do |cf|
# Optional: local checkouts take precedence over revisions.
# cf.picoruby_cloudflare_worker_wasm_mgem_dir = "/path/to/picoruby-cloudflare-worker-wasm"
# cf.mruby_rack_mgem_dir = "/path/to/mruby-rack"
# cf.picoruby_cloudflare_worker_wasm_revision = "<commit SHA>"
# cf.mruby_rack_mgem_revision = "<commit SHA>"
end
# conf.gem gemdir: File.join(__dir__, "vendor/my-gem")
conf.worker_export(
app: "app.rb",
output_dir: "generated/worker",
wrangler_config: "wrangler.jsonc",
environment: ENV["CLOUDFLARE_ENV"],
project_root: __dir__,
)
endPlace this require in build_config.rb, after PicoRuby has loaded its build system.
cloudflare_worker! configures Emscripten, Wasm longjmp, the Worker HAL, PicoRuby, Rack, and the required core mrbgems.
Add frameworks such as Sinatra in your application configuration. ABI-specific final link settings, such as JSPI exports, belong to the runtime library.
Set the attributes in the cloudflare_worker! block. It receives the CrossBuild object itself and runs before validation and build setup.
Calling without a block is also supported; in that case, set any overrides beforehand. The ! marks its changes to the build configuration.
Both directory attributes default to nil; in that case the gem is declared with github: and checksum_hash: using its revision attribute.
A directory takes precedence over its revision, and relative directory paths are resolved against the build_config directory.
Revision attributes default to the values bundled in this gem; assigning nil restores those defaults.
Dependency source selection no longer reads PICORUBY_WORKER_WASM_GEM_DIR or MRUBY_RACK_GEM_DIR, or accepts worker: / rack: arguments.
The default Worker source is pinned to tag 0.10.0 (27a25497d5c4f11fc08cea767d9ead015f772e28).
Rack is pinned to the latest master commit at the time of this update, afd805f1991b46639ce1fbb778820fe586155bc2.
You can override the Worker revision with another tag or commit SHA.
Before publishing this gem, verify that a fresh checkout can fetch both pinned sources.
Relative paths passed to worker_export are resolved against project_root, which defaults to the build_config directory.
The generated Rakefile runs PicoRuby's Rake in a separate process and keeps build output in the application's .picoruby-build/ directory.
The application is compiled with the mrbcfile resolved by CrossBuild, without relying on an existing build/host/bin/mrbc.
ES module output and responsibilities
generated/worker/
app.bin
bindings.js
package.json # private: true, type: module
manifest.json # Generator version, Worker revision, artifact SHA256 hashes
runtime/
index.js # createWorker({ app, bindingTypes, rackEnv?, afterRequest? })
runtime.js
host-bridge.js
durable-object.js
picoruby-worker.js
picoruby-worker.wasm
tools/ # Binding registry generation scripts
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.
Shared JS and registry generation scripts are copied from the same mrbgem checkout used to build Wasm.
Their source locations are templates/runtime/ and templates/tools/. The Worker repository's spike copies them into spike/src/ and spike/scripts/ during its build.
If that layout changes, update the exporter and configured Worker ref together.
createWorker creates and closes a VM for each request, without sharing env between requests.
Its optional rackEnv(request, env, ctx) callback supplies JSON-compatible
request values to Ruby. afterRequest(request, env, ctx, rackEnv, response)
receives a JSON-compatible snapshot of the Ruby Rack env and may replace the
response. The generated src/index.js shows where to enable both callbacks.
These callbacks require Worker ABI 8; the current default Worker pin predates
them, so configure a compatible local checkout or published Worker revision.
The generated runtime also exports PicoRubyDurableObject for Wrangler.
The low-level createRuntime / dispatch / closeRuntime functions are also re-exported.
If you explicitly reuse a VM, the runtime library serializes dispatches to that VM.
The output is intended to be bundled with Wrangler; it does not make .wasm / .bin imports directly usable in Node.js.
Check the license requirements of the original mrbgems and any additional dependencies before redistributing artifacts.
Rake dependencies determine when to recompile the application, and export leaves files unchanged when their content is identical. 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. 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.
Bindings, environments, and Wrangler
The type registry is generated from kv_namespaces, queues.producers, and
durable_objects.bindings in wrangler.jsonc.
JSONC comments and trailing commas are supported. Invalid configuration, duplicate binding names, and nonexistent environments cause build errors.
Variable values and secrets are not embedded in build artifacts.
kv = Cloudflare::KV.from_env(env, "CACHE_KV")
kv.put("key", "value", ttl: 60)
value = env["cloudflare.env"].CACHE_KV.get("key")
Cloudflare::Queue.from_env(env, "EVENTS").send("created")
objects = Cloudflare::DurableObject.from_env(env, "OBJECTS")
objects.put("counter", { "value" => 1 })
counter = objects.get("counter")
token = ENV["API_TOKEN"]Durable Object put accepts a Cloudflare::DurableObject::POJO, a simple Hash
or Array, or an object responding to to_pojo; get returns a POJO for a
JSON object and an Array containing POJOs for a JSON array.
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.
Access uses Rack middleware Rack::Cloudflare::Access (an alias of Cloudflare::Access):
app = Rack::Builder.new do
use Rack::Cloudflare::Access, team: "my-team"
run lambda { |env|
identity = env["cloudflare.identity"]
[200, { "content-type" => "text/plain" }, [identity.email]]
}
end
Rackup::Handler::CloudflareWorker.run(app)Omit team: to read CF_ACCESS_TEAM from the request's Worker environment.
Use the prefix of <team>.cloudflareaccess.com, not the full URL.
Access is an HTTP identity API, so it needs no KV/Queue-style resource binding.
Before calling the application, the middleware reads the authorization cookie, fetches and decodes
the identity, and stores an AccessIdentity (email, user_uuid, raw_data) in the Rack env.
The helper Cloudflare::Access.get_identity(token, team: "my-team") is also available.
It uses the generic Cloudflare.fetch(url, method:, headers:, body:) wrapper, which returns
status, headers and a buffered UTF-8 body (up to 1 MiB, ten-second timeout, no redirects).
Identity lookup does not locally validate JWT signatures or application audience. Protect the application with Access and validate
tokens as described in the Cloudflare documentation.
The middleware returns 503 for missing/invalid configuration, 401 for a missing/invalid cookie
or an Access 401/403 response, and 502 for upstream/protocol failures, without calling the application.
The generated example applies middleware only to /access; other example routes remain public.
Worker tag 0.10.0 includes Access and Durable Object support.
mruby-rack commit afd805f1991b46639ce1fbb778820fe586155bc2 includes Access middleware support. This template uses both by default.
No local mrbgem checkout overrides are required.
With npm run dev / npm run deploy, Wrangler's custom build runs Rake.
build.watch_dir covers app.rb and build_config.rb. Update the watch list when adding Ruby files.
CLOUDFLARE_ENV=staging npm run dev
CLOUDFLARE_ENV=staging npm run deploySelecting a named environment with only --env staging does not pass the environment name to the custom build.
Make sure Wrangler and the exporter use the same CLOUDFLARE_ENV value.
Define resource bindings for each environment; they are not inherited from the top-level configuration.
Tests and reproduction
bundle exec rake test
PICORUBY_ROOT=/path/to/picoruby \
PICORUBY_WORKER_WASM_GEM_DIR=/path/to/picoruby-cloudflare-worker-wasm \
MRUBY_RACK_GEM_DIR=/path/to/mruby-rack \
bundle exec rake test:integrationThe mrbgem environment variables in this test command are inputs to the integration harness only. It writes explicit directory attributes into the generated build_config and clears those variables before invoking the build.
Unit tests cover the CLI, overwrite protection, path validation, DSL, compiler selection, incremental export, missing-Wasm recovery, and preservation of bytecode after compilation failures. Integration tests cover project generation, dependency installation, builds, Wrangler dry-run, local HTTP and hot reload, ENV/KV TTL/Queue through actual Wasm, env isolation between concurrent requests, environment switching, and missing-Wasm recovery. Integration tests require a Node.js version with JSPI support. They do not deploy Workers or create Cloudflare resources. All artifacts and step-by-step logs are retained in the temporary directory printed by the test for troubleshooting.
Tested with PicoRuby 33540f66d9aba633d4d3ebd6707d5c12baebb652, the Worker/Rack revisions above,
Ruby 4.0.5, Emscripten 5.0.7 and Homebrew Emscripten 6.0.9, Node.js 26.8.1, and Wrangler 4.125.0.
The compatibility date is 2026-08-22, tested with the pinned Wrangler version.
A dry-run does not start workerd, so verify local HTTP responses when updating Wrangler or the compatibility date.
Rerun the integration tests when PicoRuby or the mruby submodule's build API changes.
License
This gem is available under the MIT License.
Optional Worker plugins
At build-time export, resolved mrbgems containing cloudflare-plugin.json are
recognized automatically. The manifest declares format_version: 1, a unique
id, a gem-relative js_template, and npm_dependencies. The template must
export createPlugin() returning the runtime's plugin definition. Selected
modules and their registration are emitted into generated/worker; absent
plugins emit no module or SDK imports. Removing a gem removes its previously
exported plugin files. The runtime must support createCloudflareBindings's
third { plugins } argument.
For the local OpenAI mrbgem, add this to build_config.rb:
conf.gem gemdir: "/Users/udzura/ghq/github.com/udzura/picoruby-ai-sdk-openai"After bundle exec rake build, run npm install --prefix generated/worker to
install its generated dependency declarations. No packages are installed by the
exporter; missing dependencies fail module resolution when bundling/running JS.
Use Rackup::Handler::CloudflareWorker.build with
use AISDK::OpenAI::Middleware to expose env["ai-sdk.openai"] in Ruby.
Pondro Durable Objects
With the separate picoruby-cloudflare-pondro mgem, declare the binding and
Ruby class allowlist in build_config.rb:
conf.gem gemdir: "/path/to/picoruby-cloudflare-pondro"
conf.worker_export(
app: "app.rb", output_dir: "generated/worker",
wrangler_config: "wrangler.jsonc", project_root: __dir__,
environment: ENV["CLOUDFLARE_ENV"],
pondro: { binding: "PONDRO", classes: ["Counter"] },
)Register the same class/binding in Ruby with Pondro.register("Counter", Counter, binding: "PONDRO"). The template detects the mgem's cloudflare-plugin.json,
copies its host module, registers the configured caller plugin, and emits a DO
wrapper in generated/worker/entry.js. The wrapper re-exports the original
Worker entry, including its default handler and existing session DO exports.
No Pondro-specific changes to src/index.js are needed.
Pondro requires Worker ABI 9; the current built-in Worker pin predates that
extension. Configure the compatible local picoruby_cloudflare_worker_wasm_mgem_dir
until an ABI 9 revision is published. The exporter rejects an incompatible
runtime or a missing Pondro mgem before emitting the integration.
The source wrangler.jsonc remains intact. The exporter writes a generated
.picoruby-cloudflare-wrangler.jsonc beside it in the project root, with the
wrapper as main, the Pondro binding and a SQLite creation migration. Relative
paths, .dev.vars, existing bindings and migrations retain their meaning.
An existing matching Pondro binding/migration is reused; conflicts fail with an
error. The optional class_name (default PondroDurableObject) and
migration_tag (default pondro-v1) fields customize the generated declaration.
wrangler_config must be in project_root for this integration.
Use bundle exec rake dev, check, or deploy; these build first and choose
the generated config automatically. New projects' npm scripts call those tasks.
For an older project, use these tasks or update its npm scripts accordingly.
Calling wrangler dev against the original config bypasses the generated DO.
CLOUDFLARE_ENV=staging selects the same environment for export and Wrangler.
Removing both the Pondro mgem and pondro: declaration removes the generated
plugin, wrapper and effective config on the next build. Add
/.picoruby-cloudflare-wrangler.jsonc to older projects' .gitignore.
For an already deployed Worker, keep applied migration history in the source
Wrangler config before changing/removing the generated DO declaration. The
exporter preserves that history; it does not generate deletion/rename migrations.
The full local example and actual Wasm/workerd tests are in the Worker runtime
repository's examples/pondro directory. Pondro adds no npm SDK dependency.