Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
101 commits
Select commit Hold shift + click to select a range
0fa5591
adding react things
remorses Feb 9, 2025
420227a
adding js extension
remorses Feb 9, 2025
f949eca
fix tsc errors
remorses Feb 9, 2025
50edca0
making vite plugin
remorses Feb 9, 2025
e4b82f1
it works, vite optimize deps thing is awful, added util in exclude or…
remorses Feb 9, 2025
76698ad
nn
remorses Feb 9, 2025
2dc1480
spiceflow works, removed noExternal because commonjs does not work th…
remorses Feb 9, 2025
c9b94b6
kind of works, adding example app in react and react method
remorses Feb 9, 2025
fc86f44
fix, use parcel rsc-html-stream
remorses Feb 9, 2025
66808b9
adding e2e tests
remorses Feb 9, 2025
2c593ca
fix e2e
remorses Feb 9, 2025
5a43d58
adding prettier, fixing tests
remorses Feb 9, 2025
6c39c92
fix tests because of deleted config
remorses Feb 9, 2025
e544c99
nn
remorses Feb 9, 2025
762cc46
redirect works on top level
remorses Feb 9, 2025
879ed71
vendored medley router
remorses Feb 9, 2025
5863664
vendoring router, better types refactoring, only one InternalRoute,
remorses Feb 9, 2025
3f28d55
vendoring router, testing * does not do anything weird
remorses Feb 9, 2025
7355268
adding layouts support, api part still works
remorses Feb 9, 2025
8f5cb42
adding id to the router
remorses Feb 9, 2025
7204e9c
adding hono trie router
remorses Feb 9, 2025
11c0461
using hono router
remorses Feb 9, 2025
869ca83
adding more hono router, strugglying with *
remorses Feb 9, 2025
01f3d9e
trying to make getAllDecodedParams work
remorses Feb 9, 2025
55ea8fb
fix * handling for a case, another one is todo
remorses Feb 9, 2025
90adb97
fixed * somehow
remorses Feb 9, 2025
561c281
fixed extractWildcardParam, tests were inverted
remorses Feb 9, 2025
0b27f8c
remove logs
remorses Feb 9, 2025
d3d98dc
it works, managed to make the vite plugin work, fixed the issue where…
remorses Feb 10, 2025
5a26a90
build kind of works too
remorses Feb 10, 2025
d24a928
trying better generateId, still does not work
remorses Feb 10, 2025
2f9da19
fix redirect thing
remorses Feb 10, 2025
c1c3445
layouts seem to work too
remorses Feb 10, 2025
6bf8bf1
more layouts
remorses Feb 10, 2025
d70da5e
tried links too
remorses Feb 10, 2025
75f3443
tailwindcss works
remorses Feb 10, 2025
df79c24
fix build time css, debugging missing server references
remorses Feb 10, 2025
76a569e
fix missing server references from client components in build
remorses Feb 10, 2025
1e57519
fix css during build, need to copy them, thanks to preserveEntrySigna…
remorses Feb 10, 2025
41db076
use routeSorter to make sure static routes have priority
remorses Feb 10, 2025
bffac7d
checking what happens on errors
remorses Feb 10, 2025
e75204f
run prettier
remorses Feb 10, 2025
5815178
put rsc entry inside spiceflow
remorses Feb 10, 2025
c74c6af
fix tsc errors
remorses Feb 10, 2025
7cdecea
adding a bit of error handling in ssr and client
remorses Feb 10, 2025
c08e768
using react-dom/server.edge fixes hanging on error
remorses Feb 10, 2025
3d5d5ed
add css in error page
remorses Feb 10, 2025
ff956c0
made link component, tested client navigations, back buttons waits wh…
remorses Feb 11, 2025
4f9e4c0
fix hmr from server, tested suspense in rsc
remorses Feb 11, 2025
692e631
added how-is-this-not-illegal, even simpler
remorses Feb 11, 2025
981aaa2
added pokemon view, works well
remorses Feb 11, 2025
54ffe26
added progress component
remorses Feb 11, 2025
594cb63
payload is always a promise, this way navigations state does not trig…
remorses Feb 11, 2025
106e812
fix support for redirect, fix hot module replacement for client impor…
remorses Feb 11, 2025
0253795
all redirect tests pass
remorses Feb 11, 2025
8331a41
separating hooks from components
remorses Feb 11, 2025
95ee6c0
managed to catch errors in rsc, cool, fix page ordering with :id
remorses Feb 11, 2025
f2cc826
added not found function
remorses Feb 11, 2025
447ac5b
refactored renderReact
remorses Feb 11, 2025
77d8157
adding middleware support for react too, works pretty well, hope perf…
remorses Feb 11, 2025
312a69b
fix validation errors, forgot to change request in context
remorses Feb 11, 2025
eec8ad6
not found
remorses Feb 11, 2025
c346438
show error overlay on rsc errors
remorses Feb 12, 2025
8d07987
tested errors in useEffect
remorses Feb 12, 2025
5c1e69e
use @jacob-ebey/react-server-dom-vite directly because of pnpm bug, a…
remorses Feb 12, 2025
47716d0
tried out using hmr true
remorses Feb 12, 2025
637ea5c
use .rsc to support prerendering in cdn
remorses Feb 12, 2025
5a5fa6e
fixed progress bar, stop on data set, which means transition ended
remorses Feb 12, 2025
7467b27
added prerender feature
remorses Feb 12, 2025
db0018c
adding meta component, vendoring rsc stream
remorses Feb 12, 2025
135767a
transform stream works,
remorses Feb 12, 2025
aec5a0b
add support for relative urls in meta
remorses Feb 12, 2025
57765fc
fix meta, head, also adding server launcher support
remorses Feb 12, 2025
6c1d25f
put first arg of context first, so it is easier to use
remorses Feb 12, 2025
6f6dfe3
fix css, adding prerender example with generate stacic segments
remorses Feb 12, 2025
afe340f
write node.js to run a node server
remorses Feb 12, 2025
fbc1731
adding more abort signals
remorses Feb 12, 2025
f0793bc
renamed references files
remorses Feb 12, 2025
29aa1e3
fix static generated pages
remorses Feb 12, 2025
29aad7a
fix rsc prerender
remorses Feb 12, 2025
dae438f
got actions errors working
remorses Feb 12, 2025
39b9849
testing form actions
remorses Feb 14, 2025
1536fa3
handle errors in passthrough
remorses Feb 17, 2025
cc1259a
handle error in ssr entry
remorses Feb 17, 2025
ae43a13
made a prerelease
remorses Feb 17, 2025
3d16eb8
merge main into rsc branch
remorses Mar 1, 2026
a64a400
migrate RSC from custom implementation to @vitejs/plugin-rsc
remorses Mar 1, 2026
61ac4fc
clean up post-migration RSC leftovers and harden runtime behavior
remorses Mar 1, 2026
c5592b5
fix stale location: use getter to always return current history.location
remorses Mar 1, 2026
4b199da
Update vite.tsx
remorses Mar 1, 2026
0df73fd
add @vitejs/plugin-rsc skill: comprehensive framework author guide
remorses Mar 2, 2026
7c0211d
fix: clean up RSC merge — remove duplicated code, fix bugs, re-enable…
remorses Mar 2, 2026
27db83e
expand vite-plugin-rsc skill with React Router RSC patterns
remorses Mar 2, 2026
330bb1e
docs: add RSC alignment plan — gaps between spiceflow and @vitejs/plu…
remorses Mar 2, 2026
e93e913
chore: commit remaining workspace updates from other agents
remorses Mar 2, 2026
46e2f25
feat: RSC alignment — CSRF protection, __NO_HYDRATE error fallback, t…
remorses Mar 2, 2026
20763c2
test: add e2e tests for SSR error fallback and CSRF, fix HMR test loc…
remorses Mar 2, 2026
6897040
docs: expand Vite RSC skill HMR section with module caching internals
remorses Mar 2, 2026
c66bf36
docs: add React framework (RSC) section to README
remorses Mar 2, 2026
a86467a
test: add serverRenderCount counter and verify client HMR doesn't tri…
remorses Mar 2, 2026
101c867
test: streaming async generator from server to client component
remorses Mar 2, 2026
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
1,315 changes: 1,315 additions & 0 deletions .agents/skills/vite-plugin-rsc/SKILL.md

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions .changeset/fifty-rings-poke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'spiceflow': patch
---

Clean up leftover React Server Components migration artifacts by removing unused framework-only types and stale exports, and harden the new `@vitejs/plugin-rsc` flow. This update ensures server redirects emit a correct `content-type` header, removes dead exports like `RscHandlerResult`, and makes client action refresh handling safer by setting payload updates before awaiting action results.
5 changes: 5 additions & 0 deletions .changeset/five-toes-learn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'spiceflow': patch
---

initial rsc release
12 changes: 12 additions & 0 deletions .changeset/pre.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"mode": "pre",
"tag": "rsc",
"initialVersions": {
"openapi-schema-diff": "0.0.1",
"spiceflow": "1.6.1",
"how-is-this-not-illegal": "0.1.0"
},
"changesets": [
"five-toes-learn"
]
}
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ __pycache__
debug
.env
.last-run.json
.react-router
.react-router
opensrc
/di
3 changes: 0 additions & 3 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +0,0 @@
[submodule "openapi-schema-diff"]
path = openapi-schema-diff
url = https://github.com/remorses/openapi-schema-diff.git
72 changes: 72 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,78 @@ Sometimes tests work directly on database data, using prisma. To run these tests
Never write tests yourself that call prisma or interact with database or emails. For these asks the user to write them for you.


# e2e testing (example-react)

E2e tests live in `example-react/e2e/` and use Playwright (chromium only). The dev server starts automatically via the `webServer` config in `playwright.config.ts`.

## running e2e tests

```bash
# run from example-react directory, never from root
cd example-react

# run all e2e tests
pnpm test-e2e

# filter by test name
pnpm test-e2e --grep "SSR error"

# run against production build
pnpm test-e2e-preview
```

Tests tagged `@dev` are skipped during preview runs; tests tagged `@build` are skipped during dev runs (controlled by `grepInvert` in playwright.config.ts).

## rebuild dist before testing

The Vite SSR middleware imports from `spiceflow/dist/` (the compiled package), NOT from source. If you modify files in `spiceflow/src/`, you must rebuild before e2e tests will pick up the changes:

```bash
cd spiceflow
pnpm tsc --noCheck # --noCheck skips pre-existing type errors
```

This is the most common reason e2e tests fail after code changes — stale dist files.

## writing e2e tests

- The base URL and port are defined at the top of `basic.test.ts`:
```ts
const port = Number(process.env.E2E_PORT || 6174);
const baseURL = `http://localhost:${port}`;
```
- Use `page.goto("/path")` for browser-based tests that need rendering, JS execution, or DOM interaction.
- Use Node.js `fetch(baseURL + "/path")` directly (not `page.evaluate`) when you need to control HTTP headers like `Origin` — browsers restrict forbidden headers.
- Use `page.getByTestId()`, `page.getByText()`, `page.getByRole()` for locators. Prefer test-ids for stability.
- When a `data-testid` matches multiple elements (e.g. multiple counter components on a page), use `.filter({ hasText: "..." })` to disambiguate:
```ts
const clientCounter = page.getByTestId("client-counter").filter({ hasText: "Client counter" });
await clientCounter.getByRole("button", { name: "+" }).click();
```
- If a locator's text changes during the test (e.g. HMR edits), do NOT use it through a pre-filtered variable — query the page directly for the new text.

## adding test routes

To add a route for e2e testing, add it in `example-react/src/main.tsx` using the spiceflow API:

```ts
.page("/my-test-route", async () => {
return <MyComponent />;
})
```

Client components used in tests should be created in `example-react/src/app/` with a `"use client"` directive.

## HMR tests

- `createEditor("src/app/file.tsx")` from `e2e/helper.ts` edits a file and auto-reverts on dispose.
- Always call `file[Symbol.dispose]()` or use `try/finally` to restore files after edits.
- When editing files, make sure the `replace()` string actually exists in the source. For example, `client.tsx` has `name = "Client"` as a default prop — the literal string "Client counter" does NOT exist in the file, so `replace("Client counter", ...)` would be a no-op and the HMR test would silently fail.
- **Client HMR preserves state**: editing a client component triggers React Fast Refresh without a server re-render. Client state is preserved. Vite's SSR environment logs `page reload` internally but the browser does not actually reload — Fast Refresh handles it.
- **Server HMR preserves server state**: editing a server component triggers RSC HMR. Server-side state (e.g. counters stored in module scope) is preserved. Client state is also preserved because no full page reload occurs.
- The home page has a `serverRenderCount` counter (`data-testid="server-render-count"`) that increments on each RSC render. Use it in tests to verify whether a server re-render happened.


# website

the website uses react-router v7.
Expand Down
133 changes: 133 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Spiceflow is a lightweight, type-safe API framework for building web services us
- Can easily generate OpenAPI spec based on your routes
- Native support for [Fern](https://github.com/fern-api/fern) to generate docs and SDKs (see example docs [here](https://remorses.docs.buildwithfern.com))
- Support for [Model Context Protocol](https://modelcontextprotocol.io/) to easily wire your app with LLMs
- Full-stack React framework with React Server Components (RSC), server actions, layouts, and automatic client code splitting
- Type safe RPC client generation
- Simple and intuitive API
- Uses web standards for requests and responses
Expand Down Expand Up @@ -1563,3 +1564,135 @@ export const client: SpiceflowClient.Create<App> = createSpiceflowClient<App>(
{},
)
```

## React Framework (RSC)

Spiceflow includes a full-stack React framework built on React Server Components (RSC). It uses Vite with `@vitejs/plugin-rsc` under the hood. Server components run on the server by default, and you use `"use client"` to mark interactive components that need to run in the browser.

### Setup

Install the dependencies and create a Vite config:

```bash
npm install spiceflow react react-dom
```

```ts
// vite.config.ts
import { defineConfig } from 'vite'
import { spiceflowPlugin } from 'spiceflow/dist/vite'

export default defineConfig({
plugins: [
spiceflowPlugin({
entry: './src/main.tsx',
}),
],
})
```

### App Entry (Server Component)

The entry file defines your routes using `.page()` for pages and `.layout()` for layouts. This file runs in the RSC environment on the server.

```tsx
// src/main.tsx
import { Spiceflow } from 'spiceflow'
import { Counter } from './app/counter'

const app = new Spiceflow()
.layout('/*', async ({ children }) => {
return (
<html>
<head>
<meta charSet="UTF-8" />
</head>
<body>{children}</body>
</html>
)
})
.page('/', async () => {
// This runs on the server — you can fetch data, access databases, etc.
const data = await fetchSomeData()
return (
<div>
<h1>Welcome</h1>
<p>Server-rendered data: {data.message}</p>
<Counter />
</div>
)
})
.page('/about', async () => {
return <div><h1>About</h1></div>
})

export default app
```

### Client Components

Mark interactive components with `"use client"` at the top of the file. These are hydrated in the browser and can use hooks like `useState`.

```tsx
// src/app/counter.tsx
'use client'

import { useState } from 'react'

export function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
)
}
```

### Server Actions

Use `"use server"` to define functions that run on the server but can be called from client components (e.g. form actions).

```tsx
// src/app/actions.tsx
'use server'

export async function submitForm(formData: FormData) {
const name = formData.get('name')
await saveToDatabase(name)
}
```

### Client Code Splitting

Code splitting of client components is **automatic** — you don't need `React.lazy()` or dynamic `import()`. Each `"use client"` file becomes a separate chunk, and the browser only loads the chunks needed for the current page.

**How it works:** when the RSC flight stream is sent to the browser, it contains references to client component chunks rather than the actual code. The browser resolves and loads only the chunks referenced on the current page. If route `/about` uses `<Map />` and route `/dashboard` uses `<Chart />`, visiting `/about` will never download the Chart component's JavaScript.

**Avoid barrel files with `"use client"`.** If you have a single file with `"use client"` that re-exports many components, all of them end up in one chunk — defeating code splitting. Instead, put `"use client"` in each individual component file:

```tsx
// BAD — one big chunk for everything
// src/components/index.tsx
'use client'
export { Chart } from './chart'
export { Map } from './map'
export { Table } from './table'
```

```tsx
// GOOD — each component is its own chunk
// src/components/chart.tsx
'use client'
export function Chart() { /* ... */ }

// src/components/map.tsx
'use client'
export function Map() { /* ... */ }

// Re-export barrel has no directive, just passes through
// src/components/index.tsx
export { Chart } from './chart'
export { Map } from './map'
```
Loading
Loading