Skip to content

feat(router): support JSX HTTP/WebSocket routes and page companions - #561

Open
yangshun wants to merge 5 commits into
voidzero-dev:mainfrom
yangshun:codex/jsx-server-routes
Open

yangshun wants to merge 5 commits into
voidzero-dev:mainfrom
yangshun:codex/jsx-server-routes

Conversation

@yangshun

@yangshun yangshun commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This adds support for .tsx and .jsx in HTTP/WebSocket route files and recognizes .server.tsx/.server.jsx page companions.

It also fixes existing .server.tsx companions being discovered as standalone pages such as /foo.server and included in generated client component manifests.

The extension enables JSX syntax; the file's location and naming determine its role.

This PR ended up larger than I imagined, mainly because there are many cases to handle. Half of the changes are tests and docs updates. There's also a fair bit of consolidation going on.

P.S. TBH I didn't start out wanting to support JSX for WebSockets, but the route conventions + extensions logic was quite entangled and it was hard to untangle them. Adding support for JSX WebSockets was the easiest way forward while not making the code messy. One use case I convinced myself to add support for JSX in WebSockets is chat apps that sends rendered HTML over WebSockets (hopefully that's not too much of a stretch to imagine!).

Motivations

It's only a matter of time before someone requests for this, given the precedents for JSX-capable HTTP route files.

  • Vercel OG image generation: the official examples use api/og.tsx and api/og.jsx, as well as Next.js app/api/og/route.tsx, to generate images from JSX inside HTTP handlers.
  • TanStack Start server routes: the documentation defines HTTP server.handlers in routes/hello.tsx, alongside a JSX page component.

Christoph also mentioned that he prefers .tsx.

Scope

File Behavior
routes/api.tsx / routes/api.jsx HTTP handlers at /api
routes/api.dev.tsx / routes/api.prod.jsx HTTP handlers restricted to development / production
routes/chat.ws.tsx / routes/chat.ws.jsx WebSocket handlers at /chat
routes/chat.dev.ws.tsx / routes/chat.prod.ws.jsx WebSocket handlers restricted to development / production
pages/foo.server.tsx / pages/foo.server.jsx Loaders, actions, and page configuration for the matching page, excluded from standalone page discovery and generated client component manifests
pages/foo.tsx / pages/foo.jsx Existing page component support; no new component formats

Both JSX extensions support .dev and .prod restrictions. For WebSocket routes, the environment suffix goes before .ws.

HTTP and WebSocket discovery, filename parsing, page companion lookup, and route binding inference share ROUTE_EXTENSIONS (ts, tsx, js, jsx, mjs, mts) from shared/route-extensions.ts. The page scanner uses the derived ROUTE_EXTENSIONS_SET for membership checks. Both route scanners use the fixed listRouteFiles() helper and the existing WebSocket filename matcher to distinguish their roles. listConventionFiles() retains its existing script extensions for crons and queues.

Discovery and binding inference share the .well-known glob policy, including configured source directories. Environment filtering applies only to route files, so _-prefixed helpers retain their bindings in both environments. Companion exclusion applies only to supported script extensions, preserving page components such as web.server.md.

WebSocket resource-name inference strips JSX extensions so changing chat.ws.ts to chat.ws.tsx or chat.ws.jsx preserves the inferred Durable Object identity. JSX compilation uses the app's configured runtime.

The documentation covers supported HTTP, WebSocket, and companion extensions. General enforcement against manually importing .server.* modules from client code remains a separate feature.

Validation

  • vp run check: all 33 workspaces passed.
  • vp run test: 11,214 passed, 31 skipped.
  • vp test run --maxWorkers=4: 14,594 passed, 31 skipped.
  • Formatting and lint passed.

Regression coverage executes JSX HTTP handlers and WebSocket messages using React and Hono runtimes, checks environment filtering and generated types/bindings (including .well-known routes and underscored helpers), and executes companion loaders/actions. It also verifies that Vue, Svelte, and Markdown pages with a .server suffix remain discoverable. Production-build assertions confirm companion code is present on the server and absent from the generated client graph and assets.

Follow-up

#559 excludes server companions from React and Solid's client bundle detection, so island-only apps do not emit the full pages-client entry.

@yangshun
yangshun marked this pull request as ready for review October 23, 2026 11:06

This branch has not been deployed

No deployments
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.

1 participant