feat(cloudflare/vite): runner-mode dev with HMR via Bun.serve front-proxy#145
Draft
Mkassabov wants to merge 2 commits into
Draft
feat(cloudflare/vite): runner-mode dev with HMR via Bun.serve front-proxy#145Mkassabov wants to merge 2 commits into
Mkassabov wants to merge 2 commits into
Conversation
Bun emits a hard-coded `warn: File <path> is not in the project directory and will not be watched` line for every transpiled file outside its auto-detected project root. In this monorepo that fires hundreds of times on every `alchemy <cmd>` invocation and there is no bun flag to silence it. `foreground-child` upstream hardcodes `stdio = [0, 1, 2]`, so we vendor a minimal copy (`bin/_foreground-child.js`) that adds an optional `stderrFilter` option — pipe stderr, line-buffer, drop matches, forward the rest. Watchdog, signal proxying, IPC bridging, and exit-code forwarding are preserved verbatim from upstream (ISC, attribution at top of file). Drops the `foreground-child` dep in favor of a direct `signal-exit` dep (already a transitive). Also harden the `uncaughtException` handler for Windows: on Ctrl-C the watchdog exits with `STATUS_CONTROL_C_EXIT` (3221225786) and `signal: null` rather than `SIGINT`, so the original guard missed it.
…roxy
Wire `Cloudflare.Vite` into `bun alchemy dev` so SPA projects get a
working dev loop with HMR, falling-through to workerd when an SSR
entry exists.
Architecture: ViteDev runs Vite in middleware mode + a small control
HTTP server. FrontProxy is a Bun.serve listener that drives Vite's
connect middleware against a real `Writable` shim (so sirv's
`createReadStream(...).pipe(res)` for `/@fs/` static files works) and
proxies anything Vite doesn't handle to the workerd HTTP socket. HMR
runs on a dedicated port the browser connects to directly, bypassing
the workerd subdomain proxy. HostWorker.ts + ModuleSnapshot.ts are
the workerd-side runner pieces (worker_loader + Vite module-graph
snapshot) — present and type-clean but the SSR provisioning path is
gated until Runtime/Bindings get exposed from cloudflare-runtime.
LocalWorkerProvider branches on `props.vite` and routes through a new
`serveVite` RPC. The RPC handler spawns ViteDev + FrontProxy,
registers the front-proxy with LocalProxy, and falls back to the raw
front-proxy URL when LocalProxy is unavailable.
Windows fixes that came out of testing:
- `Sidecar/Lock.ts`: Bun on Windows EINVAL-rejects ms-since-epoch
numbers from `fs.utimes`, so pass `new Date()` on win32 only. Lock
acquire also retries (~3s, 150ms spacing) on Conflict to absorb
the bun --watch reload race where a new sidecar starts before the
old one has finalized.
- `SidecarServer.ts`: catch Lock-Conflict and exit silently — the
parent's RPC retry will connect to the canonical sidecar.
- `Sidecar/RpcClient.ts`: drop `detached: true` on win32. DETACHED
process flag allocates a new console window on Windows even with
windowsHide, so the sidecar popped a terminal every dev run.
- `Util/PlatformServices.ts`: monkey-patch `child_process.spawn`
AND `Bun.spawn` to default `windowsHide: true` on win32, so all
Effect-spawned children (Effect's BunChildProcessSpawner reaches
Bun.spawn directly) inherit it.
- `bin/exec.ts`: propagate `--profile` to spawned subprocesses via
`ALCHEMY_PROFILE` env. The ConfigProvider override only applies to
the local Effect runtime, not to children — without this the
sidecar defaulted to the "default" profile and prompted for
Cloudflare auth in dev.
- `Vite/ViteDev.ts`: `cacheDir` set to `<root>/.alchemy/vite`. Vite's
default dep-optimizer cache lives in `node_modules/.vite/deps`,
which sits inside a `bun link`-ed alchemy symlink — bun --watch
follows the symlink and reloads the dev process every time Vite
re-optimizes deps.
Catalog refs for `@distilled.cloud/cloudflare-runtime` and
`@distilled.cloud/cloudflare-vite-plugin` temporarily point at
local link: paths because the runner-mode work depends on a
`globalOutbound: { name: "internet" }` change to LocalProxy in
cloudflare-runtime that hasn't been published yet (without it the
local-proxy worker can't fetch loopback addresses on Windows —
WSARecv #64). Revert the catalog before merging once that ships.
32b5158 to
875c6e4
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Wire
Cloudflare.Viteintobun alchemy devso SPA projects get a working dev loop with Vite HMR. SSR projects (cloudflare-vite-pluginvirtual entry) detected at runtime and gated until the workerd-side runner pieces are wired throughserveVite.Before:
bun alchemy devcrashed withBundleError: Failed to find real path for bundle: undefinedfor anyCloudflare.Viteresource becauseLocalWorkerProviderhad no Vite branch.After:
Browser loads the SPA via the workerd LocalProxy → Bun.serve front-proxy → Vite middleware. Vite's HMR runs on a dedicated port the browser connects to directly. Editing
web/src/main.tshot-updates without a page refresh. The HMR port is persisted acrossbun --watchreloads so the browser auto-reconnects when the dev process restarts.A small auto-accept Vite plugin appends
if (import.meta.hot) import.meta.hot.accept()to user.ts/.js/.tsx/.jsxfiles outsidenode_modulesthat don't already mentionimport.meta.hot— boilerplate-free hot-update for plain SPAs.Architecture
Cloudflare/Local/Vite/ViteDev.ts— spawns Vite in middleware mode with a dedicated HMR port; control HTTP server for SSR module-graph snapshots.Cloudflare/Local/Vite/FrontProxy.ts— Bun.serve listener that drives Vite's connect middleware against a realWritableshim (so sirv'screateReadStream(file).pipe(res)for/@fs/works) and falls through to workerd for SSR.Cloudflare/Local/Vite/AutoAcceptPlugin.ts— auto-injectsimport.meta.hot.accept().Cloudflare/Local/Vite/HostWorker.ts+ModuleSnapshot.ts— workerd-side runner-mode pieces (worker_loader bootstrap + SSR module-graph walk). Type-clean but currently gated; activation needsRuntime/Bindingsexposed fromcloudflare-runtime/RuntimeServices.Cloudflare/Local/Sidecar.ts+SidecarHandlers.ts— newserveViteRPC. Hash-keyed reuse map keeps the same Vite scope alive acrossbun --watchreloads on POSIX.Cloudflare/Workers/LocalWorkerProvider.ts— branches onprops.vite.Windows fixes that came out of testing
Sidecar/Lock.ts—Date.now()→new Date()on win32 (Bunfs.utimesEINVAL on millis); bounded retry on lock conflict to absorb the bun --watch reload race.SidecarServer.ts— exit silently onLockError(Conflict)instead of dumping a stack trace.Sidecar/RpcClient.ts— dropdetached: trueon win32.DETACHED_PROCESSallocates a console window regardless ofwindowsHide. Trade-off: sidecar dies with parent on Windows (Vite restarts on alchemy.run.ts edits), but the persisted HMR port lets the browser auto-reconnect.Util/PlatformServices.ts— patchchild_process.spawn/etc. andBun.spawn/spawnSyncto defaultwindowsHide: trueon win32.bin/exec.ts— propagate--profileto subprocesses viaALCHEMY_PROFILEenv. ConfigProvider override only applied to the local Effect runtime; the sidecar saw "default" profile and prompted for Cloudflare auth.Vite/ViteDev.ts—cacheDirset to<root>/.alchemy/vite(absolute viapath.resolve). Vite's default cache lives undernode_modules/.vite/deps, which sits inside abun link-ed alchemy symlink — bun --watch follows the symlink and reloads the dev process every time Vite re-optimizes deps.Vite 8 compatibility
FrontProxy.tsshim is a realWritableand implements the fullServerResponsesurface Vite 8 + sirv exercise:setHeader/getHeader/removeHeader/appendHeader/getHeaders/getHeaderNames/hasHeader/writeHead/flushHeadersplus stream_write.appendHeaderis required for Vite 8'sVary: Sec-Fetch-Destand stream-piping is required for sirv to serveenv.mjsetc. with the right MIME type.