@tanstack/cta-framework-react-cra 0.43.1 → 0.44.1

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.
Files changed (52) hide show
  1. package/add-ons/apollo-client/README.md +150 -0
  2. package/add-ons/apollo-client/assets/src/routes/demo.apollo-client.tsx +75 -0
  3. package/add-ons/apollo-client/info.json +19 -0
  4. package/add-ons/apollo-client/package.json +8 -0
  5. package/add-ons/apollo-client/small-logo.svg +11 -0
  6. package/add-ons/convex/package.json +2 -2
  7. package/add-ons/db/assets/src/hooks/demo.useChat.ts +1 -1
  8. package/add-ons/db/assets/src/routes/demo/db-chat-api.ts +4 -1
  9. package/add-ons/db/package.json +1 -1
  10. package/add-ons/mcp/package.json +1 -1
  11. package/add-ons/neon/package.json +1 -1
  12. package/add-ons/oRPC/assets/src/routes/api.$.ts +1 -1
  13. package/add-ons/oRPC/package.json +7 -7
  14. package/add-ons/sentry/assets/instrument.server.mjs +16 -9
  15. package/add-ons/sentry/assets/src/routes/demo/sentry.testing.tsx +42 -2
  16. package/add-ons/sentry/package.json +1 -1
  17. package/add-ons/shadcn/package.json +1 -1
  18. package/add-ons/start/assets/src/router.tsx.ejs +34 -10
  19. package/add-ons/start/package.json +2 -2
  20. package/add-ons/store/package.json +3 -3
  21. package/add-ons/storybook/package.json +2 -2
  22. package/add-ons/tanstack-query/info.json +1 -1
  23. package/examples/tanchat/assets/src/hooks/useAudioRecorder.ts +85 -0
  24. package/examples/tanchat/assets/src/hooks/useTTS.ts +78 -0
  25. package/examples/tanchat/assets/src/lib/model-selection.ts +78 -0
  26. package/examples/tanchat/assets/src/lib/vendor-capabilities.ts +55 -0
  27. package/examples/tanchat/assets/src/routes/demo/api.available-providers.ts +35 -0
  28. package/examples/tanchat/assets/src/routes/demo/api.image.ts +74 -0
  29. package/examples/tanchat/assets/src/routes/demo/api.structured.ts +168 -0
  30. package/examples/tanchat/assets/src/routes/demo/{api.tanchat.ts.ejs → api.tanchat.ts} +24 -7
  31. package/examples/tanchat/assets/src/routes/demo/api.transcription.ts +89 -0
  32. package/examples/tanchat/assets/src/routes/demo/api.tts.ts +81 -0
  33. package/examples/tanchat/assets/src/routes/demo/image.tsx +257 -0
  34. package/examples/tanchat/assets/src/routes/demo/structured.tsx +460 -0
  35. package/examples/tanchat/assets/src/routes/demo/tanchat.css +14 -7
  36. package/examples/tanchat/assets/src/routes/demo/tanchat.tsx +301 -81
  37. package/examples/tanchat/info.json +10 -7
  38. package/examples/tanchat/package.json +8 -5
  39. package/package.json +2 -2
  40. package/project/base/README.md.ejs +1 -1
  41. package/project/base/src/routes/__root.tsx.ejs +14 -6
  42. package/tests/react-cra.test.ts +14 -0
  43. package/tests/snapshots/react-cra/cr-js-form-npm.json +1 -1
  44. package/tests/snapshots/react-cra/cr-js-npm.json +1 -1
  45. package/tests/snapshots/react-cra/cr-ts-npm.json +1 -1
  46. package/tests/snapshots/react-cra/cr-ts-start-apollo-client-npm.json +31 -0
  47. package/tests/snapshots/react-cra/cr-ts-start-npm.json +3 -3
  48. package/tests/snapshots/react-cra/cr-ts-start-tanstack-query-npm.json +3 -3
  49. package/tests/snapshots/react-cra/fr-ts-biome-npm.json +2 -2
  50. package/tests/snapshots/react-cra/fr-ts-npm.json +1 -1
  51. package/tests/snapshots/react-cra/fr-ts-tw-npm.json +1 -1
  52. package/toolchains/biome/assets/biome.json +2 -1
@@ -1,10 +1,28 @@
1
1
  import { useEffect, useRef, useState } from 'react'
2
2
  import { createFileRoute } from '@tanstack/react-router'
3
- import { Send, Square } from 'lucide-react'
3
+ import {
4
+ Send,
5
+ Square,
6
+ Mic,
7
+ MicOff,
8
+ Volume2,
9
+ VolumeX,
10
+ Loader2,
11
+ } from 'lucide-react'
4
12
  import { Streamdown } from 'streamdown'
5
13
 
6
14
  import { useGuitarRecommendationChat } from '@/lib/example.ai-hook'
7
15
  import type { ChatMessages } from '@/lib/example.ai-hook'
16
+ import {
17
+ MODEL_OPTIONS,
18
+ getAvailableModelOptions,
19
+ getStoredModelPreference,
20
+ setStoredModelPreference,
21
+ } from '@/lib/model-selection'
22
+ import type { Provider, ModelOption } from '@/lib/model-selection'
23
+ import { hasCapability } from '@/lib/vendor-capabilities'
24
+ import { useAudioRecorder } from '@/hooks/useAudioRecorder'
25
+ import { useTTS } from '@/hooks/useTTS'
8
26
 
9
27
  import GuitarRecommendation from '@/components/example-GuitarRecommendation'
10
28
 
@@ -35,7 +53,19 @@ function ChattingLayout({ children }: { children: React.ReactNode }) {
35
53
  )
36
54
  }
37
55
 
38
- function Messages({ messages }: { messages: ChatMessages }) {
56
+ function Messages({
57
+ messages,
58
+ playingId,
59
+ onSpeak,
60
+ onStopSpeak,
61
+ canPlayTTS,
62
+ }: {
63
+ messages: ChatMessages
64
+ playingId: string | null
65
+ onSpeak: (text: string, id: string) => void
66
+ onStopSpeak: () => void
67
+ canPlayTTS: boolean
68
+ }) {
39
69
  const messagesContainerRef = useRef<HTMLDivElement>(null)
40
70
 
41
71
  useEffect(() => {
@@ -49,80 +79,245 @@ function Messages({ messages }: { messages: ChatMessages }) {
49
79
  return null
50
80
  }
51
81
 
82
+ // Extract text content from message parts
83
+ const getTextContent = (
84
+ parts: ChatMessages[number]['parts'],
85
+ ): string | null => {
86
+ for (const part of parts) {
87
+ if (part.type === 'text' && part.content) {
88
+ return part.content
89
+ }
90
+ }
91
+ return null
92
+ }
93
+
52
94
  return (
53
95
  <div
54
96
  ref={messagesContainerRef}
55
97
  className="flex-1 overflow-y-auto pb-4 min-h-0"
56
98
  >
57
99
  <div className="max-w-3xl mx-auto w-full px-4">
58
- {messages.map((message) => (
59
- <div
60
- key={message.id}
61
- className={`p-4 ${
62
- message.role === 'assistant'
63
- ? 'bg-linear-to-r from-orange-500/5 to-red-600/5'
64
- : 'bg-transparent'
65
- }`}
66
- >
67
- <div className="flex items-start gap-4 max-w-3xl mx-auto w-full">
68
- {message.role === 'assistant' ? (
69
- <div className="w-8 h-8 rounded-lg bg-linear-to-r from-orange-500 to-red-600 mt-2 flex items-center justify-center text-sm font-medium text-white flex-shrink-0">
70
- AI
71
- </div>
72
- ) : (
73
- <div className="w-8 h-8 rounded-lg bg-gray-700 flex items-center justify-center text-sm font-medium text-white flex-shrink-0">
74
- Y
100
+ {messages.map((message) => {
101
+ const textContent = getTextContent(message.parts)
102
+ const isPlaying = playingId === message.id
103
+
104
+ return (
105
+ <div
106
+ key={message.id}
107
+ className={`p-4 ${
108
+ message.role === 'assistant'
109
+ ? 'bg-linear-to-r from-orange-500/5 to-red-600/5'
110
+ : 'bg-transparent'
111
+ }`}
112
+ >
113
+ <div className="flex items-start gap-4 max-w-3xl mx-auto w-full">
114
+ {message.role === 'assistant' ? (
115
+ <div className="w-8 h-8 rounded-lg bg-linear-to-r from-orange-500 to-red-600 mt-2 flex items-center justify-center text-sm font-medium text-white flex-shrink-0">
116
+ AI
117
+ </div>
118
+ ) : (
119
+ <div className="w-8 h-8 rounded-lg bg-gray-700 flex items-center justify-center text-sm font-medium text-white flex-shrink-0">
120
+ Y
121
+ </div>
122
+ )}
123
+ <div className="flex-1 min-w-0">
124
+ {message.parts.map((part, index) => {
125
+ if (part.type === 'text' && part.content) {
126
+ return (
127
+ <div
128
+ className="flex-1 min-w-0 prose dark:prose-invert max-w-none prose-sm"
129
+ key={index}
130
+ >
131
+ <Streamdown>{part.content}</Streamdown>
132
+ </div>
133
+ )
134
+ }
135
+ // Guitar recommendation card
136
+ if (
137
+ part.type === 'tool-call' &&
138
+ part.name === 'recommendGuitar' &&
139
+ part.output
140
+ ) {
141
+ return (
142
+ <div key={part.id} className="max-w-[80%] mx-auto">
143
+ <GuitarRecommendation id={String(part.output?.id)} />
144
+ </div>
145
+ )
146
+ }
147
+ return null
148
+ })}
75
149
  </div>
76
- )}
77
- <div className="flex-1 min-w-0">
78
- {message.parts.map((part, index) => {
79
- if (part.type === 'text' && part.content) {
80
- return (
81
- <div
82
- className="flex-1 min-w-0 prose dark:prose-invert max-w-none prose-sm"
83
- key={index}
84
- >
85
- <Streamdown>{part.content}</Streamdown>
86
- </div>
87
- )
88
- }
89
- // Guitar recommendation card
90
- if (
91
- part.type === 'tool-call' &&
92
- part.name === 'recommendGuitar' &&
93
- part.output
94
- ) {
95
- return (
96
- <div key={part.id} className="max-w-[80%] mx-auto">
97
- <GuitarRecommendation id={String(part.output?.id)} />
98
- </div>
99
- )
100
- }
101
- return null
102
- })}
150
+ {/* TTS button for assistant messages */}
151
+ {message.role === 'assistant' && textContent && canPlayTTS && (
152
+ <button
153
+ onClick={() =>
154
+ isPlaying
155
+ ? onStopSpeak()
156
+ : onSpeak(textContent, message.id)
157
+ }
158
+ className="flex-shrink-0 p-2 text-gray-400 hover:text-orange-400 transition-colors"
159
+ title={isPlaying ? 'Stop speaking' : 'Read aloud'}
160
+ >
161
+ {isPlaying ? (
162
+ <VolumeX className="w-4 h-4" />
163
+ ) : (
164
+ <Volume2 className="w-4 h-4" />
165
+ )}
166
+ </button>
167
+ )}
103
168
  </div>
104
169
  </div>
105
- </div>
106
- ))}
170
+ )
171
+ })}
107
172
  </div>
108
173
  </div>
109
174
  )
110
175
  }
111
176
 
112
177
  function ChatPage() {
113
- const { messages, sendMessage, isLoading, stop } =
114
- useGuitarRecommendationChat()
115
178
  const [input, setInput] = useState('')
179
+ const [availableProviders, setAvailableProviders] = useState<Provider[]>([])
180
+ const [selectedModel, setSelectedModel] = useState<ModelOption | null>(null)
181
+ const [isCheckingProviders, setIsCheckingProviders] = useState(true)
182
+
183
+ // Audio hooks
184
+ const { isRecording, isTranscribing, startRecording, stopRecording } =
185
+ useAudioRecorder()
186
+ const { playingId, speak, stop: stopTTS } = useTTS()
187
+
188
+ // Fetch available providers on mount
189
+ useEffect(() => {
190
+ fetch('/demo/api/available-providers')
191
+ .then((res) => res.json())
192
+ .then((data) => {
193
+ setAvailableProviders(data.providers)
194
+
195
+ // Set default model based on stored preference or first available
196
+ const storedPref = getStoredModelPreference()
197
+ const availableOptions = getAvailableModelOptions(data.providers)
198
+
199
+ if (
200
+ storedPref &&
201
+ availableOptions.some((o) => o.model === storedPref.model)
202
+ ) {
203
+ setSelectedModel(storedPref)
204
+ } else if (availableOptions.length > 0) {
205
+ setSelectedModel(availableOptions[0])
206
+ }
207
+
208
+ setIsCheckingProviders(false)
209
+ })
210
+ .catch(() => {
211
+ // Fallback to Ollama if can't reach API
212
+ setAvailableProviders(['ollama'])
213
+ const ollamaOption = MODEL_OPTIONS.find((m) => m.provider === 'ollama')
214
+ if (ollamaOption) setSelectedModel(ollamaOption)
215
+ setIsCheckingProviders(false)
216
+ })
217
+ }, [])
218
+
219
+ const availableModelOptions = getAvailableModelOptions(availableProviders)
220
+
221
+ // Check if current provider supports TTS (only OpenAI)
222
+ const canPlayTTS =
223
+ selectedModel && hasCapability(selectedModel.provider, 'tts')
224
+ const canRecordAudio =
225
+ selectedModel && hasCapability(selectedModel.provider, 'transcription')
226
+
227
+ const { messages, sendMessage, isLoading, stop } =
228
+ useGuitarRecommendationChat(
229
+ selectedModel?.provider || 'anthropic',
230
+ selectedModel?.model || 'claude-haiku-4-5',
231
+ )
232
+
233
+ const handleModelChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
234
+ const selected = availableModelOptions.find(
235
+ (m) => `${m.provider}:${m.model}` === e.target.value,
236
+ )
237
+ if (selected) {
238
+ setSelectedModel(selected)
239
+ setStoredModelPreference(selected)
240
+ }
241
+ }
242
+
243
+ const handleMicClick = async () => {
244
+ if (isRecording) {
245
+ const transcribedText = await stopRecording()
246
+ if (transcribedText) {
247
+ setInput((prev) =>
248
+ prev ? `${prev} ${transcribedText}` : transcribedText,
249
+ )
250
+ }
251
+ } else {
252
+ await startRecording()
253
+ }
254
+ }
116
255
 
117
256
  const Layout = messages.length ? ChattingLayout : InitalLayout
118
257
 
258
+ if (isCheckingProviders) {
259
+ return (
260
+ <div className="relative flex h-[calc(100vh-80px)] bg-gray-900 items-center justify-center">
261
+ <Loader2 className="w-8 h-8 text-orange-500 animate-spin" />
262
+ </div>
263
+ )
264
+ }
265
+
266
+ if (availableModelOptions.length === 0) {
267
+ return (
268
+ <div className="relative flex h-[calc(100vh-80px)] bg-gray-900">
269
+ <div className="flex-1 flex items-center justify-center px-4">
270
+ <div className="text-center max-w-xl">
271
+ <h1 className="text-2xl font-bold text-white mb-4">
272
+ No AI Providers Available
273
+ </h1>
274
+ <p className="text-gray-400 mb-4">
275
+ Please configure at least one AI provider in your{' '}
276
+ <code className="text-orange-400">.env.local</code> file, or
277
+ ensure Ollama is running locally.
278
+ </p>
279
+ </div>
280
+ </div>
281
+ </div>
282
+ )
283
+ }
284
+
119
285
  return (
120
286
  <div className="relative flex h-[calc(100vh-80px)] bg-gray-900">
121
287
  <div className="flex-1 flex flex-col min-h-0">
122
- <Messages messages={messages} />
288
+ <Messages
289
+ messages={messages}
290
+ playingId={playingId}
291
+ onSpeak={speak}
292
+ onStopSpeak={stopTTS}
293
+ canPlayTTS={!!canPlayTTS}
294
+ />
123
295
 
124
296
  <Layout>
125
297
  <div className="space-y-3">
298
+ {/* Model Selector */}
299
+ <div className="flex justify-center">
300
+ <select
301
+ value={
302
+ selectedModel
303
+ ? `${selectedModel.provider}:${selectedModel.model}`
304
+ : ''
305
+ }
306
+ onChange={handleModelChange}
307
+ disabled={isLoading}
308
+ className="rounded-lg border border-orange-500/20 bg-gray-800/50 px-3 py-1.5 text-xs text-gray-300 focus:outline-none focus:ring-2 focus:ring-orange-500/50"
309
+ >
310
+ {availableModelOptions.map((option) => (
311
+ <option
312
+ key={`${option.provider}:${option.model}`}
313
+ value={`${option.provider}:${option.model}`}
314
+ >
315
+ {option.label}
316
+ </option>
317
+ ))}
318
+ </select>
319
+ </div>
320
+
126
321
  {isLoading && (
127
322
  <div className="flex items-center justify-center">
128
323
  <button
@@ -143,36 +338,61 @@ function ChatPage() {
143
338
  }
144
339
  }}
145
340
  >
146
- <div className="relative max-w-xl mx-auto">
147
- <textarea
148
- value={input}
149
- onChange={(e) => setInput(e.target.value)}
150
- placeholder="Type something clever..."
151
- className="w-full rounded-lg border border-orange-500/20 bg-gray-800/50 pl-4 pr-12 py-3 text-sm text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-orange-500/50 focus:border-transparent resize-none overflow-hidden shadow-lg"
152
- rows={1}
153
- style={{ minHeight: '44px', maxHeight: '200px' }}
154
- disabled={isLoading}
155
- onInput={(e) => {
156
- const target = e.target as HTMLTextAreaElement
157
- target.style.height = 'auto'
158
- target.style.height =
159
- Math.min(target.scrollHeight, 200) + 'px'
160
- }}
161
- onKeyDown={(e) => {
162
- if (e.key === 'Enter' && !e.shiftKey && input.trim()) {
163
- e.preventDefault()
164
- sendMessage(input)
165
- setInput('')
166
- }
167
- }}
168
- />
169
- <button
170
- type="submit"
171
- disabled={!input.trim() || isLoading}
172
- className="absolute right-2 top-1/2 -translate-y-1/2 p-2 text-orange-500 hover:text-orange-400 disabled:text-gray-500 transition-colors focus:outline-none"
173
- >
174
- <Send className="w-4 h-4" />
175
- </button>
341
+ <div className="relative max-w-xl mx-auto flex items-center gap-2">
342
+ {/* Mic button - only show if OpenAI is available */}
343
+ {canRecordAudio && (
344
+ <button
345
+ type="button"
346
+ onClick={handleMicClick}
347
+ disabled={isLoading || isTranscribing}
348
+ className={`p-3 rounded-lg transition-colors ${
349
+ isRecording
350
+ ? 'bg-red-600 hover:bg-red-700 text-white'
351
+ : 'bg-gray-800/50 text-gray-400 hover:text-orange-400 border border-orange-500/20'
352
+ } disabled:opacity-50`}
353
+ title={isRecording ? 'Stop recording' : 'Start recording'}
354
+ >
355
+ {isTranscribing ? (
356
+ <Loader2 className="w-4 h-4 animate-spin" />
357
+ ) : isRecording ? (
358
+ <MicOff className="w-4 h-4" />
359
+ ) : (
360
+ <Mic className="w-4 h-4" />
361
+ )}
362
+ </button>
363
+ )}
364
+
365
+ <div className="relative flex-1">
366
+ <textarea
367
+ value={input}
368
+ onChange={(e) => setInput(e.target.value)}
369
+ placeholder="Type something clever..."
370
+ className="w-full rounded-lg border border-orange-500/20 bg-gray-800/50 pl-4 pr-12 py-3 text-sm text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-orange-500/50 focus:border-transparent resize-none overflow-hidden shadow-lg"
371
+ rows={1}
372
+ style={{ minHeight: '44px', maxHeight: '200px' }}
373
+ disabled={isLoading}
374
+ onInput={(e) => {
375
+ const target = e.target as HTMLTextAreaElement
376
+ target.style.height = 'auto'
377
+ target.style.height =
378
+ Math.min(target.scrollHeight, 200) + 'px'
379
+ }}
380
+ onKeyDown={(e) => {
381
+ if (e.key === 'Enter' && !e.shiftKey && input.trim()) {
382
+ e.preventDefault()
383
+ sendMessage(input)
384
+ setInput('')
385
+ }
386
+ }}
387
+ />
388
+ <button
389
+ type="submit"
390
+ disabled={!input.trim() || isLoading}
391
+ className="absolute right-2 top-1/2 -translate-y-1/2 p-2 text-orange-500 hover:text-orange-400 disabled:text-gray-500 transition-colors focus:outline-none"
392
+ >
393
+ <Send className="w-4 h-4" />
394
+ </button>
395
+ </div>
176
396
  </div>
177
397
  </form>
178
398
  </div>
@@ -15,15 +15,18 @@
15
15
  "jsName": "ChatDemo"
16
16
  },
17
17
  {
18
- "icon": "Guitar",
19
- "url": "/example/guitars",
20
- "name": "Guitar Demo",
21
- "path": "src/routes/example.guitars.tsx",
22
- "jsName": "GuitarDemo"
18
+ "icon": "ImageIcon",
19
+ "url": "/demo/image",
20
+ "name": "Generate Image",
21
+ "path": "src/routes/demo/image.tsx",
22
+ "jsName": "ImageDemo"
23
23
  },
24
24
  {
25
- "path": "src/routes/example.guitars/$guitarId.tsx",
26
- "jsName": "GuitarById"
25
+ "icon": "ChefHat",
26
+ "url": "/demo/structured",
27
+ "name": "Structured Output",
28
+ "path": "src/routes/demo/structured.tsx",
29
+ "jsName": "StructuredDemo"
27
30
  }
28
31
  ],
29
32
  "integrations": [
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "dependencies": {
3
- "@tanstack/ai": "^0.0.1",
4
- "@tanstack/ai-anthropic": "^0.0.1",
5
- "@tanstack/ai-client": "^0.0.1",
6
- "@tanstack/ai-react": "^0.0.1",
7
- "@tanstack/react-ai-devtools": "^0.0.1",
3
+ "@tanstack/ai": "latest",
4
+ "@tanstack/ai-anthropic": "latest",
5
+ "@tanstack/ai-client": "latest",
6
+ "@tanstack/ai-gemini": "latest",
7
+ "@tanstack/ai-ollama": "latest",
8
+ "@tanstack/ai-openai": "latest",
9
+ "@tanstack/ai-react": "latest",
10
+ "@tanstack/react-ai-devtools": "latest",
8
11
  "highlight.js": "^11.11.1",
9
12
  "streamdown": "^1.6.5",
10
13
  "lucide-react": "^0.544.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/cta-framework-react-cra",
3
- "version": "0.43.1",
3
+ "version": "0.44.1",
4
4
  "description": "CTA Framework for React (Create React App)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -23,7 +23,7 @@
23
23
  "author": "Jack Herrington <jherr@pobox.com>",
24
24
  "license": "MIT",
25
25
  "dependencies": {
26
- "@tanstack/cta-engine": "0.43.1"
26
+ "@tanstack/cta-engine": "0.44.1"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^24.6.0",
@@ -6,7 +6,7 @@ To run this application:
6
6
 
7
7
  ```bash
8
8
  <%= packageManager %> install
9
- <%= getPackageManagerRunScript('start') %>
9
+ <%= getPackageManagerRunScript('dev') %>
10
10
  ```
11
11
 
12
12
  # Building For Production
@@ -1,5 +1,6 @@
1
+ <% let hasContext = addOnEnabled["apollo-client"] || addOnEnabled["tanstack-query"]; %>
1
2
  <% if (!fileRouter) { ignoreFile() } %>import { <% if (addOnEnabled.start) { %>
2
- HeadContent<% } else { %>Outlet<% } %><% if (addOnEnabled.start) { %>, Scripts<% } %>, <% if (addOnEnabled["tanstack-query"]) { %>createRootRouteWithContext<% } else { %>createRootRoute<% } %> } from '@tanstack/react-router'
3
+ HeadContent<% } else { %>Outlet<% } %><% if (addOnEnabled.start) { %>, Scripts<% } %>, <% if (hasContext) { %>createRootRouteWithContext<% } else { %>createRootRoute<% } %> } from '@tanstack/react-router'
3
4
  import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools';
4
5
  import { TanStackDevtools } from '@tanstack/react-devtools'
5
6
  <% if (addOns.length) { %>
@@ -9,21 +10,28 @@ import <%= integration.jsName %> from '<%= relativePath(integration.path, true)
9
10
  <% } %>
10
11
  <% if (addOnEnabled.start) { %>
11
12
  import appCss from '../styles.css?url'
12
- <% } %><% if (addOnEnabled["tanstack-query"]) { %>
13
+ <% } %>
14
+ <% if (addOnEnabled["apollo-client"]) { %>
15
+ import type { ApolloClientIntegration } from "@apollo/client-integration-tanstack-start";
16
+ <% } %>
17
+ <% if (addOnEnabled["tanstack-query"]) { %>
13
18
  import type { QueryClient } from '@tanstack/react-query'
14
19
  <% if (addOnEnabled.tRPC) { %>
15
20
  import type { TRPCRouter } from '@/integrations/trpc/router'
16
21
  import type { TRPCOptionsProxy } from '@trpc/tanstack-react-query'
17
22
  <% } %>
18
-
19
- interface MyRouterContext {
23
+ <% } %>
24
+ <% if (hasContext) { %>
25
+ interface MyRouterContext <% if (addOnEnabled["apollo-client"]) {%> extends ApolloClientIntegration.RouterContext <%} %>{
26
+ <% if (addOnEnabled["tanstack-query"]) { %>
20
27
  queryClient: QueryClient
21
28
  <% if (addOnEnabled.tRPC) { %>
22
29
  trpc: TRPCOptionsProxy<TRPCRouter>
23
30
  <% } %>
31
+ <% } %>
24
32
  }<% } %>
25
-
26
- export const Route = <% if (addOnEnabled["tanstack-query"]) { %>createRootRouteWithContext<MyRouterContext>()<% } else { %>createRootRoute<% } %>({
33
+
34
+ export const Route = <% if (hasContext) { %>createRootRouteWithContext<MyRouterContext>()<% } else { %>createRootRoute<% } %>({
27
35
  <% if (addOnEnabled.start) { %>
28
36
  head: () => ({
29
37
  meta: [
@@ -148,3 +148,17 @@ test('file router with add-on start on npm', async () => {
148
148
  './snapshots/react-cra/cr-ts-start-tanstack-query-npm.json',
149
149
  )
150
150
  })
151
+
152
+ test('file router with add-on start and apollo-client on npm', async () => {
153
+ const { environment, output } = createMemoryEnvironment()
154
+ const options = {
155
+ ...(await createReactOptions(['start', 'apollo-client'])),
156
+ tailwind: true,
157
+ typescript: true,
158
+ } as Options
159
+ await createApp(environment, options)
160
+ const cleanedOutput = cleanupOutput(options, output)
161
+ await expect(JSON.stringify(cleanedOutput, null, 2)).toMatchFileSnapshot(
162
+ './snapshots/react-cra/cr-ts-start-apollo-client-npm.json',
163
+ )
164
+ })
@@ -17,7 +17,7 @@
17
17
  "/src/routes/demo/form.simple.jsx": "import { createFileRoute } from '@tanstack/react-router'\nimport { z } from 'zod'\n\nimport { useAppForm } from '@/hooks/demo.form'\n\nexport const Route = createFileRoute('/demo/form')({\n component: SimpleForm,\n})\n\nconst schema = z.object({\n title: z.string().min(1, 'Title is required'),\n description: z.string().min(1, 'Description is required'),\n})\n\nfunction SimpleForm() {\n const form = useAppForm({\n defaultValues: {\n title: '',\n description: '',\n },\n validators: {\n onBlur: schema,\n },\n onSubmit: ({ value }) => {\n console.log(value)\n // Show success message\n alert('Form submitted successfully!')\n },\n })\n\n return (\n <div\n className=\"flex items-center justify-center min-h-screen bg-gradient-to-br from-purple-100 to-blue-100 p-4 text-white\"\n style={{\n backgroundImage:\n 'radial-gradient(50% 50% at 5% 40%, #add8e6 0%, #0000ff 70%, #00008b 100%)',\n }}\n >\n <div className=\"w-full max-w-2xl p-8 rounded-xl backdrop-blur-md bg-black/50 shadow-xl border-8 border-black/10\">\n <form\n onSubmit={(e) => {\n e.preventDefault()\n e.stopPropagation()\n form.handleSubmit()\n }}\n className=\"space-y-6\"\n >\n <form.AppField name=\"title\">\n {(field) => <field.TextField label=\"Title\" />}\n </form.AppField>\n\n <form.AppField name=\"description\">\n {(field) => <field.TextArea label=\"Description\" />}\n </form.AppField>\n\n <div className=\"flex justify-end\">\n <form.AppForm>\n <form.SubscribeButton label=\"Submit\" />\n </form.AppForm>\n </div>\n </form>\n </div>\n </div>\n )\n}\n",
18
18
  "/src/routes/index.jsx": "import { createFileRoute } from '@tanstack/react-router'\nimport logo from '../logo.svg'\nimport '../App.css'\n\nexport const Route = createFileRoute('/')({\n component: App,\n})\n\nfunction App() {\n return (\n <div className=\"App\">\n <header className=\"App-header\">\n <img src={logo} className=\"App-logo\" alt=\"logo\" />\n <p>\n Edit <code>src/routes/index.tsx</code> and save to reload.\n </p>\n <a\n className=\"App-link\"\n href=\"https://reactjs.org\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n Learn React\n </a>\n <a\n className=\"App-link\"\n href=\"https://tanstack.com\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n Learn TanStack\n </a>\n </header>\n </div>\n )\n}\n",
19
19
  "/src/styles.css": "\nbody {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\",\n \"Ubuntu\", \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\",\n sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n font-family: source-code-pro, Menlo, Monaco, Consolas, \"Courier New\",\n monospace;\n}\n",
20
- "README.md": "Welcome to your new TanStack app! \n\n# Getting Started\n\nTo run this application:\n\n```bash\nnpm install\nnpm run start\n```\n\n# Building For Production\n\nTo build this application for production:\n\n```bash\nnpm run build\n```\n\n## Testing\n\nThis project uses [Vitest](https://vitest.dev/) for testing. You can run the tests with:\n\n```bash\nnpm run test\n```\n\n## Styling\n\nThis project uses CSS for styling.\n\n\n\n\n## Routing\nThis project uses [TanStack Router](https://tanstack.com/router). The initial setup is a file based router. Which means that the routes are managed as files in `src/routes`.\n\n### Adding A Route\n\nTo add a new route to your application just add another a new file in the `./src/routes` directory.\n\nTanStack will automatically generate the content of the route file for you.\n\nNow that you have two routes you can use a `Link` component to navigate between them.\n\n### Adding Links\n\nTo use SPA (Single Page Application) navigation you will need to import the `Link` component from `@tanstack/react-router`.\n\n```tsx\nimport { Link } from \"@tanstack/react-router\";\n```\n\nThen anywhere in your JSX you can use it like so:\n\n```tsx\n<Link to=\"/about\">About</Link>\n```\n\nThis will create a link that will navigate to the `/about` route.\n\nMore information on the `Link` component can be found in the [Link documentation](https://tanstack.com/router/v1/docs/framework/react/api/router/linkComponent).\n\n### Using A Layout\n\nIn the File Based Routing setup the layout is located in `src/routes/__root.tsx`. Anything you add to the root route will appear in all the routes. The route content will appear in the JSX where you use the `<Outlet />` component.\n\nHere is an example layout that includes a header:\n\n```tsx\nimport { Outlet, createRootRoute } from '@tanstack/react-router'\nimport { TanStackRouterDevtools } from '@tanstack/react-router-devtools'\n\nimport { Link } from \"@tanstack/react-router\";\n\nexport const Route = createRootRoute({\n component: () => (\n <>\n <header>\n <nav>\n <Link to=\"/\">Home</Link>\n <Link to=\"/about\">About</Link>\n </nav>\n </header>\n <Outlet />\n <TanStackRouterDevtools />\n </>\n ),\n})\n```\n\nThe `<TanStackRouterDevtools />` component is not required so you can remove it if you don't want it in your layout.\n\nMore information on layouts can be found in the [Layouts documentation](https://tanstack.com/router/latest/docs/framework/react/guide/routing-concepts#layouts).\n\n\n## Data Fetching\n\nThere are multiple ways to fetch data in your application. You can use TanStack Query to fetch data from a server. But you can also use the `loader` functionality built into TanStack Router to load the data for a route before it's rendered.\n\nFor example:\n\n```tsx\nconst peopleRoute = createRoute({\n getParentRoute: () => rootRoute,\n path: \"/people\",\n loader: async () => {\n const response = await fetch(\"https://swapi.dev/api/people\");\n return response.json();\n },\n component: () => {\n const data = peopleRoute.useLoaderData();\n return (\n <ul>\n {data.results.map((person) => (\n <li key={person.name}>{person.name}</li>\n ))}\n </ul>\n );\n },\n});\n```\n\nLoaders simplify your data fetching logic dramatically. Check out more information in the [Loader documentation](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#loader-parameters).\n\n### React-Query\n\nReact-Query is an excellent addition or alternative to route loading and integrating it into you application is a breeze.\n\nFirst add your dependencies:\n\n```bash\nnpm install @tanstack/react-query @tanstack/react-query-devtools\n```\n\nNext we'll need to create a query client and provider. We recommend putting those in `main.jsx`.\n\n```tsx\nimport { QueryClient, QueryClientProvider } from \"@tanstack/react-query\";\n\n// ...\n\nconst queryClient = new QueryClient();\n\n// ...\n\nif (!rootElement.innerHTML) {\n const root = ReactDOM.createRoot(rootElement);\n\n root.render(\n <QueryClientProvider client={queryClient}>\n <RouterProvider router={router} />\n </QueryClientProvider>\n );\n}\n```\n\nYou can also add TanStack Query Devtools to the root route (optional).\n\n```tsx\nimport { ReactQueryDevtools } from \"@tanstack/react-query-devtools\";\n\nconst rootRoute = createRootRoute({\n component: () => (\n <>\n <Outlet />\n <ReactQueryDevtools buttonPosition=\"top-right\" />\n <TanStackRouterDevtools />\n </>\n ),\n});\n```\n\nNow you can use `useQuery` to fetch your data.\n\n```tsx\nimport { useQuery } from \"@tanstack/react-query\";\n\nimport \"./App.css\";\n\nfunction App() {\n const { data } = useQuery({\n queryKey: [\"people\"],\n queryFn: () =>\n fetch(\"https://swapi.dev/api/people\")\n .then((res) => res.json())\n .then((data) => data.results),\n initialData: [],\n });\n\n return (\n <div>\n <ul>\n {data.map((person) => (\n <li key={person.name}>{person.name}</li>\n ))}\n </ul>\n </div>\n );\n}\n\nexport default App;\n```\n\nYou can find out everything you need to know on how to use React-Query in the [React-Query documentation](https://tanstack.com/query/latest/docs/framework/react/overview).\n\n## State Management\n\nAnother common requirement for React applications is state management. There are many options for state management in React. TanStack Store provides a great starting point for your project.\n\nFirst you need to add TanStack Store as a dependency:\n\n```bash\nnpm install @tanstack/store\n```\n\nNow let's create a simple counter in the `src/App.jsx` file as a demonstration.\n\n```tsx\nimport { useStore } from \"@tanstack/react-store\";\nimport { Store } from \"@tanstack/store\";\nimport \"./App.css\";\n\nconst countStore = new Store(0);\n\nfunction App() {\n const count = useStore(countStore);\n return (\n <div>\n <button onClick={() => countStore.setState((n) => n + 1)}>\n Increment - {count}\n </button>\n </div>\n );\n}\n\nexport default App;\n```\n\nOne of the many nice features of TanStack Store is the ability to derive state from other state. That derived state will update when the base state updates.\n\nLet's check this out by doubling the count using derived state.\n\n```tsx\nimport { useStore } from \"@tanstack/react-store\";\nimport { Store, Derived } from \"@tanstack/store\";\nimport \"./App.css\";\n\nconst countStore = new Store(0);\n\nconst doubledStore = new Derived({\n fn: () => countStore.state * 2,\n deps: [countStore],\n});\ndoubledStore.mount();\n\nfunction App() {\n const count = useStore(countStore);\n const doubledCount = useStore(doubledStore);\n\n return (\n <div>\n <button onClick={() => countStore.setState((n) => n + 1)}>\n Increment - {count}\n </button>\n <div>Doubled - {doubledCount}</div>\n </div>\n );\n}\n\nexport default App;\n```\n\nWe use the `Derived` class to create a new store that is derived from another store. The `Derived` class has a `mount` method that will start the derived store updating.\n\nOnce we've created the derived store we can use it in the `App` component just like we would any other store using the `useStore` hook.\n\nYou can find out everything you need to know on how to use TanStack Store in the [TanStack Store documentation](https://tanstack.com/store/latest).\n\n# Demo files\n\nFiles prefixed with `demo` can be safely deleted. They are there to provide a starting point for you to play around with the features you've installed.\n\n# Learn More\n\nYou can learn more about all of the offerings from TanStack in the [TanStack documentation](https://tanstack.com).\n",
20
+ "README.md": "Welcome to your new TanStack app! \n\n# Getting Started\n\nTo run this application:\n\n```bash\nnpm install\nnpm run dev\n```\n\n# Building For Production\n\nTo build this application for production:\n\n```bash\nnpm run build\n```\n\n## Testing\n\nThis project uses [Vitest](https://vitest.dev/) for testing. You can run the tests with:\n\n```bash\nnpm run test\n```\n\n## Styling\n\nThis project uses CSS for styling.\n\n\n\n\n## Routing\nThis project uses [TanStack Router](https://tanstack.com/router). The initial setup is a file based router. Which means that the routes are managed as files in `src/routes`.\n\n### Adding A Route\n\nTo add a new route to your application just add another a new file in the `./src/routes` directory.\n\nTanStack will automatically generate the content of the route file for you.\n\nNow that you have two routes you can use a `Link` component to navigate between them.\n\n### Adding Links\n\nTo use SPA (Single Page Application) navigation you will need to import the `Link` component from `@tanstack/react-router`.\n\n```tsx\nimport { Link } from \"@tanstack/react-router\";\n```\n\nThen anywhere in your JSX you can use it like so:\n\n```tsx\n<Link to=\"/about\">About</Link>\n```\n\nThis will create a link that will navigate to the `/about` route.\n\nMore information on the `Link` component can be found in the [Link documentation](https://tanstack.com/router/v1/docs/framework/react/api/router/linkComponent).\n\n### Using A Layout\n\nIn the File Based Routing setup the layout is located in `src/routes/__root.tsx`. Anything you add to the root route will appear in all the routes. The route content will appear in the JSX where you use the `<Outlet />` component.\n\nHere is an example layout that includes a header:\n\n```tsx\nimport { Outlet, createRootRoute } from '@tanstack/react-router'\nimport { TanStackRouterDevtools } from '@tanstack/react-router-devtools'\n\nimport { Link } from \"@tanstack/react-router\";\n\nexport const Route = createRootRoute({\n component: () => (\n <>\n <header>\n <nav>\n <Link to=\"/\">Home</Link>\n <Link to=\"/about\">About</Link>\n </nav>\n </header>\n <Outlet />\n <TanStackRouterDevtools />\n </>\n ),\n})\n```\n\nThe `<TanStackRouterDevtools />` component is not required so you can remove it if you don't want it in your layout.\n\nMore information on layouts can be found in the [Layouts documentation](https://tanstack.com/router/latest/docs/framework/react/guide/routing-concepts#layouts).\n\n\n## Data Fetching\n\nThere are multiple ways to fetch data in your application. You can use TanStack Query to fetch data from a server. But you can also use the `loader` functionality built into TanStack Router to load the data for a route before it's rendered.\n\nFor example:\n\n```tsx\nconst peopleRoute = createRoute({\n getParentRoute: () => rootRoute,\n path: \"/people\",\n loader: async () => {\n const response = await fetch(\"https://swapi.dev/api/people\");\n return response.json();\n },\n component: () => {\n const data = peopleRoute.useLoaderData();\n return (\n <ul>\n {data.results.map((person) => (\n <li key={person.name}>{person.name}</li>\n ))}\n </ul>\n );\n },\n});\n```\n\nLoaders simplify your data fetching logic dramatically. Check out more information in the [Loader documentation](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#loader-parameters).\n\n### React-Query\n\nReact-Query is an excellent addition or alternative to route loading and integrating it into you application is a breeze.\n\nFirst add your dependencies:\n\n```bash\nnpm install @tanstack/react-query @tanstack/react-query-devtools\n```\n\nNext we'll need to create a query client and provider. We recommend putting those in `main.jsx`.\n\n```tsx\nimport { QueryClient, QueryClientProvider } from \"@tanstack/react-query\";\n\n// ...\n\nconst queryClient = new QueryClient();\n\n// ...\n\nif (!rootElement.innerHTML) {\n const root = ReactDOM.createRoot(rootElement);\n\n root.render(\n <QueryClientProvider client={queryClient}>\n <RouterProvider router={router} />\n </QueryClientProvider>\n );\n}\n```\n\nYou can also add TanStack Query Devtools to the root route (optional).\n\n```tsx\nimport { ReactQueryDevtools } from \"@tanstack/react-query-devtools\";\n\nconst rootRoute = createRootRoute({\n component: () => (\n <>\n <Outlet />\n <ReactQueryDevtools buttonPosition=\"top-right\" />\n <TanStackRouterDevtools />\n </>\n ),\n});\n```\n\nNow you can use `useQuery` to fetch your data.\n\n```tsx\nimport { useQuery } from \"@tanstack/react-query\";\n\nimport \"./App.css\";\n\nfunction App() {\n const { data } = useQuery({\n queryKey: [\"people\"],\n queryFn: () =>\n fetch(\"https://swapi.dev/api/people\")\n .then((res) => res.json())\n .then((data) => data.results),\n initialData: [],\n });\n\n return (\n <div>\n <ul>\n {data.map((person) => (\n <li key={person.name}>{person.name}</li>\n ))}\n </ul>\n </div>\n );\n}\n\nexport default App;\n```\n\nYou can find out everything you need to know on how to use React-Query in the [React-Query documentation](https://tanstack.com/query/latest/docs/framework/react/overview).\n\n## State Management\n\nAnother common requirement for React applications is state management. There are many options for state management in React. TanStack Store provides a great starting point for your project.\n\nFirst you need to add TanStack Store as a dependency:\n\n```bash\nnpm install @tanstack/store\n```\n\nNow let's create a simple counter in the `src/App.jsx` file as a demonstration.\n\n```tsx\nimport { useStore } from \"@tanstack/react-store\";\nimport { Store } from \"@tanstack/store\";\nimport \"./App.css\";\n\nconst countStore = new Store(0);\n\nfunction App() {\n const count = useStore(countStore);\n return (\n <div>\n <button onClick={() => countStore.setState((n) => n + 1)}>\n Increment - {count}\n </button>\n </div>\n );\n}\n\nexport default App;\n```\n\nOne of the many nice features of TanStack Store is the ability to derive state from other state. That derived state will update when the base state updates.\n\nLet's check this out by doubling the count using derived state.\n\n```tsx\nimport { useStore } from \"@tanstack/react-store\";\nimport { Store, Derived } from \"@tanstack/store\";\nimport \"./App.css\";\n\nconst countStore = new Store(0);\n\nconst doubledStore = new Derived({\n fn: () => countStore.state * 2,\n deps: [countStore],\n});\ndoubledStore.mount();\n\nfunction App() {\n const count = useStore(countStore);\n const doubledCount = useStore(doubledStore);\n\n return (\n <div>\n <button onClick={() => countStore.setState((n) => n + 1)}>\n Increment - {count}\n </button>\n <div>Doubled - {doubledCount}</div>\n </div>\n );\n}\n\nexport default App;\n```\n\nWe use the `Derived` class to create a new store that is derived from another store. The `Derived` class has a `mount` method that will start the derived store updating.\n\nOnce we've created the derived store we can use it in the `App` component just like we would any other store using the `useStore` hook.\n\nYou can find out everything you need to know on how to use TanStack Store in the [TanStack Store documentation](https://tanstack.com/store/latest).\n\n# Demo files\n\nFiles prefixed with `demo` can be safely deleted. They are there to provide a starting point for you to play around with the features you've installed.\n\n# Learn More\n\nYou can learn more about all of the offerings from TanStack in the [TanStack documentation](https://tanstack.com).\n",
21
21
  "index.html": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <link rel=\"icon\" href=\"/favicon.ico\" />\n <meta name=\"theme-color\" content=\"#000000\" />\n <meta\n name=\"description\"\n content=\"Web site created using create-tsrouter-app\"\n />\n <link rel=\"apple-touch-icon\" href=\"/logo192.png\" />\n <link rel=\"manifest\" href=\"/manifest.json\" />\n <title>Create TanStack App - TEST</title>\n </head>\n <body>\n <div id=\"app\"></div>\n <script type=\"module\" src=\"/src/main.jsx\"></script>\n </body>\n</html>\n",
22
22
  "package.json": "{\n \"name\": \"TEST\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite --port 3000\",\n \"build\": \"vite build && tsc\",\n \"preview\": \"vite preview\",\n \"test\": \"vitest run\"\n },\n \"dependencies\": {\n \"@tanstack/react-devtools\": \"^0.7.0\",\n \"@tanstack/react-form\": \"^1.0.0\",\n \"@tanstack/react-router\": \"^1.132.0\",\n \"@tanstack/react-router-devtools\": \"^1.132.0\",\n \"@tanstack/router-plugin\": \"^1.132.0\",\n \"react\": \"^19.2.0\",\n \"react-dom\": \"^19.2.0\",\n \"zod\": \"^4.1.11\"\n },\n \"devDependencies\": {\n \"@tanstack/devtools-vite\": \"^0.3.11\",\n \"@testing-library/dom\": \"^10.4.0\",\n \"@testing-library/react\": \"^16.2.0\",\n \"@types/react\": \"^19.2.0\",\n \"@types/react-dom\": \"^19.2.0\",\n \"@vitejs/plugin-react\": \"^5.0.4\",\n \"jsdom\": \"^27.0.0\",\n \"typescript\": \"^5.7.2\",\n \"vite\": \"^7.1.7\",\n \"vitest\": \"^3.0.5\",\n \"web-vitals\": \"^5.1.0\"\n }\n}",
23
23
  "vite.config.js": "import { defineConfig } from 'vite'\nimport { devtools } from '@tanstack/devtools-vite'\nimport viteReact from '@vitejs/plugin-react'\nimport { tanstackRouter } from '@tanstack/router-plugin/vite'\nimport { fileURLToPath, URL } from 'node:url'\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n plugins: [\n devtools(),\n tanstackRouter({\n target: 'react',\n autoCodeSplitting: true,\n }),\n viteReact(),\n ],\n resolve: {\n alias: {\n '@': fileURLToPath(new URL('./src', import.meta.url)),\n },\n },\n})\n"