@geekmidas/cli 1.2.1 → 1.2.2

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.
@@ -101,6 +101,7 @@ describe('Client Generator', () => {
101
101
  port: 3000,
102
102
  dependencies: [],
103
103
  routes: './src/**/*.ts',
104
+ resolvedDeployTarget: 'dokploy',
104
105
  },
105
106
  auth: {
106
107
  type: 'backend',
@@ -108,12 +109,14 @@ describe('Client Generator', () => {
108
109
  port: 3001,
109
110
  dependencies: [],
110
111
  routes: './src/**/*.ts',
112
+ resolvedDeployTarget: 'dokploy',
111
113
  },
112
114
  web: {
113
115
  type: 'frontend',
114
116
  path: 'apps/web',
115
117
  port: 3002,
116
118
  dependencies: ['api', 'auth'],
119
+ resolvedDeployTarget: 'dokploy',
117
120
  },
118
121
  },
119
122
  services: {},
@@ -137,12 +140,14 @@ describe('Client Generator', () => {
137
140
  port: 3000,
138
141
  dependencies: [],
139
142
  routes: './src/**/*.ts',
143
+ resolvedDeployTarget: 'dokploy',
140
144
  },
141
145
  worker: {
142
146
  type: 'backend',
143
147
  path: 'apps/worker',
144
148
  port: 3001,
145
149
  dependencies: [],
150
+ resolvedDeployTarget: 'dokploy',
146
151
  // No routes - not an HTTP backend
147
152
  },
148
153
  web: {
@@ -150,6 +155,7 @@ describe('Client Generator', () => {
150
155
  path: 'apps/web',
151
156
  port: 3002,
152
157
  dependencies: ['api', 'worker'],
158
+ resolvedDeployTarget: 'dokploy',
153
159
  },
154
160
  },
155
161
  services: {},
@@ -173,6 +179,7 @@ describe('Client Generator', () => {
173
179
  port: 3000,
174
180
  dependencies: [],
175
181
  routes: './src/**/*.ts',
182
+ resolvedDeployTarget: 'dokploy',
176
183
  },
177
184
  },
178
185
  services: {},
@@ -198,24 +205,28 @@ describe('Client Generator', () => {
198
205
  port: 3000,
199
206
  dependencies: [],
200
207
  routes: './src/**/*.ts',
208
+ resolvedDeployTarget: 'dokploy',
201
209
  },
202
210
  web: {
203
211
  type: 'frontend',
204
212
  path: 'apps/web',
205
213
  port: 3001,
206
214
  dependencies: ['api'],
215
+ resolvedDeployTarget: 'dokploy',
207
216
  },
208
217
  admin: {
209
218
  type: 'frontend',
210
219
  path: 'apps/admin',
211
220
  port: 3002,
212
221
  dependencies: ['api'],
222
+ resolvedDeployTarget: 'dokploy',
213
223
  },
214
224
  docs: {
215
225
  type: 'frontend',
216
226
  path: 'apps/docs',
217
227
  port: 3003,
218
228
  dependencies: [], // No API dependency
229
+ resolvedDeployTarget: 'dokploy',
219
230
  },
220
231
  },
221
232
  services: {},
@@ -239,6 +250,7 @@ describe('Client Generator', () => {
239
250
  port: 3000,
240
251
  dependencies: [],
241
252
  routes: './src/**/*.ts',
253
+ resolvedDeployTarget: 'dokploy',
242
254
  },
243
255
  },
244
256
  services: {},
@@ -280,6 +292,7 @@ describe('Client Generator', () => {
280
292
  path: 'apps/web',
281
293
  port: 3001,
282
294
  dependencies: [],
295
+ resolvedDeployTarget: 'dokploy',
283
296
  },
284
297
  },
285
298
  services: {},
@@ -303,6 +316,7 @@ describe('Client Generator', () => {
303
316
  port: 3000,
304
317
  dependencies: [],
305
318
  routes: './src/**/*.ts',
319
+ resolvedDeployTarget: 'dokploy',
306
320
  },
307
321
  },
308
322
  services: {},
@@ -332,6 +346,7 @@ describe('Client Generator', () => {
332
346
  port: 3000,
333
347
  dependencies: [],
334
348
  routes: './src/endpoints/**/*.ts',
349
+ resolvedDeployTarget: 'dokploy',
335
350
  },
336
351
  web: {
337
352
  type: 'frontend',
@@ -339,6 +354,7 @@ describe('Client Generator', () => {
339
354
  port: 3001,
340
355
  dependencies: ['api'],
341
356
  client: { output: 'lib/api' },
357
+ resolvedDeployTarget: 'dokploy',
342
358
  },
343
359
  },
344
360
  services: {},
@@ -354,4 +370,412 @@ describe('Client Generator', () => {
354
370
  expect(results[0]?.reason).toBe('No endpoints found in backend');
355
371
  });
356
372
  });
373
+
374
+ describe('hot reload - hash-based change detection', () => {
375
+ let testDir: string;
376
+
377
+ beforeEach(() => {
378
+ testDir = join(
379
+ tmpdir(),
380
+ `gkm-hotreload-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
381
+ );
382
+ mkdirSync(testDir, { recursive: true });
383
+ clearSpecHashCache();
384
+ });
385
+
386
+ afterEach(() => {
387
+ if (existsSync(testDir)) {
388
+ rmSync(testDir, { recursive: true, force: true });
389
+ }
390
+ });
391
+
392
+ function createEndpointFile(
393
+ dir: string,
394
+ filename: string,
395
+ exportName: string,
396
+ path: string,
397
+ method: string,
398
+ ): void {
399
+ const content = `
400
+ import { e } from '@geekmidas/constructs/endpoints';
401
+ import { z } from 'zod';
402
+
403
+ export const ${exportName} = e
404
+ .${method.toLowerCase()}('${path}')
405
+ .output(z.object({ message: z.string() }))
406
+ .handle(async () => ({ message: 'Hello' }));
407
+ `;
408
+ const { writeFileSync } = require('node:fs');
409
+ const { dirname } = require('node:path');
410
+ mkdirSync(dirname(join(dir, filename)), { recursive: true });
411
+ writeFileSync(join(dir, filename), content);
412
+ }
413
+
414
+ function createWorkspace(root: string): NormalizedWorkspace {
415
+ return {
416
+ name: 'test',
417
+ root,
418
+ apps: {
419
+ api: {
420
+ type: 'backend',
421
+ path: 'apps/api',
422
+ port: 3000,
423
+ dependencies: [],
424
+ routes: './src/endpoints/**/*.ts',
425
+ resolvedDeployTarget: 'dokploy',
426
+ },
427
+ web: {
428
+ type: 'frontend',
429
+ path: 'apps/web',
430
+ port: 3001,
431
+ dependencies: ['api'],
432
+ client: { output: 'src/api' },
433
+ resolvedDeployTarget: 'dokploy',
434
+ },
435
+ },
436
+ services: {},
437
+ deploy: { default: 'dokploy' },
438
+ shared: { packages: [] },
439
+ secrets: {},
440
+ };
441
+ }
442
+
443
+ it('should generate client on first call', async () => {
444
+ const apiDir = join(testDir, 'apps/api');
445
+ const webDir = join(testDir, 'apps/web');
446
+ mkdirSync(webDir, { recursive: true });
447
+
448
+ createEndpointFile(
449
+ apiDir,
450
+ 'src/endpoints/users.ts',
451
+ 'getUsers',
452
+ '/users',
453
+ 'GET',
454
+ );
455
+
456
+ const workspace = createWorkspace(testDir);
457
+ const results = await generateClientForFrontend(workspace, 'web', {
458
+ force: true,
459
+ });
460
+
461
+ expect(results).toHaveLength(1);
462
+ expect(results[0]?.generated).toBe(true);
463
+ expect(results[0]?.endpointCount).toBe(1);
464
+ expect(existsSync(join(webDir, 'src/api/openapi.ts'))).toBe(true);
465
+ });
466
+
467
+ it('should skip regeneration when schema has not changed', async () => {
468
+ const apiDir = join(testDir, 'apps/api');
469
+ const webDir = join(testDir, 'apps/web');
470
+ mkdirSync(webDir, { recursive: true });
471
+
472
+ createEndpointFile(
473
+ apiDir,
474
+ 'src/endpoints/users.ts',
475
+ 'getUsers',
476
+ '/users',
477
+ 'GET',
478
+ );
479
+
480
+ const workspace = createWorkspace(testDir);
481
+
482
+ // First call - should generate
483
+ const firstResults = await generateClientForFrontend(workspace, 'web', {
484
+ force: true,
485
+ });
486
+ expect(firstResults[0]?.generated).toBe(true);
487
+
488
+ // Second call - should skip (no changes)
489
+ const secondResults = await generateClientForFrontend(workspace, 'web');
490
+ expect(secondResults[0]?.generated).toBe(false);
491
+ expect(secondResults[0]?.reason).toBe('No schema changes detected');
492
+ });
493
+
494
+ it('should regenerate when endpoint schema changes', async () => {
495
+ const apiDir = join(testDir, 'apps/api');
496
+ const webDir = join(testDir, 'apps/web');
497
+ mkdirSync(webDir, { recursive: true });
498
+
499
+ createEndpointFile(
500
+ apiDir,
501
+ 'src/endpoints/users.ts',
502
+ 'getUsers',
503
+ '/users',
504
+ 'GET',
505
+ );
506
+
507
+ const workspace = createWorkspace(testDir);
508
+
509
+ // First call - generate initial client
510
+ const firstResults = await generateClientForFrontend(workspace, 'web', {
511
+ force: true,
512
+ });
513
+ expect(firstResults[0]?.generated).toBe(true);
514
+
515
+ // Second call without changes - should skip
516
+ const secondResults = await generateClientForFrontend(workspace, 'web');
517
+ expect(secondResults[0]?.generated).toBe(false);
518
+
519
+ // Modify endpoint - add new route
520
+ createEndpointFile(
521
+ apiDir,
522
+ 'src/endpoints/posts.ts',
523
+ 'getPosts',
524
+ '/posts',
525
+ 'GET',
526
+ );
527
+
528
+ // Third call - should regenerate due to schema change
529
+ const thirdResults = await generateClientForFrontend(workspace, 'web');
530
+ expect(thirdResults[0]?.generated).toBe(true);
531
+ expect(thirdResults[0]?.endpointCount).toBe(2);
532
+ });
533
+
534
+ it('should regenerate when endpoint path changes (via new file)', async () => {
535
+ const apiDir = join(testDir, 'apps/api');
536
+ const webDir = join(testDir, 'apps/web');
537
+ mkdirSync(webDir, { recursive: true });
538
+
539
+ createEndpointFile(
540
+ apiDir,
541
+ 'src/endpoints/users.ts',
542
+ 'getUsers',
543
+ '/users',
544
+ 'GET',
545
+ );
546
+
547
+ const workspace = createWorkspace(testDir);
548
+
549
+ // First call
550
+ await generateClientForFrontend(workspace, 'web', { force: true });
551
+
552
+ // Second call - skip
553
+ const skipResult = await generateClientForFrontend(workspace, 'web');
554
+ expect(skipResult[0]?.generated).toBe(false);
555
+
556
+ // Add endpoint with different path (new file to avoid ESM cache)
557
+ // This simulates what happens when a developer changes a path -
558
+ // in real dev server, the watcher would reload the module
559
+ createEndpointFile(
560
+ apiDir,
561
+ 'src/endpoints/api-users.ts',
562
+ 'getApiUsers',
563
+ '/api/users',
564
+ 'GET',
565
+ );
566
+
567
+ // Third call - should regenerate because schema changed
568
+ const regenerateResult = await generateClientForFrontend(
569
+ workspace,
570
+ 'web',
571
+ );
572
+ expect(regenerateResult[0]?.generated).toBe(true);
573
+ expect(regenerateResult[0]?.endpointCount).toBe(2); // Now has 2 endpoints
574
+ });
575
+
576
+ it('should skip when only handler implementation changes', async () => {
577
+ const apiDir = join(testDir, 'apps/api');
578
+ const webDir = join(testDir, 'apps/web');
579
+ mkdirSync(webDir, { recursive: true });
580
+
581
+ createEndpointFile(
582
+ apiDir,
583
+ 'src/endpoints/users.ts',
584
+ 'getUsers',
585
+ '/users',
586
+ 'GET',
587
+ );
588
+
589
+ const workspace = createWorkspace(testDir);
590
+
591
+ // First call
592
+ await generateClientForFrontend(workspace, 'web', { force: true });
593
+
594
+ // Change only the handler implementation (not the schema)
595
+ const { writeFileSync } = require('node:fs');
596
+ const modifiedContent = `
597
+ import { e } from '@geekmidas/constructs/endpoints';
598
+ import { z } from 'zod';
599
+
600
+ export const getUsers = e
601
+ .get('/users')
602
+ .output(z.object({ message: z.string() }))
603
+ .handle(async () => {
604
+ // Different implementation - added a comment and console.log
605
+ console.log('Getting users');
606
+ return { message: 'Hello World' };
607
+ });
608
+ `;
609
+ writeFileSync(join(apiDir, 'src/endpoints/users.ts'), modifiedContent);
610
+
611
+ // Second call - should skip (schema unchanged, only implementation changed)
612
+ const results = await generateClientForFrontend(workspace, 'web');
613
+ expect(results[0]?.generated).toBe(false);
614
+ expect(results[0]?.reason).toBe('No schema changes detected');
615
+ });
616
+
617
+ it('should regenerate for multiple frontends when backend changes', async () => {
618
+ const apiDir = join(testDir, 'apps/api');
619
+ const webDir = join(testDir, 'apps/web');
620
+ const adminDir = join(testDir, 'apps/admin');
621
+ mkdirSync(webDir, { recursive: true });
622
+ mkdirSync(adminDir, { recursive: true });
623
+
624
+ createEndpointFile(
625
+ apiDir,
626
+ 'src/endpoints/users.ts',
627
+ 'getUsers',
628
+ '/users',
629
+ 'GET',
630
+ );
631
+
632
+ const workspace: NormalizedWorkspace = {
633
+ name: 'test',
634
+ root: testDir,
635
+ apps: {
636
+ api: {
637
+ type: 'backend',
638
+ path: 'apps/api',
639
+ port: 3000,
640
+ dependencies: [],
641
+ routes: './src/endpoints/**/*.ts',
642
+ resolvedDeployTarget: 'dokploy',
643
+ },
644
+ web: {
645
+ type: 'frontend',
646
+ path: 'apps/web',
647
+ port: 3001,
648
+ dependencies: ['api'],
649
+ client: { output: 'src/api' },
650
+ resolvedDeployTarget: 'dokploy',
651
+ },
652
+ admin: {
653
+ type: 'frontend',
654
+ path: 'apps/admin',
655
+ port: 3002,
656
+ dependencies: ['api'],
657
+ client: { output: 'src/client' },
658
+ resolvedDeployTarget: 'dokploy',
659
+ },
660
+ },
661
+ services: {},
662
+ deploy: { default: 'dokploy' },
663
+ shared: { packages: [] },
664
+ secrets: {},
665
+ };
666
+
667
+ // Generate for both frontends
668
+ await generateClientForFrontend(workspace, 'web', { force: true });
669
+ await generateClientForFrontend(workspace, 'admin', { force: true });
670
+
671
+ // Both should skip
672
+ const webSkip = await generateClientForFrontend(workspace, 'web');
673
+ const adminSkip = await generateClientForFrontend(workspace, 'admin');
674
+ expect(webSkip[0]?.generated).toBe(false);
675
+ expect(adminSkip[0]?.generated).toBe(false);
676
+
677
+ // Add new endpoint
678
+ createEndpointFile(
679
+ apiDir,
680
+ 'src/endpoints/products.ts',
681
+ 'getProducts',
682
+ '/products',
683
+ 'GET',
684
+ );
685
+
686
+ // Both should regenerate
687
+ const webRegen = await generateClientForFrontend(workspace, 'web');
688
+ const adminRegen = await generateClientForFrontend(workspace, 'admin');
689
+ expect(webRegen[0]?.generated).toBe(true);
690
+ expect(adminRegen[0]?.generated).toBe(true);
691
+ });
692
+
693
+ it('should only regenerate affected frontend when specific backend changes', async () => {
694
+ const apiDir = join(testDir, 'apps/api');
695
+ const authDir = join(testDir, 'apps/auth');
696
+ const webDir = join(testDir, 'apps/web');
697
+ const adminDir = join(testDir, 'apps/admin');
698
+ mkdirSync(webDir, { recursive: true });
699
+ mkdirSync(adminDir, { recursive: true });
700
+
701
+ createEndpointFile(
702
+ apiDir,
703
+ 'src/endpoints/users.ts',
704
+ 'getUsers',
705
+ '/users',
706
+ 'GET',
707
+ );
708
+ createEndpointFile(
709
+ authDir,
710
+ 'src/endpoints/login.ts',
711
+ 'login',
712
+ '/login',
713
+ 'POST',
714
+ );
715
+
716
+ const workspace: NormalizedWorkspace = {
717
+ name: 'test',
718
+ root: testDir,
719
+ apps: {
720
+ api: {
721
+ type: 'backend',
722
+ path: 'apps/api',
723
+ port: 3000,
724
+ dependencies: [],
725
+ routes: './src/endpoints/**/*.ts',
726
+ resolvedDeployTarget: 'dokploy',
727
+ },
728
+ auth: {
729
+ type: 'backend',
730
+ path: 'apps/auth',
731
+ port: 3001,
732
+ dependencies: [],
733
+ routes: './src/endpoints/**/*.ts',
734
+ resolvedDeployTarget: 'dokploy',
735
+ },
736
+ web: {
737
+ type: 'frontend',
738
+ path: 'apps/web',
739
+ port: 3002,
740
+ dependencies: ['api'], // Only depends on api
741
+ client: { output: 'src/api' },
742
+ resolvedDeployTarget: 'dokploy',
743
+ },
744
+ admin: {
745
+ type: 'frontend',
746
+ path: 'apps/admin',
747
+ port: 3003,
748
+ dependencies: ['auth'], // Only depends on auth
749
+ client: { output: 'src/client' },
750
+ resolvedDeployTarget: 'dokploy',
751
+ },
752
+ },
753
+ services: {},
754
+ deploy: { default: 'dokploy' },
755
+ shared: { packages: [] },
756
+ secrets: {},
757
+ };
758
+
759
+ // Initial generation
760
+ await generateClientForFrontend(workspace, 'web', { force: true });
761
+ await generateClientForFrontend(workspace, 'admin', { force: true });
762
+
763
+ // Change only the api backend
764
+ createEndpointFile(
765
+ apiDir,
766
+ 'src/endpoints/products.ts',
767
+ 'getProducts',
768
+ '/products',
769
+ 'GET',
770
+ );
771
+
772
+ // web should regenerate (depends on api)
773
+ const webResult = await generateClientForFrontend(workspace, 'web');
774
+ expect(webResult[0]?.generated).toBe(true);
775
+
776
+ // admin should skip (depends on auth, which didn't change)
777
+ const adminResult = await generateClientForFrontend(workspace, 'admin');
778
+ expect(adminResult[0]?.generated).toBe(false);
779
+ });
780
+ });
357
781
  });