@geekmidas/cli 0.18.0 → 0.20.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/dist/{bundler-C74EKlNa.cjs → bundler-CyHg1v_T.cjs} +3 -3
- package/dist/{bundler-C74EKlNa.cjs.map → bundler-CyHg1v_T.cjs.map} +1 -1
- package/dist/{bundler-B6z6HEeh.mjs → bundler-DQIuE3Kn.mjs} +3 -3
- package/dist/{bundler-B6z6HEeh.mjs.map → bundler-DQIuE3Kn.mjs.map} +1 -1
- package/dist/{config-DYULeEv8.mjs → config-BaYqrF3n.mjs} +48 -10
- package/dist/config-BaYqrF3n.mjs.map +1 -0
- package/dist/{config-AmInkU7k.cjs → config-CxrLu8ia.cjs} +53 -9
- package/dist/config-CxrLu8ia.cjs.map +1 -0
- package/dist/config.cjs +4 -1
- package/dist/config.d.cts +27 -2
- package/dist/config.d.cts.map +1 -1
- package/dist/config.d.mts +27 -2
- package/dist/config.d.mts.map +1 -1
- package/dist/config.mjs +3 -2
- package/dist/dokploy-api-B0w17y4_.mjs +3 -0
- package/dist/{dokploy-api-CaETb2L6.mjs → dokploy-api-B9qR2Yn1.mjs} +1 -1
- package/dist/{dokploy-api-CaETb2L6.mjs.map → dokploy-api-B9qR2Yn1.mjs.map} +1 -1
- package/dist/dokploy-api-BnGeUqN4.cjs +3 -0
- package/dist/{dokploy-api-C7F9VykY.cjs → dokploy-api-C5czOZoc.cjs} +1 -1
- package/dist/{dokploy-api-C7F9VykY.cjs.map → dokploy-api-C5czOZoc.cjs.map} +1 -1
- package/dist/{encryption-D7Efcdi9.cjs → encryption-BAz0xQ1Q.cjs} +1 -1
- package/dist/{encryption-D7Efcdi9.cjs.map → encryption-BAz0xQ1Q.cjs.map} +1 -1
- package/dist/{encryption-h4Nb6W-M.mjs → encryption-JtMsiGNp.mjs} +2 -2
- package/dist/{encryption-h4Nb6W-M.mjs.map → encryption-JtMsiGNp.mjs.map} +1 -1
- package/dist/index-CWN-bgrO.d.mts +495 -0
- package/dist/index-CWN-bgrO.d.mts.map +1 -0
- package/dist/index-DEWYvYvg.d.cts +495 -0
- package/dist/index-DEWYvYvg.d.cts.map +1 -0
- package/dist/index.cjs +2640 -564
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2635 -564
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-CZVcfxk-.mjs → openapi-CgqR6Jkw.mjs} +3 -3
- package/dist/{openapi-CZVcfxk-.mjs.map → openapi-CgqR6Jkw.mjs.map} +1 -1
- package/dist/{openapi-C89hhkZC.cjs → openapi-DfpxS0xv.cjs} +8 -2
- package/dist/{openapi-C89hhkZC.cjs.map → openapi-DfpxS0xv.cjs.map} +1 -1
- package/dist/{openapi-react-query-CM2_qlW9.mjs → openapi-react-query-5rSortLH.mjs} +1 -1
- package/dist/{openapi-react-query-CM2_qlW9.mjs.map → openapi-react-query-5rSortLH.mjs.map} +1 -1
- package/dist/{openapi-react-query-iKjfLzff.cjs → openapi-react-query-DvNpdDpM.cjs} +1 -1
- package/dist/{openapi-react-query-iKjfLzff.cjs.map → openapi-react-query-DvNpdDpM.cjs.map} +1 -1
- package/dist/openapi-react-query.cjs +1 -1
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/openapi.cjs +3 -2
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +3 -2
- package/dist/{storage-Bn3K9Ccu.cjs → storage-BPRgh3DU.cjs} +136 -5
- package/dist/storage-BPRgh3DU.cjs.map +1 -0
- package/dist/{storage-nkGIjeXt.mjs → storage-DNj_I11J.mjs} +1 -1
- package/dist/storage-Dhst7BhI.mjs +272 -0
- package/dist/storage-Dhst7BhI.mjs.map +1 -0
- package/dist/{storage-UfyTn7Zm.cjs → storage-fOR8dMu5.cjs} +1 -1
- package/dist/{types-iFk5ms7y.d.mts → types-K2uQJ-FO.d.mts} +2 -2
- package/dist/{types-BgaMXsUa.d.cts.map → types-K2uQJ-FO.d.mts.map} +1 -1
- package/dist/{types-BgaMXsUa.d.cts → types-l53qUmGt.d.cts} +2 -2
- package/dist/{types-iFk5ms7y.d.mts.map → types-l53qUmGt.d.cts.map} +1 -1
- package/dist/workspace/index.cjs +19 -0
- package/dist/workspace/index.d.cts +3 -0
- package/dist/workspace/index.d.mts +3 -0
- package/dist/workspace/index.mjs +3 -0
- package/dist/workspace-CPLEZDZf.mjs +3788 -0
- package/dist/workspace-CPLEZDZf.mjs.map +1 -0
- package/dist/workspace-iWgBlX6h.cjs +3885 -0
- package/dist/workspace-iWgBlX6h.cjs.map +1 -0
- package/package.json +9 -4
- package/src/build/__tests__/workspace-build.spec.ts +215 -0
- package/src/build/index.ts +189 -1
- package/src/config.ts +71 -14
- package/src/deploy/__tests__/docker.spec.ts +1 -1
- package/src/deploy/__tests__/index.spec.ts +305 -1
- package/src/deploy/index.ts +426 -4
- package/src/deploy/types.ts +32 -0
- package/src/dev/__tests__/index.spec.ts +572 -1
- package/src/dev/index.ts +582 -2
- package/src/docker/__tests__/compose.spec.ts +425 -0
- package/src/docker/__tests__/templates.spec.ts +145 -0
- package/src/docker/compose.ts +248 -0
- package/src/docker/index.ts +159 -3
- package/src/docker/templates.ts +219 -4
- package/src/index.ts +24 -0
- package/src/init/__tests__/generators.spec.ts +17 -24
- package/src/init/__tests__/init.spec.ts +157 -5
- package/src/init/generators/auth.ts +220 -0
- package/src/init/generators/config.ts +61 -4
- package/src/init/generators/docker.ts +115 -8
- package/src/init/generators/env.ts +7 -127
- package/src/init/generators/index.ts +1 -0
- package/src/init/generators/models.ts +3 -1
- package/src/init/generators/monorepo.ts +154 -10
- package/src/init/generators/package.ts +5 -3
- package/src/init/generators/web.ts +213 -0
- package/src/init/index.ts +290 -58
- package/src/init/templates/api.ts +38 -29
- package/src/init/templates/index.ts +132 -4
- package/src/init/templates/minimal.ts +33 -35
- package/src/init/templates/serverless.ts +16 -19
- package/src/init/templates/worker.ts +50 -25
- package/src/init/versions.ts +47 -0
- package/src/secrets/keystore.ts +144 -0
- package/src/secrets/storage.ts +109 -6
- package/src/test/index.ts +97 -0
- package/src/workspace/__tests__/client-generator.spec.ts +357 -0
- package/src/workspace/__tests__/index.spec.ts +543 -0
- package/src/workspace/__tests__/schema.spec.ts +519 -0
- package/src/workspace/__tests__/type-inference.spec.ts +251 -0
- package/src/workspace/client-generator.ts +307 -0
- package/src/workspace/index.ts +372 -0
- package/src/workspace/schema.ts +368 -0
- package/src/workspace/types.ts +336 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/tsdown.config.ts +1 -0
- package/dist/config-AmInkU7k.cjs.map +0 -1
- package/dist/config-DYULeEv8.mjs.map +0 -1
- package/dist/dokploy-api-B7KxOQr3.cjs +0 -3
- package/dist/dokploy-api-DHvfmWbi.mjs +0 -3
- package/dist/storage-BaOP55oq.mjs +0 -147
- package/dist/storage-BaOP55oq.mjs.map +0 -1
- package/dist/storage-Bn3K9Ccu.cjs.map +0 -1
package/src/docker/compose.ts
CHANGED
|
@@ -3,6 +3,10 @@ import type {
|
|
|
3
3
|
ComposeServicesConfig,
|
|
4
4
|
ServiceConfig,
|
|
5
5
|
} from '../types';
|
|
6
|
+
import type {
|
|
7
|
+
NormalizedAppConfig,
|
|
8
|
+
NormalizedWorkspace,
|
|
9
|
+
} from '../workspace/types.js';
|
|
6
10
|
|
|
7
11
|
/** Default Docker images for services */
|
|
8
12
|
export const DEFAULT_SERVICE_IMAGES: Record<ComposeServiceName, string> = {
|
|
@@ -271,3 +275,247 @@ networks:
|
|
|
271
275
|
driver: bridge
|
|
272
276
|
`;
|
|
273
277
|
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Options for workspace compose generation.
|
|
281
|
+
*/
|
|
282
|
+
export interface WorkspaceComposeOptions {
|
|
283
|
+
/** Container registry URL */
|
|
284
|
+
registry?: string;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Generate docker-compose.yml for a workspace with all apps as services.
|
|
289
|
+
* Apps can communicate with each other via service names.
|
|
290
|
+
* @internal Exported for testing
|
|
291
|
+
*/
|
|
292
|
+
export function generateWorkspaceCompose(
|
|
293
|
+
workspace: NormalizedWorkspace,
|
|
294
|
+
options: WorkspaceComposeOptions = {},
|
|
295
|
+
): string {
|
|
296
|
+
const { registry } = options;
|
|
297
|
+
const apps = Object.entries(workspace.apps);
|
|
298
|
+
const services = workspace.services;
|
|
299
|
+
|
|
300
|
+
// Determine which infrastructure services to include
|
|
301
|
+
const hasPostgres = services.db !== undefined && services.db !== false;
|
|
302
|
+
const hasRedis = services.cache !== undefined && services.cache !== false;
|
|
303
|
+
const hasMail = services.mail !== undefined && services.mail !== false;
|
|
304
|
+
|
|
305
|
+
// Get image versions from config
|
|
306
|
+
const postgresImage = getInfraServiceImage('postgres', services.db);
|
|
307
|
+
const redisImage = getInfraServiceImage('redis', services.cache);
|
|
308
|
+
|
|
309
|
+
let yaml = `# Docker Compose for ${workspace.name} workspace
|
|
310
|
+
# Generated by gkm - do not edit manually
|
|
311
|
+
|
|
312
|
+
services:
|
|
313
|
+
`;
|
|
314
|
+
|
|
315
|
+
// Generate service for each app
|
|
316
|
+
for (const [appName, app] of apps) {
|
|
317
|
+
yaml += generateAppService(appName, app, apps, {
|
|
318
|
+
registry,
|
|
319
|
+
hasPostgres,
|
|
320
|
+
hasRedis,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Add infrastructure services
|
|
325
|
+
if (hasPostgres) {
|
|
326
|
+
yaml += `
|
|
327
|
+
postgres:
|
|
328
|
+
image: ${postgresImage}
|
|
329
|
+
container_name: ${workspace.name}-postgres
|
|
330
|
+
restart: unless-stopped
|
|
331
|
+
environment:
|
|
332
|
+
POSTGRES_USER: \${POSTGRES_USER:-postgres}
|
|
333
|
+
POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:-postgres}
|
|
334
|
+
POSTGRES_DB: \${POSTGRES_DB:-app}
|
|
335
|
+
volumes:
|
|
336
|
+
- postgres_data:/var/lib/postgresql/data
|
|
337
|
+
healthcheck:
|
|
338
|
+
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
339
|
+
interval: 5s
|
|
340
|
+
timeout: 5s
|
|
341
|
+
retries: 5
|
|
342
|
+
networks:
|
|
343
|
+
- workspace-network
|
|
344
|
+
`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (hasRedis) {
|
|
348
|
+
yaml += `
|
|
349
|
+
redis:
|
|
350
|
+
image: ${redisImage}
|
|
351
|
+
container_name: ${workspace.name}-redis
|
|
352
|
+
restart: unless-stopped
|
|
353
|
+
volumes:
|
|
354
|
+
- redis_data:/data
|
|
355
|
+
healthcheck:
|
|
356
|
+
test: ["CMD", "redis-cli", "ping"]
|
|
357
|
+
interval: 5s
|
|
358
|
+
timeout: 5s
|
|
359
|
+
retries: 5
|
|
360
|
+
networks:
|
|
361
|
+
- workspace-network
|
|
362
|
+
`;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (hasMail) {
|
|
366
|
+
yaml += `
|
|
367
|
+
mailpit:
|
|
368
|
+
image: axllent/mailpit:latest
|
|
369
|
+
container_name: ${workspace.name}-mailpit
|
|
370
|
+
restart: unless-stopped
|
|
371
|
+
ports:
|
|
372
|
+
- "8025:8025" # Web UI
|
|
373
|
+
- "1025:1025" # SMTP
|
|
374
|
+
networks:
|
|
375
|
+
- workspace-network
|
|
376
|
+
`;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Add volumes section
|
|
380
|
+
yaml += `
|
|
381
|
+
volumes:
|
|
382
|
+
`;
|
|
383
|
+
|
|
384
|
+
if (hasPostgres) {
|
|
385
|
+
yaml += ` postgres_data:
|
|
386
|
+
`;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (hasRedis) {
|
|
390
|
+
yaml += ` redis_data:
|
|
391
|
+
`;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Add networks section
|
|
395
|
+
yaml += `
|
|
396
|
+
networks:
|
|
397
|
+
workspace-network:
|
|
398
|
+
driver: bridge
|
|
399
|
+
`;
|
|
400
|
+
|
|
401
|
+
return yaml;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Get infrastructure service image with version.
|
|
406
|
+
*/
|
|
407
|
+
function getInfraServiceImage(
|
|
408
|
+
serviceName: 'postgres' | 'redis',
|
|
409
|
+
config: boolean | { version?: string; image?: string } | undefined,
|
|
410
|
+
): string {
|
|
411
|
+
const defaults: Record<'postgres' | 'redis', string> = {
|
|
412
|
+
postgres: 'postgres:16-alpine',
|
|
413
|
+
redis: 'redis:7-alpine',
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
if (!config || config === true) {
|
|
417
|
+
return defaults[serviceName];
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (typeof config === 'object') {
|
|
421
|
+
if (config.image) {
|
|
422
|
+
return config.image;
|
|
423
|
+
}
|
|
424
|
+
if (config.version) {
|
|
425
|
+
const baseImage = serviceName === 'postgres' ? 'postgres' : 'redis';
|
|
426
|
+
return `${baseImage}:${config.version}`;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return defaults[serviceName];
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Generate a service definition for an app.
|
|
435
|
+
*/
|
|
436
|
+
function generateAppService(
|
|
437
|
+
appName: string,
|
|
438
|
+
app: NormalizedAppConfig,
|
|
439
|
+
allApps: [string, NormalizedAppConfig][],
|
|
440
|
+
options: {
|
|
441
|
+
registry?: string;
|
|
442
|
+
hasPostgres: boolean;
|
|
443
|
+
hasRedis: boolean;
|
|
444
|
+
},
|
|
445
|
+
): string {
|
|
446
|
+
const { registry, hasPostgres, hasRedis } = options;
|
|
447
|
+
const imageRef = registry ? `\${REGISTRY:-${registry}}/` : '';
|
|
448
|
+
|
|
449
|
+
// Health check path - frontends use /, backends use /health
|
|
450
|
+
const healthCheckPath = app.type === 'frontend' ? '/' : '/health';
|
|
451
|
+
const healthCheckCmd =
|
|
452
|
+
app.type === 'frontend'
|
|
453
|
+
? `["CMD", "wget", "-q", "--spider", "http://localhost:${app.port}/"]`
|
|
454
|
+
: `["CMD", "wget", "-q", "--spider", "http://localhost:${app.port}${healthCheckPath}"]`;
|
|
455
|
+
|
|
456
|
+
let yaml = `
|
|
457
|
+
${appName}:
|
|
458
|
+
build:
|
|
459
|
+
context: .
|
|
460
|
+
dockerfile: .gkm/docker/Dockerfile.${appName}
|
|
461
|
+
image: ${imageRef}\${${appName.toUpperCase()}_IMAGE:-${appName}}:\${TAG:-latest}
|
|
462
|
+
container_name: ${appName}
|
|
463
|
+
restart: unless-stopped
|
|
464
|
+
ports:
|
|
465
|
+
- "\${${appName.toUpperCase()}_PORT:-${app.port}}:${app.port}"
|
|
466
|
+
environment:
|
|
467
|
+
- NODE_ENV=production
|
|
468
|
+
- PORT=${app.port}
|
|
469
|
+
`;
|
|
470
|
+
|
|
471
|
+
// Add dependency URLs - apps can reach other apps by service name
|
|
472
|
+
for (const dep of app.dependencies) {
|
|
473
|
+
const depApp = allApps.find(([name]) => name === dep)?.[1];
|
|
474
|
+
if (depApp) {
|
|
475
|
+
yaml += ` - ${dep.toUpperCase()}_URL=http://${dep}:${depApp.port}
|
|
476
|
+
`;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Add infrastructure service URLs for backend apps
|
|
481
|
+
if (app.type === 'backend') {
|
|
482
|
+
if (hasPostgres) {
|
|
483
|
+
yaml += ` - DATABASE_URL=\${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/app}
|
|
484
|
+
`;
|
|
485
|
+
}
|
|
486
|
+
if (hasRedis) {
|
|
487
|
+
yaml += ` - REDIS_URL=\${REDIS_URL:-redis://redis:6379}
|
|
488
|
+
`;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
yaml += ` healthcheck:
|
|
493
|
+
test: ${healthCheckCmd}
|
|
494
|
+
interval: 30s
|
|
495
|
+
timeout: 3s
|
|
496
|
+
retries: 3
|
|
497
|
+
`;
|
|
498
|
+
|
|
499
|
+
// Add depends_on for dependencies and infrastructure
|
|
500
|
+
const dependencies: string[] = [...app.dependencies];
|
|
501
|
+
if (app.type === 'backend') {
|
|
502
|
+
if (hasPostgres) dependencies.push('postgres');
|
|
503
|
+
if (hasRedis) dependencies.push('redis');
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (dependencies.length > 0) {
|
|
507
|
+
yaml += ` depends_on:
|
|
508
|
+
`;
|
|
509
|
+
for (const dep of dependencies) {
|
|
510
|
+
yaml += ` ${dep}:
|
|
511
|
+
condition: service_healthy
|
|
512
|
+
`;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
yaml += ` networks:
|
|
517
|
+
- workspace-network
|
|
518
|
+
`;
|
|
519
|
+
|
|
520
|
+
return yaml;
|
|
521
|
+
}
|
package/src/docker/index.ts
CHANGED
|
@@ -2,14 +2,21 @@ import { execSync } from 'node:child_process';
|
|
|
2
2
|
import { copyFileSync, existsSync, unlinkSync } from 'node:fs';
|
|
3
3
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { basename, join } from 'node:path';
|
|
5
|
-
import { loadConfig } from '../config';
|
|
6
|
-
import {
|
|
5
|
+
import { loadConfig, loadWorkspaceConfig } from '../config';
|
|
6
|
+
import type { NormalizedWorkspace } from '../workspace/types.js';
|
|
7
|
+
import {
|
|
8
|
+
generateDockerCompose,
|
|
9
|
+
generateMinimalDockerCompose,
|
|
10
|
+
generateWorkspaceCompose,
|
|
11
|
+
} from './compose';
|
|
7
12
|
import {
|
|
8
13
|
detectPackageManager,
|
|
9
14
|
findLockfilePath,
|
|
15
|
+
generateBackendDockerfile,
|
|
10
16
|
generateDockerEntrypoint,
|
|
11
17
|
generateDockerignore,
|
|
12
18
|
generateMultiStageDockerfile,
|
|
19
|
+
generateNextjsDockerfile,
|
|
13
20
|
generateSlimDockerfile,
|
|
14
21
|
hasTurboConfig,
|
|
15
22
|
isMonorepo,
|
|
@@ -58,7 +65,17 @@ export interface DockerGeneratedFiles {
|
|
|
58
65
|
*/
|
|
59
66
|
export async function dockerCommand(
|
|
60
67
|
options: DockerOptions,
|
|
61
|
-
): Promise<DockerGeneratedFiles> {
|
|
68
|
+
): Promise<DockerGeneratedFiles | WorkspaceDockerResult> {
|
|
69
|
+
// Load config with workspace detection
|
|
70
|
+
const loadedConfig = await loadWorkspaceConfig();
|
|
71
|
+
|
|
72
|
+
// Route to workspace docker mode for multi-app workspaces
|
|
73
|
+
if (loadedConfig.type === 'workspace') {
|
|
74
|
+
logger.log('📦 Detected workspace configuration');
|
|
75
|
+
return workspaceDockerCommand(loadedConfig.workspace, options);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Single-app mode - use existing logic
|
|
62
79
|
const config = await loadConfig();
|
|
63
80
|
const dockerConfig = resolveDockerConfig(config);
|
|
64
81
|
|
|
@@ -318,3 +335,142 @@ async function pushDockerImage(
|
|
|
318
335
|
);
|
|
319
336
|
}
|
|
320
337
|
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Result of generating Docker files for a single app in a workspace.
|
|
341
|
+
*/
|
|
342
|
+
export interface AppDockerResult {
|
|
343
|
+
appName: string;
|
|
344
|
+
type: 'backend' | 'frontend';
|
|
345
|
+
dockerfile: string;
|
|
346
|
+
imageName: string;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Result of workspace docker command.
|
|
351
|
+
*/
|
|
352
|
+
export interface WorkspaceDockerResult {
|
|
353
|
+
apps: AppDockerResult[];
|
|
354
|
+
dockerCompose: string;
|
|
355
|
+
dockerignore: string;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Get the package name from package.json in an app directory.
|
|
360
|
+
*/
|
|
361
|
+
function getAppPackageName(appPath: string): string | undefined {
|
|
362
|
+
try {
|
|
363
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
364
|
+
const pkg = require(`${appPath}/package.json`);
|
|
365
|
+
return pkg.name;
|
|
366
|
+
} catch {
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Generate Dockerfiles for all apps in a workspace.
|
|
373
|
+
* @internal Exported for testing
|
|
374
|
+
*/
|
|
375
|
+
export async function workspaceDockerCommand(
|
|
376
|
+
workspace: NormalizedWorkspace,
|
|
377
|
+
options: DockerOptions,
|
|
378
|
+
): Promise<WorkspaceDockerResult> {
|
|
379
|
+
const results: AppDockerResult[] = [];
|
|
380
|
+
const apps = Object.entries(workspace.apps);
|
|
381
|
+
|
|
382
|
+
logger.log(`\n🐳 Generating Dockerfiles for workspace: ${workspace.name}`);
|
|
383
|
+
|
|
384
|
+
// Create docker output directory
|
|
385
|
+
const dockerDir = join(workspace.root, '.gkm', 'docker');
|
|
386
|
+
await mkdir(dockerDir, { recursive: true });
|
|
387
|
+
|
|
388
|
+
// Detect package manager
|
|
389
|
+
const packageManager = detectPackageManager(workspace.root);
|
|
390
|
+
logger.log(` Package manager: ${packageManager}`);
|
|
391
|
+
|
|
392
|
+
// Generate Dockerfile for each app
|
|
393
|
+
for (const [appName, app] of apps) {
|
|
394
|
+
const appPath = app.path;
|
|
395
|
+
const fullAppPath = join(workspace.root, appPath);
|
|
396
|
+
|
|
397
|
+
// Get package name for turbo prune (use package.json name or app name)
|
|
398
|
+
const turboPackage = getAppPackageName(fullAppPath) ?? appName;
|
|
399
|
+
|
|
400
|
+
// Determine image name
|
|
401
|
+
const imageName = appName;
|
|
402
|
+
|
|
403
|
+
logger.log(`\n 📄 Generating Dockerfile for ${appName} (${app.type})`);
|
|
404
|
+
|
|
405
|
+
let dockerfile: string;
|
|
406
|
+
|
|
407
|
+
if (app.type === 'frontend') {
|
|
408
|
+
// Generate Next.js Dockerfile
|
|
409
|
+
dockerfile = generateNextjsDockerfile({
|
|
410
|
+
imageName,
|
|
411
|
+
baseImage: 'node:22-alpine',
|
|
412
|
+
port: app.port,
|
|
413
|
+
appPath,
|
|
414
|
+
turboPackage,
|
|
415
|
+
packageManager,
|
|
416
|
+
});
|
|
417
|
+
} else {
|
|
418
|
+
// Generate backend Dockerfile
|
|
419
|
+
dockerfile = generateBackendDockerfile({
|
|
420
|
+
imageName,
|
|
421
|
+
baseImage: 'node:22-alpine',
|
|
422
|
+
port: app.port,
|
|
423
|
+
appPath,
|
|
424
|
+
turboPackage,
|
|
425
|
+
packageManager,
|
|
426
|
+
healthCheckPath: '/health',
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Write Dockerfile with app-specific name
|
|
431
|
+
const dockerfilePath = join(dockerDir, `Dockerfile.${appName}`);
|
|
432
|
+
await writeFile(dockerfilePath, dockerfile);
|
|
433
|
+
logger.log(` Generated: .gkm/docker/Dockerfile.${appName}`);
|
|
434
|
+
|
|
435
|
+
results.push({
|
|
436
|
+
appName,
|
|
437
|
+
type: app.type,
|
|
438
|
+
dockerfile: dockerfilePath,
|
|
439
|
+
imageName,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Generate shared .dockerignore
|
|
444
|
+
const dockerignore = generateDockerignore();
|
|
445
|
+
const dockerignorePath = join(workspace.root, '.dockerignore');
|
|
446
|
+
await writeFile(dockerignorePath, dockerignore);
|
|
447
|
+
logger.log(`\n Generated: .dockerignore (workspace root)`);
|
|
448
|
+
|
|
449
|
+
// Generate docker-compose.yml for workspace
|
|
450
|
+
const dockerCompose = generateWorkspaceCompose(workspace, {
|
|
451
|
+
registry: options.registry,
|
|
452
|
+
});
|
|
453
|
+
const composePath = join(dockerDir, 'docker-compose.yml');
|
|
454
|
+
await writeFile(composePath, dockerCompose);
|
|
455
|
+
logger.log(` Generated: .gkm/docker/docker-compose.yml`);
|
|
456
|
+
|
|
457
|
+
// Summary
|
|
458
|
+
logger.log(
|
|
459
|
+
`\n✅ Generated ${results.length} Dockerfile(s) + docker-compose.yml`,
|
|
460
|
+
);
|
|
461
|
+
logger.log('\n📋 Build commands:');
|
|
462
|
+
for (const result of results) {
|
|
463
|
+
const icon = result.type === 'backend' ? '⚙️' : '🌐';
|
|
464
|
+
logger.log(
|
|
465
|
+
` ${icon} docker build -f .gkm/docker/Dockerfile.${result.appName} -t ${result.imageName} .`,
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
logger.log('\n📋 Run all services:');
|
|
469
|
+
logger.log(' docker compose -f .gkm/docker/docker-compose.yml up --build');
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
apps: results,
|
|
473
|
+
dockerCompose: composePath,
|
|
474
|
+
dockerignore: dockerignorePath,
|
|
475
|
+
};
|
|
476
|
+
}
|
package/src/docker/templates.ts
CHANGED
|
@@ -15,6 +15,18 @@ export interface DockerTemplateOptions {
|
|
|
15
15
|
packageManager: PackageManager;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
export interface FrontendDockerfileOptions {
|
|
19
|
+
imageName: string;
|
|
20
|
+
baseImage: string;
|
|
21
|
+
port: number;
|
|
22
|
+
/** App path relative to workspace root */
|
|
23
|
+
appPath: string;
|
|
24
|
+
/** Package name for turbo prune */
|
|
25
|
+
turboPackage: string;
|
|
26
|
+
/** Detected package manager */
|
|
27
|
+
packageManager: PackageManager;
|
|
28
|
+
}
|
|
29
|
+
|
|
18
30
|
export interface MultiStageDockerfileOptions extends DockerTemplateOptions {
|
|
19
31
|
/** Enable turbo prune for monorepo optimization */
|
|
20
32
|
turbo?: boolean;
|
|
@@ -274,8 +286,14 @@ WORKDIR /app
|
|
|
274
286
|
# Copy source (deps already installed)
|
|
275
287
|
COPY . .
|
|
276
288
|
|
|
277
|
-
#
|
|
278
|
-
RUN
|
|
289
|
+
# Debug: Show node_modules/.bin contents and build production server
|
|
290
|
+
RUN echo "=== node_modules/.bin contents ===" && \
|
|
291
|
+
ls -la node_modules/.bin/ 2>/dev/null || echo "node_modules/.bin not found" && \
|
|
292
|
+
echo "=== Checking for gkm ===" && \
|
|
293
|
+
which gkm 2>/dev/null || echo "gkm not in PATH" && \
|
|
294
|
+
ls -la node_modules/.bin/gkm 2>/dev/null || echo "gkm binary not found in node_modules/.bin" && \
|
|
295
|
+
echo "=== Running build ===" && \
|
|
296
|
+
./node_modules/.bin/gkm build --provider server --production
|
|
279
297
|
|
|
280
298
|
# Stage 3: Production
|
|
281
299
|
FROM ${baseImage} AS runner
|
|
@@ -365,8 +383,14 @@ WORKDIR /app
|
|
|
365
383
|
# Copy pruned source
|
|
366
384
|
COPY --from=pruner /app/out/full/ ./
|
|
367
385
|
|
|
368
|
-
#
|
|
369
|
-
RUN
|
|
386
|
+
# Debug: Show node_modules/.bin contents and build production server
|
|
387
|
+
RUN echo "=== node_modules/.bin contents ===" && \
|
|
388
|
+
ls -la node_modules/.bin/ 2>/dev/null || echo "node_modules/.bin not found" && \
|
|
389
|
+
echo "=== Checking for gkm ===" && \
|
|
390
|
+
which gkm 2>/dev/null || echo "gkm not in PATH" && \
|
|
391
|
+
ls -la node_modules/.bin/gkm 2>/dev/null || echo "gkm binary not found in node_modules/.bin" && \
|
|
392
|
+
echo "=== Running build ===" && \
|
|
393
|
+
./node_modules/.bin/gkm build --provider server --production
|
|
370
394
|
|
|
371
395
|
# Stage 4: Production
|
|
372
396
|
FROM ${baseImage} AS runner
|
|
@@ -535,3 +559,194 @@ export function resolveDockerConfig(
|
|
|
535
559
|
compose: docker.compose,
|
|
536
560
|
};
|
|
537
561
|
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Generate a Dockerfile for Next.js frontend apps using standalone output.
|
|
565
|
+
* Uses turbo prune for monorepo optimization.
|
|
566
|
+
* @internal Exported for testing
|
|
567
|
+
*/
|
|
568
|
+
export function generateNextjsDockerfile(
|
|
569
|
+
options: FrontendDockerfileOptions,
|
|
570
|
+
): string {
|
|
571
|
+
const { baseImage, port, appPath, turboPackage, packageManager } = options;
|
|
572
|
+
|
|
573
|
+
const pm = getPmConfig(packageManager);
|
|
574
|
+
const installPm = pm.install ? `RUN ${pm.install}` : '';
|
|
575
|
+
|
|
576
|
+
// For turbo builds, we can't use --frozen-lockfile because turbo prune
|
|
577
|
+
// creates a subset that may not perfectly match. Use relaxed install.
|
|
578
|
+
const turboInstallCmd = getTurboInstallCmd(packageManager);
|
|
579
|
+
|
|
580
|
+
// Use pnpm dlx for pnpm (avoids global bin dir issues in Docker)
|
|
581
|
+
const turboCmd = packageManager === 'pnpm' ? 'pnpm dlx turbo' : 'npx turbo';
|
|
582
|
+
|
|
583
|
+
return `# syntax=docker/dockerfile:1
|
|
584
|
+
# Next.js standalone Dockerfile with turbo prune optimization
|
|
585
|
+
|
|
586
|
+
# Stage 1: Prune monorepo
|
|
587
|
+
FROM ${baseImage} AS pruner
|
|
588
|
+
|
|
589
|
+
WORKDIR /app
|
|
590
|
+
|
|
591
|
+
${installPm}
|
|
592
|
+
|
|
593
|
+
COPY . .
|
|
594
|
+
|
|
595
|
+
# Prune to only include necessary packages
|
|
596
|
+
RUN ${turboCmd} prune ${turboPackage} --docker
|
|
597
|
+
|
|
598
|
+
# Stage 2: Install dependencies
|
|
599
|
+
FROM ${baseImage} AS deps
|
|
600
|
+
|
|
601
|
+
WORKDIR /app
|
|
602
|
+
|
|
603
|
+
${installPm}
|
|
604
|
+
|
|
605
|
+
# Copy pruned lockfile and package.jsons
|
|
606
|
+
COPY --from=pruner /app/out/${pm.lockfile} ./
|
|
607
|
+
COPY --from=pruner /app/out/json/ ./
|
|
608
|
+
|
|
609
|
+
# Install dependencies
|
|
610
|
+
RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
|
|
611
|
+
${turboInstallCmd}
|
|
612
|
+
|
|
613
|
+
# Stage 3: Build
|
|
614
|
+
FROM deps AS builder
|
|
615
|
+
|
|
616
|
+
WORKDIR /app
|
|
617
|
+
|
|
618
|
+
# Copy pruned source
|
|
619
|
+
COPY --from=pruner /app/out/full/ ./
|
|
620
|
+
|
|
621
|
+
# Set Next.js to produce standalone output
|
|
622
|
+
ENV NEXT_TELEMETRY_DISABLED=1
|
|
623
|
+
|
|
624
|
+
# Build the application
|
|
625
|
+
RUN ${turboCmd} run build --filter=${turboPackage}
|
|
626
|
+
|
|
627
|
+
# Stage 4: Production
|
|
628
|
+
FROM ${baseImage} AS runner
|
|
629
|
+
|
|
630
|
+
WORKDIR /app
|
|
631
|
+
|
|
632
|
+
# Install tini for proper signal handling
|
|
633
|
+
RUN apk add --no-cache tini
|
|
634
|
+
|
|
635
|
+
# Create non-root user
|
|
636
|
+
RUN addgroup --system --gid 1001 nodejs && \\
|
|
637
|
+
adduser --system --uid 1001 nextjs
|
|
638
|
+
|
|
639
|
+
# Set environment
|
|
640
|
+
ENV NODE_ENV=production
|
|
641
|
+
ENV NEXT_TELEMETRY_DISABLED=1
|
|
642
|
+
ENV PORT=${port}
|
|
643
|
+
ENV HOSTNAME="0.0.0.0"
|
|
644
|
+
|
|
645
|
+
# Copy static files and standalone output
|
|
646
|
+
COPY --from=builder --chown=nextjs:nodejs /app/${appPath}/.next/standalone ./
|
|
647
|
+
COPY --from=builder --chown=nextjs:nodejs /app/${appPath}/.next/static ./${appPath}/.next/static
|
|
648
|
+
COPY --from=builder --chown=nextjs:nodejs /app/${appPath}/public ./${appPath}/public
|
|
649
|
+
|
|
650
|
+
# Health check
|
|
651
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \\
|
|
652
|
+
CMD wget -q --spider http://localhost:${port}/ || exit 1
|
|
653
|
+
|
|
654
|
+
USER nextjs
|
|
655
|
+
|
|
656
|
+
EXPOSE ${port}
|
|
657
|
+
|
|
658
|
+
ENTRYPOINT ["/sbin/tini", "--"]
|
|
659
|
+
CMD ["node", "${appPath}/server.js"]
|
|
660
|
+
`;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Generate a Dockerfile for backend apps in a workspace.
|
|
665
|
+
* Uses turbo prune for monorepo optimization.
|
|
666
|
+
* @internal Exported for testing
|
|
667
|
+
*/
|
|
668
|
+
export function generateBackendDockerfile(
|
|
669
|
+
options: FrontendDockerfileOptions & { healthCheckPath?: string },
|
|
670
|
+
): string {
|
|
671
|
+
const {
|
|
672
|
+
baseImage,
|
|
673
|
+
port,
|
|
674
|
+
appPath,
|
|
675
|
+
turboPackage,
|
|
676
|
+
packageManager,
|
|
677
|
+
healthCheckPath = '/health',
|
|
678
|
+
} = options;
|
|
679
|
+
|
|
680
|
+
const pm = getPmConfig(packageManager);
|
|
681
|
+
const installPm = pm.install ? `RUN ${pm.install}` : '';
|
|
682
|
+
const turboInstallCmd = getTurboInstallCmd(packageManager);
|
|
683
|
+
const turboCmd = packageManager === 'pnpm' ? 'pnpm dlx turbo' : 'npx turbo';
|
|
684
|
+
|
|
685
|
+
return `# syntax=docker/dockerfile:1
|
|
686
|
+
# Backend Dockerfile with turbo prune optimization
|
|
687
|
+
|
|
688
|
+
# Stage 1: Prune monorepo
|
|
689
|
+
FROM ${baseImage} AS pruner
|
|
690
|
+
|
|
691
|
+
WORKDIR /app
|
|
692
|
+
|
|
693
|
+
${installPm}
|
|
694
|
+
|
|
695
|
+
COPY . .
|
|
696
|
+
|
|
697
|
+
# Prune to only include necessary packages
|
|
698
|
+
RUN ${turboCmd} prune ${turboPackage} --docker
|
|
699
|
+
|
|
700
|
+
# Stage 2: Install dependencies
|
|
701
|
+
FROM ${baseImage} AS deps
|
|
702
|
+
|
|
703
|
+
WORKDIR /app
|
|
704
|
+
|
|
705
|
+
${installPm}
|
|
706
|
+
|
|
707
|
+
# Copy pruned lockfile and package.jsons
|
|
708
|
+
COPY --from=pruner /app/out/${pm.lockfile} ./
|
|
709
|
+
COPY --from=pruner /app/out/json/ ./
|
|
710
|
+
|
|
711
|
+
# Install dependencies
|
|
712
|
+
RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
|
|
713
|
+
${turboInstallCmd}
|
|
714
|
+
|
|
715
|
+
# Stage 3: Build
|
|
716
|
+
FROM deps AS builder
|
|
717
|
+
|
|
718
|
+
WORKDIR /app
|
|
719
|
+
|
|
720
|
+
# Copy pruned source
|
|
721
|
+
COPY --from=pruner /app/out/full/ ./
|
|
722
|
+
|
|
723
|
+
# Build production server using gkm
|
|
724
|
+
RUN cd ${appPath} && ./node_modules/.bin/gkm build --provider server --production
|
|
725
|
+
|
|
726
|
+
# Stage 4: Production
|
|
727
|
+
FROM ${baseImage} AS runner
|
|
728
|
+
|
|
729
|
+
WORKDIR /app
|
|
730
|
+
|
|
731
|
+
RUN apk add --no-cache tini
|
|
732
|
+
|
|
733
|
+
RUN addgroup --system --gid 1001 nodejs && \\
|
|
734
|
+
adduser --system --uid 1001 hono
|
|
735
|
+
|
|
736
|
+
# Copy bundled server
|
|
737
|
+
COPY --from=builder --chown=hono:nodejs /app/${appPath}/.gkm/server/dist/server.mjs ./
|
|
738
|
+
|
|
739
|
+
ENV NODE_ENV=production
|
|
740
|
+
ENV PORT=${port}
|
|
741
|
+
|
|
742
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
|
|
743
|
+
CMD wget -q --spider http://localhost:${port}${healthCheckPath} || exit 1
|
|
744
|
+
|
|
745
|
+
USER hono
|
|
746
|
+
|
|
747
|
+
EXPOSE ${port}
|
|
748
|
+
|
|
749
|
+
ENTRYPOINT ["/sbin/tini", "--"]
|
|
750
|
+
CMD ["node", "server.mjs"]
|
|
751
|
+
`;
|
|
752
|
+
}
|