@benjavicente/start-client-core 1.167.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +12 -0
- package/bin/intent.js +25 -0
- package/dist/esm/client/ServerFunctionSerializationAdapter.d.ts +7 -0
- package/dist/esm/client/ServerFunctionSerializationAdapter.js +18 -0
- package/dist/esm/client/ServerFunctionSerializationAdapter.js.map +1 -0
- package/dist/esm/client/hydrateStart.d.ts +2 -0
- package/dist/esm/client/hydrateStart.js +31 -0
- package/dist/esm/client/hydrateStart.js.map +1 -0
- package/dist/esm/client/index.d.ts +2 -0
- package/dist/esm/client/index.js +2 -0
- package/dist/esm/client-rpc/createClientRpc.d.ts +6 -0
- package/dist/esm/client-rpc/createClientRpc.js +21 -0
- package/dist/esm/client-rpc/createClientRpc.js.map +1 -0
- package/dist/esm/client-rpc/frame-decoder.d.ts +23 -0
- package/dist/esm/client-rpc/frame-decoder.js +231 -0
- package/dist/esm/client-rpc/frame-decoder.js.map +1 -0
- package/dist/esm/client-rpc/index.d.ts +1 -0
- package/dist/esm/client-rpc/index.js +2 -0
- package/dist/esm/client-rpc/serverFnFetcher.d.ts +1 -0
- package/dist/esm/client-rpc/serverFnFetcher.js +231 -0
- package/dist/esm/client-rpc/serverFnFetcher.js.map +1 -0
- package/dist/esm/constants.d.ts +53 -0
- package/dist/esm/constants.js +46 -0
- package/dist/esm/constants.js.map +1 -0
- package/dist/esm/createMiddleware.d.ts +195 -0
- package/dist/esm/createMiddleware.js +26 -0
- package/dist/esm/createMiddleware.js.map +1 -0
- package/dist/esm/createServerFn.d.ts +131 -0
- package/dist/esm/createServerFn.js +200 -0
- package/dist/esm/createServerFn.js.map +1 -0
- package/dist/esm/createStart.d.ts +50 -0
- package/dist/esm/createStart.js +29 -0
- package/dist/esm/createStart.js.map +1 -0
- package/dist/esm/fake-start-entry.d.ts +2 -0
- package/dist/esm/fake-start-entry.js +7 -0
- package/dist/esm/fake-start-entry.js.map +1 -0
- package/dist/esm/getDefaultSerovalPlugins.d.ts +1 -0
- package/dist/esm/getDefaultSerovalPlugins.js +10 -0
- package/dist/esm/getDefaultSerovalPlugins.js.map +1 -0
- package/dist/esm/getGlobalStartContext.d.ts +3 -0
- package/dist/esm/getGlobalStartContext.js +12 -0
- package/dist/esm/getGlobalStartContext.js.map +1 -0
- package/dist/esm/getRouterInstance.d.ts +2 -0
- package/dist/esm/getRouterInstance.js +8 -0
- package/dist/esm/getRouterInstance.js.map +1 -0
- package/dist/esm/getStartContextServerOnly.d.ts +2 -0
- package/dist/esm/getStartContextServerOnly.js +8 -0
- package/dist/esm/getStartContextServerOnly.js.map +1 -0
- package/dist/esm/getStartOptions.d.ts +2 -0
- package/dist/esm/getStartOptions.js +8 -0
- package/dist/esm/getStartOptions.js.map +1 -0
- package/dist/esm/global.d.ts +7 -0
- package/dist/esm/index.d.ts +20 -0
- package/dist/esm/index.js +12 -0
- package/dist/esm/safeObjectMerge.d.ts +10 -0
- package/dist/esm/safeObjectMerge.js +30 -0
- package/dist/esm/safeObjectMerge.js.map +1 -0
- package/dist/esm/serverRoute.d.ts +65 -0
- package/dist/esm/startEntry.d.ts +8 -0
- package/dist/esm/tests/createServerFn.test-d.d.ts +1 -0
- package/dist/esm/tests/createServerMiddleware.test-d.d.ts +1 -0
- package/package.json +98 -0
- package/skills/start-core/SKILL.md +210 -0
- package/skills/start-core/deployment/SKILL.md +306 -0
- package/skills/start-core/execution-model/SKILL.md +302 -0
- package/skills/start-core/middleware/SKILL.md +365 -0
- package/skills/start-core/server-functions/SKILL.md +335 -0
- package/skills/start-core/server-routes/SKILL.md +280 -0
- package/src/client/ServerFunctionSerializationAdapter.ts +16 -0
- package/src/client/hydrateStart.ts +43 -0
- package/src/client/index.ts +2 -0
- package/src/client-rpc/createClientRpc.ts +20 -0
- package/src/client-rpc/frame-decoder.ts +389 -0
- package/src/client-rpc/index.ts +1 -0
- package/src/client-rpc/serverFnFetcher.ts +416 -0
- package/src/constants.ts +90 -0
- package/src/createMiddleware.ts +824 -0
- package/src/createServerFn.ts +813 -0
- package/src/createStart.ts +166 -0
- package/src/fake-start-entry.ts +2 -0
- package/src/getDefaultSerovalPlugins.ts +17 -0
- package/src/getGlobalStartContext.ts +18 -0
- package/src/getRouterInstance.ts +8 -0
- package/src/getStartContextServerOnly.ts +4 -0
- package/src/getStartOptions.ts +8 -0
- package/src/global.ts +9 -0
- package/src/index.tsx +119 -0
- package/src/safeObjectMerge.ts +38 -0
- package/src/serverRoute.ts +509 -0
- package/src/start-entry.d.ts +11 -0
- package/src/startEntry.ts +10 -0
- package/src/tests/createServerFn.test-d.ts +866 -0
- package/src/tests/createServerMiddleware.test-d.ts +810 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: start-core/deployment
|
|
3
|
+
description: >-
|
|
4
|
+
Deploy to Cloudflare Workers, Netlify, Vercel, Node.js/Docker,
|
|
5
|
+
Bun, Railway. Selective SSR (ssr option per route), SPA mode,
|
|
6
|
+
static prerendering, ISR with Cache-Control headers, SEO and
|
|
7
|
+
head management.
|
|
8
|
+
type: sub-skill
|
|
9
|
+
library: tanstack-start
|
|
10
|
+
library_version: '1.166.2'
|
|
11
|
+
requires:
|
|
12
|
+
- start-core
|
|
13
|
+
sources:
|
|
14
|
+
- TanStack/router:docs/start/framework/react/guide/hosting.md
|
|
15
|
+
- TanStack/router:docs/start/framework/react/guide/selective-ssr.md
|
|
16
|
+
- TanStack/router:docs/start/framework/react/guide/static-prerendering.md
|
|
17
|
+
- TanStack/router:docs/start/framework/react/guide/seo.md
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# Deployment and Rendering
|
|
21
|
+
|
|
22
|
+
TanStack Start deploys to any hosting provider via Vite and Nitro. This skill covers hosting setup, SSR configuration, prerendering, and SEO.
|
|
23
|
+
|
|
24
|
+
## Hosting Providers
|
|
25
|
+
|
|
26
|
+
### Cloudflare Workers
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pnpm add -D @cloudflare/vite-plugin wrangler
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
// vite.config.ts
|
|
34
|
+
import { defineConfig } from 'vite'
|
|
35
|
+
import { tanstackStart } from '@benjavicente/react-start/plugin/vite'
|
|
36
|
+
import { cloudflare } from '@cloudflare/vite-plugin'
|
|
37
|
+
import viteReact from '@vitejs/plugin-react'
|
|
38
|
+
|
|
39
|
+
export default defineConfig({
|
|
40
|
+
plugins: [
|
|
41
|
+
cloudflare({ viteEnvironment: { name: 'ssr' } }),
|
|
42
|
+
tanstackStart(),
|
|
43
|
+
viteReact(),
|
|
44
|
+
],
|
|
45
|
+
})
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```jsonc
|
|
49
|
+
// wrangler.jsonc
|
|
50
|
+
{
|
|
51
|
+
"name": "my-app",
|
|
52
|
+
"compatibility_date": "2025-09-02",
|
|
53
|
+
"compatibility_flags": ["nodejs_compat"],
|
|
54
|
+
"main": "@benjavicente/react-start/server-entry",
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Deploy: `npx wrangler login && pnpm run deploy`
|
|
59
|
+
|
|
60
|
+
### Netlify
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pnpm add -D @netlify/vite-plugin-tanstack-start
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
// vite.config.ts
|
|
68
|
+
import { defineConfig } from 'vite'
|
|
69
|
+
import { tanstackStart } from '@benjavicente/react-start/plugin/vite'
|
|
70
|
+
import netlify from '@netlify/vite-plugin-tanstack-start'
|
|
71
|
+
import viteReact from '@vitejs/plugin-react'
|
|
72
|
+
|
|
73
|
+
export default defineConfig({
|
|
74
|
+
plugins: [tanstackStart(), netlify(), viteReact()],
|
|
75
|
+
})
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Deploy: `npx netlify deploy`
|
|
79
|
+
|
|
80
|
+
### Nitro (Vercel, Railway, Node.js, Docker)
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npm install nitro@npm:nitro-nightly@latest
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
// vite.config.ts
|
|
88
|
+
import { defineConfig } from 'vite'
|
|
89
|
+
import { tanstackStart } from '@benjavicente/react-start/plugin/vite'
|
|
90
|
+
import { nitro } from 'nitro/vite'
|
|
91
|
+
import viteReact from '@vitejs/plugin-react'
|
|
92
|
+
|
|
93
|
+
export default defineConfig({
|
|
94
|
+
plugins: [tanstackStart(), nitro(), viteReact()],
|
|
95
|
+
})
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Build and start: `npm run build && node .output/server/index.mjs`
|
|
99
|
+
|
|
100
|
+
### Bun
|
|
101
|
+
|
|
102
|
+
Bun deployment requires React 19. For React 18, use Node.js deployment.
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
// vite.config.ts — add bun preset to nitro
|
|
106
|
+
plugins: [tanstackStart(), nitro({ preset: 'bun' }), viteReact()]
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Selective SSR
|
|
110
|
+
|
|
111
|
+
Control SSR per route with the `ssr` property.
|
|
112
|
+
|
|
113
|
+
### `ssr: true` (default)
|
|
114
|
+
|
|
115
|
+
Runs `beforeLoad` and `loader` on server, renders component on server:
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
119
|
+
ssr: true, // default
|
|
120
|
+
loader: () => fetchPost(), // runs on server during SSR
|
|
121
|
+
component: PostPage, // rendered on server
|
|
122
|
+
})
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### `ssr: false`
|
|
126
|
+
|
|
127
|
+
Disables server execution of `beforeLoad`/`loader` and server rendering:
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
export const Route = createFileRoute('/dashboard')({
|
|
131
|
+
ssr: false,
|
|
132
|
+
loader: () => fetchDashboard(), // runs on client only
|
|
133
|
+
component: DashboardPage, // rendered on client only
|
|
134
|
+
})
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### `ssr: 'data-only'`
|
|
138
|
+
|
|
139
|
+
Runs `beforeLoad`/`loader` on server but renders component on client only:
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
export const Route = createFileRoute('/canvas')({
|
|
143
|
+
ssr: 'data-only',
|
|
144
|
+
loader: () => fetchCanvasData(), // runs on server
|
|
145
|
+
component: CanvasPage, // rendered on client only
|
|
146
|
+
})
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Functional Form
|
|
150
|
+
|
|
151
|
+
Decide SSR at runtime based on params/search:
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
export const Route = createFileRoute('/docs/$docType/$docId')({
|
|
155
|
+
ssr: ({ params }) => {
|
|
156
|
+
if (params.status === 'success' && params.value.docType === 'sheet') {
|
|
157
|
+
return false
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### SSR Inheritance
|
|
164
|
+
|
|
165
|
+
Children inherit parent SSR config and can only be MORE restrictive:
|
|
166
|
+
|
|
167
|
+
- `true` → `data-only` or `false` (allowed)
|
|
168
|
+
- `false` → `true` (NOT allowed — parent `false` wins)
|
|
169
|
+
|
|
170
|
+
### Default SSR
|
|
171
|
+
|
|
172
|
+
Change the default for all routes in `src/start.ts`:
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
import { createStart } from '@benjavicente/react-start'
|
|
176
|
+
|
|
177
|
+
export const startInstance = createStart(() => ({
|
|
178
|
+
defaultSsr: false,
|
|
179
|
+
}))
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Static Prerendering
|
|
183
|
+
|
|
184
|
+
Generate static HTML at build time:
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
// vite.config.ts
|
|
188
|
+
tanstackStart({
|
|
189
|
+
prerender: {
|
|
190
|
+
enabled: true,
|
|
191
|
+
crawlLinks: true,
|
|
192
|
+
concurrency: 14,
|
|
193
|
+
failOnError: true,
|
|
194
|
+
},
|
|
195
|
+
})
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Static routes are auto-discovered. Dynamic routes (e.g. `/users/$userId`) require `crawlLinks` or explicit `pages` config.
|
|
199
|
+
|
|
200
|
+
## SEO and Head Management
|
|
201
|
+
|
|
202
|
+
### Basic Meta Tags
|
|
203
|
+
|
|
204
|
+
```tsx
|
|
205
|
+
export const Route = createFileRoute('/')({
|
|
206
|
+
head: () => ({
|
|
207
|
+
meta: [
|
|
208
|
+
{ title: 'My App - Home' },
|
|
209
|
+
{ name: 'description', content: 'Welcome to My App' },
|
|
210
|
+
],
|
|
211
|
+
}),
|
|
212
|
+
})
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Dynamic Meta from Loader Data
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
219
|
+
loader: async ({ params }) => fetchPost(params.postId),
|
|
220
|
+
head: ({ loaderData }) => ({
|
|
221
|
+
meta: [
|
|
222
|
+
{ title: loaderData.title },
|
|
223
|
+
{ name: 'description', content: loaderData.excerpt },
|
|
224
|
+
{ property: 'og:title', content: loaderData.title },
|
|
225
|
+
{ property: 'og:image', content: loaderData.coverImage },
|
|
226
|
+
],
|
|
227
|
+
}),
|
|
228
|
+
})
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Structured Data (JSON-LD)
|
|
232
|
+
|
|
233
|
+
```tsx
|
|
234
|
+
head: ({ loaderData }) => ({
|
|
235
|
+
scripts: [
|
|
236
|
+
{
|
|
237
|
+
type: 'application/ld+json',
|
|
238
|
+
children: JSON.stringify({
|
|
239
|
+
'@context': 'https://schema.org',
|
|
240
|
+
'@type': 'Article',
|
|
241
|
+
headline: loaderData.title,
|
|
242
|
+
}),
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
})
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Dynamic Sitemap via Server Route
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
// src/routes/sitemap[.]xml.ts
|
|
252
|
+
export const Route = createFileRoute('/sitemap.xml')({
|
|
253
|
+
server: {
|
|
254
|
+
handlers: {
|
|
255
|
+
GET: async () => {
|
|
256
|
+
const posts = await fetchAllPosts()
|
|
257
|
+
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
|
|
258
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
259
|
+
${posts.map((p) => `<url><loc>https://myapp.com/posts/${p.id}</loc></url>`).join('')}
|
|
260
|
+
</urlset>`
|
|
261
|
+
return new Response(sitemap, {
|
|
262
|
+
headers: { 'Content-Type': 'application/xml' },
|
|
263
|
+
})
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
})
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Common Mistakes
|
|
271
|
+
|
|
272
|
+
### 1. HIGH: Missing nodejs_compat flag for Cloudflare Workers
|
|
273
|
+
|
|
274
|
+
```jsonc
|
|
275
|
+
// WRONG — Node.js APIs fail at runtime
|
|
276
|
+
{ "compatibility_flags": [] }
|
|
277
|
+
|
|
278
|
+
// CORRECT
|
|
279
|
+
{ "compatibility_flags": ["nodejs_compat"] }
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### 2. MEDIUM: Bun deployment with React 18
|
|
283
|
+
|
|
284
|
+
Bun-specific deployment only works with React 19. Use Node.js deployment for React 18.
|
|
285
|
+
|
|
286
|
+
### 3. MEDIUM: Child route loosening parent SSR config
|
|
287
|
+
|
|
288
|
+
```tsx
|
|
289
|
+
// Parent sets ssr: false
|
|
290
|
+
// WRONG — child cannot upgrade to ssr: true
|
|
291
|
+
const parentRoute = createFileRoute('/dashboard')({ ssr: false })
|
|
292
|
+
const childRoute = createFileRoute('/dashboard/stats')({
|
|
293
|
+
ssr: true, // IGNORED — parent false wins
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
// CORRECT — children can only be MORE restrictive
|
|
297
|
+
const parentRoute = createFileRoute('/dashboard')({ ssr: 'data-only' })
|
|
298
|
+
const childRoute = createFileRoute('/dashboard/stats')({
|
|
299
|
+
ssr: false, // OK — more restrictive than parent
|
|
300
|
+
})
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## Cross-References
|
|
304
|
+
|
|
305
|
+
- [start-core/server-routes](../server-routes/SKILL.md) — API endpoints for sitemaps, robots.txt
|
|
306
|
+
- [start-core/execution-model](../execution-model/SKILL.md) — SSR affects where code runs
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: start-core/execution-model
|
|
3
|
+
description: >-
|
|
4
|
+
Isomorphic-by-default principle, environment boundary functions
|
|
5
|
+
(createServerFn, createServerOnlyFn, createClientOnlyFn,
|
|
6
|
+
createIsomorphicFn), ClientOnly component, useHydrated hook,
|
|
7
|
+
import protection, dead code elimination, environment variable
|
|
8
|
+
safety (VITE_ prefix, process.env).
|
|
9
|
+
type: sub-skill
|
|
10
|
+
library: tanstack-start
|
|
11
|
+
library_version: '1.166.2'
|
|
12
|
+
requires:
|
|
13
|
+
- start-core
|
|
14
|
+
sources:
|
|
15
|
+
- TanStack/router:docs/start/framework/react/guide/execution-model.md
|
|
16
|
+
- TanStack/router:docs/start/framework/react/guide/environment-variables.md
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Execution Model
|
|
20
|
+
|
|
21
|
+
Understanding where code runs is fundamental to TanStack Start. This skill covers the isomorphic execution model and how to control environment boundaries.
|
|
22
|
+
|
|
23
|
+
> **CRITICAL**: ALL code in TanStack Start is isomorphic by default — it runs in BOTH server and client bundles. Route loaders run on BOTH server (during SSR) AND client (during navigation). Server-only operations MUST use `createServerFn`.
|
|
24
|
+
> **CRITICAL**: Module-level `process.env` access runs in both environments. Secret values leak into the client bundle. Access secrets ONLY inside `createServerFn` or `createServerOnlyFn`.
|
|
25
|
+
> **CRITICAL**: `VITE_` prefixed environment variables are exposed to the client bundle. Server secrets must NOT have the `VITE_` prefix.
|
|
26
|
+
|
|
27
|
+
## Execution Control APIs
|
|
28
|
+
|
|
29
|
+
| API | Use Case | Client Behavior | Server Behavior |
|
|
30
|
+
| ------------------------ | ------------------------- | ------------------------- | --------------------- |
|
|
31
|
+
| `createServerFn()` | RPC calls, data mutations | Network request to server | Direct execution |
|
|
32
|
+
| `createServerOnlyFn(fn)` | Utility functions | Throws error | Direct execution |
|
|
33
|
+
| `createClientOnlyFn(fn)` | Browser utilities | Direct execution | Throws error |
|
|
34
|
+
| `createIsomorphicFn()` | Different impl per env | Uses `.client()` impl | Uses `.server()` impl |
|
|
35
|
+
| `<ClientOnly>` | Browser-only components | Renders children | Renders fallback |
|
|
36
|
+
| `useHydrated()` | Hydration-dependent logic | `true` after hydration | `false` |
|
|
37
|
+
|
|
38
|
+
## Server-Only Execution
|
|
39
|
+
|
|
40
|
+
### createServerFn (RPC pattern)
|
|
41
|
+
|
|
42
|
+
The primary way to run server-only code. On the client, calls become fetch requests:
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
// Use @tanstack/<framework>-start for your framework (react, solid, vue)
|
|
46
|
+
import { createServerFn } from '@benjavicente/react-start'
|
|
47
|
+
|
|
48
|
+
const fetchUser = createServerFn().handler(async () => {
|
|
49
|
+
const secret = process.env.API_SECRET // safe — server only
|
|
50
|
+
return await db.users.find()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// Client calls this via network request
|
|
54
|
+
const user = await fetchUser()
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### createServerOnlyFn (throws on client)
|
|
58
|
+
|
|
59
|
+
For utility functions that must never run on client:
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
// Use @tanstack/<framework>-start for your framework (react, solid, vue)
|
|
63
|
+
import { createServerOnlyFn } from '@benjavicente/react-start'
|
|
64
|
+
|
|
65
|
+
const getSecret = createServerOnlyFn(() => process.env.DATABASE_URL)
|
|
66
|
+
|
|
67
|
+
// Server: returns the value
|
|
68
|
+
// Client: THROWS an error
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Client-Only Execution
|
|
72
|
+
|
|
73
|
+
### createClientOnlyFn
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
// Use @tanstack/<framework>-start for your framework (react, solid, vue)
|
|
77
|
+
import { createClientOnlyFn } from '@benjavicente/react-start'
|
|
78
|
+
|
|
79
|
+
const saveToStorage = createClientOnlyFn((key: string, value: string) => {
|
|
80
|
+
localStorage.setItem(key, value)
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### ClientOnly Component
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
// Use @tanstack/<framework>-router for your framework (react, solid, vue)
|
|
88
|
+
import { ClientOnly } from '@benjavicente/react-router'
|
|
89
|
+
|
|
90
|
+
function Analytics() {
|
|
91
|
+
return (
|
|
92
|
+
<ClientOnly fallback={null}>
|
|
93
|
+
<GoogleAnalyticsScript />
|
|
94
|
+
</ClientOnly>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### useHydrated Hook
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
// Use @tanstack/<framework>-router for your framework (react, solid, vue)
|
|
103
|
+
import { useHydrated } from '@benjavicente/react-router'
|
|
104
|
+
|
|
105
|
+
function TimeZoneDisplay() {
|
|
106
|
+
const hydrated = useHydrated()
|
|
107
|
+
const timeZone = hydrated
|
|
108
|
+
? Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
109
|
+
: 'UTC'
|
|
110
|
+
|
|
111
|
+
return <div>Your timezone: {timeZone}</div>
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Behavior: SSR → `false`, first client render → `false`, after hydration → `true` (stays `true`).
|
|
116
|
+
|
|
117
|
+
## Environment-Specific Implementations
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
// Use @tanstack/<framework>-start for your framework (react, solid, vue)
|
|
121
|
+
import { createIsomorphicFn } from '@benjavicente/react-start'
|
|
122
|
+
|
|
123
|
+
const getDeviceInfo = createIsomorphicFn()
|
|
124
|
+
.server(() => ({ type: 'server', platform: process.platform }))
|
|
125
|
+
.client(() => ({ type: 'client', userAgent: navigator.userAgent }))
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Environment Variables
|
|
129
|
+
|
|
130
|
+
### Server-Side (inside createServerFn)
|
|
131
|
+
|
|
132
|
+
Access any variable via `process.env`:
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
const connectDb = createServerFn().handler(async () => {
|
|
136
|
+
const url = process.env.DATABASE_URL // no prefix needed
|
|
137
|
+
return createConnection(url)
|
|
138
|
+
})
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Client-Side (components)
|
|
142
|
+
|
|
143
|
+
Only `VITE_` prefixed variables are available:
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
// Framework-specific component type (React.ReactNode, JSX.Element, etc.)
|
|
147
|
+
function ApiProvider({ children }: { children: React.ReactNode }) {
|
|
148
|
+
const apiUrl = import.meta.env.VITE_API_URL // available
|
|
149
|
+
// import.meta.env.DATABASE_URL → undefined (security)
|
|
150
|
+
return (
|
|
151
|
+
<ApiContext.Provider value={{ apiUrl }}>{children}</ApiContext.Provider>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Runtime Client Variables
|
|
157
|
+
|
|
158
|
+
If you need server-side variables on the client without `VITE_` prefix, pass them through a server function:
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
const getRuntimeVar = createServerFn({ method: 'GET' }).handler(() => {
|
|
162
|
+
return process.env.MY_RUNTIME_VAR
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
export const Route = createFileRoute('/')({
|
|
166
|
+
loader: async () => {
|
|
167
|
+
const foo = await getRuntimeVar()
|
|
168
|
+
return { foo }
|
|
169
|
+
},
|
|
170
|
+
component: () => {
|
|
171
|
+
const { foo } = Route.useLoaderData()
|
|
172
|
+
return <div>{foo}</div>
|
|
173
|
+
},
|
|
174
|
+
})
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Type Safety for Environment Variables
|
|
178
|
+
|
|
179
|
+
```tsx
|
|
180
|
+
// src/env.d.ts
|
|
181
|
+
/// <reference types="vite/client" />
|
|
182
|
+
|
|
183
|
+
interface ImportMetaEnv {
|
|
184
|
+
readonly VITE_APP_NAME: string
|
|
185
|
+
readonly VITE_API_URL: string
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
interface ImportMeta {
|
|
189
|
+
readonly env: ImportMetaEnv
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
declare global {
|
|
193
|
+
namespace NodeJS {
|
|
194
|
+
interface ProcessEnv {
|
|
195
|
+
readonly DATABASE_URL: string
|
|
196
|
+
readonly JWT_SECRET: string
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export {}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Common Mistakes
|
|
205
|
+
|
|
206
|
+
### 1. CRITICAL: Assuming loaders are server-only
|
|
207
|
+
|
|
208
|
+
```tsx
|
|
209
|
+
// WRONG — loader runs on BOTH server and client
|
|
210
|
+
export const Route = createFileRoute('/dashboard')({
|
|
211
|
+
loader: async () => {
|
|
212
|
+
const secret = process.env.API_SECRET // LEAKED to client
|
|
213
|
+
return fetch(`https://api.example.com/data`, {
|
|
214
|
+
headers: { Authorization: secret },
|
|
215
|
+
})
|
|
216
|
+
},
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
// CORRECT — use createServerFn
|
|
220
|
+
const getData = createServerFn({ method: 'GET' }).handler(async () => {
|
|
221
|
+
const secret = process.env.API_SECRET
|
|
222
|
+
return fetch(`https://api.example.com/data`, {
|
|
223
|
+
headers: { Authorization: secret },
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
export const Route = createFileRoute('/dashboard')({
|
|
228
|
+
loader: () => getData(),
|
|
229
|
+
})
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### 2. CRITICAL: Exposing secrets via module-level process.env
|
|
233
|
+
|
|
234
|
+
```tsx
|
|
235
|
+
// WRONG — runs in both environments, value in client bundle
|
|
236
|
+
const apiKey = process.env.SECRET_KEY
|
|
237
|
+
export function fetchData() {
|
|
238
|
+
/* uses apiKey */
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// CORRECT — access inside server function only
|
|
242
|
+
const fetchData = createServerFn({ method: 'GET' }).handler(async () => {
|
|
243
|
+
const apiKey = process.env.SECRET_KEY
|
|
244
|
+
return fetch(url, { headers: { Authorization: apiKey } })
|
|
245
|
+
})
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### 3. CRITICAL: Using VITE\_ prefix for server secrets
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
# WRONG — exposed to client bundle
|
|
252
|
+
VITE_SECRET_API_KEY=sk_live_xxx
|
|
253
|
+
|
|
254
|
+
# CORRECT — no prefix for server secrets
|
|
255
|
+
SECRET_API_KEY=sk_live_xxx
|
|
256
|
+
|
|
257
|
+
# CORRECT — VITE_ only for public client values
|
|
258
|
+
VITE_APP_NAME=My App
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### 4. HIGH: Hydration mismatches
|
|
262
|
+
|
|
263
|
+
```tsx
|
|
264
|
+
// WRONG — different content server vs client
|
|
265
|
+
function CurrentTime() {
|
|
266
|
+
return <div>{new Date().toLocaleString()}</div>
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// CORRECT — consistent rendering
|
|
270
|
+
function CurrentTime() {
|
|
271
|
+
const [time, setTime] = useState<string>()
|
|
272
|
+
useEffect(() => {
|
|
273
|
+
setTime(new Date().toLocaleString())
|
|
274
|
+
}, [])
|
|
275
|
+
return <div>{time || 'Loading...'}</div>
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Architecture Decision Framework
|
|
280
|
+
|
|
281
|
+
**Server-Only** (`createServerFn` / `createServerOnlyFn`):
|
|
282
|
+
|
|
283
|
+
- Sensitive data (env vars, secrets)
|
|
284
|
+
- Database connections, file system
|
|
285
|
+
- External API keys
|
|
286
|
+
|
|
287
|
+
**Client-Only** (`createClientOnlyFn` / `<ClientOnly>`):
|
|
288
|
+
|
|
289
|
+
- DOM manipulation, browser APIs
|
|
290
|
+
- localStorage, geolocation
|
|
291
|
+
- Analytics/tracking
|
|
292
|
+
|
|
293
|
+
**Isomorphic** (default / `createIsomorphicFn`):
|
|
294
|
+
|
|
295
|
+
- Data formatting, business logic
|
|
296
|
+
- Shared utilities
|
|
297
|
+
- Route loaders (they're isomorphic by nature)
|
|
298
|
+
|
|
299
|
+
## Cross-References
|
|
300
|
+
|
|
301
|
+
- [start-core/server-functions](../server-functions/SKILL.md) — the primary server boundary
|
|
302
|
+
- [start-core/deployment](../deployment/SKILL.md) — deployment target affects execution
|