@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.
- package/CHANGELOG.md +20 -0
- package/dist/{config-B62g483e.mjs → config-Bbu1JKvf.mjs} +2 -2
- package/dist/{config-B62g483e.mjs.map → config-Bbu1JKvf.mjs.map} +1 -1
- package/dist/{config-Cuo8vFsp.cjs → config-DujV8DbR.cjs} +2 -2
- package/dist/{config-Cuo8vFsp.cjs.map → config-DujV8DbR.cjs.map} +1 -1
- package/dist/config.cjs +2 -2
- package/dist/config.d.cts +1 -1
- package/dist/config.d.mts +1 -1
- package/dist/config.mjs +2 -2
- package/dist/deploy/sniffer-loader.cjs +1 -1
- package/dist/{fullstack-secrets-Bchl2MDd.cjs → fullstack-secrets-BYzi6Hwl.cjs} +3 -2
- package/dist/{fullstack-secrets-Bchl2MDd.cjs.map → fullstack-secrets-BYzi6Hwl.cjs.map} +1 -1
- package/dist/{fullstack-secrets-P84v8zWn.mjs → fullstack-secrets-l8s3II_H.mjs} +3 -2
- package/dist/{fullstack-secrets-P84v8zWn.mjs.map → fullstack-secrets-l8s3II_H.mjs.map} +1 -1
- package/dist/{index-Dt_dZ7K4.d.mts → index-BBvGMG_A.d.mts} +162 -68
- package/dist/index-BBvGMG_A.d.mts.map +1 -0
- package/dist/{index-C3t5VL4R.d.cts → index-Dz2a7xQU.d.cts} +162 -68
- package/dist/index-Dz2a7xQU.d.cts.map +1 -0
- package/dist/index.cjs +1366 -149
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +1366 -149
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-B_rJjImN.cjs → openapi-0JUzU5Tv.cjs} +72 -15
- package/dist/openapi-0JUzU5Tv.cjs.map +1 -0
- package/dist/{openapi-CflxypuN.mjs → openapi-C5AycKTR.mjs} +73 -16
- package/dist/openapi-C5AycKTR.mjs.map +1 -0
- package/dist/openapi.cjs +3 -3
- package/dist/openapi.d.cts +7 -0
- package/dist/openapi.d.cts.map +1 -1
- package/dist/openapi.d.mts +7 -0
- package/dist/openapi.d.mts.map +1 -1
- package/dist/openapi.mjs +3 -3
- package/dist/{reconcile-DAAKa0YR.mjs → reconcile-CYGmqXvq.mjs} +2 -2
- package/dist/{reconcile-DAAKa0YR.mjs.map → reconcile-CYGmqXvq.mjs.map} +1 -1
- package/dist/{reconcile-CZtzdrnc.cjs → reconcile-D93YG7G5.cjs} +2 -2
- package/dist/{reconcile-CZtzdrnc.cjs.map → reconcile-D93YG7G5.cjs.map} +1 -1
- package/dist/workspace/index.cjs +4 -1
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +2 -2
- package/dist/workspace/index.mjs +2 -2
- package/dist/{workspace-CGYykWfn.cjs → workspace-CQTT7HJQ.cjs} +107 -27
- package/dist/workspace-CQTT7HJQ.cjs.map +1 -0
- package/dist/{workspace-Bi4X7Yzy.mjs → workspace-D-37Ylqo.mjs} +90 -28
- package/dist/workspace-D-37Ylqo.mjs.map +1 -0
- package/docs/dev-server-resilience-design.md +189 -0
- package/package.json +4 -4
- package/src/__tests__/openapi.spec.ts +99 -9
- package/src/build/__tests__/workspace-build.spec.ts +7 -7
- package/src/build/index.ts +25 -9
- package/src/credentials/__tests__/helpers.ts +1 -1
- package/src/credentials/__tests__/workspaceCredentials.spec.ts +1 -1
- package/src/deploy/__tests__/domain.spec.ts +12 -12
- package/src/deploy/__tests__/env-resolver.spec.ts +29 -1
- package/src/deploy/__tests__/index.spec.ts +14 -14
- package/src/deploy/__tests__/sniffer.spec.ts +9 -9
- package/src/deploy/domain.ts +19 -11
- package/src/deploy/env-resolver.ts +5 -10
- package/src/deploy/index.ts +25 -8
- package/src/deploy/sniffer.ts +11 -5
- package/src/deploy/types.ts +1 -1
- package/src/dev/__tests__/index.spec.ts +11 -11
- package/src/dev/index.ts +43 -9
- package/src/docker/__tests__/compose.spec.ts +4 -4
- package/src/docker/compose.ts +5 -3
- package/src/docker/index.ts +34 -6
- package/src/docker/templates.ts +138 -0
- package/src/generators/OpenApiTsGenerator.ts +22 -0
- package/src/generators/__tests__/OpenApiTsGenerator.registry.spec.ts +77 -0
- package/src/index.ts +6 -2
- package/src/init/__tests__/generators.spec.ts +147 -0
- package/src/init/__tests__/init.spec.ts +1 -1
- package/src/init/generators/mobile-expo.ts +576 -0
- package/src/init/generators/monorepo.ts +42 -11
- package/src/init/generators/web-tanstack.ts +348 -0
- package/src/init/index.ts +40 -3
- package/src/init/templates/index.ts +31 -0
- package/src/init/versions.ts +2 -2
- package/src/openapi.ts +110 -23
- package/src/secrets/__tests__/reconcile.spec.ts +1 -1
- package/src/setup/__tests__/reconcile-secrets.spec.ts +2 -2
- package/src/setup/fullstack-secrets.ts +7 -1
- package/src/setup/index.ts +2 -1
- package/src/test/__tests__/__fixtures__/workspace.ts +1 -1
- package/src/test/__tests__/web.spec.ts +2 -2
- package/src/workspace/__tests__/client-generator.spec.ts +6 -6
- package/src/workspace/__tests__/index.spec.ts +96 -4
- package/src/workspace/__tests__/publicEnv.spec.ts +64 -0
- package/src/workspace/__tests__/schema.spec.ts +27 -30
- package/src/workspace/__tests__/type-inference.spec.ts +5 -5
- package/src/workspace/client-generator.ts +10 -7
- package/src/workspace/index.ts +17 -3
- package/src/workspace/publicEnv.ts +57 -0
- package/src/workspace/schema.ts +126 -27
- package/src/workspace/types.ts +40 -64
- package/dist/index-C3t5VL4R.d.cts.map +0 -1
- package/dist/index-Dt_dZ7K4.d.mts.map +0 -1
- package/dist/openapi-B_rJjImN.cjs.map +0 -1
- package/dist/openapi-CflxypuN.mjs.map +0 -1
- package/dist/workspace-Bi4X7Yzy.mjs.map +0 -1
- 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 === '
|
|
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();
|
package/src/setup/index.ts
CHANGED
|
@@ -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 =
|
|
362
|
+
const icon =
|
|
363
|
+
app.type === 'web' ? '🌐' : app.type === 'mobile' ? '📱' : '🔧';
|
|
363
364
|
logger.log(` ${icon} ${name} → http://localhost:${app.port}`);
|
|
364
365
|
}
|
|
365
366
|
|
|
@@ -104,7 +104,7 @@ describe('web (frontend) app context', () => {
|
|
|
104
104
|
dependencies: [],
|
|
105
105
|
},
|
|
106
106
|
web: {
|
|
107
|
-
type: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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('
|
|
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: '
|
|
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: '
|
|
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
|
|
482
|
+
it('should accept auth app as backend with better-auth framework', () => {
|
|
483
483
|
const config = {
|
|
484
484
|
apps: {
|
|
485
485
|
auth: {
|
|
486
|
-
type: '
|
|
486
|
+
type: 'backend' as const,
|
|
487
487
|
path: 'apps/auth',
|
|
488
488
|
port: 3002,
|
|
489
|
-
|
|
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('
|
|
497
|
-
expect(result.apps.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
|
|
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: '
|
|
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 =
|
|
512
|
+
const result = validateWorkspaceConfig(config);
|
|
513
513
|
|
|
514
|
-
expect(result.
|
|
515
|
-
|
|
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
|
|
518
|
+
it('should allow auth backend with additional backend properties', () => {
|
|
522
519
|
const config = {
|
|
523
520
|
apps: {
|
|
524
521
|
auth: {
|
|
525
|
-
type: '
|
|
522
|
+
type: 'backend' as const,
|
|
526
523
|
path: 'apps/auth',
|
|
527
524
|
port: 3002,
|
|
528
|
-
|
|
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.
|
|
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
|
|
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: '
|
|
552
|
+
type: 'backend' as const,
|
|
556
553
|
path: 'apps/auth',
|
|
557
554
|
port: 3002,
|
|
558
|
-
|
|
555
|
+
framework: 'better-auth' as const,
|
|
559
556
|
},
|
|
560
557
|
web: {
|
|
561
|
-
type: '
|
|
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.
|
|
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
|
|
574
|
+
it('should reject auth backend with invalid framework value', () => {
|
|
578
575
|
const config = {
|
|
579
576
|
apps: {
|
|
580
577
|
auth: {
|
|
581
|
-
type: '
|
|
578
|
+
type: 'backend' as const,
|
|
582
579
|
path: 'apps/auth',
|
|
583
580
|
port: 3002,
|
|
584
|
-
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
|
58
|
+
* Get backend apps that a web/mobile client depends on.
|
|
59
59
|
*/
|
|
60
60
|
export function getBackendDependencies(
|
|
61
61
|
workspace: NormalizedWorkspace,
|
|
62
|
-
|
|
62
|
+
clientAppName: string,
|
|
63
63
|
): string[] {
|
|
64
|
-
const
|
|
65
|
-
if (!
|
|
64
|
+
const clientApp = workspace.apps[clientAppName];
|
|
65
|
+
if (!clientApp || (clientApp.type !== 'web' && clientApp.type !== 'mobile')) {
|
|
66
66
|
return [];
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
return
|
|
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
|
|
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 (
|
|
85
|
+
if (
|
|
86
|
+
(app.type === 'web' || app.type === 'mobile') &&
|
|
87
|
+
app.dependencies.includes(backendAppName)
|
|
88
|
+
) {
|
|
86
89
|
dependentApps.push(appName);
|
|
87
90
|
}
|
|
88
91
|
}
|
package/src/workspace/index.ts
CHANGED
|
@@ -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: '
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
+
}
|