Skip to content

[rsbuild-plugin] Prototype an Rsbuild plugin that runs Workers in workerd - #16362

Draft
james-elicx wants to merge 5 commits into
mainfrom
t3/rspack-rsbuild-workerd-plugin-poc
Draft

james-elicx wants to merge 5 commits into
mainfrom
t3/rspack-rsbuild-workerd-plugin-poc

Conversation

@james-elicx

@james-elicx james-elicx commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

Fixes #[insert GH or internal issue link(s)].

A proof of concept for an Rsbuild/rspack equivalent of @cloudflare/vite-plugin. The Rsbuild plugin and the shared package are private, so nothing new is published.

The goal is a thin Rsbuild wrapper over the Vite plugin's existing behaviour. Bundler-agnostic code is moved out of the Vite plugin into the private @cloudflare/workers-dev-shared package, which both plugins bundle. The Rsbuild plugin keeps only the Rsbuild glue: hooks, environment config, rspack externals/plugins and its middleware order. It's about 1,070 lines (packages/rsbuild-plugin-cloudflare/src).

What it does

@cloudflare/rsbuild-plugin takes the same options as cloudflare() (except tunnel/viteEnvironment). Each Worker gets its own Rsbuild environment (cloudflare for the entry Worker, cloudflare_<directory> for auxiliary Workers), and the user's environments are the client.

  • rsbuild dev: after each compile the Workers are loaded into Miniflare with the same options as the Vite plugin: router/asset/proxy Workers, _headers/_redirects, static routing, WebSockets, the inspector and /__debug, cron/email triggers, remote bindings, the local explorer, and the dev registry. HTML navigations go through the asset Worker, so html_handling/not_found_handling apply as in Vite.
  • rsbuild build: writes Cloudflare Build Output for every Worker, including assets-only/dev-only Workers, public/ and assets imported by the Worker.
  • rsbuild preview: runs the Build Output with the shared preview options.
  • Bundling: Node.js compat (including nodejs_als), additional modules (.wasm, ?init, .bin, .txt, .html, .sql), cloudflare:* externals and Worker sourcemaps as sourcemap modules. Resolution uses the same conditions and main fields as the Vite plugin.

Moved from the Vite plugin into @cloudflare/workers-dev-shared

Moved, not copied. The Vite plugin's src diff is +278/−4,400 lines, and the specs for the moved code moved with it:

  • plugin config resolution (resolvePluginConfig's core), env loading, asset config
  • the Miniflare dev/preview options builders (including the per-Worker dev options, getDevUserWorkerOptions), the Miniflare logger, remote bindings
  • the request handler, WebSocket handling, the dev middlewares, /__debug
  • the router and asset Workers
  • Build Output writing and reading, moveImportedAssets
  • additional module rules and the .wasm?init module, hasNodeJsAls/isNodeAlsModule

Changes to existing logic in the Vite plugin

The aim was no behaviour change. These are the only deviations I found when auditing the diff:

  1. New: the main plugin object has api: { pluginConfig } (the options passed to cloudflare()), so the e2e harness can read them from a resolved Vite config. Additive; patch changeset included.
  2. Preview error order: vite preview now reads the Build Output before validating the tunnel config (allowedMail without name). With both problems present, the Build Output error is reported first.
  3. __VITE_HEADERS__ is now JSON.stringify(headers ?? {}). Vite 8 always sets server.headers to {} by default, so there's no difference there; I haven't checked whether Vite 7 can leave it undefined.
  4. Published types inline the private shared package's types (dts.resolve) and keep miniflare/@cloudflare/config as imports. The d.ts imports only @cloudflare/config, miniflare and vite. The @cloudflare/workers-utils import is gone because the types that needed it moved.
  5. Some internal exports of src/utils.ts were removed (they moved). They weren't public.
  6. Per-Worker dev options now come from the shared getDevUserWorkerOptions, which the Vite plugin extends with its module-runner bindings, exports and dev options. The options are the same. The only difference is the order of two internal assertions, which would only fail on a bug.

Everything else is a verbatim move plus import updates.

Running the playground e2es against both plugins

The existing playground suite runs against Rsbuild without spec changes:

  • playground/__test-utils__/rsbuild.ts loads each playground's vite.config, reads api.pluginConfig, and translates a small set of Vite options: root, base, server.headers, resolve.alias, mode/envDir, HTML inputs, the SSR build's sourcemap/entryFileNames, and plugin-react/basic-ssl. It throws on Vite plugins it can't translate.
  • vitest-setup.ts branches on CLOUDFLARE_TEST_BUNDLER=rsbuild.
  • playground/vitest.config.e2e.rsbuild.ts skips Vite-specific playgrounds and tests, each with the reason.
  • New scripts test:ci:rsbuild:serve / test:ci:rsbuild:build with turbo tasks, plus a separate ubuntu-only CI job. It runs without --only, so turbo builds the plugins and keys the cache on them.

Locally: Rsbuild serve and build pass on everything that isn't skipped (73 spec files, of which serve runs 71 and build 68; the rest skip themselves with runIf), and the full Vite suite still passes (96 files, serve and build).

Running the Vite plugin's e2e/ suite against both plugins

pnpm turbo test:e2e:rsbuild -F @cloudflare/vite-plugin runs packages/vite-plugin-cloudflare/e2e against Rsbuild, also without spec changes. CI runs it as an extra step of the Linux leg of "Vite Plugin E2E".

  • e2e/rsbuild.ts packs the private Rsbuild plugin in a global setup. Its dependencies are the Vite plugin's, which the mock registry already publishes. It also converts each seeded fixture:
    • @cloudflare/vite-plugin/vite/@vitejs/plugin-react become @cloudflare/rsbuild-plugin (the packed tarball), @rsbuild/core and @rsbuild/plugin-react;
    • vite scripts become rsbuild ones;
    • vite.config becomes rsbuild.config, with the cloudflare() options passed unchanged to pluginCloudflare().
  • seed() calls it when CLOUDFLARE_TEST_BUNDLER=rsbuild. That one line is the only change to the existing helpers.
  • e2e/vitest.config.rsbuild.ts skips dynamic (Vite's dependency optimizer), invalid-worker-environment-options (Vite environment validation), nodejs-compat-warnings (asserts the "ssr" environment name) and containers-image-free.

Locally, basic (pnpm/npm/yarn × dev/build+preview), unresolved-main and the remote-bindings tests that don't need credentials all pass. The suite found two Rsbuild plugin gaps, now fixed, and one difference that remains:

  • rsbuild dev didn't print the server URL for Worker-only projects, because Rsbuild only prints it for client environments.
  • An unresolvable Worker entrypoint was reported as a build error while rsbuild dev kept running. It now fails at startup with Failed to resolve Worker entrypoint "…", as in Vite.
  • The Rsbuild CLI loads .env.[NODE_ENV] files into process.env, where they override the plugin's .env.[mode] files. So with NODE_ENV=test (which Vitest sets and the commands inherit), the values differ from Vite's. The suite clears NODE_ENV; the plugin can't see which variables Rsbuild loaded, so this difference stays.

Skipped for Rsbuild (playgrounds)

  • Vite features: child environments, hot channel, virtual modules, custom buildApp, middleware mode, bundledDev, child compilers, the error overlay (errors), prerendering, module-resolution, resolve.external, partyserver, custom output directories.
  • Vite server behaviour: server.fs.deny 403s (sensitive-files), HMR log strings, not serving HTML from publicDir paths inside the root.
  • Rollup's preserveModules (dynamic-import-paths).
  • config-changes: it evaluates crypto.randomUUID() at global scope, which only works because Vite's module runner evaluates lazily.
  • Export-change restarts: these use the module runner.
  • SWC always lowers using declarations: the Worker runs them, but the output-shape assertion is skipped.
  • Containers: not supported yet.

Not supported yet

  • containers, prerendering, the pretty HTML error page (needs a public Miniflare API), unsafeHandleRuntimeRestart
  • in-isolate HMR / the module runner, child environments, the tunnel, CLI shortcuts and the cf CLI

Things to look at

  • Whether @cloudflare/workers-dev-shared is the right home, or whether some of it belongs in Miniflare.
  • The harness's HTML handling: it turns each HTML input's module script into an Rsbuild entry and uses the page as its template.

  • Tests
    • Tests included/updated
    • Automated tests not possible - manual testing has been completed as follows:
    • Additional testing not necessary because:
  • Public documentation
    • Cloudflare docs PR(s):
    • Documentation not necessary because: the Rsbuild plugin is an unpublished, private prototype, and the Vite plugin's only addition is api.pluginConfig for tooling.

Note

This is a contribution from an AI agent: Claude Code, Claude Opus 5.5. It was written under the direction of @james-elicx, who will review it.

@james-elicx james-elicx added the ci:no-changeset-required Skip pull request checks for a valid changeset label Oct 19, 2026
@changeset-bot

changeset-bot Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 7bc7ad7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@cloudflare/vite-plugin Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

…kerd

Adds a private `@cloudflare/rsbuild-plugin` prototype that builds the Worker
from `cloudflare.config.ts` with rspack, runs it in Miniflare during
`rsbuild dev` and `rsbuild preview`, and writes the Build Output
Specification on `rsbuild build`.

Logic the Vite plugin already had is moved into a private
`@cloudflare/workers-dev-shared` package so both plugins use it: config
loading, local bindings, Node.js compat, Miniflare exports, persistence and
Build Output manifests.
…playground e2es

Move the rest of the Vite plugin's bundler-agnostic dev, preview and build
logic into `@cloudflare/workers-dev-shared`, so the Rsbuild plugin gets the
same Miniflare options, middlewares, asset handling, multi-Worker support,
remote bindings, additional modules and Build Output as the Vite plugin.

Run the Vite plugin's playground suite against the Rsbuild plugin without
changing the specs: a harness translates each playground's Vite config, and
Vite-specific playgrounds and tests are skipped with their reasons.
@james-elicx
james-elicx force-pushed the t3/rspack-rsbuild-workerd-plugin-poc branch from 2399268 to cb496a4 Compare October 19, 2026 19:33
@github-project-automation github-project-automation Bot moved this to Untriaged in workers-sdk Oct 19, 2026
@pkg-pr-new

pkg-pr-new Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
@cloudflare/autoconfig

npm i https://pkg.pr.new/@cloudflare/autoconfig@16362

@cloudflare/build-output-utils

npm i https://pkg.pr.new/@cloudflare/build-output-utils@16362

@cloudflare/codemods

npm i https://pkg.pr.new/@cloudflare/codemods@16362

@cloudflare/config

npm i https://pkg.pr.new/@cloudflare/config@16362

@cloudflare/containers-shared

npm i https://pkg.pr.new/@cloudflare/containers-shared@16362

create-cloudflare

npm i https://pkg.pr.new/create-cloudflare@16362

@cloudflare/deploy-helpers

npm i https://pkg.pr.new/@cloudflare/deploy-helpers@16362

@cloudflare/kv-asset-handler

npm i https://pkg.pr.new/@cloudflare/kv-asset-handler@16362

miniflare

npm i https://pkg.pr.new/miniflare@16362

@cloudflare/pages-functions

npm i https://pkg.pr.new/@cloudflare/pages-functions@16362

@cloudflare/pages-shared

npm i https://pkg.pr.new/@cloudflare/pages-shared@16362

@cloudflare/runtime-types

npm i https://pkg.pr.new/@cloudflare/runtime-types@16362

@cloudflare/unenv-preset

npm i https://pkg.pr.new/@cloudflare/unenv-preset@16362

@cloudflare/vite-plugin

npm i https://pkg.pr.new/@cloudflare/vite-plugin@16362

@cloudflare/vitest-plugin

npm i https://pkg.pr.new/@cloudflare/vitest-plugin@16362

@cloudflare/workers-auth

npm i https://pkg.pr.new/@cloudflare/workers-auth@16362

@cloudflare/workers-editor-shared

npm i https://pkg.pr.new/@cloudflare/workers-editor-shared@16362

@cloudflare/workers-utils

npm i https://pkg.pr.new/@cloudflare/workers-utils@16362

wrangler

npm i https://pkg.pr.new/wrangler@16362

commit: 7bc7ad7

…he .dev.vars fixture

The Vite playground jobs build only the Vite plugin, so importing the Rsbuild
harness eagerly failed every spec. The root .gitignore excludes .dev.vars, so
the Rsbuild fixture's secret was missing in CI.
Move the per-Worker part of the Vite plugin's dev Miniflare options into
`getDevUserWorkerOptions` in `@cloudflare/workers-dev-shared`. The Vite plugin
extends it with its module-runner bindings. The Rsbuild plugin uses it as-is,
so it now also enables headful browser rendering when requested, and like the
Vite plugin it turns off Miniflare's request logging.

Also stop rebuilding the whole Worker environment config just to read its
entry, and tidy the entry Worker middleware setup.
seed() converts each fixture to an Rsbuild project when
CLOUDFLARE_TEST_BUNDLER=rsbuild, installing a packed copy of the private
plugin. Fixes two gaps the suite found: Worker-only projects didn't print
the dev server URL, and an unresolvable entrypoint didn't fail at startup.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:no-changeset-required Skip pull request checks for a valid changeset

Projects

Status: Untriaged

Development

Successfully merging this pull request may close these issues.

2 participants