@geekmidas/cli 0.53.0 → 0.54.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/dist/index.cjs +63 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +63 -6
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/deploy/__tests__/__fixtures__/route-apps/endpoints/auth.ts +16 -0
- package/src/deploy/__tests__/__fixtures__/route-apps/endpoints/health.ts +13 -0
- package/src/deploy/__tests__/__fixtures__/route-apps/endpoints/users.ts +15 -0
- package/src/deploy/__tests__/__fixtures__/route-apps/services.ts +46 -0
- package/src/deploy/__tests__/env-resolver.spec.ts +5 -3
- package/src/deploy/__tests__/sniffer.spec.ts +174 -0
- package/src/deploy/sniffer.ts +107 -4
- package/src/docker/templates.ts +3 -3
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geekmidas/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.54.0",
|
|
4
4
|
"description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -49,11 +49,11 @@
|
|
|
49
49
|
"openapi-typescript": "^7.4.2",
|
|
50
50
|
"pg": "~8.17.1",
|
|
51
51
|
"prompts": "~2.4.2",
|
|
52
|
+
"@geekmidas/constructs": "~0.9.0",
|
|
52
53
|
"@geekmidas/envkit": "~0.7.0",
|
|
53
54
|
"@geekmidas/errors": "~0.1.0",
|
|
54
|
-
"@geekmidas/
|
|
55
|
-
"@geekmidas/
|
|
56
|
-
"@geekmidas/schema": "~0.1.0"
|
|
55
|
+
"@geekmidas/schema": "~0.1.0",
|
|
56
|
+
"@geekmidas/logger": "~0.4.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@types/lodash.kebabcase": "^4.1.9",
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test endpoint with multiple services.
|
|
3
|
+
* getEnvironment() should return ['AUTH_SECRET', 'AUTH_URL', 'DATABASE_URL', 'DB_POOL_SIZE'].
|
|
4
|
+
*/
|
|
5
|
+
import { e } from '@geekmidas/constructs/endpoints';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { authService, databaseService } from '../services';
|
|
8
|
+
|
|
9
|
+
export const login = e
|
|
10
|
+
.services([databaseService, authService])
|
|
11
|
+
.post('/auth/login')
|
|
12
|
+
.body(z.object({ email: z.string(), password: z.string() }))
|
|
13
|
+
.output(z.object({ token: z.string() }))
|
|
14
|
+
.handle(async () => {
|
|
15
|
+
return { token: 'test-token' };
|
|
16
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test endpoint without any services.
|
|
3
|
+
* getEnvironment() should return [].
|
|
4
|
+
*/
|
|
5
|
+
import { e } from '@geekmidas/constructs/endpoints';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
|
|
8
|
+
export const healthCheck = e
|
|
9
|
+
.get('/health')
|
|
10
|
+
.output(z.object({ status: z.string() }))
|
|
11
|
+
.handle(async () => {
|
|
12
|
+
return { status: 'ok' };
|
|
13
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test endpoint with database service.
|
|
3
|
+
* getEnvironment() should return ['DATABASE_URL', 'DB_POOL_SIZE'].
|
|
4
|
+
*/
|
|
5
|
+
import { e } from '@geekmidas/constructs/endpoints';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { databaseService } from '../services';
|
|
8
|
+
|
|
9
|
+
export const getUsers = e
|
|
10
|
+
.services([databaseService])
|
|
11
|
+
.get('/users')
|
|
12
|
+
.output(z.array(z.object({ id: z.string(), name: z.string() })))
|
|
13
|
+
.handle(async () => {
|
|
14
|
+
return [{ id: '1', name: 'Test User' }];
|
|
15
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test services for route-based app sniffing fixtures.
|
|
3
|
+
* These services access environment variables via envParser.create().
|
|
4
|
+
*/
|
|
5
|
+
import type { Service } from '@geekmidas/services';
|
|
6
|
+
|
|
7
|
+
// Database service - requires DATABASE_URL
|
|
8
|
+
export const databaseService = {
|
|
9
|
+
serviceName: 'database' as const,
|
|
10
|
+
async register({ envParser }) {
|
|
11
|
+
const config = envParser
|
|
12
|
+
.create((get: any) => ({
|
|
13
|
+
url: get('DATABASE_URL').string(),
|
|
14
|
+
poolSize: get('DB_POOL_SIZE').string().transform(Number).optional(),
|
|
15
|
+
}))
|
|
16
|
+
.parse();
|
|
17
|
+
return { url: config.url };
|
|
18
|
+
},
|
|
19
|
+
} satisfies Service<'database', { url: string }>;
|
|
20
|
+
|
|
21
|
+
// Cache service - requires REDIS_URL
|
|
22
|
+
export const cacheService = {
|
|
23
|
+
serviceName: 'cache' as const,
|
|
24
|
+
async register({ envParser }) {
|
|
25
|
+
const config = envParser
|
|
26
|
+
.create((get: any) => ({
|
|
27
|
+
url: get('REDIS_URL').string(),
|
|
28
|
+
}))
|
|
29
|
+
.parse();
|
|
30
|
+
return { url: config.url };
|
|
31
|
+
},
|
|
32
|
+
} satisfies Service<'cache', { url: string }>;
|
|
33
|
+
|
|
34
|
+
// Auth service - requires AUTH_SECRET and AUTH_URL
|
|
35
|
+
export const authService = {
|
|
36
|
+
serviceName: 'auth' as const,
|
|
37
|
+
async register({ envParser }) {
|
|
38
|
+
const config = envParser
|
|
39
|
+
.create((get: any) => ({
|
|
40
|
+
secret: get('AUTH_SECRET').string(),
|
|
41
|
+
url: get('AUTH_URL').string(),
|
|
42
|
+
}))
|
|
43
|
+
.parse();
|
|
44
|
+
return { secret: config.secret, url: config.url };
|
|
45
|
+
},
|
|
46
|
+
} satisfies Service<'auth', { secret: string; url: string }>;
|
|
@@ -191,15 +191,17 @@ describe('resolveEnvVar', () => {
|
|
|
191
191
|
expect(resolveEnvVar('PORT', context)).toBe('8080');
|
|
192
192
|
});
|
|
193
193
|
|
|
194
|
-
it('should resolve NODE_ENV
|
|
194
|
+
it('should resolve NODE_ENV to production for all stages (deployed apps)', () => {
|
|
195
|
+
// NODE_ENV is always 'production' for deployed apps
|
|
196
|
+
// gkm dev handles development mode separately
|
|
195
197
|
expect(resolveEnvVar('NODE_ENV', createContext({ stage: 'production' }))).toBe(
|
|
196
198
|
'production',
|
|
197
199
|
);
|
|
198
200
|
expect(resolveEnvVar('NODE_ENV', createContext({ stage: 'staging' }))).toBe(
|
|
199
|
-
'
|
|
201
|
+
'production',
|
|
200
202
|
);
|
|
201
203
|
expect(resolveEnvVar('NODE_ENV', createContext({ stage: 'development' }))).toBe(
|
|
202
|
-
'
|
|
204
|
+
'production',
|
|
203
205
|
);
|
|
204
206
|
});
|
|
205
207
|
|
|
@@ -5,6 +5,7 @@ import type { NormalizedAppConfig } from '../../workspace/types';
|
|
|
5
5
|
import {
|
|
6
6
|
_sniffEntryFile,
|
|
7
7
|
_sniffEnvParser,
|
|
8
|
+
_sniffRouteFiles,
|
|
8
9
|
sniffAllApps,
|
|
9
10
|
sniffAppEnvironment,
|
|
10
11
|
} from '../sniffer';
|
|
@@ -13,6 +14,7 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
13
14
|
const __dirname = dirname(__filename);
|
|
14
15
|
const fixturesPath = resolve(__dirname, '__fixtures__/entry-apps');
|
|
15
16
|
const envParserFixturesPath = resolve(__dirname, '__fixtures__/env-parsers');
|
|
17
|
+
const routeAppsFixturesPath = resolve(__dirname, '__fixtures__/route-apps');
|
|
16
18
|
|
|
17
19
|
describe('sniffAppEnvironment', () => {
|
|
18
20
|
const workspacePath = '/test/workspace';
|
|
@@ -544,3 +546,175 @@ describe('sniffAppEnvironment with envParser apps', () => {
|
|
|
544
546
|
expect(result.requiredEnvVars).toEqual([]);
|
|
545
547
|
});
|
|
546
548
|
});
|
|
549
|
+
|
|
550
|
+
describe('route files sniffing via _sniffRouteFiles', () => {
|
|
551
|
+
// These tests verify the route-based sniffing for apps with routes config.
|
|
552
|
+
// Each test uses fixture files that export endpoints with services.
|
|
553
|
+
|
|
554
|
+
it('should sniff environment variables from endpoint with single service', async () => {
|
|
555
|
+
const result = await _sniffRouteFiles(
|
|
556
|
+
'./endpoints/users.ts',
|
|
557
|
+
routeAppsFixturesPath,
|
|
558
|
+
routeAppsFixturesPath,
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
expect(result.envVars).toContain('DATABASE_URL');
|
|
562
|
+
// DB_POOL_SIZE is optional, may or may not be captured
|
|
563
|
+
expect(result.error).toBeUndefined();
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it('should sniff environment variables from endpoint with multiple services', async () => {
|
|
567
|
+
const result = await _sniffRouteFiles(
|
|
568
|
+
'./endpoints/auth.ts',
|
|
569
|
+
routeAppsFixturesPath,
|
|
570
|
+
routeAppsFixturesPath,
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
expect(result.envVars).toContain('DATABASE_URL');
|
|
574
|
+
expect(result.envVars).toContain('AUTH_SECRET');
|
|
575
|
+
expect(result.envVars).toContain('AUTH_URL');
|
|
576
|
+
expect(result.error).toBeUndefined();
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
it('should return empty for endpoint without services', async () => {
|
|
580
|
+
const result = await _sniffRouteFiles(
|
|
581
|
+
'./endpoints/health.ts',
|
|
582
|
+
routeAppsFixturesPath,
|
|
583
|
+
routeAppsFixturesPath,
|
|
584
|
+
);
|
|
585
|
+
|
|
586
|
+
expect(result.envVars).toEqual([]);
|
|
587
|
+
expect(result.error).toBeUndefined();
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
it('should sniff all endpoints matching glob pattern', async () => {
|
|
591
|
+
const result = await _sniffRouteFiles(
|
|
592
|
+
'./endpoints/**/*.ts',
|
|
593
|
+
routeAppsFixturesPath,
|
|
594
|
+
routeAppsFixturesPath,
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
// Should capture env vars from all endpoints
|
|
598
|
+
expect(result.envVars).toContain('DATABASE_URL');
|
|
599
|
+
expect(result.envVars).toContain('AUTH_SECRET');
|
|
600
|
+
expect(result.envVars).toContain('AUTH_URL');
|
|
601
|
+
expect(result.error).toBeUndefined();
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it('should return empty for non-existent pattern', async () => {
|
|
605
|
+
const result = await _sniffRouteFiles(
|
|
606
|
+
'./nonexistent/**/*.ts',
|
|
607
|
+
routeAppsFixturesPath,
|
|
608
|
+
routeAppsFixturesPath,
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
expect(result.envVars).toEqual([]);
|
|
612
|
+
expect(result.error).toBeUndefined();
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it('should handle array of patterns', async () => {
|
|
616
|
+
const result = await _sniffRouteFiles(
|
|
617
|
+
['./endpoints/users.ts', './endpoints/health.ts'],
|
|
618
|
+
routeAppsFixturesPath,
|
|
619
|
+
routeAppsFixturesPath,
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
expect(result.envVars).toContain('DATABASE_URL');
|
|
623
|
+
expect(result.error).toBeUndefined();
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
it('should deduplicate env vars from multiple endpoints using same service', async () => {
|
|
627
|
+
const result = await _sniffRouteFiles(
|
|
628
|
+
['./endpoints/users.ts', './endpoints/auth.ts'],
|
|
629
|
+
routeAppsFixturesPath,
|
|
630
|
+
routeAppsFixturesPath,
|
|
631
|
+
);
|
|
632
|
+
|
|
633
|
+
// DATABASE_URL is used by both endpoints, should only appear once
|
|
634
|
+
const databaseUrlCount = result.envVars.filter(
|
|
635
|
+
(v) => v === 'DATABASE_URL',
|
|
636
|
+
).length;
|
|
637
|
+
expect(databaseUrlCount).toBe(1);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it('should return sorted env vars', async () => {
|
|
641
|
+
const result = await _sniffRouteFiles(
|
|
642
|
+
'./endpoints/**/*.ts',
|
|
643
|
+
routeAppsFixturesPath,
|
|
644
|
+
routeAppsFixturesPath,
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
const sorted = [...result.envVars].sort();
|
|
648
|
+
expect(result.envVars).toEqual(sorted);
|
|
649
|
+
});
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
describe('sniffAppEnvironment with route-based apps', () => {
|
|
653
|
+
// Integration tests for sniffAppEnvironment with route-based apps
|
|
654
|
+
|
|
655
|
+
it('should use route sniffing for apps with routes config', async () => {
|
|
656
|
+
const app: NormalizedAppConfig = {
|
|
657
|
+
type: 'backend',
|
|
658
|
+
path: routeAppsFixturesPath,
|
|
659
|
+
port: 3000,
|
|
660
|
+
dependencies: [],
|
|
661
|
+
resolvedDeployTarget: 'dokploy',
|
|
662
|
+
routes: './endpoints/**/*.ts',
|
|
663
|
+
envParser: './src/config/env#envParser', // Should be ignored when routes exist
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
const result = await sniffAppEnvironment(
|
|
667
|
+
app,
|
|
668
|
+
'api',
|
|
669
|
+
routeAppsFixturesPath,
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
expect(result.appName).toBe('api');
|
|
673
|
+
expect(result.requiredEnvVars).toContain('DATABASE_URL');
|
|
674
|
+
expect(result.requiredEnvVars).toContain('AUTH_SECRET');
|
|
675
|
+
expect(result.requiredEnvVars).toContain('AUTH_URL');
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
it('should prefer requiredEnv over route sniffing', async () => {
|
|
679
|
+
const app: NormalizedAppConfig = {
|
|
680
|
+
type: 'backend',
|
|
681
|
+
path: routeAppsFixturesPath,
|
|
682
|
+
port: 3000,
|
|
683
|
+
dependencies: [],
|
|
684
|
+
resolvedDeployTarget: 'dokploy',
|
|
685
|
+
routes: './endpoints/**/*.ts',
|
|
686
|
+
requiredEnv: ['CUSTOM_VAR'], // Should use this instead
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
const result = await sniffAppEnvironment(
|
|
690
|
+
app,
|
|
691
|
+
'api',
|
|
692
|
+
routeAppsFixturesPath,
|
|
693
|
+
);
|
|
694
|
+
|
|
695
|
+
expect(result.requiredEnvVars).toEqual(['CUSTOM_VAR']);
|
|
696
|
+
// Should NOT contain the sniffed vars
|
|
697
|
+
expect(result.requiredEnvVars).not.toContain('DATABASE_URL');
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
it('should handle route pattern that matches no files', async () => {
|
|
701
|
+
const app: NormalizedAppConfig = {
|
|
702
|
+
type: 'backend',
|
|
703
|
+
path: routeAppsFixturesPath,
|
|
704
|
+
port: 3000,
|
|
705
|
+
dependencies: [],
|
|
706
|
+
resolvedDeployTarget: 'dokploy',
|
|
707
|
+
routes: './nonexistent/**/*.ts',
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
const result = await sniffAppEnvironment(
|
|
711
|
+
app,
|
|
712
|
+
'api',
|
|
713
|
+
routeAppsFixturesPath,
|
|
714
|
+
{ logWarnings: false },
|
|
715
|
+
);
|
|
716
|
+
|
|
717
|
+
expect(result.appName).toBe('api');
|
|
718
|
+
expect(result.requiredEnvVars).toEqual([]);
|
|
719
|
+
});
|
|
720
|
+
});
|
package/src/deploy/sniffer.ts
CHANGED
|
@@ -2,12 +2,30 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { dirname, resolve } from 'node:path';
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
|
+
import type { Construct } from '@geekmidas/constructs';
|
|
6
|
+
import { Endpoint } from '@geekmidas/constructs/endpoints';
|
|
7
|
+
import { Function as GkmFunction } from '@geekmidas/constructs/functions';
|
|
8
|
+
import { Cron } from '@geekmidas/constructs/crons';
|
|
9
|
+
import { Subscriber } from '@geekmidas/constructs/subscribers';
|
|
5
10
|
import type { SniffResult } from '@geekmidas/envkit/sniffer';
|
|
11
|
+
import fg from 'fast-glob';
|
|
6
12
|
import type { NormalizedAppConfig } from '../workspace/types.js';
|
|
7
13
|
|
|
8
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
15
|
const __dirname = dirname(__filename);
|
|
10
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Check if a value is a gkm construct (Endpoint, Function, Cron, or Subscriber).
|
|
19
|
+
*/
|
|
20
|
+
function isConstruct(value: unknown): value is Construct {
|
|
21
|
+
return (
|
|
22
|
+
Endpoint.isEndpoint(value) ||
|
|
23
|
+
GkmFunction.isFunction(value) ||
|
|
24
|
+
Cron.isCron(value) ||
|
|
25
|
+
Subscriber.isSubscriber(value)
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
11
29
|
/**
|
|
12
30
|
* Resolve the path to a sniffer helper file.
|
|
13
31
|
* Handles both dev (.ts with tsx) and production (.mjs from dist).
|
|
@@ -67,8 +85,9 @@ export interface SniffAppOptions {
|
|
|
67
85
|
* 1. Frontend apps: Returns empty (no server secrets)
|
|
68
86
|
* 2. Apps with `requiredEnv`: Uses explicit list from config
|
|
69
87
|
* 3. Entry apps: Imports entry file in subprocess to capture config.parse() calls
|
|
70
|
-
* 4.
|
|
71
|
-
* 5. Apps with
|
|
88
|
+
* 4. Route-based apps: Loads route files and calls getEnvironment() on each construct
|
|
89
|
+
* 5. Apps with `envParser` (no routes): Runs SnifferEnvironmentParser to detect usage
|
|
90
|
+
* 6. Apps with neither: Returns empty
|
|
72
91
|
*
|
|
73
92
|
* This function handles "fire and forget" async operations gracefully,
|
|
74
93
|
* capturing errors and unhandled rejections without failing the build.
|
|
@@ -110,7 +129,20 @@ export async function sniffAppEnvironment(
|
|
|
110
129
|
return { appName, requiredEnvVars: result.envVars };
|
|
111
130
|
}
|
|
112
131
|
|
|
113
|
-
// 4.
|
|
132
|
+
// 4. Route-based apps - load routes and call getEnvironment() on each construct
|
|
133
|
+
if (app.routes) {
|
|
134
|
+
const result = await sniffRouteFiles(app.routes, app.path, workspacePath);
|
|
135
|
+
|
|
136
|
+
if (logWarnings && result.error) {
|
|
137
|
+
console.warn(
|
|
138
|
+
`[sniffer] ${appName}: Route sniffing threw error (env vars still captured): ${result.error.message}`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { appName, requiredEnvVars: result.envVars };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 5. Apps with envParser but no routes - run sniffer to detect env var usage
|
|
114
146
|
if (app.envParser) {
|
|
115
147
|
const result = await sniffEnvParser(app.envParser, app.path, workspacePath);
|
|
116
148
|
|
|
@@ -232,6 +264,73 @@ async function sniffEntryFile(
|
|
|
232
264
|
});
|
|
233
265
|
}
|
|
234
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Sniff route files by loading constructs and calling getEnvironment().
|
|
269
|
+
*
|
|
270
|
+
* Route-based apps have endpoints, functions, crons, and subscribers that
|
|
271
|
+
* use services. Each service's register() method accesses environment variables.
|
|
272
|
+
* This function mimics what the bundler does during build to capture those vars.
|
|
273
|
+
*
|
|
274
|
+
* @param routes - Glob pattern(s) for route files
|
|
275
|
+
* @param appPath - The app's path relative to workspace (e.g., 'apps/api')
|
|
276
|
+
* @param workspacePath - Absolute path to workspace root
|
|
277
|
+
* @returns EntrySniffResult with env vars and optional error
|
|
278
|
+
*/
|
|
279
|
+
async function sniffRouteFiles(
|
|
280
|
+
routes: string | string[],
|
|
281
|
+
appPath: string,
|
|
282
|
+
workspacePath: string,
|
|
283
|
+
): Promise<EntrySniffResult> {
|
|
284
|
+
const fullAppPath = resolve(workspacePath, appPath);
|
|
285
|
+
const patterns = Array.isArray(routes) ? routes : [routes];
|
|
286
|
+
|
|
287
|
+
const envVars = new Set<string>();
|
|
288
|
+
let error: Error | undefined;
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
// Find all route files matching the patterns
|
|
292
|
+
const files = await fg(patterns, {
|
|
293
|
+
cwd: fullAppPath,
|
|
294
|
+
absolute: true,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Import each file and find constructs
|
|
298
|
+
for (const file of files) {
|
|
299
|
+
try {
|
|
300
|
+
const module = await import(file);
|
|
301
|
+
|
|
302
|
+
// Check all exports for constructs
|
|
303
|
+
for (const [, exportValue] of Object.entries(module)) {
|
|
304
|
+
if (isConstruct(exportValue)) {
|
|
305
|
+
// Call getEnvironment() to capture env vars from services
|
|
306
|
+
try {
|
|
307
|
+
const constructEnvVars = await exportValue.getEnvironment();
|
|
308
|
+
constructEnvVars.forEach((v) => envVars.add(v));
|
|
309
|
+
} catch (e) {
|
|
310
|
+
// Individual construct may fail, continue with others
|
|
311
|
+
console.warn(
|
|
312
|
+
`[sniffer] Failed to get environment for construct in ${file}: ${e instanceof Error ? e.message : String(e)}`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
} catch (e) {
|
|
318
|
+
// Individual file import may fail, continue with others
|
|
319
|
+
console.warn(
|
|
320
|
+
`[sniffer] Failed to import ${file}: ${e instanceof Error ? e.message : String(e)}`,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
} catch (e) {
|
|
325
|
+
error = e instanceof Error ? e : new Error(String(e));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
envVars: Array.from(envVars).sort(),
|
|
330
|
+
error,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
235
334
|
/**
|
|
236
335
|
* Run the SnifferEnvironmentParser on an envParser module to detect
|
|
237
336
|
* which environment variables it accesses.
|
|
@@ -333,4 +432,8 @@ export async function sniffAllApps(
|
|
|
333
432
|
}
|
|
334
433
|
|
|
335
434
|
// Export for testing
|
|
336
|
-
export {
|
|
435
|
+
export {
|
|
436
|
+
sniffEnvParser as _sniffEnvParser,
|
|
437
|
+
sniffEntryFile as _sniffEntryFile,
|
|
438
|
+
sniffRouteFiles as _sniffRouteFiles,
|
|
439
|
+
};
|
package/src/docker/templates.ts
CHANGED
|
@@ -293,7 +293,7 @@ WORKDIR /app
|
|
|
293
293
|
COPY . .
|
|
294
294
|
|
|
295
295
|
# Build production server using gkm
|
|
296
|
-
RUN
|
|
296
|
+
RUN ${pm.exec} gkm build --provider server --production
|
|
297
297
|
|
|
298
298
|
# Stage 3: Production
|
|
299
299
|
FROM ${baseImage} AS runner
|
|
@@ -384,7 +384,7 @@ WORKDIR /app
|
|
|
384
384
|
COPY --from=pruner /app/out/full/ ./
|
|
385
385
|
|
|
386
386
|
# Build production server using gkm
|
|
387
|
-
RUN
|
|
387
|
+
RUN ${pm.exec} gkm build --provider server --production
|
|
388
388
|
|
|
389
389
|
# Stage 4: Production
|
|
390
390
|
FROM ${baseImage} AS runner
|
|
@@ -756,7 +756,7 @@ RUN if [ -n "$GKM_ENCRYPTED_CREDENTIALS" ]; then \
|
|
|
756
756
|
fi
|
|
757
757
|
|
|
758
758
|
# Build production server using gkm
|
|
759
|
-
RUN cd ${appPath} &&
|
|
759
|
+
RUN cd ${appPath} && ${pm.exec} gkm build --provider server --production
|
|
760
760
|
|
|
761
761
|
# Stage 4: Production
|
|
762
762
|
FROM ${baseImage} AS runner
|