@geekmidas/cli 1.10.35 → 1.10.37

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 (100) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/{config-B62g483e.mjs → config-Bbu1JKvf.mjs} +2 -2
  3. package/dist/{config-B62g483e.mjs.map → config-Bbu1JKvf.mjs.map} +1 -1
  4. package/dist/{config-Cuo8vFsp.cjs → config-DujV8DbR.cjs} +2 -2
  5. package/dist/{config-Cuo8vFsp.cjs.map → config-DujV8DbR.cjs.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/{fullstack-secrets-Bchl2MDd.cjs → fullstack-secrets-BYzi6Hwl.cjs} +3 -2
  12. package/dist/{fullstack-secrets-Bchl2MDd.cjs.map → fullstack-secrets-BYzi6Hwl.cjs.map} +1 -1
  13. package/dist/{fullstack-secrets-P84v8zWn.mjs → fullstack-secrets-l8s3II_H.mjs} +3 -2
  14. package/dist/{fullstack-secrets-P84v8zWn.mjs.map → fullstack-secrets-l8s3II_H.mjs.map} +1 -1
  15. package/dist/{index-Dt_dZ7K4.d.mts → index-BBvGMG_A.d.mts} +162 -68
  16. package/dist/index-BBvGMG_A.d.mts.map +1 -0
  17. package/dist/{index-C3t5VL4R.d.cts → index-Dz2a7xQU.d.cts} +162 -68
  18. package/dist/index-Dz2a7xQU.d.cts.map +1 -0
  19. package/dist/index.cjs +1366 -149
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.mjs +1366 -149
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/{openapi-B_rJjImN.cjs → openapi-0JUzU5Tv.cjs} +72 -15
  24. package/dist/openapi-0JUzU5Tv.cjs.map +1 -0
  25. package/dist/{openapi-CflxypuN.mjs → openapi-C5AycKTR.mjs} +73 -16
  26. package/dist/openapi-C5AycKTR.mjs.map +1 -0
  27. package/dist/openapi.cjs +3 -3
  28. package/dist/openapi.d.cts +7 -0
  29. package/dist/openapi.d.cts.map +1 -1
  30. package/dist/openapi.d.mts +7 -0
  31. package/dist/openapi.d.mts.map +1 -1
  32. package/dist/openapi.mjs +3 -3
  33. package/dist/{reconcile-DAAKa0YR.mjs → reconcile-CYGmqXvq.mjs} +2 -2
  34. package/dist/{reconcile-DAAKa0YR.mjs.map → reconcile-CYGmqXvq.mjs.map} +1 -1
  35. package/dist/{reconcile-CZtzdrnc.cjs → reconcile-D93YG7G5.cjs} +2 -2
  36. package/dist/{reconcile-CZtzdrnc.cjs.map → reconcile-D93YG7G5.cjs.map} +1 -1
  37. package/dist/workspace/index.cjs +4 -1
  38. package/dist/workspace/index.d.cts +2 -2
  39. package/dist/workspace/index.d.mts +2 -2
  40. package/dist/workspace/index.mjs +2 -2
  41. package/dist/{workspace-CGYykWfn.cjs → workspace-CQTT7HJQ.cjs} +107 -27
  42. package/dist/workspace-CQTT7HJQ.cjs.map +1 -0
  43. package/dist/{workspace-Bi4X7Yzy.mjs → workspace-D-37Ylqo.mjs} +90 -28
  44. package/dist/workspace-D-37Ylqo.mjs.map +1 -0
  45. package/docs/dev-server-resilience-design.md +189 -0
  46. package/package.json +4 -4
  47. package/src/__tests__/openapi.spec.ts +99 -9
  48. package/src/build/__tests__/workspace-build.spec.ts +7 -7
  49. package/src/build/index.ts +25 -9
  50. package/src/credentials/__tests__/helpers.ts +1 -1
  51. package/src/credentials/__tests__/workspaceCredentials.spec.ts +1 -1
  52. package/src/deploy/__tests__/domain.spec.ts +12 -12
  53. package/src/deploy/__tests__/env-resolver.spec.ts +29 -1
  54. package/src/deploy/__tests__/index.spec.ts +14 -14
  55. package/src/deploy/__tests__/sniffer.spec.ts +9 -9
  56. package/src/deploy/domain.ts +19 -11
  57. package/src/deploy/env-resolver.ts +5 -10
  58. package/src/deploy/index.ts +25 -8
  59. package/src/deploy/sniffer.ts +11 -5
  60. package/src/deploy/types.ts +1 -1
  61. package/src/dev/__tests__/index.spec.ts +11 -11
  62. package/src/dev/index.ts +43 -9
  63. package/src/docker/__tests__/compose.spec.ts +4 -4
  64. package/src/docker/compose.ts +5 -3
  65. package/src/docker/index.ts +34 -6
  66. package/src/docker/templates.ts +138 -0
  67. package/src/generators/OpenApiTsGenerator.ts +22 -0
  68. package/src/generators/__tests__/OpenApiTsGenerator.registry.spec.ts +77 -0
  69. package/src/index.ts +6 -2
  70. package/src/init/__tests__/generators.spec.ts +147 -0
  71. package/src/init/__tests__/init.spec.ts +1 -1
  72. package/src/init/generators/mobile-expo.ts +576 -0
  73. package/src/init/generators/monorepo.ts +42 -11
  74. package/src/init/generators/web-tanstack.ts +348 -0
  75. package/src/init/index.ts +40 -3
  76. package/src/init/templates/index.ts +31 -0
  77. package/src/init/versions.ts +2 -2
  78. package/src/openapi.ts +110 -23
  79. package/src/secrets/__tests__/reconcile.spec.ts +1 -1
  80. package/src/setup/__tests__/reconcile-secrets.spec.ts +2 -2
  81. package/src/setup/fullstack-secrets.ts +7 -1
  82. package/src/setup/index.ts +2 -1
  83. package/src/test/__tests__/__fixtures__/workspace.ts +1 -1
  84. package/src/test/__tests__/web.spec.ts +2 -2
  85. package/src/workspace/__tests__/client-generator.spec.ts +6 -6
  86. package/src/workspace/__tests__/index.spec.ts +96 -4
  87. package/src/workspace/__tests__/publicEnv.spec.ts +64 -0
  88. package/src/workspace/__tests__/schema.spec.ts +27 -30
  89. package/src/workspace/__tests__/type-inference.spec.ts +5 -5
  90. package/src/workspace/client-generator.ts +10 -7
  91. package/src/workspace/index.ts +17 -3
  92. package/src/workspace/publicEnv.ts +57 -0
  93. package/src/workspace/schema.ts +126 -27
  94. package/src/workspace/types.ts +40 -64
  95. package/dist/index-C3t5VL4R.d.cts.map +0 -1
  96. package/dist/index-Dt_dZ7K4.d.mts.map +0 -1
  97. package/dist/openapi-B_rJjImN.cjs.map +0 -1
  98. package/dist/openapi-CflxypuN.mjs.map +0 -1
  99. package/dist/workspace-Bi4X7Yzy.mjs.map +0 -1
  100. package/dist/workspace-CGYykWfn.cjs.map +0 -1
@@ -55,13 +55,19 @@ export function generateFullstackCustomSecrets(
55
55
  const frontendPorts: number[] = [];
56
56
 
57
57
  for (const [appName, appConfig] of Object.entries(workspace.apps)) {
58
- if (appConfig.type === 'frontend') {
58
+ if (appConfig.type === 'web') {
59
59
  frontendPorts.push(appConfig.port);
60
60
  const upperName = appName.toUpperCase();
61
61
  customs[`${upperName}_URL`] = `http://localhost:${appConfig.port}`;
62
62
  continue;
63
63
  }
64
64
 
65
+ // Mobile apps (Expo, etc.) don't run on a host port the backend
66
+ // cares about for CORS, and don't need DB credentials.
67
+ if (appConfig.type === 'mobile') {
68
+ continue;
69
+ }
70
+
65
71
  // Backend apps with database: generate per-app DB passwords and URLs
66
72
  const password = generateDbPassword();
67
73
  const upperName = appName.toUpperCase();
@@ -359,7 +359,8 @@ function printSummary(workspace: NormalizedWorkspace, stage: string): void {
359
359
 
360
360
  logger.log('📋 Apps:');
361
361
  for (const [name, app] of Object.entries(workspace.apps)) {
362
- const icon = app.type === 'frontend' ? '🌐' : '🔧';
362
+ const icon =
363
+ app.type === 'web' ? '🌐' : app.type === 'mobile' ? '📱' : '🔧';
363
364
  logger.log(` ${icon} ${name} → http://localhost:${app.port}`);
364
365
  }
365
366
 
@@ -57,7 +57,7 @@ export function createFullstackWorkspace(): ReturnType<
57
57
  dependencies: [],
58
58
  },
59
59
  web: {
60
- type: 'frontend',
60
+ type: 'web',
61
61
  path: 'apps/web',
62
62
  port: 3001,
63
63
  framework: 'nextjs',
@@ -104,7 +104,7 @@ describe('web (frontend) app context', () => {
104
104
  dependencies: [],
105
105
  },
106
106
  web: {
107
- type: 'frontend',
107
+ type: 'web',
108
108
  path: 'apps/web',
109
109
  port: 3001,
110
110
  framework: 'nextjs',
@@ -149,7 +149,7 @@ describe('web (frontend) app context', () => {
149
149
  dependencies: [],
150
150
  },
151
151
  web: {
152
- type: 'frontend',
152
+ type: 'web',
153
153
  path: 'apps/web',
154
154
  port: 3001,
155
155
  framework: 'nextjs',
@@ -108,7 +108,7 @@ describe('Client Generator', () => {
108
108
  resolvedDeployTarget: 'dokploy',
109
109
  },
110
110
  web: {
111
- type: 'frontend',
111
+ type: 'web',
112
112
  path: 'apps/web',
113
113
  port: 3002,
114
114
  dependencies: ['api', 'auth'],
@@ -147,7 +147,7 @@ describe('Client Generator', () => {
147
147
  // No routes - not an HTTP backend
148
148
  },
149
149
  web: {
150
- type: 'frontend',
150
+ type: 'web',
151
151
  path: 'apps/web',
152
152
  port: 3002,
153
153
  dependencies: ['api', 'worker'],
@@ -204,21 +204,21 @@ describe('Client Generator', () => {
204
204
  resolvedDeployTarget: 'dokploy',
205
205
  },
206
206
  web: {
207
- type: 'frontend',
207
+ type: 'web',
208
208
  path: 'apps/web',
209
209
  port: 3001,
210
210
  dependencies: ['api'],
211
211
  resolvedDeployTarget: 'dokploy',
212
212
  },
213
213
  admin: {
214
- type: 'frontend',
214
+ type: 'web',
215
215
  path: 'apps/admin',
216
216
  port: 3002,
217
217
  dependencies: ['api'],
218
218
  resolvedDeployTarget: 'dokploy',
219
219
  },
220
220
  docs: {
221
- type: 'frontend',
221
+ type: 'web',
222
222
  path: 'apps/docs',
223
223
  port: 3003,
224
224
  dependencies: [], // No API dependency
@@ -291,7 +291,7 @@ describe('Client Generator', () => {
291
291
  root: '/project',
292
292
  apps: {
293
293
  web: {
294
- type: 'frontend',
294
+ type: 'web',
295
295
  path: 'apps/web',
296
296
  port: 3000,
297
297
  dependencies: [],
@@ -406,7 +406,7 @@ describe('getAppGkmConfig', () => {
406
406
  const config: WorkspaceConfig = {
407
407
  apps: {
408
408
  web: {
409
- type: 'frontend',
409
+ type: 'web',
410
410
  path: 'apps/web',
411
411
  port: 3001,
412
412
  framework: 'nextjs',
@@ -444,7 +444,7 @@ describe('getAppBuildOrder', () => {
444
444
  const config: WorkspaceConfig = {
445
445
  apps: {
446
446
  web: {
447
- type: 'frontend',
447
+ type: 'web',
448
448
  path: 'apps/web',
449
449
  port: 3001,
450
450
  framework: 'nextjs',
@@ -509,7 +509,7 @@ describe('getDependencyEnvVars', () => {
509
509
  const config: WorkspaceConfig = {
510
510
  apps: {
511
511
  web: {
512
- type: 'frontend',
512
+ type: 'web',
513
513
  path: 'apps/web',
514
514
  port: 3001,
515
515
  framework: 'nextjs',
@@ -545,7 +545,7 @@ describe('getDependencyEnvVars', () => {
545
545
  const config: WorkspaceConfig = {
546
546
  apps: {
547
547
  web: {
548
- type: 'frontend',
548
+ type: 'web',
549
549
  path: 'apps/web',
550
550
  port: 3001,
551
551
  framework: 'nextjs',
@@ -604,6 +604,98 @@ describe('getDependencyEnvVars', () => {
604
604
 
605
605
  expect(envVars).toEqual({});
606
606
  });
607
+
608
+ it('should emit VITE_ prefix for vite apps', () => {
609
+ const config: WorkspaceConfig = {
610
+ apps: {
611
+ web: {
612
+ type: 'frontend',
613
+ path: 'apps/web',
614
+ port: 5173,
615
+ framework: 'vite',
616
+ dependencies: ['api'],
617
+ },
618
+ api: {
619
+ type: 'backend',
620
+ path: 'apps/api',
621
+ port: 3000,
622
+ routes: './src/**/*.ts',
623
+ },
624
+ },
625
+ };
626
+
627
+ const workspace = normalizeWorkspace(config, '/project');
628
+ const envVars = getDependencyEnvVars(workspace, 'web');
629
+
630
+ expect(envVars).toEqual({
631
+ API_URL: 'http://localhost:3000',
632
+ VITE_API_URL: 'http://localhost:3000',
633
+ });
634
+ expect(envVars.NEXT_PUBLIC_API_URL).toBeUndefined();
635
+ });
636
+
637
+ it('should emit VITE_ prefix for tanstack-start apps', () => {
638
+ const config: WorkspaceConfig = {
639
+ apps: {
640
+ web: {
641
+ type: 'frontend',
642
+ path: 'apps/web',
643
+ port: 3000,
644
+ framework: 'tanstack-start',
645
+ dependencies: ['api', 'auth'],
646
+ },
647
+ api: {
648
+ type: 'backend',
649
+ path: 'apps/api',
650
+ port: 3001,
651
+ routes: './src/**/*.ts',
652
+ },
653
+ auth: {
654
+ type: 'backend',
655
+ path: 'apps/auth',
656
+ port: 3002,
657
+ entry: './src/index.ts',
658
+ },
659
+ },
660
+ };
661
+
662
+ const workspace = normalizeWorkspace(config, '/project');
663
+ const envVars = getDependencyEnvVars(workspace, 'web');
664
+
665
+ expect(envVars).toEqual({
666
+ API_URL: 'http://localhost:3001',
667
+ VITE_API_URL: 'http://localhost:3001',
668
+ AUTH_URL: 'http://localhost:3002',
669
+ VITE_AUTH_URL: 'http://localhost:3002',
670
+ });
671
+ });
672
+
673
+ it('should emit only un-prefixed URLs for remix apps', () => {
674
+ const config: WorkspaceConfig = {
675
+ apps: {
676
+ web: {
677
+ type: 'frontend',
678
+ path: 'apps/web',
679
+ port: 3000,
680
+ framework: 'remix',
681
+ dependencies: ['api'],
682
+ },
683
+ api: {
684
+ type: 'backend',
685
+ path: 'apps/api',
686
+ port: 3001,
687
+ routes: './src/**/*.ts',
688
+ },
689
+ },
690
+ };
691
+
692
+ const workspace = normalizeWorkspace(config, '/project');
693
+ const envVars = getDependencyEnvVars(workspace, 'web');
694
+
695
+ expect(envVars).toEqual({
696
+ API_URL: 'http://localhost:3001',
697
+ });
698
+ });
607
699
  });
608
700
 
609
701
  describe('getEndpointForStage', () => {
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ getPublicEnvPrefix,
4
+ PUBLIC_ENV_PREFIXES,
5
+ stripPublicPrefix,
6
+ } from '../publicEnv.ts';
7
+
8
+ describe('getPublicEnvPrefix', () => {
9
+ it('returns NEXT_PUBLIC_ for nextjs', () => {
10
+ expect(getPublicEnvPrefix('nextjs')).toBe('NEXT_PUBLIC_');
11
+ });
12
+
13
+ it('returns VITE_ for vite', () => {
14
+ expect(getPublicEnvPrefix('vite')).toBe('VITE_');
15
+ });
16
+
17
+ it('returns VITE_ for tanstack-start', () => {
18
+ expect(getPublicEnvPrefix('tanstack-start')).toBe('VITE_');
19
+ });
20
+
21
+ it('returns EXPO_PUBLIC_ for expo', () => {
22
+ expect(getPublicEnvPrefix('expo')).toBe('EXPO_PUBLIC_');
23
+ });
24
+
25
+ it('returns empty string for remix', () => {
26
+ expect(getPublicEnvPrefix('remix')).toBe('');
27
+ });
28
+
29
+ it('falls back to NEXT_PUBLIC_ when framework is undefined', () => {
30
+ expect(getPublicEnvPrefix(undefined)).toBe('NEXT_PUBLIC_');
31
+ });
32
+
33
+ it('falls back to NEXT_PUBLIC_ for backend frameworks', () => {
34
+ expect(getPublicEnvPrefix('hono')).toBe('NEXT_PUBLIC_');
35
+ expect(getPublicEnvPrefix('better-auth')).toBe('NEXT_PUBLIC_');
36
+ });
37
+ });
38
+
39
+ describe('stripPublicPrefix', () => {
40
+ it('strips NEXT_PUBLIC_', () => {
41
+ expect(stripPublicPrefix('NEXT_PUBLIC_AUTH_URL')).toBe('AUTH_URL');
42
+ });
43
+
44
+ it('strips VITE_', () => {
45
+ expect(stripPublicPrefix('VITE_API_URL')).toBe('API_URL');
46
+ });
47
+
48
+ it('strips EXPO_PUBLIC_', () => {
49
+ expect(stripPublicPrefix('EXPO_PUBLIC_API_URL')).toBe('API_URL');
50
+ });
51
+
52
+ it('returns null when no known prefix matches', () => {
53
+ expect(stripPublicPrefix('AUTH_URL')).toBeNull();
54
+ expect(stripPublicPrefix('REACT_APP_FOO')).toBeNull();
55
+ });
56
+ });
57
+
58
+ describe('PUBLIC_ENV_PREFIXES', () => {
59
+ it('exposes NEXT_PUBLIC_, VITE_, and EXPO_PUBLIC_', () => {
60
+ expect(PUBLIC_ENV_PREFIXES).toContain('NEXT_PUBLIC_');
61
+ expect(PUBLIC_ENV_PREFIXES).toContain('VITE_');
62
+ expect(PUBLIC_ENV_PREFIXES).toContain('EXPO_PUBLIC_');
63
+ });
64
+ });
@@ -47,7 +47,7 @@ describe('WorkspaceConfigSchema', () => {
47
47
  openapi: { enabled: true, title: 'My API' },
48
48
  },
49
49
  web: {
50
- type: 'frontend' as const,
50
+ type: 'web' as const,
51
51
  path: 'apps/web',
52
52
  port: 3001,
53
53
  framework: 'nextjs' as const,
@@ -82,7 +82,7 @@ describe('WorkspaceConfigSchema', () => {
82
82
 
83
83
  expect(result.name).toBe('my-saas');
84
84
  expect(result.apps.api.type).toBe('backend');
85
- expect(result.apps.web.type).toBe('frontend');
85
+ expect(result.apps.web.type).toBe('web');
86
86
  expect(result.apps.web.dependencies).toEqual(['api']);
87
87
  expect(result.shared?.packages).toEqual(['packages/*']);
88
88
  expect(result.deploy?.default).toBe('dokploy');
@@ -163,7 +163,7 @@ describe('WorkspaceConfigSchema', () => {
163
163
  const config = {
164
164
  apps: {
165
165
  web: {
166
- type: 'frontend' as const,
166
+ type: 'web' as const,
167
167
  path: 'apps/web',
168
168
  port: 3001,
169
169
  // Missing framework
@@ -197,7 +197,7 @@ describe('WorkspaceConfigSchema', () => {
197
197
  const config = {
198
198
  apps: {
199
199
  web: {
200
- type: 'frontend' as const,
200
+ type: 'web' as const,
201
201
  path: 'apps/web',
202
202
  port: 3001,
203
203
  framework: 'nextjs' as const,
@@ -479,53 +479,50 @@ describe('WorkspaceConfigSchema', () => {
479
479
  });
480
480
 
481
481
  describe('auth app configuration', () => {
482
- it('should accept auth app with provider', () => {
482
+ it('should accept auth app as backend with better-auth framework', () => {
483
483
  const config = {
484
484
  apps: {
485
485
  auth: {
486
- type: 'auth' as const,
486
+ type: 'backend' as const,
487
487
  path: 'apps/auth',
488
488
  port: 3002,
489
- provider: 'better-auth' as const,
489
+ framework: 'better-auth' as const,
490
490
  },
491
491
  },
492
492
  };
493
493
 
494
494
  const result = validateWorkspaceConfig(config);
495
495
 
496
- expect(result.apps.auth.type).toBe('auth');
497
- expect(result.apps.auth.provider).toBe('better-auth');
496
+ expect(result.apps.auth.type).toBe('backend');
497
+ expect(result.apps.auth.framework).toBe('better-auth');
498
498
  });
499
499
 
500
- it('should reject auth app without provider', () => {
500
+ it('should accept auth backend without explicit framework', () => {
501
+ // Backend type defaults are permissive — framework is optional.
501
502
  const config = {
502
503
  apps: {
503
504
  auth: {
504
- type: 'auth' as const,
505
+ type: 'backend' as const,
505
506
  path: 'apps/auth',
506
507
  port: 3002,
507
- // Missing provider
508
508
  },
509
509
  },
510
510
  };
511
511
 
512
- const result = safeValidateWorkspaceConfig(config);
512
+ const result = validateWorkspaceConfig(config);
513
513
 
514
- expect(result.success).toBe(false);
515
- if (result.error) {
516
- const formatted = formatValidationErrors(result.error);
517
- expect(formatted).toContain('Auth apps must have provider defined');
518
- }
514
+ expect(result.apps.auth.type).toBe('backend');
515
+ expect(result.apps.auth.framework).toBeUndefined();
519
516
  });
520
517
 
521
- it('should allow auth app with backend properties', () => {
518
+ it('should allow auth backend with additional backend properties', () => {
522
519
  const config = {
523
520
  apps: {
524
521
  auth: {
525
- type: 'auth' as const,
522
+ type: 'backend' as const,
526
523
  path: 'apps/auth',
527
524
  port: 3002,
528
- provider: 'better-auth' as const,
525
+ framework: 'better-auth' as const,
529
526
  envParser: './src/config/env',
530
527
  logger: './src/logger',
531
528
  telescope: true,
@@ -535,12 +532,12 @@ describe('WorkspaceConfigSchema', () => {
535
532
 
536
533
  const result = validateWorkspaceConfig(config);
537
534
 
538
- expect(result.apps.auth.type).toBe('auth');
535
+ expect(result.apps.auth.framework).toBe('better-auth');
539
536
  expect(result.apps.auth.envParser).toBe('./src/config/env');
540
537
  expect(result.apps.auth.telescope).toBe(true);
541
538
  });
542
539
 
543
- it('should validate fullstack workspace with auth app', () => {
540
+ it('should validate fullstack workspace with auth backend', () => {
544
541
  const config = {
545
542
  name: 'fullstack-app',
546
543
  apps: {
@@ -552,13 +549,13 @@ describe('WorkspaceConfigSchema', () => {
552
549
  dependencies: ['auth'],
553
550
  },
554
551
  auth: {
555
- type: 'auth' as const,
552
+ type: 'backend' as const,
556
553
  path: 'apps/auth',
557
554
  port: 3002,
558
- provider: 'better-auth' as const,
555
+ framework: 'better-auth' as const,
559
556
  },
560
557
  web: {
561
- type: 'frontend' as const,
558
+ type: 'web' as const,
562
559
  path: 'apps/web',
563
560
  port: 3001,
564
561
  framework: 'nextjs' as const,
@@ -570,18 +567,18 @@ describe('WorkspaceConfigSchema', () => {
570
567
  const result = validateWorkspaceConfig(config);
571
568
 
572
569
  expect(result.apps.api.dependencies).toEqual(['auth']);
573
- expect(result.apps.auth.type).toBe('auth');
570
+ expect(result.apps.auth.framework).toBe('better-auth');
574
571
  expect(result.apps.web.dependencies).toEqual(['api', 'auth']);
575
572
  });
576
573
 
577
- it('should reject invalid auth provider', () => {
574
+ it('should reject auth backend with invalid framework value', () => {
578
575
  const config = {
579
576
  apps: {
580
577
  auth: {
581
- type: 'auth' as const,
578
+ type: 'backend' as const,
582
579
  path: 'apps/auth',
583
580
  port: 3002,
584
- provider: 'invalid-provider',
581
+ framework: 'invalid-framework',
585
582
  },
586
583
  },
587
584
  };
@@ -14,7 +14,7 @@ describe('Type Inference', () => {
14
14
  routes: './src/**/*.ts',
15
15
  },
16
16
  web: {
17
- type: 'frontend',
17
+ type: 'web',
18
18
  path: 'apps/web',
19
19
  port: 3001,
20
20
  framework: 'nextjs',
@@ -46,7 +46,7 @@ describe('Type Inference', () => {
46
46
  routes: './src/**/*.ts',
47
47
  },
48
48
  web: {
49
- type: 'frontend',
49
+ type: 'web',
50
50
  path: 'apps/web',
51
51
  port: 3002,
52
52
  framework: 'nextjs',
@@ -69,7 +69,7 @@ describe('Type Inference', () => {
69
69
  routes: './src/**/*.ts',
70
70
  },
71
71
  web: {
72
- type: 'frontend',
72
+ type: 'web',
73
73
  path: 'apps/web',
74
74
  port: 3001,
75
75
  framework: 'nextjs',
@@ -186,14 +186,14 @@ describe('Type Inference', () => {
186
186
  dependencies: ['user-service'],
187
187
  },
188
188
  'web-app': {
189
- type: 'frontend',
189
+ type: 'web',
190
190
  path: 'apps/web',
191
191
  port: 3003,
192
192
  framework: 'nextjs',
193
193
  dependencies: ['api-gateway'],
194
194
  },
195
195
  'admin-dashboard': {
196
- type: 'frontend',
196
+ type: 'web',
197
197
  path: 'apps/admin',
198
198
  port: 3004,
199
199
  framework: 'nextjs',
@@ -55,25 +55,25 @@ export function shouldRegenerateClient(
55
55
  }
56
56
 
57
57
  /**
58
- * Get backend apps that a frontend depends on.
58
+ * Get backend apps that a web/mobile client depends on.
59
59
  */
60
60
  export function getBackendDependencies(
61
61
  workspace: NormalizedWorkspace,
62
- frontendAppName: string,
62
+ clientAppName: string,
63
63
  ): string[] {
64
- const frontendApp = workspace.apps[frontendAppName];
65
- if (!frontendApp || frontendApp.type !== 'frontend') {
64
+ const clientApp = workspace.apps[clientAppName];
65
+ if (!clientApp || (clientApp.type !== 'web' && clientApp.type !== 'mobile')) {
66
66
  return [];
67
67
  }
68
68
 
69
- return frontendApp.dependencies.filter((dep) => {
69
+ return clientApp.dependencies.filter((dep) => {
70
70
  const depApp = workspace.apps[dep];
71
71
  return depApp?.type === 'backend' && depApp.routes;
72
72
  });
73
73
  }
74
74
 
75
75
  /**
76
- * Get frontend apps that depend on a backend app.
76
+ * Get web/mobile apps that depend on a backend app.
77
77
  */
78
78
  export function getDependentFrontends(
79
79
  workspace: NormalizedWorkspace,
@@ -82,7 +82,10 @@ export function getDependentFrontends(
82
82
  const dependentApps: string[] = [];
83
83
 
84
84
  for (const [appName, app] of Object.entries(workspace.apps)) {
85
- if (app.type === 'frontend' && app.dependencies.includes(backendAppName)) {
85
+ if (
86
+ (app.type === 'web' || app.type === 'mobile') &&
87
+ app.dependencies.includes(backendAppName)
88
+ ) {
86
89
  dependentApps.push(appName);
87
90
  }
88
91
  }
@@ -1,5 +1,6 @@
1
1
  import { basename } from 'node:path';
2
2
  import type { GkmConfig } from '../types.js';
3
+ import { getPublicEnvPrefix } from './publicEnv.js';
3
4
  import {
4
5
  formatValidationErrors,
5
6
  safeValidateWorkspaceConfig,
@@ -17,6 +18,11 @@ import type {
17
18
  } from './types.js';
18
19
  import { isWorkspaceConfig } from './types.js';
19
20
 
21
+ export {
22
+ getPublicEnvPrefix,
23
+ PUBLIC_ENV_PREFIXES,
24
+ stripPublicPrefix,
25
+ } from './publicEnv.js';
20
26
  export {
21
27
  formatValidationErrors,
22
28
  getDeployTargetError,
@@ -105,7 +111,7 @@ function validateDependencies<TApps extends AppsRecord>(apps: TApps): void {
105
111
  * logger: './src/logger',
106
112
  * },
107
113
  * web: {
108
- * type: 'frontend',
114
+ * type: 'web',
109
115
  * framework: 'nextjs',
110
116
  * path: 'apps/web',
111
117
  * port: 3001,
@@ -350,7 +356,12 @@ export function getAppBuildOrder(workspace: NormalizedWorkspace): string[] {
350
356
 
351
357
  /**
352
358
  * Generate environment variables for app dependencies.
353
- * Each dependency gets both a `{DEP_NAME}_URL` and `NEXT_PUBLIC_{DEP_NAME}_URL` variable.
359
+ *
360
+ * Each dependency gets the un-prefixed `{DEP}_URL` (for server-side use) and,
361
+ * when the consuming app's framework supports a public-var prefix, also the
362
+ * prefixed form (e.g. `NEXT_PUBLIC_{DEP}_URL` for Next.js, `VITE_{DEP}_URL`
363
+ * for Vite/TanStack Start). The prefixed form is what gets bundled into the
364
+ * client at build time.
354
365
  */
355
366
  export function getDependencyEnvVars(
356
367
  workspace: NormalizedWorkspace,
@@ -361,6 +372,7 @@ export function getDependencyEnvVars(
361
372
  if (!app) return {};
362
373
 
363
374
  const env: Record<string, string> = {};
375
+ const publicPrefix = getPublicEnvPrefix(app.framework);
364
376
 
365
377
  for (const depName of app.dependencies) {
366
378
  const dep = workspace.apps[depName];
@@ -368,7 +380,9 @@ export function getDependencyEnvVars(
368
380
  const url = `${urlPrefix}:${dep.port}`;
369
381
  const envKey = `${depName.toUpperCase()}_URL`;
370
382
  env[envKey] = url;
371
- env[`NEXT_PUBLIC_${envKey}`] = url;
383
+ if (publicPrefix) {
384
+ env[`${publicPrefix}${envKey}`] = url;
385
+ }
372
386
  }
373
387
  }
374
388
 
@@ -0,0 +1,57 @@
1
+ import type { Framework } from './types.js';
2
+
3
+ /**
4
+ * All public env-var prefixes the toolbox understands.
5
+ *
6
+ * Used when resolving incoming var names back to a dependency
7
+ * (e.g. `VITE_AUTH_URL` → `auth`). A var matching any of these
8
+ * is considered safe to inline into a client bundle.
9
+ */
10
+ export const PUBLIC_ENV_PREFIXES = [
11
+ 'NEXT_PUBLIC_',
12
+ 'VITE_',
13
+ 'EXPO_PUBLIC_',
14
+ ] as const;
15
+
16
+ /**
17
+ * Resolve the public env-var prefix that a frontend framework's bundler
18
+ * inlines into client code at build time:
19
+ *
20
+ * - `nextjs` → `NEXT_PUBLIC_`
21
+ * - `vite` → `VITE_`
22
+ * - `tanstack-start` → `VITE_` (uses Vite under the hood)
23
+ * - `expo` → `EXPO_PUBLIC_`
24
+ * - `remix` → `''` (Remix exposes via loaders, no prefix convention)
25
+ *
26
+ * For backend frameworks or unspecified, falls back to `NEXT_PUBLIC_` to
27
+ * preserve the historical default. Backend apps never read these vars
28
+ * from a bundle, so the prefix only affects scaffolding/symmetry.
29
+ */
30
+ export function getPublicEnvPrefix(framework?: Framework): string {
31
+ switch (framework) {
32
+ case 'vite':
33
+ case 'tanstack-start':
34
+ return 'VITE_';
35
+ case 'expo':
36
+ return 'EXPO_PUBLIC_';
37
+ case 'remix':
38
+ return '';
39
+ case 'nextjs':
40
+ return 'NEXT_PUBLIC_';
41
+ default:
42
+ return 'NEXT_PUBLIC_';
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Strip a known public prefix from a var name.
48
+ * Returns the un-prefixed name, or `null` if no prefix matched.
49
+ */
50
+ export function stripPublicPrefix(name: string): string | null {
51
+ for (const prefix of PUBLIC_ENV_PREFIXES) {
52
+ if (prefix && name.startsWith(prefix)) {
53
+ return name.slice(prefix.length);
54
+ }
55
+ }
56
+ return null;
57
+ }