@geekmidas/cli 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/{Route53Provider-xrWuBXih.cjs → Route53Provider-Bs7Arms9.cjs} +3 -2
- package/dist/Route53Provider-Bs7Arms9.cjs.map +1 -0
- package/dist/{Route53Provider-DOWmFnwN.mjs → Route53Provider-C8mS0zY6.mjs} +3 -2
- package/dist/Route53Provider-C8mS0zY6.mjs.map +1 -0
- package/dist/{config-C1bidhvG.mjs → config-DfCJ29PQ.mjs} +2 -2
- package/dist/{config-C1bidhvG.mjs.map → config-DfCJ29PQ.mjs.map} +1 -1
- package/dist/{config-C1dM7aZb.cjs → config-ZQM1vBoz.cjs} +2 -2
- package/dist/{config-C1dM7aZb.cjs.map → config-ZQM1vBoz.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/{index-DzmZ6SUW.d.cts → index-B58qjyBd.d.cts} +27 -1
- package/dist/index-B58qjyBd.d.cts.map +1 -0
- package/dist/{index-DvpWzLD7.d.mts → index-C0SpUT9Y.d.mts} +27 -1
- package/dist/index-C0SpUT9Y.d.mts.map +1 -0
- package/dist/index.cjs +117 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +117 -49
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-9k6a6VA4.mjs → openapi-BcSjLfWq.mjs} +2 -2
- package/dist/{openapi-9k6a6VA4.mjs.map → openapi-BcSjLfWq.mjs.map} +1 -1
- package/dist/{openapi-Dcja4e1C.cjs → openapi-D6Hcfov0.cjs} +2 -2
- package/dist/{openapi-Dcja4e1C.cjs.map → openapi-D6Hcfov0.cjs.map} +1 -1
- 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 +1 -1
- package/dist/workspace/index.d.mts +1 -1
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-CeFgIDC-.cjs → workspace-2Do2YcGZ.cjs} +5 -1
- package/dist/{workspace-CeFgIDC-.cjs.map → workspace-2Do2YcGZ.cjs.map} +1 -1
- package/dist/{workspace-Cb_I7oCJ.mjs → workspace-BW2iU37P.mjs} +5 -1
- package/dist/{workspace-Cb_I7oCJ.mjs.map → workspace-BW2iU37P.mjs.map} +1 -1
- package/package.json +2 -2
- package/src/deploy/__tests__/Route53Provider.spec.ts +23 -0
- package/src/deploy/__tests__/env-resolver.spec.ts +384 -2
- package/src/deploy/__tests__/index.spec.ts +393 -5
- package/src/deploy/__tests__/sniffer.spec.ts +104 -93
- package/src/deploy/dns/Route53Provider.ts +4 -1
- package/src/deploy/env-resolver.ts +20 -0
- package/src/deploy/index.ts +83 -24
- package/src/deploy/sniffer.ts +39 -7
- package/src/init/generators/monorepo.ts +7 -1
- package/src/init/generators/web.ts +45 -2
- package/src/workspace/schema.ts +8 -0
- package/src/workspace/types.ts +23 -0
- package/dist/Route53Provider-DOWmFnwN.mjs.map +0 -1
- package/dist/Route53Provider-xrWuBXih.cjs.map +0 -1
- package/dist/index-DvpWzLD7.d.mts.map +0 -1
- package/dist/index-DzmZ6SUW.d.cts.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geekmidas/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -53,9 +53,9 @@
|
|
|
53
53
|
"pg": "~8.17.1",
|
|
54
54
|
"prompts": "~2.4.2",
|
|
55
55
|
"tsx": "~4.20.3",
|
|
56
|
-
"@geekmidas/constructs": "~1.0.0",
|
|
57
56
|
"@geekmidas/envkit": "~1.0.0",
|
|
58
57
|
"@geekmidas/errors": "~1.0.0",
|
|
58
|
+
"@geekmidas/constructs": "~1.0.0",
|
|
59
59
|
"@geekmidas/logger": "~1.0.0",
|
|
60
60
|
"@geekmidas/schema": "~1.0.0"
|
|
61
61
|
},
|
|
@@ -399,4 +399,27 @@ describe('Route53Provider', () => {
|
|
|
399
399
|
).rejects.toThrow('No hosted zone found for domain');
|
|
400
400
|
});
|
|
401
401
|
});
|
|
402
|
+
|
|
403
|
+
describe('default region', () => {
|
|
404
|
+
it('should use us-east-1 as default region when none specified', () => {
|
|
405
|
+
// This test verifies the provider can be created without region
|
|
406
|
+
// and doesn't throw "Region is missing" error
|
|
407
|
+
const providerWithoutRegion = new Route53Provider({
|
|
408
|
+
endpoint: LOCALSTACK_ENDPOINT,
|
|
409
|
+
hostedZoneId: 'test-zone',
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
expect(providerWithoutRegion.name).toBe('route53');
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('should use provided region when specified', () => {
|
|
416
|
+
const providerWithRegion = new Route53Provider({
|
|
417
|
+
endpoint: LOCALSTACK_ENDPOINT,
|
|
418
|
+
region: 'eu-west-1',
|
|
419
|
+
hostedZoneId: 'test-zone',
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
expect(providerWithRegion.name).toBe('route53');
|
|
423
|
+
});
|
|
424
|
+
});
|
|
402
425
|
});
|
|
@@ -295,6 +295,9 @@ describe('resolveEnvVar', () => {
|
|
|
295
295
|
it('should resolve custom variable from userSecrets.custom', () => {
|
|
296
296
|
const context = createContext({
|
|
297
297
|
userSecrets: {
|
|
298
|
+
stage: 'production',
|
|
299
|
+
createdAt: '2024-01-01T00:00:00Z',
|
|
300
|
+
updatedAt: '2024-01-01T00:00:00Z',
|
|
298
301
|
custom: { MY_API_KEY: 'secret-api-key' },
|
|
299
302
|
urls: {},
|
|
300
303
|
services: {},
|
|
@@ -307,6 +310,9 @@ describe('resolveEnvVar', () => {
|
|
|
307
310
|
it('should resolve URL variables from userSecrets.urls', () => {
|
|
308
311
|
const context = createContext({
|
|
309
312
|
userSecrets: {
|
|
313
|
+
stage: 'production',
|
|
314
|
+
createdAt: '2024-01-01T00:00:00Z',
|
|
315
|
+
updatedAt: '2024-01-01T00:00:00Z',
|
|
310
316
|
custom: {},
|
|
311
317
|
urls: { DATABASE_URL: 'postgresql://external:5432/db' },
|
|
312
318
|
services: {},
|
|
@@ -321,9 +327,19 @@ describe('resolveEnvVar', () => {
|
|
|
321
327
|
it('should resolve POSTGRES_PASSWORD from userSecrets.services', () => {
|
|
322
328
|
const context = createContext({
|
|
323
329
|
userSecrets: {
|
|
330
|
+
stage: 'production',
|
|
331
|
+
createdAt: '2024-01-01T00:00:00Z',
|
|
332
|
+
updatedAt: '2024-01-01T00:00:00Z',
|
|
324
333
|
custom: {},
|
|
325
334
|
urls: {},
|
|
326
|
-
services: {
|
|
335
|
+
services: {
|
|
336
|
+
postgres: {
|
|
337
|
+
host: 'localhost',
|
|
338
|
+
port: 5432,
|
|
339
|
+
username: 'postgres',
|
|
340
|
+
password: 'pg-password',
|
|
341
|
+
},
|
|
342
|
+
},
|
|
327
343
|
},
|
|
328
344
|
});
|
|
329
345
|
|
|
@@ -333,9 +349,19 @@ describe('resolveEnvVar', () => {
|
|
|
333
349
|
it('should resolve REDIS_PASSWORD from userSecrets.services', () => {
|
|
334
350
|
const context = createContext({
|
|
335
351
|
userSecrets: {
|
|
352
|
+
stage: 'production',
|
|
353
|
+
createdAt: '2024-01-01T00:00:00Z',
|
|
354
|
+
updatedAt: '2024-01-01T00:00:00Z',
|
|
336
355
|
custom: {},
|
|
337
356
|
urls: {},
|
|
338
|
-
services: {
|
|
357
|
+
services: {
|
|
358
|
+
redis: {
|
|
359
|
+
host: 'localhost',
|
|
360
|
+
port: 6379,
|
|
361
|
+
username: 'default',
|
|
362
|
+
password: 'redis-password',
|
|
363
|
+
},
|
|
364
|
+
},
|
|
339
365
|
},
|
|
340
366
|
});
|
|
341
367
|
|
|
@@ -347,6 +373,151 @@ describe('resolveEnvVar', () => {
|
|
|
347
373
|
|
|
348
374
|
expect(resolveEnvVar('UNKNOWN_VAR', context)).toBeUndefined();
|
|
349
375
|
});
|
|
376
|
+
|
|
377
|
+
describe('dependency URLs', () => {
|
|
378
|
+
it('should resolve AUTH_URL from dependencyUrls', () => {
|
|
379
|
+
const context = createContext({
|
|
380
|
+
dependencyUrls: { auth: 'https://auth.example.com' },
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
expect(resolveEnvVar('AUTH_URL', context)).toBe(
|
|
384
|
+
'https://auth.example.com',
|
|
385
|
+
);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('should resolve API_URL from dependencyUrls', () => {
|
|
389
|
+
const context = createContext({
|
|
390
|
+
dependencyUrls: { api: 'https://api.example.com' },
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
expect(resolveEnvVar('API_URL', context)).toBe('https://api.example.com');
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('should resolve any {DEP}_URL pattern from dependencyUrls', () => {
|
|
397
|
+
const context = createContext({
|
|
398
|
+
dependencyUrls: {
|
|
399
|
+
payments: 'https://payments.example.com',
|
|
400
|
+
notifications: 'https://notifications.example.com',
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
expect(resolveEnvVar('PAYMENTS_URL', context)).toBe(
|
|
405
|
+
'https://payments.example.com',
|
|
406
|
+
);
|
|
407
|
+
expect(resolveEnvVar('NOTIFICATIONS_URL', context)).toBe(
|
|
408
|
+
'https://notifications.example.com',
|
|
409
|
+
);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('should return undefined for missing dependency URL', () => {
|
|
413
|
+
const context = createContext({
|
|
414
|
+
dependencyUrls: { auth: 'https://auth.example.com' },
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
expect(resolveEnvVar('API_URL', context)).toBeUndefined();
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('should return undefined when dependencyUrls is not provided', () => {
|
|
421
|
+
const context = createContext();
|
|
422
|
+
|
|
423
|
+
expect(resolveEnvVar('AUTH_URL', context)).toBeUndefined();
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it('should handle custom domain from config', () => {
|
|
427
|
+
const context = createContext({
|
|
428
|
+
dependencyUrls: { auth: 'https://login.myapp.com' },
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
expect(resolveEnvVar('AUTH_URL', context)).toBe(
|
|
432
|
+
'https://login.myapp.com',
|
|
433
|
+
);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('should prefer user secrets over dependency URLs', () => {
|
|
437
|
+
const context = createContext({
|
|
438
|
+
dependencyUrls: { auth: 'https://auth.example.com' },
|
|
439
|
+
userSecrets: {
|
|
440
|
+
stage: 'production',
|
|
441
|
+
createdAt: '2024-01-01T00:00:00Z',
|
|
442
|
+
updatedAt: '2024-01-01T00:00:00Z',
|
|
443
|
+
custom: { AUTH_URL: 'https://custom-auth.example.com' },
|
|
444
|
+
urls: {},
|
|
445
|
+
services: {},
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// User secrets are checked after dependency URLs, so dependency URL wins
|
|
450
|
+
// If you want user secrets to override, the order in resolveEnvVar should change
|
|
451
|
+
expect(resolveEnvVar('AUTH_URL', context)).toBe(
|
|
452
|
+
'https://auth.example.com',
|
|
453
|
+
);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
describe('NEXT_PUBLIC_ prefix', () => {
|
|
457
|
+
it('should resolve NEXT_PUBLIC_API_URL from dependencyUrls.api', () => {
|
|
458
|
+
const context = createContext({
|
|
459
|
+
dependencyUrls: { api: 'https://api.example.com' },
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
expect(resolveEnvVar('NEXT_PUBLIC_API_URL', context)).toBe(
|
|
463
|
+
'https://api.example.com',
|
|
464
|
+
);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('should resolve NEXT_PUBLIC_AUTH_URL from dependencyUrls.auth', () => {
|
|
468
|
+
const context = createContext({
|
|
469
|
+
dependencyUrls: { auth: 'https://auth.example.com' },
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
expect(resolveEnvVar('NEXT_PUBLIC_AUTH_URL', context)).toBe(
|
|
473
|
+
'https://auth.example.com',
|
|
474
|
+
);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it('should resolve both AUTH_URL and NEXT_PUBLIC_AUTH_URL to same value', () => {
|
|
478
|
+
const context = createContext({
|
|
479
|
+
dependencyUrls: { auth: 'https://auth.example.com' },
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
expect(resolveEnvVar('AUTH_URL', context)).toBe(
|
|
483
|
+
'https://auth.example.com',
|
|
484
|
+
);
|
|
485
|
+
expect(resolveEnvVar('NEXT_PUBLIC_AUTH_URL', context)).toBe(
|
|
486
|
+
'https://auth.example.com',
|
|
487
|
+
);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('should resolve NEXT_PUBLIC_ prefix for any dependency', () => {
|
|
491
|
+
const context = createContext({
|
|
492
|
+
dependencyUrls: {
|
|
493
|
+
payments: 'https://payments.example.com',
|
|
494
|
+
notifications: 'https://notifications.example.com',
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
expect(resolveEnvVar('NEXT_PUBLIC_PAYMENTS_URL', context)).toBe(
|
|
499
|
+
'https://payments.example.com',
|
|
500
|
+
);
|
|
501
|
+
expect(resolveEnvVar('NEXT_PUBLIC_NOTIFICATIONS_URL', context)).toBe(
|
|
502
|
+
'https://notifications.example.com',
|
|
503
|
+
);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it('should return undefined for missing NEXT_PUBLIC_ dependency URL', () => {
|
|
507
|
+
const context = createContext({
|
|
508
|
+
dependencyUrls: { auth: 'https://auth.example.com' },
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
expect(resolveEnvVar('NEXT_PUBLIC_API_URL', context)).toBeUndefined();
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it('should return undefined when dependencyUrls is not provided', () => {
|
|
515
|
+
const context = createContext();
|
|
516
|
+
|
|
517
|
+
expect(resolveEnvVar('NEXT_PUBLIC_AUTH_URL', context)).toBeUndefined();
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
});
|
|
350
521
|
});
|
|
351
522
|
|
|
352
523
|
describe('resolveEnvVars', () => {
|
|
@@ -490,4 +661,215 @@ describe('validateEnvVars', () => {
|
|
|
490
661
|
expect(result.missing).toEqual([]);
|
|
491
662
|
expect(result.resolved).toEqual({});
|
|
492
663
|
});
|
|
664
|
+
|
|
665
|
+
it('should resolve dependency URLs in validation', () => {
|
|
666
|
+
const context = createContext({
|
|
667
|
+
dependencyUrls: {
|
|
668
|
+
auth: 'https://auth.example.com',
|
|
669
|
+
api: 'https://api.example.com',
|
|
670
|
+
},
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
const result = validateEnvVars(['PORT', 'AUTH_URL', 'API_URL'], context);
|
|
674
|
+
|
|
675
|
+
expect(result.valid).toBe(true);
|
|
676
|
+
expect(result.missing).toEqual([]);
|
|
677
|
+
expect(result.resolved).toEqual({
|
|
678
|
+
PORT: '3000',
|
|
679
|
+
AUTH_URL: 'https://auth.example.com',
|
|
680
|
+
API_URL: 'https://api.example.com',
|
|
681
|
+
});
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it('should report missing dependency URLs', () => {
|
|
685
|
+
const context = createContext({
|
|
686
|
+
dependencyUrls: { auth: 'https://auth.example.com' },
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
const result = validateEnvVars(
|
|
690
|
+
['PORT', 'AUTH_URL', 'PAYMENTS_URL'],
|
|
691
|
+
context,
|
|
692
|
+
);
|
|
693
|
+
|
|
694
|
+
expect(result.valid).toBe(false);
|
|
695
|
+
expect(result.missing).toEqual(['PAYMENTS_URL']);
|
|
696
|
+
expect(result.resolved).toEqual({
|
|
697
|
+
PORT: '3000',
|
|
698
|
+
AUTH_URL: 'https://auth.example.com',
|
|
699
|
+
});
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Tests for Docker build arg extraction logic.
|
|
705
|
+
* This simulates the behavior in deploy/index.ts where NEXT_PUBLIC_* vars
|
|
706
|
+
* are extracted from resolved vars for Docker build args.
|
|
707
|
+
*/
|
|
708
|
+
describe('Docker build arg extraction', () => {
|
|
709
|
+
const createContext = (
|
|
710
|
+
overrides: Partial<EnvResolverContext> = {},
|
|
711
|
+
): EnvResolverContext => ({
|
|
712
|
+
app: {
|
|
713
|
+
type: 'frontend',
|
|
714
|
+
path: 'apps/web',
|
|
715
|
+
port: 3001,
|
|
716
|
+
dependencies: ['api', 'auth'],
|
|
717
|
+
resolvedDeployTarget: 'dokploy',
|
|
718
|
+
},
|
|
719
|
+
appName: 'web',
|
|
720
|
+
stage: 'production',
|
|
721
|
+
state: createEmptyState('production', 'proj_test', 'env-123'),
|
|
722
|
+
appHostname: 'web.example.com',
|
|
723
|
+
frontendUrls: [],
|
|
724
|
+
...overrides,
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Simulates the build arg extraction logic from deploy/index.ts
|
|
729
|
+
*/
|
|
730
|
+
function extractBuildArgs(resolved: Record<string, string>): {
|
|
731
|
+
buildArgs: string[];
|
|
732
|
+
publicUrlArgNames: string[];
|
|
733
|
+
} {
|
|
734
|
+
const buildArgs: string[] = [];
|
|
735
|
+
const publicUrlArgNames: string[] = [];
|
|
736
|
+
|
|
737
|
+
for (const [key, value] of Object.entries(resolved)) {
|
|
738
|
+
if (key.startsWith('NEXT_PUBLIC_')) {
|
|
739
|
+
buildArgs.push(`${key}=${value}`);
|
|
740
|
+
publicUrlArgNames.push(key);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
return { buildArgs, publicUrlArgNames };
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
it('should extract NEXT_PUBLIC_* vars as build args', () => {
|
|
748
|
+
const context = createContext({
|
|
749
|
+
dependencyUrls: {
|
|
750
|
+
api: 'https://api.example.com',
|
|
751
|
+
auth: 'https://auth.example.com',
|
|
752
|
+
},
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
const sniffedVars = [
|
|
756
|
+
'NEXT_PUBLIC_API_URL',
|
|
757
|
+
'NEXT_PUBLIC_AUTH_URL',
|
|
758
|
+
'NEXT_PUBLIC_STRIPE_KEY',
|
|
759
|
+
];
|
|
760
|
+
|
|
761
|
+
const { resolved } = validateEnvVars(sniffedVars, context);
|
|
762
|
+
|
|
763
|
+
// Simulate user secrets providing STRIPE_KEY
|
|
764
|
+
resolved.NEXT_PUBLIC_STRIPE_KEY = 'pk_test_123';
|
|
765
|
+
|
|
766
|
+
const { buildArgs, publicUrlArgNames } = extractBuildArgs(resolved);
|
|
767
|
+
|
|
768
|
+
expect(publicUrlArgNames).toEqual([
|
|
769
|
+
'NEXT_PUBLIC_API_URL',
|
|
770
|
+
'NEXT_PUBLIC_AUTH_URL',
|
|
771
|
+
'NEXT_PUBLIC_STRIPE_KEY',
|
|
772
|
+
]);
|
|
773
|
+
expect(buildArgs).toEqual([
|
|
774
|
+
'NEXT_PUBLIC_API_URL=https://api.example.com',
|
|
775
|
+
'NEXT_PUBLIC_AUTH_URL=https://auth.example.com',
|
|
776
|
+
'NEXT_PUBLIC_STRIPE_KEY=pk_test_123',
|
|
777
|
+
]);
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
it('should NOT include server-only vars in build args', () => {
|
|
781
|
+
const context = createContext({
|
|
782
|
+
dependencyUrls: { api: 'https://api.example.com' },
|
|
783
|
+
appCredentials: { dbUser: 'web', dbPassword: 'pass' },
|
|
784
|
+
postgres: { host: 'postgres', port: 5432, database: 'mydb' },
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
const sniffedVars = [
|
|
788
|
+
'NEXT_PUBLIC_API_URL',
|
|
789
|
+
'DATABASE_URL',
|
|
790
|
+
'STRIPE_SECRET_KEY',
|
|
791
|
+
];
|
|
792
|
+
|
|
793
|
+
const { resolved } = validateEnvVars(sniffedVars, context);
|
|
794
|
+
|
|
795
|
+
// Add server-only secret
|
|
796
|
+
resolved.STRIPE_SECRET_KEY = 'sk_test_secret';
|
|
797
|
+
|
|
798
|
+
const { buildArgs, publicUrlArgNames } = extractBuildArgs(resolved);
|
|
799
|
+
|
|
800
|
+
// Only NEXT_PUBLIC_* should be in build args
|
|
801
|
+
expect(publicUrlArgNames).toEqual(['NEXT_PUBLIC_API_URL']);
|
|
802
|
+
expect(buildArgs).toEqual(['NEXT_PUBLIC_API_URL=https://api.example.com']);
|
|
803
|
+
|
|
804
|
+
// Server vars should still be in resolved (for runtime)
|
|
805
|
+
expect(resolved.DATABASE_URL).toBe(
|
|
806
|
+
'postgresql://web:pass@postgres:5432/mydb',
|
|
807
|
+
);
|
|
808
|
+
expect(resolved.STRIPE_SECRET_KEY).toBe('sk_test_secret');
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
it('should handle mixed frontend vars correctly', () => {
|
|
812
|
+
const context = createContext({
|
|
813
|
+
dependencyUrls: {
|
|
814
|
+
api: 'https://api.example.com',
|
|
815
|
+
auth: 'https://auth.example.com',
|
|
816
|
+
},
|
|
817
|
+
userSecrets: {
|
|
818
|
+
stage: 'production',
|
|
819
|
+
createdAt: '2024-01-01T00:00:00Z',
|
|
820
|
+
updatedAt: '2024-01-01T00:00:00Z',
|
|
821
|
+
custom: {
|
|
822
|
+
NEXT_PUBLIC_POSTHOG_KEY: 'phc_test123',
|
|
823
|
+
STRIPE_SECRET_KEY: 'sk_test_secret',
|
|
824
|
+
},
|
|
825
|
+
urls: {},
|
|
826
|
+
services: {},
|
|
827
|
+
},
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
const sniffedVars = [
|
|
831
|
+
// From dependencies (auto-generated)
|
|
832
|
+
'NEXT_PUBLIC_API_URL',
|
|
833
|
+
'NEXT_PUBLIC_AUTH_URL',
|
|
834
|
+
// From client config
|
|
835
|
+
'NEXT_PUBLIC_POSTHOG_KEY',
|
|
836
|
+
// From server config
|
|
837
|
+
'STRIPE_SECRET_KEY',
|
|
838
|
+
'DATABASE_URL',
|
|
839
|
+
];
|
|
840
|
+
|
|
841
|
+
const { resolved, missing } = validateEnvVars(sniffedVars, context);
|
|
842
|
+
|
|
843
|
+
// DATABASE_URL is missing (no postgres config)
|
|
844
|
+
expect(missing).toContain('DATABASE_URL');
|
|
845
|
+
|
|
846
|
+
const { buildArgs, publicUrlArgNames } = extractBuildArgs(resolved);
|
|
847
|
+
|
|
848
|
+
// Only NEXT_PUBLIC_* should be build args
|
|
849
|
+
expect(publicUrlArgNames).toHaveLength(3);
|
|
850
|
+
expect(publicUrlArgNames).toContain('NEXT_PUBLIC_API_URL');
|
|
851
|
+
expect(publicUrlArgNames).toContain('NEXT_PUBLIC_AUTH_URL');
|
|
852
|
+
expect(publicUrlArgNames).toContain('NEXT_PUBLIC_POSTHOG_KEY');
|
|
853
|
+
|
|
854
|
+
// Server secret should NOT be in build args
|
|
855
|
+
expect(publicUrlArgNames).not.toContain('STRIPE_SECRET_KEY');
|
|
856
|
+
|
|
857
|
+
// But should be in resolved for runtime
|
|
858
|
+
expect(resolved.STRIPE_SECRET_KEY).toBe('sk_test_secret');
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
it('should return empty build args when no NEXT_PUBLIC_* vars', () => {
|
|
862
|
+
const context = createContext({
|
|
863
|
+
appCredentials: { dbUser: 'web', dbPassword: 'pass' },
|
|
864
|
+
postgres: { host: 'postgres', port: 5432, database: 'mydb' },
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
const sniffedVars = ['DATABASE_URL', 'PORT'];
|
|
868
|
+
|
|
869
|
+
const { resolved } = validateEnvVars(sniffedVars, context);
|
|
870
|
+
const { buildArgs, publicUrlArgNames } = extractBuildArgs(resolved);
|
|
871
|
+
|
|
872
|
+
expect(buildArgs).toEqual([]);
|
|
873
|
+
expect(publicUrlArgNames).toEqual([]);
|
|
874
|
+
});
|
|
493
875
|
});
|