@geekmidas/cli 0.48.0 → 0.49.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/{dokploy-api-DvzIDxTj.mjs → dokploy-api-94KzmTVf.mjs} +4 -4
- package/dist/dokploy-api-94KzmTVf.mjs.map +1 -0
- package/dist/dokploy-api-CItuaWTq.mjs +3 -0
- package/dist/dokploy-api-DBNE8MDt.cjs +3 -0
- package/dist/{dokploy-api-BDLu0qWi.cjs → dokploy-api-YD8WCQfW.cjs} +4 -4
- package/dist/dokploy-api-YD8WCQfW.cjs.map +1 -0
- package/dist/index.cjs +2392 -1888
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2389 -1885
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -4
- package/src/build/__tests__/handler-templates.spec.ts +947 -0
- package/src/deploy/__tests__/__fixtures__/entry-apps/async-entry.ts +24 -0
- package/src/deploy/__tests__/__fixtures__/entry-apps/nested-config-entry.ts +24 -0
- package/src/deploy/__tests__/__fixtures__/entry-apps/no-env-entry.ts +12 -0
- package/src/deploy/__tests__/__fixtures__/entry-apps/simple-entry.ts +14 -0
- package/src/deploy/__tests__/__fixtures__/entry-apps/throwing-entry.ts +16 -0
- package/src/deploy/__tests__/__fixtures__/env-parsers/non-function-export.ts +10 -0
- package/src/deploy/__tests__/__fixtures__/env-parsers/parseable-env-parser.ts +18 -0
- package/src/deploy/__tests__/__fixtures__/env-parsers/throwing-env-parser.ts +18 -0
- package/src/deploy/__tests__/__fixtures__/env-parsers/valid-env-parser.ts +16 -0
- package/src/deploy/__tests__/dns-verification.spec.ts +229 -0
- package/src/deploy/__tests__/dokploy-api.spec.ts +2 -3
- package/src/deploy/__tests__/domain.spec.ts +7 -3
- package/src/deploy/__tests__/env-resolver.spec.ts +469 -0
- package/src/deploy/__tests__/index.spec.ts +12 -12
- package/src/deploy/__tests__/secrets.spec.ts +4 -1
- package/src/deploy/__tests__/sniffer.spec.ts +326 -1
- package/src/deploy/__tests__/state.spec.ts +844 -0
- package/src/deploy/dns/hostinger-api.ts +4 -1
- package/src/deploy/dns/index.ts +113 -1
- package/src/deploy/docker.ts +1 -2
- package/src/deploy/dokploy-api.ts +18 -9
- package/src/deploy/domain.ts +5 -4
- package/src/deploy/env-resolver.ts +278 -0
- package/src/deploy/index.ts +525 -119
- package/src/deploy/secrets.ts +7 -2
- package/src/deploy/sniffer-envkit-patch.ts +43 -0
- package/src/deploy/sniffer-hooks.ts +52 -0
- package/src/deploy/sniffer-loader.ts +23 -0
- package/src/deploy/sniffer-worker.ts +74 -0
- package/src/deploy/sniffer.ts +136 -14
- package/src/deploy/state.ts +162 -1
- package/src/init/versions.ts +3 -3
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/dokploy-api-BDLu0qWi.cjs.map +0 -1
- package/dist/dokploy-api-BN3V57z1.mjs +0 -3
- package/dist/dokploy-api-BdCKjFDA.cjs +0 -3
- package/dist/dokploy-api-DvzIDxTj.mjs.map +0 -1
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entry app fixture with async initialization.
|
|
3
|
+
* Tests that env vars are captured even with async code paths.
|
|
4
|
+
*/
|
|
5
|
+
import { EnvironmentParser } from '@geekmidas/envkit';
|
|
6
|
+
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
8
|
+
const _config = new EnvironmentParser(process.env as Record<string, string>)
|
|
9
|
+
.create((get) => ({
|
|
10
|
+
port: get('PORT').string().transform(Number),
|
|
11
|
+
databaseUrl: get('DATABASE_URL').string(),
|
|
12
|
+
}))
|
|
13
|
+
.parse();
|
|
14
|
+
|
|
15
|
+
// Simulate async initialization (fire-and-forget style)
|
|
16
|
+
const init = async () => {
|
|
17
|
+
await Promise.resolve();
|
|
18
|
+
// This would normally connect to database, etc.
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Fire-and-forget promise
|
|
22
|
+
init().catch(() => {
|
|
23
|
+
// Silently ignore - common pattern in entry apps
|
|
24
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entry app fixture with nested configuration.
|
|
3
|
+
* Tests that nested env var access is tracked correctly.
|
|
4
|
+
*/
|
|
5
|
+
import { EnvironmentParser } from '@geekmidas/envkit';
|
|
6
|
+
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
8
|
+
const _config = new EnvironmentParser(process.env as Record<string, string>)
|
|
9
|
+
.create((get) => ({
|
|
10
|
+
server: {
|
|
11
|
+
port: get('PORT').string().transform(Number),
|
|
12
|
+
host: get('HOST').string(),
|
|
13
|
+
},
|
|
14
|
+
database: {
|
|
15
|
+
url: get('DATABASE_URL').string(),
|
|
16
|
+
poolSize: get('DB_POOL_SIZE').string().transform(Number),
|
|
17
|
+
},
|
|
18
|
+
auth: {
|
|
19
|
+
secret: get('BETTER_AUTH_SECRET').string(),
|
|
20
|
+
url: get('BETTER_AUTH_URL').string(),
|
|
21
|
+
trustedOrigins: get('BETTER_AUTH_TRUSTED_ORIGINS').string(),
|
|
22
|
+
},
|
|
23
|
+
}))
|
|
24
|
+
.parse();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple entry app fixture that uses @geekmidas/envkit.
|
|
3
|
+
* Used for testing entry app sniffing via subprocess.
|
|
4
|
+
*/
|
|
5
|
+
import { EnvironmentParser } from '@geekmidas/envkit';
|
|
6
|
+
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
8
|
+
const _config = new EnvironmentParser(process.env as Record<string, string>)
|
|
9
|
+
.create((get) => ({
|
|
10
|
+
port: get('PORT').string().transform(Number),
|
|
11
|
+
databaseUrl: get('DATABASE_URL').string(),
|
|
12
|
+
redisUrl: get('REDIS_URL').string(),
|
|
13
|
+
}))
|
|
14
|
+
.parse();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entry app fixture that throws during initialization.
|
|
3
|
+
* Tests that env vars are still captured even when the entry throws.
|
|
4
|
+
*/
|
|
5
|
+
import { EnvironmentParser } from '@geekmidas/envkit';
|
|
6
|
+
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
8
|
+
const _config = new EnvironmentParser(process.env as Record<string, string>)
|
|
9
|
+
.create((get) => ({
|
|
10
|
+
port: get('PORT').string().transform(Number),
|
|
11
|
+
apiKey: get('API_KEY').string(),
|
|
12
|
+
}))
|
|
13
|
+
.parse();
|
|
14
|
+
|
|
15
|
+
// Throw after env vars are accessed
|
|
16
|
+
throw new Error('Initialization failed: Missing required configuration');
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A fixture that exports a non-function for testing error handling.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Export a string instead of a function
|
|
6
|
+
export const envParser = 'not a function';
|
|
7
|
+
|
|
8
|
+
// Default export is also not a function
|
|
9
|
+
const defaultValue = { notAFunction: true };
|
|
10
|
+
export default defaultValue;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An envParser that returns a config with a parse() method.
|
|
3
|
+
*/
|
|
4
|
+
import type { SnifferEnvironmentParser } from '@geekmidas/envkit/sniffer';
|
|
5
|
+
|
|
6
|
+
export function envParser(parser: SnifferEnvironmentParser) {
|
|
7
|
+
// Create a config that will fail on parse() due to missing env vars
|
|
8
|
+
// This tests the try/catch around result.parse()
|
|
9
|
+
const config = parser.create((get) => ({
|
|
10
|
+
port: get('PORT').string(),
|
|
11
|
+
databaseUrl: get('DATABASE_URL').string(),
|
|
12
|
+
apiKey: get('API_KEY').string(),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
return config;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default envParser;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An envParser that accesses some env vars then throws.
|
|
3
|
+
*/
|
|
4
|
+
import type { SnifferEnvironmentParser } from '@geekmidas/envkit/sniffer';
|
|
5
|
+
|
|
6
|
+
export function envParser(parser: SnifferEnvironmentParser) {
|
|
7
|
+
const config = parser.create((get) => ({
|
|
8
|
+
port: get('PORT').string(),
|
|
9
|
+
apiKey: get('API_KEY').string(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
// Throw after creating the parser but before returning
|
|
13
|
+
throw new Error('EnvParser initialization failed');
|
|
14
|
+
|
|
15
|
+
return config;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default envParser;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A valid envParser fixture that returns a parseable config.
|
|
3
|
+
*/
|
|
4
|
+
import type { SnifferEnvironmentParser } from '@geekmidas/envkit/sniffer';
|
|
5
|
+
|
|
6
|
+
export function envParser(parser: SnifferEnvironmentParser) {
|
|
7
|
+
return parser.create((get) => ({
|
|
8
|
+
port: get('PORT').string(),
|
|
9
|
+
database: {
|
|
10
|
+
url: get('DATABASE_URL').string(),
|
|
11
|
+
poolSize: get('DB_POOL_SIZE').string(),
|
|
12
|
+
},
|
|
13
|
+
}));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default envParser;
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { createEmptyState, type DokployStageState } from '../state';
|
|
3
|
+
|
|
4
|
+
// Mock dns/promises lookup
|
|
5
|
+
vi.mock('node:dns/promises', () => ({
|
|
6
|
+
lookup: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
// Import after mocking
|
|
10
|
+
import { lookup } from 'node:dns/promises';
|
|
11
|
+
import { verifyDnsRecords, resolveHostnameToIp } from '../dns/index';
|
|
12
|
+
|
|
13
|
+
describe('resolveHostnameToIp', () => {
|
|
14
|
+
const mockLookup = vi.mocked(lookup);
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.clearAllMocks();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should resolve hostname to IPv4 address', async () => {
|
|
21
|
+
mockLookup.mockResolvedValue({ address: '1.2.3.4', family: 4 });
|
|
22
|
+
|
|
23
|
+
const ip = await resolveHostnameToIp('example.com');
|
|
24
|
+
|
|
25
|
+
expect(ip).toBe('1.2.3.4');
|
|
26
|
+
expect(mockLookup).toHaveBeenCalledWith('example.com', { family: 4 });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should throw error when resolution fails', async () => {
|
|
30
|
+
mockLookup.mockRejectedValue(new Error('NXDOMAIN'));
|
|
31
|
+
|
|
32
|
+
await expect(resolveHostnameToIp('invalid.example.com')).rejects.toThrow(
|
|
33
|
+
'Failed to resolve IP for invalid.example.com: NXDOMAIN',
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('verifyDnsRecords', () => {
|
|
39
|
+
const mockLookup = vi.mocked(lookup);
|
|
40
|
+
let state: DokployStageState;
|
|
41
|
+
|
|
42
|
+
// Suppress console.log during tests
|
|
43
|
+
const originalLog = console.log;
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
vi.clearAllMocks();
|
|
47
|
+
state = createEmptyState('production', 'env-123');
|
|
48
|
+
console.log = vi.fn();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
console.log = originalLog;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should verify DNS records that resolve correctly', async () => {
|
|
56
|
+
mockLookup.mockResolvedValue({ address: '1.2.3.4', family: 4 });
|
|
57
|
+
|
|
58
|
+
const appHostnames = new Map([
|
|
59
|
+
['api', 'api.example.com'],
|
|
60
|
+
['auth', 'auth.example.com'],
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
const results = await verifyDnsRecords(appHostnames, '1.2.3.4', state);
|
|
64
|
+
|
|
65
|
+
expect(results).toHaveLength(2);
|
|
66
|
+
expect(results[0]).toMatchObject({
|
|
67
|
+
hostname: 'api.example.com',
|
|
68
|
+
appName: 'api',
|
|
69
|
+
verified: true,
|
|
70
|
+
resolvedIp: '1.2.3.4',
|
|
71
|
+
expectedIp: '1.2.3.4',
|
|
72
|
+
});
|
|
73
|
+
expect(results[1]).toMatchObject({
|
|
74
|
+
hostname: 'auth.example.com',
|
|
75
|
+
appName: 'auth',
|
|
76
|
+
verified: true,
|
|
77
|
+
resolvedIp: '1.2.3.4',
|
|
78
|
+
expectedIp: '1.2.3.4',
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Should have stored verification in state
|
|
82
|
+
expect(state.dnsVerified).toBeDefined();
|
|
83
|
+
expect(state.dnsVerified!['api.example.com']?.serverIp).toBe('1.2.3.4');
|
|
84
|
+
expect(state.dnsVerified!['auth.example.com']?.serverIp).toBe('1.2.3.4');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should skip verification for already-verified hostnames', async () => {
|
|
88
|
+
// Pre-populate state with verified hostname
|
|
89
|
+
state.dnsVerified = {
|
|
90
|
+
'api.example.com': {
|
|
91
|
+
serverIp: '1.2.3.4',
|
|
92
|
+
verifiedAt: '2024-01-01T00:00:00.000Z',
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const appHostnames = new Map([['api', 'api.example.com']]);
|
|
97
|
+
|
|
98
|
+
const results = await verifyDnsRecords(appHostnames, '1.2.3.4', state);
|
|
99
|
+
|
|
100
|
+
expect(results).toHaveLength(1);
|
|
101
|
+
expect(results[0]).toMatchObject({
|
|
102
|
+
hostname: 'api.example.com',
|
|
103
|
+
appName: 'api',
|
|
104
|
+
verified: true,
|
|
105
|
+
skipped: true,
|
|
106
|
+
expectedIp: '1.2.3.4',
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Should NOT have called lookup for cached result
|
|
110
|
+
expect(mockLookup).not.toHaveBeenCalled();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should re-verify when server IP changes', async () => {
|
|
114
|
+
// Pre-populate state with different server IP
|
|
115
|
+
state.dnsVerified = {
|
|
116
|
+
'api.example.com': {
|
|
117
|
+
serverIp: '9.9.9.9',
|
|
118
|
+
verifiedAt: '2024-01-01T00:00:00.000Z',
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
mockLookup.mockResolvedValue({ address: '1.2.3.4', family: 4 });
|
|
123
|
+
|
|
124
|
+
const appHostnames = new Map([['api', 'api.example.com']]);
|
|
125
|
+
|
|
126
|
+
const results = await verifyDnsRecords(appHostnames, '1.2.3.4', state);
|
|
127
|
+
|
|
128
|
+
expect(results).toHaveLength(1);
|
|
129
|
+
expect(results[0]).toMatchObject({
|
|
130
|
+
hostname: 'api.example.com',
|
|
131
|
+
appName: 'api',
|
|
132
|
+
verified: true,
|
|
133
|
+
resolvedIp: '1.2.3.4',
|
|
134
|
+
});
|
|
135
|
+
// Should NOT be skipped (it was re-verified)
|
|
136
|
+
expect(results[0]?.skipped).toBeUndefined();
|
|
137
|
+
|
|
138
|
+
// Should have called lookup since IP changed
|
|
139
|
+
expect(mockLookup).toHaveBeenCalledTimes(1);
|
|
140
|
+
|
|
141
|
+
// Should have updated verification
|
|
142
|
+
expect(state.dnsVerified!['api.example.com']?.serverIp).toBe('1.2.3.4');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should handle DNS resolution failure gracefully', async () => {
|
|
146
|
+
mockLookup.mockRejectedValue(new Error('NXDOMAIN'));
|
|
147
|
+
|
|
148
|
+
const appHostnames = new Map([['api', 'api.example.com']]);
|
|
149
|
+
|
|
150
|
+
const results = await verifyDnsRecords(appHostnames, '1.2.3.4', state);
|
|
151
|
+
|
|
152
|
+
expect(results).toHaveLength(1);
|
|
153
|
+
expect(results[0]).toMatchObject({
|
|
154
|
+
hostname: 'api.example.com',
|
|
155
|
+
appName: 'api',
|
|
156
|
+
verified: false,
|
|
157
|
+
expectedIp: '1.2.3.4',
|
|
158
|
+
error: expect.stringContaining('NXDOMAIN'),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Should NOT have stored verification for failed lookup
|
|
162
|
+
expect(state.dnsVerified).toBeUndefined();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should detect when DNS resolves to wrong IP', async () => {
|
|
166
|
+
mockLookup.mockResolvedValue({ address: '9.9.9.9', family: 4 });
|
|
167
|
+
|
|
168
|
+
const appHostnames = new Map([['api', 'api.example.com']]);
|
|
169
|
+
|
|
170
|
+
const results = await verifyDnsRecords(appHostnames, '1.2.3.4', state);
|
|
171
|
+
|
|
172
|
+
expect(results).toHaveLength(1);
|
|
173
|
+
expect(results[0]).toMatchObject({
|
|
174
|
+
hostname: 'api.example.com',
|
|
175
|
+
appName: 'api',
|
|
176
|
+
verified: false,
|
|
177
|
+
resolvedIp: '9.9.9.9',
|
|
178
|
+
expectedIp: '1.2.3.4',
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Should NOT have stored verification for wrong IP
|
|
182
|
+
expect(state.dnsVerified).toBeUndefined();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should handle mix of verified, cached, and pending hostnames', async () => {
|
|
186
|
+
// Pre-populate state with one verified hostname
|
|
187
|
+
state.dnsVerified = {
|
|
188
|
+
'cached.example.com': {
|
|
189
|
+
serverIp: '1.2.3.4',
|
|
190
|
+
verifiedAt: '2024-01-01T00:00:00.000Z',
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
mockLookup
|
|
195
|
+
.mockResolvedValueOnce({ address: '1.2.3.4', family: 4 })
|
|
196
|
+
.mockRejectedValueOnce(new Error('NXDOMAIN'));
|
|
197
|
+
|
|
198
|
+
const appHostnames = new Map([
|
|
199
|
+
['cached', 'cached.example.com'],
|
|
200
|
+
['new-verified', 'new.example.com'],
|
|
201
|
+
['pending', 'pending.example.com'],
|
|
202
|
+
]);
|
|
203
|
+
|
|
204
|
+
const results = await verifyDnsRecords(appHostnames, '1.2.3.4', state);
|
|
205
|
+
|
|
206
|
+
expect(results).toHaveLength(3);
|
|
207
|
+
|
|
208
|
+
// Cached should be skipped
|
|
209
|
+
expect(results[0]).toMatchObject({
|
|
210
|
+
hostname: 'cached.example.com',
|
|
211
|
+
verified: true,
|
|
212
|
+
skipped: true,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// New should be verified
|
|
216
|
+
expect(results[1]).toMatchObject({
|
|
217
|
+
hostname: 'new.example.com',
|
|
218
|
+
verified: true,
|
|
219
|
+
resolvedIp: '1.2.3.4',
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Pending should fail
|
|
223
|
+
expect(results[2]).toMatchObject({
|
|
224
|
+
hostname: 'pending.example.com',
|
|
225
|
+
verified: false,
|
|
226
|
+
error: expect.stringContaining('NXDOMAIN'),
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
});
|
|
@@ -544,9 +544,8 @@ describe('DokployApi', () => {
|
|
|
544
544
|
projectId: 'proj_1',
|
|
545
545
|
environmentId: 'env_1',
|
|
546
546
|
appName: 'mydb',
|
|
547
|
-
databaseName: 'app',
|
|
548
547
|
databaseUser: 'postgres',
|
|
549
|
-
dockerImage: 'postgres:
|
|
548
|
+
dockerImage: 'postgres:18',
|
|
550
549
|
});
|
|
551
550
|
});
|
|
552
551
|
|
|
@@ -632,7 +631,7 @@ describe('DokployApi', () => {
|
|
|
632
631
|
projectId: 'proj_1',
|
|
633
632
|
environmentId: 'env_1',
|
|
634
633
|
appName: 'mycache',
|
|
635
|
-
dockerImage: 'redis:
|
|
634
|
+
dockerImage: 'redis:8',
|
|
636
635
|
});
|
|
637
636
|
});
|
|
638
637
|
|
|
@@ -100,9 +100,7 @@ describe('resolveHost', () => {
|
|
|
100
100
|
});
|
|
101
101
|
|
|
102
102
|
describe('isMainFrontendApp', () => {
|
|
103
|
-
const createApp = (
|
|
104
|
-
type: 'backend' | 'frontend',
|
|
105
|
-
): NormalizedAppConfig => ({
|
|
103
|
+
const createApp = (type: 'backend' | 'frontend'): NormalizedAppConfig => ({
|
|
106
104
|
type,
|
|
107
105
|
path: 'apps/test',
|
|
108
106
|
port: 3000,
|
|
@@ -145,6 +143,12 @@ describe('isMainFrontendApp', () => {
|
|
|
145
143
|
};
|
|
146
144
|
expect(isMainFrontendApp('admin', apps.admin, apps)).toBe(false);
|
|
147
145
|
});
|
|
146
|
+
|
|
147
|
+
it('should return false when no frontend apps in allApps (edge case)', () => {
|
|
148
|
+
// Edge case: checking a frontend app against an empty allApps
|
|
149
|
+
const frontendApp = createApp('frontend');
|
|
150
|
+
expect(isMainFrontendApp('myapp', frontendApp, {})).toBe(false);
|
|
151
|
+
});
|
|
148
152
|
});
|
|
149
153
|
|
|
150
154
|
describe('generatePublicUrlBuildArgs', () => {
|