Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 4 additions & 4 deletions examples/tanstack-ssr-workers.patch
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ index f712af5c5a73e65f8a9e64cd486a68078218b4ea..7bbec71850aefea2dd2b0d87cfd9ca6f
}
}
diff --git a/src/server.ts b/src/server.ts
index 98c671d10f422e183216c3c6ac33b9d3ff07e712..200022cafe2f901c7cee6a6c92dc89f7a241e25c 100644
index 276e876364b7ec56cc28b6a2a57229c2835d681e..73deca587d2c5dc5a35d40e073f2b6ed737cec38 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -1,5 +1,5 @@
Expand All @@ -293,7 +293,7 @@ index 98c671d10f422e183216c3c6ac33b9d3ff07e712..200022cafe2f901c7cee6a6c92dc89f7
import { api } from "../convex/_generated/api.js";

diff --git a/vite.config.ts b/vite.config.ts
index c1fa9a2d4c1224a8cbda8132e633b7b066e137b4..3e6badb4024aed63c5e612137eb9fc19df6cbb9c 100644
index 8e68d2fff175c09d037e869b6661cae7188aaa51..3e156c689f99f1e5ada6dd2e38c10494338c9504 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,12 +1,12 @@
Expand All @@ -309,8 +309,8 @@ index c1fa9a2d4c1224a8cbda8132e633b7b066e137b4..3e6badb4024aed63c5e612137eb9fc19
- convexSsr(),
+ cloudflare({ viteEnvironment: { name: "ssr" } }),
// Prerenders /about only. `prerender: { enabled: true }` would also prerender every static
// route, and crawling would follow /about's links.
tanstackStart({ pages: [{ path: "/about", prerender: { enabled: true, crawlLinks: false } }] }),
// route, and crawling would follow /about's links. The header marks the prerender's requests
// for src/server.ts; a prerender renders without a visitor.
diff --git a/wrangler.jsonc b/wrangler.jsonc
new file mode 100644
index 0000000000000000000000000000000000000000..48c90a9e2b5789638c5baf349fe0cd88dcbbb282
Expand Down
25 changes: 21 additions & 4 deletions examples/tanstack-ssr-workers/src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { useQuery, type QueryClient } from "@tanstack/react-query";
import { convexQuery, type ConvexQueryClient } from "@convex-dev/react-query";
import {
SessionProvider,
SignedIn,
SignedOut,
sessionBeforeLoad,
useSignOutSession,
} from "@samebase/convex-hosting-tanstack-start/convex-auth";
Expand Down Expand Up @@ -69,13 +71,28 @@ function Root() {
}

function SessionHeader() {
// `useQuery` does not fetch on the server, so the header of a prerendered page stays empty until
// the browser has the answer.
// The server render knows the session from its token, so a rendered page carries the right
// header; a prerendered page has no token and shows its header once the browser knows.
return (
<>
<SignedOut>
<Link to="/signin">Sign in</Link>
</SignedOut>
<SignedIn>
<SignedInHeader />
</SignedIn>
</>
);
}

function SignedInHeader() {
// The root loader has queried the user for a page rendered as the user, so the name is in the
// cache; `useQuery` does not fetch on the server, so a prerendered page shows it when the browser
// has it.
const { data: me } = useQuery(convexQuery(api.users.me, {}));
const signOut = useSignOutSession("/auth/signout");
const [signOutFailed, setSignOutFailed] = useState(false);
if (me === undefined) return null;
if (me === null) return <Link to="/signin">Sign in</Link>;
if (!me) return null;
return (
<>
{`Signed in as ${me.name}`}
Expand Down
4 changes: 3 additions & 1 deletion examples/tanstack-ssr-workers/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ const server = {
cookies,
cookieOptions,
});
const token = await session.getToken();
// The build's prerender renders without a visitor, so its page, which every visitor gets, must
// not say signed out: vite.config.ts marks its requests, and they get no token at all.
const token = request.headers.has("x-prerender") ? undefined : await session.getToken();
const page = await handler.fetch(request, { context: { token } });
const headers = new Headers(page.headers);
cookies.applyTo(headers);
Expand Down
8 changes: 6 additions & 2 deletions examples/tanstack-ssr-workers/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ export default defineConfig({
plugins: [
cloudflare({ viteEnvironment: { name: "ssr" } }),
// Prerenders /about only. `prerender: { enabled: true }` would also prerender every static
// route, and crawling would follow /about's links.
tanstackStart({ pages: [{ path: "/about", prerender: { enabled: true, crawlLinks: false } }] }),
// route, and crawling would follow /about's links. The header marks the prerender's requests
// for src/server.ts; a prerender renders without a visitor.
tanstackStart({
prerender: { headers: { "x-prerender": "1" } },
pages: [{ path: "/about", prerender: { enabled: true, crawlLinks: false } }],
}),
react(),
],
});
25 changes: 21 additions & 4 deletions examples/tanstack-ssr/src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { useQuery, type QueryClient } from "@tanstack/react-query";
import { convexQuery, type ConvexQueryClient } from "@convex-dev/react-query";
import {
SessionProvider,
SignedIn,
SignedOut,
sessionBeforeLoad,
useSignOutSession,
} from "@samebase/convex-hosting-tanstack-start/convex-auth";
Expand Down Expand Up @@ -69,13 +71,28 @@ function Root() {
}

function SessionHeader() {
// `useQuery` does not fetch on the server, so the header of a prerendered page stays empty until
// the browser has the answer.
// The server render knows the session from its token, so a rendered page carries the right
// header; a prerendered page has no token and shows its header once the browser knows.
return (
<>
<SignedOut>
<Link to="/signin">Sign in</Link>
</SignedOut>
<SignedIn>
<SignedInHeader />
</SignedIn>
</>
);
}

function SignedInHeader() {
// The root loader has queried the user for a page rendered as the user, so the name is in the
// cache; `useQuery` does not fetch on the server, so a prerendered page shows it when the browser
// has it.
const { data: me } = useQuery(convexQuery(api.users.me, {}));
const signOut = useSignOutSession("/auth/signout");
const [signOutFailed, setSignOutFailed] = useState(false);
if (me === undefined) return null;
if (me === null) return <Link to="/signin">Sign in</Link>;
if (!me) return null;
return (
<>
{`Signed in as ${me.name}`}
Expand Down
4 changes: 3 additions & 1 deletion examples/tanstack-ssr/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ const server = {
cookies,
cookieOptions,
});
const token = await session.getToken();
// The build's prerender renders without a visitor, so its page, which every visitor gets, must
// not say signed out: vite.config.ts marks its requests, and they get no token at all.
const token = request.headers.has("x-prerender") ? undefined : await session.getToken();
const page = await handler.fetch(request, { context: { token } });
const headers = new Headers(page.headers);
cookies.applyTo(headers);
Expand Down
8 changes: 6 additions & 2 deletions examples/tanstack-ssr/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ export default defineConfig({
plugins: [
convexSsr(),
// Prerenders /about only. `prerender: { enabled: true }` would also prerender every static
// route, and crawling would follow /about's links.
tanstackStart({ pages: [{ path: "/about", prerender: { enabled: true, crawlLinks: false } }] }),
// route, and crawling would follow /about's links. The header marks the prerender's requests
// for src/server.ts; a prerender renders without a visitor.
tanstackStart({
prerender: { headers: { "x-prerender": "1" } },
pages: [{ path: "/about", prerender: { enabled: true, crawlLinks: false } }],
}),
react(),
],
});
5 changes: 5 additions & 0 deletions packages/convex-hosting-tanstack-start/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
override of 0.6.0 is gone with it. `convexSsr()` resolves `node:stream` to a stub for the server
build: TanStack imports Node's stream classes for a branch that never runs where React's Web
Stream renderer exists, and Convex's bundler had nothing to resolve them to.
- The request context's token may be `undefined`: a render that knows nothing about a session, the
build's prerender, whose page every visitor gets. `SignedIn` and `SignedOut` wait for the browser
on it, and `redirectWithoutSession` redirects it like a render without a session. The app's
`src/server.ts` passes `undefined` for the prerender's requests, which it tells apart by the
request header that `tanstackStart({ prerender: { headers } })` sends.

## 0.8.0

Expand Down
15 changes: 11 additions & 4 deletions packages/convex-hosting-tanstack-start/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,17 +107,24 @@ does in its
[`src/server.ts`](https://github.com/samebase/convex-hosting/blob/main/examples/tanstack-ssr/src/server.ts):

```ts
const token = await session.getToken();
const token = request.headers.has("x-prerender") ? undefined : await session.getToken();
const page = await handler.fetch(request, { context: { token } });
```

The build's prerender renders a page without a visitor, and every visitor gets that page, so its
render must not say signed out. TanStack's prerender sends the request headers of
`tanstackStart({ prerender: { headers } })`; the example sets one, and `src/server.ts` gives such a
request no token at all. `SignedIn` and `SignedOut` then wait for the browser on that page, and a
gate redirects it as it redirects a request without a session.

`convex/http.ts` then forwards every request that is not a GET, and the page render, to the build
with `server.fetch(request)`, as the example's
[`convex/http.ts`](https://github.com/samebase/convex-hosting/blob/main/examples/tanstack-ssr/convex/http.ts)
does. `@samebase/convex-hosting-tanstack-start/convex-auth` has the rest, and loading it declares
`requestContext` as `{ token: string | null }` in the `Register` of `@tanstack/react-start`, which
makes `context` required and typed in `handler.fetch` and gives every route a typed `serverContext`.
The app creates its Convex client with `expectAuth: true`.
`requestContext` as `{ token: string | null | undefined }` in the `Register` of
`@tanstack/react-start`, which makes `context` required and typed in `handler.fetch` and gives every
route a typed `serverContext`. The token is `undefined` for a render that knows nothing about a
session, the prerender. The app creates its Convex client with `expectAuth: true`.

Nothing in this entry is about hosting: it is the TanStack Start integration that Convex Auth does
not ship, and the
Expand Down
19 changes: 11 additions & 8 deletions packages/convex-hosting-tanstack-start/src/convex-auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,13 @@ import { useConvexAuth, type ConvexReactClient } from "convex/react";
import { createContext, useContext, useEffect, useRef, type ReactNode } from "react";

/**
* The request context of a page render: the session's access token, or `null` without one. The
* app's `src/server.ts` reads it off the cookies and passes it to the server entry.
* The request context of a page render: the session's access token, `null` without one, or
* `undefined` for a render that knows nothing about a session: the build's prerender, whose page
* every visitor gets. The app's `src/server.ts` reads it off the cookies and passes it to the
* server entry.
*/
interface RequestContext {
token: string | null;
token: string | null | undefined;
}

// Start's own `Register`, where it reads `requestContext`. TypeScript 5.9 does not merge a
Expand Down Expand Up @@ -104,8 +106,8 @@ function ReleaseWhenSignedOut({ client }: { client: ConvexReactClient }) {
*
* Convex Auth reports loading during the server render and the hydration, so the token of the
* server render decides until the provider settles: a string means that the server rendered as the
* user, and `null` that it found no session; a render without a request context has none. Both
* renders read the same token, so the hydration matches.
* user, and `null` that it found no session; `undefined`, the build's prerender or a render without
* a request context, knows nothing. Both renders read the same token, so the hydration matches.
*/
function useSignedIn(): boolean | undefined {
const token = useContext(SessionToken);
Expand Down Expand Up @@ -144,14 +146,15 @@ export function useSignOutSession(url: string): () => Promise<boolean> {
}

/**
* A gate's `beforeLoad`: a server render without a session throws `redirect(options)`. Only the
* server render has the cookie; a browser navigation has no `serverContext`.
* A gate's `beforeLoad`: a server render without a session throws `redirect(options)`, whether it
* found none or knows nothing about one. Only the server render has the cookie; a browser
* navigation has no `serverContext`.
*/
export function redirectWithoutSession<TOptions>(
serverContext: RequestContext | undefined,
options: ValidateRedirectOptions<RegisteredRouter, TOptions>,
) {
if (serverContext?.token === null) {
if (serverContext !== undefined && !serverContext.token) {
throw redirect<
RegisteredRouter,
InferTo<TOptions>,
Expand Down
Loading