The project is in a healthy, maintained state
Generate Rack-based Cloudflare Worker projects and configure PicoRuby cross builds. Export matching WebAssembly, ES modules, application bytecode, and binding registries for Wrangler.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

~> 13.0
 Project Readme

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 --version

Emscripten 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 dev

Use 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__,
  )
end

Place 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 deploy

Selecting 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:integration

The 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.