@geekmidas/cli 1.10.35 → 1.10.36

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 (63) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/{config-Cuo8vFsp.cjs → config-QnuOcYXp.cjs} +2 -2
  3. package/dist/{config-Cuo8vFsp.cjs.map → config-QnuOcYXp.cjs.map} +1 -1
  4. package/dist/{config-B62g483e.mjs → config-U-gojtxn.mjs} +2 -2
  5. package/dist/{config-B62g483e.mjs.map → config-U-gojtxn.mjs.map} +1 -1
  6. package/dist/config.cjs +2 -2
  7. package/dist/config.d.cts +1 -1
  8. package/dist/config.d.mts +1 -1
  9. package/dist/config.mjs +2 -2
  10. package/dist/deploy/sniffer-loader.cjs +1 -1
  11. package/dist/{index-Dt_dZ7K4.d.mts → index-D7iT4dnv.d.mts} +41 -4
  12. package/dist/index-D7iT4dnv.d.mts.map +1 -0
  13. package/dist/{index-C3t5VL4R.d.cts → index-DRQq26DF.d.cts} +41 -4
  14. package/dist/index-DRQq26DF.d.cts.map +1 -0
  15. package/dist/index.cjs +1094 -30
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.mjs +1094 -30
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/{openapi-CflxypuN.mjs → openapi-BA0e3I_s.mjs} +65 -15
  20. package/dist/openapi-BA0e3I_s.mjs.map +1 -0
  21. package/dist/{openapi-B_rJjImN.cjs → openapi-Bb4UEyZN.cjs} +65 -15
  22. package/dist/openapi-Bb4UEyZN.cjs.map +1 -0
  23. package/dist/openapi.cjs +3 -3
  24. package/dist/openapi.d.cts +7 -0
  25. package/dist/openapi.d.cts.map +1 -1
  26. package/dist/openapi.d.mts +7 -0
  27. package/dist/openapi.d.mts.map +1 -1
  28. package/dist/openapi.mjs +3 -3
  29. package/dist/workspace/index.cjs +4 -1
  30. package/dist/workspace/index.d.cts +2 -2
  31. package/dist/workspace/index.d.mts +2 -2
  32. package/dist/workspace/index.mjs +2 -2
  33. package/dist/{workspace-CGYykWfn.cjs → workspace-BobDOIG9.cjs} +74 -3
  34. package/dist/workspace-BobDOIG9.cjs.map +1 -0
  35. package/dist/{workspace-Bi4X7Yzy.mjs → workspace-Cgmvgwh8.mjs} +57 -4
  36. package/dist/workspace-Cgmvgwh8.mjs.map +1 -0
  37. package/package.json +4 -4
  38. package/src/__tests__/openapi.spec.ts +99 -9
  39. package/src/deploy/__tests__/env-resolver.spec.ts +28 -0
  40. package/src/deploy/env-resolver.ts +5 -10
  41. package/src/deploy/index.ts +13 -7
  42. package/src/deploy/sniffer.ts +10 -4
  43. package/src/dev/index.ts +29 -1
  44. package/src/index.ts +6 -2
  45. package/src/init/__tests__/generators.spec.ts +147 -0
  46. package/src/init/generators/mobile-expo.ts +576 -0
  47. package/src/init/generators/monorepo.ts +42 -11
  48. package/src/init/generators/web-tanstack.ts +348 -0
  49. package/src/init/index.ts +40 -3
  50. package/src/init/templates/index.ts +31 -0
  51. package/src/init/versions.ts +2 -2
  52. package/src/openapi.ts +110 -23
  53. package/src/workspace/__tests__/index.spec.ts +92 -0
  54. package/src/workspace/__tests__/publicEnv.spec.ts +64 -0
  55. package/src/workspace/index.ts +16 -2
  56. package/src/workspace/publicEnv.ts +59 -0
  57. package/src/workspace/types.ts +6 -1
  58. package/dist/index-C3t5VL4R.d.cts.map +0 -1
  59. package/dist/index-Dt_dZ7K4.d.mts.map +0 -1
  60. package/dist/openapi-B_rJjImN.cjs.map +0 -1
  61. package/dist/openapi-CflxypuN.mjs.map +0 -1
  62. package/dist/workspace-Bi4X7Yzy.mjs.map +0 -1
  63. package/dist/workspace-CGYykWfn.cjs.map +0 -1
@@ -0,0 +1,576 @@
1
+ import type { GeneratedFile, TemplateOptions } from '../templates/index.js';
2
+ import { GEEKMIDAS_VERSIONS } from '../versions.js';
3
+
4
+ /**
5
+ * Generate an Expo mobile app for the fullstack template.
6
+ *
7
+ * Mirrors the structure of rezgo/apps/app: NativeWind for styling,
8
+ * expo-router for navigation, better-auth + magic link, React Query
9
+ * for data, all wired into the toolbox's typed API client.
10
+ */
11
+ export function generateExpoAppFiles(
12
+ options: TemplateOptions,
13
+ ): GeneratedFile[] {
14
+ if (!options.monorepo || options.template !== 'fullstack') {
15
+ return [];
16
+ }
17
+
18
+ const packageName = `@${options.name}/app`;
19
+ const apiPackage = `@${options.name}/api`;
20
+ const modelsPackage = `@${options.name}/models`;
21
+ // Expo bundle id / scheme — kept simple; the user can rename later.
22
+ const slug = options.name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
23
+ const scheme = slug.replace(/-/g, '');
24
+ const bundleId = `com.${scheme}.app`;
25
+
26
+ const packageJson = {
27
+ name: packageName,
28
+ version: '0.0.1',
29
+ main: 'expo-router/entry',
30
+ private: true,
31
+ scripts: {
32
+ dev: 'gkm exec -- expo start -c',
33
+ ios: 'expo start --ios',
34
+ android: 'expo start --android',
35
+ web: 'expo start --web',
36
+ typecheck: 'tsc --noEmit',
37
+ },
38
+ dependencies: {
39
+ [apiPackage]: 'workspace:*',
40
+ [modelsPackage]: 'workspace:*',
41
+ '@better-auth/expo': '~1.2.0',
42
+ '@geekmidas/client': GEEKMIDAS_VERSIONS['@geekmidas/client'],
43
+ '@geekmidas/envkit': GEEKMIDAS_VERSIONS['@geekmidas/envkit'],
44
+ '@react-navigation/native': '^7.1.0',
45
+ '@tanstack/react-query': '~5.80.0',
46
+ 'better-auth': '~1.2.0',
47
+ expo: '~55.0.0',
48
+ 'expo-constants': '~55.0.0',
49
+ 'expo-dev-client': '~55.0.0',
50
+ 'expo-linking': '~55.0.0',
51
+ 'expo-router': '~55.0.0',
52
+ 'expo-secure-store': '~14.2.0',
53
+ 'expo-splash-screen': '~55.0.0',
54
+ 'expo-status-bar': '~55.0.0',
55
+ nativewind: '~4.2.0',
56
+ react: '19.2.0',
57
+ 'react-dom': '19.2.0',
58
+ 'react-native': '0.83.6',
59
+ 'react-native-gesture-handler': '~2.30.0',
60
+ 'react-native-reanimated': '~4.2.0',
61
+ 'react-native-safe-area-context': '~5.6.0',
62
+ 'react-native-screens': '~4.23.0',
63
+ 'react-native-web': '~0.21.0',
64
+ tailwindcss: '~3.4.0',
65
+ },
66
+ devDependencies: {
67
+ '@babel/core': '^7.25.0',
68
+ '@types/react': '~19.0.0',
69
+ typescript: '~5.8.2',
70
+ },
71
+ };
72
+
73
+ const appConfig = `import type { ConfigContext, ExpoConfig } from '@expo/config';
74
+
75
+ /**
76
+ * Expo runtime config. Public env vars (\`EXPO_PUBLIC_*\`) are inlined
77
+ * at build time; everything else is read at runtime from \`extra\`.
78
+ */
79
+ export default function config(_context: ConfigContext): ExpoConfig {
80
+ return {
81
+ name: '${options.name}',
82
+ slug: '${slug}',
83
+ version: '0.0.1',
84
+ orientation: 'portrait',
85
+ icon: './assets/icon.png',
86
+ scheme: '${scheme}',
87
+ userInterfaceStyle: 'automatic',
88
+ newArchEnabled: true,
89
+ ios: {
90
+ supportsTablet: true,
91
+ bundleIdentifier: '${bundleId}',
92
+ },
93
+ android: {
94
+ package: '${bundleId}',
95
+ adaptiveIcon: {
96
+ foregroundImage: './assets/adaptive-icon.png',
97
+ backgroundColor: '#ffffff',
98
+ },
99
+ edgeToEdgeEnabled: true,
100
+ },
101
+ web: {
102
+ output: 'static',
103
+ },
104
+ plugins: [
105
+ 'expo-router',
106
+ 'expo-secure-store',
107
+ [
108
+ 'expo-splash-screen',
109
+ {
110
+ image: './assets/splash.png',
111
+ imageWidth: 200,
112
+ resizeMode: 'contain',
113
+ backgroundColor: '#ffffff',
114
+ },
115
+ ],
116
+ ],
117
+ experiments: {
118
+ typedRoutes: true,
119
+ },
120
+ };
121
+ }
122
+ `;
123
+
124
+ const easJson = {
125
+ cli: {
126
+ version: '>= 10.2.1',
127
+ appVersionSource: 'remote',
128
+ },
129
+ build: {
130
+ dev: {
131
+ developmentClient: true,
132
+ distribution: 'internal',
133
+ environment: 'development',
134
+ env: {
135
+ EXPO_PUBLIC_API_URL: 'http://localhost:3000',
136
+ EXPO_PUBLIC_AUTH_URL: 'http://localhost:3002',
137
+ },
138
+ },
139
+ preview: {
140
+ distribution: 'internal',
141
+ environment: 'preview',
142
+ env: {
143
+ EXPO_PUBLIC_API_URL: 'https://api.example.com',
144
+ EXPO_PUBLIC_AUTH_URL: 'https://auth.example.com',
145
+ },
146
+ },
147
+ production: {
148
+ autoIncrement: true,
149
+ environment: 'production',
150
+ env: {
151
+ EXPO_PUBLIC_API_URL: 'https://api.example.com',
152
+ EXPO_PUBLIC_AUTH_URL: 'https://auth.example.com',
153
+ },
154
+ },
155
+ },
156
+ };
157
+
158
+ const babelConfig = `module.exports = function (api) {
159
+ api.cache(true);
160
+ return {
161
+ presets: [
162
+ ['babel-preset-expo', { jsxImportSource: 'nativewind' }],
163
+ 'nativewind/babel',
164
+ ],
165
+ };
166
+ };
167
+ `;
168
+
169
+ const metroConfig = `const { getDefaultConfig } = require('expo/metro-config');
170
+ const { withNativeWind } = require('nativewind/metro');
171
+
172
+ const config = getDefaultConfig(__dirname);
173
+
174
+ module.exports = withNativeWind(config, { input: './global.css' });
175
+ `;
176
+
177
+ const tsConfig = {
178
+ extends: 'expo/tsconfig.base',
179
+ compilerOptions: {
180
+ strict: true,
181
+ allowImportingTsExtensions: true,
182
+ types: ['nativewind/types'],
183
+ paths: {
184
+ '@/*': ['./*'],
185
+ [modelsPackage]: ['../../packages/models/src'],
186
+ [`${modelsPackage}/*`]: ['../../packages/models/src/*'],
187
+ [`${apiPackage}/client`]: ['../../apps/api/.gkm/openapi.ts'],
188
+ },
189
+ },
190
+ include: [
191
+ '**/*.ts',
192
+ '**/*.tsx',
193
+ '.expo/types/**/*.ts',
194
+ 'expo-env.d.ts',
195
+ 'nativewind-env.d.ts',
196
+ ],
197
+ };
198
+
199
+ const tailwindConfig = `import type { Config } from 'tailwindcss';
200
+
201
+ export default {
202
+ content: ['./app/**/*.{ts,tsx}', './components/**/*.{ts,tsx}'],
203
+ presets: [require('nativewind/preset')],
204
+ theme: {
205
+ extend: {},
206
+ },
207
+ plugins: [],
208
+ } satisfies Config;
209
+ `;
210
+
211
+ const globalCss = `@tailwind base;
212
+ @tailwind components;
213
+ @tailwind utilities;
214
+ `;
215
+
216
+ const expoEnvDts = `/// <reference types="expo/types" />\n`;
217
+
218
+ const nativewindEnvDts = `/// <reference types="nativewind/types" />\n`;
219
+
220
+ const configTs = `import { EnvironmentParser } from '@geekmidas/envkit';
221
+
222
+ /**
223
+ * Public app config. EXPO_PUBLIC_* vars are inlined into the bundle at
224
+ * build time, so they're safe to read in client code.
225
+ */
226
+ const envParser = new EnvironmentParser({
227
+ EXPO_PUBLIC_API_URL: process.env.EXPO_PUBLIC_API_URL,
228
+ EXPO_PUBLIC_AUTH_URL: process.env.EXPO_PUBLIC_AUTH_URL,
229
+ });
230
+
231
+ export const config = envParser
232
+ .create((get) => ({
233
+ apiUrl: get('EXPO_PUBLIC_API_URL').string(),
234
+ authUrl: get('EXPO_PUBLIC_AUTH_URL').string(),
235
+ }))
236
+ .parse();
237
+ `;
238
+
239
+ const queryClientTs = `import { QueryClient } from '@tanstack/react-query';
240
+
241
+ export const queryClient = new QueryClient({
242
+ defaultOptions: {
243
+ queries: {
244
+ staleTime: 60 * 1000,
245
+ },
246
+ },
247
+ });
248
+ `;
249
+
250
+ const authClientTs = `import { expoClient } from '@better-auth/expo/client';
251
+ import { magicLinkClient } from 'better-auth/client/plugins';
252
+ import { createAuthClient } from 'better-auth/react';
253
+ import * as SecureStore from 'expo-secure-store';
254
+
255
+ import { config } from '@/config.ts';
256
+
257
+ export const STORAGE_PREFIX = '${scheme}';
258
+ export const COOKIE_STORE_KEY = \`\${STORAGE_PREFIX}_cookie\`;
259
+
260
+ export const authClient = createAuthClient({
261
+ baseURL: config.authUrl,
262
+ plugins: [
263
+ expoClient({
264
+ scheme: '${scheme}',
265
+ storagePrefix: STORAGE_PREFIX,
266
+ storage: SecureStore,
267
+ }),
268
+ magicLinkClient(),
269
+ ],
270
+ });
271
+
272
+ export const { signIn, useSession } = authClient;
273
+
274
+ export async function signOut() {
275
+ try {
276
+ await authClient.signOut();
277
+ } finally {
278
+ await SecureStore.deleteItemAsync(COOKIE_STORE_KEY);
279
+ }
280
+ }
281
+ `;
282
+
283
+ const apiTs = `import { createApi } from '${apiPackage}/client';
284
+
285
+ import { config } from '@/config.ts';
286
+ import { authClient } from './auth-client.ts';
287
+ import { queryClient } from './query-client.ts';
288
+
289
+ export function createAppApi(options?: { headers?: Record<string, string> }) {
290
+ return createApi({
291
+ baseURL: config.apiUrl,
292
+ queryClient,
293
+ headers: options?.headers,
294
+ onRequest: (cfg) => {
295
+ const cookie = authClient.getCookie();
296
+ const next = { ...cfg, credentials: 'omit' as const };
297
+ if (cookie) {
298
+ return {
299
+ ...next,
300
+ headers: { ...(next.headers ?? {}), Cookie: cookie },
301
+ };
302
+ }
303
+ return next;
304
+ },
305
+ });
306
+ }
307
+ `;
308
+
309
+ const apiContextTsx = `import { createContext, type ReactNode, use } from 'react';
310
+ import type { createAppApi } from './api.ts';
311
+
312
+ export type Api = ReturnType<typeof createAppApi>;
313
+
314
+ const ApiContext = createContext<Api | null>(null);
315
+
316
+ export function ApiProvider({
317
+ api,
318
+ children,
319
+ }: {
320
+ api: Api;
321
+ children: ReactNode;
322
+ }) {
323
+ return <ApiContext.Provider value={api}>{children}</ApiContext.Provider>;
324
+ }
325
+
326
+ export function useApi(): Api {
327
+ const api = use(ApiContext);
328
+ if (!api) {
329
+ throw new Error('useApi must be used within an ApiProvider');
330
+ }
331
+ return api;
332
+ }
333
+ `;
334
+
335
+ const layoutTsx = `import '../global.css';
336
+
337
+ import { QueryClientProvider } from '@tanstack/react-query';
338
+ import { Stack, useRouter, useSegments } from 'expo-router';
339
+ import { StatusBar } from 'expo-status-bar';
340
+ import { useEffect, useMemo } from 'react';
341
+ import 'react-native-reanimated';
342
+
343
+ import { authClient } from '@/lib/auth-client.ts';
344
+ import { ApiProvider } from '@/lib/api-context.tsx';
345
+ import { createAppApi } from '@/lib/api.ts';
346
+ import { queryClient } from '@/lib/query-client.ts';
347
+
348
+ function AuthGate({ children }: { children: React.ReactNode }) {
349
+ const router = useRouter();
350
+ const segments = useSegments();
351
+ const { data: session, isPending } = authClient.useSession();
352
+
353
+ useEffect(() => {
354
+ if (isPending) return;
355
+ const onAuthRoute = segments[0] === 'login';
356
+ if (!session && !onAuthRoute) {
357
+ router.replace('/login');
358
+ } else if (session && onAuthRoute) {
359
+ router.replace('/');
360
+ }
361
+ }, [isPending, session, segments, router]);
362
+
363
+ return <>{children}</>;
364
+ }
365
+
366
+ export default function RootLayout() {
367
+ const api = useMemo(() => createAppApi(), []);
368
+
369
+ return (
370
+ <QueryClientProvider client={queryClient}>
371
+ <ApiProvider api={api}>
372
+ <AuthGate>
373
+ <Stack screenOptions={{ headerShown: false }}>
374
+ <Stack.Screen name="index" />
375
+ <Stack.Screen name="login" />
376
+ </Stack>
377
+ </AuthGate>
378
+ <StatusBar style="auto" />
379
+ </ApiProvider>
380
+ </QueryClientProvider>
381
+ );
382
+ }
383
+ `;
384
+
385
+ const indexTsx = `import { Pressable, Text, View } from 'react-native';
386
+ import { SafeAreaView } from 'react-native-safe-area-context';
387
+
388
+ import { signOut, useSession } from '@/lib/auth-client.ts';
389
+ import { useApi } from '@/lib/api-context.tsx';
390
+
391
+ export default function Home() {
392
+ const { data: session } = useSession();
393
+ const api = useApi();
394
+ const { data: health } = api.useQuery('GET /health', {});
395
+
396
+ return (
397
+ <SafeAreaView className="flex-1 bg-white">
398
+ <View className="flex-1 items-center justify-center gap-4 px-6">
399
+ <Text className="text-3xl font-bold text-slate-900">
400
+ Welcome to ${options.name}
401
+ </Text>
402
+ {session ? (
403
+ <Text className="text-sm text-slate-500">
404
+ Signed in as {session.user.email}
405
+ </Text>
406
+ ) : null}
407
+ {health ? (
408
+ <Text className="text-xs text-slate-400">
409
+ API: {JSON.stringify(health)}
410
+ </Text>
411
+ ) : null}
412
+ <Pressable
413
+ onPress={signOut}
414
+ className="mt-4 rounded-lg bg-slate-900 px-6 py-3"
415
+ >
416
+ <Text className="font-semibold text-white">Sign out</Text>
417
+ </Pressable>
418
+ </View>
419
+ </SafeAreaView>
420
+ );
421
+ }
422
+ `;
423
+
424
+ const loginTsx = `import * as Linking from 'expo-linking';
425
+ import { useState } from 'react';
426
+ import {
427
+ KeyboardAvoidingView,
428
+ Platform,
429
+ Pressable,
430
+ Text,
431
+ TextInput,
432
+ View,
433
+ } from 'react-native';
434
+ import { SafeAreaView } from 'react-native-safe-area-context';
435
+
436
+ import { authClient } from '@/lib/auth-client.ts';
437
+
438
+ const EMAIL_RE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;
439
+
440
+ export default function LoginScreen() {
441
+ const [email, setEmail] = useState('');
442
+ const [submitting, setSubmitting] = useState(false);
443
+ const [sent, setSent] = useState(false);
444
+ const [error, setError] = useState<string | null>(null);
445
+
446
+ async function handleSubmit() {
447
+ if (!EMAIL_RE.test(email.trim())) {
448
+ setError('Enter a valid email address');
449
+ return;
450
+ }
451
+ setError(null);
452
+ setSubmitting(true);
453
+
454
+ const callbackURL = Linking.createURL('/').replace(/^(\\w+):\\/\\/\\//, '$1://');
455
+ try {
456
+ const result = await authClient.signIn.magicLink({
457
+ email: email.trim(),
458
+ callbackURL,
459
+ });
460
+ if (result.error) {
461
+ setError(result.error.message ?? 'Could not send magic link');
462
+ return;
463
+ }
464
+ setSent(true);
465
+ } catch (err) {
466
+ setError(err instanceof Error ? err.message : 'Something went wrong');
467
+ } finally {
468
+ setSubmitting(false);
469
+ }
470
+ }
471
+
472
+ if (sent) {
473
+ return (
474
+ <SafeAreaView className="flex-1 bg-white">
475
+ <View className="flex-1 items-center justify-center px-6">
476
+ <Text className="text-2xl font-bold text-slate-900">
477
+ Check your inbox
478
+ </Text>
479
+ <Text className="mt-3 text-center text-slate-500">
480
+ We sent a sign-in link to {email.trim()}
481
+ </Text>
482
+ </View>
483
+ </SafeAreaView>
484
+ );
485
+ }
486
+
487
+ return (
488
+ <SafeAreaView className="flex-1 bg-white">
489
+ <KeyboardAvoidingView
490
+ behavior={Platform.OS === 'ios' ? 'padding' : undefined}
491
+ className="flex-1"
492
+ >
493
+ <View className="flex-1 justify-center px-6">
494
+ <Text className="text-3xl font-bold text-slate-900">Sign in</Text>
495
+ <Text className="mt-1 text-sm text-slate-500">
496
+ We'll email you a one-time sign-in link.
497
+ </Text>
498
+
499
+ <Text className="mt-6 text-xs font-semibold uppercase text-slate-500">
500
+ Email
501
+ </Text>
502
+ <TextInput
503
+ value={email}
504
+ onChangeText={(v) => {
505
+ setEmail(v);
506
+ if (error) setError(null);
507
+ }}
508
+ autoCapitalize="none"
509
+ autoCorrect={false}
510
+ keyboardType="email-address"
511
+ placeholder="you@example.com"
512
+ placeholderTextColor="#94a3b8"
513
+ editable={!submitting}
514
+ className="mt-2 rounded-lg border border-slate-300 px-4 py-3"
515
+ />
516
+ {error ? (
517
+ <Text className="mt-2 text-sm text-red-600">{error}</Text>
518
+ ) : null}
519
+
520
+ <Pressable
521
+ onPress={handleSubmit}
522
+ disabled={submitting}
523
+ className="mt-6 items-center rounded-lg bg-slate-900 py-4 disabled:opacity-60"
524
+ >
525
+ <Text className="font-semibold text-white">
526
+ {submitting ? 'Sending…' : 'Email me a sign-in link'}
527
+ </Text>
528
+ </Pressable>
529
+ </View>
530
+ </KeyboardAvoidingView>
531
+ </SafeAreaView>
532
+ );
533
+ }
534
+ `;
535
+
536
+ const gitignore = `node_modules/
537
+ .expo/
538
+ dist/
539
+ *.log
540
+ .env*.local
541
+ ios/
542
+ android/
543
+ .eas/
544
+ `;
545
+
546
+ return [
547
+ {
548
+ path: 'apps/app/package.json',
549
+ content: `${JSON.stringify(packageJson, null, 2)}\n`,
550
+ },
551
+ { path: 'apps/app/app.config.ts', content: appConfig },
552
+ {
553
+ path: 'apps/app/eas.json',
554
+ content: `${JSON.stringify(easJson, null, 2)}\n`,
555
+ },
556
+ { path: 'apps/app/babel.config.js', content: babelConfig },
557
+ { path: 'apps/app/metro.config.js', content: metroConfig },
558
+ {
559
+ path: 'apps/app/tsconfig.json',
560
+ content: `${JSON.stringify(tsConfig, null, 2)}\n`,
561
+ },
562
+ { path: 'apps/app/tailwind.config.ts', content: tailwindConfig },
563
+ { path: 'apps/app/global.css', content: globalCss },
564
+ { path: 'apps/app/expo-env.d.ts', content: expoEnvDts },
565
+ { path: 'apps/app/nativewind-env.d.ts', content: nativewindEnvDts },
566
+ { path: 'apps/app/config.ts', content: configTs },
567
+ { path: 'apps/app/lib/query-client.ts', content: queryClientTs },
568
+ { path: 'apps/app/lib/auth-client.ts', content: authClientTs },
569
+ { path: 'apps/app/lib/api.ts', content: apiTs },
570
+ { path: 'apps/app/lib/api-context.tsx', content: apiContextTsx },
571
+ { path: 'apps/app/app/_layout.tsx', content: layoutTsx },
572
+ { path: 'apps/app/app/index.tsx', content: indexTsx },
573
+ { path: 'apps/app/app/login.tsx', content: loginTsx },
574
+ { path: 'apps/app/.gitignore', content: gitignore },
575
+ ];
576
+ }
@@ -314,6 +314,47 @@ export default defineConfig({
314
314
  return files;
315
315
  }
316
316
 
317
+ /**
318
+ * Generate the frontend app entry for the workspace config based on the
319
+ * selected `frontendFramework`. Defaults to Next.js for backwards compat.
320
+ */
321
+ function generateFrontendAppEntry(options: TemplateOptions): string {
322
+ switch (options.frontendFramework) {
323
+ case 'tanstack-start':
324
+ return `web: {
325
+ type: 'frontend',
326
+ framework: 'tanstack-start',
327
+ path: 'apps/web',
328
+ port: 3001,
329
+ dependencies: ['api', 'auth'],
330
+ config: {
331
+ client: './src/config/client.ts',
332
+ server: './src/config/server.ts',
333
+ },
334
+ },`;
335
+ case 'expo':
336
+ return `app: {
337
+ type: 'frontend',
338
+ framework: 'expo',
339
+ path: 'apps/app',
340
+ port: 8081,
341
+ dependencies: ['api', 'auth'],
342
+ },`;
343
+ default:
344
+ return `web: {
345
+ type: 'frontend',
346
+ framework: 'nextjs',
347
+ path: 'apps/web',
348
+ port: 3001,
349
+ dependencies: ['api', 'auth'],
350
+ config: {
351
+ client: './src/config/client.ts',
352
+ server: './src/config/server.ts',
353
+ },
354
+ },`;
355
+ }
356
+ }
357
+
317
358
  /**
318
359
  * Generate gkm.config.ts with defineWorkspace for fullstack template
319
360
  */
@@ -368,17 +409,7 @@ export default defineWorkspace({
368
409
  envParser: './src/config/env#envParser',
369
410
  logger: './src/config/logger#logger',
370
411
  },
371
- web: {
372
- type: 'frontend',
373
- framework: 'nextjs',
374
- path: 'apps/web',
375
- port: 3001,
376
- dependencies: ['api', 'auth'],
377
- config: {
378
- client: './src/config/client.ts',
379
- server: './src/config/server.ts',
380
- },
381
- },
412
+ ${generateFrontendAppEntry(options)}
382
413
  },
383
414
  shared: {
384
415
  packages: ['packages/*'],