@geekmidas/cli 1.10.35 → 1.10.36
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 +10 -0
- package/dist/{config-Cuo8vFsp.cjs → config-QnuOcYXp.cjs} +2 -2
- package/dist/{config-Cuo8vFsp.cjs.map → config-QnuOcYXp.cjs.map} +1 -1
- package/dist/{config-B62g483e.mjs → config-U-gojtxn.mjs} +2 -2
- package/dist/{config-B62g483e.mjs.map → config-U-gojtxn.mjs.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-loader.cjs +1 -1
- package/dist/{index-Dt_dZ7K4.d.mts → index-D7iT4dnv.d.mts} +41 -4
- package/dist/index-D7iT4dnv.d.mts.map +1 -0
- package/dist/{index-C3t5VL4R.d.cts → index-DRQq26DF.d.cts} +41 -4
- package/dist/index-DRQq26DF.d.cts.map +1 -0
- package/dist/index.cjs +1094 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +1094 -30
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-CflxypuN.mjs → openapi-BA0e3I_s.mjs} +65 -15
- package/dist/openapi-BA0e3I_s.mjs.map +1 -0
- package/dist/{openapi-B_rJjImN.cjs → openapi-Bb4UEyZN.cjs} +65 -15
- package/dist/openapi-Bb4UEyZN.cjs.map +1 -0
- package/dist/openapi.cjs +3 -3
- package/dist/openapi.d.cts +7 -0
- package/dist/openapi.d.cts.map +1 -1
- package/dist/openapi.d.mts +7 -0
- package/dist/openapi.d.mts.map +1 -1
- package/dist/openapi.mjs +3 -3
- package/dist/workspace/index.cjs +4 -1
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +2 -2
- package/dist/workspace/index.mjs +2 -2
- package/dist/{workspace-CGYykWfn.cjs → workspace-BobDOIG9.cjs} +74 -3
- package/dist/workspace-BobDOIG9.cjs.map +1 -0
- package/dist/{workspace-Bi4X7Yzy.mjs → workspace-Cgmvgwh8.mjs} +57 -4
- package/dist/workspace-Cgmvgwh8.mjs.map +1 -0
- package/package.json +4 -4
- package/src/__tests__/openapi.spec.ts +99 -9
- package/src/deploy/__tests__/env-resolver.spec.ts +28 -0
- package/src/deploy/env-resolver.ts +5 -10
- package/src/deploy/index.ts +13 -7
- package/src/deploy/sniffer.ts +10 -4
- package/src/dev/index.ts +29 -1
- package/src/index.ts +6 -2
- package/src/init/__tests__/generators.spec.ts +147 -0
- package/src/init/generators/mobile-expo.ts +576 -0
- package/src/init/generators/monorepo.ts +42 -11
- package/src/init/generators/web-tanstack.ts +348 -0
- package/src/init/index.ts +40 -3
- package/src/init/templates/index.ts +31 -0
- package/src/init/versions.ts +2 -2
- package/src/openapi.ts +110 -23
- package/src/workspace/__tests__/index.spec.ts +92 -0
- package/src/workspace/__tests__/publicEnv.spec.ts +64 -0
- package/src/workspace/index.ts +16 -2
- package/src/workspace/publicEnv.ts +59 -0
- package/src/workspace/types.ts +6 -1
- package/dist/index-C3t5VL4R.d.cts.map +0 -1
- package/dist/index-Dt_dZ7K4.d.mts.map +0 -1
- package/dist/openapi-B_rJjImN.cjs.map +0 -1
- package/dist/openapi-CflxypuN.mjs.map +0 -1
- package/dist/workspace-Bi4X7Yzy.mjs.map +0 -1
- package/dist/workspace-CGYykWfn.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.36",
|
|
4
4
|
"description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -57,10 +57,10 @@
|
|
|
57
57
|
"tsx": "~4.20.3",
|
|
58
58
|
"yaml": "~2.8.2",
|
|
59
59
|
"@geekmidas/constructs": "~3.0.9",
|
|
60
|
-
"@geekmidas/envkit": "~1.0.5",
|
|
61
60
|
"@geekmidas/errors": "~1.0.0",
|
|
62
|
-
"@geekmidas/
|
|
63
|
-
"@geekmidas/
|
|
61
|
+
"@geekmidas/schema": "~1.0.0",
|
|
62
|
+
"@geekmidas/envkit": "~1.0.6",
|
|
63
|
+
"@geekmidas/logger": "~1.0.1"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@types/lodash.kebabcase": "^4.1.9",
|
|
@@ -2,12 +2,6 @@ import { existsSync, realpathSync } from 'node:fs';
|
|
|
2
2
|
import { mkdir, readFile, rm } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
-
import {
|
|
6
|
-
generateOpenApi,
|
|
7
|
-
OPENAPI_OUTPUT_PATH,
|
|
8
|
-
openapiCommand,
|
|
9
|
-
resolveOpenApiConfig,
|
|
10
|
-
} from '../openapi';
|
|
11
5
|
import type { GkmConfig } from '../types';
|
|
12
6
|
import {
|
|
13
7
|
cleanupDir,
|
|
@@ -16,6 +10,33 @@ import {
|
|
|
16
10
|
createTestFile,
|
|
17
11
|
} from './test-helpers';
|
|
18
12
|
|
|
13
|
+
const spawnCalls: Array<{ cmd: string; args: string[]; cwd: string }> = [];
|
|
14
|
+
let mockSpawnExitCode = 0;
|
|
15
|
+
|
|
16
|
+
vi.mock('node:child_process', async (importOriginal) => {
|
|
17
|
+
const actual = await importOriginal<typeof import('node:child_process')>();
|
|
18
|
+
return {
|
|
19
|
+
...actual,
|
|
20
|
+
spawn: vi.fn((cmd: string, args: string[], opts: any) => {
|
|
21
|
+
spawnCalls.push({ cmd, args: args ?? [], cwd: opts?.cwd ?? '' });
|
|
22
|
+
const child: any = {
|
|
23
|
+
on(event: string, cb: (code?: number) => void) {
|
|
24
|
+
if (event === 'close') queueMicrotask(() => cb(mockSpawnExitCode));
|
|
25
|
+
return child;
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
return child;
|
|
29
|
+
}),
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const {
|
|
34
|
+
generateOpenApi,
|
|
35
|
+
OPENAPI_OUTPUT_PATH,
|
|
36
|
+
openapiCommand,
|
|
37
|
+
resolveOpenApiConfig,
|
|
38
|
+
} = await import('../openapi');
|
|
39
|
+
|
|
19
40
|
describe('resolveOpenApiConfig', () => {
|
|
20
41
|
const baseConfig: GkmConfig = {
|
|
21
42
|
routes: './src/endpoints/**/*.ts',
|
|
@@ -483,7 +504,7 @@ describe('openapiCommand - workspace mode', () => {
|
|
|
483
504
|
vi.restoreAllMocks();
|
|
484
505
|
});
|
|
485
506
|
|
|
486
|
-
it('should generate OpenAPI for
|
|
507
|
+
it('should generate OpenAPI for a single app via --app flag', async () => {
|
|
487
508
|
// Create workspace structure
|
|
488
509
|
const apiDir = join(tempDir, 'apps/api');
|
|
489
510
|
await mkdir(apiDir, { recursive: true });
|
|
@@ -515,10 +536,11 @@ describe('openapiCommand - workspace mode', () => {
|
|
|
515
536
|
}),
|
|
516
537
|
);
|
|
517
538
|
|
|
518
|
-
|
|
539
|
+
// Subprocess invocation simulated: CWD = appPath, --app passed
|
|
540
|
+
process.chdir(apiDir);
|
|
519
541
|
const consoleSpy = vi.spyOn(console, 'log');
|
|
520
542
|
|
|
521
|
-
await openapiCommand({ cwd: tempDir });
|
|
543
|
+
await openapiCommand({ cwd: tempDir, app: 'api' });
|
|
522
544
|
|
|
523
545
|
// Should generate OpenAPI in the backend app's .gkm folder
|
|
524
546
|
const outputPath = join(apiDir, OPENAPI_OUTPUT_PATH);
|
|
@@ -532,4 +554,72 @@ describe('openapiCommand - workspace mode', () => {
|
|
|
532
554
|
expect.stringContaining('[api] Generated OpenAPI'),
|
|
533
555
|
);
|
|
534
556
|
});
|
|
557
|
+
|
|
558
|
+
it('should throw when --app references unknown app', async () => {
|
|
559
|
+
await createTestFile(
|
|
560
|
+
tempDir,
|
|
561
|
+
'gkm.config.json',
|
|
562
|
+
JSON.stringify({
|
|
563
|
+
name: 'test-workspace',
|
|
564
|
+
apps: {
|
|
565
|
+
api: {
|
|
566
|
+
type: 'backend',
|
|
567
|
+
path: 'apps/api',
|
|
568
|
+
port: 3000,
|
|
569
|
+
routes: './src/endpoints/**/*.ts',
|
|
570
|
+
openapi: { enabled: true },
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
}),
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
await expect(
|
|
577
|
+
openapiCommand({ cwd: tempDir, app: 'missing' }),
|
|
578
|
+
).rejects.toThrow(/App "missing" not found/);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it('should spawn one subprocess per backend app in multi-app mode', async () => {
|
|
582
|
+
// Create workspace with two backend apps
|
|
583
|
+
const apiDir = join(tempDir, 'apps/api');
|
|
584
|
+
const adminDir = join(tempDir, 'apps/admin');
|
|
585
|
+
await mkdir(apiDir, { recursive: true });
|
|
586
|
+
await mkdir(adminDir, { recursive: true });
|
|
587
|
+
|
|
588
|
+
await createTestFile(
|
|
589
|
+
tempDir,
|
|
590
|
+
'gkm.config.json',
|
|
591
|
+
JSON.stringify({
|
|
592
|
+
name: 'test-workspace',
|
|
593
|
+
apps: {
|
|
594
|
+
api: {
|
|
595
|
+
type: 'backend',
|
|
596
|
+
path: 'apps/api',
|
|
597
|
+
port: 3000,
|
|
598
|
+
routes: './src/endpoints/**/*.ts',
|
|
599
|
+
openapi: { enabled: true },
|
|
600
|
+
},
|
|
601
|
+
admin: {
|
|
602
|
+
type: 'backend',
|
|
603
|
+
path: 'apps/admin',
|
|
604
|
+
port: 3001,
|
|
605
|
+
routes: './src/endpoints/**/*.ts',
|
|
606
|
+
openapi: { enabled: true },
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
}),
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
spawnCalls.length = 0;
|
|
613
|
+
mockSpawnExitCode = 0;
|
|
614
|
+
|
|
615
|
+
await openapiCommand({ cwd: tempDir });
|
|
616
|
+
|
|
617
|
+
expect(spawnCalls).toHaveLength(2);
|
|
618
|
+
expect(spawnCalls[0]?.cwd).toBe(apiDir);
|
|
619
|
+
expect(spawnCalls[0]?.args).toContain('--app');
|
|
620
|
+
expect(spawnCalls[0]?.args).toContain('api');
|
|
621
|
+
expect(spawnCalls[1]?.cwd).toBe(adminDir);
|
|
622
|
+
expect(spawnCalls[1]?.args).toContain('--app');
|
|
623
|
+
expect(spawnCalls[1]?.args).toContain('admin');
|
|
624
|
+
});
|
|
535
625
|
});
|
|
@@ -517,6 +517,34 @@ describe('resolveEnvVar', () => {
|
|
|
517
517
|
expect(resolveEnvVar('NEXT_PUBLIC_AUTH_URL', context)).toBeUndefined();
|
|
518
518
|
});
|
|
519
519
|
});
|
|
520
|
+
|
|
521
|
+
describe('VITE_ prefix', () => {
|
|
522
|
+
it('should resolve VITE_API_URL from dependencyUrls.api', () => {
|
|
523
|
+
const context = createContext({
|
|
524
|
+
dependencyUrls: { api: 'https://api.example.com' },
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
expect(resolveEnvVar('VITE_API_URL', context)).toBe(
|
|
528
|
+
'https://api.example.com',
|
|
529
|
+
);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it('should resolve all of AUTH_URL, NEXT_PUBLIC_AUTH_URL, and VITE_AUTH_URL to the same value', () => {
|
|
533
|
+
const context = createContext({
|
|
534
|
+
dependencyUrls: { auth: 'https://auth.example.com' },
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
expect(resolveEnvVar('AUTH_URL', context)).toBe(
|
|
538
|
+
'https://auth.example.com',
|
|
539
|
+
);
|
|
540
|
+
expect(resolveEnvVar('NEXT_PUBLIC_AUTH_URL', context)).toBe(
|
|
541
|
+
'https://auth.example.com',
|
|
542
|
+
);
|
|
543
|
+
expect(resolveEnvVar('VITE_AUTH_URL', context)).toBe(
|
|
544
|
+
'https://auth.example.com',
|
|
545
|
+
);
|
|
546
|
+
});
|
|
547
|
+
});
|
|
520
548
|
});
|
|
521
549
|
});
|
|
522
550
|
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { randomBytes } from 'node:crypto';
|
|
10
10
|
import type { StageSecrets } from '../secrets/types';
|
|
11
|
+
import { stripPublicPrefix } from '../workspace/publicEnv';
|
|
11
12
|
import type { NormalizedAppConfig } from '../workspace/types';
|
|
12
13
|
import {
|
|
13
14
|
type AppDbCredentials,
|
|
@@ -208,17 +209,11 @@ export function resolveEnvVar(
|
|
|
208
209
|
}
|
|
209
210
|
|
|
210
211
|
// Check dependency URLs (e.g., AUTH_URL -> dependencyUrls.auth)
|
|
211
|
-
// Also supports
|
|
212
|
+
// Also supports any known public prefix (NEXT_PUBLIC_, VITE_, ...) so
|
|
213
|
+
// VITE_AUTH_URL and NEXT_PUBLIC_AUTH_URL both resolve to dependencyUrls.auth.
|
|
212
214
|
if (context.dependencyUrls && varName.endsWith('_URL')) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (varName.startsWith('NEXT_PUBLIC_')) {
|
|
216
|
-
// NEXT_PUBLIC_AUTH_URL -> auth
|
|
217
|
-
depName = varName.slice(12, -4).toLowerCase();
|
|
218
|
-
} else {
|
|
219
|
-
// AUTH_URL -> auth
|
|
220
|
-
depName = varName.slice(0, -4).toLowerCase();
|
|
221
|
-
}
|
|
215
|
+
const stripped = stripPublicPrefix(varName) ?? varName;
|
|
216
|
+
const depName = stripped.slice(0, -4).toLowerCase();
|
|
222
217
|
|
|
223
218
|
if (context.dependencyUrls[depName]) {
|
|
224
219
|
return context.dependencyUrls[depName];
|
package/src/deploy/index.ts
CHANGED
|
@@ -61,6 +61,7 @@ import { readStageSecrets } from '../secrets/storage.js';
|
|
|
61
61
|
import {
|
|
62
62
|
getAppBuildOrder,
|
|
63
63
|
getDeployTargetError,
|
|
64
|
+
getPublicEnvPrefix,
|
|
64
65
|
isDeployTargetSupported,
|
|
65
66
|
} from '../workspace/index.js';
|
|
66
67
|
import type { NormalizedWorkspace } from '../workspace/types.js';
|
|
@@ -1661,7 +1662,8 @@ export async function workspaceDeployCommand(
|
|
|
1661
1662
|
dependencyUrls,
|
|
1662
1663
|
};
|
|
1663
1664
|
|
|
1664
|
-
// Resolve all env vars BEFORE Docker build (
|
|
1665
|
+
// Resolve all env vars BEFORE Docker build (public-prefixed vars
|
|
1666
|
+
// must be present at bundler build time so they get inlined).
|
|
1665
1667
|
const sniffedVars = sniffedApps.get(appName)?.requiredEnvVars ?? [];
|
|
1666
1668
|
const { valid, missing, resolved } = validateEnvVars(
|
|
1667
1669
|
sniffedVars,
|
|
@@ -1678,14 +1680,18 @@ export async function workspaceDeployCommand(
|
|
|
1678
1680
|
);
|
|
1679
1681
|
}
|
|
1680
1682
|
|
|
1681
|
-
// Build args:
|
|
1683
|
+
// Build args: only the framework's public-prefixed vars get baked
|
|
1684
|
+
// into the bundle. Server-only vars stay as runtime env.
|
|
1685
|
+
const publicPrefix = getPublicEnvPrefix(app.framework);
|
|
1682
1686
|
const buildArgs: string[] = [];
|
|
1683
1687
|
const publicUrlArgNames: string[] = [];
|
|
1684
1688
|
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
+
if (publicPrefix) {
|
|
1690
|
+
for (const [key, value] of Object.entries(resolved)) {
|
|
1691
|
+
if (key.startsWith(publicPrefix)) {
|
|
1692
|
+
buildArgs.push(`${key}=${value}`);
|
|
1693
|
+
publicUrlArgNames.push(key);
|
|
1694
|
+
}
|
|
1689
1695
|
}
|
|
1690
1696
|
}
|
|
1691
1697
|
|
|
@@ -1693,7 +1699,7 @@ export async function workspaceDeployCommand(
|
|
|
1693
1699
|
logger.log(` Build args: ${publicUrlArgNames.join(', ')}`);
|
|
1694
1700
|
}
|
|
1695
1701
|
|
|
1696
|
-
// Build Docker image with
|
|
1702
|
+
// Build Docker image with public-prefixed vars as build args
|
|
1697
1703
|
const imageName = `${workspace.name}-${appName}`;
|
|
1698
1704
|
const imageRef = registry
|
|
1699
1705
|
? `${registry}/${imageName}:${imageTag}`
|
package/src/deploy/sniffer.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { dirname, resolve } from 'node:path';
|
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
6
|
import type { SniffResult } from '@geekmidas/envkit/sniffer';
|
|
7
7
|
import { normalizeRoutes } from '../workspace/client-generator.js';
|
|
8
|
+
import { getPublicEnvPrefix } from '../workspace/publicEnv.js';
|
|
8
9
|
import type { NormalizedAppConfig } from '../workspace/types.js';
|
|
9
10
|
|
|
10
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -117,10 +118,15 @@ export async function sniffAppEnvironment(
|
|
|
117
118
|
|
|
118
119
|
// 1. Frontend apps - handle dependencies and config sniffing
|
|
119
120
|
if (app.type === 'frontend') {
|
|
120
|
-
// Auto-generate
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
);
|
|
121
|
+
// Auto-generate {prefix}{DEP}_URL from dependencies, where prefix matches
|
|
122
|
+
// the framework's public-var convention (NEXT_PUBLIC_, VITE_, ...).
|
|
123
|
+
// For frameworks without a prefix (e.g. Remix), no dep var is required.
|
|
124
|
+
const publicPrefix = getPublicEnvPrefix(app.framework);
|
|
125
|
+
const depVars = publicPrefix
|
|
126
|
+
? (app.dependencies ?? []).map(
|
|
127
|
+
(dep) => `${publicPrefix}${dep.toUpperCase()}_URL`,
|
|
128
|
+
)
|
|
129
|
+
: [];
|
|
124
130
|
|
|
125
131
|
// If config specified, sniff by importing the file(s)
|
|
126
132
|
// The file calls .parse() at module load, which triggers sniffer to capture vars
|
package/src/dev/index.ts
CHANGED
|
@@ -688,6 +688,23 @@ const FRONTEND_FRAMEWORKS: Record<FrontendFramework, FrontendFrameworkSpec> = {
|
|
|
688
688
|
dependency: 'vite',
|
|
689
689
|
installCommand: 'pnpm add -D vite',
|
|
690
690
|
},
|
|
691
|
+
'tanstack-start': {
|
|
692
|
+
displayName: 'TanStack Start',
|
|
693
|
+
configFiles: [
|
|
694
|
+
'vite.config.js',
|
|
695
|
+
'vite.config.ts',
|
|
696
|
+
'vite.config.mjs',
|
|
697
|
+
'vite.config.cjs',
|
|
698
|
+
],
|
|
699
|
+
dependency: '@tanstack/react-start',
|
|
700
|
+
installCommand: 'pnpm add @tanstack/react-start @tanstack/react-router',
|
|
701
|
+
},
|
|
702
|
+
expo: {
|
|
703
|
+
displayName: 'Expo',
|
|
704
|
+
configFiles: ['app.config.ts', 'app.config.js', 'app.json'],
|
|
705
|
+
dependency: 'expo',
|
|
706
|
+
installCommand: 'pnpm add expo',
|
|
707
|
+
},
|
|
691
708
|
};
|
|
692
709
|
|
|
693
710
|
/**
|
|
@@ -699,7 +716,18 @@ function detectFrontendFramework(
|
|
|
699
716
|
fullPath: string,
|
|
700
717
|
deps: Record<string, string>,
|
|
701
718
|
): FrontendFramework | undefined {
|
|
702
|
-
|
|
719
|
+
// tanstack-start before vite/remix so its more specific dep wins; vite last
|
|
720
|
+
// because plain vite is the fallback when neither nextjs/remix/tanstack match.
|
|
721
|
+
// Order matters: more specific deps win over general ones.
|
|
722
|
+
// expo first (own dep), then tanstack-start (uses vite), remix (uses vite),
|
|
723
|
+
// nextjs (own dep), and plain vite last as the fallback.
|
|
724
|
+
const order: FrontendFramework[] = [
|
|
725
|
+
'expo',
|
|
726
|
+
'tanstack-start',
|
|
727
|
+
'nextjs',
|
|
728
|
+
'remix',
|
|
729
|
+
'vite',
|
|
730
|
+
];
|
|
703
731
|
for (const name of order) {
|
|
704
732
|
if (deps[FRONTEND_FRAMEWORKS[name].dependency]) {
|
|
705
733
|
return name;
|
package/src/index.ts
CHANGED
|
@@ -286,13 +286,17 @@ program
|
|
|
286
286
|
program
|
|
287
287
|
.command('openapi')
|
|
288
288
|
.description('Generate OpenAPI specification from endpoints')
|
|
289
|
-
.
|
|
289
|
+
.option(
|
|
290
|
+
'--app <name>',
|
|
291
|
+
'Workspace mode: generate for a single named backend app',
|
|
292
|
+
)
|
|
293
|
+
.action(async (options: { app?: string }) => {
|
|
290
294
|
try {
|
|
291
295
|
const globalOptions = program.opts();
|
|
292
296
|
if (globalOptions.cwd) {
|
|
293
297
|
process.chdir(globalOptions.cwd);
|
|
294
298
|
}
|
|
295
|
-
await openapiCommand({});
|
|
299
|
+
await openapiCommand({ app: options.app });
|
|
296
300
|
} catch (error) {
|
|
297
301
|
console.error(formatError(error));
|
|
298
302
|
process.exit(1);
|
|
@@ -2,11 +2,13 @@ import { describe, expect, it } from 'vitest';
|
|
|
2
2
|
import { generateConfigFiles } from '../generators/config.js';
|
|
3
3
|
import { generateDockerFiles } from '../generators/docker.js';
|
|
4
4
|
import { generateEnvFiles } from '../generators/env.js';
|
|
5
|
+
import { generateExpoAppFiles } from '../generators/mobile-expo.js';
|
|
5
6
|
import { generateModelsPackage } from '../generators/models.js';
|
|
6
7
|
import { generateMonorepoFiles } from '../generators/monorepo.js';
|
|
7
8
|
import { generatePackageJson } from '../generators/package.js';
|
|
8
9
|
import { generateTestFiles } from '../generators/test.js';
|
|
9
10
|
import { generateUiPackageFiles } from '../generators/ui.js';
|
|
11
|
+
import { generateTanStackWebFiles } from '../generators/web-tanstack.js';
|
|
10
12
|
import { apiTemplate } from '../templates/api.js';
|
|
11
13
|
import type { TemplateOptions } from '../templates/index.js';
|
|
12
14
|
import { minimalTemplate } from '../templates/minimal.js';
|
|
@@ -431,6 +433,151 @@ describe('generateMonorepoFiles', () => {
|
|
|
431
433
|
});
|
|
432
434
|
});
|
|
433
435
|
|
|
436
|
+
describe('generateTanStackWebFiles', () => {
|
|
437
|
+
const fullstackOptions: TemplateOptions = {
|
|
438
|
+
...baseOptions,
|
|
439
|
+
template: 'fullstack',
|
|
440
|
+
monorepo: true,
|
|
441
|
+
apiPath: 'apps/api',
|
|
442
|
+
frontendFramework: 'tanstack-start',
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
it('returns empty array for non-fullstack template', () => {
|
|
446
|
+
expect(generateTanStackWebFiles(baseOptions)).toHaveLength(0);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('generates the expected file paths', () => {
|
|
450
|
+
const files = generateTanStackWebFiles(fullstackOptions);
|
|
451
|
+
const paths = files.map((f) => f.path);
|
|
452
|
+
expect(paths).toContain('apps/web/package.json');
|
|
453
|
+
expect(paths).toContain('apps/web/vite.config.ts');
|
|
454
|
+
expect(paths).toContain('apps/web/src/router.tsx');
|
|
455
|
+
expect(paths).toContain('apps/web/src/routes/__root.tsx');
|
|
456
|
+
expect(paths).toContain('apps/web/src/routes/index.tsx');
|
|
457
|
+
expect(paths).toContain('apps/web/src/config/client.ts');
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('uses VITE_ prefix in client config, not NEXT_PUBLIC_', () => {
|
|
461
|
+
const files = generateTanStackWebFiles(fullstackOptions);
|
|
462
|
+
const clientConfig = files.find(
|
|
463
|
+
(f) => f.path === 'apps/web/src/config/client.ts',
|
|
464
|
+
);
|
|
465
|
+
expect(clientConfig).toBeDefined();
|
|
466
|
+
expect(clientConfig!.content).toContain('VITE_API_URL');
|
|
467
|
+
expect(clientConfig!.content).toContain('VITE_AUTH_URL');
|
|
468
|
+
expect(clientConfig!.content).not.toContain('NEXT_PUBLIC_');
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it('declares tanstack and vite as dependencies', () => {
|
|
472
|
+
const files = generateTanStackWebFiles(fullstackOptions);
|
|
473
|
+
const pkg = JSON.parse(
|
|
474
|
+
files.find((f) => f.path === 'apps/web/package.json')!.content,
|
|
475
|
+
);
|
|
476
|
+
expect(pkg.dependencies['@tanstack/react-start']).toBeDefined();
|
|
477
|
+
expect(pkg.dependencies['@tanstack/react-router']).toBeDefined();
|
|
478
|
+
expect(pkg.devDependencies.vite).toBeDefined();
|
|
479
|
+
expect(pkg.dependencies.next).toBeUndefined();
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
describe('generateExpoAppFiles', () => {
|
|
484
|
+
const fullstackOptions: TemplateOptions = {
|
|
485
|
+
...baseOptions,
|
|
486
|
+
template: 'fullstack',
|
|
487
|
+
monorepo: true,
|
|
488
|
+
apiPath: 'apps/api',
|
|
489
|
+
frontendFramework: 'expo',
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
it('returns empty array for non-fullstack template', () => {
|
|
493
|
+
expect(generateExpoAppFiles(baseOptions)).toHaveLength(0);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it('generates the expected Expo file paths', () => {
|
|
497
|
+
const files = generateExpoAppFiles(fullstackOptions);
|
|
498
|
+
const paths = files.map((f) => f.path);
|
|
499
|
+
expect(paths).toContain('apps/app/package.json');
|
|
500
|
+
expect(paths).toContain('apps/app/app.config.ts');
|
|
501
|
+
expect(paths).toContain('apps/app/eas.json');
|
|
502
|
+
expect(paths).toContain('apps/app/babel.config.js');
|
|
503
|
+
expect(paths).toContain('apps/app/metro.config.js');
|
|
504
|
+
expect(paths).toContain('apps/app/app/_layout.tsx');
|
|
505
|
+
expect(paths).toContain('apps/app/app/index.tsx');
|
|
506
|
+
expect(paths).toContain('apps/app/app/login.tsx');
|
|
507
|
+
expect(paths).toContain('apps/app/lib/auth-client.ts');
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('uses EXPO_PUBLIC_ prefix in config.ts', () => {
|
|
511
|
+
const files = generateExpoAppFiles(fullstackOptions);
|
|
512
|
+
const configTs = files.find((f) => f.path === 'apps/app/config.ts');
|
|
513
|
+
expect(configTs).toBeDefined();
|
|
514
|
+
expect(configTs!.content).toContain('EXPO_PUBLIC_API_URL');
|
|
515
|
+
expect(configTs!.content).toContain('EXPO_PUBLIC_AUTH_URL');
|
|
516
|
+
expect(configTs!.content).not.toContain('VITE_');
|
|
517
|
+
expect(configTs!.content).not.toContain('NEXT_PUBLIC_');
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it('seeds eas.json with EXPO_PUBLIC_ env vars per profile', () => {
|
|
521
|
+
const files = generateExpoAppFiles(fullstackOptions);
|
|
522
|
+
const eas = JSON.parse(
|
|
523
|
+
files.find((f) => f.path === 'apps/app/eas.json')!.content,
|
|
524
|
+
);
|
|
525
|
+
expect(eas.build.dev.env.EXPO_PUBLIC_API_URL).toBeDefined();
|
|
526
|
+
expect(eas.build.dev.env.EXPO_PUBLIC_AUTH_URL).toBeDefined();
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it('declares expo + better-auth/expo as dependencies', () => {
|
|
530
|
+
const files = generateExpoAppFiles(fullstackOptions);
|
|
531
|
+
const pkg = JSON.parse(
|
|
532
|
+
files.find((f) => f.path === 'apps/app/package.json')!.content,
|
|
533
|
+
);
|
|
534
|
+
expect(pkg.dependencies.expo).toBeDefined();
|
|
535
|
+
expect(pkg.dependencies['@better-auth/expo']).toBeDefined();
|
|
536
|
+
expect(pkg.dependencies['expo-router']).toBeDefined();
|
|
537
|
+
expect(pkg.dependencies.next).toBeUndefined();
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
describe('generateMonorepoFiles - frontendFramework wiring', () => {
|
|
542
|
+
const fullstackBase: TemplateOptions = {
|
|
543
|
+
...baseOptions,
|
|
544
|
+
template: 'fullstack',
|
|
545
|
+
monorepo: true,
|
|
546
|
+
apiPath: 'apps/api',
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
it('emits framework: nextjs by default', () => {
|
|
550
|
+
const files = generateMonorepoFiles(
|
|
551
|
+
{ ...fullstackBase, frontendFramework: 'nextjs' },
|
|
552
|
+
minimalTemplate,
|
|
553
|
+
);
|
|
554
|
+
const cfg = files.find((f) => f.path === 'gkm.config.ts');
|
|
555
|
+
expect(cfg!.content).toContain("framework: 'nextjs'");
|
|
556
|
+
expect(cfg!.content).toContain("path: 'apps/web'");
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it('emits framework: tanstack-start when selected', () => {
|
|
560
|
+
const files = generateMonorepoFiles(
|
|
561
|
+
{ ...fullstackBase, frontendFramework: 'tanstack-start' },
|
|
562
|
+
minimalTemplate,
|
|
563
|
+
);
|
|
564
|
+
const cfg = files.find((f) => f.path === 'gkm.config.ts');
|
|
565
|
+
expect(cfg!.content).toContain("framework: 'tanstack-start'");
|
|
566
|
+
expect(cfg!.content).toContain("path: 'apps/web'");
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it('emits framework: expo and apps/app path when selected', () => {
|
|
570
|
+
const files = generateMonorepoFiles(
|
|
571
|
+
{ ...fullstackBase, frontendFramework: 'expo' },
|
|
572
|
+
minimalTemplate,
|
|
573
|
+
);
|
|
574
|
+
const cfg = files.find((f) => f.path === 'gkm.config.ts');
|
|
575
|
+
expect(cfg!.content).toContain("framework: 'expo'");
|
|
576
|
+
expect(cfg!.content).toContain("path: 'apps/app'");
|
|
577
|
+
expect(cfg!.content).toContain('port: 8081');
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
|
|
434
581
|
describe('generateModelsPackage', () => {
|
|
435
582
|
it('should return empty array for non-monorepo', () => {
|
|
436
583
|
const files = generateModelsPackage(baseOptions);
|