Skip to content

Commit 212554e

Browse files
authored
docs: TanStack Start + Rsbuild recipe and example app (#1810)
1 parent 7d8776f commit 212554e

31 files changed

Lines changed: 3567 additions & 72 deletions

File tree

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# ArkEnv + TanStack Start (Rsbuild) Example
2+
3+
This example demonstrates `@arkenv/rsbuild-plugin` with [TanStack Start](https://tanstack.com/start) on Rsbuild:
4+
5+
- A single `src/env.ts` is the typed source of truth (`import { env } from "./env"`)
6+
- **Client graph**: plugin inlines coerced `PUBLIC_*` literals and guards server-only keys
7+
- **SSR graph**: `env.ts` runs as-is → boot-time validation against the real environment, including inside `createServerFn` handlers
8+
- Reading `env.DATABASE_URL` in the browser throws (try the button on the home page)
9+
10+
## Setup
11+
12+
```ts title="src/env.ts"
13+
import arkenv from "@arkenv/core";
14+
15+
export const env = arkenv({
16+
DATABASE_URL: "string = 'postgres://localhost:5432/tanstackstartrsbuild'",
17+
PORT: "number.port = 3000",
18+
PUBLIC_API_URL: "string = 'https://api.example.com'",
19+
NODE_ENV: "'development' | 'production' | 'test' = 'development'",
20+
});
21+
```
22+
23+
```ts title="rsbuild.config.ts"
24+
import { pluginReact } from "@rsbuild/plugin-react";
25+
import { tanstackStart } from "@tanstack/react-start/plugin/rsbuild";
26+
import { arkenvRsbuildPlugin } from "@arkenv/rsbuild-plugin";
27+
import { defineConfig } from "@rsbuild/core";
28+
29+
export default defineConfig({
30+
server: {
31+
port: 3000,
32+
},
33+
plugins: [
34+
tanstackStart({ srcDirectory: "src" }),
35+
pluginReact(),
36+
arkenvRsbuildPlugin(),
37+
],
38+
});
39+
```
40+
41+
## Usage
42+
43+
```tsx
44+
import { createServerFn } from "@tanstack/react-start";
45+
import { env } from "./env";
46+
47+
const readDatabaseUrl = createServerFn({ method: "GET" }).handler(() => {
48+
return env.DATABASE_URL; // server-only: real value, validated at boot
49+
});
50+
51+
env.PUBLIC_API_URL; // string (inlined on the client)
52+
env.DATABASE_URL; // throws in the browser; works in SSR and server functions
53+
```
54+
55+
## Running the Example
56+
57+
```bash
58+
# Install dependencies
59+
pnpm install
60+
61+
# Start dev server
62+
pnpm dev
63+
64+
# Build for production
65+
pnpm build
66+
67+
# Start production server
68+
pnpm start
69+
```
70+
71+
## Documentation
72+
73+
- [TanStack Start guide](https://github.com/yamcodes/arkenv/blob/v1/apps/www/content/docs/frameworks/tanstack-start.mdx) (`/docs/frameworks/tanstack-start` on the v1 docs site)
74+
- [Rsbuild plugin docs](https://github.com/yamcodes/arkenv/blob/v1/apps/www/content/docs/reference/rsbuild-plugin.mdx) (`/docs/reference/rsbuild-plugin` on the v1 docs site)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"name": "tanstack-start-rsbuild-playground",
3+
"private": true,
4+
"version": "0.0.0",
5+
"type": "module",
6+
"sideEffects": false,
7+
"scripts": {
8+
"dev": "rsbuild dev",
9+
"build": "rsbuild build && tsc --noEmit",
10+
"start": "rsbuild preview",
11+
"preview": "rsbuild preview",
12+
"fix": "pnpm -w run fix",
13+
"clean": "rimraf dist node_modules .output"
14+
},
15+
"dependencies": {
16+
"@arkenv/core": "workspace:*",
17+
"@arkenv/rsbuild-plugin": "workspace:*",
18+
"@tanstack/react-router": "1.170.32",
19+
"@tanstack/react-start": "1.168.49",
20+
"arktype": "catalog:",
21+
"react": "catalog:",
22+
"react-dom": "catalog:"
23+
},
24+
"devDependencies": {
25+
"@rsbuild/core": "2.2.3",
26+
"@rsbuild/plugin-react": "2.1.0",
27+
"@types/node": "catalog:",
28+
"@types/react": "catalog:",
29+
"@types/react-dom": "catalog:",
30+
"rimraf": "catalog:",
31+
"typescript": "catalog:"
32+
},
33+
"engines": {
34+
"node": ">=22.12.0"
35+
},
36+
"arkenvExamples": [
37+
{
38+
"name": "with-tanstack-start-rsbuild",
39+
"packageManager": "npm",
40+
"exclude": [
41+
".tanstack"
42+
]
43+
}
44+
]
45+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { arkenvRsbuildPlugin } from "@arkenv/rsbuild-plugin";
2+
import { defineConfig } from "@rsbuild/core";
3+
import { pluginReact } from "@rsbuild/plugin-react";
4+
import { tanstackStart } from "@tanstack/react-start/plugin/rsbuild";
5+
6+
export default defineConfig({
7+
server: {
8+
port: 3000,
9+
},
10+
plugins: [
11+
tanstackStart({ srcDirectory: "src" }),
12+
pluginReact(),
13+
arkenvRsbuildPlugin(),
14+
],
15+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { useState } from "react";
2+
import { env } from "~/env";
3+
4+
function LeakedSecret() {
5+
// Accessing server-only DATABASE_URL directly in client render tree throws an error
6+
return <p>Server key leaked: {env.DATABASE_URL}</p>;
7+
}
8+
9+
export function SecretLeakButton() {
10+
const [attempted, setAttempted] = useState(false);
11+
12+
if (attempted) {
13+
return <LeakedSecret />;
14+
}
15+
16+
return (
17+
<button type="button" onClick={() => setAttempted(true)}>
18+
Try reading DATABASE_URL on the client
19+
</button>
20+
);
21+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import arkenv from "@arkenv/core";
2+
3+
/**
4+
* Validated environment for this app.
5+
*/
6+
export const env = arkenv({
7+
DATABASE_URL: "string = 'postgres://localhost:5432/tanstackstartrsbuild'",
8+
PORT: "number.port = 3000",
9+
PUBLIC_API_URL: "string = 'https://api.example.com'",
10+
NODE_ENV: "'development' | 'production' | 'test' = 'development'",
11+
});
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/* eslint-disable */
2+
3+
// @ts-nocheck
4+
5+
// noinspection JSUnusedGlobalSymbols
6+
7+
// This file was automatically generated by TanStack Router.
8+
// You should NOT make any changes in this file as it will be overwritten.
9+
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
10+
11+
import { Route as rootRouteImport } from './routes/__root'
12+
import { Route as IndexRouteImport } from './routes/index'
13+
14+
const IndexRoute = IndexRouteImport.update({
15+
id: '/',
16+
path: '/',
17+
getParentRoute: () => rootRouteImport,
18+
} as any)
19+
20+
export interface FileRoutesByFullPath {
21+
'/': typeof IndexRoute
22+
}
23+
export interface FileRoutesByTo {
24+
'/': typeof IndexRoute
25+
}
26+
export interface FileRoutesById {
27+
__root__: typeof rootRouteImport
28+
'/': typeof IndexRoute
29+
}
30+
export interface FileRouteTypes {
31+
fileRoutesByFullPath: FileRoutesByFullPath
32+
fullPaths: '/'
33+
fileRoutesByTo: FileRoutesByTo
34+
to: '/'
35+
id: '__root__' | '/'
36+
fileRoutesById: FileRoutesById
37+
}
38+
export interface RootRouteChildren {
39+
IndexRoute: typeof IndexRoute
40+
}
41+
42+
declare module '@tanstack/react-router' {
43+
interface FileRoutesByPath {
44+
'/': {
45+
id: '/'
46+
path: '/'
47+
fullPath: '/'
48+
preLoaderRoute: typeof IndexRouteImport
49+
parentRoute: typeof rootRouteImport
50+
}
51+
}
52+
}
53+
54+
const rootRouteChildren: RootRouteChildren = {
55+
IndexRoute: IndexRoute,
56+
}
57+
export const routeTree = rootRouteImport
58+
._addFileChildren(rootRouteChildren)
59+
._addFileTypes<FileRouteTypes>()
60+
61+
import type { getRouter } from './router.tsx'
62+
import type { createStart } from '@tanstack/react-start'
63+
declare module '@tanstack/react-start' {
64+
interface Register {
65+
ssr: true
66+
router: Awaited<ReturnType<typeof getRouter>>
67+
}
68+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { createRouter } from "@tanstack/react-router";
2+
import { routeTree } from "./routeTree.gen";
3+
4+
export function getRouter() {
5+
return createRouter({
6+
routeTree,
7+
defaultPreload: "intent",
8+
});
9+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import {
2+
createRootRoute,
3+
HeadContent,
4+
Outlet,
5+
Scripts,
6+
} from "@tanstack/react-router";
7+
8+
export const Route = createRootRoute({
9+
head: () => ({
10+
meta: [
11+
{
12+
charSet: "utf-8",
13+
},
14+
{
15+
name: "viewport",
16+
content: "width=device-width, initial-scale=1",
17+
},
18+
{
19+
title: "ArkEnv + TanStack Start",
20+
},
21+
],
22+
}),
23+
component: RootDocument,
24+
});
25+
26+
function RootDocument() {
27+
return (
28+
<html lang="en">
29+
<head>
30+
<HeadContent />
31+
</head>
32+
<body>
33+
<Outlet />
34+
<Scripts />
35+
</body>
36+
</html>
37+
);
38+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { createFileRoute } from "@tanstack/react-router";
2+
import { createServerFn } from "@tanstack/react-start";
3+
import { SecretLeakButton } from "~/components/secret-leak-button";
4+
import { env } from "~/env";
5+
6+
const getDatabaseHost = createServerFn({ method: "GET" }).handler(() => {
7+
// Server functions can safely read server secrets without leaking them to the client:
8+
const url = new URL(env.DATABASE_URL);
9+
return url.host;
10+
});
11+
12+
export const Route = createFileRoute("/")({
13+
component: Home,
14+
loader: () => getDatabaseHost(),
15+
});
16+
17+
function Home() {
18+
const dbHost = Route.useLoaderData();
19+
20+
return (
21+
<main style={{ fontFamily: "system-ui, sans-serif", padding: "2rem" }}>
22+
<h1>API: {env.PUBLIC_API_URL}</h1>
23+
<p>Public key inlined into the client bundle: {env.PUBLIC_API_URL}</p>
24+
<p>Database host loaded through createServerFn: {dbHost}</p>
25+
<SecretLeakButton />
26+
</main>
27+
);
28+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"include": ["**/*.ts", "**/*.tsx", "**/*.d.ts"],
3+
"compilerOptions": {
4+
"paths": {
5+
"~/*": ["./src/*"]
6+
},
7+
"strict": true,
8+
"esModuleInterop": true,
9+
"jsx": "react-jsx",
10+
"module": "ESNext",
11+
"moduleResolution": "Bundler",
12+
"lib": ["DOM", "DOM.Iterable", "ES2022"],
13+
"isolatedModules": true,
14+
"resolveJsonModule": true,
15+
"skipLibCheck": true,
16+
"target": "ES2022",
17+
"allowJs": true,
18+
"forceConsistentCasingInFileNames": true,
19+
"noEmit": true
20+
}
21+
}

0 commit comments

Comments
 (0)