@geekmidas/cli 0.18.0 → 0.20.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/{bundler-C74EKlNa.cjs → bundler-CyHg1v_T.cjs} +3 -3
- package/dist/{bundler-C74EKlNa.cjs.map → bundler-CyHg1v_T.cjs.map} +1 -1
- package/dist/{bundler-B6z6HEeh.mjs → bundler-DQIuE3Kn.mjs} +3 -3
- package/dist/{bundler-B6z6HEeh.mjs.map → bundler-DQIuE3Kn.mjs.map} +1 -1
- package/dist/{config-DYULeEv8.mjs → config-BaYqrF3n.mjs} +48 -10
- package/dist/config-BaYqrF3n.mjs.map +1 -0
- package/dist/{config-AmInkU7k.cjs → config-CxrLu8ia.cjs} +53 -9
- package/dist/config-CxrLu8ia.cjs.map +1 -0
- package/dist/config.cjs +4 -1
- package/dist/config.d.cts +27 -2
- package/dist/config.d.cts.map +1 -1
- package/dist/config.d.mts +27 -2
- package/dist/config.d.mts.map +1 -1
- package/dist/config.mjs +3 -2
- package/dist/dokploy-api-B0w17y4_.mjs +3 -0
- package/dist/{dokploy-api-CaETb2L6.mjs → dokploy-api-B9qR2Yn1.mjs} +1 -1
- package/dist/{dokploy-api-CaETb2L6.mjs.map → dokploy-api-B9qR2Yn1.mjs.map} +1 -1
- package/dist/dokploy-api-BnGeUqN4.cjs +3 -0
- package/dist/{dokploy-api-C7F9VykY.cjs → dokploy-api-C5czOZoc.cjs} +1 -1
- package/dist/{dokploy-api-C7F9VykY.cjs.map → dokploy-api-C5czOZoc.cjs.map} +1 -1
- package/dist/{encryption-D7Efcdi9.cjs → encryption-BAz0xQ1Q.cjs} +1 -1
- package/dist/{encryption-D7Efcdi9.cjs.map → encryption-BAz0xQ1Q.cjs.map} +1 -1
- package/dist/{encryption-h4Nb6W-M.mjs → encryption-JtMsiGNp.mjs} +2 -2
- package/dist/{encryption-h4Nb6W-M.mjs.map → encryption-JtMsiGNp.mjs.map} +1 -1
- package/dist/index-CWN-bgrO.d.mts +495 -0
- package/dist/index-CWN-bgrO.d.mts.map +1 -0
- package/dist/index-DEWYvYvg.d.cts +495 -0
- package/dist/index-DEWYvYvg.d.cts.map +1 -0
- package/dist/index.cjs +2640 -564
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2635 -564
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-CZVcfxk-.mjs → openapi-CgqR6Jkw.mjs} +3 -3
- package/dist/{openapi-CZVcfxk-.mjs.map → openapi-CgqR6Jkw.mjs.map} +1 -1
- package/dist/{openapi-C89hhkZC.cjs → openapi-DfpxS0xv.cjs} +8 -2
- package/dist/{openapi-C89hhkZC.cjs.map → openapi-DfpxS0xv.cjs.map} +1 -1
- package/dist/{openapi-react-query-CM2_qlW9.mjs → openapi-react-query-5rSortLH.mjs} +1 -1
- package/dist/{openapi-react-query-CM2_qlW9.mjs.map → openapi-react-query-5rSortLH.mjs.map} +1 -1
- package/dist/{openapi-react-query-iKjfLzff.cjs → openapi-react-query-DvNpdDpM.cjs} +1 -1
- package/dist/{openapi-react-query-iKjfLzff.cjs.map → openapi-react-query-DvNpdDpM.cjs.map} +1 -1
- package/dist/openapi-react-query.cjs +1 -1
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/openapi.cjs +3 -2
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +3 -2
- package/dist/{storage-Bn3K9Ccu.cjs → storage-BPRgh3DU.cjs} +136 -5
- package/dist/storage-BPRgh3DU.cjs.map +1 -0
- package/dist/{storage-nkGIjeXt.mjs → storage-DNj_I11J.mjs} +1 -1
- package/dist/storage-Dhst7BhI.mjs +272 -0
- package/dist/storage-Dhst7BhI.mjs.map +1 -0
- package/dist/{storage-UfyTn7Zm.cjs → storage-fOR8dMu5.cjs} +1 -1
- package/dist/{types-iFk5ms7y.d.mts → types-K2uQJ-FO.d.mts} +2 -2
- package/dist/{types-BgaMXsUa.d.cts.map → types-K2uQJ-FO.d.mts.map} +1 -1
- package/dist/{types-BgaMXsUa.d.cts → types-l53qUmGt.d.cts} +2 -2
- package/dist/{types-iFk5ms7y.d.mts.map → types-l53qUmGt.d.cts.map} +1 -1
- package/dist/workspace/index.cjs +19 -0
- package/dist/workspace/index.d.cts +3 -0
- package/dist/workspace/index.d.mts +3 -0
- package/dist/workspace/index.mjs +3 -0
- package/dist/workspace-CPLEZDZf.mjs +3788 -0
- package/dist/workspace-CPLEZDZf.mjs.map +1 -0
- package/dist/workspace-iWgBlX6h.cjs +3885 -0
- package/dist/workspace-iWgBlX6h.cjs.map +1 -0
- package/package.json +9 -4
- package/src/build/__tests__/workspace-build.spec.ts +215 -0
- package/src/build/index.ts +189 -1
- package/src/config.ts +71 -14
- package/src/deploy/__tests__/docker.spec.ts +1 -1
- package/src/deploy/__tests__/index.spec.ts +305 -1
- package/src/deploy/index.ts +426 -4
- package/src/deploy/types.ts +32 -0
- package/src/dev/__tests__/index.spec.ts +572 -1
- package/src/dev/index.ts +582 -2
- package/src/docker/__tests__/compose.spec.ts +425 -0
- package/src/docker/__tests__/templates.spec.ts +145 -0
- package/src/docker/compose.ts +248 -0
- package/src/docker/index.ts +159 -3
- package/src/docker/templates.ts +219 -4
- package/src/index.ts +24 -0
- package/src/init/__tests__/generators.spec.ts +17 -24
- package/src/init/__tests__/init.spec.ts +157 -5
- package/src/init/generators/auth.ts +220 -0
- package/src/init/generators/config.ts +61 -4
- package/src/init/generators/docker.ts +115 -8
- package/src/init/generators/env.ts +7 -127
- package/src/init/generators/index.ts +1 -0
- package/src/init/generators/models.ts +3 -1
- package/src/init/generators/monorepo.ts +154 -10
- package/src/init/generators/package.ts +5 -3
- package/src/init/generators/web.ts +213 -0
- package/src/init/index.ts +290 -58
- package/src/init/templates/api.ts +38 -29
- package/src/init/templates/index.ts +132 -4
- package/src/init/templates/minimal.ts +33 -35
- package/src/init/templates/serverless.ts +16 -19
- package/src/init/templates/worker.ts +50 -25
- package/src/init/versions.ts +47 -0
- package/src/secrets/keystore.ts +144 -0
- package/src/secrets/storage.ts +109 -6
- package/src/test/index.ts +97 -0
- package/src/workspace/__tests__/client-generator.spec.ts +357 -0
- package/src/workspace/__tests__/index.spec.ts +543 -0
- package/src/workspace/__tests__/schema.spec.ts +519 -0
- package/src/workspace/__tests__/type-inference.spec.ts +251 -0
- package/src/workspace/client-generator.ts +307 -0
- package/src/workspace/index.ts +372 -0
- package/src/workspace/schema.ts +368 -0
- package/src/workspace/types.ts +336 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/tsdown.config.ts +1 -0
- package/dist/config-AmInkU7k.cjs.map +0 -1
- package/dist/config-DYULeEv8.mjs.map +0 -1
- package/dist/dokploy-api-B7KxOQr3.cjs +0 -3
- package/dist/dokploy-api-DHvfmWbi.mjs +0 -3
- package/dist/storage-BaOP55oq.mjs +0 -147
- package/dist/storage-BaOP55oq.mjs.map +0 -1
- package/dist/storage-Bn3K9Ccu.cjs.map +0 -1
|
@@ -1,13 +1,24 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
1
2
|
import type { AddressInfo } from 'node:net';
|
|
2
3
|
import { createServer } from 'node:net';
|
|
3
|
-
import {
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
7
|
+
import type {
|
|
8
|
+
NormalizedAppConfig,
|
|
9
|
+
NormalizedWorkspace,
|
|
10
|
+
} from '../../workspace/index.js';
|
|
4
11
|
import {
|
|
12
|
+
checkPortConflicts,
|
|
5
13
|
findAvailablePort,
|
|
14
|
+
generateAllDependencyEnvVars,
|
|
6
15
|
isPortAvailable,
|
|
7
16
|
normalizeHooksConfig,
|
|
8
17
|
normalizeProductionConfig,
|
|
9
18
|
normalizeStudioConfig,
|
|
10
19
|
normalizeTelescopeConfig,
|
|
20
|
+
validateFrontendApp,
|
|
21
|
+
validateFrontendApps,
|
|
11
22
|
} from '../index';
|
|
12
23
|
|
|
13
24
|
// Skip port-related tests in CI due to flaky port binding issues
|
|
@@ -396,3 +407,563 @@ describe('normalizeProductionConfig', () => {
|
|
|
396
407
|
expect(result?.bundle).toBe(false);
|
|
397
408
|
});
|
|
398
409
|
});
|
|
410
|
+
|
|
411
|
+
describe('Workspace Dev Server', () => {
|
|
412
|
+
/**
|
|
413
|
+
* Helper to create a test workspace configuration.
|
|
414
|
+
* Automatically adds resolvedDeployTarget to each app.
|
|
415
|
+
*/
|
|
416
|
+
function createTestWorkspace(
|
|
417
|
+
apps: Record<string, Omit<NormalizedAppConfig, 'resolvedDeployTarget'>>,
|
|
418
|
+
overrides: Partial<NormalizedWorkspace> = {},
|
|
419
|
+
): NormalizedWorkspace {
|
|
420
|
+
const appsWithDeployTarget: NormalizedWorkspace['apps'] = {};
|
|
421
|
+
for (const [name, app] of Object.entries(apps)) {
|
|
422
|
+
appsWithDeployTarget[name] = {
|
|
423
|
+
...app,
|
|
424
|
+
resolvedDeployTarget: 'dokploy',
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
name: 'test-workspace',
|
|
429
|
+
root: '/test/workspace',
|
|
430
|
+
apps: appsWithDeployTarget,
|
|
431
|
+
services: {},
|
|
432
|
+
deploy: { default: 'dokploy' },
|
|
433
|
+
shared: { packages: ['packages/*'] },
|
|
434
|
+
secrets: {},
|
|
435
|
+
...overrides,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
describe('checkPortConflicts', () => {
|
|
440
|
+
it('should return empty array when no conflicts', () => {
|
|
441
|
+
const workspace = createTestWorkspace({
|
|
442
|
+
api: {
|
|
443
|
+
type: 'backend',
|
|
444
|
+
path: 'apps/api',
|
|
445
|
+
port: 3000,
|
|
446
|
+
dependencies: [],
|
|
447
|
+
},
|
|
448
|
+
web: {
|
|
449
|
+
type: 'frontend',
|
|
450
|
+
path: 'apps/web',
|
|
451
|
+
port: 3001,
|
|
452
|
+
dependencies: [],
|
|
453
|
+
},
|
|
454
|
+
admin: {
|
|
455
|
+
type: 'frontend',
|
|
456
|
+
path: 'apps/admin',
|
|
457
|
+
port: 3002,
|
|
458
|
+
dependencies: [],
|
|
459
|
+
},
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
const conflicts = checkPortConflicts(workspace);
|
|
463
|
+
expect(conflicts).toEqual([]);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('should detect port conflicts between two apps', () => {
|
|
467
|
+
const workspace = createTestWorkspace({
|
|
468
|
+
api: {
|
|
469
|
+
type: 'backend',
|
|
470
|
+
path: 'apps/api',
|
|
471
|
+
port: 3000,
|
|
472
|
+
dependencies: [],
|
|
473
|
+
},
|
|
474
|
+
web: {
|
|
475
|
+
type: 'frontend',
|
|
476
|
+
path: 'apps/web',
|
|
477
|
+
port: 3000, // Same port as api!
|
|
478
|
+
dependencies: [],
|
|
479
|
+
},
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
const conflicts = checkPortConflicts(workspace);
|
|
483
|
+
expect(conflicts).toHaveLength(1);
|
|
484
|
+
expect(conflicts[0]).toEqual({
|
|
485
|
+
app1: 'api',
|
|
486
|
+
app2: 'web',
|
|
487
|
+
port: 3000,
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it('should detect multiple port conflicts', () => {
|
|
492
|
+
const workspace = createTestWorkspace({
|
|
493
|
+
api: {
|
|
494
|
+
type: 'backend',
|
|
495
|
+
path: 'apps/api',
|
|
496
|
+
port: 3000,
|
|
497
|
+
dependencies: [],
|
|
498
|
+
},
|
|
499
|
+
auth: {
|
|
500
|
+
type: 'backend',
|
|
501
|
+
path: 'apps/auth',
|
|
502
|
+
port: 3000, // Conflicts with api
|
|
503
|
+
dependencies: [],
|
|
504
|
+
},
|
|
505
|
+
web: {
|
|
506
|
+
type: 'frontend',
|
|
507
|
+
path: 'apps/web',
|
|
508
|
+
port: 3001,
|
|
509
|
+
dependencies: [],
|
|
510
|
+
},
|
|
511
|
+
admin: {
|
|
512
|
+
type: 'frontend',
|
|
513
|
+
path: 'apps/admin',
|
|
514
|
+
port: 3001, // Conflicts with web
|
|
515
|
+
dependencies: [],
|
|
516
|
+
},
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
const conflicts = checkPortConflicts(workspace);
|
|
520
|
+
expect(conflicts).toHaveLength(2);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it('should handle single app workspace', () => {
|
|
524
|
+
const workspace = createTestWorkspace({
|
|
525
|
+
api: {
|
|
526
|
+
type: 'backend',
|
|
527
|
+
path: 'apps/api',
|
|
528
|
+
port: 3000,
|
|
529
|
+
dependencies: [],
|
|
530
|
+
},
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
const conflicts = checkPortConflicts(workspace);
|
|
534
|
+
expect(conflicts).toEqual([]);
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
describe('generateAllDependencyEnvVars', () => {
|
|
539
|
+
it('should generate empty object for apps with no dependencies', () => {
|
|
540
|
+
const workspace = createTestWorkspace({
|
|
541
|
+
api: {
|
|
542
|
+
type: 'backend',
|
|
543
|
+
path: 'apps/api',
|
|
544
|
+
port: 3000,
|
|
545
|
+
dependencies: [],
|
|
546
|
+
},
|
|
547
|
+
web: {
|
|
548
|
+
type: 'frontend',
|
|
549
|
+
path: 'apps/web',
|
|
550
|
+
port: 3001,
|
|
551
|
+
dependencies: [],
|
|
552
|
+
},
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
const env = generateAllDependencyEnvVars(workspace);
|
|
556
|
+
expect(env).toEqual({});
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it('should generate URL env vars for dependencies', () => {
|
|
560
|
+
const workspace = createTestWorkspace({
|
|
561
|
+
api: {
|
|
562
|
+
type: 'backend',
|
|
563
|
+
path: 'apps/api',
|
|
564
|
+
port: 3000,
|
|
565
|
+
dependencies: [],
|
|
566
|
+
},
|
|
567
|
+
auth: {
|
|
568
|
+
type: 'backend',
|
|
569
|
+
path: 'apps/auth',
|
|
570
|
+
port: 3001,
|
|
571
|
+
dependencies: [],
|
|
572
|
+
},
|
|
573
|
+
web: {
|
|
574
|
+
type: 'frontend',
|
|
575
|
+
path: 'apps/web',
|
|
576
|
+
port: 3002,
|
|
577
|
+
dependencies: ['api', 'auth'],
|
|
578
|
+
},
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
const env = generateAllDependencyEnvVars(workspace);
|
|
582
|
+
expect(env).toEqual({
|
|
583
|
+
API_URL: 'http://localhost:3000',
|
|
584
|
+
AUTH_URL: 'http://localhost:3001',
|
|
585
|
+
});
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it('should handle cross-dependencies between backend apps', () => {
|
|
589
|
+
const workspace = createTestWorkspace({
|
|
590
|
+
'api-gateway': {
|
|
591
|
+
type: 'backend',
|
|
592
|
+
path: 'apps/api-gateway',
|
|
593
|
+
port: 3000,
|
|
594
|
+
dependencies: [],
|
|
595
|
+
},
|
|
596
|
+
'user-service': {
|
|
597
|
+
type: 'backend',
|
|
598
|
+
path: 'apps/user-service',
|
|
599
|
+
port: 3001,
|
|
600
|
+
dependencies: [],
|
|
601
|
+
},
|
|
602
|
+
'order-service': {
|
|
603
|
+
type: 'backend',
|
|
604
|
+
path: 'apps/order-service',
|
|
605
|
+
port: 3002,
|
|
606
|
+
dependencies: ['user-service'],
|
|
607
|
+
},
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
const env = generateAllDependencyEnvVars(workspace);
|
|
611
|
+
expect(env).toEqual({
|
|
612
|
+
'USER-SERVICE_URL': 'http://localhost:3001',
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
it('should use custom URL prefix', () => {
|
|
617
|
+
const workspace = createTestWorkspace({
|
|
618
|
+
api: {
|
|
619
|
+
type: 'backend',
|
|
620
|
+
path: 'apps/api',
|
|
621
|
+
port: 3000,
|
|
622
|
+
dependencies: [],
|
|
623
|
+
},
|
|
624
|
+
web: {
|
|
625
|
+
type: 'frontend',
|
|
626
|
+
path: 'apps/web',
|
|
627
|
+
port: 3001,
|
|
628
|
+
dependencies: ['api'],
|
|
629
|
+
},
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
const env = generateAllDependencyEnvVars(workspace, 'http://127.0.0.1');
|
|
633
|
+
expect(env).toEqual({
|
|
634
|
+
API_URL: 'http://127.0.0.1:3000',
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
it('should handle complex dependency graph', () => {
|
|
639
|
+
const workspace = createTestWorkspace({
|
|
640
|
+
api: {
|
|
641
|
+
type: 'backend',
|
|
642
|
+
path: 'apps/api',
|
|
643
|
+
port: 3000,
|
|
644
|
+
dependencies: [],
|
|
645
|
+
},
|
|
646
|
+
auth: {
|
|
647
|
+
type: 'backend',
|
|
648
|
+
path: 'apps/auth',
|
|
649
|
+
port: 3001,
|
|
650
|
+
dependencies: [],
|
|
651
|
+
},
|
|
652
|
+
payments: {
|
|
653
|
+
type: 'backend',
|
|
654
|
+
path: 'apps/payments',
|
|
655
|
+
port: 3002,
|
|
656
|
+
dependencies: ['auth'], // Payments depends on auth
|
|
657
|
+
},
|
|
658
|
+
web: {
|
|
659
|
+
type: 'frontend',
|
|
660
|
+
path: 'apps/web',
|
|
661
|
+
port: 3003,
|
|
662
|
+
dependencies: ['api', 'auth'], // Web depends on api and auth
|
|
663
|
+
},
|
|
664
|
+
admin: {
|
|
665
|
+
type: 'frontend',
|
|
666
|
+
path: 'apps/admin',
|
|
667
|
+
port: 3004,
|
|
668
|
+
dependencies: ['api', 'payments'], // Admin depends on api and payments
|
|
669
|
+
},
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
const env = generateAllDependencyEnvVars(workspace);
|
|
673
|
+
expect(env).toEqual({
|
|
674
|
+
AUTH_URL: 'http://localhost:3001',
|
|
675
|
+
API_URL: 'http://localhost:3000',
|
|
676
|
+
PAYMENTS_URL: 'http://localhost:3002',
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
describe('validateFrontendApp', () => {
|
|
682
|
+
let testDir: string;
|
|
683
|
+
|
|
684
|
+
beforeEach(() => {
|
|
685
|
+
// Create a unique temp directory for each test
|
|
686
|
+
testDir = join(
|
|
687
|
+
tmpdir(),
|
|
688
|
+
`gkm-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
689
|
+
);
|
|
690
|
+
mkdirSync(testDir, { recursive: true });
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
afterEach(() => {
|
|
694
|
+
// Clean up temp directory
|
|
695
|
+
if (existsSync(testDir)) {
|
|
696
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
it('should validate a properly configured Next.js app', async () => {
|
|
701
|
+
// Create a valid Next.js app structure
|
|
702
|
+
const appDir = join(testDir, 'apps/web');
|
|
703
|
+
mkdirSync(appDir, { recursive: true });
|
|
704
|
+
|
|
705
|
+
// Create next.config.js
|
|
706
|
+
writeFileSync(join(appDir, 'next.config.js'), 'module.exports = {}');
|
|
707
|
+
|
|
708
|
+
// Create package.json with next dependency
|
|
709
|
+
writeFileSync(
|
|
710
|
+
join(appDir, 'package.json'),
|
|
711
|
+
JSON.stringify({
|
|
712
|
+
name: 'web',
|
|
713
|
+
dependencies: { next: '^14.0.0', react: '^18.0.0' },
|
|
714
|
+
scripts: { dev: 'next dev' },
|
|
715
|
+
}),
|
|
716
|
+
);
|
|
717
|
+
|
|
718
|
+
const result = await validateFrontendApp('web', 'apps/web', testDir);
|
|
719
|
+
|
|
720
|
+
expect(result.valid).toBe(true);
|
|
721
|
+
expect(result.errors).toHaveLength(0);
|
|
722
|
+
expect(result.warnings).toHaveLength(0);
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
it('should detect missing Next.js config file', async () => {
|
|
726
|
+
const appDir = join(testDir, 'apps/web');
|
|
727
|
+
mkdirSync(appDir, { recursive: true });
|
|
728
|
+
|
|
729
|
+
// Create package.json without next.config.js
|
|
730
|
+
writeFileSync(
|
|
731
|
+
join(appDir, 'package.json'),
|
|
732
|
+
JSON.stringify({
|
|
733
|
+
name: 'web',
|
|
734
|
+
dependencies: { next: '^14.0.0' },
|
|
735
|
+
scripts: { dev: 'next dev' },
|
|
736
|
+
}),
|
|
737
|
+
);
|
|
738
|
+
|
|
739
|
+
const result = await validateFrontendApp('web', 'apps/web', testDir);
|
|
740
|
+
|
|
741
|
+
expect(result.valid).toBe(false);
|
|
742
|
+
expect(result.errors).toContainEqual(
|
|
743
|
+
expect.stringContaining('Next.js config file not found'),
|
|
744
|
+
);
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
it('should detect missing Next.js dependency', async () => {
|
|
748
|
+
const appDir = join(testDir, 'apps/web');
|
|
749
|
+
mkdirSync(appDir, { recursive: true });
|
|
750
|
+
|
|
751
|
+
// Create next.config.js
|
|
752
|
+
writeFileSync(join(appDir, 'next.config.js'), 'module.exports = {}');
|
|
753
|
+
|
|
754
|
+
// Create package.json WITHOUT next dependency
|
|
755
|
+
writeFileSync(
|
|
756
|
+
join(appDir, 'package.json'),
|
|
757
|
+
JSON.stringify({
|
|
758
|
+
name: 'web',
|
|
759
|
+
dependencies: { react: '^18.0.0' },
|
|
760
|
+
scripts: { dev: 'echo no next' },
|
|
761
|
+
}),
|
|
762
|
+
);
|
|
763
|
+
|
|
764
|
+
const result = await validateFrontendApp('web', 'apps/web', testDir);
|
|
765
|
+
|
|
766
|
+
expect(result.valid).toBe(false);
|
|
767
|
+
expect(result.errors).toContainEqual(
|
|
768
|
+
expect.stringContaining('Next.js not found in dependencies'),
|
|
769
|
+
);
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
it('should detect missing package.json', async () => {
|
|
773
|
+
const appDir = join(testDir, 'apps/web');
|
|
774
|
+
mkdirSync(appDir, { recursive: true });
|
|
775
|
+
|
|
776
|
+
// Create next.config.js but no package.json
|
|
777
|
+
writeFileSync(join(appDir, 'next.config.js'), 'module.exports = {}');
|
|
778
|
+
|
|
779
|
+
const result = await validateFrontendApp('web', 'apps/web', testDir);
|
|
780
|
+
|
|
781
|
+
expect(result.valid).toBe(false);
|
|
782
|
+
expect(result.errors).toContainEqual(
|
|
783
|
+
expect.stringContaining('package.json not found'),
|
|
784
|
+
);
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
it('should warn about missing dev script', async () => {
|
|
788
|
+
const appDir = join(testDir, 'apps/web');
|
|
789
|
+
mkdirSync(appDir, { recursive: true });
|
|
790
|
+
|
|
791
|
+
writeFileSync(join(appDir, 'next.config.js'), 'module.exports = {}');
|
|
792
|
+
|
|
793
|
+
// Create package.json WITHOUT dev script
|
|
794
|
+
writeFileSync(
|
|
795
|
+
join(appDir, 'package.json'),
|
|
796
|
+
JSON.stringify({
|
|
797
|
+
name: 'web',
|
|
798
|
+
dependencies: { next: '^14.0.0' },
|
|
799
|
+
// No scripts
|
|
800
|
+
}),
|
|
801
|
+
);
|
|
802
|
+
|
|
803
|
+
const result = await validateFrontendApp('web', 'apps/web', testDir);
|
|
804
|
+
|
|
805
|
+
expect(result.valid).toBe(true); // Still valid, just a warning
|
|
806
|
+
expect(result.warnings).toContainEqual(
|
|
807
|
+
expect.stringContaining('No "dev" script found'),
|
|
808
|
+
);
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
it('should accept next.config.ts', async () => {
|
|
812
|
+
const appDir = join(testDir, 'apps/web');
|
|
813
|
+
mkdirSync(appDir, { recursive: true });
|
|
814
|
+
|
|
815
|
+
// Create next.config.ts (TypeScript config)
|
|
816
|
+
writeFileSync(
|
|
817
|
+
join(appDir, 'next.config.ts'),
|
|
818
|
+
'export default { reactStrictMode: true }',
|
|
819
|
+
);
|
|
820
|
+
|
|
821
|
+
writeFileSync(
|
|
822
|
+
join(appDir, 'package.json'),
|
|
823
|
+
JSON.stringify({
|
|
824
|
+
name: 'web',
|
|
825
|
+
dependencies: { next: '^14.0.0' },
|
|
826
|
+
scripts: { dev: 'next dev' },
|
|
827
|
+
}),
|
|
828
|
+
);
|
|
829
|
+
|
|
830
|
+
const result = await validateFrontendApp('web', 'apps/web', testDir);
|
|
831
|
+
|
|
832
|
+
expect(result.valid).toBe(true);
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
it('should accept next.config.mjs', async () => {
|
|
836
|
+
const appDir = join(testDir, 'apps/web');
|
|
837
|
+
mkdirSync(appDir, { recursive: true });
|
|
838
|
+
|
|
839
|
+
// Create next.config.mjs (ESM config)
|
|
840
|
+
writeFileSync(
|
|
841
|
+
join(appDir, 'next.config.mjs'),
|
|
842
|
+
'export default { reactStrictMode: true }',
|
|
843
|
+
);
|
|
844
|
+
|
|
845
|
+
writeFileSync(
|
|
846
|
+
join(appDir, 'package.json'),
|
|
847
|
+
JSON.stringify({
|
|
848
|
+
name: 'web',
|
|
849
|
+
dependencies: { next: '^14.0.0' },
|
|
850
|
+
scripts: { dev: 'next dev' },
|
|
851
|
+
}),
|
|
852
|
+
);
|
|
853
|
+
|
|
854
|
+
const result = await validateFrontendApp('web', 'apps/web', testDir);
|
|
855
|
+
|
|
856
|
+
expect(result.valid).toBe(true);
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
it('should detect next in devDependencies', async () => {
|
|
860
|
+
const appDir = join(testDir, 'apps/web');
|
|
861
|
+
mkdirSync(appDir, { recursive: true });
|
|
862
|
+
|
|
863
|
+
writeFileSync(join(appDir, 'next.config.js'), 'module.exports = {}');
|
|
864
|
+
|
|
865
|
+
// Next in devDependencies (valid)
|
|
866
|
+
writeFileSync(
|
|
867
|
+
join(appDir, 'package.json'),
|
|
868
|
+
JSON.stringify({
|
|
869
|
+
name: 'web',
|
|
870
|
+
devDependencies: { next: '^14.0.0' },
|
|
871
|
+
scripts: { dev: 'next dev' },
|
|
872
|
+
}),
|
|
873
|
+
);
|
|
874
|
+
|
|
875
|
+
const result = await validateFrontendApp('web', 'apps/web', testDir);
|
|
876
|
+
|
|
877
|
+
expect(result.valid).toBe(true);
|
|
878
|
+
});
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
describe('validateFrontendApps', () => {
|
|
882
|
+
let testDir: string;
|
|
883
|
+
|
|
884
|
+
beforeEach(() => {
|
|
885
|
+
testDir = join(
|
|
886
|
+
tmpdir(),
|
|
887
|
+
`gkm-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
888
|
+
);
|
|
889
|
+
mkdirSync(testDir, { recursive: true });
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
afterEach(() => {
|
|
893
|
+
if (existsSync(testDir)) {
|
|
894
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
895
|
+
}
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
it('should validate only frontend apps', async () => {
|
|
899
|
+
// Create valid frontend app
|
|
900
|
+
const webDir = join(testDir, 'apps/web');
|
|
901
|
+
mkdirSync(webDir, { recursive: true });
|
|
902
|
+
writeFileSync(join(webDir, 'next.config.js'), 'module.exports = {}');
|
|
903
|
+
writeFileSync(
|
|
904
|
+
join(webDir, 'package.json'),
|
|
905
|
+
JSON.stringify({
|
|
906
|
+
name: 'web',
|
|
907
|
+
dependencies: { next: '^14.0.0' },
|
|
908
|
+
scripts: { dev: 'next dev' },
|
|
909
|
+
}),
|
|
910
|
+
);
|
|
911
|
+
|
|
912
|
+
const workspace: NormalizedWorkspace = {
|
|
913
|
+
name: 'test-workspace',
|
|
914
|
+
root: testDir,
|
|
915
|
+
apps: {
|
|
916
|
+
api: {
|
|
917
|
+
type: 'backend',
|
|
918
|
+
path: 'apps/api',
|
|
919
|
+
port: 3000,
|
|
920
|
+
dependencies: [],
|
|
921
|
+
resolvedDeployTarget: 'dokploy',
|
|
922
|
+
},
|
|
923
|
+
web: {
|
|
924
|
+
type: 'frontend',
|
|
925
|
+
path: 'apps/web',
|
|
926
|
+
port: 3001,
|
|
927
|
+
dependencies: ['api'],
|
|
928
|
+
resolvedDeployTarget: 'dokploy',
|
|
929
|
+
},
|
|
930
|
+
},
|
|
931
|
+
services: {},
|
|
932
|
+
deploy: { default: 'dokploy' },
|
|
933
|
+
shared: { packages: [] },
|
|
934
|
+
secrets: {},
|
|
935
|
+
};
|
|
936
|
+
|
|
937
|
+
const results = await validateFrontendApps(workspace);
|
|
938
|
+
|
|
939
|
+
// Should only validate the frontend app
|
|
940
|
+
expect(results).toHaveLength(1);
|
|
941
|
+
expect(results[0]?.appName).toBe('web');
|
|
942
|
+
expect(results[0]?.valid).toBe(true);
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
it('should return empty array for backend-only workspace', async () => {
|
|
946
|
+
const workspace: NormalizedWorkspace = {
|
|
947
|
+
name: 'test-workspace',
|
|
948
|
+
root: testDir,
|
|
949
|
+
apps: {
|
|
950
|
+
api: {
|
|
951
|
+
type: 'backend',
|
|
952
|
+
path: 'apps/api',
|
|
953
|
+
port: 3000,
|
|
954
|
+
dependencies: [],
|
|
955
|
+
resolvedDeployTarget: 'dokploy',
|
|
956
|
+
},
|
|
957
|
+
},
|
|
958
|
+
services: {},
|
|
959
|
+
deploy: { default: 'dokploy' },
|
|
960
|
+
shared: { packages: [] },
|
|
961
|
+
secrets: {},
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
const results = await validateFrontendApps(workspace);
|
|
965
|
+
|
|
966
|
+
expect(results).toHaveLength(0);
|
|
967
|
+
});
|
|
968
|
+
});
|
|
969
|
+
});
|