@geekmidas/cli 1.2.1 → 1.2.3

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.
@@ -1,11 +1,18 @@
1
- import { existsSync, mkdirSync, rmSync } from 'node:fs';
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from 'node:fs';
2
8
  import { tmpdir } from 'node:os';
3
9
  import { join } from 'node:path';
4
10
  import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
11
  import {
6
- clearSpecHashCache,
7
- generateClientForFrontend,
12
+ copyAllClients,
13
+ copyClientToFrontends,
8
14
  getBackendDependencies,
15
+ getBackendOpenApiPath,
9
16
  getDependentFrontends,
10
17
  getFirstRoute,
11
18
  normalizeRoutes,
@@ -101,6 +108,7 @@ describe('Client Generator', () => {
101
108
  port: 3000,
102
109
  dependencies: [],
103
110
  routes: './src/**/*.ts',
111
+ resolvedDeployTarget: 'dokploy',
104
112
  },
105
113
  auth: {
106
114
  type: 'backend',
@@ -108,12 +116,14 @@ describe('Client Generator', () => {
108
116
  port: 3001,
109
117
  dependencies: [],
110
118
  routes: './src/**/*.ts',
119
+ resolvedDeployTarget: 'dokploy',
111
120
  },
112
121
  web: {
113
122
  type: 'frontend',
114
123
  path: 'apps/web',
115
124
  port: 3002,
116
125
  dependencies: ['api', 'auth'],
126
+ resolvedDeployTarget: 'dokploy',
117
127
  },
118
128
  },
119
129
  services: {},
@@ -137,12 +147,14 @@ describe('Client Generator', () => {
137
147
  port: 3000,
138
148
  dependencies: [],
139
149
  routes: './src/**/*.ts',
150
+ resolvedDeployTarget: 'dokploy',
140
151
  },
141
152
  worker: {
142
153
  type: 'backend',
143
154
  path: 'apps/worker',
144
155
  port: 3001,
145
156
  dependencies: [],
157
+ resolvedDeployTarget: 'dokploy',
146
158
  // No routes - not an HTTP backend
147
159
  },
148
160
  web: {
@@ -150,6 +162,7 @@ describe('Client Generator', () => {
150
162
  path: 'apps/web',
151
163
  port: 3002,
152
164
  dependencies: ['api', 'worker'],
165
+ resolvedDeployTarget: 'dokploy',
153
166
  },
154
167
  },
155
168
  services: {},
@@ -173,6 +186,7 @@ describe('Client Generator', () => {
173
186
  port: 3000,
174
187
  dependencies: [],
175
188
  routes: './src/**/*.ts',
189
+ resolvedDeployTarget: 'dokploy',
176
190
  },
177
191
  },
178
192
  services: {},
@@ -198,24 +212,28 @@ describe('Client Generator', () => {
198
212
  port: 3000,
199
213
  dependencies: [],
200
214
  routes: './src/**/*.ts',
215
+ resolvedDeployTarget: 'dokploy',
201
216
  },
202
217
  web: {
203
218
  type: 'frontend',
204
219
  path: 'apps/web',
205
220
  port: 3001,
206
221
  dependencies: ['api'],
222
+ resolvedDeployTarget: 'dokploy',
207
223
  },
208
224
  admin: {
209
225
  type: 'frontend',
210
226
  path: 'apps/admin',
211
227
  port: 3002,
212
228
  dependencies: ['api'],
229
+ resolvedDeployTarget: 'dokploy',
213
230
  },
214
231
  docs: {
215
232
  type: 'frontend',
216
233
  path: 'apps/docs',
217
234
  port: 3003,
218
235
  dependencies: [], // No API dependency
236
+ resolvedDeployTarget: 'dokploy',
219
237
  },
220
238
  },
221
239
  services: {},
@@ -239,6 +257,7 @@ describe('Client Generator', () => {
239
257
  port: 3000,
240
258
  dependencies: [],
241
259
  routes: './src/**/*.ts',
260
+ resolvedDeployTarget: 'dokploy',
242
261
  },
243
262
  },
244
263
  services: {},
@@ -252,7 +271,71 @@ describe('Client Generator', () => {
252
271
  });
253
272
  });
254
273
 
255
- describe('generateClientForFrontend', () => {
274
+ describe('getBackendOpenApiPath', () => {
275
+ it('should return correct path for backend app', () => {
276
+ const workspace: NormalizedWorkspace = {
277
+ name: 'test',
278
+ root: '/project',
279
+ apps: {
280
+ api: {
281
+ type: 'backend',
282
+ path: 'apps/api',
283
+ port: 3000,
284
+ dependencies: [],
285
+ routes: './src/**/*.ts',
286
+ resolvedDeployTarget: 'dokploy',
287
+ },
288
+ },
289
+ services: {},
290
+ deploy: { default: 'dokploy' },
291
+ shared: { packages: [] },
292
+ secrets: {},
293
+ };
294
+
295
+ const path = getBackendOpenApiPath(workspace, 'api');
296
+ expect(path).toBe('/project/apps/api/.gkm/openapi.ts');
297
+ });
298
+
299
+ it('should return null for non-backend app', () => {
300
+ const workspace: NormalizedWorkspace = {
301
+ name: 'test',
302
+ root: '/project',
303
+ apps: {
304
+ web: {
305
+ type: 'frontend',
306
+ path: 'apps/web',
307
+ port: 3000,
308
+ dependencies: [],
309
+ resolvedDeployTarget: 'dokploy',
310
+ },
311
+ },
312
+ services: {},
313
+ deploy: { default: 'dokploy' },
314
+ shared: { packages: [] },
315
+ secrets: {},
316
+ };
317
+
318
+ const path = getBackendOpenApiPath(workspace, 'web');
319
+ expect(path).toBeNull();
320
+ });
321
+
322
+ it('should return null for non-existent app', () => {
323
+ const workspace: NormalizedWorkspace = {
324
+ name: 'test',
325
+ root: '/project',
326
+ apps: {},
327
+ services: {},
328
+ deploy: { default: 'dokploy' },
329
+ shared: { packages: [] },
330
+ secrets: {},
331
+ };
332
+
333
+ const path = getBackendOpenApiPath(workspace, 'nonexistent');
334
+ expect(path).toBeNull();
335
+ });
336
+ });
337
+
338
+ describe('copyClientToFrontends', () => {
256
339
  let testDir: string;
257
340
 
258
341
  beforeEach(() => {
@@ -261,7 +344,6 @@ describe('Client Generator', () => {
261
344
  `gkm-client-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
262
345
  );
263
346
  mkdirSync(testDir, { recursive: true });
264
- clearSpecHashCache();
265
347
  });
266
348
 
267
349
  afterEach(() => {
@@ -270,7 +352,120 @@ describe('Client Generator', () => {
270
352
  }
271
353
  });
272
354
 
273
- it('should return empty array for frontend with no backend dependencies', async () => {
355
+ function createOpenApiFile(
356
+ appDir: string,
357
+ endpoints: Array<{ method: string; path: string }>,
358
+ ): void {
359
+ const endpointAuthEntries = endpoints
360
+ .map((ep) => ` '${ep.method.toUpperCase()} ${ep.path}': null,`)
361
+ .join('\n');
362
+
363
+ const content = `// Auto-generated by @geekmidas/cli - DO NOT EDIT
364
+
365
+ export const endpointAuth = {
366
+ ${endpointAuthEntries}
367
+ } as const;
368
+
369
+ export const paths = {};
370
+ export function createApi() { return {}; }
371
+ `;
372
+
373
+ const gkmDir = join(appDir, '.gkm');
374
+ mkdirSync(gkmDir, { recursive: true });
375
+ writeFileSync(join(gkmDir, 'openapi.ts'), content);
376
+ }
377
+
378
+ it('should copy client to dependent frontends', async () => {
379
+ const apiDir = join(testDir, 'apps/api');
380
+ const webDir = join(testDir, 'apps/web');
381
+ mkdirSync(webDir, { recursive: true });
382
+
383
+ createOpenApiFile(apiDir, [
384
+ { method: 'GET', path: '/users' },
385
+ { method: 'POST', path: '/users' },
386
+ ]);
387
+
388
+ const workspace: NormalizedWorkspace = {
389
+ name: 'test',
390
+ root: testDir,
391
+ apps: {
392
+ api: {
393
+ type: 'backend',
394
+ path: 'apps/api',
395
+ port: 3000,
396
+ dependencies: [],
397
+ routes: './src/endpoints/**/*.ts',
398
+ resolvedDeployTarget: 'dokploy',
399
+ },
400
+ web: {
401
+ type: 'frontend',
402
+ path: 'apps/web',
403
+ port: 3001,
404
+ dependencies: ['api'],
405
+ client: { output: 'src/api' },
406
+ resolvedDeployTarget: 'dokploy',
407
+ },
408
+ },
409
+ services: {},
410
+ deploy: { default: 'dokploy' },
411
+ shared: { packages: [] },
412
+ secrets: {},
413
+ };
414
+
415
+ const results = await copyClientToFrontends(workspace, 'api', {
416
+ silent: true,
417
+ });
418
+
419
+ expect(results).toHaveLength(1);
420
+ expect(results[0]?.success).toBe(true);
421
+ expect(results[0]?.frontendApp).toBe('web');
422
+ expect(results[0]?.backendApp).toBe('api');
423
+ expect(results[0]?.endpointCount).toBe(2);
424
+ expect(existsSync(join(webDir, 'src/api/api.ts'))).toBe(true);
425
+ });
426
+
427
+ it('should skip frontends without client.output configured', async () => {
428
+ const apiDir = join(testDir, 'apps/api');
429
+ const webDir = join(testDir, 'apps/web');
430
+ mkdirSync(webDir, { recursive: true });
431
+
432
+ createOpenApiFile(apiDir, [{ method: 'GET', path: '/users' }]);
433
+
434
+ const workspace: NormalizedWorkspace = {
435
+ name: 'test',
436
+ root: testDir,
437
+ apps: {
438
+ api: {
439
+ type: 'backend',
440
+ path: 'apps/api',
441
+ port: 3000,
442
+ dependencies: [],
443
+ routes: './src/endpoints/**/*.ts',
444
+ resolvedDeployTarget: 'dokploy',
445
+ },
446
+ web: {
447
+ type: 'frontend',
448
+ path: 'apps/web',
449
+ port: 3001,
450
+ dependencies: ['api'],
451
+ // No client.output configured
452
+ resolvedDeployTarget: 'dokploy',
453
+ },
454
+ },
455
+ services: {},
456
+ deploy: { default: 'dokploy' },
457
+ shared: { packages: [] },
458
+ secrets: {},
459
+ };
460
+
461
+ const results = await copyClientToFrontends(workspace, 'api', {
462
+ silent: true,
463
+ });
464
+
465
+ expect(results).toHaveLength(0);
466
+ });
467
+
468
+ it('should return empty results for non-backend app', async () => {
274
469
  const workspace: NormalizedWorkspace = {
275
470
  name: 'test',
276
471
  root: testDir,
@@ -280,6 +475,7 @@ describe('Client Generator', () => {
280
475
  path: 'apps/web',
281
476
  port: 3001,
282
477
  dependencies: [],
478
+ resolvedDeployTarget: 'dokploy',
283
479
  },
284
480
  },
285
481
  services: {},
@@ -288,11 +484,20 @@ describe('Client Generator', () => {
288
484
  secrets: {},
289
485
  };
290
486
 
291
- const results = await generateClientForFrontend(workspace, 'web');
487
+ const results = await copyClientToFrontends(workspace, 'web', {
488
+ silent: true,
489
+ });
490
+
292
491
  expect(results).toEqual([]);
293
492
  });
294
493
 
295
- it('should return empty array for non-frontend app', async () => {
494
+ it('should return empty results when openapi file does not exist', async () => {
495
+ const apiDir = join(testDir, 'apps/api');
496
+ const webDir = join(testDir, 'apps/web');
497
+ mkdirSync(apiDir, { recursive: true });
498
+ mkdirSync(webDir, { recursive: true });
499
+ // Don't create openapi file
500
+
296
501
  const workspace: NormalizedWorkspace = {
297
502
  name: 'test',
298
503
  root: testDir,
@@ -302,7 +507,16 @@ describe('Client Generator', () => {
302
507
  path: 'apps/api',
303
508
  port: 3000,
304
509
  dependencies: [],
305
- routes: './src/**/*.ts',
510
+ routes: './src/endpoints/**/*.ts',
511
+ resolvedDeployTarget: 'dokploy',
512
+ },
513
+ web: {
514
+ type: 'frontend',
515
+ path: 'apps/web',
516
+ port: 3001,
517
+ dependencies: ['api'],
518
+ client: { output: 'src/api' },
519
+ resolvedDeployTarget: 'dokploy',
306
520
  },
307
521
  },
308
522
  services: {},
@@ -311,16 +525,21 @@ describe('Client Generator', () => {
311
525
  secrets: {},
312
526
  };
313
527
 
314
- const results = await generateClientForFrontend(workspace, 'api');
528
+ const results = await copyClientToFrontends(workspace, 'api', {
529
+ silent: true,
530
+ });
531
+
315
532
  expect(results).toEqual([]);
316
533
  });
317
534
 
318
- it('should use configured client output path', async () => {
319
- // Create minimal workspace structure
535
+ it('should copy to multiple frontends', async () => {
320
536
  const apiDir = join(testDir, 'apps/api');
321
537
  const webDir = join(testDir, 'apps/web');
322
- mkdirSync(join(apiDir, 'src/endpoints'), { recursive: true });
538
+ const adminDir = join(testDir, 'apps/admin');
323
539
  mkdirSync(webDir, { recursive: true });
540
+ mkdirSync(adminDir, { recursive: true });
541
+
542
+ createOpenApiFile(apiDir, [{ method: 'GET', path: '/users' }]);
324
543
 
325
544
  const workspace: NormalizedWorkspace = {
326
545
  name: 'test',
@@ -332,13 +551,23 @@ describe('Client Generator', () => {
332
551
  port: 3000,
333
552
  dependencies: [],
334
553
  routes: './src/endpoints/**/*.ts',
554
+ resolvedDeployTarget: 'dokploy',
335
555
  },
336
556
  web: {
337
557
  type: 'frontend',
338
558
  path: 'apps/web',
339
559
  port: 3001,
340
560
  dependencies: ['api'],
341
- client: { output: 'lib/api' },
561
+ client: { output: 'src/api' },
562
+ resolvedDeployTarget: 'dokploy',
563
+ },
564
+ admin: {
565
+ type: 'frontend',
566
+ path: 'apps/admin',
567
+ port: 3002,
568
+ dependencies: ['api'],
569
+ client: { output: 'lib/client' },
570
+ resolvedDeployTarget: 'dokploy',
342
571
  },
343
572
  },
344
573
  services: {},
@@ -347,11 +576,235 @@ describe('Client Generator', () => {
347
576
  secrets: {},
348
577
  };
349
578
 
350
- // This will fail to generate because there are no endpoints,
351
- // but we can verify the output path would be correct
352
- const results = await generateClientForFrontend(workspace, 'web');
353
- expect(results).toHaveLength(1);
354
- expect(results[0]?.reason).toBe('No endpoints found in backend');
579
+ const results = await copyClientToFrontends(workspace, 'api', {
580
+ silent: true,
581
+ });
582
+
583
+ expect(results).toHaveLength(2);
584
+ expect(results.every((r) => r.success)).toBe(true);
585
+ expect(existsSync(join(webDir, 'src/api/api.ts'))).toBe(true);
586
+ expect(existsSync(join(adminDir, 'lib/client/api.ts'))).toBe(true);
587
+ });
588
+
589
+ it('should add header comment to copied file', async () => {
590
+ const apiDir = join(testDir, 'apps/api');
591
+ const webDir = join(testDir, 'apps/web');
592
+ mkdirSync(webDir, { recursive: true });
593
+
594
+ createOpenApiFile(apiDir, [{ method: 'GET', path: '/users' }]);
595
+
596
+ const workspace: NormalizedWorkspace = {
597
+ name: 'test',
598
+ root: testDir,
599
+ apps: {
600
+ api: {
601
+ type: 'backend',
602
+ path: 'apps/api',
603
+ port: 3000,
604
+ dependencies: [],
605
+ routes: './src/endpoints/**/*.ts',
606
+ resolvedDeployTarget: 'dokploy',
607
+ },
608
+ web: {
609
+ type: 'frontend',
610
+ path: 'apps/web',
611
+ port: 3001,
612
+ dependencies: ['api'],
613
+ client: { output: 'src/api' },
614
+ resolvedDeployTarget: 'dokploy',
615
+ },
616
+ },
617
+ services: {},
618
+ deploy: { default: 'dokploy' },
619
+ shared: { packages: [] },
620
+ secrets: {},
621
+ };
622
+
623
+ await copyClientToFrontends(workspace, 'api', { silent: true });
624
+
625
+ const content = readFileSync(join(webDir, 'src/api/api.ts'), 'utf-8');
626
+ expect(content).toContain('Auto-generated API client for api');
627
+ expect(content).toContain('DO NOT EDIT');
628
+ });
629
+
630
+ it('should use backend name in filename when frontend has multiple backends', async () => {
631
+ const apiDir = join(testDir, 'apps/api');
632
+ const authDir = join(testDir, 'apps/auth');
633
+ const webDir = join(testDir, 'apps/web');
634
+ mkdirSync(webDir, { recursive: true });
635
+
636
+ createOpenApiFile(apiDir, [{ method: 'GET', path: '/users' }]);
637
+ createOpenApiFile(authDir, [{ method: 'POST', path: '/login' }]);
638
+
639
+ const workspace: NormalizedWorkspace = {
640
+ name: 'test',
641
+ root: testDir,
642
+ apps: {
643
+ api: {
644
+ type: 'backend',
645
+ path: 'apps/api',
646
+ port: 3000,
647
+ dependencies: [],
648
+ routes: './src/endpoints/**/*.ts',
649
+ resolvedDeployTarget: 'dokploy',
650
+ },
651
+ auth: {
652
+ type: 'backend',
653
+ path: 'apps/auth',
654
+ port: 3001,
655
+ dependencies: [],
656
+ routes: './src/endpoints/**/*.ts',
657
+ resolvedDeployTarget: 'dokploy',
658
+ },
659
+ web: {
660
+ type: 'frontend',
661
+ path: 'apps/web',
662
+ port: 3002,
663
+ dependencies: ['api', 'auth'], // Multiple backends
664
+ client: { output: 'src/api' },
665
+ resolvedDeployTarget: 'dokploy',
666
+ },
667
+ },
668
+ services: {},
669
+ deploy: { default: 'dokploy' },
670
+ shared: { packages: [] },
671
+ secrets: {},
672
+ };
673
+
674
+ await copyClientToFrontends(workspace, 'api', { silent: true });
675
+ await copyClientToFrontends(workspace, 'auth', { silent: true });
676
+
677
+ // Should use {backend}.ts naming
678
+ expect(existsSync(join(webDir, 'src/api/api.ts'))).toBe(true);
679
+ expect(existsSync(join(webDir, 'src/api/auth.ts'))).toBe(true);
680
+ });
681
+ });
682
+
683
+ describe('copyAllClients', () => {
684
+ let testDir: string;
685
+
686
+ beforeEach(() => {
687
+ testDir = join(
688
+ tmpdir(),
689
+ `gkm-client-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
690
+ );
691
+ mkdirSync(testDir, { recursive: true });
692
+ });
693
+
694
+ afterEach(() => {
695
+ if (existsSync(testDir)) {
696
+ rmSync(testDir, { recursive: true, force: true });
697
+ }
698
+ });
699
+
700
+ function createOpenApiFile(
701
+ appDir: string,
702
+ endpoints: Array<{ method: string; path: string }>,
703
+ ): void {
704
+ const endpointAuthEntries = endpoints
705
+ .map((ep) => ` '${ep.method.toUpperCase()} ${ep.path}': null,`)
706
+ .join('\n');
707
+
708
+ const content = `// Auto-generated by @geekmidas/cli - DO NOT EDIT
709
+
710
+ export const endpointAuth = {
711
+ ${endpointAuthEntries}
712
+ } as const;
713
+
714
+ export const paths = {};
715
+ export function createApi() { return {}; }
716
+ `;
717
+
718
+ const gkmDir = join(appDir, '.gkm');
719
+ mkdirSync(gkmDir, { recursive: true });
720
+ writeFileSync(join(gkmDir, 'openapi.ts'), content);
721
+ }
722
+
723
+ it('should copy from all backends to their dependent frontends', async () => {
724
+ const apiDir = join(testDir, 'apps/api');
725
+ const authDir = join(testDir, 'apps/auth');
726
+ const webDir = join(testDir, 'apps/web');
727
+ const adminDir = join(testDir, 'apps/admin');
728
+ mkdirSync(webDir, { recursive: true });
729
+ mkdirSync(adminDir, { recursive: true });
730
+
731
+ createOpenApiFile(apiDir, [{ method: 'GET', path: '/users' }]);
732
+ createOpenApiFile(authDir, [{ method: 'POST', path: '/login' }]);
733
+
734
+ const workspace: NormalizedWorkspace = {
735
+ name: 'test',
736
+ root: testDir,
737
+ apps: {
738
+ api: {
739
+ type: 'backend',
740
+ path: 'apps/api',
741
+ port: 3000,
742
+ dependencies: [],
743
+ routes: './src/endpoints/**/*.ts',
744
+ resolvedDeployTarget: 'dokploy',
745
+ },
746
+ auth: {
747
+ type: 'backend',
748
+ path: 'apps/auth',
749
+ port: 3001,
750
+ dependencies: [],
751
+ routes: './src/endpoints/**/*.ts',
752
+ resolvedDeployTarget: 'dokploy',
753
+ },
754
+ web: {
755
+ type: 'frontend',
756
+ path: 'apps/web',
757
+ port: 3002,
758
+ dependencies: ['api'],
759
+ client: { output: 'src/api' },
760
+ resolvedDeployTarget: 'dokploy',
761
+ },
762
+ admin: {
763
+ type: 'frontend',
764
+ path: 'apps/admin',
765
+ port: 3003,
766
+ dependencies: ['auth'],
767
+ client: { output: 'lib/client' },
768
+ resolvedDeployTarget: 'dokploy',
769
+ },
770
+ },
771
+ services: {},
772
+ deploy: { default: 'dokploy' },
773
+ shared: { packages: [] },
774
+ secrets: {},
775
+ };
776
+
777
+ const results = await copyAllClients(workspace, { silent: true });
778
+
779
+ expect(results).toHaveLength(2);
780
+ expect(results.every((r) => r.success)).toBe(true);
781
+ expect(existsSync(join(webDir, 'src/api/api.ts'))).toBe(true);
782
+ expect(existsSync(join(adminDir, 'lib/client/auth.ts'))).toBe(true);
783
+ });
784
+
785
+ it('should return empty array when no backends have routes', async () => {
786
+ const workspace: NormalizedWorkspace = {
787
+ name: 'test',
788
+ root: testDir,
789
+ apps: {
790
+ worker: {
791
+ type: 'backend',
792
+ path: 'apps/worker',
793
+ port: 3000,
794
+ dependencies: [],
795
+ // No routes
796
+ resolvedDeployTarget: 'dokploy',
797
+ },
798
+ },
799
+ services: {},
800
+ deploy: { default: 'dokploy' },
801
+ shared: { packages: [] },
802
+ secrets: {},
803
+ };
804
+
805
+ const results = await copyAllClients(workspace, { silent: true });
806
+
807
+ expect(results).toEqual([]);
355
808
  });
356
809
  });
357
810
  });