@geekmidas/cli 1.10.29 → 1.10.31
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 +18 -0
- package/dist/{CachedStateProvider-37pG1HBx.cjs → CachedStateProvider-3couMAQk.cjs} +1 -1
- package/dist/CachedStateProvider-C4QqvlPE.mjs +3 -0
- package/dist/{CachedStateProvider-C8A-cCm5.mjs → CachedStateProvider-CQr31ups.mjs} +1 -1
- package/dist/{CachedStateProvider-C8A-cCm5.mjs.map → CachedStateProvider-CQr31ups.mjs.map} +1 -1
- package/dist/{CachedStateProvider-CxsXOKkg.cjs → CachedStateProvider-Cyn17x3x.cjs} +1 -1
- package/dist/{CachedStateProvider-CxsXOKkg.cjs.map → CachedStateProvider-Cyn17x3x.cjs.map} +1 -1
- package/dist/{HostingerProvider-5KYmwoK2.cjs → HostingerProvider-C5oLa7N7.cjs} +1 -1
- package/dist/{HostingerProvider-5KYmwoK2.cjs.map → HostingerProvider-C5oLa7N7.cjs.map} +1 -1
- package/dist/{HostingerProvider-ANWchdiK.mjs → HostingerProvider-YvCg3Mus.mjs} +1 -1
- package/dist/{HostingerProvider-ANWchdiK.mjs.map → HostingerProvider-YvCg3Mus.mjs.map} +1 -1
- package/dist/{LocalStateProvider-Dp0KkRcw.mjs → LocalStateProvider-BciOhGR0.mjs} +1 -1
- package/dist/{LocalStateProvider-Dp0KkRcw.mjs.map → LocalStateProvider-BciOhGR0.mjs.map} +1 -1
- package/dist/{LocalStateProvider-CLifRC0Y.cjs → LocalStateProvider-DI6n11UM.cjs} +1 -1
- package/dist/{LocalStateProvider-CLifRC0Y.cjs.map → LocalStateProvider-DI6n11UM.cjs.map} +1 -1
- package/dist/{Route53Provider-QoPgcXxn.mjs → Route53Provider-CYlDOrYO.mjs} +1 -1
- package/dist/{Route53Provider-QoPgcXxn.mjs.map → Route53Provider-CYlDOrYO.mjs.map} +1 -1
- package/dist/{Route53Provider-owQQ4pn6.cjs → Route53Provider-DISZ6E8f.cjs} +1 -1
- package/dist/{Route53Provider-owQQ4pn6.cjs.map → Route53Provider-DISZ6E8f.cjs.map} +1 -1
- package/dist/{SSMStateProvider-CT8tjl9o.cjs → SSMStateProvider-BljH5efc.cjs} +1 -1
- package/dist/{SSMStateProvider-CT8tjl9o.cjs.map → SSMStateProvider-BljH5efc.cjs.map} +1 -1
- package/dist/{SSMStateProvider-CksOTB8M.mjs → SSMStateProvider-DXT1dIdI.mjs} +1 -1
- package/dist/{SSMStateProvider-CksOTB8M.mjs.map → SSMStateProvider-DXT1dIdI.mjs.map} +1 -1
- package/dist/{backup-provisioner-BEXoHTuC.mjs → backup-provisioner-CCUcj28C.mjs} +1 -1
- package/dist/{backup-provisioner-BEXoHTuC.mjs.map → backup-provisioner-CCUcj28C.mjs.map} +1 -1
- package/dist/{backup-provisioner-C4noe75O.cjs → backup-provisioner-CpirEvO1.cjs} +1 -1
- package/dist/{backup-provisioner-C4noe75O.cjs.map → backup-provisioner-CpirEvO1.cjs.map} +1 -1
- package/dist/{bundler-BG6P249p.cjs → bundler-CLOenWgi.cjs} +2 -2
- package/dist/{bundler-BG6P249p.cjs.map → bundler-CLOenWgi.cjs.map} +1 -1
- package/dist/{bundler-c6QvGUBM.mjs → bundler-E5VcXsPH.mjs} +2 -2
- package/dist/{bundler-c6QvGUBM.mjs.map → bundler-E5VcXsPH.mjs.map} +1 -1
- package/dist/client-generator-CUVVtAOo.mjs +23 -0
- package/dist/client-generator-CUVVtAOo.mjs.map +1 -0
- package/dist/client-generator-D-T4ypQo.cjs +35 -0
- package/dist/client-generator-D-T4ypQo.cjs.map +1 -0
- package/dist/{config-jsRYHOHU.mjs → config-B62g483e.mjs} +2 -2
- package/dist/{config-jsRYHOHU.mjs.map → config-B62g483e.mjs.map} +1 -1
- package/dist/{config-D3ORuiUs.cjs → config-Cuo8vFsp.cjs} +2 -2
- package/dist/{config-D3ORuiUs.cjs.map → config-Cuo8vFsp.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/deploy/sniffer-worker.cjs +2 -0
- package/dist/deploy/sniffer-worker.cjs.map +1 -1
- package/dist/deploy/sniffer-worker.mjs +2 -0
- package/dist/deploy/sniffer-worker.mjs.map +1 -1
- package/dist/dokploy-api-BPYkG-ZZ.cjs +3 -0
- package/dist/dokploy-api-BiSzWM4v.mjs +3 -0
- package/dist/{dokploy-api-Dvyq-LsM.cjs → dokploy-api-CqOIKHme.cjs} +1 -1
- package/dist/{dokploy-api-Dvyq-LsM.cjs.map → dokploy-api-CqOIKHme.cjs.map} +1 -1
- package/dist/{dokploy-api-BIT0683o.mjs → dokploy-api-sfZFlWuE.mjs} +1 -1
- package/dist/{dokploy-api-BIT0683o.mjs.map → dokploy-api-sfZFlWuE.mjs.map} +1 -1
- package/dist/{encryption-CipsS0KW.cjs → encryption-BVS7Vmzs.cjs} +1 -1
- package/dist/encryption-Bd_m4q3g.mjs +3 -0
- package/dist/{encryption-DSkIlEZ6.mjs → encryption-CO5Q4Bhf.mjs} +1 -1
- package/dist/{encryption-DSkIlEZ6.mjs.map → encryption-CO5Q4Bhf.mjs.map} +1 -1
- package/dist/{encryption-DgKS-Dv9.cjs → encryption-MfXAiFTm.cjs} +1 -1
- package/dist/{encryption-DgKS-Dv9.cjs.map → encryption-MfXAiFTm.cjs.map} +1 -1
- package/dist/{index-CY-ieuRp.d.mts → index-C-_rW4KF.d.mts} +3 -33
- package/dist/index-C-_rW4KF.d.mts.map +1 -0
- package/dist/{index-BkibYzso.d.cts → index-C3t5VL4R.d.cts} +2 -32
- package/dist/index-C3t5VL4R.d.cts.map +1 -0
- package/dist/index.cjs +102 -380
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +78 -356
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-CQCoLuzh.mjs → openapi-DHq5r17U.mjs} +7 -42
- package/dist/openapi-DHq5r17U.mjs.map +1 -0
- package/dist/{openapi-react-query-C4UdILaI.mjs → openapi-react-query-COpH7Mmz.mjs} +1 -1
- package/dist/{openapi-react-query-C4UdILaI.mjs.map → openapi-react-query-COpH7Mmz.mjs.map} +1 -1
- package/dist/{openapi-react-query-DYbBq-WJ.cjs → openapi-react-query-CxyQTTdF.cjs} +1 -1
- package/dist/{openapi-react-query-DYbBq-WJ.cjs.map → openapi-react-query-CxyQTTdF.cjs.map} +1 -1
- package/dist/openapi-react-query.cjs +1 -1
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/{openapi-CBU4TEi-.cjs → openapi-w-_smBTO.cjs} +7 -54
- package/dist/openapi-w-_smBTO.cjs.map +1 -0
- package/dist/openapi.cjs +4 -3
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +4 -3
- package/dist/{reconcile-CZtzdrnc.cjs → reconcile-C8KIy4Uy.cjs} +1 -1
- package/dist/{reconcile-CZtzdrnc.cjs.map → reconcile-C8KIy4Uy.cjs.map} +1 -1
- package/dist/{reconcile-DAAKa0YR.mjs → reconcile-D423q2QW.mjs} +1 -1
- package/dist/{reconcile-DAAKa0YR.mjs.map → reconcile-D423q2QW.mjs.map} +1 -1
- package/dist/sniffer-BqSVVrWA.mjs +356 -0
- package/dist/sniffer-BqSVVrWA.mjs.map +1 -0
- package/dist/sniffer-CBhzByiY.cjs +386 -0
- package/dist/sniffer-CBhzByiY.cjs.map +1 -0
- package/dist/sniffer-Cpq_cbyz.mjs +4 -0
- package/dist/sniffer-eZ13G10M.cjs +4 -0
- package/dist/sync-B2CN_KS5.cjs +4 -0
- package/dist/{sync-DGXXSk2v.cjs → sync-BWY6yoUG.cjs} +1 -1
- package/dist/{sync-DGXXSk2v.cjs.map → sync-BWY6yoUG.cjs.map} +1 -1
- package/dist/{sync-D_NowTkZ.mjs → sync-Dc-QA4Mx.mjs} +1 -1
- package/dist/{sync-D_NowTkZ.mjs.map → sync-Dc-QA4Mx.mjs.map} +1 -1
- package/dist/{sync-COnAugP-.mjs → sync-fzT5WX_y.mjs} +1 -1
- package/dist/{types-OszPdw9m.d.mts → types-BqAbYt5a.d.mts} +1 -1
- package/dist/{types-OszPdw9m.d.mts.map → types-BqAbYt5a.d.mts.map} +1 -1
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +3 -3
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-D4z4A4cq.mjs → workspace-Bi4X7Yzy.mjs} +1 -6
- package/dist/{workspace-D4z4A4cq.mjs.map → workspace-Bi4X7Yzy.mjs.map} +1 -1
- package/dist/{workspace-4SP3Gx4Y.cjs → workspace-CGYykWfn.cjs} +1 -6
- package/dist/{workspace-4SP3Gx4Y.cjs.map → workspace-CGYykWfn.cjs.map} +1 -1
- package/package.json +3 -3
- package/src/__tests__/openapi.spec.ts +0 -315
- package/src/build/__tests__/manifests.spec.ts +66 -0
- package/src/build/index.ts +54 -1
- package/src/deploy/__tests__/__fixtures__/env-parsers/optional-env-parser.ts +21 -0
- package/src/deploy/__tests__/sniffer.spec.ts +123 -0
- package/src/deploy/sniffer-worker.ts +2 -1
- package/src/deploy/sniffer.ts +86 -12
- package/src/docker/compose.ts +2 -0
- package/src/generators/EndpointGenerator.ts +1 -1
- package/src/index.ts +8 -0
- package/src/init/generators/monorepo.ts +15 -1
- package/src/init/generators/package.ts +2 -0
- package/src/init/index.ts +2 -4
- package/src/init/templates/index.ts +5 -0
- package/src/init/versions.ts +1 -1
- package/src/openapi.ts +1 -35
- package/src/types.ts +7 -0
- package/src/workspace/__tests__/client-generator.spec.ts +1 -485
- package/src/workspace/client-generator.ts +1 -145
- package/src/workspace/index.ts +0 -1
- package/src/workspace/schema.ts +0 -9
- package/src/workspace/types.ts +0 -29
- package/dist/CachedStateProvider-CC0DyO09.mjs +0 -3
- package/dist/dokploy-api-BOx71j6F.cjs +0 -3
- package/dist/dokploy-api-BZGk-lkD.mjs +0 -3
- package/dist/encryption-Db2zvGqM.mjs +0 -3
- package/dist/index-BkibYzso.d.cts.map +0 -1
- package/dist/index-CY-ieuRp.d.mts.map +0 -1
- package/dist/openapi-CBU4TEi-.cjs.map +0 -1
- package/dist/openapi-CQCoLuzh.mjs.map +0 -1
- package/dist/sync-D1Pa30oV.cjs +0 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geekmidas/cli",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.31",
|
|
4
4
|
"description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
"prompts": "~2.4.2",
|
|
57
57
|
"tsx": "~4.20.3",
|
|
58
58
|
"yaml": "~2.8.2",
|
|
59
|
-
"@geekmidas/constructs": "~3.0.
|
|
60
|
-
"@geekmidas/envkit": "~1.0.
|
|
59
|
+
"@geekmidas/constructs": "~3.0.7",
|
|
60
|
+
"@geekmidas/envkit": "~1.0.5",
|
|
61
61
|
"@geekmidas/errors": "~1.0.0",
|
|
62
62
|
"@geekmidas/logger": "~1.0.0",
|
|
63
63
|
"@geekmidas/schema": "~1.0.0"
|
|
@@ -532,319 +532,4 @@ describe('openapiCommand - workspace mode', () => {
|
|
|
532
532
|
expect.stringContaining('[api] Generated OpenAPI'),
|
|
533
533
|
);
|
|
534
534
|
});
|
|
535
|
-
|
|
536
|
-
it('should copy OpenAPI to frontend app with client.output', async () => {
|
|
537
|
-
// Create workspace structure
|
|
538
|
-
const apiDir = join(tempDir, 'apps/api');
|
|
539
|
-
const webDir = join(tempDir, 'apps/web');
|
|
540
|
-
await mkdir(apiDir, { recursive: true });
|
|
541
|
-
await mkdir(webDir, { recursive: true });
|
|
542
|
-
|
|
543
|
-
// Create endpoint in backend app
|
|
544
|
-
await createMockEndpointFile(
|
|
545
|
-
apiDir,
|
|
546
|
-
'src/endpoints/users.ts',
|
|
547
|
-
'getUsers',
|
|
548
|
-
'/users',
|
|
549
|
-
'GET',
|
|
550
|
-
);
|
|
551
|
-
|
|
552
|
-
// Create workspace config with frontend that depends on backend
|
|
553
|
-
await createTestFile(
|
|
554
|
-
tempDir,
|
|
555
|
-
'gkm.config.json',
|
|
556
|
-
JSON.stringify({
|
|
557
|
-
name: 'test-workspace',
|
|
558
|
-
apps: {
|
|
559
|
-
api: {
|
|
560
|
-
type: 'backend',
|
|
561
|
-
path: 'apps/api',
|
|
562
|
-
port: 3000,
|
|
563
|
-
routes: './src/endpoints/**/*.ts',
|
|
564
|
-
openapi: { enabled: true },
|
|
565
|
-
},
|
|
566
|
-
web: {
|
|
567
|
-
type: 'frontend',
|
|
568
|
-
framework: 'nextjs',
|
|
569
|
-
path: 'apps/web',
|
|
570
|
-
port: 3001,
|
|
571
|
-
dependencies: ['api'],
|
|
572
|
-
client: {
|
|
573
|
-
output: './src/api',
|
|
574
|
-
},
|
|
575
|
-
},
|
|
576
|
-
},
|
|
577
|
-
}),
|
|
578
|
-
);
|
|
579
|
-
|
|
580
|
-
process.chdir(tempDir);
|
|
581
|
-
const consoleSpy = vi.spyOn(console, 'log');
|
|
582
|
-
|
|
583
|
-
await openapiCommand({ cwd: tempDir });
|
|
584
|
-
|
|
585
|
-
// Should generate OpenAPI in backend app
|
|
586
|
-
const backendOutput = join(apiDir, OPENAPI_OUTPUT_PATH);
|
|
587
|
-
expect(existsSync(backendOutput)).toBe(true);
|
|
588
|
-
|
|
589
|
-
// Should copy to frontend app's client output path
|
|
590
|
-
const frontendOutput = join(webDir, 'src/api/openapi.ts');
|
|
591
|
-
expect(existsSync(frontendOutput)).toBe(true);
|
|
592
|
-
|
|
593
|
-
// Content should be the same
|
|
594
|
-
const backendContent = await readFile(backendOutput, 'utf-8');
|
|
595
|
-
const frontendContent = await readFile(frontendOutput, 'utf-8');
|
|
596
|
-
expect(frontendContent).toBe(backendContent);
|
|
597
|
-
|
|
598
|
-
expect(consoleSpy).toHaveBeenCalledWith(
|
|
599
|
-
expect.stringContaining('[web] ./src/api/openapi.ts'),
|
|
600
|
-
);
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
it('should only copy to frontend apps that depend on the backend', async () => {
|
|
604
|
-
// Create workspace structure with multiple apps
|
|
605
|
-
const apiDir = join(tempDir, 'apps/api');
|
|
606
|
-
const authDir = join(tempDir, 'apps/auth');
|
|
607
|
-
const webDir = join(tempDir, 'apps/web');
|
|
608
|
-
const adminDir = join(tempDir, 'apps/admin');
|
|
609
|
-
await mkdir(apiDir, { recursive: true });
|
|
610
|
-
await mkdir(authDir, { recursive: true });
|
|
611
|
-
await mkdir(webDir, { recursive: true });
|
|
612
|
-
await mkdir(adminDir, { recursive: true });
|
|
613
|
-
|
|
614
|
-
// Create endpoints
|
|
615
|
-
await createMockEndpointFile(
|
|
616
|
-
apiDir,
|
|
617
|
-
'src/endpoints/users.ts',
|
|
618
|
-
'getUsers',
|
|
619
|
-
'/users',
|
|
620
|
-
'GET',
|
|
621
|
-
);
|
|
622
|
-
await createMockEndpointFile(
|
|
623
|
-
authDir,
|
|
624
|
-
'src/endpoints/login.ts',
|
|
625
|
-
'login',
|
|
626
|
-
'/login',
|
|
627
|
-
'POST',
|
|
628
|
-
);
|
|
629
|
-
|
|
630
|
-
// Create workspace config
|
|
631
|
-
await createTestFile(
|
|
632
|
-
tempDir,
|
|
633
|
-
'gkm.config.json',
|
|
634
|
-
JSON.stringify({
|
|
635
|
-
name: 'test-workspace',
|
|
636
|
-
apps: {
|
|
637
|
-
api: {
|
|
638
|
-
type: 'backend',
|
|
639
|
-
path: 'apps/api',
|
|
640
|
-
port: 3000,
|
|
641
|
-
routes: './src/endpoints/**/*.ts',
|
|
642
|
-
openapi: { enabled: true },
|
|
643
|
-
},
|
|
644
|
-
auth: {
|
|
645
|
-
type: 'backend',
|
|
646
|
-
path: 'apps/auth',
|
|
647
|
-
port: 3001,
|
|
648
|
-
routes: './src/endpoints/**/*.ts',
|
|
649
|
-
openapi: { enabled: true },
|
|
650
|
-
},
|
|
651
|
-
web: {
|
|
652
|
-
type: 'frontend',
|
|
653
|
-
framework: 'nextjs',
|
|
654
|
-
path: 'apps/web',
|
|
655
|
-
port: 3002,
|
|
656
|
-
dependencies: ['api'], // Only depends on api
|
|
657
|
-
client: {
|
|
658
|
-
output: './src/api',
|
|
659
|
-
},
|
|
660
|
-
},
|
|
661
|
-
admin: {
|
|
662
|
-
type: 'frontend',
|
|
663
|
-
framework: 'nextjs',
|
|
664
|
-
path: 'apps/admin',
|
|
665
|
-
port: 3003,
|
|
666
|
-
dependencies: ['auth'], // Only depends on auth
|
|
667
|
-
client: {
|
|
668
|
-
output: './src/client',
|
|
669
|
-
},
|
|
670
|
-
},
|
|
671
|
-
},
|
|
672
|
-
}),
|
|
673
|
-
);
|
|
674
|
-
|
|
675
|
-
process.chdir(tempDir);
|
|
676
|
-
|
|
677
|
-
await openapiCommand({ cwd: tempDir });
|
|
678
|
-
|
|
679
|
-
// Web should have api's OpenAPI (not auth's)
|
|
680
|
-
const webApiOutput = join(webDir, 'src/api/openapi.ts');
|
|
681
|
-
expect(existsSync(webApiOutput)).toBe(true);
|
|
682
|
-
const webContent = await readFile(webApiOutput, 'utf-8');
|
|
683
|
-
expect(webContent).toContain("'/users'");
|
|
684
|
-
expect(webContent).not.toContain("'/login'");
|
|
685
|
-
|
|
686
|
-
// Admin should have auth's OpenAPI (not api's)
|
|
687
|
-
const adminClientOutput = join(adminDir, 'src/client/openapi.ts');
|
|
688
|
-
expect(existsSync(adminClientOutput)).toBe(true);
|
|
689
|
-
const adminContent = await readFile(adminClientOutput, 'utf-8');
|
|
690
|
-
expect(adminContent).toContain("'/login'");
|
|
691
|
-
expect(adminContent).not.toContain("'/users'");
|
|
692
|
-
});
|
|
693
|
-
|
|
694
|
-
it('should not copy to frontend with empty dependencies array', async () => {
|
|
695
|
-
// Create workspace structure
|
|
696
|
-
const apiDir = join(tempDir, 'apps/api');
|
|
697
|
-
const webDir = join(tempDir, 'apps/web');
|
|
698
|
-
await mkdir(apiDir, { recursive: true });
|
|
699
|
-
await mkdir(webDir, { recursive: true });
|
|
700
|
-
|
|
701
|
-
await createMockEndpointFile(
|
|
702
|
-
apiDir,
|
|
703
|
-
'src/endpoints/users.ts',
|
|
704
|
-
'getUsers',
|
|
705
|
-
'/users',
|
|
706
|
-
'GET',
|
|
707
|
-
);
|
|
708
|
-
|
|
709
|
-
// Frontend with empty dependencies array (depends on nothing)
|
|
710
|
-
await createTestFile(
|
|
711
|
-
tempDir,
|
|
712
|
-
'gkm.config.json',
|
|
713
|
-
JSON.stringify({
|
|
714
|
-
name: 'test-workspace',
|
|
715
|
-
apps: {
|
|
716
|
-
api: {
|
|
717
|
-
type: 'backend',
|
|
718
|
-
path: 'apps/api',
|
|
719
|
-
port: 3000,
|
|
720
|
-
routes: './src/endpoints/**/*.ts',
|
|
721
|
-
openapi: { enabled: true },
|
|
722
|
-
},
|
|
723
|
-
web: {
|
|
724
|
-
type: 'frontend',
|
|
725
|
-
framework: 'nextjs',
|
|
726
|
-
path: 'apps/web',
|
|
727
|
-
port: 3001,
|
|
728
|
-
dependencies: [], // Empty array means depends on nothing
|
|
729
|
-
client: {
|
|
730
|
-
output: './src/api',
|
|
731
|
-
},
|
|
732
|
-
},
|
|
733
|
-
},
|
|
734
|
-
}),
|
|
735
|
-
);
|
|
736
|
-
|
|
737
|
-
process.chdir(tempDir);
|
|
738
|
-
|
|
739
|
-
await openapiCommand({ cwd: tempDir });
|
|
740
|
-
|
|
741
|
-
// Should NOT copy to frontend since it has no dependencies
|
|
742
|
-
const frontendOutput = join(webDir, 'src/api/openapi.ts');
|
|
743
|
-
expect(existsSync(frontendOutput)).toBe(false);
|
|
744
|
-
});
|
|
745
|
-
|
|
746
|
-
it('should skip frontend apps without client.output', async () => {
|
|
747
|
-
const apiDir = join(tempDir, 'apps/api');
|
|
748
|
-
const webDir = join(tempDir, 'apps/web');
|
|
749
|
-
await mkdir(apiDir, { recursive: true });
|
|
750
|
-
await mkdir(webDir, { recursive: true });
|
|
751
|
-
|
|
752
|
-
await createMockEndpointFile(
|
|
753
|
-
apiDir,
|
|
754
|
-
'src/endpoints/users.ts',
|
|
755
|
-
'getUsers',
|
|
756
|
-
'/users',
|
|
757
|
-
'GET',
|
|
758
|
-
);
|
|
759
|
-
|
|
760
|
-
// Frontend without client.output
|
|
761
|
-
await createTestFile(
|
|
762
|
-
tempDir,
|
|
763
|
-
'gkm.config.json',
|
|
764
|
-
JSON.stringify({
|
|
765
|
-
name: 'test-workspace',
|
|
766
|
-
apps: {
|
|
767
|
-
api: {
|
|
768
|
-
type: 'backend',
|
|
769
|
-
path: 'apps/api',
|
|
770
|
-
port: 3000,
|
|
771
|
-
routes: './src/endpoints/**/*.ts',
|
|
772
|
-
openapi: { enabled: true },
|
|
773
|
-
},
|
|
774
|
-
web: {
|
|
775
|
-
type: 'frontend',
|
|
776
|
-
framework: 'nextjs',
|
|
777
|
-
path: 'apps/web',
|
|
778
|
-
port: 3001,
|
|
779
|
-
dependencies: ['api'],
|
|
780
|
-
// No client.output configured
|
|
781
|
-
},
|
|
782
|
-
},
|
|
783
|
-
}),
|
|
784
|
-
);
|
|
785
|
-
|
|
786
|
-
process.chdir(tempDir);
|
|
787
|
-
|
|
788
|
-
await openapiCommand({ cwd: tempDir });
|
|
789
|
-
|
|
790
|
-
// Backend should still have OpenAPI generated
|
|
791
|
-
expect(existsSync(join(apiDir, OPENAPI_OUTPUT_PATH))).toBe(true);
|
|
792
|
-
|
|
793
|
-
// But no files should be created in frontend
|
|
794
|
-
expect(existsSync(join(webDir, 'src/api/openapi.ts'))).toBe(false);
|
|
795
|
-
});
|
|
796
|
-
|
|
797
|
-
it('should handle nested client.output paths', async () => {
|
|
798
|
-
const apiDir = join(tempDir, 'apps/api');
|
|
799
|
-
const webDir = join(tempDir, 'apps/web');
|
|
800
|
-
await mkdir(apiDir, { recursive: true });
|
|
801
|
-
await mkdir(webDir, { recursive: true });
|
|
802
|
-
|
|
803
|
-
await createMockEndpointFile(
|
|
804
|
-
apiDir,
|
|
805
|
-
'src/endpoints/users.ts',
|
|
806
|
-
'getUsers',
|
|
807
|
-
'/users',
|
|
808
|
-
'GET',
|
|
809
|
-
);
|
|
810
|
-
|
|
811
|
-
// Deeply nested client output path
|
|
812
|
-
await createTestFile(
|
|
813
|
-
tempDir,
|
|
814
|
-
'gkm.config.json',
|
|
815
|
-
JSON.stringify({
|
|
816
|
-
name: 'test-workspace',
|
|
817
|
-
apps: {
|
|
818
|
-
api: {
|
|
819
|
-
type: 'backend',
|
|
820
|
-
path: 'apps/api',
|
|
821
|
-
port: 3000,
|
|
822
|
-
routes: './src/endpoints/**/*.ts',
|
|
823
|
-
openapi: { enabled: true },
|
|
824
|
-
},
|
|
825
|
-
web: {
|
|
826
|
-
type: 'frontend',
|
|
827
|
-
framework: 'nextjs',
|
|
828
|
-
path: 'apps/web',
|
|
829
|
-
port: 3001,
|
|
830
|
-
dependencies: ['api'],
|
|
831
|
-
client: {
|
|
832
|
-
output: './src/lib/api/generated',
|
|
833
|
-
},
|
|
834
|
-
},
|
|
835
|
-
},
|
|
836
|
-
}),
|
|
837
|
-
);
|
|
838
|
-
|
|
839
|
-
process.chdir(tempDir);
|
|
840
|
-
|
|
841
|
-
await openapiCommand({ cwd: tempDir });
|
|
842
|
-
|
|
843
|
-
// Should create nested directories and file
|
|
844
|
-
const frontendOutput = join(webDir, 'src/lib/api/generated/openapi.ts');
|
|
845
|
-
expect(existsSync(frontendOutput)).toBe(true);
|
|
846
|
-
|
|
847
|
-
const content = await readFile(frontendOutput, 'utf-8');
|
|
848
|
-
expect(content).toContain('export interface paths');
|
|
849
|
-
});
|
|
850
535
|
});
|
|
@@ -346,6 +346,72 @@ describe('generateServerManifest', () => {
|
|
|
346
346
|
});
|
|
347
347
|
});
|
|
348
348
|
|
|
349
|
+
describe('generateAwsManifest (optional env vars)', () => {
|
|
350
|
+
itWithDir(
|
|
351
|
+
'should preserve ? suffix on optional env vars in route environment',
|
|
352
|
+
async ({ dir }) => {
|
|
353
|
+
const routes: RouteInfo[] = [
|
|
354
|
+
{
|
|
355
|
+
path: '/users',
|
|
356
|
+
method: 'GET',
|
|
357
|
+
handler: '.gkm/aws/getUsers.handler',
|
|
358
|
+
authorizer: 'none',
|
|
359
|
+
environment: ['DATABASE_URL', 'PORT?', 'LOG_LEVEL?'],
|
|
360
|
+
},
|
|
361
|
+
];
|
|
362
|
+
|
|
363
|
+
await generateAwsManifest(dir, routes, [], [], []);
|
|
364
|
+
|
|
365
|
+
const content = await readFile(join(dir, 'manifest', 'aws.ts'), 'utf-8');
|
|
366
|
+
|
|
367
|
+
expect(content).toContain('"DATABASE_URL"');
|
|
368
|
+
expect(content).toContain('"PORT?"');
|
|
369
|
+
expect(content).toContain('"LOG_LEVEL?"');
|
|
370
|
+
},
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
itWithDir(
|
|
374
|
+
'should preserve ? suffix on optional env vars in function environment',
|
|
375
|
+
async ({ dir }) => {
|
|
376
|
+
const functions: FunctionInfo[] = [
|
|
377
|
+
{
|
|
378
|
+
name: 'processData',
|
|
379
|
+
handler: '.gkm/aws/processData.handler',
|
|
380
|
+
environment: ['DATABASE_URL', 'TIMEOUT?'],
|
|
381
|
+
},
|
|
382
|
+
];
|
|
383
|
+
|
|
384
|
+
await generateAwsManifest(dir, [], functions, [], []);
|
|
385
|
+
|
|
386
|
+
const content = await readFile(join(dir, 'manifest', 'aws.ts'), 'utf-8');
|
|
387
|
+
|
|
388
|
+
expect(content).toContain('"DATABASE_URL"');
|
|
389
|
+
expect(content).toContain('"TIMEOUT?"');
|
|
390
|
+
},
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
itWithDir(
|
|
394
|
+
'should preserve ? suffix on optional env vars in subscriber environment',
|
|
395
|
+
async ({ dir }) => {
|
|
396
|
+
const subscribers: SubscriberInfo[] = [
|
|
397
|
+
{
|
|
398
|
+
name: 'orderHandler',
|
|
399
|
+
handler: '.gkm/aws/orderHandler.handler',
|
|
400
|
+
subscribedEvents: ['order.created'],
|
|
401
|
+
environment: ['DATABASE_URL', 'RETRY_LIMIT?'],
|
|
402
|
+
},
|
|
403
|
+
];
|
|
404
|
+
|
|
405
|
+
await generateAwsManifest(dir, [], [], [], subscribers);
|
|
406
|
+
|
|
407
|
+
const content = await readFile(join(dir, 'manifest', 'aws.ts'), 'utf-8');
|
|
408
|
+
|
|
409
|
+
expect(content).toContain('"DATABASE_URL"');
|
|
410
|
+
expect(content).toContain('"RETRY_LIMIT?"');
|
|
411
|
+
},
|
|
412
|
+
);
|
|
413
|
+
});
|
|
414
|
+
|
|
349
415
|
describe('generateAwsManifest (partitioned)', () => {
|
|
350
416
|
itWithDir(
|
|
351
417
|
'should generate manifest with partitioned routes and flat functions',
|
package/src/build/index.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
type GeneratedConstruct,
|
|
27
27
|
SubscriberGenerator,
|
|
28
28
|
} from '../generators';
|
|
29
|
+
import { generateOpenApi, openapiCommand } from '../openapi.js';
|
|
29
30
|
import {
|
|
30
31
|
type BuildOptions,
|
|
31
32
|
type BuildResult,
|
|
@@ -195,6 +196,26 @@ export async function buildCommand(
|
|
|
195
196
|
const rootOutputDir = join(process.cwd(), '.gkm');
|
|
196
197
|
await mkdir(rootOutputDir, { recursive: true });
|
|
197
198
|
|
|
199
|
+
// When --mark-optional is set, sniff env vars from the envParser to determine
|
|
200
|
+
// which are optional. Optional vars get a `?` suffix in each construct's
|
|
201
|
+
// environment array (e.g. `PORT?` instead of `PORT`).
|
|
202
|
+
let optionalVarSet: Set<string> | undefined;
|
|
203
|
+
if (options.markOptional && config.envParser) {
|
|
204
|
+
try {
|
|
205
|
+
const { _sniffEnvParser } = await import('../deploy/sniffer.js');
|
|
206
|
+
const sniffed = await _sniffEnvParser(
|
|
207
|
+
config.envParser,
|
|
208
|
+
process.cwd(),
|
|
209
|
+
process.cwd(),
|
|
210
|
+
);
|
|
211
|
+
if (sniffed.optionalEnvVars.length > 0) {
|
|
212
|
+
optionalVarSet = new Set(sniffed.optionalEnvVars);
|
|
213
|
+
}
|
|
214
|
+
} catch {
|
|
215
|
+
// Non-fatal — constructs are still built without optional markers
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
198
219
|
// Build for each provider and generate per-provider manifests
|
|
199
220
|
let result: BuildResult = {};
|
|
200
221
|
for (const provider of resolved.providers) {
|
|
@@ -213,12 +234,17 @@ export async function buildCommand(
|
|
|
213
234
|
resolved.enableOpenApi,
|
|
214
235
|
options.skipBundle ?? false,
|
|
215
236
|
options.stage,
|
|
237
|
+
optionalVarSet,
|
|
216
238
|
);
|
|
217
239
|
// Keep the master key from the server provider
|
|
218
240
|
if (providerResult.masterKey) {
|
|
219
241
|
result = providerResult;
|
|
220
242
|
}
|
|
221
243
|
}
|
|
244
|
+
|
|
245
|
+
// Generate OpenAPI spec as part of the build
|
|
246
|
+
await generateOpenApi(config, { bustCache: true });
|
|
247
|
+
|
|
222
248
|
return result;
|
|
223
249
|
}
|
|
224
250
|
|
|
@@ -237,6 +263,7 @@ async function buildForProvider(
|
|
|
237
263
|
enableOpenApi: boolean,
|
|
238
264
|
skipBundle: boolean,
|
|
239
265
|
stage?: string,
|
|
266
|
+
optionalVarSet?: Set<string>,
|
|
240
267
|
): Promise<BuildResult> {
|
|
241
268
|
const outputDir = join(process.cwd(), '.gkm', provider);
|
|
242
269
|
|
|
@@ -246,7 +273,7 @@ async function buildForProvider(
|
|
|
246
273
|
logger.log(`\nGenerating handlers for provider: ${provider}`);
|
|
247
274
|
|
|
248
275
|
// Build all constructs in parallel
|
|
249
|
-
const [
|
|
276
|
+
const [rawRoutes, rawFunctions, rawCrons, rawSubscribers] = await Promise.all(
|
|
250
277
|
[
|
|
251
278
|
endpointGenerator.build(context, endpoints, outputDir, {
|
|
252
279
|
provider,
|
|
@@ -258,6 +285,29 @@ async function buildForProvider(
|
|
|
258
285
|
],
|
|
259
286
|
);
|
|
260
287
|
|
|
288
|
+
// Apply optional marker (`?` suffix) to env vars in each construct's environment
|
|
289
|
+
const markEnv = optionalVarSet
|
|
290
|
+
? (vars: string[] | undefined) =>
|
|
291
|
+
vars?.map((v) => (optionalVarSet.has(v) ? `${v}?` : v))
|
|
292
|
+
: (vars: string[] | undefined) => vars;
|
|
293
|
+
|
|
294
|
+
const routes = rawRoutes.map((r) => ({
|
|
295
|
+
...r,
|
|
296
|
+
environment: markEnv(r.environment),
|
|
297
|
+
}));
|
|
298
|
+
const functionInfos = rawFunctions.map((f) => ({
|
|
299
|
+
...f,
|
|
300
|
+
environment: markEnv(f.environment),
|
|
301
|
+
}));
|
|
302
|
+
const cronInfos = rawCrons.map((c) => ({
|
|
303
|
+
...c,
|
|
304
|
+
environment: markEnv(c.environment),
|
|
305
|
+
}));
|
|
306
|
+
const subscriberInfos = rawSubscribers.map((s) => ({
|
|
307
|
+
...s,
|
|
308
|
+
environment: markEnv(s.environment),
|
|
309
|
+
}));
|
|
310
|
+
|
|
261
311
|
logger.log(
|
|
262
312
|
`Generated ${routes.length} routes, ${functionInfos.length} functions, ${cronInfos.length} crons, ${subscriberInfos.length} subscribers for ${provider}`,
|
|
263
313
|
);
|
|
@@ -472,6 +522,9 @@ export async function workspaceBuildCommand(
|
|
|
472
522
|
|
|
473
523
|
logger.log(`\n✅ Workspace build complete!`);
|
|
474
524
|
|
|
525
|
+
// Generate OpenAPI specs and copy to frontend apps
|
|
526
|
+
await openapiCommand({ cwd: workspace.root });
|
|
527
|
+
|
|
475
528
|
// Summary
|
|
476
529
|
logger.log(`\n📋 Build Summary:`);
|
|
477
530
|
for (const result of results) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An envParser fixture that mixes required and optional/default variables.
|
|
3
|
+
* Used to test the markOptional feature.
|
|
4
|
+
*/
|
|
5
|
+
import type { SnifferEnvironmentParser } from '@geekmidas/envkit/sniffer';
|
|
6
|
+
|
|
7
|
+
export function envParser(parser: SnifferEnvironmentParser) {
|
|
8
|
+
return parser.create((get) => ({
|
|
9
|
+
// Required
|
|
10
|
+
databaseUrl: get('DATABASE_URL').string(),
|
|
11
|
+
apiKey: get('API_KEY').string(),
|
|
12
|
+
// Optional via .optional()
|
|
13
|
+
logLevel: get('LOG_LEVEL').string().optional(),
|
|
14
|
+
// Optional via .default()
|
|
15
|
+
port: get('PORT').string().default('3000'),
|
|
16
|
+
// Optional via .default() after transform
|
|
17
|
+
timeout: get('TIMEOUT').string().transform(Number).default(5000),
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default envParser;
|
|
@@ -193,6 +193,126 @@ describe('sniffAppEnvironment', () => {
|
|
|
193
193
|
const result = await sniffAppEnvironment(app, 'api', workspacePath);
|
|
194
194
|
|
|
195
195
|
expect(result.requiredEnvVars).toEqual([]);
|
|
196
|
+
expect(result.optionalEnvVars).toEqual([]);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('markOptional option', () => {
|
|
201
|
+
it('returns optionalEnvVars populated from envParser with optional/default vars', async () => {
|
|
202
|
+
const app = createApp({
|
|
203
|
+
path: envParserFixturesPath,
|
|
204
|
+
envParser: './optional-env-parser#envParser',
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const result = await sniffAppEnvironment(
|
|
208
|
+
app,
|
|
209
|
+
'api',
|
|
210
|
+
envParserFixturesPath,
|
|
211
|
+
{
|
|
212
|
+
logWarnings: false,
|
|
213
|
+
},
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
// All vars present regardless of markOptional
|
|
217
|
+
expect(result.requiredEnvVars).toContain('DATABASE_URL');
|
|
218
|
+
expect(result.requiredEnvVars).toContain('API_KEY');
|
|
219
|
+
expect(result.requiredEnvVars).toContain('LOG_LEVEL');
|
|
220
|
+
expect(result.requiredEnvVars).toContain('PORT');
|
|
221
|
+
expect(result.requiredEnvVars).toContain('TIMEOUT');
|
|
222
|
+
|
|
223
|
+
// Optional vars populated (no ? suffix since markOptional not set)
|
|
224
|
+
expect(result.optionalEnvVars).toContain('LOG_LEVEL');
|
|
225
|
+
expect(result.optionalEnvVars).toContain('PORT');
|
|
226
|
+
expect(result.optionalEnvVars).toContain('TIMEOUT');
|
|
227
|
+
expect(result.optionalEnvVars).not.toContain('DATABASE_URL');
|
|
228
|
+
expect(result.optionalEnvVars).not.toContain('API_KEY');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('suffixes optional vars with ? in requiredEnvVars when markOptional is true', async () => {
|
|
232
|
+
const app = createApp({
|
|
233
|
+
path: envParserFixturesPath,
|
|
234
|
+
envParser: './optional-env-parser#envParser',
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const result = await sniffAppEnvironment(
|
|
238
|
+
app,
|
|
239
|
+
'api',
|
|
240
|
+
envParserFixturesPath,
|
|
241
|
+
{
|
|
242
|
+
logWarnings: false,
|
|
243
|
+
markOptional: true,
|
|
244
|
+
},
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
// Required vars unchanged
|
|
248
|
+
expect(result.requiredEnvVars).toContain('DATABASE_URL');
|
|
249
|
+
expect(result.requiredEnvVars).toContain('API_KEY');
|
|
250
|
+
|
|
251
|
+
// Optional vars suffixed with ?
|
|
252
|
+
expect(result.requiredEnvVars).toContain('LOG_LEVEL?');
|
|
253
|
+
expect(result.requiredEnvVars).toContain('PORT?');
|
|
254
|
+
expect(result.requiredEnvVars).toContain('TIMEOUT?');
|
|
255
|
+
|
|
256
|
+
// Plain names not present for optional vars
|
|
257
|
+
expect(result.requiredEnvVars).not.toContain('LOG_LEVEL');
|
|
258
|
+
expect(result.requiredEnvVars).not.toContain('PORT');
|
|
259
|
+
expect(result.requiredEnvVars).not.toContain('TIMEOUT');
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('does not add ? suffix when markOptional is false (default)', async () => {
|
|
263
|
+
const app = createApp({
|
|
264
|
+
path: envParserFixturesPath,
|
|
265
|
+
envParser: './optional-env-parser#envParser',
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const result = await sniffAppEnvironment(
|
|
269
|
+
app,
|
|
270
|
+
'api',
|
|
271
|
+
envParserFixturesPath,
|
|
272
|
+
{
|
|
273
|
+
logWarnings: false,
|
|
274
|
+
markOptional: false,
|
|
275
|
+
},
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
// No ? suffixes regardless of optionality
|
|
279
|
+
for (const v of result.requiredEnvVars) {
|
|
280
|
+
expect(v).not.toMatch(/\?$/);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('returns empty optionalEnvVars for apps with no optional vars', async () => {
|
|
285
|
+
const app = createApp({
|
|
286
|
+
path: envParserFixturesPath,
|
|
287
|
+
envParser: './valid-env-parser#envParser',
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const result = await sniffAppEnvironment(
|
|
291
|
+
app,
|
|
292
|
+
'api',
|
|
293
|
+
envParserFixturesPath,
|
|
294
|
+
{
|
|
295
|
+
logWarnings: false,
|
|
296
|
+
markOptional: true,
|
|
297
|
+
},
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
expect(result.optionalEnvVars).toEqual([]);
|
|
301
|
+
// No ? suffixes when nothing is optional
|
|
302
|
+
for (const v of result.requiredEnvVars) {
|
|
303
|
+
expect(v).not.toMatch(/\?$/);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('returns empty optionalEnvVars for frontend apps', async () => {
|
|
308
|
+
const app = createApp({
|
|
309
|
+
type: 'frontend',
|
|
310
|
+
dependencies: ['api'],
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const result = await sniffAppEnvironment(app, 'web', workspacePath);
|
|
314
|
+
|
|
315
|
+
expect(result.optionalEnvVars).toEqual([]);
|
|
196
316
|
});
|
|
197
317
|
});
|
|
198
318
|
});
|
|
@@ -234,16 +354,19 @@ describe('sniffAllApps', () => {
|
|
|
234
354
|
expect(results.get('api')).toEqual({
|
|
235
355
|
appName: 'api',
|
|
236
356
|
requiredEnvVars: [],
|
|
357
|
+
optionalEnvVars: [],
|
|
237
358
|
});
|
|
238
359
|
|
|
239
360
|
expect(results.get('auth')).toEqual({
|
|
240
361
|
appName: 'auth',
|
|
241
362
|
requiredEnvVars: [],
|
|
363
|
+
optionalEnvVars: [],
|
|
242
364
|
});
|
|
243
365
|
|
|
244
366
|
expect(results.get('web')).toEqual({
|
|
245
367
|
appName: 'web',
|
|
246
368
|
requiredEnvVars: ['NEXT_PUBLIC_API_URL', 'NEXT_PUBLIC_AUTH_URL'],
|
|
369
|
+
optionalEnvVars: [],
|
|
247
370
|
});
|
|
248
371
|
});
|
|
249
372
|
|
|
@@ -57,9 +57,10 @@ async function sniff(): Promise<void> {
|
|
|
57
57
|
// Retrieve captured env vars from the global sniffer
|
|
58
58
|
const sniffer = globalThis.__envSniffer;
|
|
59
59
|
const envVars = sniffer ? sniffer.getEnvironmentVariables() : [];
|
|
60
|
+
const optionalEnvVars = sniffer ? sniffer.getOptionalVariables() : [];
|
|
60
61
|
|
|
61
62
|
// Output result as JSON
|
|
62
|
-
console.log(JSON.stringify({ envVars, error }));
|
|
63
|
+
console.log(JSON.stringify({ envVars, optionalEnvVars, error }));
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
// Handle unhandled rejections (fire-and-forget promises)
|