Skip to content

feat(cloudflare): support Vite dev bindings#622

Draft
agcty wants to merge 5 commits into
alchemy-run:mainfrom
agcty:codex/cloudflare-vite-dev-bindings
Draft

feat(cloudflare): support Vite dev bindings#622
agcty wants to merge 5 commits into
alchemy-run:mainfrom
agcty:codex/cloudflare-vite-dev-bindings

Conversation

@agcty

@agcty agcty commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Status / stack

Draft follow-up to #615. This PR is intentionally stacked on top of the manifest-consumer work:

  1. feat(vite-plugin): RSC dev support via viteEnvironment child environments cloudflare-tools#47 - RSC dev support via viteEnvironment child environments.
  2. feat(vite-plugin): RSC build manifest for deploy cloudflare-tools#50 - distilled Vite build manifest.
  3. feat(cloudflare): consume distilled Vite manifests #615 - Alchemy consumes the distilled Vite manifest for deploys.
  4. This PR - hardens alchemy dev for Vite Workers with Alchemy-managed bindings.

Because the stack crosses forks, GitHub cannot cleanly target this PR at the fork branch from #615 while still opening it against alchemy-run/alchemy-effect. Until #615 lands, this PR's diff will include that base commit. After #615 merges, the diff should collapse to this follow-up commit.

Refs #621.

Summary

  • Recompute local Worker instance identity from the resolved Worker config, not unresolved props, so alchemy dev restarts/rebinds a Vite Worker when Alchemy-managed binding identity or env values change.
  • Avoid duplicating native Cloudflare bindings as local text/json env values when env contains a bindable resource.
  • Normalize local Worker URLs returned from the dev provider so path joins do not accidentally produce proxy-breaking // URLs.
  • Log local binding mode per Worker start/update, distinguishing local runtime bindings from remote-backed Cloudflare bindings and unsupported local binding types.
  • Document Cloudflare.Vite dev semantics: Vite owns the dev server/HMR loop; Alchemy owns resources and binding configuration.
  • Add a TanStack Start fixture plus a live dev test that exercises Vite HMR, R2 remote binding access, and Alchemy rebinding on a second apply.

Architecture

This keeps the same line as #615:

Vite owns the app Worker. Alchemy owns resources, bindings, diffs, and deploy/dev orchestration.

In dev mode that means:

  • Vite serves the framework app and owns HMR.
  • Alchemy starts a local Worker/proxy around that Vite server.
  • Alchemy resolves env into the runtime binding hooks consumed by the distilled Cloudflare Vite plugin.
  • Local-capable bindings stay local; remote-capable account resources such as R2/KV/D1 use remote-backed bindings where the runtime supports them.
  • When alchemy.run.ts changes the binding set, Alchemy re-applies the resource graph and restarts/rebinds the local Vite Worker behind the same local URL.

Verification

  • bun install --frozen-lockfile
  • bun tsgo -b packages/alchemy/tsconfig.test.json
  • bunx oxfmt --check ...
  • git diff --check
  • bun run --filter alchemy test -- test/Cloudflare/Website/Vite.test.ts -t "Vite: ignores manifest-like files copied into client assets"
  • Live Cloudflare test: bun run --filter alchemy test -- test/Cloudflare/Website/Vite.test.ts -t "Vite dev: TanStack Start keeps Alchemy-managed R2 bindings"
    • renders the TanStack route through local Vite dev
    • edits a route file and observes Vite HMR without another Alchemy apply
    • writes/reads through an Alchemy-managed R2 bucket binding
    • re-applies with a different bucket and marker
    • verifies the same local URL now sees the new binding and not the old bucket data
    • destroys the temporary buckets/Worker
  • Direct TanStack fixture build smoke through Vite.viteBuild(...), producing client assets and an SSR bundle.
  • Manual browser smoke at http://localhost:1337: verified route render, R2 route write/read, HMR route edit, then restored the file and destroyed the manual Cloudflare stage.
  • bun generate:api-reference

Notes

This does not add full remote Worker execution with Vite HMR. It hardens the local Vite HMR path with Alchemy-managed bindings, which matches the current official Cloudflare Vite plugin development model more closely.

Reviewer-round follow-ups

  • Fixed local dev instance hashing to use stable, serializable hash material instead of runtime binding hook closures. This avoids no-op alchemy dev applies restarting unchanged Workers while still detecting binding/env/Vite/crons changes.
  • Included Vite topology/root and cron inputs in the local dev instance hash.
  • Made live-test cleanup strict on success, so a passing run cannot silently leak R2 buckets; cleanup remains best-effort only after an assertion failure so the original failure is preserved.
  • Kept viteEnvironment.name optional so Alchemy does not narrow the distilled plugin passthrough shape.
  • Clarified the local-dev binding docs table as common examples rather than an exhaustive implementation matrix.
  • Noted: the bun.lock distilled workspace version normalization reflects the already-checked-out distilled/packages/* versions (0.25.1), not a separate submodule pointer change in this PR.

Final lifecycle review follow-ups

  • QueueConsumer changes now invalidate/restart the target local Worker through LocalRuntime reload callbacks, with pending reloads covering startup and restart windows.
  • Worker restarts preserve the browser-facing proxy but clear the backend pointer while the new Worker starts, avoiding stale proxy backends.
  • dev: false and external-dev-string Workers now diff from persisted props/bindings, not an in-memory stub hash, so no-op applies remain stable across provider restarts.
  • The TanStack dev-binding fixture intentionally does not set build.rolldownOptions.external = ["cloudflare:workers"]; that default belongs to the Cloudflare Vite integration path, not each app fixture. The fixture also does not advertise standalone vite build, because Alchemy-managed dev/build is the path that injects the integration.

Separate stack note

The local Durable Object build-manifest test still depends on the lower Cloudflare Vite manifest work emitting __distilled-build.json for that fixture. That remains part of the #615 / cloudflare-tools #50 stack rather than this dev-bindings follow-up.

@agcty agcty force-pushed the codex/cloudflare-vite-dev-bindings branch 3 times, most recently from 86d932a to 1ac0e63 Compare June 16, 2026 13:59
@agcty

agcty commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

Final review update after the extra lifecycle pass:

  • QueueConsumer changes now invalidate the target local Worker directly through LocalRuntime reload callbacks, instead of relying on Worker diff ordering.
  • Reloads that happen while a Worker is still starting or between stop/start are recorded as pending reloads and consumed after the Worker registers.
  • Worker restarts preserve the browser-facing proxy but clear the backend pointer while the new Worker starts, so the proxy cannot keep pointing at a closed backend.
  • dev: false and external-dev-string Workers now diff from persisted props/bindings, not an in-memory hash, so no-op applies stay stable across provider restarts.
  • The TanStack dev-binding fixture no longer advertises standalone vite build; Alchemy-managed dev/build is the path that injects the Cloudflare Vite integration. App-level build.rolldownOptions.external = ["cloudflare:workers"] should not be required.

Validation rerun:

  • bunx oxfmt --check ...
  • bun tsgo -b packages/alchemy/tsconfig.test.json
  • git diff --check
  • bun vitest run test/Cloudflare/Website/Vite.test.ts -t "Vite: ignores manifest-like files copied into client assets"
  • live Cloudflare-backed TanStack dev-binding test with R2 binding

One separate stack note: the local Durable Object build-manifest test still depends on the lower Cloudflare Vite manifest work emitting __distilled-build.json for that fixture. I did not fold that into this dev-bindings PR.

@agcty agcty force-pushed the codex/cloudflare-vite-dev-bindings branch 2 times, most recently from d3d317b to 3b9fc2f Compare June 16, 2026 14:30
@john-royal john-royal self-assigned this Jun 16, 2026
@agcty agcty force-pushed the codex/cloudflare-vite-dev-bindings branch 2 times, most recently from 39e2be9 to 2a8a850 Compare June 18, 2026 09:37
@agcty agcty force-pushed the codex/cloudflare-vite-dev-bindings branch from 2a8a850 to d1b431f Compare June 18, 2026 09:47
@agcty agcty force-pushed the codex/cloudflare-vite-dev-bindings branch 4 times, most recently from cddae18 to 2f2f6e2 Compare June 18, 2026 10:54
@agcty agcty force-pushed the codex/cloudflare-vite-dev-bindings branch from 2f2f6e2 to b160bfc Compare June 18, 2026 10:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants