@geekmidas/cli 1.10.30 → 1.10.32
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 +19 -0
- package/bin/gkm.mjs +1 -1
- 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 +1 -1
- 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/{index-BkibYzso.d.cts → index-C3t5VL4R.d.cts} +2 -32
- package/dist/index-C3t5VL4R.d.cts.map +1 -0
- package/dist/{index-CY-ieuRp.d.mts → index-Dt_dZ7K4.d.mts} +2 -32
- package/dist/index-Dt_dZ7K4.d.mts.map +1 -0
- package/dist/index.cjs +65 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +65 -26
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-ClA8lIhO.cjs → openapi-BQft1jm8.cjs} +5 -22
- package/dist/openapi-BQft1jm8.cjs.map +1 -0
- package/dist/{openapi-CTARkkkn.mjs → openapi-DE8b-vin.mjs} +6 -23
- package/dist/openapi-DE8b-vin.mjs.map +1 -0
- package/dist/openapi.cjs +3 -3
- package/dist/openapi.mjs +3 -3
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +2 -2
- 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 +4 -4
- package/src/__tests__/openapi.spec.ts +0 -315
- package/src/build/__tests__/manifests.spec.ts +66 -0
- package/src/build/index.ts +12 -1
- package/src/build/types.ts +5 -0
- package/src/deploy/__tests__/__fixtures__/env-parsers/optional-env-parser.ts +21 -0
- package/src/deploy/__tests__/index.spec.ts +1 -3
- 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/CronGenerator.ts +3 -1
- package/src/generators/EndpointGenerator.ts +6 -2
- package/src/generators/FunctionGenerator.ts +3 -1
- package/src/generators/SubscriberGenerator.ts +3 -1
- package/src/index.ts +8 -0
- package/src/init/generators/docker.ts +2 -2
- package/src/init/generators/monorepo.ts +15 -1
- package/src/init/index.ts +2 -4
- package/src/init/versions.ts +2 -2
- 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/index-BkibYzso.d.cts.map +0 -1
- package/dist/index-CY-ieuRp.d.mts.map +0 -1
- package/dist/openapi-CTARkkkn.mjs.map +0 -1
- package/dist/openapi-ClA8lIhO.cjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geekmidas/cli",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.32",
|
|
4
4
|
"description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -56,10 +56,10 @@
|
|
|
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.8",
|
|
60
|
+
"@geekmidas/envkit": "~1.0.5",
|
|
61
61
|
"@geekmidas/errors": "~1.0.0",
|
|
62
|
-
"@geekmidas/logger": "~1.0.
|
|
62
|
+
"@geekmidas/logger": "~1.0.1",
|
|
63
63
|
"@geekmidas/schema": "~1.0.0"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
@@ -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,
|
|
@@ -157,6 +158,7 @@ export async function buildCommand(
|
|
|
157
158
|
hooks,
|
|
158
159
|
production,
|
|
159
160
|
dockerServices,
|
|
161
|
+
markOptional: options.markOptional ?? false,
|
|
160
162
|
};
|
|
161
163
|
|
|
162
164
|
// Initialize generators
|
|
@@ -219,6 +221,10 @@ export async function buildCommand(
|
|
|
219
221
|
result = providerResult;
|
|
220
222
|
}
|
|
221
223
|
}
|
|
224
|
+
|
|
225
|
+
// Generate OpenAPI spec as part of the build
|
|
226
|
+
await generateOpenApi(config, { bustCache: true });
|
|
227
|
+
|
|
222
228
|
return result;
|
|
223
229
|
}
|
|
224
230
|
|
|
@@ -245,7 +251,9 @@ async function buildForProvider(
|
|
|
245
251
|
|
|
246
252
|
logger.log(`\nGenerating handlers for provider: ${provider}`);
|
|
247
253
|
|
|
248
|
-
// Build all constructs in parallel
|
|
254
|
+
// Build all constructs in parallel.
|
|
255
|
+
// context.markOptional is forwarded to each generator so that
|
|
256
|
+
// getEnvironment({ markOptional }) produces `VARNAME?` for optional vars.
|
|
249
257
|
const [routes, functionInfos, cronInfos, subscriberInfos] = await Promise.all(
|
|
250
258
|
[
|
|
251
259
|
endpointGenerator.build(context, endpoints, outputDir, {
|
|
@@ -472,6 +480,9 @@ export async function workspaceBuildCommand(
|
|
|
472
480
|
|
|
473
481
|
logger.log(`\n✅ Workspace build complete!`);
|
|
474
482
|
|
|
483
|
+
// Generate OpenAPI specs and copy to frontend apps
|
|
484
|
+
await openapiCommand({ cwd: workspace.root });
|
|
485
|
+
|
|
475
486
|
// Summary
|
|
476
487
|
logger.log(`\n📋 Build Summary:`);
|
|
477
488
|
for (const result of results) {
|
package/src/build/types.ts
CHANGED
|
@@ -90,6 +90,11 @@ export interface BuildContext {
|
|
|
90
90
|
redis?: boolean;
|
|
91
91
|
rabbitmq?: boolean;
|
|
92
92
|
};
|
|
93
|
+
/**
|
|
94
|
+
* When true, optional env vars get a `?` suffix in each construct's
|
|
95
|
+
* environment array (e.g. `PORT?` instead of `PORT`).
|
|
96
|
+
*/
|
|
97
|
+
markOptional?: boolean;
|
|
93
98
|
}
|
|
94
99
|
|
|
95
100
|
export interface ProviderBuildResult {
|
|
@@ -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;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { HttpResponse, http } from 'msw';
|
|
2
2
|
import { setupServer } from 'msw/node';
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { getAppBuildOrder } from '../../workspace/index.js';
|
|
4
5
|
import type { NormalizedWorkspace } from '../../workspace/types.js';
|
|
5
6
|
import { DokployApi } from '../dokploy-api';
|
|
6
7
|
import {
|
|
@@ -536,8 +537,6 @@ describe('workspaceDeployCommand', () => {
|
|
|
536
537
|
},
|
|
537
538
|
});
|
|
538
539
|
|
|
539
|
-
// Import getAppBuildOrder to verify ordering
|
|
540
|
-
const { getAppBuildOrder } = require('../../workspace/index.js');
|
|
541
540
|
const order = getAppBuildOrder(workspace);
|
|
542
541
|
|
|
543
542
|
// api and auth should come before web
|
|
@@ -577,7 +576,6 @@ describe('workspaceDeployCommand', () => {
|
|
|
577
576
|
},
|
|
578
577
|
});
|
|
579
578
|
|
|
580
|
-
const { getAppBuildOrder } = require('../../workspace/index.js');
|
|
581
579
|
const order = getAppBuildOrder(workspace);
|
|
582
580
|
|
|
583
581
|
// db -> api -> web
|
|
@@ -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)
|