Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .github/workflows/vite-dev-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Throwaway experiment for the refactor/vite-dev-proxy branch; deleted before
# merge. It answers one question a macOS laptop cannot: on a Linux runner under
# the pinned Bun, does the Vite dev server (running inside Bun) come up, and does
# it forward a WebSocket upgrade to the CMS instead of hanging? The earlier
# E2E workflow died on exactly this before a single test ran.
name: Vite dev smoke (experiment)

on:
push:
branches:
- refactor/vite-dev-proxy

permissions:
contents: read

jobs:
vite-dev:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4

# Reads "packageManager" from package.json.
- uses: oven-sh/setup-bun@v2

- run: bun install --frozen-lockfile

- name: Boot the CMS and the Vite dev server
run: |
bun --version
PORT=3001 DATABASE_URL=sqlite:./.tmp/smoke.db UPLOADS_DIR=./.tmp/smoke-uploads bun server/index.ts > cms.log 2>&1 &
PORT=3001 bun scripts/vite.ts --host 127.0.0.1 --port 5173 --strictPort > vite.log 2>&1 &
for i in $(seq 1 60); do
if curl -fsS -o /dev/null http://127.0.0.1:5173/admin; then
echo "vite ready after ${i}s"; break
fi
sleep 1
done
curl -fsS -o /dev/null http://127.0.0.1:5173/admin || { echo "VITE NEVER CAME UP"; cat vite.log; exit 1; }

# An unauthenticated upgrade is refused by the CMS, but the point is that
# the status line comes back through the proxy at all: before the fix the
# request hung and the proxy died on socket.destroySoon().
- name: WebSocket upgrade through the Vite proxy returns a status line
run: |
set +e
out=$(curl -s -i -m 10 \
-H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
http://127.0.0.1:5173/admin/api/cms/site-socket 2>&1 | head -1)
echo "upgrade response: ${out:-<none within 10s>}"
[ -n "$out" ] || exit 1

- name: Vite process survived the upgrade
run: |
curl -fsS -o /dev/null http://127.0.0.1:5173/admin && echo "vite still serving"

- name: Logs
if: always()
run: |
echo "--- vite.log ---"; cat vite.log || true
echo "--- cms.log ---"; tail -20 cms.log || true
7 changes: 0 additions & 7 deletions docs/e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,6 @@ so publishing never reloads the admin app mid-test. The Vite dev proxy follows
the configured CMS `PORT`, keeping the Playwright admin UI pointed at the
disposable CMS instead of any regular dev server on port 3001.

When Vite itself runs on Bun, its Node-compatible native proxy can stop
draining multi-megabyte request bodies after socket backpressure fills both
sides. `largeBodyDevProxyPlugin` intercepts only known-length CMS API bodies of
at least 1 MiB, buffers them with the same 128 MiB ceiling as `Bun.serve`, and
forwards them with an explicit `Content-Length`. Small requests, non-CMS
traffic, and streaming AI responses remain on Vite's native proxy.

For debugging against a server you started yourself, set
`E2E_REUSE_SERVER=1` and override `E2E_ADMIN_BASE_URL` /
`E2E_PUBLIC_BASE_URL` as needed. Do not use reuse mode for CI or for
Expand Down
21 changes: 9 additions & 12 deletions docs/features/site-shell.md
Original file line number Diff line number Diff line change
Expand Up @@ -590,18 +590,15 @@ they commit: the `ColorInput` primitive throttles picker-drag change events
so a color drag cannot fill the socket backlog past the provider's send
gate.

In production the socket is same-origin. Under `vite dev` it is NOT: the
socket dials the CMS port directly, bypassing the Vite proxy
(`src/admin/pages/site/collab/socketUrl.ts`). `scripts/vite.ts` runs Vite
inside Bun, and Bun's `node:http` ClientRequest never emits `'upgrade'`, so a
proxied 101 takes the non-upgrade fallback: the browser socket hangs in
`readyState 0` forever — never opening, never closing, so the provider's
reconnect path is never even reached — and when that connection later ends,
the proxy's `socket.destroySoon()` call (an API Bun's socket lacks) throws
uncaught and kills the whole dev process. Only the PORT is swapped; the
hostname is preserved, because the session cookie is `SameSite=Lax` and
`localhost` ↔ `127.0.0.1` is a cross-site handshake that would drop it.
`devWorkflow.test.ts` gates the proxy against re-enabling `ws` forwarding.
The socket is same-origin in production and under `vite dev` alike: the
Vite proxy forwards the upgrade to the CMS (`ws: true` on the `/admin/api`
entry in `vite.config.ts`), so the provider simply dials
`window.location.host` + `SITE_SOCKET_PATH`. Before Bun 1.4.1 the
`node:http` client Vite runs on inside Bun never emitted `'upgrade'`, so the
socket had to dial the CMS port directly and the dev process could be killed
by a `socket.destroySoon()` call Bun lacked; 1.4.1 fixed both, and
`devWorkflow.test.ts` now gates the proxy the other way, requiring `ws`
forwarding to stay on.

**Presence** (`src/admin/pages/site/collab/awarenessState.ts`; per-frame
publishers in `collab/framePresencePublishers.ts`, rendering in
Expand Down
7 changes: 3 additions & 4 deletions scripts/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,10 +258,9 @@ const processes: DevProcess[] = [
{
name: 'vite',
command: viteCommand('--host', '127.0.0.1', '--port', String(VITE_PORT), '--strictPort'),
// vite.config.ts reads PORT for both the proxy target and the collab
// socket's dev port. Inheriting it from the developer's shell happened to
// work only because CMS_PORT's default matches the config's — pass it
// explicitly so the two can't drift. `scripts/e2e-dev.ts` already does.
// vite.config.ts reads PORT for the proxy target. Inheriting it from the
// developer's shell happened to work only because CMS_PORT's default
// matches the config's; pass it explicitly so the two can't drift.
env: { PORT: String(CMS_PORT) },
},
]
Expand Down
116 changes: 0 additions & 116 deletions scripts/lib/largeBodyDevProxy.ts

This file was deleted.

78 changes: 0 additions & 78 deletions src/__tests__/collab/socketUrl.test.ts

This file was deleted.

20 changes: 8 additions & 12 deletions src/__tests__/devWorkflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,24 +89,20 @@ describe('development workflow', () => {
expect(viteConfig).toContain("const CMS_DEV_SERVER_ORIGIN = `http://localhost:${process.env.PORT ?? '3001'}`")
expect(viteConfig).toContain('target: CMS_DEV_SERVER_ORIGIN')
expect(viteConfig).toContain('changeOrigin: true')
expect(viteConfig).toContain('largeBodyDevProxyPlugin()')
expect(viteConfig).toContain('shouldBufferLargeDevProxyRequest(req)')
})

it('Vite never forwards WebSocket upgrades, and the collab socket gets the CMS port instead', () => {
it('Vite forwards WebSocket upgrades, so the collab socket is same-origin in dev', () => {
const viteConfig = readSiteFile('vite.config.ts')
const devScript = readSiteFile('scripts/dev.ts')

// Vite runs inside Bun (scripts/vite.ts), and Bun's node:http client never
// emits 'upgrade'. Enabling `ws` forwarding makes the browser socket hang
// and then kills the dev process via `socket.destroySoon()`. The collab
// socket dials the CMS port directly instead — never re-enable this.
expect(viteConfig).not.toMatch(/^\s*ws:\s*true/m)
// Since Bun 1.4.1 the node:http client emits 'upgrade', so the collab
// socket rides the same proxy as every other /admin/api request and the
// dev server is same-origin like production. No port is dialled directly.
expect(viteConfig).toMatch(/^\s*ws:\s*true/m)
expect(viteConfig).not.toContain('VITE_CMS_DEV_PORT')

// The dialled port must come from the same source as the proxy target so
// they cannot drift, and dev.ts must actually hand PORT to the Vite child
// (it previously relied on both defaults happening to be 3001).
expect(viteConfig).toContain("'import.meta.env.VITE_CMS_DEV_PORT': JSON.stringify(process.env.PORT ?? '3001')")
// dev.ts must hand PORT to the Vite child so the proxy target cannot
// drift from the CMS it started (both defaults happen to be 3001).
expect(devScript).toContain('env: { PORT: String(CMS_PORT) }')
})

Expand Down
50 changes: 0 additions & 50 deletions src/__tests__/largeBodyDevProxy.test.ts

This file was deleted.

Loading
Loading