@tanstack/cta-framework-react-cra 0.43.0 → 0.44.0

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 (48) 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/prisma/package.json.ejs +1 -1
  13. package/add-ons/sentry/assets/instrument.server.mjs +16 -9
  14. package/add-ons/sentry/assets/src/routes/demo/sentry.testing.tsx +42 -2
  15. package/add-ons/shadcn/package.json +1 -1
  16. package/add-ons/start/assets/src/router.tsx.ejs +34 -10
  17. package/add-ons/start/package.json +2 -2
  18. package/add-ons/store/package.json +3 -3
  19. package/add-ons/storybook/package.json +2 -2
  20. package/dist/index.js +0 -3
  21. package/dist/types/index.d.ts +0 -2
  22. package/examples/tanchat/assets/src/hooks/useAudioRecorder.ts +85 -0
  23. package/examples/tanchat/assets/src/hooks/useTTS.ts +78 -0
  24. package/examples/tanchat/assets/src/lib/model-selection.ts +78 -0
  25. package/examples/tanchat/assets/src/lib/vendor-capabilities.ts +55 -0
  26. package/examples/tanchat/assets/src/routes/demo/api.available-providers.ts +35 -0
  27. package/examples/tanchat/assets/src/routes/demo/api.image.ts +74 -0
  28. package/examples/tanchat/assets/src/routes/demo/api.structured.ts +168 -0
  29. package/examples/tanchat/assets/src/routes/demo/api.tanchat.ts +89 -0
  30. package/examples/tanchat/assets/src/routes/demo/api.transcription.ts +89 -0
  31. package/examples/tanchat/assets/src/routes/demo/api.tts.ts +81 -0
  32. package/examples/tanchat/assets/src/routes/demo/image.tsx +257 -0
  33. package/examples/tanchat/assets/src/routes/demo/structured.tsx +460 -0
  34. package/examples/tanchat/assets/src/routes/demo/tanchat.css +14 -7
  35. package/examples/tanchat/assets/src/routes/demo/tanchat.tsx +301 -81
  36. package/examples/tanchat/info.json +10 -7
  37. package/examples/tanchat/package.json +8 -5
  38. package/package.json +2 -3
  39. package/project/base/src/routes/__root.tsx.ejs +14 -6
  40. package/src/index.ts +0 -5
  41. package/tests/react-cra.test.ts +14 -0
  42. package/tests/snapshots/react-cra/cr-ts-start-apollo-client-npm.json +31 -0
  43. package/tests/snapshots/react-cra/cr-ts-start-npm.json +2 -2
  44. package/tests/snapshots/react-cra/cr-ts-start-tanstack-query-npm.json +2 -2
  45. package/dist/checksum.js +0 -3
  46. package/dist/types/checksum.d.ts +0 -1
  47. package/examples/tanchat/assets/src/routes/demo/api.tanchat.ts.ejs +0 -72
  48. package/src/checksum.ts +0 -3
@@ -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.0",
3
+ "version": "0.44.0",
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.0"
26
+ "@tanstack/cta-engine": "0.44.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^24.6.0",
@@ -31,7 +31,6 @@
31
31
  "vitest": "^3.1.1"
32
32
  },
33
33
  "scripts": {
34
- "prebuild": "node ../../scripts/generate-checksums.js",
35
34
  "build": "tsc",
36
35
  "dev": "tsc --watch",
37
36
  "test": "eslint ./src && vitest run",
@@ -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: [
package/src/index.ts CHANGED
@@ -7,8 +7,6 @@ import {
7
7
  scanProjectDirectory,
8
8
  } from '@tanstack/cta-engine'
9
9
 
10
- import { contentChecksum } from './checksum.js'
11
-
12
10
  import type { FrameworkDefinition } from '@tanstack/cta-engine'
13
11
 
14
12
  export function createFrameworkDefinition(): FrameworkDefinition {
@@ -47,12 +45,9 @@ export function createFrameworkDefinition(): FrameworkDefinition {
47
45
  forceTypescript: true,
48
46
  },
49
47
  },
50
- contentChecksum,
51
48
  }
52
49
  }
53
50
 
54
51
  export function register() {
55
52
  registerFramework(createFrameworkDefinition())
56
53
  }
57
-
58
- export { contentChecksum }
@@ -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
+ })