@geekmidas/cli 0.18.0 → 0.19.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 +2639 -563
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2634 -563
- 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 +8 -3
- 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
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { NormalizedWorkspace } from '../../workspace/types.js';
|
|
2
3
|
import {
|
|
3
4
|
type ComposeOptions,
|
|
4
5
|
DEFAULT_SERVICE_IMAGES,
|
|
5
6
|
DEFAULT_SERVICE_VERSIONS,
|
|
6
7
|
generateDockerCompose,
|
|
7
8
|
generateMinimalDockerCompose,
|
|
9
|
+
generateWorkspaceCompose,
|
|
8
10
|
} from '../compose';
|
|
9
11
|
|
|
10
12
|
/** Helper to get full default image reference */
|
|
@@ -529,3 +531,426 @@ describe('generateMinimalDockerCompose', () => {
|
|
|
529
531
|
expect(yaml).toContain('image: ${IMAGE_NAME:-minimal-api}:${TAG:-latest}');
|
|
530
532
|
});
|
|
531
533
|
});
|
|
534
|
+
|
|
535
|
+
describe('generateWorkspaceCompose', () => {
|
|
536
|
+
/** Create a minimal workspace config for testing */
|
|
537
|
+
function createWorkspace(
|
|
538
|
+
overrides: Partial<NormalizedWorkspace> = {},
|
|
539
|
+
): NormalizedWorkspace {
|
|
540
|
+
return {
|
|
541
|
+
name: 'test-workspace',
|
|
542
|
+
root: '/workspace',
|
|
543
|
+
apps: {
|
|
544
|
+
api: {
|
|
545
|
+
type: 'backend',
|
|
546
|
+
path: 'apps/api',
|
|
547
|
+
port: 3000,
|
|
548
|
+
dependencies: [],
|
|
549
|
+
},
|
|
550
|
+
web: {
|
|
551
|
+
type: 'frontend',
|
|
552
|
+
path: 'apps/web',
|
|
553
|
+
port: 3001,
|
|
554
|
+
dependencies: ['api'],
|
|
555
|
+
framework: 'nextjs',
|
|
556
|
+
},
|
|
557
|
+
},
|
|
558
|
+
services: {},
|
|
559
|
+
deploy: { default: 'dokploy' },
|
|
560
|
+
shared: { packages: [] },
|
|
561
|
+
secrets: {},
|
|
562
|
+
...overrides,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
describe('header and structure', () => {
|
|
567
|
+
it('should include workspace name in header comment', () => {
|
|
568
|
+
const workspace = createWorkspace();
|
|
569
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
570
|
+
|
|
571
|
+
expect(yaml).toContain('# Docker Compose for test-workspace workspace');
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it('should include generated file warning', () => {
|
|
575
|
+
const workspace = createWorkspace();
|
|
576
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
577
|
+
|
|
578
|
+
expect(yaml).toContain('# Generated by gkm - do not edit manually');
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it('should include services section', () => {
|
|
582
|
+
const workspace = createWorkspace();
|
|
583
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
584
|
+
|
|
585
|
+
expect(yaml).toContain('services:');
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it('should include networks section', () => {
|
|
589
|
+
const workspace = createWorkspace();
|
|
590
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
591
|
+
|
|
592
|
+
expect(yaml).toContain('networks:');
|
|
593
|
+
expect(yaml).toContain('workspace-network:');
|
|
594
|
+
expect(yaml).toContain('driver: bridge');
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
describe('app services', () => {
|
|
599
|
+
it('should generate service for each app', () => {
|
|
600
|
+
const workspace = createWorkspace();
|
|
601
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
602
|
+
|
|
603
|
+
expect(yaml).toContain('api:');
|
|
604
|
+
expect(yaml).toContain('web:');
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it('should reference correct Dockerfile for each app', () => {
|
|
608
|
+
const workspace = createWorkspace();
|
|
609
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
610
|
+
|
|
611
|
+
expect(yaml).toContain('dockerfile: .gkm/docker/Dockerfile.api');
|
|
612
|
+
expect(yaml).toContain('dockerfile: .gkm/docker/Dockerfile.web');
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it('should set container name from app name', () => {
|
|
616
|
+
const workspace = createWorkspace();
|
|
617
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
618
|
+
|
|
619
|
+
expect(yaml).toContain('container_name: api');
|
|
620
|
+
expect(yaml).toContain('container_name: web');
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it('should configure port mapping for each app', () => {
|
|
624
|
+
const workspace = createWorkspace();
|
|
625
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
626
|
+
|
|
627
|
+
expect(yaml).toContain('"${API_PORT:-3000}:3000"');
|
|
628
|
+
expect(yaml).toContain('"${WEB_PORT:-3001}:3001"');
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it('should set PORT environment variable for each app', () => {
|
|
632
|
+
const workspace = createWorkspace();
|
|
633
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
634
|
+
|
|
635
|
+
expect(yaml).toContain('- PORT=3000');
|
|
636
|
+
expect(yaml).toContain('- PORT=3001');
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it('should set NODE_ENV to production for all apps', () => {
|
|
640
|
+
const workspace = createWorkspace();
|
|
641
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
642
|
+
|
|
643
|
+
const matches = yaml.match(/- NODE_ENV=production/g);
|
|
644
|
+
expect(matches?.length).toBe(2);
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
it('should configure restart policy for all apps', () => {
|
|
648
|
+
const workspace = createWorkspace();
|
|
649
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
650
|
+
|
|
651
|
+
const matches = yaml.match(/restart: unless-stopped/g);
|
|
652
|
+
expect(matches?.length).toBeGreaterThanOrEqual(2);
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
it('should attach all apps to workspace-network', () => {
|
|
656
|
+
const workspace = createWorkspace();
|
|
657
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
658
|
+
|
|
659
|
+
const matches = yaml.match(/- workspace-network/g);
|
|
660
|
+
expect(matches?.length).toBeGreaterThanOrEqual(2);
|
|
661
|
+
});
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
describe('service discovery', () => {
|
|
665
|
+
it('should add dependency URLs for frontend apps', () => {
|
|
666
|
+
const workspace = createWorkspace();
|
|
667
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
668
|
+
|
|
669
|
+
// web depends on api, so should have API_URL
|
|
670
|
+
expect(yaml).toContain('- API_URL=http://api:3000');
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
it('should handle multiple dependencies', () => {
|
|
674
|
+
const workspace = createWorkspace({
|
|
675
|
+
apps: {
|
|
676
|
+
api: {
|
|
677
|
+
type: 'backend',
|
|
678
|
+
path: 'apps/api',
|
|
679
|
+
port: 3000,
|
|
680
|
+
dependencies: [],
|
|
681
|
+
},
|
|
682
|
+
auth: {
|
|
683
|
+
type: 'backend',
|
|
684
|
+
path: 'apps/auth',
|
|
685
|
+
port: 3002,
|
|
686
|
+
dependencies: [],
|
|
687
|
+
},
|
|
688
|
+
web: {
|
|
689
|
+
type: 'frontend',
|
|
690
|
+
path: 'apps/web',
|
|
691
|
+
port: 3001,
|
|
692
|
+
dependencies: ['api', 'auth'],
|
|
693
|
+
framework: 'nextjs',
|
|
694
|
+
},
|
|
695
|
+
},
|
|
696
|
+
});
|
|
697
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
698
|
+
|
|
699
|
+
expect(yaml).toContain('- API_URL=http://api:3000');
|
|
700
|
+
expect(yaml).toContain('- AUTH_URL=http://auth:3002');
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
it('should not add dependency URLs for apps with no dependencies', () => {
|
|
704
|
+
const workspace = createWorkspace({
|
|
705
|
+
apps: {
|
|
706
|
+
api: {
|
|
707
|
+
type: 'backend',
|
|
708
|
+
path: 'apps/api',
|
|
709
|
+
port: 3000,
|
|
710
|
+
dependencies: [],
|
|
711
|
+
},
|
|
712
|
+
},
|
|
713
|
+
});
|
|
714
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
715
|
+
|
|
716
|
+
// api has no dependencies, so no *_URL vars
|
|
717
|
+
expect(yaml).not.toMatch(/_URL=http:\/\//);
|
|
718
|
+
});
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
describe('health checks', () => {
|
|
722
|
+
it('should configure health check for backend apps at /health', () => {
|
|
723
|
+
const workspace = createWorkspace({
|
|
724
|
+
apps: {
|
|
725
|
+
api: {
|
|
726
|
+
type: 'backend',
|
|
727
|
+
path: 'apps/api',
|
|
728
|
+
port: 3000,
|
|
729
|
+
dependencies: [],
|
|
730
|
+
},
|
|
731
|
+
},
|
|
732
|
+
});
|
|
733
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
734
|
+
|
|
735
|
+
expect(yaml).toContain('http://localhost:3000/health');
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
it('should configure health check for frontend apps at /', () => {
|
|
739
|
+
const workspace = createWorkspace({
|
|
740
|
+
apps: {
|
|
741
|
+
web: {
|
|
742
|
+
type: 'frontend',
|
|
743
|
+
path: 'apps/web',
|
|
744
|
+
port: 3001,
|
|
745
|
+
dependencies: [],
|
|
746
|
+
framework: 'nextjs',
|
|
747
|
+
},
|
|
748
|
+
},
|
|
749
|
+
});
|
|
750
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
751
|
+
|
|
752
|
+
expect(yaml).toContain('http://localhost:3001/');
|
|
753
|
+
});
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
describe('depends_on', () => {
|
|
757
|
+
it('should add depends_on for app dependencies', () => {
|
|
758
|
+
const workspace = createWorkspace();
|
|
759
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
760
|
+
|
|
761
|
+
// web depends on api
|
|
762
|
+
expect(yaml).toContain('depends_on:');
|
|
763
|
+
expect(yaml).toContain('condition: service_healthy');
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
it('should not add depends_on for apps without dependencies', () => {
|
|
767
|
+
const workspace = createWorkspace({
|
|
768
|
+
apps: {
|
|
769
|
+
api: {
|
|
770
|
+
type: 'backend',
|
|
771
|
+
path: 'apps/api',
|
|
772
|
+
port: 3000,
|
|
773
|
+
dependencies: [],
|
|
774
|
+
},
|
|
775
|
+
},
|
|
776
|
+
});
|
|
777
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
778
|
+
|
|
779
|
+
// api section should not have depends_on
|
|
780
|
+
const apiSection = yaml.split('api:')[1]?.split(/^ {2}\w+:/m)[0];
|
|
781
|
+
expect(apiSection).not.toContain('depends_on:');
|
|
782
|
+
});
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
describe('infrastructure services', () => {
|
|
786
|
+
it('should add postgres service when db is configured', () => {
|
|
787
|
+
const workspace = createWorkspace({
|
|
788
|
+
services: { db: true },
|
|
789
|
+
});
|
|
790
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
791
|
+
|
|
792
|
+
expect(yaml).toContain('postgres:');
|
|
793
|
+
expect(yaml).toContain('image: postgres:16-alpine');
|
|
794
|
+
expect(yaml).toContain('container_name: test-workspace-postgres');
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
it('should add DATABASE_URL for backend apps when postgres is enabled', () => {
|
|
798
|
+
const workspace = createWorkspace({
|
|
799
|
+
services: { db: true },
|
|
800
|
+
});
|
|
801
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
802
|
+
|
|
803
|
+
expect(yaml).toContain(
|
|
804
|
+
'DATABASE_URL=${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/app}',
|
|
805
|
+
);
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
it('should add redis service when cache is configured', () => {
|
|
809
|
+
const workspace = createWorkspace({
|
|
810
|
+
services: { cache: true },
|
|
811
|
+
});
|
|
812
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
813
|
+
|
|
814
|
+
expect(yaml).toContain('redis:');
|
|
815
|
+
expect(yaml).toContain('image: redis:7-alpine');
|
|
816
|
+
expect(yaml).toContain('container_name: test-workspace-redis');
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
it('should add REDIS_URL for backend apps when redis is enabled', () => {
|
|
820
|
+
const workspace = createWorkspace({
|
|
821
|
+
services: { cache: true },
|
|
822
|
+
});
|
|
823
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
824
|
+
|
|
825
|
+
expect(yaml).toContain('REDIS_URL=${REDIS_URL:-redis://redis:6379}');
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
it('should add mailpit service when mail is configured', () => {
|
|
829
|
+
const workspace = createWorkspace({
|
|
830
|
+
services: { mail: true },
|
|
831
|
+
});
|
|
832
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
833
|
+
|
|
834
|
+
expect(yaml).toContain('mailpit:');
|
|
835
|
+
expect(yaml).toContain('image: axllent/mailpit:latest');
|
|
836
|
+
expect(yaml).toContain('- "8025:8025"'); // Web UI
|
|
837
|
+
expect(yaml).toContain('- "1025:1025"'); // SMTP
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
it('should add postgres_data volume when postgres is enabled', () => {
|
|
841
|
+
const workspace = createWorkspace({
|
|
842
|
+
services: { db: true },
|
|
843
|
+
});
|
|
844
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
845
|
+
|
|
846
|
+
expect(yaml).toContain('postgres_data:');
|
|
847
|
+
expect(yaml).toContain('postgres_data:/var/lib/postgresql/data');
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
it('should add redis_data volume when redis is enabled', () => {
|
|
851
|
+
const workspace = createWorkspace({
|
|
852
|
+
services: { cache: true },
|
|
853
|
+
});
|
|
854
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
855
|
+
|
|
856
|
+
expect(yaml).toContain('redis_data:');
|
|
857
|
+
expect(yaml).toContain('redis_data:/data');
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
it('should include healthchecks for infrastructure services', () => {
|
|
861
|
+
const workspace = createWorkspace({
|
|
862
|
+
services: { db: true, cache: true },
|
|
863
|
+
});
|
|
864
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
865
|
+
|
|
866
|
+
expect(yaml).toContain('pg_isready');
|
|
867
|
+
expect(yaml).toContain('redis-cli');
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
it('should add depends_on for infrastructure services', () => {
|
|
871
|
+
const workspace = createWorkspace({
|
|
872
|
+
services: { db: true, cache: true },
|
|
873
|
+
});
|
|
874
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
875
|
+
|
|
876
|
+
// Backend apps should depend on postgres and redis
|
|
877
|
+
expect(yaml).toMatch(/postgres:\s+condition: service_healthy/);
|
|
878
|
+
expect(yaml).toMatch(/redis:\s+condition: service_healthy/);
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
it('should not add infrastructure depends_on for frontend apps', () => {
|
|
882
|
+
const workspace = createWorkspace({
|
|
883
|
+
apps: {
|
|
884
|
+
web: {
|
|
885
|
+
type: 'frontend',
|
|
886
|
+
path: 'apps/web',
|
|
887
|
+
port: 3001,
|
|
888
|
+
dependencies: [],
|
|
889
|
+
framework: 'nextjs',
|
|
890
|
+
},
|
|
891
|
+
},
|
|
892
|
+
services: { db: true },
|
|
893
|
+
});
|
|
894
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
895
|
+
|
|
896
|
+
// Frontend should not depend on postgres
|
|
897
|
+
const webSection = yaml.split('web:')[1]?.split(/^ {2}\w+:/m)[0];
|
|
898
|
+
expect(webSection).not.toContain('postgres:');
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
it('should support custom postgres version', () => {
|
|
902
|
+
const workspace = createWorkspace({
|
|
903
|
+
services: { db: { version: '15-alpine' } },
|
|
904
|
+
});
|
|
905
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
906
|
+
|
|
907
|
+
expect(yaml).toContain('image: postgres:15-alpine');
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
it('should support custom postgres image', () => {
|
|
911
|
+
const workspace = createWorkspace({
|
|
912
|
+
services: { db: { image: 'postgis/postgis:16-3.4-alpine' } },
|
|
913
|
+
});
|
|
914
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
915
|
+
|
|
916
|
+
expect(yaml).toContain('image: postgis/postgis:16-3.4-alpine');
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
it('should support custom redis version', () => {
|
|
920
|
+
const workspace = createWorkspace({
|
|
921
|
+
services: { cache: { version: '6-alpine' } },
|
|
922
|
+
});
|
|
923
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
924
|
+
|
|
925
|
+
expect(yaml).toContain('image: redis:6-alpine');
|
|
926
|
+
});
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
describe('registry configuration', () => {
|
|
930
|
+
it('should include registry when provided', () => {
|
|
931
|
+
const workspace = createWorkspace();
|
|
932
|
+
const yaml = generateWorkspaceCompose(workspace, {
|
|
933
|
+
registry: 'ghcr.io/myorg',
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
expect(yaml).toContain('${REGISTRY:-ghcr.io/myorg}/');
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
it('should work without registry', () => {
|
|
940
|
+
const workspace = createWorkspace();
|
|
941
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
942
|
+
|
|
943
|
+
expect(yaml).not.toContain('${REGISTRY');
|
|
944
|
+
});
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
describe('image naming', () => {
|
|
948
|
+
it('should use app name for image with environment override', () => {
|
|
949
|
+
const workspace = createWorkspace();
|
|
950
|
+
const yaml = generateWorkspaceCompose(workspace);
|
|
951
|
+
|
|
952
|
+
expect(yaml).toContain('${API_IMAGE:-api}:${TAG:-latest}');
|
|
953
|
+
expect(yaml).toContain('${WEB_IMAGE:-web}:${TAG:-latest}');
|
|
954
|
+
});
|
|
955
|
+
});
|
|
956
|
+
});
|
|
@@ -5,9 +5,11 @@ import type { GkmConfig } from '../../types';
|
|
|
5
5
|
import {
|
|
6
6
|
detectPackageManager,
|
|
7
7
|
findLockfilePath,
|
|
8
|
+
generateBackendDockerfile,
|
|
8
9
|
generateDockerEntrypoint,
|
|
9
10
|
generateDockerignore,
|
|
10
11
|
generateMultiStageDockerfile,
|
|
12
|
+
generateNextjsDockerfile,
|
|
11
13
|
generateSlimDockerfile,
|
|
12
14
|
getLockfileName,
|
|
13
15
|
hasTurboConfig,
|
|
@@ -421,4 +423,147 @@ describe('docker templates', () => {
|
|
|
421
423
|
expect(hasTurboConfig('/test/project')).toBe(false);
|
|
422
424
|
});
|
|
423
425
|
});
|
|
426
|
+
|
|
427
|
+
describe('generateNextjsDockerfile', () => {
|
|
428
|
+
const baseOptions = {
|
|
429
|
+
imageName: 'web',
|
|
430
|
+
baseImage: 'node:22-alpine',
|
|
431
|
+
port: 3001,
|
|
432
|
+
appPath: 'apps/web',
|
|
433
|
+
turboPackage: '@myapp/web',
|
|
434
|
+
packageManager: 'pnpm' as const,
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
it('should generate Next.js standalone Dockerfile', () => {
|
|
438
|
+
const dockerfile = generateNextjsDockerfile(baseOptions);
|
|
439
|
+
|
|
440
|
+
expect(dockerfile).toContain('# Next.js standalone Dockerfile');
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('should include four stages: pruner, deps, builder, runner', () => {
|
|
444
|
+
const dockerfile = generateNextjsDockerfile(baseOptions);
|
|
445
|
+
|
|
446
|
+
expect(dockerfile).toContain('AS pruner');
|
|
447
|
+
expect(dockerfile).toContain('AS deps');
|
|
448
|
+
expect(dockerfile).toContain('AS builder');
|
|
449
|
+
expect(dockerfile).toContain('AS runner');
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('should use turbo prune for monorepo optimization', () => {
|
|
453
|
+
const dockerfile = generateNextjsDockerfile(baseOptions);
|
|
454
|
+
|
|
455
|
+
expect(dockerfile).toContain('turbo prune @myapp/web --docker');
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('should copy standalone output', () => {
|
|
459
|
+
const dockerfile = generateNextjsDockerfile(baseOptions);
|
|
460
|
+
|
|
461
|
+
expect(dockerfile).toContain('.next/standalone');
|
|
462
|
+
expect(dockerfile).toContain('.next/static');
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it('should set NEXT_TELEMETRY_DISABLED', () => {
|
|
466
|
+
const dockerfile = generateNextjsDockerfile(baseOptions);
|
|
467
|
+
|
|
468
|
+
expect(dockerfile).toContain('NEXT_TELEMETRY_DISABLED=1');
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it('should create nextjs user', () => {
|
|
472
|
+
const dockerfile = generateNextjsDockerfile(baseOptions);
|
|
473
|
+
|
|
474
|
+
expect(dockerfile).toContain('adduser --system --uid 1001 nextjs');
|
|
475
|
+
expect(dockerfile).toContain('USER nextjs');
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('should expose configured port', () => {
|
|
479
|
+
const dockerfile = generateNextjsDockerfile(baseOptions);
|
|
480
|
+
|
|
481
|
+
expect(dockerfile).toContain('EXPOSE 3001');
|
|
482
|
+
expect(dockerfile).toContain('ENV PORT=3001');
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it('should use npm when specified', () => {
|
|
486
|
+
const dockerfile = generateNextjsDockerfile({
|
|
487
|
+
...baseOptions,
|
|
488
|
+
packageManager: 'npm',
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
expect(dockerfile).toContain('npx turbo');
|
|
492
|
+
expect(dockerfile).toContain('package-lock.json');
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
describe('generateBackendDockerfile', () => {
|
|
497
|
+
const baseOptions = {
|
|
498
|
+
imageName: 'api',
|
|
499
|
+
baseImage: 'node:22-alpine',
|
|
500
|
+
port: 3000,
|
|
501
|
+
appPath: 'apps/api',
|
|
502
|
+
turboPackage: '@myapp/api',
|
|
503
|
+
packageManager: 'pnpm' as const,
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
it('should generate backend Dockerfile with turbo prune', () => {
|
|
507
|
+
const dockerfile = generateBackendDockerfile(baseOptions);
|
|
508
|
+
|
|
509
|
+
expect(dockerfile).toContain('# Backend Dockerfile with turbo prune');
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it('should include four stages: pruner, deps, builder, runner', () => {
|
|
513
|
+
const dockerfile = generateBackendDockerfile(baseOptions);
|
|
514
|
+
|
|
515
|
+
expect(dockerfile).toContain('AS pruner');
|
|
516
|
+
expect(dockerfile).toContain('AS deps');
|
|
517
|
+
expect(dockerfile).toContain('AS builder');
|
|
518
|
+
expect(dockerfile).toContain('AS runner');
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it('should use turbo prune for the package', () => {
|
|
522
|
+
const dockerfile = generateBackendDockerfile(baseOptions);
|
|
523
|
+
|
|
524
|
+
expect(dockerfile).toContain('turbo prune @myapp/api --docker');
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it('should build using gkm', () => {
|
|
528
|
+
const dockerfile = generateBackendDockerfile(baseOptions);
|
|
529
|
+
|
|
530
|
+
expect(dockerfile).toContain('gkm build --provider server --production');
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it('should copy bundled server.mjs', () => {
|
|
534
|
+
const dockerfile = generateBackendDockerfile(baseOptions);
|
|
535
|
+
|
|
536
|
+
expect(dockerfile).toContain('.gkm/server/dist/server.mjs');
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
it('should create hono user', () => {
|
|
540
|
+
const dockerfile = generateBackendDockerfile(baseOptions);
|
|
541
|
+
|
|
542
|
+
expect(dockerfile).toContain('adduser --system --uid 1001 hono');
|
|
543
|
+
expect(dockerfile).toContain('USER hono');
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it('should include health check with default path', () => {
|
|
547
|
+
const dockerfile = generateBackendDockerfile(baseOptions);
|
|
548
|
+
|
|
549
|
+
expect(dockerfile).toContain('HEALTHCHECK');
|
|
550
|
+
expect(dockerfile).toContain('/health');
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it('should use custom health check path when provided', () => {
|
|
554
|
+
const dockerfile = generateBackendDockerfile({
|
|
555
|
+
...baseOptions,
|
|
556
|
+
healthCheckPath: '/api/health',
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
expect(dockerfile).toContain('/api/health');
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it('should expose configured port', () => {
|
|
563
|
+
const dockerfile = generateBackendDockerfile(baseOptions);
|
|
564
|
+
|
|
565
|
+
expect(dockerfile).toContain('EXPOSE 3000');
|
|
566
|
+
expect(dockerfile).toContain('ENV PORT=3000');
|
|
567
|
+
});
|
|
568
|
+
});
|
|
424
569
|
});
|