spiceflow React framework. RSC stack based on @vitejs/plugin-rsc#39
Draft
spiceflow React framework. RSC stack based on @vitejs/plugin-rsc#39
Conversation
… the react server would be optimized for browser
… the client modules where getting duplicated with raw import, fix vite ?import thing
Merge 228 commits from main (since divergence at ed746c4) into the rsc branch, bringing the RSC framework up to date with all core improvements. ## Merged from main - **Standard Schema validation**: replaced Ajv + zod-to-json-schema with `@standard-schema/spec` (`~standard.validate`). Dropped `@sinclair/typebox`, `ajv`, `ajv-formats`, `lodash.clonedeep` deps. - **Zod v4**: upgraded from v3 to `^4.3.6`, added `isZod4()` helper. - **New `.route()` method**: flexible multi-method registration with `method` as single value, array, or `'*'` wildcard. - **`.safePath()` method**: type-safe path builder using new `RoutePaths` type param. - **`waitUntil` support**: constructor option + `wrappedWaitUntil` in context for background task lifecycle management. - **`disableSuperJsonUnlessRpc`**: opt-in to skip SuperJSON for non-RPC clients, with `superjsonSerialize` moved to a class method with RPC detection. - **MCP refactor**: extracted `openapi-to-mcp.ts`, switched to `McpServer` class, added `addMcpTools()`, tool name sanitization, basePath-aware transport URLs. - **`handle()` as arrow function** with optional `{ state }` second argument for dependency injection (Cloudflare-compatible binding). - **`body` → `request` schema alias**: `InputSchema.request` with `GetRequestSchema` helper, client types updated accordingly. - **`copy-anything`** replaces `lodash.clonedeep`. - **`handleForNode`/`listenForNode`** extracted to `_node-server` module. - **Website**: migrated from Remix to React Router v7, wrangler v3→v4. - **SSE streaming**: always returns SSE format even for single-value generators, added `content-encoding: none` and `cache-control: no-cache` headers. - **Client**: added `retries` option, `url` field in `ClientResponse`. - **`onError`**: drops `MaybeArray`, receives `path` (with query string). - **CORS**: fix for Cloudflare due to uppercase header change. - **Various**: `joinBasePaths()` helper, status code validation (100-599 range), error `statusCode` fallback, route override support. ## Preserved from rsc - **TrieRouter** (multi-match for page + layout routing) - **React Server Components**: `page()`, `layout()`, `staticPage()`, `renderReact()` - **Vite plugin**, client/SSR entries, error boundaries, progress bar - **RSC types**: `NodeKind`, `InternalRoute.id/kind`, `SpiceflowContext.children` - **RSC example app** and Playwright e2e tests ## Conflict resolution 17 files had conflicts. Strategy: keep rsc's architecture (TrieRouter, React rendering, JSX) and port main's improvements into it. All `.ts` import extensions normalized to `.js` (rsc convention for NodeNext module resolution). ## Test results - **216 tests passing** (was 174 before merge — gained 42 new tests from main) - **7 tests skipped**: TrieRouter behavioral differences vs MedleyRouter (route override last-registered-wins, wildcard priority, catch-all precedence) - **4 todo** tests (unchanged from before)
Replace the custom RSC setup (@jacob-ebey/react-server-dom-vite + unplugin-rsc + hand-rolled reference tracking) with the official @vitejs/plugin-rsc@0.5.21. ## What changed **Dependencies:** - Remove @jacob-ebey/react-server-dom-vite, unplugin-rsc, @hiogawa/transforms, @hiogawa/utils - Add @vitejs/plugin-rsc@0.5.21, react-server-dom-webpack@19.2.4 - Upgrade react/react-dom to 19.2.4 **vite.tsx — completely rewritten:** - Replace ~600 lines of custom RSC plugin code (manual client/server reference tracking, custom environment configs, SSE-based HMR, manual module graph walking) with a thin wrapper around `rsc()` from @vitejs/plugin-rsc - Add `spiceflow:optimize-deps-rewrite` plugin to rewrite optimizeDeps entries with `spiceflow >` prefix so vendor CJS files resolve through the framework package (where plugin-rsc is installed) rather than from the app root - Add `spiceflow:auto-use-client` plugin for client-by-default behavior — auto injects `"use client"` into user source files unless they already have a directive, are the app entry, node_modules, spiceflow internals, or .server.* **Entry points updated to use plugin-rsc APIs:** - entry.client.tsx: import from `@vitejs/plugin-rsc/browser` instead of custom references.browser + react-server-dom-vite/client - entry.ssr.tsx: import `createFromReadableStream` from `@vitejs/plugin-rsc/ssr`, use `import.meta.viteRsc.loadModule` and `import.meta.viteRsc.loadBootstrapScriptContent` - entry.rsc.tsx: simplified — just imports app entry and exports handler **spiceflow.tsx — renderReact updated:** - Import renderToReadableStream, decodeReply, decodeAction, decodeFormState, loadServerAction from `@vitejs/plugin-rsc/rsc` instead of custom wrappers - Remove manual client reference manifest handling **Deleted files (old custom RSC infrastructure):** - references.browser.tsx, references.rsc.tsx, references.ssr.tsx - utils/client-reference.ts, utils/normalize.ts - react/types/index.ts **Example app fixes for stricter RSC semantics:** - action.tsx: make getCounter async (plugin-rsc rejects non-async exports in "use server" modules) - index.tsx: receive counter + serverRandom as props instead of calling server functions during render (client components can't call server functions during SSR initial render) - main.tsx: fetch data in page handlers and pass as props — correct RSC pattern - e2e tests: fix selectors for dual counter elements, replace `using` keyword with try/finally for Playwright compat ## Test results - 12/12 Playwright e2e tests pass (dev mode) - 216/216 spiceflow package tests pass - Production build: 4/5 stages succeed, prerender step has a timing issue with __vite_rsc_assets_manifest.js generation (TODO)
Remove dead symbols left after switching to @vitejs/plugin-rsc and tighten two runtime edges so the new stack behaves predictably. The cleanup drops unused framework-only exports and old debug-only vestiges, while preserving the new official plugin flow. Client action handling now updates payload state before awaiting action results to avoid stale UI timing, and SSR redirect responses now emit a proper content-type header for correct downstream handling. Also add a patch changeset documenting this migration follow-up.
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ❌ Deployment failed View logs |
spiceflow-website-worker | 101c867 | Mar 02 2026, 03:55 PM |
The previous code captured history.location once at module init time, so router.location was always the initial location object and never reflected subsequent navigations. Changing to a getter ensures every access returns the live value from the history instance.
Detailed architecture reference for building RSC-based frameworks on top of @vitejs/plugin-rsc. Covers the three-environment model (RSC/SSR/client), the 5-step scan+build pipeline and why scan phases are required, complete annotated entry point implementations, client-side navigation patterns (listenNavigation + request conventions), server action dispatch (both post-hydration and progressive enhancement), RSC payload design, CSS handling, cross-environment module loading APIs, HMR integration, SSR error recovery with CSR fallback, no-SSR and browser-only modes, production deployment structure, and known architectural constraints. Derived from reading all examples (starter, basic, react-router, browser, no-ssr) and internal docs (architecture.md, bundler-comparison.md, notes/) in the vite-plugin-react repository.
… tests ## Bugs fixed - **isRedirectError/isNotFoundError called with wrong type**: these functions expect a ReactServerErrorContext (parsed from error digest) but were receiving raw Error objects, so redirect and not-found errors were never caught in the API error handling path. Now properly wrapped with getErrorContext(err). - **turnHandlerResultIntoResponse crash when route is undefined**: the React middleware path passed undefined for the route parameter, but the method body accessed route.type without optional chaining. Added route?.type guard. - **Unreachable code in prerender.ts**: continue after throw was dead code. ## Duplicated code removed - checkOptionalParameter existed in both trie-router/url.ts and trie-router/utils.ts — removed the unused copy from url.ts. - Standalone turnHandlerResultIntoResponse + superjsonSerialize functions duplicated the class methods but were missing disableSuperJsonUnlessRpc support. Removed them; React middleware path now uses the class method. - ReactServerErrorContext type was defined in both components.tsx and errors.tsx — components.tsx now imports from errors.tsx. - InternalRoute was re-exported from spiceflow.tsx despite being defined in types.ts — removed re-export, openapi.ts imports from types.ts directly. ## Routing improvements - Replaced sort()[0] route selection with pickBestRoute that properly scores routes: fewer wildcards > fewer named params > more segments > last registered wins for same pattern (override semantics). - :param now always beats * regardless of registration order. ## Visibility and cleanup - router and usedIds made private on Spiceflow class. - Deleted play.js scratch benchmark file. - Added Hono MIT attribution to trie-router/node.ts. ## Tests - Re-enabled all 7 previously skipped tests, updated expectations to match trie router behavior (/* matches parent path, ALL /* catches all methods). - Added new test: :param beats wildcard regardless of registration order. - Strengthened * param test to assert returned value, not just status. - 224 tests passing, 0 skipped.
Add four new sections derived from reading react-router's RSC implementation
(packages/react-router/lib/rsc/):
- **RSC Payload Design**: expanded from minimal {root, formState} to show the
discriminated union pattern (render | action | redirect | manifest). The payload
is where frameworks communicate control flow to the client — redirects, errors,
route patches all travel through one RSC stream.
- **Server vs Client Component Data Passing**: server components receive data as
direct props during RSC rendering, client components need hooks/context because
they execute later during hydration. Shows isClientReference() detection and the
WithComponentProps wrapper pattern from React Router.
- **CSRF Protection**: Origin header validation on server action POST requests
with allowedOrigins whitelist pattern.
- **Action + Rerender Streaming**: action result streams immediately while
revalidation (re-running loaders + rendering) happens in parallel. Uses
renderToReadableStream's ability to serialize promises for incremental delivery.
…gin-rsc best practices Compared spiceflow's RSC implementation against the plugin-rsc framework author guide and identified 7 issues ranging from a security gap (no CSRF protection on server actions) to missing build config options (defineEncryptionKey, validateImports). Plan reviewed and validated by oracle agent which confirmed all items and added 3 additional findings: createFromReadableStream placement, defineEncryptionKey for production stability, and validateImports for build-time safety.
…emporary references Align spiceflow with @vitejs/plugin-rsc best practices: **CSRF protection** (spiceflow.tsx) - Validate Origin header on POST requests before processing server actions - Return 403 'origin mismatch' for cross-origin requests - Support allowedActionOrigins option for trusted origins (string or RegExp) - Check runs before try/catch so 403 is returned directly, not swallowed **SSR error fallback** (entry.ssr.tsx + entry.client.tsx) - Replace inert data-no-hydrate attribute with self.__NO_HYDRATE=1 in bootstrap script - Browser entry checks '__NO_HYDRATE' in globalThis and uses createRoot instead of hydrateRoot, avoiding hydration mismatch errors against the error shell HTML - Added unit tests for the error shell rendering and flag detection **Temporary references** (spiceflow.tsx) - Create temporaryReferences set per request, shared between decodeReply and renderToReadableStream so non-serializable values (DOM nodes, React elements) round-trip correctly through server action encode/decode **RSC HMR self-accept** (entry.rsc.tsx) - Accept own HMR updates so server code changes trigger efficient RSC stream re-render instead of full page reload **validateImports** (vite.tsx) - Enable validateImports option to catch server-only code leaking into client bundles **Test page** (example-react) - Add /ssr-error-fallback route with ThrowsDuringSSR component for e2e testing
…ators
**New e2e tests:**
- SSR error fallback: verify CSR recovery when SSR fails via __NO_HYDRATE
- SSR error fallback: verify __NO_HYDRATE flag is set on globalThis
- SSR error fallback: verify normal pages don't contain __NO_HYDRATE
- CSRF: verify cross-origin POST returns 403
- CSRF: verify same-origin POST passes CSRF check
**HMR test fixes:**
- Fix strict mode violations in client/server HMR tests by using
.filter({ hasText: 'Client counter' }) to disambiguate the locator
when multiple client-counter test-id elements exist on the page
- Skip client HMR test: RSC architecture causes SSR page reload on client
component changes, which races with client-side HMR and prevents
the edited text from appearing reliably
Add detailed documentation covering: - How Vite's ModuleRunner caches modules via EvaluatedModules Map (promise-based caching, single-line cache check in cachedRequest) - Invalidation flow: goes UP to importers, not DOWN to dependencies, so unchanged route handlers/libs are served from cache instantly - The RSC plugin's hotUpdate hook: checks client boundary membership, sends rsc:update for server-only changes, React Fast Refresh for client changes - Performance analysis for large apps: only edited file + importers up to HMR boundary are re-evaluated, no bundling step - Three patterns for persistent resources (DB pools) across HMR: globalThis, import.meta.hot.data, or stable unchanged files - ASCII flow diagram showing the full server-side HMR pipeline - Source links to vite/src/module-runner/*.ts for each mechanism
Add documentation for the full-stack React framework features:
- Setup instructions with spiceflowPlugin Vite config
- App entry pattern with .page() and .layout() for server components
- Client components with "use client" directive and useState example
- Server actions with "use server" directive
- Client code splitting explanation: automatic per-file chunking via
@vitejs/plugin-rsc, how the RSC flight stream references chunks on
demand, and the barrel file anti-pattern ("use client" on a barrel
defeats splitting — put directive in each individual component file)
Also add React framework bullet to the Features list.
…gger server re-render Add a server-side render counter (serverRenderCount) that increments on each RSC render of the home page, exposed via data-testid="server-render-count". The client HMR test now asserts that: - Editing a client component does NOT increment the server render count - Client state is preserved (counter stays at 1 after edit) - The edited text appears via React Fast Refresh This proves client component edits only trigger client-side HMR, not a server re-render. Vite's SSR environment logs 'page reload' internally but the browser does not actually reload — React Fast Refresh handles the update. Also update AGENTS.md e2e testing section with: - Accurate HMR behavior (client HMR preserves state, no server re-render) - Warning about replace() no-ops when the search string doesn't exist in source - Documentation for the serverRenderCount test helper
Add e2e test proving that an async generator passed as a prop from a server component to a client component streams items incrementally — the client starts rendering before the generator completes. The test uses waitUntil:'commit' so Playwright begins observing while the HTML stream is still open. It asserts: - 'message-1' is visible while the 'done' marker is NOT (generator has ~3s left) - All 3 items arrive in order once the generator finishes This works because React 19's flight protocol natively serializes async iterables: the server yields values progressively via X/x flight chunks, and the client reconstructs a live AsyncIterable backed by a promise queue that resolves as chunks arrive over the wire. New files: - example-react/src/app/streaming-consumer.tsx — client component consuming AsyncIterable<string> via useEffect + for-await - /streaming route in main.tsx with 1.5s delays between yields
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.
Context
This branch replaces the previous custom React Server Components setup with the official
@vitejs/plugin-rscstack and aligns Spiceflow’s React runtime with Vite’s RSC environments.Main changes
@vitejs/plugin-rscforrsc/ssr/clientenvironments.@vitejs/plugin-rsc/rsc,/ssr,/browser)."use client"injection in the Spiceflow Vite plugin (with framework/entry/server-file exclusions)..rscfetch path +__rscquery) and hydration via embedded flight payload.MetaProvider+ collected head tags + HTML stream transform).content-type, and hardens client action payload update timing.Validation
cd spiceflow && rm -rf dist && pnpm tsc --noCheckcd spiceflow && pnpm test --runcd example-react && rm -rf node_modules/.vite && DEBUG_SPICEFLOW=1 npx playwright test --grep-invert '@dev|@build' --timeout 30000