@geekmidas/cli 1.10.36 → 1.10.38
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 +16 -0
- package/dist/{config-U-gojtxn.mjs → config-Bbu1JKvf.mjs} +2 -2
- package/dist/{config-U-gojtxn.mjs.map → config-Bbu1JKvf.mjs.map} +1 -1
- package/dist/{config-QnuOcYXp.cjs → config-DujV8DbR.cjs} +2 -2
- package/dist/{config-QnuOcYXp.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/{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-D7iT4dnv.d.mts → index-BBvGMG_A.d.mts} +124 -67
- package/dist/index-BBvGMG_A.d.mts.map +1 -0
- package/dist/{index-DRQq26DF.d.cts → index-Dz2a7xQU.d.cts} +124 -67
- package/dist/index-Dz2a7xQU.d.cts.map +1 -0
- package/dist/index.cjs +247 -94
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +247 -94
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-BA0e3I_s.mjs → openapi-DF9GyhyZ.mjs} +11 -4
- package/dist/{openapi-BA0e3I_s.mjs.map → openapi-DF9GyhyZ.mjs.map} +1 -1
- package/dist/{openapi-Bb4UEyZN.cjs → openapi-DHp49UJV.cjs} +10 -3
- package/dist/{openapi-Bb4UEyZN.cjs.map → openapi-DHp49UJV.cjs.map} +1 -1
- package/dist/openapi.cjs +3 -3
- 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 +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-BobDOIG9.cjs → workspace-CQTT7HJQ.cjs} +34 -25
- package/dist/workspace-CQTT7HJQ.cjs.map +1 -0
- package/dist/{workspace-Cgmvgwh8.mjs → workspace-D-37Ylqo.mjs} +34 -25
- package/dist/workspace-D-37Ylqo.mjs.map +1 -0
- package/docs/dev-server-resilience-design.md +189 -0
- package/package.json +5 -5
- 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 +1 -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/index.ts +12 -1
- package/src/deploy/sniffer.ts +1 -1
- package/src/deploy/types.ts +1 -1
- package/src/dev/__tests__/index.spec.ts +11 -11
- package/src/dev/index.ts +14 -8
- 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 +98 -0
- package/src/init/__tests__/init.spec.ts +1 -1
- package/src/init/generators/monorepo.ts +3 -3
- package/src/init/versions.ts +3 -3
- package/src/openapi.ts +5 -1
- 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 +4 -4
- 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 +1 -1
- package/src/workspace/publicEnv.ts +2 -4
- package/src/workspace/schema.ts +126 -27
- package/src/workspace/types.ts +40 -69
- package/dist/index-D7iT4dnv.d.mts.map +0 -1
- package/dist/index-DRQq26DF.d.cts.map +0 -1
- package/dist/workspace-BobDOIG9.cjs.map +0 -1
- package/dist/workspace-Cgmvgwh8.mjs.map +0 -1
package/src/deploy/domain.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getPublicEnvPrefix } from '../workspace/index.js';
|
|
1
2
|
import type {
|
|
2
3
|
DokployWorkspaceConfig,
|
|
3
4
|
NormalizedAppConfig,
|
|
@@ -71,7 +72,7 @@ export function isMainFrontendApp(
|
|
|
71
72
|
app: NormalizedAppConfig,
|
|
72
73
|
allApps: Record<string, NormalizedAppConfig>,
|
|
73
74
|
): boolean {
|
|
74
|
-
if (app.type !== '
|
|
75
|
+
if (app.type !== 'web') {
|
|
75
76
|
return false;
|
|
76
77
|
}
|
|
77
78
|
|
|
@@ -82,7 +83,7 @@ export function isMainFrontendApp(
|
|
|
82
83
|
|
|
83
84
|
// Otherwise, check if this is the first frontend
|
|
84
85
|
for (const [name, a] of Object.entries(allApps)) {
|
|
85
|
-
if (a.type === '
|
|
86
|
+
if (a.type === 'web') {
|
|
86
87
|
return name === appName;
|
|
87
88
|
}
|
|
88
89
|
}
|
|
@@ -91,9 +92,13 @@ export function isMainFrontendApp(
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
/**
|
|
94
|
-
* Generate public URL build args for a
|
|
95
|
+
* Generate public URL build args for a web/mobile app based on its dependencies.
|
|
95
96
|
*
|
|
96
|
-
*
|
|
97
|
+
* The prefix is chosen by the app's framework (NEXT_PUBLIC_, VITE_,
|
|
98
|
+
* EXPO_PUBLIC_, or none for Remix). Apps without a public prefix get no
|
|
99
|
+
* build args — they should fetch URLs at runtime instead.
|
|
100
|
+
*
|
|
101
|
+
* @param app - The web/mobile app configuration
|
|
97
102
|
* @param deployedUrls - Map of app name to deployed public URL
|
|
98
103
|
* @returns Array of build args like 'NEXT_PUBLIC_API_URL=https://api.example.com'
|
|
99
104
|
*/
|
|
@@ -101,14 +106,14 @@ export function generatePublicUrlBuildArgs(
|
|
|
101
106
|
app: NormalizedAppConfig,
|
|
102
107
|
deployedUrls: Record<string, string>,
|
|
103
108
|
): string[] {
|
|
104
|
-
const
|
|
109
|
+
const prefix = getPublicEnvPrefix(app);
|
|
110
|
+
if (!prefix) return [];
|
|
105
111
|
|
|
112
|
+
const buildArgs: string[] = [];
|
|
106
113
|
for (const dep of app.dependencies) {
|
|
107
114
|
const publicUrl = deployedUrls[dep];
|
|
108
115
|
if (publicUrl) {
|
|
109
|
-
|
|
110
|
-
const envVarName = `NEXT_PUBLIC_${dep.toUpperCase()}_URL`;
|
|
111
|
-
buildArgs.push(`${envVarName}=${publicUrl}`);
|
|
116
|
+
buildArgs.push(`${prefix}${dep.toUpperCase()}_URL=${publicUrl}`);
|
|
112
117
|
}
|
|
113
118
|
}
|
|
114
119
|
|
|
@@ -118,9 +123,12 @@ export function generatePublicUrlBuildArgs(
|
|
|
118
123
|
/**
|
|
119
124
|
* Get public URL arg names from app dependencies.
|
|
120
125
|
*
|
|
121
|
-
* @param app - The
|
|
122
|
-
* @returns Array of arg names like 'NEXT_PUBLIC_API_URL'
|
|
126
|
+
* @param app - The web/mobile app configuration
|
|
127
|
+
* @returns Array of arg names like 'NEXT_PUBLIC_API_URL', or [] for frameworks
|
|
128
|
+
* without a public prefix (e.g. Remix).
|
|
123
129
|
*/
|
|
124
130
|
export function getPublicUrlArgNames(app: NormalizedAppConfig): string[] {
|
|
125
|
-
|
|
131
|
+
const prefix = getPublicEnvPrefix(app);
|
|
132
|
+
if (!prefix) return [];
|
|
133
|
+
return app.dependencies.map((dep) => `${prefix}${dep.toUpperCase()}_URL`);
|
|
126
134
|
}
|
package/src/deploy/index.ts
CHANGED
|
@@ -1202,11 +1202,22 @@ export async function workspaceDeployCommand(
|
|
|
1202
1202
|
// ==================================================================
|
|
1203
1203
|
// Separate apps by type for two-phase deployment
|
|
1204
1204
|
// ==================================================================
|
|
1205
|
+
// Mobile apps deploy via their own toolchain (e.g. EAS Build for Expo)
|
|
1206
|
+
// — skip them in the Dokploy deploy phases.
|
|
1207
|
+
const skippedMobileApps = appsToDeployNames.filter(
|
|
1208
|
+
(name) => workspace.apps[name]!.type === 'mobile',
|
|
1209
|
+
);
|
|
1210
|
+
if (skippedMobileApps.length > 0) {
|
|
1211
|
+
logger.log(
|
|
1212
|
+
`\n📱 Skipping ${skippedMobileApps.length} mobile app(s) — deploy via framework toolchain: ${skippedMobileApps.join(', ')}`,
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1205
1216
|
const backendApps = appsToDeployNames.filter(
|
|
1206
1217
|
(name) => workspace.apps[name]!.type === 'backend',
|
|
1207
1218
|
);
|
|
1208
1219
|
const frontendApps = appsToDeployNames.filter(
|
|
1209
|
-
(name) => workspace.apps[name]!.type === '
|
|
1220
|
+
(name) => workspace.apps[name]!.type === 'web',
|
|
1210
1221
|
);
|
|
1211
1222
|
|
|
1212
1223
|
// ==================================================================
|
package/src/deploy/sniffer.ts
CHANGED
|
@@ -117,7 +117,7 @@ export async function sniffAppEnvironment(
|
|
|
117
117
|
const { logWarnings = true, markOptional = false } = options;
|
|
118
118
|
|
|
119
119
|
// 1. Frontend apps - handle dependencies and config sniffing
|
|
120
|
-
if (app.type === '
|
|
120
|
+
if (app.type === 'web' || app.type === 'mobile') {
|
|
121
121
|
// Auto-generate {prefix}{DEP}_URL from dependencies, where prefix matches
|
|
122
122
|
// the framework's public-var convention (NEXT_PUBLIC_, VITE_, ...).
|
|
123
123
|
// For frameworks without a prefix (e.g. Remix), no dep var is required.
|
package/src/deploy/types.ts
CHANGED
|
@@ -455,13 +455,13 @@ describe('Workspace Dev Server', () => {
|
|
|
455
455
|
dependencies: [],
|
|
456
456
|
},
|
|
457
457
|
web: {
|
|
458
|
-
type: '
|
|
458
|
+
type: 'web',
|
|
459
459
|
path: 'apps/web',
|
|
460
460
|
port: 3001,
|
|
461
461
|
dependencies: [],
|
|
462
462
|
},
|
|
463
463
|
admin: {
|
|
464
|
-
type: '
|
|
464
|
+
type: 'web',
|
|
465
465
|
path: 'apps/admin',
|
|
466
466
|
port: 3002,
|
|
467
467
|
dependencies: [],
|
|
@@ -481,7 +481,7 @@ describe('Workspace Dev Server', () => {
|
|
|
481
481
|
dependencies: [],
|
|
482
482
|
},
|
|
483
483
|
web: {
|
|
484
|
-
type: '
|
|
484
|
+
type: 'web',
|
|
485
485
|
path: 'apps/web',
|
|
486
486
|
port: 3000, // Same port as api!
|
|
487
487
|
dependencies: [],
|
|
@@ -512,13 +512,13 @@ describe('Workspace Dev Server', () => {
|
|
|
512
512
|
dependencies: [],
|
|
513
513
|
},
|
|
514
514
|
web: {
|
|
515
|
-
type: '
|
|
515
|
+
type: 'web',
|
|
516
516
|
path: 'apps/web',
|
|
517
517
|
port: 3001,
|
|
518
518
|
dependencies: [],
|
|
519
519
|
},
|
|
520
520
|
admin: {
|
|
521
|
-
type: '
|
|
521
|
+
type: 'web',
|
|
522
522
|
path: 'apps/admin',
|
|
523
523
|
port: 3001, // Conflicts with web
|
|
524
524
|
dependencies: [],
|
|
@@ -554,7 +554,7 @@ describe('Workspace Dev Server', () => {
|
|
|
554
554
|
dependencies: [],
|
|
555
555
|
},
|
|
556
556
|
web: {
|
|
557
|
-
type: '
|
|
557
|
+
type: 'web',
|
|
558
558
|
path: 'apps/web',
|
|
559
559
|
port: 3001,
|
|
560
560
|
dependencies: [],
|
|
@@ -580,7 +580,7 @@ describe('Workspace Dev Server', () => {
|
|
|
580
580
|
dependencies: [],
|
|
581
581
|
},
|
|
582
582
|
web: {
|
|
583
|
-
type: '
|
|
583
|
+
type: 'web',
|
|
584
584
|
path: 'apps/web',
|
|
585
585
|
port: 3002,
|
|
586
586
|
dependencies: ['api', 'auth'],
|
|
@@ -634,7 +634,7 @@ describe('Workspace Dev Server', () => {
|
|
|
634
634
|
dependencies: [],
|
|
635
635
|
},
|
|
636
636
|
web: {
|
|
637
|
-
type: '
|
|
637
|
+
type: 'web',
|
|
638
638
|
path: 'apps/web',
|
|
639
639
|
port: 3001,
|
|
640
640
|
dependencies: ['api'],
|
|
@@ -669,13 +669,13 @@ describe('Workspace Dev Server', () => {
|
|
|
669
669
|
dependencies: ['auth'], // Payments depends on auth
|
|
670
670
|
},
|
|
671
671
|
web: {
|
|
672
|
-
type: '
|
|
672
|
+
type: 'web',
|
|
673
673
|
path: 'apps/web',
|
|
674
674
|
port: 3003,
|
|
675
675
|
dependencies: ['api', 'auth'], // Web depends on api and auth
|
|
676
676
|
},
|
|
677
677
|
admin: {
|
|
678
|
-
type: '
|
|
678
|
+
type: 'web',
|
|
679
679
|
path: 'apps/admin',
|
|
680
680
|
port: 3004,
|
|
681
681
|
dependencies: ['api', 'payments'], // Admin depends on api and payments
|
|
@@ -1056,7 +1056,7 @@ describe('Workspace Dev Server', () => {
|
|
|
1056
1056
|
resolvedDeployTarget: 'dokploy',
|
|
1057
1057
|
},
|
|
1058
1058
|
web: {
|
|
1059
|
-
type: '
|
|
1059
|
+
type: 'web',
|
|
1060
1060
|
path: 'apps/web',
|
|
1061
1061
|
port: 3001,
|
|
1062
1062
|
dependencies: ['api'],
|
package/src/dev/index.ts
CHANGED
|
@@ -856,7 +856,7 @@ export async function validateFrontendApps(
|
|
|
856
856
|
const results: FrontendValidationResult[] = [];
|
|
857
857
|
|
|
858
858
|
for (const [appName, app] of Object.entries(workspace.apps)) {
|
|
859
|
-
if (app.type === '
|
|
859
|
+
if (app.type === 'web') {
|
|
860
860
|
const result = await validateFrontendApp(
|
|
861
861
|
appName,
|
|
862
862
|
app.path,
|
|
@@ -920,13 +920,19 @@ async function workspaceDevCommand(
|
|
|
920
920
|
([_, app]) => app.type === 'backend',
|
|
921
921
|
);
|
|
922
922
|
const frontendApps = Object.entries(workspace.apps).filter(
|
|
923
|
-
([_, app]) => app.type === '
|
|
923
|
+
([_, app]) => app.type === 'web',
|
|
924
|
+
);
|
|
925
|
+
const mobileApps = Object.entries(workspace.apps).filter(
|
|
926
|
+
([_, app]) => app.type === 'mobile',
|
|
924
927
|
);
|
|
925
928
|
|
|
926
929
|
logger.log(`\n🚀 Starting workspace: ${workspace.name}`);
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
+
const counts = [
|
|
931
|
+
`${backendApps.length} backend`,
|
|
932
|
+
`${frontendApps.length} web`,
|
|
933
|
+
];
|
|
934
|
+
if (mobileApps.length > 0) counts.push(`${mobileApps.length} mobile`);
|
|
935
|
+
logger.log(` ${counts.join(', ')} app(s)`);
|
|
930
936
|
|
|
931
937
|
// Check for port conflicts
|
|
932
938
|
const conflicts = checkPortConflicts(workspace);
|
|
@@ -1030,9 +1036,9 @@ async function workspaceDevCommand(
|
|
|
1030
1036
|
app.dependencies.length > 0
|
|
1031
1037
|
? ` (depends on: ${app.dependencies.join(', ')})`
|
|
1032
1038
|
: '';
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
);
|
|
1039
|
+
const icon =
|
|
1040
|
+
app.type === 'backend' ? '🔧' : app.type === 'mobile' ? '📱' : '🌐';
|
|
1041
|
+
logger.log(` ${icon} ${appName} → http://localhost:${app.port}${deps}`);
|
|
1036
1042
|
}
|
|
1037
1043
|
|
|
1038
1044
|
// Find the config file path for GKM_CONFIG_PATH
|
|
@@ -647,7 +647,7 @@ describe('generateWorkspaceCompose', () => {
|
|
|
647
647
|
resolvedDeployTarget: 'dokploy',
|
|
648
648
|
},
|
|
649
649
|
web: {
|
|
650
|
-
type: '
|
|
650
|
+
type: 'web',
|
|
651
651
|
path: 'apps/web',
|
|
652
652
|
port: 3001,
|
|
653
653
|
dependencies: ['api'],
|
|
@@ -793,7 +793,7 @@ describe('generateWorkspaceCompose', () => {
|
|
|
793
793
|
resolvedDeployTarget: 'dokploy',
|
|
794
794
|
},
|
|
795
795
|
web: {
|
|
796
|
-
type: '
|
|
796
|
+
type: 'web',
|
|
797
797
|
path: 'apps/web',
|
|
798
798
|
port: 3001,
|
|
799
799
|
dependencies: ['api', 'auth'],
|
|
@@ -849,7 +849,7 @@ describe('generateWorkspaceCompose', () => {
|
|
|
849
849
|
const workspace = createWorkspace({
|
|
850
850
|
apps: {
|
|
851
851
|
web: {
|
|
852
|
-
type: '
|
|
852
|
+
type: 'web',
|
|
853
853
|
path: 'apps/web',
|
|
854
854
|
port: 3001,
|
|
855
855
|
dependencies: [],
|
|
@@ -1016,7 +1016,7 @@ describe('generateWorkspaceCompose', () => {
|
|
|
1016
1016
|
const workspace = createWorkspace({
|
|
1017
1017
|
apps: {
|
|
1018
1018
|
web: {
|
|
1019
|
-
type: '
|
|
1019
|
+
type: 'web',
|
|
1020
1020
|
path: 'apps/web',
|
|
1021
1021
|
port: 3001,
|
|
1022
1022
|
dependencies: [],
|
package/src/docker/compose.ts
CHANGED
|
@@ -444,8 +444,10 @@ export function generateWorkspaceCompose(
|
|
|
444
444
|
services:
|
|
445
445
|
`;
|
|
446
446
|
|
|
447
|
-
// Generate service for each app
|
|
447
|
+
// Generate service for each app (mobile apps deploy via their own
|
|
448
|
+
// toolchain — no compose service)
|
|
448
449
|
for (const [appName, app] of apps) {
|
|
450
|
+
if (app.type === 'mobile') continue;
|
|
449
451
|
yaml += generateAppService(appName, app, apps, {
|
|
450
452
|
registry,
|
|
451
453
|
projectName: workspace.name,
|
|
@@ -698,9 +700,9 @@ function generateAppService(
|
|
|
698
700
|
const imageRef = registry ? `\${REGISTRY:-${registry}}/` : '';
|
|
699
701
|
|
|
700
702
|
// Health check path - frontends use /, backends use /health
|
|
701
|
-
const healthCheckPath = app.type === '
|
|
703
|
+
const healthCheckPath = app.type === 'web' ? '/' : '/health';
|
|
702
704
|
const healthCheckCmd =
|
|
703
|
-
app.type === '
|
|
705
|
+
app.type === 'web'
|
|
704
706
|
? `["CMD", "wget", "-q", "--spider", "http://localhost:${app.port}/"]`
|
|
705
707
|
: `["CMD", "wget", "-q", "--spider", "http://localhost:${app.port}${healthCheckPath}"]`;
|
|
706
708
|
|
package/src/docker/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { copyFileSync, existsSync, readFileSync, unlinkSync } from 'node:fs';
|
|
|
3
3
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { basename, join } from 'node:path';
|
|
5
5
|
import { loadConfig, loadWorkspaceConfig } from '../config';
|
|
6
|
+
import { getPublicUrlArgNames } from '../deploy/domain.js';
|
|
6
7
|
import type { NormalizedWorkspace } from '../workspace/types.js';
|
|
7
8
|
import {
|
|
8
9
|
generateDockerCompose,
|
|
@@ -18,7 +19,9 @@ import {
|
|
|
18
19
|
generateEntryDockerfile,
|
|
19
20
|
generateMultiStageDockerfile,
|
|
20
21
|
generateNextjsDockerfile,
|
|
22
|
+
generateNodeWebDockerfile,
|
|
21
23
|
generateSlimDockerfile,
|
|
24
|
+
generateViteStaticDockerfile,
|
|
22
25
|
hasTurboConfig,
|
|
23
26
|
isMonorepo,
|
|
24
27
|
resolveDockerConfig,
|
|
@@ -342,7 +345,7 @@ async function pushDockerImage(
|
|
|
342
345
|
*/
|
|
343
346
|
export interface AppDockerResult {
|
|
344
347
|
appName: string;
|
|
345
|
-
type: 'backend' | '
|
|
348
|
+
type: 'backend' | 'web' | 'mobile';
|
|
346
349
|
dockerfile: string;
|
|
347
350
|
imageName: string;
|
|
348
351
|
}
|
|
@@ -396,6 +399,15 @@ export async function workspaceDockerCommand(
|
|
|
396
399
|
|
|
397
400
|
// Generate Dockerfile for each app
|
|
398
401
|
for (const [appName, app] of apps) {
|
|
402
|
+
// Mobile apps deploy via their own toolchain (e.g. EAS Build for Expo)
|
|
403
|
+
// — no Docker image is produced.
|
|
404
|
+
if (app.type === 'mobile') {
|
|
405
|
+
logger.log(
|
|
406
|
+
`\n 📱 Skipping Docker for ${appName} (mobile app — deploy via framework toolchain)`,
|
|
407
|
+
);
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
|
|
399
411
|
const appPath = app.path;
|
|
400
412
|
const fullAppPath = join(workspace.root, appPath);
|
|
401
413
|
|
|
@@ -411,16 +423,31 @@ export async function workspaceDockerCommand(
|
|
|
411
423
|
|
|
412
424
|
let dockerfile: string;
|
|
413
425
|
|
|
414
|
-
if (app.type === '
|
|
415
|
-
|
|
416
|
-
|
|
426
|
+
if (app.type === 'web') {
|
|
427
|
+
const publicUrlArgs = getPublicUrlArgNames(app);
|
|
428
|
+
const webOpts = {
|
|
417
429
|
imageName,
|
|
418
430
|
baseImage: 'node:22-alpine',
|
|
419
431
|
port: app.port,
|
|
420
432
|
appPath,
|
|
421
433
|
turboPackage,
|
|
422
434
|
packageManager,
|
|
423
|
-
|
|
435
|
+
publicUrlArgs,
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
switch (app.framework) {
|
|
439
|
+
case 'vite':
|
|
440
|
+
dockerfile = generateViteStaticDockerfile(webOpts);
|
|
441
|
+
break;
|
|
442
|
+
case 'tanstack-start':
|
|
443
|
+
case 'remix':
|
|
444
|
+
dockerfile = generateNodeWebDockerfile(webOpts);
|
|
445
|
+
break;
|
|
446
|
+
default:
|
|
447
|
+
// nextjs (and any unspecified web framework — schema requires
|
|
448
|
+
// a valid framework, so this is just a default).
|
|
449
|
+
dockerfile = generateNextjsDockerfile(webOpts);
|
|
450
|
+
}
|
|
424
451
|
} else if (app.entry) {
|
|
425
452
|
// Backend with custom entry point - use tsdown bundling
|
|
426
453
|
dockerfile = generateEntryDockerfile({
|
|
@@ -479,7 +506,8 @@ export async function workspaceDockerCommand(
|
|
|
479
506
|
);
|
|
480
507
|
logger.log('\n📋 Build commands:');
|
|
481
508
|
for (const result of results) {
|
|
482
|
-
const icon =
|
|
509
|
+
const icon =
|
|
510
|
+
result.type === 'backend' ? '⚙️' : result.type === 'mobile' ? '📱' : '🌐';
|
|
483
511
|
logger.log(
|
|
484
512
|
` ${icon} docker build -f .gkm/docker/Dockerfile.${result.appName} -t ${result.imageName} .`,
|
|
485
513
|
);
|
package/src/docker/templates.ts
CHANGED
|
@@ -927,3 +927,141 @@ ENTRYPOINT ["/sbin/tini", "--"]
|
|
|
927
927
|
CMD ["node", "index.mjs"]
|
|
928
928
|
`;
|
|
929
929
|
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Generate a Dockerfile for Node-based SSR web frameworks (TanStack Start,
|
|
933
|
+
* Remix). Builds with turbo, then runs the framework's production start
|
|
934
|
+
* script via `npm start`.
|
|
935
|
+
*
|
|
936
|
+
* Public URL build args are declared so framework bundlers (Vite, Vinxi)
|
|
937
|
+
* can inline them at build time. Per-framework prefix is the caller's job.
|
|
938
|
+
* @internal Exported for testing
|
|
939
|
+
*/
|
|
940
|
+
export function generateNodeWebDockerfile(
|
|
941
|
+
options: FrontendDockerfileOptions,
|
|
942
|
+
): string {
|
|
943
|
+
const {
|
|
944
|
+
baseImage,
|
|
945
|
+
port,
|
|
946
|
+
appPath,
|
|
947
|
+
turboPackage,
|
|
948
|
+
packageManager,
|
|
949
|
+
publicUrlArgs = [],
|
|
950
|
+
} = options;
|
|
951
|
+
|
|
952
|
+
const pm = getPmConfig(packageManager);
|
|
953
|
+
const installPm = pm.install ? `RUN ${pm.install}` : '';
|
|
954
|
+
const turboInstallCmd = getTurboInstallCmd(packageManager);
|
|
955
|
+
const turboCmd = packageManager === 'pnpm' ? 'pnpm dlx turbo' : 'npx turbo';
|
|
956
|
+
const startCmd =
|
|
957
|
+
packageManager === 'pnpm'
|
|
958
|
+
? 'pnpm start'
|
|
959
|
+
: `${pm.lockfile.startsWith('package-lock') ? 'npm' : packageManager} start`;
|
|
960
|
+
|
|
961
|
+
const argDecls = publicUrlArgs.map((a) => `ARG ${a}=""`).join('\n');
|
|
962
|
+
const envDecls = publicUrlArgs.map((a) => `ENV ${a}=$${a}`).join('\n');
|
|
963
|
+
|
|
964
|
+
return `# syntax=docker/dockerfile:1
|
|
965
|
+
# Node SSR web Dockerfile (TanStack Start / Remix) with turbo prune
|
|
966
|
+
|
|
967
|
+
FROM ${baseImage} AS pruner
|
|
968
|
+
WORKDIR /app
|
|
969
|
+
${installPm}
|
|
970
|
+
COPY . .
|
|
971
|
+
RUN ${turboCmd} prune ${turboPackage} --docker
|
|
972
|
+
|
|
973
|
+
FROM ${baseImage} AS deps
|
|
974
|
+
WORKDIR /app
|
|
975
|
+
${installPm}
|
|
976
|
+
COPY --from=pruner /app/out/${pm.lockfile} ./
|
|
977
|
+
COPY --from=pruner /app/out/json/ ./
|
|
978
|
+
RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
|
|
979
|
+
${turboInstallCmd}
|
|
980
|
+
|
|
981
|
+
FROM deps AS builder
|
|
982
|
+
WORKDIR /app
|
|
983
|
+
${argDecls}
|
|
984
|
+
${envDecls}
|
|
985
|
+
COPY --from=pruner /app/out/full/ ./
|
|
986
|
+
COPY --from=pruner /app/tsconfig.* ./
|
|
987
|
+
RUN ${turboCmd} run build --filter=${turboPackage}
|
|
988
|
+
|
|
989
|
+
FROM ${baseImage} AS runner
|
|
990
|
+
WORKDIR /app
|
|
991
|
+
RUN apk add --no-cache tini
|
|
992
|
+
RUN addgroup --system --gid 1001 nodejs && \\
|
|
993
|
+
adduser --system --uid 1001 app
|
|
994
|
+
|
|
995
|
+
# Copy the whole built workspace — frameworks differ on output paths
|
|
996
|
+
COPY --from=builder --chown=app:nodejs /app/ ./
|
|
997
|
+
|
|
998
|
+
ENV NODE_ENV=production
|
|
999
|
+
ENV PORT=${port}
|
|
1000
|
+
ENV HOSTNAME="0.0.0.0"
|
|
1001
|
+
|
|
1002
|
+
USER app
|
|
1003
|
+
EXPOSE ${port}
|
|
1004
|
+
|
|
1005
|
+
ENTRYPOINT ["/sbin/tini", "--"]
|
|
1006
|
+
CMD ["sh", "-c", "cd ${appPath} && ${startCmd}"]
|
|
1007
|
+
`;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* Generate a Dockerfile for Vite SPA apps. Builds the static bundle with
|
|
1012
|
+
* turbo, then serves it via nginx.
|
|
1013
|
+
* @internal Exported for testing
|
|
1014
|
+
*/
|
|
1015
|
+
export function generateViteStaticDockerfile(
|
|
1016
|
+
options: FrontendDockerfileOptions,
|
|
1017
|
+
): string {
|
|
1018
|
+
const {
|
|
1019
|
+
baseImage,
|
|
1020
|
+
port,
|
|
1021
|
+
appPath,
|
|
1022
|
+
turboPackage,
|
|
1023
|
+
packageManager,
|
|
1024
|
+
publicUrlArgs = [],
|
|
1025
|
+
} = options;
|
|
1026
|
+
|
|
1027
|
+
const pm = getPmConfig(packageManager);
|
|
1028
|
+
const installPm = pm.install ? `RUN ${pm.install}` : '';
|
|
1029
|
+
const turboInstallCmd = getTurboInstallCmd(packageManager);
|
|
1030
|
+
const turboCmd = packageManager === 'pnpm' ? 'pnpm dlx turbo' : 'npx turbo';
|
|
1031
|
+
|
|
1032
|
+
const argDecls = publicUrlArgs.map((a) => `ARG ${a}=""`).join('\n');
|
|
1033
|
+
const envDecls = publicUrlArgs.map((a) => `ENV ${a}=$${a}`).join('\n');
|
|
1034
|
+
|
|
1035
|
+
return `# syntax=docker/dockerfile:1
|
|
1036
|
+
# Vite SPA Dockerfile — builds static assets, serves via nginx
|
|
1037
|
+
|
|
1038
|
+
FROM ${baseImage} AS pruner
|
|
1039
|
+
WORKDIR /app
|
|
1040
|
+
${installPm}
|
|
1041
|
+
COPY . .
|
|
1042
|
+
RUN ${turboCmd} prune ${turboPackage} --docker
|
|
1043
|
+
|
|
1044
|
+
FROM ${baseImage} AS deps
|
|
1045
|
+
WORKDIR /app
|
|
1046
|
+
${installPm}
|
|
1047
|
+
COPY --from=pruner /app/out/${pm.lockfile} ./
|
|
1048
|
+
COPY --from=pruner /app/out/json/ ./
|
|
1049
|
+
RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
|
|
1050
|
+
${turboInstallCmd}
|
|
1051
|
+
|
|
1052
|
+
FROM deps AS builder
|
|
1053
|
+
WORKDIR /app
|
|
1054
|
+
${argDecls}
|
|
1055
|
+
${envDecls}
|
|
1056
|
+
COPY --from=pruner /app/out/full/ ./
|
|
1057
|
+
COPY --from=pruner /app/tsconfig.* ./
|
|
1058
|
+
RUN ${turboCmd} run build --filter=${turboPackage}
|
|
1059
|
+
|
|
1060
|
+
FROM nginx:alpine AS runner
|
|
1061
|
+
COPY --from=builder /app/${appPath}/dist /usr/share/nginx/html
|
|
1062
|
+
RUN printf 'server {\\n listen ${port};\\n root /usr/share/nginx/html;\\n index index.html;\\n location / { try_files $uri $uri/ /index.html; }\\n}\\n' \\
|
|
1063
|
+
> /etc/nginx/conf.d/default.conf
|
|
1064
|
+
EXPOSE ${port}
|
|
1065
|
+
CMD ["nginx", "-g", "daemon off;"]
|
|
1066
|
+
`;
|
|
1067
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Endpoint } from '@geekmidas/constructs/endpoints';
|
|
2
2
|
import {
|
|
3
|
+
getRegisteredZodJsonSchemas,
|
|
3
4
|
getSchemaMetadata,
|
|
4
5
|
StandardSchemaJsonSchema,
|
|
5
6
|
} from '@geekmidas/schema/conversion';
|
|
@@ -313,6 +314,27 @@ export class OpenApiTsGenerator {
|
|
|
313
314
|
}
|
|
314
315
|
}
|
|
315
316
|
|
|
317
|
+
// Pull in every schema registered via `.meta({ id })` in zod's global
|
|
318
|
+
// registry, even when no endpoint references it directly. This makes
|
|
319
|
+
// derived schemas like `UserSchema.pick(...)` discoverable through the
|
|
320
|
+
// named parent type without requiring the parent to be wired into an
|
|
321
|
+
// endpoint.
|
|
322
|
+
const registrySchemas = await getRegisteredZodJsonSchemas();
|
|
323
|
+
for (const [rawName, rawSchema] of Object.entries(registrySchemas)) {
|
|
324
|
+
const defName = this.pascalCase(rawName);
|
|
325
|
+
if (collectedDefs.has(defName) || generatedNames.has(defName)) {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
const {
|
|
329
|
+
id: _id,
|
|
330
|
+
$defs: _defs,
|
|
331
|
+
...rest
|
|
332
|
+
} = rawSchema as JsonSchema & {
|
|
333
|
+
id?: string;
|
|
334
|
+
};
|
|
335
|
+
collectedDefs.set(defName, rest as JsonSchema);
|
|
336
|
+
}
|
|
337
|
+
|
|
316
338
|
// Generate interfaces for collected $defs (nested schemas with .meta({ id: 'X' }))
|
|
317
339
|
for (const [defName, defSchema] of collectedDefs) {
|
|
318
340
|
if (!generatedNames.has(defName)) {
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { e } from '@geekmidas/constructs/endpoints';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
3
|
+
import { z } from 'zod/v4';
|
|
4
|
+
import { OpenApiTsGenerator } from '../OpenApiTsGenerator';
|
|
5
|
+
|
|
6
|
+
describe('OpenApiTsGenerator — zod global registry', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
z.globalRegistry.clear();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
z.globalRegistry.clear();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('emits a named interface for a registered schema even when no endpoint references it directly', async () => {
|
|
16
|
+
const UserSchema = z
|
|
17
|
+
.object({
|
|
18
|
+
id: z.string(),
|
|
19
|
+
email: z.string(),
|
|
20
|
+
name: z.string(),
|
|
21
|
+
})
|
|
22
|
+
.meta({ id: 'User' });
|
|
23
|
+
|
|
24
|
+
const UserResponseSchema = UserSchema.pick({
|
|
25
|
+
id: true,
|
|
26
|
+
name: true,
|
|
27
|
+
email: true,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const endpoint = e
|
|
31
|
+
.get('/users/:id')
|
|
32
|
+
.output(UserResponseSchema)
|
|
33
|
+
.handle(async () => ({ id: '1', name: 'a', email: 'a@b.c' }));
|
|
34
|
+
|
|
35
|
+
const generator = new OpenApiTsGenerator();
|
|
36
|
+
const content = await generator.generate([endpoint as any]);
|
|
37
|
+
|
|
38
|
+
expect(content).toMatch(/export interface User\b/);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('does not duplicate when the same id is also produced by an endpoint', async () => {
|
|
42
|
+
const UserSchema = z
|
|
43
|
+
.object({
|
|
44
|
+
id: z.string(),
|
|
45
|
+
email: z.string(),
|
|
46
|
+
name: z.string(),
|
|
47
|
+
})
|
|
48
|
+
.meta({ id: 'User' });
|
|
49
|
+
|
|
50
|
+
const endpoint = e
|
|
51
|
+
.get('/users/:id')
|
|
52
|
+
.output(UserSchema)
|
|
53
|
+
.handle(async () => ({ id: '1', name: 'a', email: 'a@b.c' }));
|
|
54
|
+
|
|
55
|
+
const generator = new OpenApiTsGenerator();
|
|
56
|
+
const content = await generator.generate([endpoint as any]);
|
|
57
|
+
|
|
58
|
+
const occurrences = content.match(/export interface User\b/g) ?? [];
|
|
59
|
+
expect(occurrences.length).toBe(1);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('emits multiple registered schemas', async () => {
|
|
63
|
+
z.object({ id: z.string() }).meta({ id: 'Foo' });
|
|
64
|
+
z.object({ id: z.string() }).meta({ id: 'Bar' });
|
|
65
|
+
|
|
66
|
+
const endpoint = e
|
|
67
|
+
.get('/ping')
|
|
68
|
+
.output(z.object({ ok: z.boolean() }))
|
|
69
|
+
.handle(async () => ({ ok: true }));
|
|
70
|
+
|
|
71
|
+
const generator = new OpenApiTsGenerator();
|
|
72
|
+
const content = await generator.generate([endpoint as any]);
|
|
73
|
+
|
|
74
|
+
expect(content).toMatch(/export interface Foo\b/);
|
|
75
|
+
expect(content).toMatch(/export interface Bar\b/);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('references one registered schema from another via the parent type', async () => {
|
|
79
|
+
const UserSchema = z
|
|
80
|
+
.object({ id: z.string(), email: z.string() })
|
|
81
|
+
.meta({ id: 'User' });
|
|
82
|
+
|
|
83
|
+
z.object({ user: UserSchema, role: z.string() }).meta({ id: 'UserRole' });
|
|
84
|
+
|
|
85
|
+
const endpoint = e
|
|
86
|
+
.get('/ping')
|
|
87
|
+
.output(z.object({ ok: z.boolean() }))
|
|
88
|
+
.handle(async () => ({ ok: true }));
|
|
89
|
+
|
|
90
|
+
const generator = new OpenApiTsGenerator();
|
|
91
|
+
const content = await generator.generate([endpoint as any]);
|
|
92
|
+
|
|
93
|
+
expect(content).toMatch(/export interface User\b/);
|
|
94
|
+
expect(content).toMatch(/export interface UserRole\b/);
|
|
95
|
+
// `user` inside UserRole should reference the named User type, not inline.
|
|
96
|
+
expect(content).toMatch(/user:\s*User\b/);
|
|
97
|
+
});
|
|
98
|
+
});
|