@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.
Files changed (118) hide show
  1. package/dist/{bundler-C74EKlNa.cjs → bundler-CyHg1v_T.cjs} +3 -3
  2. package/dist/{bundler-C74EKlNa.cjs.map → bundler-CyHg1v_T.cjs.map} +1 -1
  3. package/dist/{bundler-B6z6HEeh.mjs → bundler-DQIuE3Kn.mjs} +3 -3
  4. package/dist/{bundler-B6z6HEeh.mjs.map → bundler-DQIuE3Kn.mjs.map} +1 -1
  5. package/dist/{config-DYULeEv8.mjs → config-BaYqrF3n.mjs} +48 -10
  6. package/dist/config-BaYqrF3n.mjs.map +1 -0
  7. package/dist/{config-AmInkU7k.cjs → config-CxrLu8ia.cjs} +53 -9
  8. package/dist/config-CxrLu8ia.cjs.map +1 -0
  9. package/dist/config.cjs +4 -1
  10. package/dist/config.d.cts +27 -2
  11. package/dist/config.d.cts.map +1 -1
  12. package/dist/config.d.mts +27 -2
  13. package/dist/config.d.mts.map +1 -1
  14. package/dist/config.mjs +3 -2
  15. package/dist/dokploy-api-B0w17y4_.mjs +3 -0
  16. package/dist/{dokploy-api-CaETb2L6.mjs → dokploy-api-B9qR2Yn1.mjs} +1 -1
  17. package/dist/{dokploy-api-CaETb2L6.mjs.map → dokploy-api-B9qR2Yn1.mjs.map} +1 -1
  18. package/dist/dokploy-api-BnGeUqN4.cjs +3 -0
  19. package/dist/{dokploy-api-C7F9VykY.cjs → dokploy-api-C5czOZoc.cjs} +1 -1
  20. package/dist/{dokploy-api-C7F9VykY.cjs.map → dokploy-api-C5czOZoc.cjs.map} +1 -1
  21. package/dist/{encryption-D7Efcdi9.cjs → encryption-BAz0xQ1Q.cjs} +1 -1
  22. package/dist/{encryption-D7Efcdi9.cjs.map → encryption-BAz0xQ1Q.cjs.map} +1 -1
  23. package/dist/{encryption-h4Nb6W-M.mjs → encryption-JtMsiGNp.mjs} +2 -2
  24. package/dist/{encryption-h4Nb6W-M.mjs.map → encryption-JtMsiGNp.mjs.map} +1 -1
  25. package/dist/index-CWN-bgrO.d.mts +495 -0
  26. package/dist/index-CWN-bgrO.d.mts.map +1 -0
  27. package/dist/index-DEWYvYvg.d.cts +495 -0
  28. package/dist/index-DEWYvYvg.d.cts.map +1 -0
  29. package/dist/index.cjs +2639 -563
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.mjs +2634 -563
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/{openapi-CZVcfxk-.mjs → openapi-CgqR6Jkw.mjs} +3 -3
  34. package/dist/{openapi-CZVcfxk-.mjs.map → openapi-CgqR6Jkw.mjs.map} +1 -1
  35. package/dist/{openapi-C89hhkZC.cjs → openapi-DfpxS0xv.cjs} +8 -2
  36. package/dist/{openapi-C89hhkZC.cjs.map → openapi-DfpxS0xv.cjs.map} +1 -1
  37. package/dist/{openapi-react-query-CM2_qlW9.mjs → openapi-react-query-5rSortLH.mjs} +1 -1
  38. package/dist/{openapi-react-query-CM2_qlW9.mjs.map → openapi-react-query-5rSortLH.mjs.map} +1 -1
  39. package/dist/{openapi-react-query-iKjfLzff.cjs → openapi-react-query-DvNpdDpM.cjs} +1 -1
  40. package/dist/{openapi-react-query-iKjfLzff.cjs.map → openapi-react-query-DvNpdDpM.cjs.map} +1 -1
  41. package/dist/openapi-react-query.cjs +1 -1
  42. package/dist/openapi-react-query.mjs +1 -1
  43. package/dist/openapi.cjs +3 -2
  44. package/dist/openapi.d.cts +1 -1
  45. package/dist/openapi.d.mts +1 -1
  46. package/dist/openapi.mjs +3 -2
  47. package/dist/{storage-Bn3K9Ccu.cjs → storage-BPRgh3DU.cjs} +136 -5
  48. package/dist/storage-BPRgh3DU.cjs.map +1 -0
  49. package/dist/{storage-nkGIjeXt.mjs → storage-DNj_I11J.mjs} +1 -1
  50. package/dist/storage-Dhst7BhI.mjs +272 -0
  51. package/dist/storage-Dhst7BhI.mjs.map +1 -0
  52. package/dist/{storage-UfyTn7Zm.cjs → storage-fOR8dMu5.cjs} +1 -1
  53. package/dist/{types-iFk5ms7y.d.mts → types-K2uQJ-FO.d.mts} +2 -2
  54. package/dist/{types-BgaMXsUa.d.cts.map → types-K2uQJ-FO.d.mts.map} +1 -1
  55. package/dist/{types-BgaMXsUa.d.cts → types-l53qUmGt.d.cts} +2 -2
  56. package/dist/{types-iFk5ms7y.d.mts.map → types-l53qUmGt.d.cts.map} +1 -1
  57. package/dist/workspace/index.cjs +19 -0
  58. package/dist/workspace/index.d.cts +3 -0
  59. package/dist/workspace/index.d.mts +3 -0
  60. package/dist/workspace/index.mjs +3 -0
  61. package/dist/workspace-CPLEZDZf.mjs +3788 -0
  62. package/dist/workspace-CPLEZDZf.mjs.map +1 -0
  63. package/dist/workspace-iWgBlX6h.cjs +3885 -0
  64. package/dist/workspace-iWgBlX6h.cjs.map +1 -0
  65. package/package.json +8 -3
  66. package/src/build/__tests__/workspace-build.spec.ts +215 -0
  67. package/src/build/index.ts +189 -1
  68. package/src/config.ts +71 -14
  69. package/src/deploy/__tests__/docker.spec.ts +1 -1
  70. package/src/deploy/__tests__/index.spec.ts +305 -1
  71. package/src/deploy/index.ts +426 -4
  72. package/src/deploy/types.ts +32 -0
  73. package/src/dev/__tests__/index.spec.ts +572 -1
  74. package/src/dev/index.ts +582 -2
  75. package/src/docker/__tests__/compose.spec.ts +425 -0
  76. package/src/docker/__tests__/templates.spec.ts +145 -0
  77. package/src/docker/compose.ts +248 -0
  78. package/src/docker/index.ts +159 -3
  79. package/src/docker/templates.ts +219 -4
  80. package/src/index.ts +24 -0
  81. package/src/init/__tests__/generators.spec.ts +17 -24
  82. package/src/init/__tests__/init.spec.ts +157 -5
  83. package/src/init/generators/auth.ts +220 -0
  84. package/src/init/generators/config.ts +61 -4
  85. package/src/init/generators/docker.ts +115 -8
  86. package/src/init/generators/env.ts +7 -127
  87. package/src/init/generators/index.ts +1 -0
  88. package/src/init/generators/models.ts +3 -1
  89. package/src/init/generators/monorepo.ts +154 -10
  90. package/src/init/generators/package.ts +5 -3
  91. package/src/init/generators/web.ts +213 -0
  92. package/src/init/index.ts +290 -58
  93. package/src/init/templates/api.ts +38 -29
  94. package/src/init/templates/index.ts +132 -4
  95. package/src/init/templates/minimal.ts +33 -35
  96. package/src/init/templates/serverless.ts +16 -19
  97. package/src/init/templates/worker.ts +50 -25
  98. package/src/init/versions.ts +47 -0
  99. package/src/secrets/keystore.ts +144 -0
  100. package/src/secrets/storage.ts +109 -6
  101. package/src/test/index.ts +97 -0
  102. package/src/workspace/__tests__/client-generator.spec.ts +357 -0
  103. package/src/workspace/__tests__/index.spec.ts +543 -0
  104. package/src/workspace/__tests__/schema.spec.ts +519 -0
  105. package/src/workspace/__tests__/type-inference.spec.ts +251 -0
  106. package/src/workspace/client-generator.ts +307 -0
  107. package/src/workspace/index.ts +372 -0
  108. package/src/workspace/schema.ts +368 -0
  109. package/src/workspace/types.ts +336 -0
  110. package/tsconfig.tsbuildinfo +1 -1
  111. package/tsdown.config.ts +1 -0
  112. package/dist/config-AmInkU7k.cjs.map +0 -1
  113. package/dist/config-DYULeEv8.mjs.map +0 -1
  114. package/dist/dokploy-api-B7KxOQr3.cjs +0 -3
  115. package/dist/dokploy-api-DHvfmWbi.mjs +0 -3
  116. package/dist/storage-BaOP55oq.mjs +0 -147
  117. package/dist/storage-BaOP55oq.mjs.map +0 -1
  118. package/dist/storage-Bn3K9Ccu.cjs.map +0 -1
@@ -1,13 +1,24 @@
1
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
1
2
  import type { AddressInfo } from 'node:net';
2
3
  import { createServer } from 'node:net';
3
- import { afterEach, describe, expect, it } from 'vitest';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
7
+ import type {
8
+ NormalizedAppConfig,
9
+ NormalizedWorkspace,
10
+ } from '../../workspace/index.js';
4
11
  import {
12
+ checkPortConflicts,
5
13
  findAvailablePort,
14
+ generateAllDependencyEnvVars,
6
15
  isPortAvailable,
7
16
  normalizeHooksConfig,
8
17
  normalizeProductionConfig,
9
18
  normalizeStudioConfig,
10
19
  normalizeTelescopeConfig,
20
+ validateFrontendApp,
21
+ validateFrontendApps,
11
22
  } from '../index';
12
23
 
13
24
  // Skip port-related tests in CI due to flaky port binding issues
@@ -396,3 +407,563 @@ describe('normalizeProductionConfig', () => {
396
407
  expect(result?.bundle).toBe(false);
397
408
  });
398
409
  });
410
+
411
+ describe('Workspace Dev Server', () => {
412
+ /**
413
+ * Helper to create a test workspace configuration.
414
+ * Automatically adds resolvedDeployTarget to each app.
415
+ */
416
+ function createTestWorkspace(
417
+ apps: Record<string, Omit<NormalizedAppConfig, 'resolvedDeployTarget'>>,
418
+ overrides: Partial<NormalizedWorkspace> = {},
419
+ ): NormalizedWorkspace {
420
+ const appsWithDeployTarget: NormalizedWorkspace['apps'] = {};
421
+ for (const [name, app] of Object.entries(apps)) {
422
+ appsWithDeployTarget[name] = {
423
+ ...app,
424
+ resolvedDeployTarget: 'dokploy',
425
+ };
426
+ }
427
+ return {
428
+ name: 'test-workspace',
429
+ root: '/test/workspace',
430
+ apps: appsWithDeployTarget,
431
+ services: {},
432
+ deploy: { default: 'dokploy' },
433
+ shared: { packages: ['packages/*'] },
434
+ secrets: {},
435
+ ...overrides,
436
+ };
437
+ }
438
+
439
+ describe('checkPortConflicts', () => {
440
+ it('should return empty array when no conflicts', () => {
441
+ const workspace = createTestWorkspace({
442
+ api: {
443
+ type: 'backend',
444
+ path: 'apps/api',
445
+ port: 3000,
446
+ dependencies: [],
447
+ },
448
+ web: {
449
+ type: 'frontend',
450
+ path: 'apps/web',
451
+ port: 3001,
452
+ dependencies: [],
453
+ },
454
+ admin: {
455
+ type: 'frontend',
456
+ path: 'apps/admin',
457
+ port: 3002,
458
+ dependencies: [],
459
+ },
460
+ });
461
+
462
+ const conflicts = checkPortConflicts(workspace);
463
+ expect(conflicts).toEqual([]);
464
+ });
465
+
466
+ it('should detect port conflicts between two apps', () => {
467
+ const workspace = createTestWorkspace({
468
+ api: {
469
+ type: 'backend',
470
+ path: 'apps/api',
471
+ port: 3000,
472
+ dependencies: [],
473
+ },
474
+ web: {
475
+ type: 'frontend',
476
+ path: 'apps/web',
477
+ port: 3000, // Same port as api!
478
+ dependencies: [],
479
+ },
480
+ });
481
+
482
+ const conflicts = checkPortConflicts(workspace);
483
+ expect(conflicts).toHaveLength(1);
484
+ expect(conflicts[0]).toEqual({
485
+ app1: 'api',
486
+ app2: 'web',
487
+ port: 3000,
488
+ });
489
+ });
490
+
491
+ it('should detect multiple port conflicts', () => {
492
+ const workspace = createTestWorkspace({
493
+ api: {
494
+ type: 'backend',
495
+ path: 'apps/api',
496
+ port: 3000,
497
+ dependencies: [],
498
+ },
499
+ auth: {
500
+ type: 'backend',
501
+ path: 'apps/auth',
502
+ port: 3000, // Conflicts with api
503
+ dependencies: [],
504
+ },
505
+ web: {
506
+ type: 'frontend',
507
+ path: 'apps/web',
508
+ port: 3001,
509
+ dependencies: [],
510
+ },
511
+ admin: {
512
+ type: 'frontend',
513
+ path: 'apps/admin',
514
+ port: 3001, // Conflicts with web
515
+ dependencies: [],
516
+ },
517
+ });
518
+
519
+ const conflicts = checkPortConflicts(workspace);
520
+ expect(conflicts).toHaveLength(2);
521
+ });
522
+
523
+ it('should handle single app workspace', () => {
524
+ const workspace = createTestWorkspace({
525
+ api: {
526
+ type: 'backend',
527
+ path: 'apps/api',
528
+ port: 3000,
529
+ dependencies: [],
530
+ },
531
+ });
532
+
533
+ const conflicts = checkPortConflicts(workspace);
534
+ expect(conflicts).toEqual([]);
535
+ });
536
+ });
537
+
538
+ describe('generateAllDependencyEnvVars', () => {
539
+ it('should generate empty object for apps with no dependencies', () => {
540
+ const workspace = createTestWorkspace({
541
+ api: {
542
+ type: 'backend',
543
+ path: 'apps/api',
544
+ port: 3000,
545
+ dependencies: [],
546
+ },
547
+ web: {
548
+ type: 'frontend',
549
+ path: 'apps/web',
550
+ port: 3001,
551
+ dependencies: [],
552
+ },
553
+ });
554
+
555
+ const env = generateAllDependencyEnvVars(workspace);
556
+ expect(env).toEqual({});
557
+ });
558
+
559
+ it('should generate URL env vars for dependencies', () => {
560
+ const workspace = createTestWorkspace({
561
+ api: {
562
+ type: 'backend',
563
+ path: 'apps/api',
564
+ port: 3000,
565
+ dependencies: [],
566
+ },
567
+ auth: {
568
+ type: 'backend',
569
+ path: 'apps/auth',
570
+ port: 3001,
571
+ dependencies: [],
572
+ },
573
+ web: {
574
+ type: 'frontend',
575
+ path: 'apps/web',
576
+ port: 3002,
577
+ dependencies: ['api', 'auth'],
578
+ },
579
+ });
580
+
581
+ const env = generateAllDependencyEnvVars(workspace);
582
+ expect(env).toEqual({
583
+ API_URL: 'http://localhost:3000',
584
+ AUTH_URL: 'http://localhost:3001',
585
+ });
586
+ });
587
+
588
+ it('should handle cross-dependencies between backend apps', () => {
589
+ const workspace = createTestWorkspace({
590
+ 'api-gateway': {
591
+ type: 'backend',
592
+ path: 'apps/api-gateway',
593
+ port: 3000,
594
+ dependencies: [],
595
+ },
596
+ 'user-service': {
597
+ type: 'backend',
598
+ path: 'apps/user-service',
599
+ port: 3001,
600
+ dependencies: [],
601
+ },
602
+ 'order-service': {
603
+ type: 'backend',
604
+ path: 'apps/order-service',
605
+ port: 3002,
606
+ dependencies: ['user-service'],
607
+ },
608
+ });
609
+
610
+ const env = generateAllDependencyEnvVars(workspace);
611
+ expect(env).toEqual({
612
+ 'USER-SERVICE_URL': 'http://localhost:3001',
613
+ });
614
+ });
615
+
616
+ it('should use custom URL prefix', () => {
617
+ const workspace = createTestWorkspace({
618
+ api: {
619
+ type: 'backend',
620
+ path: 'apps/api',
621
+ port: 3000,
622
+ dependencies: [],
623
+ },
624
+ web: {
625
+ type: 'frontend',
626
+ path: 'apps/web',
627
+ port: 3001,
628
+ dependencies: ['api'],
629
+ },
630
+ });
631
+
632
+ const env = generateAllDependencyEnvVars(workspace, 'http://127.0.0.1');
633
+ expect(env).toEqual({
634
+ API_URL: 'http://127.0.0.1:3000',
635
+ });
636
+ });
637
+
638
+ it('should handle complex dependency graph', () => {
639
+ const workspace = createTestWorkspace({
640
+ api: {
641
+ type: 'backend',
642
+ path: 'apps/api',
643
+ port: 3000,
644
+ dependencies: [],
645
+ },
646
+ auth: {
647
+ type: 'backend',
648
+ path: 'apps/auth',
649
+ port: 3001,
650
+ dependencies: [],
651
+ },
652
+ payments: {
653
+ type: 'backend',
654
+ path: 'apps/payments',
655
+ port: 3002,
656
+ dependencies: ['auth'], // Payments depends on auth
657
+ },
658
+ web: {
659
+ type: 'frontend',
660
+ path: 'apps/web',
661
+ port: 3003,
662
+ dependencies: ['api', 'auth'], // Web depends on api and auth
663
+ },
664
+ admin: {
665
+ type: 'frontend',
666
+ path: 'apps/admin',
667
+ port: 3004,
668
+ dependencies: ['api', 'payments'], // Admin depends on api and payments
669
+ },
670
+ });
671
+
672
+ const env = generateAllDependencyEnvVars(workspace);
673
+ expect(env).toEqual({
674
+ AUTH_URL: 'http://localhost:3001',
675
+ API_URL: 'http://localhost:3000',
676
+ PAYMENTS_URL: 'http://localhost:3002',
677
+ });
678
+ });
679
+ });
680
+
681
+ describe('validateFrontendApp', () => {
682
+ let testDir: string;
683
+
684
+ beforeEach(() => {
685
+ // Create a unique temp directory for each test
686
+ testDir = join(
687
+ tmpdir(),
688
+ `gkm-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
689
+ );
690
+ mkdirSync(testDir, { recursive: true });
691
+ });
692
+
693
+ afterEach(() => {
694
+ // Clean up temp directory
695
+ if (existsSync(testDir)) {
696
+ rmSync(testDir, { recursive: true, force: true });
697
+ }
698
+ });
699
+
700
+ it('should validate a properly configured Next.js app', async () => {
701
+ // Create a valid Next.js app structure
702
+ const appDir = join(testDir, 'apps/web');
703
+ mkdirSync(appDir, { recursive: true });
704
+
705
+ // Create next.config.js
706
+ writeFileSync(join(appDir, 'next.config.js'), 'module.exports = {}');
707
+
708
+ // Create package.json with next dependency
709
+ writeFileSync(
710
+ join(appDir, 'package.json'),
711
+ JSON.stringify({
712
+ name: 'web',
713
+ dependencies: { next: '^14.0.0', react: '^18.0.0' },
714
+ scripts: { dev: 'next dev' },
715
+ }),
716
+ );
717
+
718
+ const result = await validateFrontendApp('web', 'apps/web', testDir);
719
+
720
+ expect(result.valid).toBe(true);
721
+ expect(result.errors).toHaveLength(0);
722
+ expect(result.warnings).toHaveLength(0);
723
+ });
724
+
725
+ it('should detect missing Next.js config file', async () => {
726
+ const appDir = join(testDir, 'apps/web');
727
+ mkdirSync(appDir, { recursive: true });
728
+
729
+ // Create package.json without next.config.js
730
+ writeFileSync(
731
+ join(appDir, 'package.json'),
732
+ JSON.stringify({
733
+ name: 'web',
734
+ dependencies: { next: '^14.0.0' },
735
+ scripts: { dev: 'next dev' },
736
+ }),
737
+ );
738
+
739
+ const result = await validateFrontendApp('web', 'apps/web', testDir);
740
+
741
+ expect(result.valid).toBe(false);
742
+ expect(result.errors).toContainEqual(
743
+ expect.stringContaining('Next.js config file not found'),
744
+ );
745
+ });
746
+
747
+ it('should detect missing Next.js dependency', async () => {
748
+ const appDir = join(testDir, 'apps/web');
749
+ mkdirSync(appDir, { recursive: true });
750
+
751
+ // Create next.config.js
752
+ writeFileSync(join(appDir, 'next.config.js'), 'module.exports = {}');
753
+
754
+ // Create package.json WITHOUT next dependency
755
+ writeFileSync(
756
+ join(appDir, 'package.json'),
757
+ JSON.stringify({
758
+ name: 'web',
759
+ dependencies: { react: '^18.0.0' },
760
+ scripts: { dev: 'echo no next' },
761
+ }),
762
+ );
763
+
764
+ const result = await validateFrontendApp('web', 'apps/web', testDir);
765
+
766
+ expect(result.valid).toBe(false);
767
+ expect(result.errors).toContainEqual(
768
+ expect.stringContaining('Next.js not found in dependencies'),
769
+ );
770
+ });
771
+
772
+ it('should detect missing package.json', async () => {
773
+ const appDir = join(testDir, 'apps/web');
774
+ mkdirSync(appDir, { recursive: true });
775
+
776
+ // Create next.config.js but no package.json
777
+ writeFileSync(join(appDir, 'next.config.js'), 'module.exports = {}');
778
+
779
+ const result = await validateFrontendApp('web', 'apps/web', testDir);
780
+
781
+ expect(result.valid).toBe(false);
782
+ expect(result.errors).toContainEqual(
783
+ expect.stringContaining('package.json not found'),
784
+ );
785
+ });
786
+
787
+ it('should warn about missing dev script', async () => {
788
+ const appDir = join(testDir, 'apps/web');
789
+ mkdirSync(appDir, { recursive: true });
790
+
791
+ writeFileSync(join(appDir, 'next.config.js'), 'module.exports = {}');
792
+
793
+ // Create package.json WITHOUT dev script
794
+ writeFileSync(
795
+ join(appDir, 'package.json'),
796
+ JSON.stringify({
797
+ name: 'web',
798
+ dependencies: { next: '^14.0.0' },
799
+ // No scripts
800
+ }),
801
+ );
802
+
803
+ const result = await validateFrontendApp('web', 'apps/web', testDir);
804
+
805
+ expect(result.valid).toBe(true); // Still valid, just a warning
806
+ expect(result.warnings).toContainEqual(
807
+ expect.stringContaining('No "dev" script found'),
808
+ );
809
+ });
810
+
811
+ it('should accept next.config.ts', async () => {
812
+ const appDir = join(testDir, 'apps/web');
813
+ mkdirSync(appDir, { recursive: true });
814
+
815
+ // Create next.config.ts (TypeScript config)
816
+ writeFileSync(
817
+ join(appDir, 'next.config.ts'),
818
+ 'export default { reactStrictMode: true }',
819
+ );
820
+
821
+ writeFileSync(
822
+ join(appDir, 'package.json'),
823
+ JSON.stringify({
824
+ name: 'web',
825
+ dependencies: { next: '^14.0.0' },
826
+ scripts: { dev: 'next dev' },
827
+ }),
828
+ );
829
+
830
+ const result = await validateFrontendApp('web', 'apps/web', testDir);
831
+
832
+ expect(result.valid).toBe(true);
833
+ });
834
+
835
+ it('should accept next.config.mjs', async () => {
836
+ const appDir = join(testDir, 'apps/web');
837
+ mkdirSync(appDir, { recursive: true });
838
+
839
+ // Create next.config.mjs (ESM config)
840
+ writeFileSync(
841
+ join(appDir, 'next.config.mjs'),
842
+ 'export default { reactStrictMode: true }',
843
+ );
844
+
845
+ writeFileSync(
846
+ join(appDir, 'package.json'),
847
+ JSON.stringify({
848
+ name: 'web',
849
+ dependencies: { next: '^14.0.0' },
850
+ scripts: { dev: 'next dev' },
851
+ }),
852
+ );
853
+
854
+ const result = await validateFrontendApp('web', 'apps/web', testDir);
855
+
856
+ expect(result.valid).toBe(true);
857
+ });
858
+
859
+ it('should detect next in devDependencies', async () => {
860
+ const appDir = join(testDir, 'apps/web');
861
+ mkdirSync(appDir, { recursive: true });
862
+
863
+ writeFileSync(join(appDir, 'next.config.js'), 'module.exports = {}');
864
+
865
+ // Next in devDependencies (valid)
866
+ writeFileSync(
867
+ join(appDir, 'package.json'),
868
+ JSON.stringify({
869
+ name: 'web',
870
+ devDependencies: { next: '^14.0.0' },
871
+ scripts: { dev: 'next dev' },
872
+ }),
873
+ );
874
+
875
+ const result = await validateFrontendApp('web', 'apps/web', testDir);
876
+
877
+ expect(result.valid).toBe(true);
878
+ });
879
+ });
880
+
881
+ describe('validateFrontendApps', () => {
882
+ let testDir: string;
883
+
884
+ beforeEach(() => {
885
+ testDir = join(
886
+ tmpdir(),
887
+ `gkm-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
888
+ );
889
+ mkdirSync(testDir, { recursive: true });
890
+ });
891
+
892
+ afterEach(() => {
893
+ if (existsSync(testDir)) {
894
+ rmSync(testDir, { recursive: true, force: true });
895
+ }
896
+ });
897
+
898
+ it('should validate only frontend apps', async () => {
899
+ // Create valid frontend app
900
+ const webDir = join(testDir, 'apps/web');
901
+ mkdirSync(webDir, { recursive: true });
902
+ writeFileSync(join(webDir, 'next.config.js'), 'module.exports = {}');
903
+ writeFileSync(
904
+ join(webDir, 'package.json'),
905
+ JSON.stringify({
906
+ name: 'web',
907
+ dependencies: { next: '^14.0.0' },
908
+ scripts: { dev: 'next dev' },
909
+ }),
910
+ );
911
+
912
+ const workspace: NormalizedWorkspace = {
913
+ name: 'test-workspace',
914
+ root: testDir,
915
+ apps: {
916
+ api: {
917
+ type: 'backend',
918
+ path: 'apps/api',
919
+ port: 3000,
920
+ dependencies: [],
921
+ resolvedDeployTarget: 'dokploy',
922
+ },
923
+ web: {
924
+ type: 'frontend',
925
+ path: 'apps/web',
926
+ port: 3001,
927
+ dependencies: ['api'],
928
+ resolvedDeployTarget: 'dokploy',
929
+ },
930
+ },
931
+ services: {},
932
+ deploy: { default: 'dokploy' },
933
+ shared: { packages: [] },
934
+ secrets: {},
935
+ };
936
+
937
+ const results = await validateFrontendApps(workspace);
938
+
939
+ // Should only validate the frontend app
940
+ expect(results).toHaveLength(1);
941
+ expect(results[0]?.appName).toBe('web');
942
+ expect(results[0]?.valid).toBe(true);
943
+ });
944
+
945
+ it('should return empty array for backend-only workspace', async () => {
946
+ const workspace: NormalizedWorkspace = {
947
+ name: 'test-workspace',
948
+ root: testDir,
949
+ apps: {
950
+ api: {
951
+ type: 'backend',
952
+ path: 'apps/api',
953
+ port: 3000,
954
+ dependencies: [],
955
+ resolvedDeployTarget: 'dokploy',
956
+ },
957
+ },
958
+ services: {},
959
+ deploy: { default: 'dokploy' },
960
+ shared: { packages: [] },
961
+ secrets: {},
962
+ };
963
+
964
+ const results = await validateFrontendApps(workspace);
965
+
966
+ expect(results).toHaveLength(0);
967
+ });
968
+ });
969
+ });