Repository navigation
feat(router): support JSX HTTP/WebSocket routes and page companions - #561
Merged
Brooooooklyn merged 5 commits intoOct 26, 2026
Merged
Conversation
yangshun
marked this pull request as ready for review
October 23, 2026 11:06
Brooooooklyn
added a commit
that referenced
this pull request
Oct 26, 2026
## Summary Depends on [#561](#561), which adds JSX server companion support. **Merge after #561, or together with it.** On `main` without #561, the page scanner still lists `index.server.tsx` as a page of its own. React and Solid decide whether to build the `pages-client` bundle with `hasNonIslandPages()`. That check counted server companions (`index.server.tsx`, `index.server.jsx`) as ordinary pages. So an island-only app still emitted a `pages-client` bundle it never used. ``` pages/ index.island.tsx island page → islands-client only index.server.tsx server companion → not a page (skipped now) status.server.v2.tsx ordinary page → still needs pages-client ✓ ``` The skip matches #561's page scanner exactly: a name that **ends in** `.server.tsx` or `.server.jsx` is a companion. A name that only contains `.server.` (`status.server.v2.tsx`) is still a page. ## Tests - `packages/{react,solid}/test/unit/plugin-pages.test.ts`: - island-only pages plus a `.server.tsx` or `.server.jsx` file → no `pages-client`; - an ordinary page next to them (`about.tsx`, `about.jsx`, `blog/…`, `status.server.v2.tsx`) → `pages-client` kept. - **Mutation check:** without the skip, 4 tests fail. With a plain "contains `.server.`" rule, the 2 `status.server.v2.tsx` cases fail. - **With #561 merged** (no conflicts): - these tests and #561's own tests pass; - a real island-only React build has no `pages-client` chunk; - `vp preview` serves only the islands script, and the island hydrates (a button click updates it). - Affected tests (`vp run test`, 508 files), lint and typecheck pass. ## Not changed here (older behavior) - `hasNonIslandPages()` checks only the file name. So a file in a `_`-prefixed folder (e.g. `_components/Button.tsx`) still counts as a page, while the scanner skips it. Switching the adapters to the scanner's own result would fix that; it's a separate change. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: LongYinan <github@lyn.one> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brooooooklyn
self-requested a review
October 26, 2026 04:56
Void sets no JSX options, so "uses your app's configured runtime" left API-only, Vue and Svelte apps without a working setup: a .tsx route fell back to React's runtime or, under the Vue template's `jsx: preserve`, was not compiled at all. Say that route JSX follows the app's JSX settings, and show Hono's JSX through `oxc.jsx` for apps without a JSX framework (tsconfig alone does not reach .jsx files). Also: link the WebSocket and companion docs to that section, drop the duplicate rows from the companion table, list companion extensions in the structure reference, and say that `chat.ws.dev.ts` is not a WebSocket route (it is served as an HTTP route). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brooooooklyn
force-pushed
the
codex/jsx-server-routes
branch
from
October 26, 2026 05:19
965467d to
3653ca1
Compare
Member
|
@codex review |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Codex Review: Something went wrong. Try again later by commenting “@codex review”. ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Member
|
@codex review |
Brooooooklyn
approved these changes
Oct 26, 2026
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.
Summary
This adds support for
.tsxand.jsxin HTTP/WebSocket route files and recognizes.server.tsx/.server.jsxpage companions.It also fixes existing
.server.tsxcompanions being discovered as standalone pages such as/foo.serverand 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!).
Warning
This PR updates the docs with a new feature, and I think docs are published at merge time, which can result in unreleased features appearing in the docs.
Motivations
It's only a matter of time before someone requests for this, given the precedents for JSX-capable HTTP route files.
api/og.tsxandapi/og.jsx, as well as Next.jsapp/api/og/route.tsx, to generate images from JSX inside HTTP handlers.server.handlersinroutes/hello.tsx, alongside a JSX page component.Christoph also mentioned that he prefers
.tsx.Scope
routes/api.tsx/routes/api.jsx/apiroutes/api.dev.tsx/routes/api.prod.jsxroutes/chat.ws.tsx/routes/chat.ws.jsx/chatroutes/chat.dev.ws.tsx/routes/chat.prod.ws.jsxpages/foo.server.tsx/pages/foo.server.jsxpages/foo.tsx/pages/foo.jsxBoth JSX extensions support
.devand.prodrestrictions. 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) fromshared/route-extensions.ts. The page scanner uses the derivedROUTE_EXTENSIONS_SETfor membership checks. Both route scanners use the fixedlistRouteFiles()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-knownglob 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 asweb.server.md.WebSocket resource-name inference strips JSX extensions so changing
chat.ws.tstochat.ws.tsxorchat.ws.jsxpreserves the inferred Durable Object identity. Void sets no JSX options: route JSX compiles with the app's own JSX settings (framework plugins, tsconfig for TS files, oroxc.jsx). The docs show Hono's JSX viaoxc.jsxfor apps without a JSX framework.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.Regression coverage executes JSX HTTP handlers and WebSocket messages using React and Hono runtimes, checks environment filtering and generated types/bindings (including
.well-knownroutes and underscored helpers), and executes companion loaders/actions. It also verifies that Vue, Svelte, and Markdown pages with a.serversuffix 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 (merged) excludes server companions from React and Solid's client bundle detection, so island-only apps do not emit the full
pages-cliententry. Until this PR lands,mainstill lists a companion such asindex.server.tsxas its own page (/index.server).Review and polish (rebased on
main)Rebased onto
main(the merge commit is dropped; the four commits are unchanged). A deep review found the code correct; one docs commit was added:Checked in real apps with that setup,
.tsxand.jsxroutes, dev andvp preview:/hello(.tsx)/hello2(.jsx)honodep)Also in that commit: the WebSocket and companion docs link to the JSX section; the companion table drops the
routes/row and the repeated pair;reference/structure.mdlists companion extensions;websockets.mdsayschat.ws.dev.tsis not a WebSocket route (it is served as an HTTP route at/chat.ws).Mutation checks: reverting
ROUTE_EXTENSIONSto the old list fails 93 tests; removing the.serverpage skip fails 20. Affected tests (vp run test --base origin/main, 518 files) pass except one unrelated platform browser test that times out at 5s under full load (passes alone); that is fixed separately.Left for the maintainer (product calls)
.tsx/.jsxfiles already inroutes/that were ignored before now become routes; a release note could tell users to prefix non-route files with_.foo.server.ts+foo.server.tsx) silently use the first by extension order, as.ts+.tsxalready did onmain.a.ts+a.tsx(likea.ts+a.jstoday) register two handlers for/awithout an error.