@geekmidas/cli 1.2.2 → 1.3.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/CHANGELOG.md +12 -0
- package/dist/{CachedStateProvider-DVyKfaMm.mjs → CachedStateProvider-BDq5WqSy.mjs} +1 -1
- package/dist/{CachedStateProvider-DVyKfaMm.mjs.map → CachedStateProvider-BDq5WqSy.mjs.map} +1 -1
- package/dist/CachedStateProvider-CI61keQ1.mjs +3 -0
- package/dist/{HostingerProvider-DqUq6e9i.mjs → HostingerProvider-B9N-TKbp.mjs} +2 -2
- package/dist/{HostingerProvider-DqUq6e9i.mjs.map → HostingerProvider-B9N-TKbp.mjs.map} +1 -1
- package/dist/{LocalStateProvider-DxoSaWUV.mjs → LocalStateProvider-BDm7ZqJo.mjs} +1 -1
- package/dist/{LocalStateProvider-DxoSaWUV.mjs.map → LocalStateProvider-BDm7ZqJo.mjs.map} +1 -1
- package/dist/{Route53Provider-KUAX3vz9.mjs → Route53Provider-DOWmFnwN.mjs} +2 -2
- package/dist/{Route53Provider-KUAX3vz9.mjs.map → Route53Provider-DOWmFnwN.mjs.map} +1 -1
- package/dist/{Route53Provider-CpRIqu69.cjs → Route53Provider-xrWuBXih.cjs} +2 -2
- package/dist/{Route53Provider-CpRIqu69.cjs.map → Route53Provider-xrWuBXih.cjs.map} +1 -1
- package/dist/{SSMStateProvider-D79o_JjM.cjs → SSMStateProvider-DGrqYll0.cjs} +8 -4
- package/dist/SSMStateProvider-DGrqYll0.cjs.map +1 -0
- package/dist/{SSMStateProvider-BjCi_58g.mjs → SSMStateProvider-DT0WV-E_.mjs} +9 -4
- package/dist/SSMStateProvider-DT0WV-E_.mjs.map +1 -0
- package/dist/{bundler-BqTN5Dj5.mjs → bundler-DgXsOSxc.mjs} +3 -3
- package/dist/{bundler-BqTN5Dj5.mjs.map → bundler-DgXsOSxc.mjs.map} +1 -1
- package/dist/chunk-Duj1WY3L.mjs +7 -0
- package/dist/{config-BQ4a36Rq.mjs → config-C1bidhvG.mjs} +2 -2
- package/dist/{config-BQ4a36Rq.mjs.map → config-C1bidhvG.mjs.map} +1 -1
- package/dist/{config-Bayob8pB.cjs → config-C1dM7aZb.cjs} +2 -2
- package/dist/{config-Bayob8pB.cjs.map → config-C1dM7aZb.cjs.map} +1 -1
- package/dist/config.cjs +2 -2
- package/dist/config.d.cts +1 -1
- package/dist/config.d.mts +2 -2
- package/dist/config.mjs +2 -2
- package/dist/{credentials-DT1dSxIx.mjs → credentials-s1kLcIzK.mjs} +1 -1
- package/dist/{credentials-DT1dSxIx.mjs.map → credentials-s1kLcIzK.mjs.map} +1 -1
- package/dist/deploy/sniffer-routes-worker.cjs +65 -0
- package/dist/deploy/sniffer-routes-worker.cjs.map +1 -0
- package/dist/deploy/sniffer-routes-worker.d.cts +1 -0
- package/dist/deploy/sniffer-routes-worker.d.mts +1 -0
- package/dist/deploy/sniffer-routes-worker.mjs +64 -0
- package/dist/deploy/sniffer-routes-worker.mjs.map +1 -0
- package/dist/dokploy-api-DSJYNx88.mjs +3 -0
- package/dist/{dokploy-api-7k3t7_zd.mjs → dokploy-api-z0833e7r.mjs} +1 -1
- package/dist/{dokploy-api-7k3t7_zd.mjs.map → dokploy-api-z0833e7r.mjs.map} +1 -1
- package/dist/{encryption-JtMsiGNp.mjs → encryption-BOH5M-f-.mjs} +1 -1
- package/dist/{encryption-JtMsiGNp.mjs.map → encryption-BOH5M-f-.mjs.map} +1 -1
- package/dist/encryption-a9TNMWav.mjs +3 -0
- package/dist/{index-Bi9vGQJy.d.mts → index-DvpWzLD7.d.mts} +5 -2
- package/dist/index-DvpWzLD7.d.mts.map +1 -0
- package/dist/{index-CufAAnge.d.cts → index-DzmZ6SUW.d.cts} +4 -1
- package/dist/index-DzmZ6SUW.d.cts.map +1 -0
- package/dist/index.cjs +100 -170
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +116 -185
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-BZ4Qik9w.mjs → openapi-9k6a6VA4.mjs} +3 -3
- package/dist/{openapi-BZ4Qik9w.mjs.map → openapi-9k6a6VA4.mjs.map} +1 -1
- package/dist/{openapi-CzfnHlhG.cjs → openapi-Dcja4e1C.cjs} +2 -8
- package/dist/{openapi-CzfnHlhG.cjs.map → openapi-Dcja4e1C.cjs.map} +1 -1
- package/dist/{openapi-react-query-DGEkD39r.mjs → openapi-react-query-DaTMSPD5.mjs} +1 -1
- package/dist/{openapi-react-query-DGEkD39r.mjs.map → openapi-react-query-DaTMSPD5.mjs.map} +1 -1
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/openapi.cjs +3 -3
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +3 -3
- package/dist/{storage-BMW6yLu3.mjs → storage-DmCbr6DI.mjs} +1 -1
- package/dist/{storage-BMW6yLu3.mjs.map → storage-DmCbr6DI.mjs.map} +1 -1
- package/dist/{storage-D8XzjVaO.mjs → storage-Dx_jZbq6.mjs} +1 -1
- package/dist/{types-BldpmqQX.d.mts → types-B9UZ7fOG.d.mts} +1 -1
- package/dist/{types-BldpmqQX.d.mts.map → types-B9UZ7fOG.d.mts.map} +1 -1
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +1 -1
- package/dist/workspace/index.d.mts +2 -2
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-CASoZOjs.mjs → workspace-Cb_I7oCJ.mjs} +5 -8
- package/dist/{workspace-CASoZOjs.mjs.map → workspace-Cb_I7oCJ.mjs.map} +1 -1
- package/dist/{workspace-BMJE18LV.cjs → workspace-CeFgIDC-.cjs} +3 -2
- package/dist/{workspace-BMJE18LV.cjs.map → workspace-CeFgIDC-.cjs.map} +1 -1
- package/package.json +2 -2
- package/src/deploy/SSMStateProvider.ts +14 -3
- package/src/deploy/StateProvider.ts +5 -1
- package/src/deploy/__tests__/SSMStateProvider.spec.ts +12 -0
- package/src/deploy/__tests__/createStateProvider.spec.ts +10 -0
- package/src/dev/index.ts +69 -106
- package/src/init/generators/web.ts +6 -2
- package/src/workspace/__tests__/client-generator.spec.ts +330 -301
- package/src/workspace/client-generator.ts +139 -199
- package/src/workspace/schema.ts +2 -0
- package/tsdown.config.ts +1 -0
- package/dist/CachedStateProvider-OiFUGr7p.mjs +0 -3
- package/dist/SSMStateProvider-BjCi_58g.mjs.map +0 -1
- package/dist/SSMStateProvider-D79o_JjM.cjs.map +0 -1
- package/dist/dokploy-api-CHa8G51l.mjs +0 -3
- package/dist/encryption-UUmaWAmz.mjs +0 -3
- package/dist/index-Bi9vGQJy.d.mts.map +0 -1
- package/dist/index-CufAAnge.d.cts.map +0 -1
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
7
|
-
|
|
12
|
+
copyAllClients,
|
|
13
|
+
copyClientToFrontends,
|
|
8
14
|
getBackendDependencies,
|
|
15
|
+
getBackendOpenApiPath,
|
|
9
16
|
getDependentFrontends,
|
|
10
17
|
getFirstRoute,
|
|
11
18
|
normalizeRoutes,
|
|
@@ -264,7 +271,71 @@ describe('Client Generator', () => {
|
|
|
264
271
|
});
|
|
265
272
|
});
|
|
266
273
|
|
|
267
|
-
describe('
|
|
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', () => {
|
|
268
339
|
let testDir: string;
|
|
269
340
|
|
|
270
341
|
beforeEach(() => {
|
|
@@ -273,7 +344,6 @@ describe('Client Generator', () => {
|
|
|
273
344
|
`gkm-client-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
274
345
|
);
|
|
275
346
|
mkdirSync(testDir, { recursive: true });
|
|
276
|
-
clearSpecHashCache();
|
|
277
347
|
});
|
|
278
348
|
|
|
279
349
|
afterEach(() => {
|
|
@@ -282,16 +352,57 @@ describe('Client Generator', () => {
|
|
|
282
352
|
}
|
|
283
353
|
});
|
|
284
354
|
|
|
285
|
-
|
|
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
|
+
|
|
286
388
|
const workspace: NormalizedWorkspace = {
|
|
287
389
|
name: 'test',
|
|
288
390
|
root: testDir,
|
|
289
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
|
+
},
|
|
290
400
|
web: {
|
|
291
401
|
type: 'frontend',
|
|
292
402
|
path: 'apps/web',
|
|
293
403
|
port: 3001,
|
|
294
|
-
dependencies: [],
|
|
404
|
+
dependencies: ['api'],
|
|
405
|
+
client: { output: 'src/api' },
|
|
295
406
|
resolvedDeployTarget: 'dokploy',
|
|
296
407
|
},
|
|
297
408
|
},
|
|
@@ -301,11 +412,25 @@ describe('Client Generator', () => {
|
|
|
301
412
|
secrets: {},
|
|
302
413
|
};
|
|
303
414
|
|
|
304
|
-
const results = await
|
|
305
|
-
|
|
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);
|
|
306
425
|
});
|
|
307
426
|
|
|
308
|
-
it('should
|
|
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
|
+
|
|
309
434
|
const workspace: NormalizedWorkspace = {
|
|
310
435
|
name: 'test',
|
|
311
436
|
root: testDir,
|
|
@@ -315,7 +440,15 @@ describe('Client Generator', () => {
|
|
|
315
440
|
path: 'apps/api',
|
|
316
441
|
port: 3000,
|
|
317
442
|
dependencies: [],
|
|
318
|
-
routes: './src/**/*.ts',
|
|
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
|
|
319
452
|
resolvedDeployTarget: 'dokploy',
|
|
320
453
|
},
|
|
321
454
|
},
|
|
@@ -325,16 +458,45 @@ describe('Client Generator', () => {
|
|
|
325
458
|
secrets: {},
|
|
326
459
|
};
|
|
327
460
|
|
|
328
|
-
const results = await
|
|
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 () => {
|
|
469
|
+
const workspace: NormalizedWorkspace = {
|
|
470
|
+
name: 'test',
|
|
471
|
+
root: testDir,
|
|
472
|
+
apps: {
|
|
473
|
+
web: {
|
|
474
|
+
type: 'frontend',
|
|
475
|
+
path: 'apps/web',
|
|
476
|
+
port: 3001,
|
|
477
|
+
dependencies: [],
|
|
478
|
+
resolvedDeployTarget: 'dokploy',
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
services: {},
|
|
482
|
+
deploy: { default: 'dokploy' },
|
|
483
|
+
shared: { packages: [] },
|
|
484
|
+
secrets: {},
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
const results = await copyClientToFrontends(workspace, 'web', {
|
|
488
|
+
silent: true,
|
|
489
|
+
});
|
|
490
|
+
|
|
329
491
|
expect(results).toEqual([]);
|
|
330
492
|
});
|
|
331
493
|
|
|
332
|
-
it('should
|
|
333
|
-
// Create minimal workspace structure
|
|
494
|
+
it('should return empty results when openapi file does not exist', async () => {
|
|
334
495
|
const apiDir = join(testDir, 'apps/api');
|
|
335
496
|
const webDir = join(testDir, 'apps/web');
|
|
336
|
-
mkdirSync(
|
|
497
|
+
mkdirSync(apiDir, { recursive: true });
|
|
337
498
|
mkdirSync(webDir, { recursive: true });
|
|
499
|
+
// Don't create openapi file
|
|
338
500
|
|
|
339
501
|
const workspace: NormalizedWorkspace = {
|
|
340
502
|
name: 'test',
|
|
@@ -353,7 +515,7 @@ describe('Client Generator', () => {
|
|
|
353
515
|
path: 'apps/web',
|
|
354
516
|
port: 3001,
|
|
355
517
|
dependencies: ['api'],
|
|
356
|
-
client: { output: '
|
|
518
|
+
client: { output: 'src/api' },
|
|
357
519
|
resolvedDeployTarget: 'dokploy',
|
|
358
520
|
},
|
|
359
521
|
},
|
|
@@ -363,58 +525,25 @@ describe('Client Generator', () => {
|
|
|
363
525
|
secrets: {},
|
|
364
526
|
};
|
|
365
527
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
expect(results).toHaveLength(1);
|
|
370
|
-
expect(results[0]?.reason).toBe('No endpoints found in backend');
|
|
371
|
-
});
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
describe('hot reload - hash-based change detection', () => {
|
|
375
|
-
let testDir: string;
|
|
528
|
+
const results = await copyClientToFrontends(workspace, 'api', {
|
|
529
|
+
silent: true,
|
|
530
|
+
});
|
|
376
531
|
|
|
377
|
-
|
|
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();
|
|
532
|
+
expect(results).toEqual([]);
|
|
384
533
|
});
|
|
385
534
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
535
|
+
it('should copy to multiple frontends', async () => {
|
|
536
|
+
const apiDir = join(testDir, 'apps/api');
|
|
537
|
+
const webDir = join(testDir, 'apps/web');
|
|
538
|
+
const adminDir = join(testDir, 'apps/admin');
|
|
539
|
+
mkdirSync(webDir, { recursive: true });
|
|
540
|
+
mkdirSync(adminDir, { recursive: true });
|
|
391
541
|
|
|
392
|
-
|
|
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
|
-
}
|
|
542
|
+
createOpenApiFile(apiDir, [{ method: 'GET', path: '/users' }]);
|
|
413
543
|
|
|
414
|
-
|
|
415
|
-
return {
|
|
544
|
+
const workspace: NormalizedWorkspace = {
|
|
416
545
|
name: 'test',
|
|
417
|
-
root,
|
|
546
|
+
root: testDir,
|
|
418
547
|
apps: {
|
|
419
548
|
api: {
|
|
420
549
|
type: 'backend',
|
|
@@ -432,202 +561,80 @@ export const ${exportName} = e
|
|
|
432
561
|
client: { output: 'src/api' },
|
|
433
562
|
resolvedDeployTarget: 'dokploy',
|
|
434
563
|
},
|
|
564
|
+
admin: {
|
|
565
|
+
type: 'frontend',
|
|
566
|
+
path: 'apps/admin',
|
|
567
|
+
port: 3002,
|
|
568
|
+
dependencies: ['api'],
|
|
569
|
+
client: { output: 'lib/client' },
|
|
570
|
+
resolvedDeployTarget: 'dokploy',
|
|
571
|
+
},
|
|
435
572
|
},
|
|
436
573
|
services: {},
|
|
437
574
|
deploy: { default: 'dokploy' },
|
|
438
575
|
shared: { packages: [] },
|
|
439
576
|
secrets: {},
|
|
440
577
|
};
|
|
441
|
-
}
|
|
442
578
|
|
|
443
|
-
|
|
444
|
-
|
|
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,
|
|
579
|
+
const results = await copyClientToFrontends(workspace, 'api', {
|
|
580
|
+
silent: true,
|
|
459
581
|
});
|
|
460
582
|
|
|
461
|
-
expect(results).toHaveLength(
|
|
462
|
-
expect(results
|
|
463
|
-
expect(
|
|
464
|
-
expect(existsSync(join(
|
|
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);
|
|
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);
|
|
532
587
|
});
|
|
533
588
|
|
|
534
|
-
it('should
|
|
589
|
+
it('should add header comment to copied file', async () => {
|
|
535
590
|
const apiDir = join(testDir, 'apps/api');
|
|
536
591
|
const webDir = join(testDir, 'apps/web');
|
|
537
592
|
mkdirSync(webDir, { recursive: true });
|
|
538
593
|
|
|
539
|
-
|
|
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
|
-
);
|
|
594
|
+
createOpenApiFile(apiDir, [{ method: 'GET', path: '/users' }]);
|
|
566
595
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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
|
+
};
|
|
588
622
|
|
|
589
|
-
|
|
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);
|
|
623
|
+
await copyClientToFrontends(workspace, 'api', { silent: true });
|
|
610
624
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
expect(
|
|
614
|
-
expect(results[0]?.reason).toBe('No schema changes detected');
|
|
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');
|
|
615
628
|
});
|
|
616
629
|
|
|
617
|
-
it('should
|
|
630
|
+
it('should use backend name in filename when frontend has multiple backends', async () => {
|
|
618
631
|
const apiDir = join(testDir, 'apps/api');
|
|
632
|
+
const authDir = join(testDir, 'apps/auth');
|
|
619
633
|
const webDir = join(testDir, 'apps/web');
|
|
620
|
-
const adminDir = join(testDir, 'apps/admin');
|
|
621
634
|
mkdirSync(webDir, { recursive: true });
|
|
622
|
-
mkdirSync(adminDir, { recursive: true });
|
|
623
635
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
'src/endpoints/users.ts',
|
|
627
|
-
'getUsers',
|
|
628
|
-
'/users',
|
|
629
|
-
'GET',
|
|
630
|
-
);
|
|
636
|
+
createOpenApiFile(apiDir, [{ method: 'GET', path: '/users' }]);
|
|
637
|
+
createOpenApiFile(authDir, [{ method: 'POST', path: '/login' }]);
|
|
631
638
|
|
|
632
639
|
const workspace: NormalizedWorkspace = {
|
|
633
640
|
name: 'test',
|
|
@@ -641,20 +648,20 @@ export const getUsers = e
|
|
|
641
648
|
routes: './src/endpoints/**/*.ts',
|
|
642
649
|
resolvedDeployTarget: 'dokploy',
|
|
643
650
|
},
|
|
644
|
-
|
|
645
|
-
type: '
|
|
646
|
-
path: 'apps/
|
|
651
|
+
auth: {
|
|
652
|
+
type: 'backend',
|
|
653
|
+
path: 'apps/auth',
|
|
647
654
|
port: 3001,
|
|
648
|
-
dependencies: [
|
|
649
|
-
|
|
655
|
+
dependencies: [],
|
|
656
|
+
routes: './src/endpoints/**/*.ts',
|
|
650
657
|
resolvedDeployTarget: 'dokploy',
|
|
651
658
|
},
|
|
652
|
-
|
|
659
|
+
web: {
|
|
653
660
|
type: 'frontend',
|
|
654
|
-
path: 'apps/
|
|
661
|
+
path: 'apps/web',
|
|
655
662
|
port: 3002,
|
|
656
|
-
dependencies: ['api'],
|
|
657
|
-
client: { output: 'src/
|
|
663
|
+
dependencies: ['api', 'auth'], // Multiple backends
|
|
664
|
+
client: { output: 'src/api' },
|
|
658
665
|
resolvedDeployTarget: 'dokploy',
|
|
659
666
|
},
|
|
660
667
|
},
|
|
@@ -664,33 +671,56 @@ export const getUsers = e
|
|
|
664
671
|
secrets: {},
|
|
665
672
|
};
|
|
666
673
|
|
|
667
|
-
|
|
668
|
-
await
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
'GET',
|
|
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)}`,
|
|
684
690
|
);
|
|
691
|
+
mkdirSync(testDir, { recursive: true });
|
|
692
|
+
});
|
|
685
693
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
expect(adminRegen[0]?.generated).toBe(true);
|
|
694
|
+
afterEach(() => {
|
|
695
|
+
if (existsSync(testDir)) {
|
|
696
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
697
|
+
}
|
|
691
698
|
});
|
|
692
699
|
|
|
693
|
-
|
|
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 () => {
|
|
694
724
|
const apiDir = join(testDir, 'apps/api');
|
|
695
725
|
const authDir = join(testDir, 'apps/auth');
|
|
696
726
|
const webDir = join(testDir, 'apps/web');
|
|
@@ -698,20 +728,8 @@ export const getUsers = e
|
|
|
698
728
|
mkdirSync(webDir, { recursive: true });
|
|
699
729
|
mkdirSync(adminDir, { recursive: true });
|
|
700
730
|
|
|
701
|
-
|
|
702
|
-
|
|
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
|
-
);
|
|
731
|
+
createOpenApiFile(apiDir, [{ method: 'GET', path: '/users' }]);
|
|
732
|
+
createOpenApiFile(authDir, [{ method: 'POST', path: '/login' }]);
|
|
715
733
|
|
|
716
734
|
const workspace: NormalizedWorkspace = {
|
|
717
735
|
name: 'test',
|
|
@@ -737,7 +755,7 @@ export const getUsers = e
|
|
|
737
755
|
type: 'frontend',
|
|
738
756
|
path: 'apps/web',
|
|
739
757
|
port: 3002,
|
|
740
|
-
dependencies: ['api'],
|
|
758
|
+
dependencies: ['api'],
|
|
741
759
|
client: { output: 'src/api' },
|
|
742
760
|
resolvedDeployTarget: 'dokploy',
|
|
743
761
|
},
|
|
@@ -745,8 +763,8 @@ export const getUsers = e
|
|
|
745
763
|
type: 'frontend',
|
|
746
764
|
path: 'apps/admin',
|
|
747
765
|
port: 3003,
|
|
748
|
-
dependencies: ['auth'],
|
|
749
|
-
client: { output: '
|
|
766
|
+
dependencies: ['auth'],
|
|
767
|
+
client: { output: 'lib/client' },
|
|
750
768
|
resolvedDeployTarget: 'dokploy',
|
|
751
769
|
},
|
|
752
770
|
},
|
|
@@ -756,26 +774,37 @@ export const getUsers = e
|
|
|
756
774
|
secrets: {},
|
|
757
775
|
};
|
|
758
776
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
'
|
|
770
|
-
|
|
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
|
+
};
|
|
771
804
|
|
|
772
|
-
|
|
773
|
-
const webResult = await generateClientForFrontend(workspace, 'web');
|
|
774
|
-
expect(webResult[0]?.generated).toBe(true);
|
|
805
|
+
const results = await copyAllClients(workspace, { silent: true });
|
|
775
806
|
|
|
776
|
-
|
|
777
|
-
const adminResult = await generateClientForFrontend(workspace, 'admin');
|
|
778
|
-
expect(adminResult[0]?.generated).toBe(false);
|
|
807
|
+
expect(results).toEqual([]);
|
|
779
808
|
});
|
|
780
809
|
});
|
|
781
810
|
});
|