Skip to content

feat: add react-server-dom-vite #33152

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

hi-ogawa
Copy link

@hi-ogawa hi-ogawa commented May 8, 2025

Summary

As a continuation from the discussion on Jacob's PR #31768, this is a new PR to add the react-server-dom-vite package and fixtures/flight-vite. To begin with, thank you to the React team for the patience with multiple PR iterations and for providing reviews. I also want to thank the Vite ecosystem for demonstrating Vite RSC integrations. I've learned fundamental concepts from existing Vite RSC frameworks and they are essential to reach this PR.

Firstly, let me clarify a bundler-level characteristic that affects how RSC integration is approached for Vite. (This has been raised in past PRs as well, but I want to provide fuller context.)

  • Vite/Rollup/Rolldown employ ESM as the unit of modules/chunks, so module loading is inherently asynchronous. Also, there's no module factory wrapper, so chunk loading is side-effectful. This means eagerly importing dependency chunks can break the execution order designed by a bundler. Thus, what Vite does on the browser is to use modulepreload. On the server, there's no alternative other than importing the chunk itself. (These are bundler-level and ESM runtime-level design concerns, and we assume Vite users are aware of them as a trade-off. It might still be interesting to find ways to circumvent and optimize this in userland, potentially suggest them as general bundler-level features, or employ new ESM specifications, but that's not the current focus for Vite RSC adoption.)
  • Vite has dependency chunk preloading in the browser build out-of-the-box: https://vitejs.dev/guide/features.html#async-chunk-loading-optimization. This allows client reference loading to automatically trigger modulepreload injection of dependency chunks at runtime in the browser. Therefore, encoding chunks into the RSC payload seems unnecessary, also with the fact that there's no way to preload ESM chunks on the server. (However, relying on Vite's preload feature might be considered an "anti-RSC" concept, since it essentially bakes the entire client manifest into the main browser bundle instead of sending chunk metadata as needed from the server. My current take (and thus this PR) is that since most Vite apps (both SPA and SSR) assume this feature, we can just use it, but challenging Vite's default mechanism might be intersting. Further discussion is very welcome.)
  • Both development and build should be runtime-agnostic. fixtures/flight-vite assumes that both SSR and RSC run on the same Node.js runtime as the main CLI process during development. However, this doesn't have to be the case in actual frameworks (for example, Jacob and RedwoodJS uses Cloudflare: https://github.com/redwoodjs/sdk). Therefore, a certain degree of generalization is required to make react-server-dom-vite portable for such usages (though this is mostly a concern for the plugin API, not the runtime API, and I haven't yet put any plugins at the react-server-dom-vite level).

Given this background, notable differences of react-server-dom-vite compared to other react-server-dom integrations are:

  • Expose a setPreloadModule(loadModule: (id: string) => Promise<unknown>) API.
  • Add moduleLoading.prepareDestinationManifest to pass browser chunk mapping to SSR directly for "prepare destination". (EDIT: removed since setPreloadModule(...) alone can handle the same logic on user land)
  • The "bundler config" is removed since reference ID remapping can be handled earlier or inside a custom setPreloadModule. Also, chunks do not need to be encoded in the RSC payload.
  • Add a global async module cache only in production builds to address the concern raised in add react-server-dom-vite impl and fixture #26926 (comment). (We could technically do the same in development since Vite requires a module invalidation timestamp ?t= for proper client reference HMR, but clearing the cache requires more logic, so I haven't attempted this at the moment.)

Although this is the API I've currently implemented for the fixtures/flight-vite demo, I'm happy to iterate on the exact shape through discussions with the React team and Vite framework maintainers.

In terms of "RSC spec" compliance, fixtures/flight-vite might look like it's achieving proper semantics. However, for transparency, I'm mentioning again here that supporting "use client" inside node_modules can be difficult in some cases during dev. Here is an example repository to demonstrate such behaviors: https://github.com/hi-ogawa/rsc-tests. Supporting it case by case based on framework-side heuristics or additional user-side configuration is likely possible (for example, moving a client boundary from node_modules to "user code" by re-exporting locally is an obvious workaround), but handling all cases uniformly behind the scenes as per the "RSC spec" is a challenge with the current unbundled dev. Eventually, this issue should disappear when we build RSC on top of a fully bundled development server with Vite/Rolldown. So, at the moment, I'd like each framework (and myself) to continue exploring approaches to support each case as best as possible.

In parallel with addressing any feedback here, I'm going to test react-server-dom-vite in existing Vite RSC frameworks. For example, I have PRs on Waku (wakujs/waku#1393), RedwoodJS (redwoodjs/sdk#360), and my own plugin (hi-ogawa/vite-plugins#768) to preliminarily test with a local build of the package. My local build is pushed to my repository, so it can be installed via "react-server-dom-vite": "https://github.com/hi-ogawa/vite-plugins/raw/refs/heads/04-24-refactor_rsc_use_react-server-dom-vite/react-server-dom-vite-19.1.0.tgz" in package.json. Anyone interested is welcome to test the package. It's normally hard to implement "prepare destination" using react-server-dom-webpack, so it might be interesting to see how switching to react-server-dom-vite helps.

My further plan for react-server-dom-vite is to publish a polished version of fixtures/flight-vite/basic to provide an out-of-the-box, "framework-less" RSC experience on Vite (something similar to Parcel). This is intended to be "framework-less", but it's likely not directly reusable for more opinionated frameworks (for example, a framework might assume a custom environment like RedwoodJS and Cloudflare, or employ its own file system conventions and transforms such as "use cache"). However, certain core aspects of the current fixtures/flight-vite/basic should be reusable (such as vite-utils.ts, findSourceMapURL, and a bunch of virtual modules). I'll try to find a way to extract the common parts and provide a helper package for Vite RSC frameworks (or eventually move them into react-server-dom-vite).

How did you test this change?

Integration tests are included in fixtures/flight-vite. Please take a look fixtures/flight-vite/README.md for the detail.

wip: copy parcel to vite

wip: add __vite_rsc_preload__ and __vite_rsc_require__

wip: more rename

wip: dev only

wip: rename

wip: remove parcelRequire

wip: build prod

wip: move findSourceMapURL to client option

chore: comment

chore: use __vite_rsc_preload__ only

wip: fix findSourceMapURL

wip: add fixtures/flight-vite

wip: build

fix: ssr client reference modulepreload

wip: add setPreloadModule API

refactor: abstract runtime

wip: rename createClientReference to registerClientReference like webpack

wip: tweak createServerReference

refactor: remove unused

wip: remove setServerCallback and align with webpack

chore: comment

chore: rename parcel to vite

chore: prettier

chore: lint

chore: fix flow

fix: global async reference cache

wip: add ClientManifest type

chore: lint

chore: add css example

wip: remove caching

chore: add suspense example

chore: rename mini to basic

chore: fix streaming in vite preview

feat: support prepare destination

chore: cleanup

test: tweak

feat: add findSourceMapURL

test: tweak fixture

chore: cleanup

test: normalize reference key

chore: fix actions imported from client + preserve `createServerReference` position

chore: temporary references

chore: use `@hiogawa/transforms` in fixture

chore: test temporary reference + early hydration

chore: move code

chore: test nonce in fixtures

chore: fix deps

chore: comment

fix: allow unsafe-eval during dev for `createFakeFunction`

chore: add inline action

chore: copy transform utils to fixture

chore: comment and readme
@hi-ogawa
Copy link
Author

hi-ogawa commented May 10, 2025

I initially didn't have css import handling, but now I just pushed one approach I've been thinking. This requires "prepare destination" to also handle ReactDOM.preinit(..., { as: "style" }), so I updated the API setPreloadModule to allow more customization. (EDIT: actually API is the same but I moved "prepare destination" logic entirely on user land.) Again I'm happy to polish API surface better, but first I'd like to confirm with React team whether handling css for client reference in this way gives proper semantics.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants