@geekmidas/cli 1.3.0 → 1.5.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.
- package/CHANGELOG.md +12 -0
- package/dist/{Route53Provider-xrWuBXih.cjs → Route53Provider-Bs7Arms9.cjs} +3 -2
- package/dist/Route53Provider-Bs7Arms9.cjs.map +1 -0
- package/dist/{Route53Provider-DOWmFnwN.mjs → Route53Provider-C8mS0zY6.mjs} +3 -2
- package/dist/Route53Provider-C8mS0zY6.mjs.map +1 -0
- package/dist/{config-C1bidhvG.mjs → config-DfCJ29PQ.mjs} +2 -2
- package/dist/{config-C1bidhvG.mjs.map → config-DfCJ29PQ.mjs.map} +1 -1
- package/dist/{config-C1dM7aZb.cjs → config-ZQM1vBoz.cjs} +2 -2
- package/dist/{config-C1dM7aZb.cjs.map → config-ZQM1vBoz.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/{index-DzmZ6SUW.d.cts → index-B58qjyBd.d.cts} +27 -1
- package/dist/index-B58qjyBd.d.cts.map +1 -0
- package/dist/{index-DvpWzLD7.d.mts → index-C0SpUT9Y.d.mts} +27 -1
- package/dist/index-C0SpUT9Y.d.mts.map +1 -0
- package/dist/index.cjs +117 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +117 -49
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-9k6a6VA4.mjs → openapi-BcSjLfWq.mjs} +2 -2
- package/dist/{openapi-9k6a6VA4.mjs.map → openapi-BcSjLfWq.mjs.map} +1 -1
- package/dist/{openapi-Dcja4e1C.cjs → openapi-D6Hcfov0.cjs} +2 -2
- package/dist/{openapi-Dcja4e1C.cjs.map → openapi-D6Hcfov0.cjs.map} +1 -1
- package/dist/openapi.cjs +3 -3
- package/dist/openapi.mjs +3 -3
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +1 -1
- package/dist/workspace/index.d.mts +1 -1
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-CeFgIDC-.cjs → workspace-2Do2YcGZ.cjs} +5 -1
- package/dist/{workspace-CeFgIDC-.cjs.map → workspace-2Do2YcGZ.cjs.map} +1 -1
- package/dist/{workspace-Cb_I7oCJ.mjs → workspace-BW2iU37P.mjs} +5 -1
- package/dist/{workspace-Cb_I7oCJ.mjs.map → workspace-BW2iU37P.mjs.map} +1 -1
- package/package.json +2 -2
- package/src/deploy/__tests__/Route53Provider.spec.ts +23 -0
- package/src/deploy/__tests__/env-resolver.spec.ts +384 -2
- package/src/deploy/__tests__/index.spec.ts +393 -5
- package/src/deploy/__tests__/sniffer.spec.ts +104 -93
- package/src/deploy/dns/Route53Provider.ts +4 -1
- package/src/deploy/env-resolver.ts +20 -0
- package/src/deploy/index.ts +83 -24
- package/src/deploy/sniffer.ts +39 -7
- package/src/init/generators/monorepo.ts +7 -1
- package/src/init/generators/web.ts +45 -2
- package/src/workspace/schema.ts +8 -0
- package/src/workspace/types.ts +23 -0
- package/dist/Route53Provider-DOWmFnwN.mjs.map +0 -1
- package/dist/Route53Provider-xrWuBXih.cjs.map +0 -1
- package/dist/index-DvpWzLD7.d.mts.map +0 -1
- package/dist/index-DzmZ6SUW.d.cts.map +0 -1
|
@@ -31,8 +31,8 @@ describe('sniffAppEnvironment', () => {
|
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
describe('frontend apps', () => {
|
|
34
|
-
it('should return empty env vars for frontend apps', async () => {
|
|
35
|
-
const app = createApp({ type: 'frontend' });
|
|
34
|
+
it('should return empty env vars for frontend apps with no dependencies', async () => {
|
|
35
|
+
const app = createApp({ type: 'frontend', dependencies: [] });
|
|
36
36
|
|
|
37
37
|
const result = await sniffAppEnvironment(app, 'web', workspacePath);
|
|
38
38
|
|
|
@@ -40,51 +40,117 @@ describe('sniffAppEnvironment', () => {
|
|
|
40
40
|
expect(result.requiredEnvVars).toEqual([]);
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
-
it('should
|
|
43
|
+
it('should return NEXT_PUBLIC_{DEP}_URL for frontend dependencies', async () => {
|
|
44
44
|
const app = createApp({
|
|
45
45
|
type: 'frontend',
|
|
46
|
-
|
|
46
|
+
dependencies: ['api', 'auth'],
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
const result = await sniffAppEnvironment(app, 'web', workspacePath);
|
|
50
50
|
|
|
51
|
-
expect(result.
|
|
51
|
+
expect(result.appName).toBe('web');
|
|
52
|
+
expect(result.requiredEnvVars).toContain('NEXT_PUBLIC_API_URL');
|
|
53
|
+
expect(result.requiredEnvVars).toContain('NEXT_PUBLIC_AUTH_URL');
|
|
54
|
+
expect(result.requiredEnvVars).toHaveLength(2);
|
|
52
55
|
});
|
|
53
|
-
});
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
it('should return requiredEnv list for entry-based apps', async () => {
|
|
57
|
+
it('should generate uppercase dep names in NEXT_PUBLIC_{DEP}_URL', async () => {
|
|
57
58
|
const app = createApp({
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
type: 'frontend',
|
|
60
|
+
dependencies: ['payments-service', 'notification_api'],
|
|
60
61
|
});
|
|
61
62
|
|
|
62
|
-
const result = await sniffAppEnvironment(app, '
|
|
63
|
+
const result = await sniffAppEnvironment(app, 'web', workspacePath);
|
|
63
64
|
|
|
64
|
-
expect(result.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
expect(result.requiredEnvVars).toContain(
|
|
66
|
+
'NEXT_PUBLIC_PAYMENTS-SERVICE_URL',
|
|
67
|
+
);
|
|
68
|
+
expect(result.requiredEnvVars).toContain(
|
|
69
|
+
'NEXT_PUBLIC_NOTIFICATION_API_URL',
|
|
70
|
+
);
|
|
69
71
|
});
|
|
70
72
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
73
|
+
describe('config sniffing', () => {
|
|
74
|
+
it('should sniff env vars from config.client path', async () => {
|
|
75
|
+
const app = createApp({
|
|
76
|
+
type: 'frontend',
|
|
77
|
+
path: fixturesPath,
|
|
78
|
+
dependencies: [],
|
|
79
|
+
config: {
|
|
80
|
+
client: './simple-entry.ts',
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const result = await sniffAppEnvironment(app, 'web', fixturesPath);
|
|
85
|
+
|
|
86
|
+
expect(result.requiredEnvVars).toContain('PORT');
|
|
87
|
+
expect(result.requiredEnvVars).toContain('DATABASE_URL');
|
|
88
|
+
expect(result.requiredEnvVars).toContain('REDIS_URL');
|
|
89
|
+
});
|
|
81
90
|
|
|
82
|
-
|
|
83
|
-
|
|
91
|
+
it('should sniff env vars from config.server path', async () => {
|
|
92
|
+
const app = createApp({
|
|
93
|
+
type: 'frontend',
|
|
94
|
+
path: fixturesPath,
|
|
95
|
+
dependencies: [],
|
|
96
|
+
config: {
|
|
97
|
+
server: './nested-config-entry.ts',
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const result = await sniffAppEnvironment(app, 'web', fixturesPath);
|
|
102
|
+
|
|
103
|
+
expect(result.requiredEnvVars).toContain('PORT');
|
|
104
|
+
expect(result.requiredEnvVars).toContain('HOST');
|
|
105
|
+
expect(result.requiredEnvVars).toContain('DATABASE_URL');
|
|
106
|
+
});
|
|
84
107
|
|
|
85
|
-
|
|
108
|
+
it('should combine vars from both config.client and config.server', async () => {
|
|
109
|
+
const app = createApp({
|
|
110
|
+
type: 'frontend',
|
|
111
|
+
path: fixturesPath,
|
|
112
|
+
dependencies: ['api'],
|
|
113
|
+
config: {
|
|
114
|
+
client: './simple-entry.ts',
|
|
115
|
+
server: './nested-config-entry.ts',
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const result = await sniffAppEnvironment(app, 'web', fixturesPath);
|
|
120
|
+
|
|
121
|
+
// Dependency var
|
|
122
|
+
expect(result.requiredEnvVars).toContain('NEXT_PUBLIC_API_URL');
|
|
123
|
+
// From simple-entry.ts
|
|
124
|
+
expect(result.requiredEnvVars).toContain('REDIS_URL');
|
|
125
|
+
// From nested-config-entry.ts
|
|
126
|
+
expect(result.requiredEnvVars).toContain('HOST');
|
|
127
|
+
expect(result.requiredEnvVars).toContain('BETTER_AUTH_SECRET');
|
|
128
|
+
});
|
|
86
129
|
|
|
87
|
-
|
|
130
|
+
it('should deduplicate vars from both config files', async () => {
|
|
131
|
+
const app = createApp({
|
|
132
|
+
type: 'frontend',
|
|
133
|
+
path: fixturesPath,
|
|
134
|
+
dependencies: [],
|
|
135
|
+
config: {
|
|
136
|
+
client: './simple-entry.ts',
|
|
137
|
+
server: './nested-config-entry.ts',
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const result = await sniffAppEnvironment(app, 'web', fixturesPath);
|
|
142
|
+
|
|
143
|
+
// Both files have PORT and DATABASE_URL, should only appear once
|
|
144
|
+
const portCount = result.requiredEnvVars.filter(
|
|
145
|
+
(v) => v === 'PORT',
|
|
146
|
+
).length;
|
|
147
|
+
const dbUrlCount = result.requiredEnvVars.filter(
|
|
148
|
+
(v) => v === 'DATABASE_URL',
|
|
149
|
+
).length;
|
|
150
|
+
|
|
151
|
+
expect(portCount).toBe(1);
|
|
152
|
+
expect(dbUrlCount).toBe(1);
|
|
153
|
+
});
|
|
88
154
|
});
|
|
89
155
|
});
|
|
90
156
|
|
|
@@ -119,9 +185,9 @@ describe('sniffAppEnvironment', () => {
|
|
|
119
185
|
});
|
|
120
186
|
|
|
121
187
|
describe('apps without env detection', () => {
|
|
122
|
-
it('should return empty when no envParser or
|
|
188
|
+
it('should return empty when no envParser, entry, or routes', async () => {
|
|
123
189
|
const app = createApp({
|
|
124
|
-
// No envParser or
|
|
190
|
+
// No envParser, entry, or routes
|
|
125
191
|
});
|
|
126
192
|
|
|
127
193
|
const result = await sniffAppEnvironment(app, 'api', workspacePath);
|
|
@@ -142,7 +208,7 @@ describe('sniffAllApps', () => {
|
|
|
142
208
|
port: 3000,
|
|
143
209
|
dependencies: [],
|
|
144
210
|
resolvedDeployTarget: 'dokploy',
|
|
145
|
-
|
|
211
|
+
// No entry, routes, or envParser - will return empty
|
|
146
212
|
},
|
|
147
213
|
auth: {
|
|
148
214
|
type: 'backend',
|
|
@@ -150,7 +216,7 @@ describe('sniffAllApps', () => {
|
|
|
150
216
|
port: 3002,
|
|
151
217
|
dependencies: [],
|
|
152
218
|
resolvedDeployTarget: 'dokploy',
|
|
153
|
-
|
|
219
|
+
// No entry, routes, or envParser - will return empty
|
|
154
220
|
},
|
|
155
221
|
web: {
|
|
156
222
|
type: 'frontend',
|
|
@@ -167,17 +233,17 @@ describe('sniffAllApps', () => {
|
|
|
167
233
|
|
|
168
234
|
expect(results.get('api')).toEqual({
|
|
169
235
|
appName: 'api',
|
|
170
|
-
requiredEnvVars: [
|
|
236
|
+
requiredEnvVars: [],
|
|
171
237
|
});
|
|
172
238
|
|
|
173
239
|
expect(results.get('auth')).toEqual({
|
|
174
240
|
appName: 'auth',
|
|
175
|
-
requiredEnvVars: [
|
|
241
|
+
requiredEnvVars: [],
|
|
176
242
|
});
|
|
177
243
|
|
|
178
244
|
expect(results.get('web')).toEqual({
|
|
179
245
|
appName: 'web',
|
|
180
|
-
requiredEnvVars: [],
|
|
246
|
+
requiredEnvVars: ['NEXT_PUBLIC_API_URL', 'NEXT_PUBLIC_AUTH_URL'],
|
|
181
247
|
});
|
|
182
248
|
});
|
|
183
249
|
|
|
@@ -324,7 +390,7 @@ describe('entry app sniffing via subprocess', () => {
|
|
|
324
390
|
describe('sniffAppEnvironment with entry apps', () => {
|
|
325
391
|
// Integration tests for sniffAppEnvironment with entry-based apps
|
|
326
392
|
|
|
327
|
-
it('should use subprocess sniffing for entry apps
|
|
393
|
+
it('should use subprocess sniffing for entry apps', async () => {
|
|
328
394
|
const app: NormalizedAppConfig = {
|
|
329
395
|
type: 'backend',
|
|
330
396
|
path: fixturesPath,
|
|
@@ -342,25 +408,6 @@ describe('sniffAppEnvironment with entry apps', () => {
|
|
|
342
408
|
expect(result.requiredEnvVars).toContain('REDIS_URL');
|
|
343
409
|
});
|
|
344
410
|
|
|
345
|
-
it('should prefer requiredEnv over sniffing for entry apps', async () => {
|
|
346
|
-
const app: NormalizedAppConfig = {
|
|
347
|
-
type: 'backend',
|
|
348
|
-
path: fixturesPath,
|
|
349
|
-
port: 3000,
|
|
350
|
-
dependencies: [],
|
|
351
|
-
resolvedDeployTarget: 'dokploy',
|
|
352
|
-
entry: './simple-entry.ts',
|
|
353
|
-
requiredEnv: ['CUSTOM_VAR', 'ANOTHER_VAR'], // Should use this instead of sniffing
|
|
354
|
-
};
|
|
355
|
-
|
|
356
|
-
const result = await sniffAppEnvironment(app, 'api', fixturesPath);
|
|
357
|
-
|
|
358
|
-
expect(result.requiredEnvVars).toEqual(['CUSTOM_VAR', 'ANOTHER_VAR']);
|
|
359
|
-
// Should NOT contain the sniffed vars since requiredEnv takes precedence
|
|
360
|
-
expect(result.requiredEnvVars).not.toContain('PORT');
|
|
361
|
-
expect(result.requiredEnvVars).not.toContain('DATABASE_URL');
|
|
362
|
-
});
|
|
363
|
-
|
|
364
411
|
it('should handle entry app that throws and still return captured env vars', async () => {
|
|
365
412
|
const app: NormalizedAppConfig = {
|
|
366
413
|
type: 'backend',
|
|
@@ -499,24 +546,6 @@ describe('sniffAppEnvironment with envParser apps', () => {
|
|
|
499
546
|
expect(result.requiredEnvVars).toContain('DB_POOL_SIZE');
|
|
500
547
|
});
|
|
501
548
|
|
|
502
|
-
it('should prefer requiredEnv over envParser sniffing', async () => {
|
|
503
|
-
const app: NormalizedAppConfig = {
|
|
504
|
-
type: 'backend',
|
|
505
|
-
path: envParserFixturesPath,
|
|
506
|
-
port: 3000,
|
|
507
|
-
dependencies: [],
|
|
508
|
-
resolvedDeployTarget: 'dokploy',
|
|
509
|
-
envParser: './valid-env-parser.ts#envParser',
|
|
510
|
-
requiredEnv: ['CUSTOM_VAR'], // Should use this instead
|
|
511
|
-
};
|
|
512
|
-
|
|
513
|
-
const result = await sniffAppEnvironment(app, 'api', envParserFixturesPath);
|
|
514
|
-
|
|
515
|
-
expect(result.requiredEnvVars).toEqual(['CUSTOM_VAR']);
|
|
516
|
-
// Should NOT contain the sniffed vars
|
|
517
|
-
expect(result.requiredEnvVars).not.toContain('PORT');
|
|
518
|
-
});
|
|
519
|
-
|
|
520
549
|
it('should handle envParser that exports non-function gracefully', async () => {
|
|
521
550
|
const app: NormalizedAppConfig = {
|
|
522
551
|
type: 'backend',
|
|
@@ -663,24 +692,6 @@ describe('sniffAppEnvironment with route-based apps', () => {
|
|
|
663
692
|
expect(result.requiredEnvVars).toContain('AUTH_URL');
|
|
664
693
|
});
|
|
665
694
|
|
|
666
|
-
it('should prefer requiredEnv over route sniffing', async () => {
|
|
667
|
-
const app: NormalizedAppConfig = {
|
|
668
|
-
type: 'backend',
|
|
669
|
-
path: routeAppsFixturesPath,
|
|
670
|
-
port: 3000,
|
|
671
|
-
dependencies: [],
|
|
672
|
-
resolvedDeployTarget: 'dokploy',
|
|
673
|
-
routes: './endpoints/**/*.ts',
|
|
674
|
-
requiredEnv: ['CUSTOM_VAR'], // Should use this instead
|
|
675
|
-
};
|
|
676
|
-
|
|
677
|
-
const result = await sniffAppEnvironment(app, 'api', routeAppsFixturesPath);
|
|
678
|
-
|
|
679
|
-
expect(result.requiredEnvVars).toEqual(['CUSTOM_VAR']);
|
|
680
|
-
// Should NOT contain the sniffed vars
|
|
681
|
-
expect(result.requiredEnvVars).not.toContain('DATABASE_URL');
|
|
682
|
-
});
|
|
683
|
-
|
|
684
695
|
it('should handle route pattern that matches no files', async () => {
|
|
685
696
|
const app: NormalizedAppConfig = {
|
|
686
697
|
type: 'backend',
|
|
@@ -45,8 +45,11 @@ export class Route53Provider implements DnsProvider {
|
|
|
45
45
|
private hostedZoneCache: Map<string, string> = new Map();
|
|
46
46
|
|
|
47
47
|
constructor(options: Route53ProviderOptions = {}) {
|
|
48
|
+
// Route53 is a global service, default to us-east-1 if no region specified
|
|
49
|
+
const region = options.region ?? process.env.AWS_REGION ?? 'us-east-1';
|
|
50
|
+
|
|
48
51
|
this.client = new Route53Client({
|
|
49
|
-
|
|
52
|
+
region,
|
|
50
53
|
...(options.endpoint && { endpoint: options.endpoint }),
|
|
51
54
|
...(options.profile && {
|
|
52
55
|
credentials: fromIni({ profile: options.profile }),
|
|
@@ -50,6 +50,8 @@ export interface EnvResolverContext {
|
|
|
50
50
|
userSecrets?: StageSecrets;
|
|
51
51
|
/** Master key for runtime decryption (optional) */
|
|
52
52
|
masterKey?: string;
|
|
53
|
+
/** URLs of deployed dependency apps (e.g., { auth: 'https://auth.example.com' }) */
|
|
54
|
+
dependencyUrls?: Record<string, string>;
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
/**
|
|
@@ -205,6 +207,24 @@ export function resolveEnvVar(
|
|
|
205
207
|
break;
|
|
206
208
|
}
|
|
207
209
|
|
|
210
|
+
// Check dependency URLs (e.g., AUTH_URL -> dependencyUrls.auth)
|
|
211
|
+
// Also supports NEXT_PUBLIC_ prefix for frontend apps (NEXT_PUBLIC_AUTH_URL -> dependencyUrls.auth)
|
|
212
|
+
if (context.dependencyUrls && varName.endsWith('_URL')) {
|
|
213
|
+
let depName: string;
|
|
214
|
+
|
|
215
|
+
if (varName.startsWith('NEXT_PUBLIC_')) {
|
|
216
|
+
// NEXT_PUBLIC_AUTH_URL -> auth
|
|
217
|
+
depName = varName.slice(12, -4).toLowerCase();
|
|
218
|
+
} else {
|
|
219
|
+
// AUTH_URL -> auth
|
|
220
|
+
depName = varName.slice(0, -4).toLowerCase();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (context.dependencyUrls[depName]) {
|
|
224
|
+
return context.dependencyUrls[depName];
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
208
228
|
// Check user-provided secrets
|
|
209
229
|
if (context.userSecrets) {
|
|
210
230
|
// Check custom secrets first
|
package/src/deploy/index.ts
CHANGED
|
@@ -73,12 +73,7 @@ import {
|
|
|
73
73
|
type DokployPostgres,
|
|
74
74
|
type DokployRedis,
|
|
75
75
|
} from './dokploy-api';
|
|
76
|
-
import {
|
|
77
|
-
generatePublicUrlBuildArgs,
|
|
78
|
-
getPublicUrlArgNames,
|
|
79
|
-
isMainFrontendApp,
|
|
80
|
-
resolveHost,
|
|
81
|
-
} from './domain.js';
|
|
76
|
+
import { isMainFrontendApp, resolveHost } from './domain.js';
|
|
82
77
|
import {
|
|
83
78
|
type EnvResolverContext,
|
|
84
79
|
formatMissingVarsError,
|
|
@@ -1375,6 +1370,16 @@ export async function workspaceDeployCommand(
|
|
|
1375
1370
|
false, // Backend apps are not main frontend
|
|
1376
1371
|
);
|
|
1377
1372
|
|
|
1373
|
+
// Build dependency URLs from already-deployed apps
|
|
1374
|
+
const dependencyUrls: Record<string, string> = {};
|
|
1375
|
+
if (app.dependencies) {
|
|
1376
|
+
for (const dep of app.dependencies) {
|
|
1377
|
+
if (publicUrls[dep]) {
|
|
1378
|
+
dependencyUrls[dep] = publicUrls[dep];
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1378
1383
|
// Build env resolver context
|
|
1379
1384
|
const envContext: EnvResolverContext = {
|
|
1380
1385
|
app,
|
|
@@ -1400,6 +1405,7 @@ export async function workspaceDeployCommand(
|
|
|
1400
1405
|
frontendUrls,
|
|
1401
1406
|
userSecrets: stageSecrets ?? undefined,
|
|
1402
1407
|
masterKey: appSecrets?.masterKey,
|
|
1408
|
+
dependencyUrls,
|
|
1403
1409
|
};
|
|
1404
1410
|
|
|
1405
1411
|
// Resolve all required environment variables
|
|
@@ -1565,13 +1571,71 @@ export async function workspaceDeployCommand(
|
|
|
1565
1571
|
// Store application ID in state
|
|
1566
1572
|
setApplicationId(state, appName, application.applicationId);
|
|
1567
1573
|
|
|
1568
|
-
//
|
|
1569
|
-
const
|
|
1574
|
+
// Build dependency URLs for frontend (same pattern as backend)
|
|
1575
|
+
const dependencyUrls: Record<string, string> = {};
|
|
1576
|
+
if (app.dependencies) {
|
|
1577
|
+
for (const dep of app.dependencies) {
|
|
1578
|
+
if (publicUrls[dep]) {
|
|
1579
|
+
dependencyUrls[dep] = publicUrls[dep];
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
// Compute hostname for this frontend app
|
|
1585
|
+
const isMainFrontend = isMainFrontendApp(appName, app, workspace.apps);
|
|
1586
|
+
const frontendHost = resolveHost(
|
|
1587
|
+
appName,
|
|
1588
|
+
app,
|
|
1589
|
+
stage,
|
|
1590
|
+
dokployConfig,
|
|
1591
|
+
isMainFrontend,
|
|
1592
|
+
);
|
|
1593
|
+
|
|
1594
|
+
// Build env context for frontend
|
|
1595
|
+
const envContext: EnvResolverContext = {
|
|
1596
|
+
app,
|
|
1597
|
+
appName,
|
|
1598
|
+
stage,
|
|
1599
|
+
state,
|
|
1600
|
+
appHostname: frontendHost,
|
|
1601
|
+
frontendUrls: [],
|
|
1602
|
+
userSecrets: stageSecrets ?? undefined,
|
|
1603
|
+
dependencyUrls,
|
|
1604
|
+
};
|
|
1605
|
+
|
|
1606
|
+
// Resolve all env vars BEFORE Docker build (NEXT_PUBLIC_* must be present at build time)
|
|
1607
|
+
const sniffedVars = sniffedApps.get(appName)?.requiredEnvVars ?? [];
|
|
1608
|
+
const { valid, missing, resolved } = validateEnvVars(
|
|
1609
|
+
sniffedVars,
|
|
1610
|
+
envContext,
|
|
1611
|
+
);
|
|
1612
|
+
|
|
1613
|
+
if (!valid) {
|
|
1614
|
+
throw new Error(formatMissingVarsError(appName, missing, stage));
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
if (Object.keys(resolved).length > 0) {
|
|
1618
|
+
logger.log(
|
|
1619
|
+
` Resolved ${Object.keys(resolved).length} env vars: ${Object.keys(resolved).join(', ')}`,
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
// Build args: all NEXT_PUBLIC_* vars must be present at Next.js build time
|
|
1624
|
+
const buildArgs: string[] = [];
|
|
1625
|
+
const publicUrlArgNames: string[] = [];
|
|
1626
|
+
|
|
1627
|
+
for (const [key, value] of Object.entries(resolved)) {
|
|
1628
|
+
if (key.startsWith('NEXT_PUBLIC_')) {
|
|
1629
|
+
buildArgs.push(`${key}=${value}`);
|
|
1630
|
+
publicUrlArgNames.push(key);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1570
1634
|
if (buildArgs.length > 0) {
|
|
1571
|
-
logger.log(`
|
|
1635
|
+
logger.log(` Build args: ${publicUrlArgNames.join(', ')}`);
|
|
1572
1636
|
}
|
|
1573
1637
|
|
|
1574
|
-
// Build Docker image with
|
|
1638
|
+
// Build Docker image with NEXT_PUBLIC_* vars as build args
|
|
1575
1639
|
const imageName = `${workspace.name}-${appName}`;
|
|
1576
1640
|
const imageRef = registry
|
|
1577
1641
|
? `${registry}/${imageName}:${imageTag}`
|
|
@@ -1589,17 +1653,22 @@ export async function workspaceDeployCommand(
|
|
|
1589
1653
|
appName,
|
|
1590
1654
|
},
|
|
1591
1655
|
buildArgs,
|
|
1592
|
-
// Pass
|
|
1593
|
-
publicUrlArgs:
|
|
1656
|
+
// Pass arg names for Dockerfile ARG generation
|
|
1657
|
+
publicUrlArgs: publicUrlArgNames,
|
|
1594
1658
|
});
|
|
1595
1659
|
|
|
1596
|
-
// Prepare environment variables
|
|
1660
|
+
// Prepare runtime environment variables
|
|
1597
1661
|
const envVars: string[] = [
|
|
1598
1662
|
`NODE_ENV=production`,
|
|
1599
1663
|
`PORT=${app.port}`,
|
|
1600
1664
|
`STAGE=${stage}`,
|
|
1601
1665
|
];
|
|
1602
1666
|
|
|
1667
|
+
// Add all resolved vars as runtime env (for SSR and server components)
|
|
1668
|
+
for (const [key, value] of Object.entries(resolved)) {
|
|
1669
|
+
envVars.push(`${key}=${value}`);
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1603
1672
|
// Configure and deploy application in Dokploy
|
|
1604
1673
|
await api.saveDockerProvider(application.applicationId, imageRef, {
|
|
1605
1674
|
registryId,
|
|
@@ -1613,17 +1682,7 @@ export async function workspaceDeployCommand(
|
|
|
1613
1682
|
logger.log(` Deploying to Dokploy...`);
|
|
1614
1683
|
await api.deployApplication(application.applicationId);
|
|
1615
1684
|
|
|
1616
|
-
//
|
|
1617
|
-
const isMainFrontend = isMainFrontendApp(appName, app, workspace.apps);
|
|
1618
|
-
const frontendHost = resolveHost(
|
|
1619
|
-
appName,
|
|
1620
|
-
app,
|
|
1621
|
-
stage,
|
|
1622
|
-
dokployConfig,
|
|
1623
|
-
isMainFrontend,
|
|
1624
|
-
);
|
|
1625
|
-
|
|
1626
|
-
// Check if domain already exists
|
|
1685
|
+
// Check if domain already exists (frontendHost computed earlier for env context)
|
|
1627
1686
|
const existingFrontendDomains = await api.getDomainsByApplicationId(
|
|
1628
1687
|
application.applicationId,
|
|
1629
1688
|
);
|
package/src/deploy/sniffer.ts
CHANGED
|
@@ -98,17 +98,49 @@ export async function sniffAppEnvironment(
|
|
|
98
98
|
): Promise<SniffedEnvironment> {
|
|
99
99
|
const { logWarnings = true } = options;
|
|
100
100
|
|
|
101
|
-
// 1. Frontend apps
|
|
101
|
+
// 1. Frontend apps - handle dependencies and config sniffing
|
|
102
102
|
if (app.type === 'frontend') {
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
// Auto-generate NEXT_PUBLIC_{DEP}_URL from dependencies
|
|
104
|
+
const depVars = (app.dependencies ?? []).map(
|
|
105
|
+
(dep) => `NEXT_PUBLIC_${dep.toUpperCase()}_URL`,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// If config specified, sniff by importing the file(s)
|
|
109
|
+
// The file calls .parse() at module load, which triggers sniffer to capture vars
|
|
110
|
+
if (app.config) {
|
|
111
|
+
const sniffedVars: string[] = [];
|
|
112
|
+
|
|
113
|
+
// Collect config paths to sniff
|
|
114
|
+
const configPaths: string[] = [];
|
|
115
|
+
if (app.config.client) configPaths.push(app.config.client);
|
|
116
|
+
if (app.config.server) configPaths.push(app.config.server);
|
|
117
|
+
|
|
118
|
+
// Sniff each config file
|
|
119
|
+
for (const configPath of configPaths) {
|
|
120
|
+
const result = await sniffEntryFile(
|
|
121
|
+
configPath,
|
|
122
|
+
app.path,
|
|
123
|
+
workspacePath,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
if (logWarnings && result.error) {
|
|
127
|
+
console.warn(
|
|
128
|
+
`[sniffer] ${appName}: Config file "${configPath}" threw error during sniffing (env vars still captured): ${result.error.message}`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
sniffedVars.push(...result.envVars);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Combine: dependency vars + sniffed vars (deduplicated)
|
|
136
|
+
const allVars = [...new Set([...depVars, ...sniffedVars])];
|
|
137
|
+
return { appName, requiredEnvVars: allVars };
|
|
138
|
+
}
|
|
105
139
|
|
|
106
|
-
|
|
107
|
-
if (app.requiredEnv && app.requiredEnv.length > 0) {
|
|
108
|
-
return { appName, requiredEnvVars: [...app.requiredEnv] };
|
|
140
|
+
return { appName, requiredEnvVars: depVars };
|
|
109
141
|
}
|
|
110
142
|
|
|
111
|
-
//
|
|
143
|
+
// 2. Entry apps - import entry file in subprocess to trigger config.parse()
|
|
112
144
|
if (app.entry) {
|
|
113
145
|
const result = await sniffEntryFile(app.entry, app.path, workspacePath);
|
|
114
146
|
|
|
@@ -48,6 +48,7 @@ export function generateMonorepoFiles(
|
|
|
48
48
|
'@biomejs/biome': '~2.3.0',
|
|
49
49
|
'@geekmidas/cli': GEEKMIDAS_VERSIONS['@geekmidas/cli'],
|
|
50
50
|
esbuild: '~0.27.0',
|
|
51
|
+
tsx: '~4.20.0',
|
|
51
52
|
turbo: '~2.3.0',
|
|
52
53
|
typescript: '~5.8.2',
|
|
53
54
|
vitest: '~4.0.0',
|
|
@@ -342,7 +343,8 @@ export default defineWorkspace({
|
|
|
342
343
|
port: 3000,
|
|
343
344
|
routes: '${getRoutesGlob()}',
|
|
344
345
|
envParser: './src/config/env#envParser',
|
|
345
|
-
logger: './src/config/logger#logger'
|
|
346
|
+
logger: './src/config/logger#logger',
|
|
347
|
+
dependencies: ['auth'],`;
|
|
346
348
|
|
|
347
349
|
if (telescope) {
|
|
348
350
|
config += `
|
|
@@ -371,6 +373,10 @@ export default defineWorkspace({
|
|
|
371
373
|
path: 'apps/web',
|
|
372
374
|
port: 3001,
|
|
373
375
|
dependencies: ['api', 'auth'],
|
|
376
|
+
config: {
|
|
377
|
+
client: './src/config/client.ts',
|
|
378
|
+
server: './src/config/server.ts',
|
|
379
|
+
},
|
|
374
380
|
client: {
|
|
375
381
|
output: './src/api',
|
|
376
382
|
},
|
|
@@ -133,12 +133,46 @@ export function getQueryClient() {
|
|
|
133
133
|
}
|
|
134
134
|
`;
|
|
135
135
|
|
|
136
|
+
// Client config - NEXT_PUBLIC_* vars (available in browser)
|
|
137
|
+
const clientConfigTs = `import { EnvironmentParser } from '@geekmidas/envkit';
|
|
138
|
+
|
|
139
|
+
// Client config - only NEXT_PUBLIC_* vars (available in browser)
|
|
140
|
+
// These values are inlined at build time by Next.js
|
|
141
|
+
const envParser = new EnvironmentParser({
|
|
142
|
+
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
|
|
143
|
+
NEXT_PUBLIC_AUTH_URL: process.env.NEXT_PUBLIC_AUTH_URL,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
export const clientConfig = envParser
|
|
147
|
+
.create((get) => ({
|
|
148
|
+
apiUrl: get('NEXT_PUBLIC_API_URL').string(),
|
|
149
|
+
authUrl: get('NEXT_PUBLIC_AUTH_URL').string(),
|
|
150
|
+
}))
|
|
151
|
+
.parse();
|
|
152
|
+
`;
|
|
153
|
+
|
|
154
|
+
// Server config - server-only vars (not available in browser)
|
|
155
|
+
const serverConfigTs = `import { EnvironmentParser } from '@geekmidas/envkit';
|
|
156
|
+
|
|
157
|
+
// Server config - all env vars (server-side only, not exposed to browser)
|
|
158
|
+
// Access these only in Server Components, Route Handlers, or Server Actions
|
|
159
|
+
const envParser = new EnvironmentParser({ ...process.env });
|
|
160
|
+
|
|
161
|
+
export const serverConfig = envParser
|
|
162
|
+
.create((get) => ({
|
|
163
|
+
// Add server-only secrets here
|
|
164
|
+
// Example: stripeSecretKey: get('STRIPE_SECRET_KEY').string(),
|
|
165
|
+
}))
|
|
166
|
+
.parse();
|
|
167
|
+
`;
|
|
168
|
+
|
|
136
169
|
// Auth client for better-auth
|
|
137
170
|
const authClientTs = `import { createAuthClient } from 'better-auth/react';
|
|
138
171
|
import { magicLinkClient } from 'better-auth/client/plugins';
|
|
172
|
+
import { clientConfig } from '~/config/client';
|
|
139
173
|
|
|
140
174
|
export const authClient = createAuthClient({
|
|
141
|
-
baseURL:
|
|
175
|
+
baseURL: clientConfig.authUrl,
|
|
142
176
|
plugins: [magicLinkClient()],
|
|
143
177
|
});
|
|
144
178
|
|
|
@@ -163,9 +197,10 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
|
|
163
197
|
// API client setup - uses createApi with shared QueryClient
|
|
164
198
|
const apiIndexTs = `import { createApi } from './openapi';
|
|
165
199
|
import { getQueryClient } from '~/lib/query-client';
|
|
200
|
+
import { clientConfig } from '~/config/client';
|
|
166
201
|
|
|
167
202
|
export const api = createApi({
|
|
168
|
-
baseURL:
|
|
203
|
+
baseURL: clientConfig.apiUrl,
|
|
169
204
|
queryClient: getQueryClient(),
|
|
170
205
|
});
|
|
171
206
|
`;
|
|
@@ -295,6 +330,14 @@ node_modules/
|
|
|
295
330
|
path: 'apps/web/src/app/page.tsx',
|
|
296
331
|
content: pageTsx,
|
|
297
332
|
},
|
|
333
|
+
{
|
|
334
|
+
path: 'apps/web/src/config/client.ts',
|
|
335
|
+
content: clientConfigTs,
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
path: 'apps/web/src/config/server.ts',
|
|
339
|
+
content: serverConfigTs,
|
|
340
|
+
},
|
|
298
341
|
{
|
|
299
342
|
path: 'apps/web/src/lib/query-client.ts',
|
|
300
343
|
content: queryClientTs,
|
package/src/workspace/schema.ts
CHANGED
|
@@ -543,6 +543,14 @@ const AppConfigSchema = z
|
|
|
543
543
|
framework: FrameworkSchema.optional(),
|
|
544
544
|
client: ClientConfigSchema.optional(),
|
|
545
545
|
|
|
546
|
+
// Frontend-specific: config file paths for env sniffing (calls .parse() at import)
|
|
547
|
+
config: z
|
|
548
|
+
.object({
|
|
549
|
+
client: z.string().optional(),
|
|
550
|
+
server: z.string().optional(),
|
|
551
|
+
})
|
|
552
|
+
.optional(),
|
|
553
|
+
|
|
546
554
|
// Auth-specific
|
|
547
555
|
provider: AuthProviderSchema.optional(),
|
|
548
556
|
})
|